From 09c4a375a7518a42f299069c4f496b9f25b84077 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 5 Sep 2021 22:31:34 +1000 Subject: [PATCH] Better error messages when insufficient candidates to fill vacancies --- src/stv/mod.rs | 25 +++++++-- tests/data/insufficient_candidates1.blt | 14 +++++ tests/data/insufficient_candidates2.blt | 17 ++++++ tests/units.rs | 74 +++++++++++++++++++++++++ tests/utils/mod.rs | 4 +- 5 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 tests/data/insufficient_candidates1.blt create mode 100644 tests/data/insufficient_candidates2.blt create mode 100644 tests/units.rs diff --git a/src/stv/mod.rs b/src/stv/mod.rs index ab86db0..d6651ae 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -553,12 +553,14 @@ impl> From for ConstraintMode { } /// An error during the STV count -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub enum STVError { /// Options for the count are invalid InvalidOptions(&'static str), /// Tie could not be resolved UnresolvedTie, + /// Unrecoverable error during the count + CannotCompleteCount(&'static str), } impl STVError { @@ -567,6 +569,7 @@ impl STVError { match self { STVError::InvalidOptions(s) => s, STVError::UnresolvedTie => "Unable to resolve tie", + STVError::CannotCompleteCount(s) => s, } } } @@ -667,6 +670,14 @@ where return Ok(false); } + // Sanity check + let num_hopefuls = state.candidates.values() + .filter(|cc| cc.state == CandidateState::Hopeful) + .count(); + if num_hopefuls == 0 { + return Err(STVError::CannotCompleteCount("Insufficient continuing candidates to complete count")); + } + // Exclude lowest hopeful exclude_hopefuls(state, &opts)?; // Cannot fail calculate_quota(state, opts); @@ -960,9 +971,11 @@ fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOp } }); - let last_winner = hopefuls[num_vacancies - 1].1; - if last_winner.votes <= total_trailing { - return Ok(false); + if num_vacancies - 1 < hopefuls.len() { + let last_winner = hopefuls[num_vacancies - 1].1; + if last_winner.votes <= total_trailing { + return Ok(false); + } } let mut leading_hopefuls: Vec<&Candidate> = hopefuls.iter().take(num_vacancies).map(|(c, _)| *c).collect(); @@ -973,7 +986,7 @@ fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOp } // Bulk elect all leading candidates - while state.num_elected < state.election.seats { + while !leading_hopefuls.is_empty() && state.num_elected < state.election.seats { let max_cands = ties::multiple_max_by(&leading_hopefuls, |c| &state.candidates[c].votes); let candidate = if max_cands.len() > 1 { choose_highest(state, opts, max_cands, "Which candidate to elect?")? @@ -1151,7 +1164,7 @@ fn can_bulk_elect(state: &CountState, num_to_exclude: usize) -> bo }) .count(); - if state.num_elected + num_hopefuls - num_to_exclude <= state.election.seats { + if num_hopefuls > 0 && state.num_elected + num_hopefuls - num_to_exclude <= state.election.seats { return true; } return false; diff --git a/tests/data/insufficient_candidates1.blt b/tests/data/insufficient_candidates1.blt new file mode 100644 index 0000000..ba3158d --- /dev/null +++ b/tests/data/insufficient_candidates1.blt @@ -0,0 +1,14 @@ +# Comment: Constructed example - insufficient candidates to fill vacancies +5 7 +1 1 0 +1 2 0 +1 3 0 +1 4 0 +1 5 0 +0 +"Candidate 1" +"Candidate 2" +"Candidate 3" +"Candidate 4" +"Candidate 5" +"Insufficient candidates example 1" diff --git a/tests/data/insufficient_candidates2.blt b/tests/data/insufficient_candidates2.blt new file mode 100644 index 0000000..baf75df --- /dev/null +++ b/tests/data/insufficient_candidates2.blt @@ -0,0 +1,17 @@ +# Comment: Constructed example - insufficient candidates after the exclusion of those with no votes +8 7 +1 1 0 +1 2 0 +1 3 0 +1 4 0 +1 5 0 +0 +"Candidate 1" +"Candidate 2" +"Candidate 3" +"Candidate 4" +"Candidate 5" +"Candidate 6" +"Candidate 7" +"Candidate 8" +"Insufficient candidates example 2" diff --git a/tests/units.rs b/tests/units.rs new file mode 100644 index 0000000..7a6ff78 --- /dev/null +++ b/tests/units.rs @@ -0,0 +1,74 @@ +/* OpenTally: Open-source election vote counting + * Copyright © 2021 Lee Yingtong Li (RunasSudo) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +mod utils; + +use opentally::election::{CountState, Election}; +use opentally::numbers::Rational; +use opentally::parser::blt; +use opentally::stv; +use opentally::ties::TieStrategy; + +#[test] +fn insufficient_candidates1() { + let stv_opts = stv::STVOptionsBuilder::default() + .ties(vec![TieStrategy::Random(String::new())]) + .build().unwrap(); + + let mut election: Election = blt::parse_path("tests/data/insufficient_candidates1.blt").expect("Syntax Error"); + stv::preprocess_election(&mut election, &stv_opts); + + let mut state = CountState::new(&election); + + stv::count_init(&mut state, &stv_opts).unwrap(); + + loop { + let result = stv::count_one_stage::(&mut state, &stv_opts); + match result { + Ok(done) => { assert_eq!(done, false); } + Err(err) => { + assert_eq!(err, stv::STVError::CannotCompleteCount("Insufficient continuing candidates to complete count")); + break; + } + } + } +} + +#[test] +fn insufficient_candidates2() { + let stv_opts = stv::STVOptionsBuilder::default() + .ties(vec![TieStrategy::Random(String::new())]) + .build().unwrap(); + + let mut election: Election = blt::parse_path("tests/data/insufficient_candidates2.blt").expect("Syntax Error"); + stv::preprocess_election(&mut election, &stv_opts); + + let mut state = CountState::new(&election); + + stv::count_init(&mut state, &stv_opts).unwrap(); + + loop { + let result = stv::count_one_stage::(&mut state, &stv_opts); + match result { + Ok(done) => { assert_eq!(done, false); } + Err(err) => { + assert_eq!(err, stv::STVError::CannotCompleteCount("Insufficient continuing candidates to complete count")); + break; + } + } + } +} diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 0214d96..a5d8f99 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -65,9 +65,7 @@ where for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Neg, { - if stv_opts.normalise_ballots { - election.normalise_ballots(); - } + stv::preprocess_election(&mut election, &stv_opts); // Initialise count state let mut state = CountState::new(&election);