mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Variables: Import Data Sets… reads tab-separated files and files without a name column
dataset.import read only commas between cells and refused a file whose first header cell wasn't `name`. A value that didn't match its variable's kind, and a repeated data set name, were reported with the row's record number, which is not the file line after a blank line or a quoted line break. A data set name over 255 characters gave no line. - When the header row, read with tabs between cells, has a tab and no comma outside quotes, tabs separate the cells. A quoted cell may then hold tabs and commas. - Without a `name` column every header cell is a variable, and the rows take the first free names Data Set 1, Data Set 2 and so on, as Capture Data Set and blank name cells do. A first header cell `name`, in any case, still holds the data set names. - Value errors, a repeated data set name and a name over 255 characters give the file line the row starts on. - The Variables panel's Import Data Sets… dialog also lists .tsv files. - import_fuzz.rs imports random text, random bytes and mutated exports, comma- and tab-separated, into a document with bound text, visibility and graph variables, selects up to three of the imported data sets in turn, and exports every data set. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -948,6 +948,70 @@ fn datasets_csv_keeps_spaces_in_quoted_cells() {
|
||||
assert_eq!(value("C"), text_value(" quoted "));
|
||||
}
|
||||
|
||||
/// A header row with a tab and no comma outside quotes makes tabs the separator; a quoted cell
|
||||
/// may then hold tabs and commas. A header row with a comma outside quotes keeps commas, even
|
||||
/// when a cell holds a tab. A tab-separated header with an unclosed quote fails with the reason.
|
||||
#[test]
|
||||
fn datasets_csv_reads_tab_separated_files() {
|
||||
let mut s = session();
|
||||
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Last, First", "kind": "text"})).unwrap();
|
||||
let tsv = "name\tTitle\tShow\r\nA\tTwo words\ttrue\r\nB\t\"a, b\tc\"\tfalse\r\n";
|
||||
let imp = s.execute("dataset.import", &json!({"csv": tsv})).unwrap();
|
||||
assert_eq!(imp["names"], json!(["A", "B"]));
|
||||
s.execute("dataset.import", &json!({"csv": "name,Title\nC,one\ttwo"})).unwrap();
|
||||
s.execute("dataset.import", &json!({"csv": "name\t\"Last, First\"\nD\tSmith, Jo"})).unwrap();
|
||||
let value = |name: &str, var: &str| s.doc().unwrap().doc.variables.dataset(name).and_then(|d| d.values.get(var)).cloned();
|
||||
let text_value = |v: &str| Some(vectorcraft_doc::DataValue::Text(v.into()));
|
||||
assert_eq!(value("A", "Title"), text_value("Two words"));
|
||||
assert_eq!(value("A", "Show"), Some(vectorcraft_doc::DataValue::Visible(true)));
|
||||
assert_eq!(value("B", "Title"), text_value("a, b\tc"));
|
||||
assert_eq!(value("B", "Show"), Some(vectorcraft_doc::DataValue::Visible(false)));
|
||||
assert_eq!(value("C", "Title"), text_value("one\ttwo"));
|
||||
assert_eq!(value("D", "Last, First"), text_value("Smith, Jo"));
|
||||
let unclosed = s.execute("dataset.import", &json!({"csv": "name\tTitle\t\"Show\nA\tx\ttrue"})).unwrap_err().to_string();
|
||||
assert!(unclosed.contains("unclosed quote"), "{unclosed}");
|
||||
}
|
||||
|
||||
/// Without a `name` column every header cell is a variable, and the rows take the first free
|
||||
/// names Data Set 1, 2 and so on. A first header cell `name`, in any case, still holds the names.
|
||||
#[test]
|
||||
fn datasets_csv_without_a_name_column_numbers_the_rows() {
|
||||
let mut s = session();
|
||||
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
|
||||
s.execute("dataset.new", &json!({"name": "Data Set 2", "values": {"Title": "kept"}})).unwrap();
|
||||
let imp = s.execute("dataset.import", &json!({"csv": "Title\nx\ny\n"})).unwrap();
|
||||
assert_eq!((imp["imported"].as_u64(), imp["updated"].as_u64()), (Some(2), Some(0)));
|
||||
assert_eq!(imp["names"], json!(["Data Set 1", "Data Set 3"]));
|
||||
let value = |s: &Session, name: &str| s.doc().unwrap().doc.variables.dataset(name).and_then(|d| d.values.get("Title")).cloned();
|
||||
let text_value = |v: &str| Some(vectorcraft_doc::DataValue::Text(v.into()));
|
||||
assert_eq!(value(&s, "Data Set 1"), text_value("x"));
|
||||
assert_eq!(value(&s, "Data Set 2"), text_value("kept"));
|
||||
assert_eq!(value(&s, "Data Set 3"), text_value("y"));
|
||||
let named = s.execute("dataset.import", &json!({"csv": "NAME,Title\nData Set 1,z"})).unwrap();
|
||||
assert_eq!((named["imported"].as_u64(), named["updated"].as_u64()), (Some(0), Some(1)));
|
||||
assert_eq!(value(&s, "Data Set 1"), text_value("z"));
|
||||
}
|
||||
|
||||
/// An error about a row's values or name gives the file line the row starts on, counting blank
|
||||
/// lines and line breaks inside quoted cells.
|
||||
#[test]
|
||||
fn datasets_csv_row_errors_name_the_file_line() {
|
||||
let mut s = session();
|
||||
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
|
||||
let error = |s: &mut Session, csv: &str| s.execute("dataset.import", &json!({"csv": csv})).unwrap_err().to_string();
|
||||
let blank_line = error(&mut s, "name,Show\n\nX,maybe");
|
||||
assert!(blank_line.contains("line 3:"), "{blank_line}");
|
||||
let line_break = error(&mut s, "name,Title,Show\nA,\"one\ntwo\",true\nB,x,maybe");
|
||||
assert!(line_break.contains("line 4:"), "{line_break}");
|
||||
let twice = error(&mut s, "name,Title\nA,x\n\nA,y");
|
||||
assert!(twice.contains("line 4:"), "{twice}");
|
||||
let long = error(&mut s, &format!("name,Title\n\n{},x", "n".repeat(256)));
|
||||
assert!(long.contains("line 3:") && long.contains("longer than 255"), "{long}");
|
||||
}
|
||||
|
||||
/// Import / export datasets as CSV: header `name` then variables; round-trip keeps values.
|
||||
#[test]
|
||||
fn datasets_csv_round_trips_and_rejects_bad_input() {
|
||||
@@ -989,7 +1053,7 @@ fn datasets_csv_round_trips_and_rejects_bad_input() {
|
||||
assert_eq!((again["imported"].as_u64(), again["updated"].as_u64()), (Some(0), Some(2)));
|
||||
assert!(s.execute("dataset.import", &json!({"csv": "name,Ghost\nX,y"})).is_err());
|
||||
assert!(s.execute("dataset.import", &json!({"csv": "name,Show\nX,maybe"})).is_err());
|
||||
assert!(s.execute("dataset.import", &json!({"csv": "Title,Name\nX,y"})).is_err(), "first column must be name");
|
||||
assert!(s.execute("dataset.import", &json!({"csv": "Title,Name\nX,y"})).is_err(), "no variable named Title");
|
||||
|
||||
let out = dir.file("sets.csv");
|
||||
s.execute("dataset.export", &json!({"path": out})).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user