1use anyhow::{bail, ensure, Context as _};
2use camino::Utf8PathBuf;
3use humantime_serde::Serde;
4use ignore::{overrides::OverrideBuilder, WalkBuilder};
5use itertools::{EitherOrBoth, Itertools as _};
6use maplit::hashmap;
7use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
8use std::{
9 borrow::Borrow,
10 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
11 fs,
12 hash::Hash,
13 path::Path,
14 str::FromStr,
15 sync::Arc,
16 time::Duration,
17};
18use url::Url;
19
20#[derive(Deserialize, Serialize, Debug, PartialEq)]
21#[serde(tag = "type")]
22pub enum TestSuite {
23 Batch(BatchTestSuite),
24 Interactive(InteractiveTestSuite),
25 Unsubmittable,
26}
27
28impl TestSuite {
29 pub fn to_yaml_pretty(&self) -> String {
30 return if let Self::Batch(suite) = self {
31 (|| -> _ {
32 let mut yaml = "---\n".to_owned();
33
34 yaml += &key_value("type", "Batch").ok()?;
35 yaml += &key_value("timelimit", Serde::from(suite.timelimit)).ok()?;
36 yaml += &key_value("match", &suite.r#match).ok()?;
37
38 yaml += if suite.cases.is_empty() {
39 "\ncases: []\n"
40 } else {
41 "\ncases:\n"
42 };
43
44 for case in &suite.cases {
45 let mut part = "".to_owned();
46
47 if let Some(name) = &case.name {
48 part += &key_value("name", name).ok()?;
49 }
50
51 part += &key_value_in_literal_style("in", &case.r#in).ok()?;
52
53 if let Some(out) = &case.out {
54 part += &key_value_in_literal_style("out", out).ok()?;
55 }
56
57 if let Some(timelimit) = case.timelimit {
58 part += &key_value("timelimit", Serde::from(timelimit)).ok()?;
59 }
60
61 if let Some(r#match) = &case.r#match {
62 part += &key_value("match", r#match).ok()?;
63 }
64
65 for (i, line) in part.lines().enumerate() {
66 yaml += match i {
67 0 => " - ",
68 _ => " ",
69 };
70 yaml += line;
71 yaml += "\n";
72 }
73 }
74
75 if suite.extend.is_empty() {
76 yaml += "\nextend: []\n";
77 } else {
78 yaml += "\nextend:\n";
79
80 for line in serde_yaml::to_string(&suite.extend)
81 .ok()?
82 .trim_start_matches("---\n")
83 .lines()
84 {
85 yaml += " ";
86 yaml += line;
87 yaml += "\n";
88 }
89 }
90
91 if serde_yaml::from_str::<Self>(&yaml).ok()? != *self {
92 return None;
93 }
94
95 Some(Ok(yaml))
96 })()
97 .unwrap_or_else(|| serde_yaml::to_string(self))
98 } else {
99 serde_yaml::to_string(self)
100 }
101 .unwrap_or_else(|e| panic!("failed to serialize: {}", e));
102
103 fn key_value(key: impl Serialize, value: impl Serialize) -> serde_yaml::Result<String> {
104 let key = serde_yaml::to_value(key)?;
105 let mut acc = serde_yaml::to_string(&hashmap!(key => value))?;
106 debug_assert!(acc.starts_with("---\n") && acc.ends_with('\n'));
107 Ok(acc.split_off(4))
108 }
109
110 fn key_value_in_literal_style(
111 key: impl Serialize,
112 value: &str,
113 ) -> serde_yaml::Result<String> {
114 (|| -> _ {
115 if !value
116 .chars()
117 .all(|c| c == ' ' || c == '\n' || !(c.is_whitespace() || c.is_control()))
118 {
119 return None;
120 }
121
122 let key = serde_yaml::to_value(&key).ok()?;
123
124 let mut acc = serde_yaml::to_string(&hashmap!(&key => serde_yaml::Value::Null))
125 .ok()?
126 .trim_start_matches("---\n")
127 .trim_end_matches('\n')
128 .trim_end_matches('~')
129 .to_owned();
130
131 acc += if value.ends_with('\n') { "|\n" } else { ">\n" };
132
133 for line in value.lines() {
134 acc += " ";
135 acc += line;
136 acc += "\n";
137 }
138
139 if serde_yaml::from_str::<HashMap<serde_yaml::Value, String>>(&acc).ok()?
140 != hashmap!(key => value.to_owned())
141 {
142 return None;
143 }
144
145 Some(Ok(acc))
146 })()
147 .unwrap_or_else(|| key_value(key, value))
148 }
149 }
150}
151
152#[derive(Deserialize, Serialize, Debug, PartialEq)]
153pub struct BatchTestSuite {
154 #[serde(default, with = "humantime_serde")]
155 pub timelimit: Option<Duration>,
156 pub r#match: Match,
157 #[serde(default)]
158 pub cases: Vec<PartialBatchTestCase>,
159 #[serde(default)]
160 pub extend: Vec<Additional>,
161}
162
163impl BatchTestSuite {
164 pub fn load_test_cases<
165 S: Borrow<str> + Eq + Hash,
166 F: FnMut(Option<&Url>) -> anyhow::Result<Vec<PartialBatchTestCase>>,
167 >(
168 &self,
169 parent_dir: &Path,
170 mut names: Option<HashSet<S>>,
171 mut prepare_system_test_cases: F,
172 ) -> anyhow::Result<Vec<BatchTestCase>> {
173 let mut cases = self.cases.clone();
174 for extend in &self.extend {
175 cases.extend(extend.load_test_cases(parent_dir, &mut prepare_system_test_cases)?);
176 }
177
178 let cases = cases
179 .into_iter()
180 .filter(
181 |PartialBatchTestCase { name, .. }| match (names.as_mut(), name.as_ref()) {
182 (Some(names), Some(name)) => names.remove(name),
183 _ => true,
184 },
185 )
186 .map(|case| BatchTestCase::new(case, self.timelimit, &self.r#match))
187 .collect();
188
189 if let Some(names) = names {
190 if !names.is_empty() {
191 bail!(
192 "No such test cases: {:?}",
193 names.iter().map(Borrow::borrow).collect::<BTreeSet<_>>(),
194 );
195 }
196 }
197
198 Ok(cases)
199 }
200}
201
202#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
203pub struct PartialBatchTestCase {
204 pub name: Option<String>,
205 #[serde(with = "serde_fn::arc_str")]
206 pub r#in: Arc<str>,
207 #[serde(default, with = "serde_fn::option_arc_str")]
208 pub out: Option<Arc<str>>,
209 #[serde(default, with = "humantime_serde")]
210 pub timelimit: Option<Duration>,
211 pub r#match: Option<Match>,
212}
213
214#[derive(Deserialize, Serialize, Debug, PartialEq)]
215#[serde(tag = "type")]
216pub enum Additional {
217 Text {
218 path: Utf8PathBuf,
219 r#in: String,
220 out: String,
221 #[serde(
222 default,
223 with = "humantime_serde",
224 skip_serializing_if = "Option::is_none"
225 )]
226 timelimit: Option<Duration>,
227 #[serde(skip_serializing_if = "Option::is_none")]
228 r#match: Option<Match>,
229 },
230 SystemTestCases {
231 #[serde(skip_serializing_if = "Option::is_none")]
232 problem: Option<Url>,
233 },
234}
235
236impl Additional {
237 fn load_test_cases(
238 &self,
239 parent_dir: &Path,
240 mut prepare_system_test_cases: impl FnMut(
241 Option<&Url>,
242 ) -> anyhow::Result<Vec<PartialBatchTestCase>>,
243 ) -> anyhow::Result<Vec<PartialBatchTestCase>> {
244 match self {
245 Self::Text {
246 path: base,
247 r#in,
248 out,
249 r#match,
250 timelimit,
251 } => {
252 let base = Path::new(base);
253 let base = parent_dir.join(base.strip_prefix(".").unwrap_or(base));
254 let base = base.strip_prefix(".").unwrap_or(&base);
255
256 let mut cases = BTreeMap::<_, (Option<_>, Option<_>)>::new();
257
258 let walk = |overrides| -> _ {
259 WalkBuilder::new(base)
260 .max_depth(Some(128))
261 .overrides(overrides)
262 .standard_filters(false)
263 .build()
264 .map::<anyhow::Result<_>, _>(|entry| {
265 let path = entry?.into_path();
266
267 if path.is_dir() {
268 return Ok(None);
269 }
270
271 let name = path
272 .file_stem()
273 .unwrap_or_default()
274 .to_string_lossy()
275 .into_owned();
276
277 let content = fs::read_to_string(&path)
278 .with_context(|| format!("Could not read {}", path.display()))?
279 .into();
280
281 Ok(Some((name, content)))
282 })
283 .flat_map(Result::transpose)
284 };
285
286 for result in walk(OverrideBuilder::new(base).add(r#in)?.build()?) {
287 let (name, content) = result?;
288 let (entry, _) = cases.entry(name.clone()).or_default();
289 ensure!(entry.is_none(), "Duplicated name: {:?}", name);
290 *entry = Some(content);
291 }
292
293 for result in walk(OverrideBuilder::new(base).add(out)?.build()?) {
294 let (name, content) = result?;
295 let (_, entry) = cases.entry(name.clone()).or_default();
296 ensure!(entry.is_none(), "Duplicated name: {:?}", name);
297 *entry = Some(content);
298 }
299
300 cases
301 .into_iter()
302 .map(|kv| {
303 let (name, r#in, out) = match kv {
304 (_, (None, None)) => unreachable!(),
305 (name, (None, Some(_))) => bail!("No input file for {:?}", name),
306 (name, (Some(r#in), out)) => (name, r#in, out),
307 };
308
309 Ok(PartialBatchTestCase {
310 name: Some(name),
311 r#in,
312 out,
313 timelimit: *timelimit,
314 r#match: r#match.clone(),
315 })
316 })
317 .collect()
318 }
319 Self::SystemTestCases { problem } => prepare_system_test_cases(problem.as_ref()),
320 }
321 }
322}
323
324#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
325pub enum Match {
326 Exact,
327 SplitWhitespace,
328 Lines,
329 Float {
330 relative_error: Option<PositiveFinite<f64>>,
331 absolute_error: Option<PositiveFinite<f64>>,
332 },
333 Checker {
334 cmd: String,
335 shell: CheckerShell,
336 },
337}
338
339#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
340pub enum CheckerShell {
341 Bash,
342}
343
344#[derive(Deserialize, Serialize, Debug, PartialEq)]
345pub struct InteractiveTestSuite {
346 #[serde(default, with = "humantime_serde")]
347 pub timelimit: Option<Duration>,
348}
349
350#[derive(Debug, Clone, PartialEq)]
351pub struct BatchTestCase {
352 pub name: Option<String>,
353 pub timelimit: Option<Duration>,
354 pub input: Arc<str>,
355 pub output: ExpectedOutput,
356}
357
358impl BatchTestCase {
359 fn new(case: PartialBatchTestCase, timelimit: Option<Duration>, matching: &Match) -> Self {
360 BatchTestCase {
361 name: case.name,
362 timelimit: case.timelimit.or(timelimit),
363 input: case.r#in,
364 output: ExpectedOutput::new(case.out, case.r#match.unwrap_or_else(|| matching.clone())),
365 }
366 }
367}
368
369#[derive(Debug, Clone, PartialEq)]
370pub enum ExpectedOutput {
371 Deterministic(DeterministicExpectedOutput),
372 Checker {
373 text: Option<Arc<str>>,
374 cmd: String,
375 shell: CheckerShell,
376 },
377}
378
379impl ExpectedOutput {
380 fn new(text: Option<Arc<str>>, matching: Match) -> Self {
381 match (text, matching) {
382 (text, Match::Checker { cmd, shell }) => Self::Checker { text, cmd, shell },
383 (Some(text), Match::Exact) => {
384 Self::Deterministic(DeterministicExpectedOutput::Exact { text })
385 }
386 (Some(text), Match::SplitWhitespace) => {
387 Self::Deterministic(DeterministicExpectedOutput::SplitWhitespace { text })
388 }
389 (Some(text), Match::Lines) => {
390 Self::Deterministic(DeterministicExpectedOutput::Lines { text })
391 }
392 (
393 Some(text),
394 Match::Float {
395 relative_error,
396 absolute_error,
397 },
398 ) => Self::Deterministic(DeterministicExpectedOutput::Float {
399 text,
400 relative_error,
401 absolute_error,
402 }),
403 (None, _) => Self::Deterministic(DeterministicExpectedOutput::Pass),
404 }
405 }
406
407 pub(crate) fn is_float(&self) -> bool {
408 matches!(
409 self,
410 Self::Deterministic(DeterministicExpectedOutput::Float { .. })
411 )
412 }
413
414 pub(crate) fn expected_stdout(&self) -> Option<&str> {
415 match self {
416 Self::Deterministic(expected) => expected.expected_stdout(),
417 Self::Checker { .. } => None,
418 }
419 }
420
421 pub(crate) fn example(&self) -> Option<&str> {
422 match self {
423 Self::Checker { text, .. } => text.as_deref(),
424 _ => None,
425 }
426 }
427}
428
429#[derive(Debug, Clone, PartialEq)]
430pub enum DeterministicExpectedOutput {
431 Pass,
432 Exact {
433 text: Arc<str>,
434 },
435 SplitWhitespace {
436 text: Arc<str>,
437 },
438 Lines {
439 text: Arc<str>,
440 },
441 Float {
442 text: Arc<str>,
443 relative_error: Option<PositiveFinite<f64>>,
444 absolute_error: Option<PositiveFinite<f64>>,
445 },
446}
447
448impl DeterministicExpectedOutput {
449 pub(crate) fn accepts(&self, actual: &str) -> bool {
450 match self {
451 Self::Pass => true,
452 Self::Exact { text } => &**text == actual,
453 Self::SplitWhitespace { text } => text.split_whitespace().eq(actual.split_whitespace()),
454 Self::Lines { text } => text.lines().eq(actual.lines()),
455 Self::Float {
456 text,
457 relative_error,
458 absolute_error,
459 } => {
460 let (text, actual) = (text.lines(), actual.lines());
461 let relative_error = relative_error.map(PositiveFinite::get).unwrap_or(0.0);
462 let absolute_error = absolute_error.map(PositiveFinite::get).unwrap_or(0.0);
463
464 text.zip_longest(actual).all(|zip| {
465 if let EitherOrBoth::Both(line1, line2) = zip {
466 let (words1, words2) = (line1.split_whitespace(), line2.split_whitespace());
467 words1.zip_longest(words2).all(|zip| match zip {
468 EitherOrBoth::Both(s1, s2) => {
469 match (s1.parse::<f64>(), s2.parse::<f64>()) {
470 (Ok(v1), Ok(v2)) => {
471 (v1 - v2).abs() <= absolute_error
472 || ((v1 - v2) / v2).abs() <= relative_error
473 }
474 _ => s1 == s2,
475 }
476 }
477 EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false,
478 })
479 } else {
480 false
481 }
482 })
483 }
484 }
485 }
486
487 pub(crate) fn expected_stdout(&self) -> Option<&str> {
488 match self {
489 Self::Pass => None,
490 Self::Exact { text }
491 | Self::SplitWhitespace { text }
492 | Self::Lines { text }
493 | Self::Float { text, .. } => Some(text),
494 }
495 }
496}
497
498#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
499#[serde(transparent)]
500pub struct PositiveFinite<F>(F);
501
502impl PositiveFinite<f64> {
503 fn new(value: f64) -> Option<Self> {
504 if value.is_sign_positive() && value.is_finite() {
505 Some(Self(value))
506 } else {
507 None
508 }
509 }
510
511 pub fn get(self) -> f64 {
512 self.0
513 }
514}
515
516impl FromStr for PositiveFinite<f64> {
517 type Err = anyhow::Error;
518
519 fn from_str(s: &str) -> anyhow::Result<Self> {
520 Self::new(s.parse()?).with_context(|| "must be positive and finite")
521 }
522}
523
524impl<'de> Deserialize<'de> for PositiveFinite<f64> {
525 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
526 where
527 D: Deserializer<'de>,
528 {
529 Self::new(f64::deserialize(deserializer)?)
530 .ok_or_else(|| D::Error::custom("must be positive and finite"))
531 }
532}
533
534mod serde_fn {
535 pub(super) mod arc_str {
536 use serde::{Deserialize, Deserializer, Serializer};
537 use std::sync::Arc;
538
539 pub(crate) fn serialize<S>(this: &Arc<str>, serializer: S) -> Result<S::Ok, S::Error>
540 where
541 S: Serializer,
542 {
543 serializer.serialize_str(this)
544 }
545
546 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Arc<str>, D::Error>
547 where
548 D: Deserializer<'de>,
549 {
550 String::deserialize(deserializer).map(Into::into)
551 }
552 }
553
554 pub(super) mod option_arc_str {
555 use serde::{Deserialize, Deserializer, Serializer};
556 use std::sync::Arc;
557
558 pub(crate) fn serialize<S>(
559 this: &Option<Arc<str>>,
560 serializer: S,
561 ) -> Result<S::Ok, S::Error>
562 where
563 S: Serializer,
564 {
565 if let Some(s) = this {
566 serializer.serialize_some(&**s)
567 } else {
568 serializer.serialize_none()
569 }
570 }
571
572 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<Arc<str>>, D::Error>
573 where
574 D: Deserializer<'de>,
575 {
576 Option::<String>::deserialize(deserializer).map(|s| s.map(Into::into))
577 }
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use crate::testsuite::{
584 Additional, BatchTestSuite, DeterministicExpectedOutput, Match, PartialBatchTestCase,
585 PositiveFinite, TestSuite,
586 };
587 use difference::assert_diff;
588 use pretty_assertions::assert_eq;
589 use std::time::Duration;
590
591 #[test]
592 fn atcoder_abc162_a() {
593 test_serialize_deserialize(
594 r#"---
595type: Batch
596timelimit: 2s
597match: Lines
598
599cases:
600 - name: Sample 1
601 in: |
602 117
603 out: |
604 Yes
605 - name: Sample 2
606 in: |
607 123
608 out: |
609 No
610 - name: Sample 3
611 in: |
612 777
613 out: |
614 Yes
615
616extend: []
617"#,
618 &TestSuite::Batch(BatchTestSuite {
619 timelimit: Some(Duration::from_secs(2)),
620 r#match: Match::Lines,
621 cases: vec![
622 PartialBatchTestCase {
623 name: Some("Sample 1".to_owned()),
624 r#in: "117\n".into(),
625 out: Some("Yes\n".into()),
626 timelimit: None,
627 r#match: None,
628 },
629 PartialBatchTestCase {
630 name: Some("Sample 2".to_owned()),
631 r#in: "123\n".into(),
632 out: Some("No\n".into()),
633 timelimit: None,
634 r#match: None,
635 },
636 PartialBatchTestCase {
637 name: Some("Sample 3".to_owned()),
638 r#in: "777\n".into(),
639 out: Some("Yes\n".into()),
640 timelimit: None,
641 r#match: None,
642 },
643 ],
644 extend: vec![],
645 }),
646 );
647
648 test_serialize_deserialize(
649 r#"---
650type: Batch
651timelimit: 2s
652match: Lines
653
654cases: []
655
656extend:
657 - type: Text
658 path: "./a"
659 in: /in/*.txt
660 out: /out/*.txt
661"#,
662 &TestSuite::Batch(BatchTestSuite {
663 timelimit: Some(Duration::from_secs(2)),
664 r#match: Match::Lines,
665 cases: vec![],
666 extend: vec![Additional::Text {
667 path: "./a".into(),
668 r#in: "/in/*.txt".into(),
669 out: "/out/*.txt".into(),
670 timelimit: None,
671 r#match: None,
672 }],
673 }),
674 );
675 }
676
677 #[test]
678 fn atcoder_abc163_a() {
679 test_serialize_deserialize(
680 r#"---
681type: Batch
682timelimit: 2s
683match:
684 Float:
685 relative_error: 0.01
686 absolute_error: 0.01
687
688cases:
689 - name: Sample 1
690 in: |
691 1
692 out: |
693 6.28318530717958623200
694 - name: Sample 2
695 in: |
696 73
697 out: |
698 458.67252742410977361942
699
700extend: []
701"#,
702 &TestSuite::Batch(BatchTestSuite {
703 timelimit: Some(Duration::from_secs(2)),
704 r#match: Match::Float {
705 relative_error: Some(PositiveFinite(0.01)),
706 absolute_error: Some(PositiveFinite(0.01)),
707 },
708 cases: vec![
709 PartialBatchTestCase {
710 name: Some("Sample 1".to_owned()),
711 r#in: "1\n".into(),
712 out: Some("6.28318530717958623200\n".into()),
713 timelimit: None,
714 r#match: None,
715 },
716 PartialBatchTestCase {
717 name: Some("Sample 2".to_owned()),
718 r#in: "73\n".into(),
719 out: Some("458.67252742410977361942\n".into()),
720 timelimit: None,
721 r#match: None,
722 },
723 ],
724 extend: vec![],
725 }),
726 );
727 }
728
729 #[test]
730 fn atcoder_arc071_c() {
731 test_serialize_deserialize(
732 r#"---
733type: Batch
734timelimit: 2s
735match: Lines
736
737cases:
738 - name: Sample 1
739 in: |
740 3
741 cbaa
742 daacc
743 acacac
744 out: |
745 aac
746 - name: Sample 2
747 in: |
748 3
749 a
750 aa
751 b
752 out: "\n"
753
754extend: []
755"#,
756 &TestSuite::Batch(BatchTestSuite {
757 timelimit: Some(Duration::from_secs(2)),
758 r#match: Match::Lines,
759 cases: vec![
760 PartialBatchTestCase {
761 name: Some("Sample 1".to_owned()),
762 r#in: "3\ncbaa\ndaacc\nacacac\n".into(),
763 out: Some("aac\n".into()),
764 timelimit: None,
765 r#match: None,
766 },
767 PartialBatchTestCase {
768 name: Some("Sample 2".to_owned()),
769 r#in: "3\na\naa\nb\n".into(),
770 out: Some("\n".into()),
771 timelimit: None,
772 r#match: None,
773 },
774 ],
775 extend: vec![],
776 }),
777 );
778 }
779
780 fn test_serialize_deserialize(yaml: &str, expected: &TestSuite) {
781 let actual = serde_yaml::from_str::<TestSuite>(yaml).unwrap();
782 assert_eq!(*expected, actual);
783 assert_diff!(yaml, &actual.to_yaml_pretty(), "\n", 0);
784 }
785
786 #[test]
787 fn expected_output_accepts() {
788 assert!(DeterministicExpectedOutput::Pass.accepts("ミ゙"));
789
790 assert!(DeterministicExpectedOutput::Exact {
791 text: "1 2\n".into()
792 }
793 .accepts("1 2\n"));
794
795 assert!(!DeterministicExpectedOutput::Exact {
796 text: "1 2\n".into()
797 }
798 .accepts("1 2\n"));
799
800 assert!(!DeterministicExpectedOutput::Exact {
801 text: "1 2\n".into()
802 }
803 .accepts("1\n2\n"));
804
805 assert!(DeterministicExpectedOutput::SplitWhitespace { text: "".into() }.accepts(""));
806
807 assert!(DeterministicExpectedOutput::SplitWhitespace { text: "\n".into() }.accepts(""));
808
809 assert!(DeterministicExpectedOutput::SplitWhitespace { text: "".into() }.accepts("\n"));
810
811 assert!(DeterministicExpectedOutput::SplitWhitespace {
812 text: "1 2\n".into()
813 }
814 .accepts("1 2\n"));
815
816 assert!(
817 DeterministicExpectedOutput::SplitWhitespace { text: "1 2".into() }.accepts("1 2\n")
818 );
819
820 assert!(DeterministicExpectedOutput::SplitWhitespace {
821 text: " 1 2 \n".into()
822 }
823 .accepts("1 2\n"));
824
825 assert!(DeterministicExpectedOutput::Lines {
826 text: "1 2\n".into()
827 }
828 .accepts("1 2\n"));
829
830 assert!(!DeterministicExpectedOutput::Lines {
831 text: "1 2\n".into()
832 }
833 .accepts("1 2\n"));
834
835 assert!(!DeterministicExpectedOutput::Lines {
836 text: "1 2\n".into()
837 }
838 .accepts("1\n2\n"));
839
840 assert!(DeterministicExpectedOutput::Float {
841 text: "10000.0\n".into(),
842 relative_error: Some(PositiveFinite(0.01)),
843 absolute_error: None,
844 }
845 .accepts("10001.0\n"));
846
847 assert!(!DeterministicExpectedOutput::Float {
848 text: "10000.0\n".into(),
849 relative_error: Some(PositiveFinite(0.01)),
850 absolute_error: None,
851 }
852 .accepts("0\n"));
853 }
854}