From c687338d4ef54bb5ddfac53caf71a20549331ba5 Mon Sep 17 00:00:00 2001 From: Sam Estep Date: Sun, 4 Apr 2021 11:15:22 -0700 Subject: [PATCH 1/3] Consolidate most examples data into a YAML file --- Cargo.lock | 21 ++- Cargo.toml | 3 +- examples/compile/err/complicated.txt | 0 examples/compile/err/hello.txt | 0 examples/compile/err/hello_syntax_errors.txt | 3 - examples/compile/out/hello_syntax_errors.txt | 0 .../out/complicated.txt => complicated.js} | 0 .../{hello_syntax_errors.qn => errors.qn} | 0 examples/{compile/out/hello.txt => hello.js} | 0 examples/run/err/complicated.txt | 0 examples/run/err/hello.txt | 0 examples/run/err/hello_syntax_errors.txt | 3 - examples/run/out/complicated.txt | 5 - examples/run/out/hello.txt | 1 - examples/run/out/hello_syntax_errors.txt | 0 src/db.rs | 3 +- tests/cli.rs | 175 +++++++++++++++--- tests/goldenfiles/examples.yml | 17 ++ tests/version.rs | 51 ++--- 19 files changed, 207 insertions(+), 75 deletions(-) delete mode 100644 examples/compile/err/complicated.txt delete mode 100644 examples/compile/err/hello.txt delete mode 100644 examples/compile/err/hello_syntax_errors.txt delete mode 100644 examples/compile/out/hello_syntax_errors.txt rename examples/{compile/out/complicated.txt => complicated.js} (100%) rename examples/{hello_syntax_errors.qn => errors.qn} (100%) rename examples/{compile/out/hello.txt => hello.js} (100%) delete mode 100644 examples/run/err/complicated.txt delete mode 100644 examples/run/err/hello.txt delete mode 100644 examples/run/err/hello_syntax_errors.txt delete mode 100644 examples/run/out/complicated.txt delete mode 100644 examples/run/out/hello.txt delete mode 100644 examples/run/out/hello_syntax_errors.txt create mode 100644 tests/goldenfiles/examples.yml diff --git a/Cargo.lock b/Cargo.lock index 5f8263b..020c879 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -719,6 +719,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + [[package]] name = "either" version = "1.6.1" @@ -2155,8 +2161,10 @@ dependencies = [ "regex", "rusty_v8", "salsa", + "serde", "serde_json", "serde_v8", + "serde_yaml", "slurp", "structopt", "strum", @@ -2167,7 +2175,6 @@ dependencies = [ "tree-sitter", "url", "vergen", - "yaml-rust", ] [[package]] @@ -2600,6 +2607,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + [[package]] name = "sha-1" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index ea6ff31..85cce8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,5 +52,6 @@ goldenfile = "1" itertools = "0.10" pretty_assertions = "0.7" regex = "1" +serde = "1" +serde_yaml = "0.8" toml = "0.5" -yaml-rust = "0.4" diff --git a/examples/compile/err/complicated.txt b/examples/compile/err/complicated.txt deleted file mode 100644 index e69de29..0000000 diff --git a/examples/compile/err/hello.txt b/examples/compile/err/hello.txt deleted file mode 100644 index e69de29..0000000 diff --git a/examples/compile/err/hello_syntax_errors.txt b/examples/compile/err/hello_syntax_errors.txt deleted file mode 100644 index d27575b..0000000 --- a/examples/compile/err/hello_syntax_errors.txt +++ /dev/null @@ -1,3 +0,0 @@ -0:6 to 0:14 Error: syntax (ERROR (string)) -0:24 to 0:24 Error: syntax (MISSING ";") -Error: Failed to parse. diff --git a/examples/compile/out/hello_syntax_errors.txt b/examples/compile/out/hello_syntax_errors.txt deleted file mode 100644 index e69de29..0000000 diff --git a/examples/compile/out/complicated.txt b/examples/complicated.js similarity index 100% rename from examples/compile/out/complicated.txt rename to examples/complicated.js diff --git a/examples/hello_syntax_errors.qn b/examples/errors.qn similarity index 100% rename from examples/hello_syntax_errors.qn rename to examples/errors.qn diff --git a/examples/compile/out/hello.txt b/examples/hello.js similarity index 100% rename from examples/compile/out/hello.txt rename to examples/hello.js diff --git a/examples/run/err/complicated.txt b/examples/run/err/complicated.txt deleted file mode 100644 index e69de29..0000000 diff --git a/examples/run/err/hello.txt b/examples/run/err/hello.txt deleted file mode 100644 index e69de29..0000000 diff --git a/examples/run/err/hello_syntax_errors.txt b/examples/run/err/hello_syntax_errors.txt deleted file mode 100644 index d27575b..0000000 --- a/examples/run/err/hello_syntax_errors.txt +++ /dev/null @@ -1,3 +0,0 @@ -0:6 to 0:14 Error: syntax (ERROR (string)) -0:24 to 0:24 Error: syntax (MISSING ";") -Error: Failed to parse. diff --git a/examples/run/out/complicated.txt b/examples/run/out/complicated.txt deleted file mode 100644 index 017fdc3..0000000 --- a/examples/run/out/complicated.txt +++ /dev/null @@ -1,5 +0,0 @@ -foo -bar -undefined -👻 ba # not a comment -z 🙃 diff --git a/examples/run/out/hello.txt b/examples/run/out/hello.txt deleted file mode 100644 index af5626b..0000000 --- a/examples/run/out/hello.txt +++ /dev/null @@ -1 +0,0 @@ -Hello, world! diff --git a/examples/run/out/hello_syntax_errors.txt b/examples/run/out/hello_syntax_errors.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/db.rs b/src/db.rs index 950a680..a0dd396 100644 --- a/src/db.rs +++ b/src/db.rs @@ -563,8 +563,7 @@ mod tests { #[test] fn test_diagnostics_hello_world() { - let (db, uri) = - foo_db(slurp::read_all_to_string("examples/hello_syntax_errors.qn").unwrap()); + let (db, uri) = foo_db(slurp::read_all_to_string("examples/errors.qn").unwrap()); let diagnostics = db.diagnostics(uri); assert_eq!( diagnostics, diff --git a/tests/cli.rs b/tests/cli.rs index 3713bd4..78711a3 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,9 +1,14 @@ use assert_cmd::{assert::OutputAssertExt, prelude::CommandCargoExt}; use goldenfile::Mint; +use pretty_assertions::assert_eq; use std::{ + collections::BTreeMap, ffi::OsStr, - path::Path, + fs::File, + io, + path::{Path, PathBuf}, process::{Command, Output}, + str, }; use std::{fs, io::Write}; @@ -17,41 +22,155 @@ fn test_help() { file.write_all(&assert.get_output().stdout).unwrap(); } -fn assert_expected(subcmd: &str, src: &Path) { - let mut cmd = Command::cargo_bin("quench").unwrap(); - let assert = cmd.env("NO_COLOR", "1").arg(subcmd).arg(&src).assert(); - let Output { - status, - stdout, - stderr, - } = assert.get_output(); +fn subcmd(name: &str, args: I) -> Output +where + I: IntoIterator, + S: AsRef, +{ + Command::cargo_bin("quench") + .unwrap() + .env("NO_COLOR", "1") + .arg(name) + .args(args) + .output() + .unwrap() +} - if stderr.is_empty() { - assert!(status.success()); +fn to_nonempty_string(bytes: Vec) -> Option { + if bytes.is_empty() { + None } else { - assert!(!status.success()); + Some(str::from_utf8(&bytes).unwrap().to_string()) } +} + +#[derive(Debug, serde::Deserialize, PartialEq)] +struct Example { + compile: Option, + status: Option, + out: Option, + err: Option, +} - let dir = src.parent().unwrap().join(subcmd); - let txt_path = src.with_extension("txt"); - let txt_name = Path::new(txt_path.file_name().unwrap()); +fn try_example(path: PathBuf) -> (String, Example) { + let stem = path.file_stem().unwrap().to_str().unwrap().to_string(); + let example = { + let Output { + status, + stdout, + stderr, + } = subcmd("compile", &[&path]); + if status.success() { + assert!(stderr.is_empty()); + { + let mut mint = Mint::new("examples"); + let mut file = mint + .new_goldenfile(Path::new(&stem).with_extension("js")) + .unwrap(); + file.write_all(&stdout).unwrap(); + } - let mut out_mint = Mint::new(dir.join("out")); - let mut out = out_mint.new_goldenfile(txt_name).unwrap(); - out.write_all(stdout).unwrap(); + let Output { + status, + stdout, + stderr, + } = subcmd("run", &[path]); + Example { + compile: None, + // we don't just use status.code() here, because we assume there was an exit code + status: { + let code = status.code().unwrap(); + if code == 0 { + None + } else { + Some(code) + } + }, + out: to_nonempty_string(stdout), + err: to_nonempty_string(stderr), + } + } else { + assert!(stdout.is_empty()); + Example { + compile: to_nonempty_string(stderr), + status: None, + out: None, + err: None, + } + } + }; + (stem, example) +} - let mut err_mint = Mint::new(dir.join("err")); - let mut err = err_mint.new_goldenfile(txt_name).unwrap(); - err.write_all(stderr).unwrap(); +fn write_literal(writer: &mut impl Write, key: &str, value: &Option) -> io::Result<()> { + if let Some(string) = value { + write!(writer, " {}: |\n", key)?; + for line in string.lines() { + write!(writer, " {}\n", line)?; + } + } + Ok(()) } -#[test] -fn test_examples() { - for entry in fs::read_dir("examples").unwrap() { - let path = entry.unwrap().path(); - if path.extension() == Some(OsStr::new("qn")) { - assert_expected("compile", &path); - assert_expected("run", &path); +fn write_example(writer: &mut impl Write, name: &str, example: &Example) -> io::Result<()> { + write!(writer, "{}:\n", name)?; + let Example { + compile, + status, + out, + err, + } = example; + write_literal(writer, "compile", compile)?; + if let Some(code) = status { + write!(writer, " status: {}\n", code)?; + } + write_literal(writer, "out", out)?; + write_literal(writer, "err", err)?; + Ok(()) +} + +type Examples = BTreeMap; + +fn write_examples(writer: &mut impl Write, examples: &Examples) -> io::Result<()> { + let mut it = examples.iter(); + if let Some((name, example)) = it.next() { + write_example(writer, name, example)?; + for (name, example) in it { + write!(writer, "\n")?; + write_example(writer, name, example)?; } } + Ok(()) +} + +#[test] +fn test_examples() { + let examples: Examples = fs::read_dir("examples") + .unwrap() + .filter_map(|entry| { + let path = entry.unwrap().path(); + if path.extension() == Some(OsStr::new("qn")) { + Some(try_example(path)) + } else { + None + } + }) + .collect(); + + let goldenfiles = "tests/goldenfiles"; + let filename = "examples.yml"; + + write_examples( + &mut Mint::new(goldenfiles).new_goldenfile(filename).unwrap(), + &examples, + ) + .unwrap(); + + assert_eq!( + examples, + serde_yaml::from_reader::( + File::open(Path::new(goldenfiles).join(filename)).unwrap() + ) + .unwrap(), + ); } diff --git a/tests/goldenfiles/examples.yml b/tests/goldenfiles/examples.yml new file mode 100644 index 0000000..401751d --- /dev/null +++ b/tests/goldenfiles/examples.yml @@ -0,0 +1,17 @@ +complicated: + out: | + foo + bar + undefined + 👻 ba # not a comment + z 🙃 + +errors: + compile: | + 0:6 to 0:14 Error: syntax (ERROR (string)) + 0:24 to 0:24 Error: syntax (MISSING ";") + Error: Failed to parse. + +hello: + out: | + Hello, world! diff --git a/tests/version.rs b/tests/version.rs index 13d007a..4f2dba3 100644 --- a/tests/version.rs +++ b/tests/version.rs @@ -1,7 +1,6 @@ use comrak::ComrakOptions; use regex::Regex; use std::{collections::HashSet, path::PathBuf, str}; -use yaml_rust::{Yaml, YamlLoader}; #[test] fn test_version() { @@ -76,36 +75,26 @@ fn test_minimum_rustc() { let ci_versions: HashSet = { let re = Regex::new(r"^(\d+\.\d+)\.\d+$").unwrap(); - YamlLoader::load_from_str(&slurp::read_all_to_string(".github/workflows/ci.yml").unwrap()) - .unwrap() - .get(0) - .unwrap() - .as_hash() - .unwrap() - .get(&Yaml::String(String::from("jobs"))) - .unwrap() - .as_hash() - .unwrap() - .get(&Yaml::String(String::from("rust"))) - .unwrap() - .as_hash() - .unwrap() - .get(&Yaml::String(String::from("strategy"))) - .unwrap() - .as_hash() - .unwrap() - .get(&Yaml::String(String::from("matrix"))) - .unwrap() - .as_hash() - .unwrap() - .get(&Yaml::String(String::from("rust"))) - .unwrap() - .as_vec() - .unwrap() - .iter() - .filter_map(|version| re.captures(version.as_str().unwrap())) - .map(|m| String::from(&m[1])) - .collect() + serde_yaml::from_str::( + &slurp::read_all_to_string(".github/workflows/ci.yml").unwrap(), + ) + .unwrap() + .get("jobs") + .unwrap() + .get("rust") + .unwrap() + .get("strategy") + .unwrap() + .get("matrix") + .unwrap() + .get("rust") + .unwrap() + .as_sequence() + .unwrap() + .iter() + .filter_map(|version| re.captures(version.as_str().unwrap())) + .map(|m| String::from(&m[1])) + .collect() }; assert!( readme_versions.is_subset(&ci_versions), From 08d046a991d7a1cb5baf97a3d3ead05de8829caa Mon Sep 17 00:00:00 2001 From: Sam Estep Date: Sun, 4 Apr 2021 11:20:49 -0700 Subject: [PATCH 2/3] Document the testing strategy a bit more --- tests/cli.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/cli.rs b/tests/cli.rs index 78711a3..1f6bdb8 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -160,12 +160,17 @@ fn test_examples() { let goldenfiles = "tests/goldenfiles"; let filename = "examples.yml"; + // assert that the actual YAML file matches what we generate from running the examples; the + // goldenfile paradigm provides a nice testing UX via its REGENERATE_GOLDENFILES env var, but we + // need to use a custom write_examples function because yaml-rust doesn't emit literal scalars + // https://github.com/chyh1990/yaml-rust/pull/132 https://github.com/chyh1990/yaml-rust/pull/137 write_examples( &mut Mint::new(goldenfiles).new_goldenfile(filename).unwrap(), &examples, ) .unwrap(); + // redundant check, to ensure that our write_examples function works correctly assert_eq!( examples, serde_yaml::from_reader::( From a614a50827b1c2ae56e5af5118364d12dbc05c33 Mon Sep 17 00:00:00 2001 From: Sam Estep Date: Sun, 4 Apr 2021 11:48:33 -0700 Subject: [PATCH 3/3] Fix shebang test in CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b109671..b319adb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,7 +91,7 @@ jobs: run: cargo install --locked --path . # partially covered by tests/cli.rs, but this checks the shebang - name: Check output of hello example - run: examples/hello.qn | git diff --no-index examples/run/out/hello.txt - + run: diff <(examples/hello.qn) <(echo 'Hello, world!') - name: Upload quench for next job uses: actions/upload-artifact@v2 with: