Merge remote-tracking branch 'gh/main' into feat/8jk6.3-date-hierarchy
# Conflicts: # context/test-evidence/formula-and-io.md # crates/improvise-io/src/import/wizard.rs # src/main.rs
This commit is contained in:
@@ -703,6 +703,77 @@ fn an_axis_naming_a_category_the_import_did_not_build_is_refused() {
|
||||
);
|
||||
}
|
||||
|
||||
/// `--time` on a column with no date format makes it a time category without
|
||||
/// dates, so there is nothing to split.
|
||||
#[test]
|
||||
fn split_date_on_a_time_field_with_no_date_format_is_refused() {
|
||||
let scratch = Scratch::new("split_date_no_format");
|
||||
let out_path = scratch.path("out.improv");
|
||||
let out = run_import(
|
||||
&dated_csv(&scratch),
|
||||
&out_path,
|
||||
&["--time", "Revenue", "--split-date", "Revenue"],
|
||||
);
|
||||
assert!(!out.status.success(), "{}", stderr_of(&out));
|
||||
assert!(
|
||||
stderr_of(&out).contains("--split-date"),
|
||||
"{}",
|
||||
stderr_of(&out)
|
||||
);
|
||||
}
|
||||
|
||||
/// A name that is no date part gets the categories that exist, never the date
|
||||
/// hint (the README's old `--axis Measure:column` was this).
|
||||
#[test]
|
||||
fn an_absent_axis_category_that_is_no_date_part_names_what_was_built() {
|
||||
let scratch = Scratch::new("axis_unknown_plain");
|
||||
let out_path = scratch.path("out.improv");
|
||||
let out = run_import(
|
||||
&dated_csv(&scratch),
|
||||
&out_path,
|
||||
&["--axis", "Measure:column"],
|
||||
);
|
||||
assert!(!out.status.success(), "{}", stderr_of(&out));
|
||||
let err = stderr_of(&out);
|
||||
assert!(
|
||||
err.contains("'Measure'") && err.contains("_Measure"),
|
||||
"{err}"
|
||||
);
|
||||
assert!(!err.contains("--split-date"), "no date hint: {err}");
|
||||
}
|
||||
|
||||
/// The README's import recipe runs as printed.
|
||||
#[test]
|
||||
fn the_readmes_import_recipe_runs() {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let readme = std::fs::read_to_string(root.join("README.md")).unwrap();
|
||||
let start = readme.find("improvise import examples/demo.csv").unwrap();
|
||||
let recipe = readme[start..]
|
||||
.split("```")
|
||||
.next()
|
||||
.unwrap()
|
||||
.replace("\\\n", " ");
|
||||
let mut args: Vec<String> = vec![];
|
||||
for (i, part) in recipe.split('"').enumerate() {
|
||||
match i % 2 {
|
||||
1 => args.push(part.to_string()),
|
||||
_ => args.extend(part.split_whitespace().map(String::from)),
|
||||
}
|
||||
}
|
||||
let at = args.iter().position(|a| a == "-o").unwrap();
|
||||
args.truncate(at);
|
||||
let scratch = Scratch::new("readme_recipe");
|
||||
let out = Command::new(bin())
|
||||
.arg("import")
|
||||
.arg(root.join("examples/demo.csv"))
|
||||
.args(args.iter().skip(3))
|
||||
.arg("-o")
|
||||
.arg(scratch.path("out.improv"))
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success(), "{}", stderr_of(&out));
|
||||
}
|
||||
|
||||
/// code-maat's reports: three schemas, a stray note, and room for the output.
|
||||
fn code_maat_folder(scratch: &Scratch) -> PathBuf {
|
||||
let dir = scratch.path("maat");
|
||||
@@ -799,6 +870,192 @@ fn a_headless_import_takes_files_and_folders_together_and_refuses_an_empty_folde
|
||||
assert!(!scratch.path("never.improv").exists());
|
||||
}
|
||||
|
||||
/// Runs `import <source> --no-wizard <extra> -o out.improv` on a source it writes.
|
||||
fn import_of(scratch: &Scratch, name: &str, body: &str, extra: &[&str]) -> (Output, PathBuf) {
|
||||
let source = scratch.path(name);
|
||||
std::fs::write(&source, body).unwrap();
|
||||
let out_path = scratch.path("out.improv");
|
||||
(run_import(&source, &out_path, extra), out_path)
|
||||
}
|
||||
|
||||
/// improvise-8jk6.4: a forced category's values are item names, so they keep
|
||||
/// their source text. Evidence: `context/test-evidence/cli.md`.
|
||||
#[test]
|
||||
fn a_csv_column_forced_to_category_keeps_its_text_verbatim() {
|
||||
let scratch = Scratch::new("forced_category_text");
|
||||
let (out, out_path) = import_of(
|
||||
&scratch,
|
||||
"commits.csv",
|
||||
"commit,lines\n00565796,3\n220944e2,5\n",
|
||||
&["--category", "commit", "--measure", "lines"],
|
||||
);
|
||||
assert!(out.status.success(), "{}", stderr_of(&out));
|
||||
let doc = std::fs::read_to_string(&out_path).unwrap();
|
||||
assert!(
|
||||
doc.lines().any(|l| l == "- |00565796|, |220944e2|"),
|
||||
"the items are the source text:\n{doc}"
|
||||
);
|
||||
for line in [
|
||||
"_Measure=lines, commit=|00565796| = 3",
|
||||
"_Measure=lines, commit=|220944e2| = 5",
|
||||
] {
|
||||
assert!(doc.lines().any(|l| l == line), "{line} missing from\n{doc}");
|
||||
}
|
||||
}
|
||||
|
||||
/// improvise-8jk6.4: JSON has no source text for a number, so a forced
|
||||
/// category names it as serde_json writes it. Evidence file.
|
||||
#[test]
|
||||
fn a_json_number_forced_to_category_is_named_as_json_writes_it() {
|
||||
let scratch = Scratch::new("forced_category_json");
|
||||
let (out, out_path) = import_of(
|
||||
&scratch,
|
||||
"commits.json",
|
||||
r#"[{"commit": 42, "n": 1}, {"commit": 1.5, "n": 2}, {"commit": 1e3, "n": 3}]"#,
|
||||
&["--category", "commit", "--measure", "n"],
|
||||
);
|
||||
assert!(out.status.success(), "{}", stderr_of(&out));
|
||||
let doc = std::fs::read_to_string(&out_path).unwrap();
|
||||
assert!(
|
||||
doc.lines().any(|l| l == "- |42|, |1.5|, |1000.0|"),
|
||||
"the items are the numbers' JSON text:\n{doc}"
|
||||
);
|
||||
}
|
||||
|
||||
/// improvise-8jk6.5: a formula the user named and that does not parse is a
|
||||
/// refusal, not a silent drop. Evidence: `context/test-evidence/cli.md`.
|
||||
#[test]
|
||||
fn an_import_formula_that_does_not_parse_refuses_names_it_and_writes_nothing() {
|
||||
let scratch = Scratch::new("bad_formula");
|
||||
let (out, out_path) = import_of(
|
||||
&scratch,
|
||||
"f.csv",
|
||||
"a,x\nk,1\n",
|
||||
&[
|
||||
"--measure",
|
||||
"x",
|
||||
"--formula",
|
||||
"Y = x +",
|
||||
"--formula",
|
||||
"Z = x",
|
||||
"--formula",
|
||||
"W = * 2",
|
||||
],
|
||||
);
|
||||
let err = stderr_of(&out);
|
||||
assert!(!out.status.success(), "a dropped formula exited 0: {err}");
|
||||
assert!(err.contains("'Y = x +'"), "names the formula: {err}");
|
||||
assert!(err.contains("'W = * 2'"), "names every bad one: {err}");
|
||||
assert!(err.contains("expected"), "carries the parse error: {err}");
|
||||
assert!(!err.contains("'Z = x'"), "names only the bad one: {err}");
|
||||
assert!(!out_path.exists(), "nothing is written");
|
||||
}
|
||||
|
||||
/// improvise-8jk6.6: an import that kept no record says why, and exits
|
||||
/// non-zero writing nothing. Evidence: `context/test-evidence/cli.md`.
|
||||
#[test]
|
||||
fn an_import_that_keeps_no_record_refuses_and_says_why() {
|
||||
let scratch = Scratch::new("kept_none");
|
||||
let (out, out_path) = import_of(&scratch, "r.json", r#"[1, 2, "three"]"#, &[]);
|
||||
let err = stderr_of(&out);
|
||||
assert!(!out.status.success(), "{err}");
|
||||
assert!(
|
||||
err.contains("all 3 records were skipped as not JSON objects"),
|
||||
"says why: {err}"
|
||||
);
|
||||
assert!(!out_path.exists(), "nothing is written");
|
||||
}
|
||||
|
||||
/// improvise-8jk6.6: a partial skip warns with a count and exits 0, a choice
|
||||
/// put to the user. Evidence: `context/test-evidence/cli.md`.
|
||||
#[test]
|
||||
fn an_import_that_skips_some_records_warns_with_a_count_and_still_writes() {
|
||||
let scratch = Scratch::new("kept_some");
|
||||
let (out, out_path) = import_of(
|
||||
&scratch,
|
||||
"p.json",
|
||||
r#"[{"a": "k", "x": 1}, 2, {"a": "m", "x": 4}]"#,
|
||||
&[],
|
||||
);
|
||||
let err = stderr_of(&out);
|
||||
assert!(out.status.success(), "{err}");
|
||||
assert!(
|
||||
err.contains("warning: Skipped 1 of 3 records that were not JSON objects"),
|
||||
"{err}"
|
||||
);
|
||||
let doc = std::fs::read_to_string(&out_path).unwrap();
|
||||
assert!(doc.contains("_Measure=x, a=m = 4"), "{doc}");
|
||||
}
|
||||
|
||||
/// A JSON array with one stray scalar among its records.
|
||||
const ONE_STRAY: &str = r#"[{"a": "k", "x": 1}, 2, {"a": "m", "x": 4}]"#;
|
||||
|
||||
/// improvise-8jk6.6, the user's ruling: `--strict` makes any skip a refusal
|
||||
/// that writes nothing. Evidence: `context/test-evidence/cli.md`.
|
||||
#[test]
|
||||
fn a_strict_import_that_skips_a_record_refuses_and_writes_nothing() {
|
||||
let scratch = Scratch::new("strict_skip");
|
||||
let (out, out_path) = import_of(&scratch, "p.json", ONE_STRAY, &["--strict"]);
|
||||
let err = stderr_of(&out);
|
||||
assert!(!out.status.success(), "{err}");
|
||||
assert!(
|
||||
err.contains("--strict: Skipped 1 of 3 records that were not JSON objects"),
|
||||
"{err}"
|
||||
);
|
||||
assert!(!out_path.exists(), "a strict refusal writes nothing");
|
||||
}
|
||||
|
||||
/// The control: `--strict` refuses a skip, not every import.
|
||||
#[test]
|
||||
fn a_strict_import_that_skips_nothing_still_writes() {
|
||||
let scratch = Scratch::new("strict_clean");
|
||||
let body = r#"[{"a": "k", "x": 1}, {"a": "m", "x": 4}]"#;
|
||||
let (out, out_path) = import_of(&scratch, "p.json", body, &["--strict"]);
|
||||
assert!(out.status.success(), "{}", stderr_of(&out));
|
||||
let doc = std::fs::read_to_string(&out_path).unwrap();
|
||||
assert!(doc.contains("_Measure=x, a=m = 4"), "{doc}");
|
||||
}
|
||||
|
||||
/// improvise-8jk6.17 (F3): a CSV decimal measure is its number, not dropped
|
||||
/// text. Evidence: `context/test-evidence/cli.md`.
|
||||
#[test]
|
||||
fn a_csv_decimal_measure_imports_as_its_number() {
|
||||
let scratch = Scratch::new("csv_decimal");
|
||||
let body = "Region,Revenue\nEast,1000.50\nWest,-2.25\n";
|
||||
let (out, out_path) = import_of(&scratch, "f.csv", body, &[]);
|
||||
assert!(out.status.success(), "{}", stderr_of(&out));
|
||||
let doc = std::fs::read_to_string(&out_path).unwrap();
|
||||
for line in [
|
||||
"Region=East, _Measure=Revenue = 1000.5",
|
||||
"Region=West, _Measure=Revenue = -2.25",
|
||||
] {
|
||||
assert!(doc.lines().any(|l| l == line), "{line} missing from\n{doc}");
|
||||
}
|
||||
}
|
||||
|
||||
/// improvise-8jk6.17 (F1): `import-json` reads a CSV as CSV, so its numeric
|
||||
/// column is a measure. Evidence: `context/test-evidence/cli.md`.
|
||||
#[test]
|
||||
fn import_json_on_a_csv_reads_its_numeric_column_as_a_measure() {
|
||||
let scratch = Scratch::new("import_json_csv");
|
||||
let csv = scratch.path("f.csv");
|
||||
std::fs::write(&csv, "Region,Revenue\nEast,1000.50\nWest,7\n").unwrap();
|
||||
let doc = scratch.path("m.improv");
|
||||
let out = run_cmd(&[&format!("import-json {}", csv.display())], &doc);
|
||||
assert!(out.status.success(), "{}", stderr_of(&out));
|
||||
let text = std::fs::read_to_string(&doc).unwrap();
|
||||
assert!(!text.contains("## Category: Revenue"), "{text}");
|
||||
for line in [
|
||||
"Region=East, _Measure=Revenue = 1000.5",
|
||||
"Region=West, _Measure=Revenue = 7",
|
||||
] {
|
||||
assert!(
|
||||
text.lines().any(|l| l == line),
|
||||
"{line} missing from\n{text}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// improvise-lpzu, the user's ruling: a folder always merges, so even one CSV
|
||||
/// keeps its file identity. Evidence: `context/test-evidence/cli.md`.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user