1use crate::types::{Ballot, Candidate, Election};
18use log::{info, trace, warn};
19use regex::Regex;
20use std::collections::{HashMap, HashSet};
21use std::io::BufRead;
22
23pub struct ParsingOptions {
25 pub remove_withdrawn_candidates: bool,
27 pub remove_empty_ballots: bool,
31 pub optimize_layout: bool,
34}
35
36pub fn parse_election(
39 input: impl BufRead,
40 options: ParsingOptions,
41) -> Result<Election, Box<dyn std::error::Error>> {
42 let re_count = Regex::new(r"^([0-9]+) ([0-9]+)$").unwrap();
43 let re_option = Regex::new(r"^\[[a-z]+(?: [a-z][a-z0-9]*)+\]$").unwrap();
44 let re_ballot = Regex::new(r"^([0-9]+)((?: [a-z0-9=]*)*) 0$").unwrap();
45
46 info!(
47 "Optimizing the in-memory layout of ballots is {}",
48 if options.optimize_layout {
49 "enabled"
50 } else {
51 "disabled"
52 }
53 );
54
55 let mut lines = input.lines().peekable();
56
57 let header = lines.next().unwrap().unwrap();
58 let cap_count = re_count.captures(&header).unwrap();
59 let num_candidates = cap_count.get(1).unwrap().as_str().parse::<usize>().unwrap();
60 let num_seats = cap_count.get(2).unwrap().as_str().parse::<usize>().unwrap();
61
62 info!("{num_seats} seats / {num_candidates} candidates");
63
64 let mut nicknames = None;
66 let mut withdrawn: HashSet<String> = HashSet::new();
67 let mut tie = None;
68 while let Some(line) = lines.peek() {
69 let line = line.as_ref().unwrap();
70 if !re_option.is_match(line) {
71 break;
72 }
73
74 let mut items = line[1..line.len() - 1].split(' ');
75 let title = items.next().unwrap();
76
77 match title {
78 "nick" => {
79 let values = items.map(|x| x.to_owned()).collect::<Vec<String>>();
80 info!("Nicknames: {values:?}");
81 nicknames = Some(values);
82 }
83 "withdrawn" => {
84 let values = items.map(|x| x.to_owned()).collect::<Vec<String>>();
85 info!("Withdrawn: {values:?}");
86 withdrawn = values.into_iter().collect::<HashSet<String>>();
87 }
88 "tie" => {
89 let values = items.map(|x| x.to_owned()).collect::<Vec<String>>();
90 info!("Tie-break order: {values:?}");
91 tie = Some(values);
92 }
93 _ => warn!("Unknown option: {title}"),
94 }
95
96 lines.next();
97 }
98
99 let nicknames: Vec<String> = nicknames.unwrap();
100 info!("Candidates (by nickname): {nicknames:?}");
101 assert_eq!(nicknames.len(), num_candidates);
102
103 let hash_nicknames: HashMap<&str, usize> = nicknames
104 .iter()
105 .enumerate()
106 .map(|(i, c)| (c.as_str(), i))
107 .collect();
108
109 let tie_order: HashMap<usize, usize> = {
110 match tie {
111 None => (0..num_candidates).map(|i| (i, i)).collect(),
112 Some(tie) => {
113 assert_eq!(
114 tie.len(),
115 num_candidates,
116 "Tie-break order must mention all candidates"
117 );
118 let mut tie_order = HashMap::new();
119 for (i, c) in tie.iter().enumerate() {
120 let id = *hash_nicknames.get(c.as_str()).unwrap();
121 assert!(
122 tie_order.insert(id, i).is_none(),
123 "Candidate mentioned twice in tie order: {c}",
124 );
125 }
126 tie_order
127 }
128 }
129 };
130
131 let mut ballots = Vec::new();
132 loop {
133 let line = lines.next().unwrap().unwrap();
134 if line == "0" {
135 break;
136 }
137 match re_ballot.captures(&line) {
138 Some(cap_ballots) => {
139 let count = cap_ballots
140 .get(1)
141 .unwrap()
142 .as_str()
143 .parse::<usize>()
144 .unwrap();
145 let order_str = cap_ballots.get(2).unwrap().as_str();
146 let order = order_str.split(' ').filter_map(|level| {
147 if level.is_empty() {
148 None
149 } else {
150 let mut level_candidates = level
151 .split('=')
152 .filter_map(|candidate| {
153 if options.remove_withdrawn_candidates
154 && withdrawn.contains(candidate)
155 {
156 None
157 } else {
158 Some(*hash_nicknames.get(candidate).unwrap())
159 }
160 })
161 .peekable();
162 if level_candidates.peek().is_none() {
163 None
164 } else {
165 Some(level_candidates)
166 }
167 }
168 });
169
170 let ballot = Ballot::new(count, order);
171 trace!(
172 "Parsed ballot: count {count} for {:?}",
173 ballot
174 .order()
175 .map(|rank| rank.iter().map(|&x| x.into()).collect::<Vec<_>>())
176 .collect::<Vec<_>>()
177 );
178 if options.remove_empty_ballots && ballot.is_empty() {
179 warn!("Removing ballot that is empty or contains only withdrawn candidates: {line}");
180 } else {
181 ballot.validate();
182 ballots.push(ballot);
183 }
184 }
185 None => {
186 warn!("Ignored line: {line:?}");
187 }
188 }
189 }
190
191 let num_ballots = ballots.iter().map(|b| b.count()).sum::<usize>();
192 info!("Number of ballots: {num_ballots}");
193
194 #[allow(clippy::assigning_clones)]
197 if options.optimize_layout {
198 ballots.sort_by(|a, b| {
199 let ita = a.order().map(|rank| rank.len());
200 let itb = b.order().map(|rank| rank.len());
201 ita.cmp(itb)
202 });
203 ballots = ballots.clone();
204 }
205
206 let candidates: Vec<Candidate> = nicknames
207 .into_iter()
208 .map(|nickname| {
209 let is_withdrawn = withdrawn.contains(&nickname);
210 Candidate {
211 name: remove_quotes(&lines.next().unwrap().unwrap()).to_string(),
212 nickname,
213 is_withdrawn,
214 }
215 })
216 .collect();
217
218 let title = remove_quotes(&lines.next().unwrap().unwrap()).to_string();
219 info!("Election title: {title}");
220
221 let election = Election {
222 title,
223 num_candidates,
224 num_seats,
225 num_ballots,
226 candidates,
227 ballots,
228 tie_order,
229 };
230 election.debug_allocations();
231 Ok(election)
232}
233
234fn remove_quotes(x: &str) -> &str {
238 assert!(x.len() >= 2);
240 assert_eq!(*x.as_bytes().first().unwrap(), b'"');
241 assert_eq!(*x.as_bytes().last().unwrap(), b'"');
242 &x[1..x.len() - 1]
243}
244
245#[cfg(test)]
246mod test {
247 use super::*;
248 use crate::util::log_tester::ThreadLocalLogger;
249 use log::Level::{Debug, Info, Warn};
250 use log::LevelFilter;
251 use std::io::Cursor;
252
253 #[test]
254 fn test_remove_quotes() {
255 assert_eq!(remove_quotes("\"foo\""), "foo");
256 assert_eq!(remove_quotes("\"Hello world\""), "Hello world");
257 }
258
259 #[test]
260 #[should_panic(expected = "assertion failed: x.len() >= 2")]
261 fn test_remove_quotes_empty() {
262 remove_quotes("");
263 }
264
265 #[test]
266 #[should_panic(expected = "assertion failed: x.len() >= 2")]
267 fn test_remove_quotes_short() {
268 remove_quotes("\"");
269 }
270
271 #[test]
272 #[should_panic(expected = "assertion `left == right` failed\n left: 102\n right: 34")]
273 fn test_remove_quotes_no_quotes() {
274 remove_quotes("foo");
275 }
276
277 #[test]
278 #[should_panic(expected = "assertion `left == right` failed\n left: 225\n right: 34")]
279 fn test_remove_quotes_mid_utf8() {
280 remove_quotes("\u{1234}foo\"");
281 }
282
283 fn basic_parsing_options() -> ParsingOptions {
285 ParsingOptions {
286 remove_withdrawn_candidates: true,
287 remove_empty_ballots: true,
288 optimize_layout: false,
289 }
290 }
291
292 #[test]
293 fn test_parse_election() {
294 let file = r#"5 2
295[nick apple banana cherry date eggplant]
296[tie cherry apple eggplant banana date]
2973 apple cherry eggplant date banana 0
2983 date=eggplant banana=cherry=apple 0
29942 cherry 0
300123 banana date 0
3010
302"Apple"
303"Banana"
304"Cherry"
305"Date"
306"Eggplant"
307"Vegetable contest"
308"#;
309 let logger = ThreadLocalLogger::start_filtered(LevelFilter::Debug);
310 let election = parse_election(Cursor::new(file), basic_parsing_options()).unwrap();
311
312 assert_eq!(
313 election,
314 Election::builder()
315 .title("Vegetable contest")
316 .num_seats(2)
317 .candidates([
318 Candidate::new("apple", false),
319 Candidate::new("banana", false),
320 Candidate::new("cherry", false),
321 Candidate::new("date", false),
322 Candidate::new("eggplant", false),
323 ])
324 .ballots(vec![
325 Ballot::new(3, [vec![0], vec![2], vec![4], vec![3], vec![1]]),
326 Ballot::new(3, [vec![3, 4], vec![1, 2, 0]]),
327 Ballot::new(42, [vec![2]]),
328 Ballot::new(123, [vec![1], vec![3]]),
329 ])
330 .check_num_ballots(171)
331 .tie_order([2, 0, 4, 1, 3])
332 .build()
333 );
334 logger.check_any_target_logs([
335 (Info, "Optimizing the in-memory layout of ballots is disabled"),
336 (Info, "2 seats / 5 candidates"),
337 (Info, "Nicknames: [\"apple\", \"banana\", \"cherry\", \"date\", \"eggplant\"]"),
338 (Info, "Tie-break order: [\"cherry\", \"apple\", \"eggplant\", \"banana\", \"date\"]"),
339 (Info, "Candidates (by nickname): [\"apple\", \"banana\", \"cherry\", \"date\", \"eggplant\"]"),
340 (Info, "Number of ballots: 171"),
341 (Info, "Election title: Vegetable contest"),
342 (Debug, "Allocations of 32 bytes: 8 => 256 bytes"),
343 (Debug, "Allocations of 192 bytes: 1 => 192 bytes"),
344 (Debug, "Ballots use 448 bytes in 9 allocations"),
345 (Debug, "Each ballot uses 112 bytes in 2.25 allocations"),
346 ]);
347 }
348
349 #[test]
350 fn test_parse_optimize_layout() {
351 let file = r#"5 2
352[nick apple banana cherry date eggplant]
353[tie cherry apple eggplant banana date]
3543 apple cherry eggplant date banana 0
3553 date=eggplant banana=cherry=apple 0
35642 cherry 0
357123 banana date 0
3580
359"Apple"
360"Banana"
361"Cherry"
362"Date"
363"Eggplant"
364"Vegetable contest"
365"#;
366 let logger = ThreadLocalLogger::start_filtered(LevelFilter::Debug);
367 let election = parse_election(
368 Cursor::new(file),
369 ParsingOptions {
370 remove_withdrawn_candidates: true,
371 remove_empty_ballots: true,
372 optimize_layout: true,
373 },
374 )
375 .unwrap();
376
377 assert_eq!(
378 election,
379 Election::builder()
380 .title("Vegetable contest")
381 .num_seats(2)
382 .candidates([
383 Candidate::new("apple", false),
384 Candidate::new("banana", false),
385 Candidate::new("cherry", false),
386 Candidate::new("date", false),
387 Candidate::new("eggplant", false),
388 ])
389 .ballots(vec![
390 Ballot::new(42, [vec![2]]),
391 Ballot::new(123, [vec![1], vec![3]]),
392 Ballot::new(3, [vec![0], vec![2], vec![4], vec![3], vec![1]]),
393 Ballot::new(3, [vec![3, 4], vec![1, 2, 0]]),
394 ])
395 .check_num_ballots(171)
396 .tie_order([2, 0, 4, 1, 3])
397 .build()
398 );
399 logger.check_any_target_logs([
400 (Info, "Optimizing the in-memory layout of ballots is enabled"),
401 (Info, "2 seats / 5 candidates"),
402 (Info, "Nicknames: [\"apple\", \"banana\", \"cherry\", \"date\", \"eggplant\"]"),
403 (Info, "Tie-break order: [\"cherry\", \"apple\", \"eggplant\", \"banana\", \"date\"]"),
404 (Info, "Candidates (by nickname): [\"apple\", \"banana\", \"cherry\", \"date\", \"eggplant\"]"),
405 (Info, "Number of ballots: 171"),
406 (Info, "Election title: Vegetable contest"),
407 (Debug, "Allocations of 32 bytes: 8 => 256 bytes"),
408 (Debug, "Allocations of 192 bytes: 1 => 192 bytes"),
409 (Debug, "Ballots use 448 bytes in 9 allocations"),
410 (Debug, "Each ballot uses 112 bytes in 2.25 allocations"),
411 ]);
412 }
413
414 #[test]
415 fn test_parse_names_with_digits() {
416 let file = r#"3 2
417[nick apple ba2nana34 cherry]
418[tie cherry apple ba2nana34]
4191 apple ba2nana34 0
4200
421"Apple"
422"Ba 2 nana 34"
423"Cherry"
424"Vegetable contest"
425"#;
426 let logger = ThreadLocalLogger::start_filtered(LevelFilter::Debug);
427 let election = parse_election(Cursor::new(file), basic_parsing_options()).unwrap();
428
429 assert_eq!(
430 election,
431 Election::builder()
432 .title("Vegetable contest")
433 .num_seats(2)
434 .candidates([
435 Candidate::new("apple", false),
436 Candidate {
437 nickname: "ba2nana34".to_owned(),
438 name: "Ba 2 nana 34".to_owned(),
439 is_withdrawn: false,
440 },
441 Candidate::new("cherry", false),
442 ])
443 .ballots(vec![Ballot::new(1, [vec![0], vec![1]])])
444 .check_num_ballots(1)
445 .tie_order([2, 0, 1])
446 .build()
447 );
448 logger.check_any_target_logs([
449 (
450 Info,
451 "Optimizing the in-memory layout of ballots is disabled",
452 ),
453 (Info, "2 seats / 3 candidates"),
454 (Info, "Nicknames: [\"apple\", \"ba2nana34\", \"cherry\"]"),
455 (
456 Info,
457 "Tie-break order: [\"cherry\", \"apple\", \"ba2nana34\"]",
458 ),
459 (
460 Info,
461 "Candidates (by nickname): [\"apple\", \"ba2nana34\", \"cherry\"]",
462 ),
463 (Info, "Number of ballots: 1"),
464 (Info, "Election title: Vegetable contest"),
465 (Debug, "Allocations of 32 bytes: 2 => 64 bytes"),
466 (Debug, "Allocations of 192 bytes: 1 => 192 bytes"),
467 (Debug, "Ballots use 256 bytes in 3 allocations"),
468 (Debug, "Each ballot uses 256 bytes in 3 allocations"),
469 ]);
470 }
471
472 #[test]
473 fn test_parse_withdrawn_keep_all() {
474 let file = r#"5 2
475[nick apple banana cherry date eggplant]
476[withdrawn cherry eggplant]
4773 apple cherry eggplant date banana 0
4783 date=eggplant banana=cherry=apple 0
47942 cherry 0
480123 banana date 0
48117 0
4820
483"Apple"
484"Banana"
485"Cherry"
486"Date"
487"Eggplant"
488"Vegetable contest"
489"#;
490 let logger = ThreadLocalLogger::start_filtered(LevelFilter::Debug);
491 let election = parse_election(
492 Cursor::new(file),
493 ParsingOptions {
494 remove_withdrawn_candidates: false,
495 remove_empty_ballots: false,
496 optimize_layout: false,
497 },
498 )
499 .unwrap();
500
501 assert_eq!(
502 election,
503 Election::builder()
504 .title("Vegetable contest")
505 .num_seats(2)
506 .candidates([
507 Candidate::new("apple", false),
508 Candidate::new("banana", false),
509 Candidate::new("cherry", true),
510 Candidate::new("date", false),
511 Candidate::new("eggplant", true),
512 ])
513 .ballots(vec![
514 Ballot::new(3, [vec![0], vec![2], vec![4], vec![3], vec![1]]),
515 Ballot::new(3, [vec![3, 4], vec![1, 2, 0]]),
516 Ballot::new(42, [vec![2]]),
517 Ballot::new(123, [vec![1], vec![3]]),
518 Ballot::empties(17),
519 ])
520 .check_num_ballots(188)
521 .build()
522 );
523 logger.check_any_target_logs([
524 (Info, "Optimizing the in-memory layout of ballots is disabled"),
525 (Info, "2 seats / 5 candidates"),
526 (Info, "Nicknames: [\"apple\", \"banana\", \"cherry\", \"date\", \"eggplant\"]"),
527 (Info, "Withdrawn: [\"cherry\", \"eggplant\"]"),
528 (Info, "Candidates (by nickname): [\"apple\", \"banana\", \"cherry\", \"date\", \"eggplant\"]"),
529 (Info, "Number of ballots: 188"),
530 (Info, "Election title: Vegetable contest"),
531 (Debug, "Allocations of 0 bytes: 2 => 0 bytes"),
532 (Debug, "Allocations of 32 bytes: 8 => 256 bytes"),
533 (Debug, "Allocations of 384 bytes: 1 => 384 bytes"),
534 (Debug, "Ballots use 640 bytes in 11 allocations"),
535 (Debug, "Each ballot uses 128 bytes in 2.2 allocations"),
536 ]);
537 }
538
539 #[test]
540 fn test_parse_withdrawn_remove_withdrawn() {
541 let file = r#"5 2
542[nick apple banana cherry date eggplant]
543[withdrawn cherry eggplant]
5443 apple cherry eggplant date banana 0
5453 date=eggplant banana=cherry=apple 0
54642 cherry 0
547123 banana date 0
54817 0
5490
550"Apple"
551"Banana"
552"Cherry"
553"Date"
554"Eggplant"
555"Vegetable contest"
556"#;
557 let logger = ThreadLocalLogger::start_filtered(LevelFilter::Debug);
558 let election = parse_election(
559 Cursor::new(file),
560 ParsingOptions {
561 remove_withdrawn_candidates: true,
562 remove_empty_ballots: false,
563 optimize_layout: false,
564 },
565 )
566 .unwrap();
567
568 assert_eq!(
569 election,
570 Election::builder()
571 .title("Vegetable contest")
572 .num_seats(2)
573 .candidates([
574 Candidate::new("apple", false),
575 Candidate::new("banana", false),
576 Candidate::new("cherry", true),
577 Candidate::new("date", false),
578 Candidate::new("eggplant", true),
579 ])
580 .ballots(vec![
581 Ballot::new(3, [vec![0], vec![3], vec![1]]),
582 Ballot::new(3, [vec![3], vec![1, 0]]),
583 Ballot::empties(42),
584 Ballot::new(123, [vec![1], vec![3]]),
585 Ballot::empties(17),
586 ])
587 .check_num_ballots(188)
588 .build()
589 );
590 logger.check_any_target_logs([
591 (Info, "Optimizing the in-memory layout of ballots is disabled"),
592 (Info, "2 seats / 5 candidates"),
593 (Info, "Nicknames: [\"apple\", \"banana\", \"cherry\", \"date\", \"eggplant\"]"),
594 (Info, "Withdrawn: [\"cherry\", \"eggplant\"]"),
595 (Info, "Candidates (by nickname): [\"apple\", \"banana\", \"cherry\", \"date\", \"eggplant\"]"),
596 (Info, "Number of ballots: 188"),
597 (Info, "Election title: Vegetable contest"),
598 (Debug, "Allocations of 0 bytes: 4 => 0 bytes"),
599 (Debug, "Allocations of 32 bytes: 6 => 192 bytes"),
600 (Debug, "Allocations of 384 bytes: 1 => 384 bytes"),
601 (Debug, "Ballots use 576 bytes in 11 allocations"),
602 (Debug, "Each ballot uses 115.2 bytes in 2.2 allocations"),
603 ]);
604 }
605
606 #[test]
607 fn test_parse_withdrawn_remove_empty_ballots() {
608 let file = r#"5 2
609[nick apple banana cherry date eggplant]
610[withdrawn cherry eggplant]
6113 apple cherry eggplant date banana 0
6123 date=eggplant banana=cherry=apple 0
61342 cherry 0
614123 banana date 0
61517 0
6160
617"Apple"
618"Banana"
619"Cherry"
620"Date"
621"Eggplant"
622"Vegetable contest"
623"#;
624 let logger = ThreadLocalLogger::start_filtered(LevelFilter::Debug);
625 let election = parse_election(
626 Cursor::new(file),
627 ParsingOptions {
628 remove_withdrawn_candidates: false,
629 remove_empty_ballots: true,
630 optimize_layout: false,
631 },
632 )
633 .unwrap();
634
635 assert_eq!(
636 election,
637 Election::builder()
638 .title("Vegetable contest")
639 .num_seats(2)
640 .candidates([
641 Candidate::new("apple", false),
642 Candidate::new("banana", false),
643 Candidate::new("cherry", true),
644 Candidate::new("date", false),
645 Candidate::new("eggplant", true),
646 ])
647 .ballots(vec![
648 Ballot::new(3, [vec![0], vec![2], vec![4], vec![3], vec![1]]),
649 Ballot::new(3, [vec![3, 4], vec![1, 2, 0]]),
650 Ballot::new(42, [vec![2]]),
651 Ballot::new(123, [vec![1], vec![3]]),
652 ])
653 .check_num_ballots(171)
654 .build()
655 );
656 logger.check_any_target_logs([
657 (Info, "Optimizing the in-memory layout of ballots is disabled"),
658 (Info, "2 seats / 5 candidates"),
659 (Info, "Nicknames: [\"apple\", \"banana\", \"cherry\", \"date\", \"eggplant\"]"),
660 (Info, "Withdrawn: [\"cherry\", \"eggplant\"]"),
661 (Info, "Candidates (by nickname): [\"apple\", \"banana\", \"cherry\", \"date\", \"eggplant\"]"),
662 (Warn, "Removing ballot that is empty or contains only withdrawn candidates: 17 0"),
663 (Info, "Number of ballots: 171"),
664 (Info, "Election title: Vegetable contest"),
665 (Debug, "Allocations of 32 bytes: 8 => 256 bytes"),
666 (Debug, "Allocations of 192 bytes: 1 => 192 bytes"),
667 (Debug, "Ballots use 448 bytes in 9 allocations"),
668 (Debug, "Each ballot uses 112 bytes in 2.25 allocations"),
669 ]);
670 }
671
672 #[test]
673 fn test_parse_withdrawn_remove_all() {
674 let file = r#"5 2
675[nick apple banana cherry date eggplant]
676[withdrawn cherry eggplant]
6773 apple cherry eggplant date banana 0
6783 date=eggplant banana=cherry=apple 0
67942 cherry 0
680123 banana date 0
68117 0
6820
683"Apple"
684"Banana"
685"Cherry"
686"Date"
687"Eggplant"
688"Vegetable contest"
689"#;
690 let logger = ThreadLocalLogger::start_filtered(LevelFilter::Debug);
691 let election = parse_election(
692 Cursor::new(file),
693 ParsingOptions {
694 remove_withdrawn_candidates: true,
695 remove_empty_ballots: true,
696 optimize_layout: false,
697 },
698 )
699 .unwrap();
700
701 assert_eq!(
702 election,
703 Election::builder()
704 .title("Vegetable contest")
705 .num_seats(2)
706 .candidates([
707 Candidate::new("apple", false),
708 Candidate::new("banana", false),
709 Candidate::new("cherry", true),
710 Candidate::new("date", false),
711 Candidate::new("eggplant", true),
712 ])
713 .ballots(vec![
714 Ballot::new(3, [vec![0], vec![3], vec![1]]),
715 Ballot::new(3, [vec![3], vec![1, 0]]),
716 Ballot::new(123, [vec![1], vec![3]]),
717 ])
718 .check_num_ballots(129)
719 .build()
720 );
721 logger.check_any_target_logs([
722 (Info, "Optimizing the in-memory layout of ballots is disabled"),
723 (Info, "2 seats / 5 candidates"),
724 (Info, "Nicknames: [\"apple\", \"banana\", \"cherry\", \"date\", \"eggplant\"]"),
725 (Info, "Withdrawn: [\"cherry\", \"eggplant\"]"),
726 (Info, "Candidates (by nickname): [\"apple\", \"banana\", \"cherry\", \"date\", \"eggplant\"]"),
727 (Warn, "Removing ballot that is empty or contains only withdrawn candidates: 42 cherry 0"),
728 (Warn, "Removing ballot that is empty or contains only withdrawn candidates: 17 0"),
729 (Info, "Number of ballots: 129"),
730 (Info, "Election title: Vegetable contest"),
731 (Debug, "Allocations of 32 bytes: 6 => 192 bytes"),
732 (Debug, "Allocations of 192 bytes: 1 => 192 bytes"),
733 (Debug, "Ballots use 384 bytes in 7 allocations"),
734 (Debug, "Each ballot uses 128 bytes in 2.3333333333333335 allocations"),
735 ]);
736 }
737
738 #[test]
739 fn test_parse_unknown_option() {
740 let file = r#"2 1
741[nick apple banana]
742[unknown foo bar]
7431 apple 0
7440
745"Apple"
746"Banana"
747"Vegetable contest"
748"#;
749 let logger = ThreadLocalLogger::start_filtered(LevelFilter::Debug);
750 let election = parse_election(Cursor::new(file), basic_parsing_options()).unwrap();
751
752 assert_eq!(
753 election,
754 Election::builder()
755 .title("Vegetable contest")
756 .num_seats(1)
757 .candidates([
758 Candidate::new("apple", false),
759 Candidate::new("banana", false),
760 ])
761 .ballots(vec![Ballot::new(1, [vec![0]])])
762 .check_num_ballots(1)
763 .build()
764 );
765 logger.check_any_target_logs([
766 (
767 Info,
768 "Optimizing the in-memory layout of ballots is disabled",
769 ),
770 (Info, "1 seats / 2 candidates"),
771 (Info, "Nicknames: [\"apple\", \"banana\"]"),
772 (Warn, "Unknown option: unknown"),
773 (Info, "Candidates (by nickname): [\"apple\", \"banana\"]"),
774 (Info, "Number of ballots: 1"),
775 (Info, "Election title: Vegetable contest"),
776 (Debug, "Allocations of 32 bytes: 2 => 64 bytes"),
777 (Debug, "Allocations of 192 bytes: 1 => 192 bytes"),
778 (Debug, "Ballots use 256 bytes in 3 allocations"),
779 (Debug, "Each ballot uses 256 bytes in 3 allocations"),
780 ]);
781 }
782
783 #[test]
784 #[should_panic(
785 expected = "assertion `left == right` failed: Tie-break order must mention all candidates\n left: 1\n right: 2"
786 )]
787 fn test_parse_tie_not_all_candidates() {
788 let file = r#"2 1
789[nick apple banana]
790[tie banana]
7911 apple 0
7920
793"Apple"
794"Banana"
795"Vegetable contest"
796"#;
797 let _ = parse_election(Cursor::new(file), basic_parsing_options());
798 }
799
800 #[test]
801 #[should_panic(expected = "Candidate mentioned twice in tie order: banana")]
802 fn test_parse_tie_repeated_candidate() {
803 let file = r#"2 1
804[nick apple banana]
805[tie banana banana]
8061 apple 0
8070
808"Apple"
809"Banana"
810"Vegetable contest"
811"#;
812 let _ = parse_election(Cursor::new(file), basic_parsing_options());
813 }
814
815 #[test]
816 #[should_panic(expected = "assertion `left == right` failed\n left: 2\n right: 1")]
817 fn test_parse_ballot_repeated_candidate() {
818 let file = r#"2 1
819[nick apple banana]
8201 apple apple 0
8210
822"Apple"
823"Banana"
824"Vegetable contest"
825"#;
826 let _ = parse_election(Cursor::new(file), basic_parsing_options());
827 }
828
829 #[test]
830 #[should_panic(expected = "called `Option::unwrap()` on a `None` value")]
831 fn test_parse_ballot_unknown_nickname() {
832 let file = r#"2 1
833[nick apple banana]
8341 appppppple 0
8350
836"appppppple"
837"bananaaaaa"
838"Vegetable contest"
839"#;
840 let _ = parse_election(Cursor::new(file), basic_parsing_options());
841 }
842}