dt-cli-tools

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

dtfilter.rs (7247B)


      1 use assert_cmd::Command;
      2 use predicates::prelude::*;
      3 use std::io::Write;
      4 use tempfile::NamedTempFile;
      5 
      6 fn dtfilter() -> Command {
      7     Command::cargo_bin("dtfilter").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 const DATA: &str = "name,value\nAlice,100\nBob,200\nCharlie,300\n";
     18 
     19 // ─── Equality ───
     20 
     21 #[test]
     22 fn filter_eq() {
     23     let f = csv_file(DATA);
     24     dtfilter().arg(f.path()).arg("--filter").arg("name=Alice").assert().success()
     25         .stdout(predicate::str::contains("Alice"))
     26         .stdout(predicate::str::contains("Bob").not());
     27 }
     28 
     29 #[test]
     30 fn filter_neq() {
     31     let f = csv_file(DATA);
     32     dtfilter().arg(f.path()).arg("--filter").arg("name!=Alice").assert().success()
     33         .stdout(predicate::str::contains("Bob"))
     34         .stdout(predicate::str::contains("Charlie"))
     35         .stdout(predicate::str::contains("Alice").not());
     36 }
     37 
     38 // ─── Numeric comparisons ───
     39 
     40 #[test]
     41 fn filter_gt() {
     42     let f = csv_file(DATA);
     43     dtfilter().arg(f.path()).arg("--filter").arg("value>150").assert().success()
     44         .stdout(predicate::str::contains("Bob"))
     45         .stdout(predicate::str::contains("Charlie"))
     46         .stdout(predicate::str::contains("Alice").not());
     47 }
     48 
     49 #[test]
     50 fn filter_lt() {
     51     let f = csv_file(DATA);
     52     dtfilter().arg(f.path()).arg("--filter").arg("value<200").assert().success()
     53         .stdout(predicate::str::contains("Alice"))
     54         .stdout(predicate::str::contains("Bob").not());
     55 }
     56 
     57 #[test]
     58 fn filter_gte() {
     59     let f = csv_file(DATA);
     60     dtfilter().arg(f.path()).arg("--filter").arg("value>=200").assert().success()
     61         .stdout(predicate::str::contains("Bob"))
     62         .stdout(predicate::str::contains("Charlie"))
     63         .stdout(predicate::str::contains("Alice").not());
     64 }
     65 
     66 #[test]
     67 fn filter_lte() {
     68     let f = csv_file(DATA);
     69     dtfilter().arg(f.path()).arg("--filter").arg("value<=200").assert().success()
     70         .stdout(predicate::str::contains("Alice"))
     71         .stdout(predicate::str::contains("Bob"))
     72         .stdout(predicate::str::contains("Charlie").not());
     73 }
     74 
     75 // ─── String matching ───
     76 
     77 #[test]
     78 fn filter_contains() {
     79     let f = csv_file(DATA);
     80     dtfilter().arg(f.path()).arg("--filter").arg("name~ob").assert().success()
     81         .stdout(predicate::str::contains("Bob"))
     82         .stdout(predicate::str::contains("Alice").not());
     83 }
     84 
     85 #[test]
     86 fn filter_not_contains() {
     87     let f = csv_file(DATA);
     88     dtfilter().arg(f.path()).arg("--filter").arg("name!~ob").assert().success()
     89         .stdout(predicate::str::contains("Alice"))
     90         .stdout(predicate::str::contains("Charlie"))
     91         .stdout(predicate::str::contains("Bob").not());
     92 }
     93 
     94 // ─── Multiple filters (AND) ───
     95 
     96 #[test]
     97 fn multiple_filters_and() {
     98     let f = csv_file(DATA);
     99     dtfilter().arg(f.path())
    100         .arg("--filter").arg("value>=200")
    101         .arg("--filter").arg("value<=300")
    102         .assert().success()
    103         .stdout(predicate::str::contains("Bob"))
    104         .stdout(predicate::str::contains("Charlie"))
    105         .stdout(predicate::str::contains("Alice").not());
    106 }
    107 
    108 // ─── Sort ───
    109 
    110 #[test]
    111 fn sort_desc() {
    112     let f = csv_file(DATA);
    113     let out = dtfilter().arg(f.path()).arg("--sort").arg("value:desc")
    114         .assert().success();
    115     let stdout = String::from_utf8(out.get_output().stdout.clone()).unwrap();
    116     let charlie_pos = stdout.find("Charlie").unwrap();
    117     let alice_pos = stdout.find("Alice").unwrap();
    118     assert!(charlie_pos < alice_pos, "Charlie (300) should appear before Alice (100) in desc sort");
    119 }
    120 
    121 #[test]
    122 fn sort_asc() {
    123     let f = csv_file(DATA);
    124     let out = dtfilter().arg(f.path()).arg("--sort").arg("value")
    125         .assert().success();
    126     let stdout = String::from_utf8(out.get_output().stdout.clone()).unwrap();
    127     let alice_pos = stdout.find("Alice").unwrap();
    128     let charlie_pos = stdout.find("Charlie").unwrap();
    129     assert!(alice_pos < charlie_pos, "Alice (100) should appear before Charlie (300) in asc sort");
    130 }
    131 
    132 // ─── Column selection ───
    133 
    134 #[test]
    135 fn columns_select() {
    136     let f = csv_file("name,value,extra\nAlice,100,x\n");
    137     dtfilter().arg(f.path()).arg("--columns").arg("name,value").assert().success()
    138         .stdout(predicate::str::contains("name"))
    139         .stdout(predicate::str::contains("extra").not());
    140 }
    141 
    142 // ─── Limit ───
    143 
    144 #[test]
    145 fn limit_output() {
    146     let f = csv_file(DATA);
    147     dtfilter().arg(f.path()).arg("--sort").arg("value:desc").arg("--limit").arg("1")
    148         .assert().success()
    149         .stdout(predicate::str::contains("Charlie"))
    150         .stdout(predicate::str::contains("Alice").not());
    151 }
    152 
    153 // ─── Output format ───
    154 
    155 #[test]
    156 fn csv_output() {
    157     let f = csv_file("name,value\nAlice,100\n");
    158     dtfilter().arg(f.path()).arg("--csv").assert().success()
    159         .stdout(predicate::str::contains("name,value"));
    160 }
    161 
    162 // ─── Windowing ───
    163 
    164 #[test]
    165 fn head_before_filter() {
    166     let f = csv_file("name,value\nAlice,100\nBob,200\nCharlie,300\n");
    167     dtfilter().arg(f.path()).arg("--head").arg("2").arg("--filter").arg("value>150")
    168         .assert().success()
    169         .stdout(predicate::str::contains("Bob"))
    170         .stdout(predicate::str::contains("Charlie").not());
    171 }
    172 
    173 #[test]
    174 fn head_tail_exclusive() {
    175     let f = csv_file("x\n1\n2\n");
    176     dtfilter().arg(f.path()).arg("--head").arg("1").arg("--tail").arg("1")
    177         .assert().code(2);
    178 }
    179 
    180 // ─── Excel ───
    181 
    182 #[test]
    183 fn filter_excel() {
    184     dtfilter().arg("demo/sales.xlsx").arg("--filter").arg("Region=East")
    185         .assert().success()
    186         .stdout(predicate::str::contains("East"))
    187         .stdout(predicate::str::contains("West").not());
    188 }
    189 
    190 // ─── Parquet ───
    191 
    192 #[test]
    193 fn filter_parquet() {
    194     dtfilter().arg("tests/fixtures/data.parquet").arg("--filter").arg("value>150")
    195         .assert().success()
    196         .stdout(predicate::str::contains("Bob"))
    197         .stdout(predicate::str::contains("Charlie"))
    198         .stdout(predicate::str::contains("Alice").not());
    199 }
    200 
    201 // ─── Arrow/IPC ───
    202 
    203 #[test]
    204 fn filter_arrow() {
    205     dtfilter().arg("tests/fixtures/data.arrow").arg("--filter").arg("value>150")
    206         .assert().success()
    207         .stdout(predicate::str::contains("Bob"))
    208         .stdout(predicate::str::contains("Charlie"))
    209         .stdout(predicate::str::contains("Alice").not());
    210 }
    211 
    212 // ─── JSON ───
    213 
    214 #[test]
    215 fn filter_json() {
    216     dtfilter().arg("tests/fixtures/data.json").arg("--filter").arg("name=Alice")
    217         .assert().success()
    218         .stdout(predicate::str::contains("Alice"))
    219         .stdout(predicate::str::contains("Bob").not());
    220 }
    221 
    222 // ─── NDJSON ───
    223 
    224 #[test]
    225 fn filter_ndjson() {
    226     dtfilter().arg("tests/fixtures/data.ndjson").arg("--filter").arg("name=Alice")
    227         .assert().success()
    228         .stdout(predicate::str::contains("Alice"))
    229         .stdout(predicate::str::contains("Bob").not());
    230 }
    231 
    232 // ─── Edge cases ───
    233 
    234 #[test]
    235 fn filter_no_matches() {
    236     let f = csv_file(DATA);
    237     dtfilter().arg(f.path()).arg("--filter").arg("name=Nobody")
    238         .assert().success()
    239         .stderr(predicate::str::contains("0 rows"));
    240 }