use anyhow::{bail, ensure, Context as _};
use humantime_serde::Serde;
use ignore::{overrides::OverrideBuilder, WalkBuilder};
use itertools::{EitherOrBoth, Itertools as _};
use maplit::hashmap;
use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
use std::{
borrow::Borrow,
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fs,
hash::Hash,
path::Path,
str::FromStr,
sync::Arc,
time::Duration,
};
#[derive(Deserialize, Serialize, Debug, PartialEq)]
#[serde(tag = "type")]
pub enum TestSuite {
Batch(BatchTestSuite),
Interactive(InteractiveTestSuite),
Unsubmittable,
}
impl TestSuite {
pub fn to_yaml_pretty(&self) -> String {
return if let Self::Batch(suite) = self {
(|| -> _ {
let mut yaml = "---\n".to_owned();
yaml += &key_value("type", "Batch").ok()?;
yaml += &key_value("timelimit", Serde::from(suite.timelimit)).ok()?;
yaml += &key_value("match", suite.r#match).ok()?;
yaml += if suite.cases.is_empty() {
"\ncases: []\n"
} else {
"\ncases:\n"
};
for case in &suite.cases {
let mut part = "".to_owned();
if let Some(name) = &case.name {
part += &key_value("name", name).ok()?;
}
part += &key_value_in_literal_style("in", &case.r#in).ok()?;
if let Some(out) = &case.out {
part += &key_value_in_literal_style("out", out).ok()?;
}
if let Some(timelimit) = case.timelimit {
part += &key_value("timelimit", Serde::from(timelimit)).ok()?;
}
if let Some(r#match) = case.r#match {
part += &key_value("match", r#match).ok()?;
}
for (i, line) in part.lines().enumerate() {
yaml += match i {
0 => " - ",
_ => " ",
};
yaml += line;
yaml += "\n";
}
}
if suite.extend.is_empty() {
yaml += "\nextend: []\n";
} else {
yaml += "\nextend:\n";
for line in serde_yaml::to_string(&suite.extend)
.ok()?
.trim_start_matches("---\n")
.lines()
{
yaml += " ";
yaml += line;
yaml += "\n";
}
}
if serde_yaml::from_str::<Self>(&yaml).ok()? != *self {
return None;
}
Some(Ok(yaml))
})()
.unwrap_or_else(|| serde_yaml::to_string(self))
} else {
serde_yaml::to_string(self)
}
.unwrap_or_else(|e| panic!("failed to serialize: {}", e));
fn key_value(key: impl Serialize, value: impl Serialize) -> serde_yaml::Result<String> {
let key = serde_yaml::to_value(key)?;
let mut acc = serde_yaml::to_string(&hashmap!(key => value))?;
debug_assert!(acc.starts_with("---\n"));
acc += "\n";
Ok(acc.split_off(4))
}
fn key_value_in_literal_style(
key: impl Serialize,
value: &str,
) -> serde_yaml::Result<String> {
(|| -> _ {
if !value
.chars()
.all(|c| c == ' ' || c == '\n' || !(c.is_whitespace() || c.is_control()))
{
return None;
}
let key = serde_yaml::to_value(&key).ok()?;
let mut acc = serde_yaml::to_string(&hashmap!(&key => serde_yaml::Value::Null))
.ok()?
.trim_start_matches("---\n")
.trim_end_matches('~')
.to_owned();
acc += if value.ends_with('\n') { "|\n" } else { ">\n" };
for line in value.lines() {
acc += " ";
acc += line;
acc += "\n";
}
if serde_yaml::from_str::<HashMap<serde_yaml::Value, String>>(&acc).ok()?
!= hashmap!(key => value.to_owned())
{
return None;
}
Some(Ok(acc))
})()
.unwrap_or_else(|| key_value(key, value))
}
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq)]
pub struct BatchTestSuite {
#[serde(default, with = "humantime_serde")]
pub timelimit: Option<Duration>,
pub r#match: Match,
#[serde(default)]
pub cases: Vec<PartialBatchTestCase>,
#[serde(default)]
pub extend: Vec<Additional>,
}
impl BatchTestSuite {
pub fn load_test_cases<S: Borrow<str> + Eq + Hash>(
&self,
parent_dir: &Path,
mut names: Option<HashSet<S>>,
) -> anyhow::Result<Vec<BatchTestCase>> {
let mut cases = self.cases.clone();
for extend in &self.extend {
cases.extend(extend.load_test_cases(parent_dir)?);
}
let cases = cases
.into_iter()
.filter(
|PartialBatchTestCase { name, .. }| match (names.as_mut(), name.as_ref()) {
(Some(names), Some(name)) => names.remove(name),
_ => true,
},
)
.map(|case| BatchTestCase::new(case, self.timelimit, self.r#match))
.collect();
if let Some(names) = names {
if !names.is_empty() {
bail!(
"No such test cases: {:?}",
names.iter().map(Borrow::borrow).collect::<BTreeSet<_>>(),
);
}
}
Ok(cases)
}
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct PartialBatchTestCase {
pub name: Option<String>,
#[serde(with = "serde_fn::arc_str")]
pub r#in: Arc<str>,
#[serde(with = "serde_fn::option_arc_str")]
pub out: Option<Arc<str>>,
#[serde(default, with = "humantime_serde")]
pub timelimit: Option<Duration>,
pub r#match: Option<Match>,
}
#[derive(Deserialize, Serialize, Debug, PartialEq)]
#[serde(tag = "type")]
pub enum Additional {
Text {
path: String,
r#in: String,
out: String,
#[serde(
default,
with = "humantime_serde",
skip_serializing_if = "Option::is_none"
)]
timelimit: Option<Duration>,
#[serde(skip_serializing_if = "Option::is_none")]
r#match: Option<Match>,
},
}
impl Additional {
fn load_test_cases(&self, parent_dir: &Path) -> anyhow::Result<Vec<PartialBatchTestCase>> {
match self {
Self::Text {
path: base,
r#in,
out,
r#match,
timelimit,
} => {
let base = Path::new(base);
let base = parent_dir.join(base.strip_prefix(".").unwrap_or(base));
let base = base.strip_prefix(".").unwrap_or(&base);
let mut cases = BTreeMap::<_, (Option<_>, Option<_>)>::new();
let walk = |overrides| -> _ {
WalkBuilder::new(base)
.max_depth(Some(128))
.overrides(overrides)
.standard_filters(false)
.build()
.map::<anyhow::Result<_>, _>(|entry| {
let path = entry?.into_path();
if path.is_dir() {
return Ok(None);
}
let name = path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
let content = fs::read_to_string(&path)
.with_context(|| format!("Could not read {}", path.display()))?
.into();
Ok(Some((name, content)))
})
.flat_map(Result::transpose)
};
for result in walk(OverrideBuilder::new(base).add(r#in)?.build()?) {
let (name, content) = result?;
let (entry, _) = cases.entry(name.clone()).or_default();
ensure!(entry.is_none(), "Duplicated name: {:?}", name);
*entry = Some(content);
}
for result in walk(OverrideBuilder::new(base).add(out)?.build()?) {
let (name, content) = result?;
let (_, entry) = cases.entry(name.clone()).or_default();
ensure!(entry.is_none(), "Duplicated name: {:?}", name);
*entry = Some(content);
}
cases
.into_iter()
.map(|kv| {
let (name, r#in, out) = match kv {
(_, (None, None)) => unreachable!(),
(name, (None, Some(_))) => bail!("No input file for {:?}", name),
(name, (Some(r#in), out)) => (name, r#in, out),
};
Ok(PartialBatchTestCase {
name: Some(name),
r#in,
out,
timelimit: *timelimit,
r#match: *r#match,
})
})
.collect()
}
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq)]
pub enum Match {
Exact,
Lines,
Float {
relative_error: Option<PositiveFinite<f64>>,
absolute_error: Option<PositiveFinite<f64>>,
},
}
#[derive(Deserialize, Serialize, Debug, PartialEq)]
pub struct InteractiveTestSuite {
#[serde(default, with = "humantime_serde")]
pub timelimit: Option<Duration>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BatchTestCase {
pub name: Option<String>,
pub timelimit: Option<Duration>,
pub input: Arc<str>,
pub output: ExpectedOutput,
}
impl BatchTestCase {
fn new(case: PartialBatchTestCase, timelimit: Option<Duration>, matching: Match) -> Self {
BatchTestCase {
name: case.name,
timelimit: case.timelimit.or(timelimit),
input: case.r#in,
output: ExpectedOutput::new(case.out, case.r#match.unwrap_or(matching)),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ExpectedOutput {
Pass,
Exact {
text: Arc<str>,
},
Lines {
text: Arc<str>,
},
Float {
text: Arc<str>,
relative_error: Option<PositiveFinite<f64>>,
absolute_error: Option<PositiveFinite<f64>>,
},
}
impl ExpectedOutput {
fn new(text: Option<Arc<str>>, matching: Match) -> Self {
match (text, matching) {
(None, _) => Self::Pass,
(Some(text), Match::Exact) => Self::Exact { text },
(Some(text), Match::Lines) => Self::Lines { text },
(
Some(text),
Match::Float {
relative_error,
absolute_error,
},
) => Self::Float {
text,
relative_error,
absolute_error,
},
}
}
pub(crate) fn is_float(&self) -> bool {
matches!(self, Self::Float { .. })
}
pub(crate) fn text(&self) -> Option<&Arc<str>> {
match self {
Self::Exact { text } | Self::Lines { text } | Self::Float { text, .. } => Some(text),
Self::Pass => None,
}
}
pub(crate) fn accepts(&self, actual: &str) -> bool {
match self {
Self::Pass => true,
Self::Exact { text } => &**text == actual,
Self::Lines { text } => text.lines().eq(actual.lines()),
Self::Float {
text,
relative_error,
absolute_error,
} => {
let (text, actual) = (text.lines(), actual.lines());
let relative_error = relative_error.map(PositiveFinite::get).unwrap_or(0.0);
let absolute_error = absolute_error.map(PositiveFinite::get).unwrap_or(0.0);
text.zip_longest(actual).all(|zip| {
if let EitherOrBoth::Both(line1, line2) = zip {
let (words1, words2) = (line1.split_whitespace(), line2.split_whitespace());
words1.zip_longest(words2).all(|zip| match zip {
EitherOrBoth::Both(s1, s2) => {
match (s1.parse::<f64>(), s2.parse::<f64>()) {
(Ok(v1), Ok(v2)) => {
(v1 - v2).abs() <= absolute_error
|| ((v1 - v2) / v2).abs() <= relative_error
}
_ => s1 == s2,
}
}
EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false,
})
} else {
false
}
})
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
#[serde(transparent)]
pub struct PositiveFinite<F>(F);
impl PositiveFinite<f64> {
fn new(value: f64) -> Option<Self> {
if value.is_sign_positive() && value.is_finite() {
Some(Self(value))
} else {
None
}
}
pub fn get(self) -> f64 {
self.0
}
}
impl FromStr for PositiveFinite<f64> {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
Self::new(s.parse()?).with_context(|| "must be positive and finite")
}
}
impl<'de> Deserialize<'de> for PositiveFinite<f64> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::new(f64::deserialize(deserializer)?)
.ok_or_else(|| D::Error::custom("must be positive and finite"))
}
}
mod serde_fn {
pub(super) mod arc_str {
use serde::{Deserialize, Deserializer, Serializer};
use std::sync::Arc;
pub(crate) fn serialize<S>(this: &Arc<str>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(this)
}
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Arc<str>, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).map(Into::into)
}
}
pub(super) mod option_arc_str {
use serde::{Deserialize, Deserializer, Serializer};
use std::sync::Arc;
pub(crate) fn serialize<S>(
this: &Option<Arc<str>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(s) = this {
serializer.serialize_some(&**s)
} else {
serializer.serialize_none()
}
}
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<Arc<str>>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer).map(|s| s.map(Into::into))
}
}
}
#[cfg(test)]
mod tests {
use crate::testsuite::{
Additional, BatchTestSuite, ExpectedOutput, Match, PartialBatchTestCase, PositiveFinite,
TestSuite,
};
use difference::assert_diff;
use pretty_assertions::assert_eq;
use std::time::Duration;
#[test]
fn atcoder_abc162_a() {
test_serialize_deserialize(
r#"---
type: Batch
timelimit: 2s
match: Lines
cases:
- name: Sample 1
in: |
117
out: |
Yes
- name: Sample 2
in: |
123
out: |
No
- name: Sample 3
in: |
777
out: |
Yes
extend: []
"#,
&TestSuite::Batch(BatchTestSuite {
timelimit: Some(Duration::from_secs(2)),
r#match: Match::Lines,
cases: vec![
PartialBatchTestCase {
name: Some("Sample 1".to_owned()),
r#in: "117\n".into(),
out: Some("Yes\n".into()),
timelimit: None,
r#match: None,
},
PartialBatchTestCase {
name: Some("Sample 2".to_owned()),
r#in: "123\n".into(),
out: Some("No\n".into()),
timelimit: None,
r#match: None,
},
PartialBatchTestCase {
name: Some("Sample 3".to_owned()),
r#in: "777\n".into(),
out: Some("Yes\n".into()),
timelimit: None,
r#match: None,
},
],
extend: vec![],
}),
);
test_serialize_deserialize(
r#"---
type: Batch
timelimit: 2s
match: Lines
cases: []
extend:
- type: Text
path: "./a"
in: /in/*.txt
out: /out/*.txt
"#,
&TestSuite::Batch(BatchTestSuite {
timelimit: Some(Duration::from_secs(2)),
r#match: Match::Lines,
cases: vec![],
extend: vec![Additional::Text {
path: "./a".to_owned(),
r#in: "/in/*.txt".into(),
out: "/out/*.txt".into(),
timelimit: None,
r#match: None,
}],
}),
);
}
#[test]
fn atcoder_abc163_a() {
test_serialize_deserialize(
r#"---
type: Batch
timelimit: 2s
match:
Float:
relative_error: 0.01
absolute_error: 0.01
cases:
- name: Sample 1
in: |
1
out: |
6.28318530717958623200
- name: Sample 2
in: |
73
out: |
458.67252742410977361942
extend: []
"#,
&TestSuite::Batch(BatchTestSuite {
timelimit: Some(Duration::from_secs(2)),
r#match: Match::Float {
relative_error: Some(PositiveFinite(0.01)),
absolute_error: Some(PositiveFinite(0.01)),
},
cases: vec![
PartialBatchTestCase {
name: Some("Sample 1".to_owned()),
r#in: "1\n".into(),
out: Some("6.28318530717958623200\n".into()),
timelimit: None,
r#match: None,
},
PartialBatchTestCase {
name: Some("Sample 2".to_owned()),
r#in: "73\n".into(),
out: Some("458.67252742410977361942\n".into()),
timelimit: None,
r#match: None,
},
],
extend: vec![],
}),
);
}
#[test]
fn atcoder_arc071_c() {
test_serialize_deserialize(
r#"---
type: Batch
timelimit: 2s
match: Lines
cases:
- name: Sample 1
in: |
3
cbaa
daacc
acacac
out: |
aac
- name: Sample 2
in: |
3
a
aa
b
out: "\n"
extend: []
"#,
&TestSuite::Batch(BatchTestSuite {
timelimit: Some(Duration::from_secs(2)),
r#match: Match::Lines,
cases: vec![
PartialBatchTestCase {
name: Some("Sample 1".to_owned()),
r#in: "3\ncbaa\ndaacc\nacacac\n".into(),
out: Some("aac\n".into()),
timelimit: None,
r#match: None,
},
PartialBatchTestCase {
name: Some("Sample 2".to_owned()),
r#in: "3\na\naa\nb\n".into(),
out: Some("\n".into()),
timelimit: None,
r#match: None,
},
],
extend: vec![],
}),
);
}
fn test_serialize_deserialize(yaml: &str, expected: &TestSuite) {
let actual = serde_yaml::from_str::<TestSuite>(yaml).unwrap();
assert_eq!(*expected, actual);
assert_diff!(yaml, &actual.to_yaml_pretty(), "\n", 0);
}
#[test]
fn expected_output_accepts() {
assert!(ExpectedOutput::Pass.accepts("ミ゙"));
assert!(ExpectedOutput::Exact {
text: "1 2\n".into()
}
.accepts("1 2\n"));
assert!(!ExpectedOutput::Exact {
text: "1 2\n".into()
}
.accepts("1 2\n"));
assert!(!ExpectedOutput::Exact {
text: "1 2\n".into()
}
.accepts("1\n2\n"));
assert!(ExpectedOutput::Lines {
text: "1 2\n".into()
}
.accepts("1 2\n"));
assert!(!ExpectedOutput::Lines {
text: "1 2\n".into()
}
.accepts("1 2\n"));
assert!(!ExpectedOutput::Lines {
text: "1 2\n".into()
}
.accepts("1\n2\n"));
assert!(ExpectedOutput::Float {
text: "10000.0\n".into(),
relative_error: Some(PositiveFinite(0.01)),
absolute_error: None,
}
.accepts("10001.0\n"));
assert!(!ExpectedOutput::Float {
text: "10000.0\n".into(),
relative_error: Some(PositiveFinite(0.01)),
absolute_error: None,
}
.accepts("0\n"));
}
}