snowchains_core/
testsuite.rs

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}