dt-cli-tools

CLI tools for viewing, filtering, and comparing tabular data files
Log | Files | Refs | README | LICENSE

dtdiff.rs (6176B)


      1 use assert_cmd::Command;
      2 use predicates::prelude::*;
      3 use std::io::Write;
      4 use tempfile::NamedTempFile;
      5 
      6 fn dtdiff() -> Command {
      7     Command::cargo_bin("dtdiff").unwrap()
      8 }
      9 
     10 fn csv_file(content: &str) -> NamedTempFile {
     11     let mut f = NamedTempFile::with_suffix(".csv").unwrap();
     12     write!(f, "{}", content).unwrap();
     13     f.flush().unwrap();
     14     f
     15 }
     16 
     17 // ─── Positional mode ───
     18 
     19 #[test]
     20 fn no_diff_exits_0() {
     21     let a = csv_file("name,value\nAlice,100\n");
     22     let b = csv_file("name,value\nAlice,100\n");
     23     dtdiff().arg(a.path()).arg(b.path()).assert().success()
     24         .stdout(predicate::str::contains("No differences"));
     25 }
     26 
     27 #[test]
     28 fn positional_diff_exits_1() {
     29     let a = csv_file("name,value\nAlice,100\n");
     30     let b = csv_file("name,value\nBob,200\n");
     31     dtdiff().arg(a.path()).arg(b.path()).assert().code(1);
     32 }
     33 
     34 #[test]
     35 fn positional_added_row() {
     36     let a = csv_file("name,value\nAlice,100\n");
     37     let b = csv_file("name,value\nAlice,100\nBob,200\n");
     38     dtdiff().arg(a.path()).arg(b.path()).assert().code(1)
     39         .stdout(predicate::str::contains("Added: 1"));
     40 }
     41 
     42 #[test]
     43 fn positional_removed_row() {
     44     let a = csv_file("name,value\nAlice,100\nBob,200\n");
     45     let b = csv_file("name,value\nAlice,100\n");
     46     dtdiff().arg(a.path()).arg(b.path()).assert().code(1)
     47         .stdout(predicate::str::contains("Removed: 1"));
     48 }
     49 
     50 // ─── Key-based mode ───
     51 
     52 #[test]
     53 fn keyed_diff_modified() {
     54     let a = csv_file("id,name\n1,Alice\n2,Bob\n");
     55     let b = csv_file("id,name\n1,Alice\n2,Robert\n");
     56     dtdiff().arg(a.path()).arg(b.path()).arg("--key").arg("id")
     57         .assert().code(1)
     58         .stdout(predicate::str::contains("Modified: 1"))
     59         .stdout(predicate::str::contains("Bob"));
     60 }
     61 
     62 #[test]
     63 fn keyed_diff_added_and_removed() {
     64     let a = csv_file("id,name\n1,Alice\n2,Bob\n");
     65     let b = csv_file("id,name\n1,Alice\n3,Charlie\n");
     66     dtdiff().arg(a.path()).arg(b.path()).arg("--key").arg("id")
     67         .assert().code(1)
     68         .stdout(predicate::str::contains("Added: 1"))
     69         .stdout(predicate::str::contains("Removed: 1"));
     70 }
     71 
     72 #[test]
     73 fn keyed_no_diff() {
     74     let a = csv_file("id,name\n1,Alice\n2,Bob\n");
     75     let b = csv_file("id,name\n2,Bob\n1,Alice\n");
     76     dtdiff().arg(a.path()).arg(b.path()).arg("--key").arg("id")
     77         .assert().success()
     78         .stdout(predicate::str::contains("No differences"));
     79 }
     80 
     81 // ─── Composite keys ───
     82 
     83 #[test]
     84 fn composite_key() {
     85     let a = csv_file("date,ticker,price\n2024-01-01,AAPL,150\n2024-01-01,GOOG,140\n");
     86     let b = csv_file("date,ticker,price\n2024-01-01,AAPL,150\n2024-01-01,GOOG,145\n");
     87     dtdiff().arg(a.path()).arg(b.path()).arg("--key").arg("date,ticker")
     88         .assert().code(1)
     89         .stdout(predicate::str::contains("Modified: 1"))
     90         .stdout(predicate::str::contains("GOOG"));
     91 }
     92 
     93 // ─── Float tolerance ───
     94 
     95 #[test]
     96 fn tolerance_suppresses_small_diff() {
     97     let a = csv_file("id,price\n1,150.000\n");
     98     let b = csv_file("id,price\n1,150.005\n");
     99     dtdiff().arg(a.path()).arg(b.path()).arg("--key").arg("id").arg("--tolerance").arg("0.01")
    100         .assert().success()
    101         .stdout(predicate::str::contains("No differences"));
    102 }
    103 
    104 #[test]
    105 fn tolerance_reports_large_diff() {
    106     let a = csv_file("id,price\n1,150.0\n");
    107     let b = csv_file("id,price\n1,155.0\n");
    108     dtdiff().arg(a.path()).arg(b.path()).arg("--key").arg("id").arg("--tolerance").arg("0.01")
    109         .assert().code(1)
    110         .stdout(predicate::str::contains("Modified: 1"));
    111 }
    112 
    113 // ─── Parquet ───
    114 
    115 #[test]
    116 fn parquet_keyed_diff() {
    117     dtdiff().arg("tests/fixtures/old.parquet").arg("tests/fixtures/new.parquet")
    118         .arg("--key").arg("id")
    119         .assert().code(1)
    120         .stdout(predicate::str::contains("Added: 1"))
    121         .stdout(predicate::str::contains("Removed: 1"));
    122 }
    123 
    124 #[test]
    125 fn parquet_no_diff() {
    126     dtdiff().arg("tests/fixtures/data.parquet").arg("tests/fixtures/data.parquet")
    127         .assert().success()
    128         .stdout(predicate::str::contains("No differences"));
    129 }
    130 
    131 // ─── Arrow/IPC ───
    132 
    133 #[test]
    134 fn arrow_keyed_diff() {
    135     dtdiff().arg("tests/fixtures/old.arrow").arg("tests/fixtures/new.arrow")
    136         .arg("--key").arg("id")
    137         .assert().code(1)
    138         .stdout(predicate::str::contains("Added: 1"))
    139         .stdout(predicate::str::contains("Removed: 1"));
    140 }
    141 
    142 // ─── JSON ───
    143 
    144 #[test]
    145 fn json_keyed_diff() {
    146     dtdiff().arg("tests/fixtures/old.json").arg("tests/fixtures/new.json")
    147         .arg("--key").arg("id")
    148         .assert().code(1)
    149         .stdout(predicate::str::contains("Modified: 1"));
    150 }
    151 
    152 // ─── NDJSON ───
    153 
    154 #[test]
    155 fn ndjson_keyed_diff() {
    156     dtdiff().arg("tests/fixtures/old.ndjson").arg("tests/fixtures/new.ndjson")
    157         .arg("--key").arg("id")
    158         .assert().code(1)
    159         .stdout(predicate::str::contains("Modified: 1"));
    160 }
    161 
    162 // ─── Output formats ───
    163 
    164 #[test]
    165 fn json_output() {
    166     let a = csv_file("id,val\n1,a\n");
    167     let b = csv_file("id,val\n1,b\n");
    168     dtdiff().arg(a.path()).arg(b.path()).arg("--key").arg("id").arg("--json")
    169         .assert().code(1)
    170         .stdout(predicate::str::contains("\"modified\""));
    171 }
    172 
    173 #[test]
    174 fn csv_output() {
    175     let a = csv_file("id,val\n1,a\n");
    176     let b = csv_file("id,val\n1,b\n");
    177     dtdiff().arg(a.path()).arg(b.path()).arg("--key").arg("id").arg("--csv")
    178         .assert().code(1)
    179         .stdout(predicate::str::contains("_status"));
    180 }
    181 
    182 #[test]
    183 fn no_color_flag() {
    184     let a = csv_file("name,value\nAlice,100\n");
    185     let b = csv_file("name,value\nBob,200\n");
    186     dtdiff().arg(a.path()).arg(b.path()).arg("--no-color")
    187         .assert().code(1);
    188 }
    189 
    190 // ─── Excel ───
    191 
    192 #[test]
    193 fn excel_keyed_diff() {
    194     dtdiff().arg("demo/old.xlsx").arg("demo/new.xlsx").arg("--key").arg("ID")
    195         .assert().code(1)
    196         .stdout(predicate::str::contains("Added: 1"))
    197         .stdout(predicate::str::contains("Removed: 1"))
    198         .stdout(predicate::str::contains("Modified: 3"));
    199 }
    200 
    201 #[test]
    202 fn excel_no_diff() {
    203     dtdiff().arg("demo/old.xlsx").arg("demo/old.xlsx")
    204         .assert().success()
    205         .stdout(predicate::str::contains("No differences"));
    206 }