xl-cli-tools

CLI tools for viewing and editing Excel files
Log | Files | Refs | README | LICENSE

test_xlfilter.rs (9248B)


      1 mod common;
      2 
      3 use assert_cmd::Command;
      4 use predicates::prelude::*;
      5 use tempfile::TempDir;
      6 
      7 fn xlfilter() -> Command {
      8     Command::cargo_bin("xlfilter").unwrap()
      9 }
     10 
     11 fn setup() -> (TempDir, std::path::PathBuf) {
     12     let dir = TempDir::new().unwrap();
     13     let path = dir.path().join("test.xlsx");
     14     common::create_filterable(&path);
     15     (dir, path)
     16 }
     17 
     18 // === Basic functionality ===
     19 
     20 #[test]
     21 fn no_flags_shows_all_rows() {
     22     let (_dir, path) = setup();
     23     xlfilter()
     24         .arg(&path)
     25         .assert()
     26         .success()
     27         .stdout(predicate::str::contains("CA"))
     28         .stdout(predicate::str::contains("NY"))
     29         .stdout(predicate::str::contains("TX"))
     30         .stdout(predicate::str::contains("FL"))
     31         .stderr(predicate::str::contains("6 rows"));
     32 }
     33 
     34 #[test]
     35 fn where_eq_string() {
     36     let (_dir, path) = setup();
     37     xlfilter()
     38         .arg(&path)
     39         .args(["--where", "State=CA"])
     40         .assert()
     41         .success()
     42         .stdout(predicate::str::contains("Los Angeles"))
     43         .stdout(predicate::str::contains("San Francisco"))
     44         .stdout(predicate::str::contains("NY").not())
     45         .stderr(predicate::str::contains("2 rows"));
     46 }
     47 
     48 #[test]
     49 fn where_gt_numeric() {
     50     let (_dir, path) = setup();
     51     xlfilter()
     52         .arg(&path)
     53         .args(["--where", "Amount>1500"])
     54         .assert()
     55         .success()
     56         .stdout(predicate::str::contains("New York"))
     57         .stdout(predicate::str::contains("Miami"))
     58         .stderr(predicate::str::contains("2 rows"));
     59 }
     60 
     61 #[test]
     62 fn where_multiple_and() {
     63     let (_dir, path) = setup();
     64     xlfilter()
     65         .arg(&path)
     66         .args(["--where", "State=CA", "--where", "Amount>1000"])
     67         .assert()
     68         .success()
     69         .stdout(predicate::str::contains("Los Angeles"))
     70         .stdout(predicate::str::contains("San Francisco").not())
     71         .stderr(predicate::str::contains("1 rows"));
     72 }
     73 
     74 #[test]
     75 fn where_not_eq() {
     76     let (_dir, path) = setup();
     77     xlfilter()
     78         .arg(&path)
     79         .args(["--where", "Status!=Draft"])
     80         .assert()
     81         .success()
     82         .stderr(predicate::str::contains("4 rows"));
     83 }
     84 
     85 #[test]
     86 fn where_contains() {
     87     let (_dir, path) = setup();
     88     xlfilter()
     89         .arg(&path)
     90         .args(["--where", "City~angel"])
     91         .assert()
     92         .success()
     93         .stdout(predicate::str::contains("Los Angeles"))
     94         .stderr(predicate::str::contains("1 rows"));
     95 }
     96 
     97 #[test]
     98 fn where_not_contains() {
     99     let (_dir, path) = setup();
    100     xlfilter()
    101         .arg(&path)
    102         .args(["--where", "Status!~raft"])
    103         .assert()
    104         .success()
    105         .stderr(predicate::str::contains("4 rows"));
    106 }
    107 
    108 #[test]
    109 fn where_no_matches() {
    110     let (_dir, path) = setup();
    111     xlfilter()
    112         .arg(&path)
    113         .args(["--where", "State=ZZ"])
    114         .assert()
    115         .success()
    116         .stderr(predicate::str::contains("0 rows"));
    117 }
    118 
    119 // === Column selection ===
    120 
    121 #[test]
    122 fn cols_by_name() {
    123     let (_dir, path) = setup();
    124     xlfilter()
    125         .arg(&path)
    126         .args(["--cols", "State,Amount"])
    127         .assert()
    128         .success()
    129         .stdout(predicate::str::contains("State"))
    130         .stdout(predicate::str::contains("Amount"))
    131         .stdout(predicate::str::contains("City").not())
    132         .stdout(predicate::str::contains("Year").not());
    133 }
    134 
    135 #[test]
    136 fn cols_by_letter() {
    137     let (_dir, path) = setup();
    138     xlfilter()
    139         .arg(&path)
    140         .args(["--cols", "A,C"])
    141         .assert()
    142         .success()
    143         .stdout(predicate::str::contains("State"))
    144         .stdout(predicate::str::contains("Amount"))
    145         .stdout(predicate::str::contains("City").not());
    146 }
    147 
    148 #[test]
    149 fn cols_mixed_letter_and_name() {
    150     let (_dir, path) = setup();
    151     xlfilter()
    152         .arg(&path)
    153         .args(["--cols", "A,Amount"])
    154         .assert()
    155         .success()
    156         .stdout(predicate::str::contains("State"))
    157         .stdout(predicate::str::contains("Amount"))
    158         .stdout(predicate::str::contains("City").not());
    159 }
    160 
    161 // === Sort ===
    162 
    163 #[test]
    164 fn sort_desc() {
    165     let (_dir, path) = setup();
    166     xlfilter()
    167         .arg(&path)
    168         .args(["--sort", "Amount:desc", "--cols", "City,Amount"])
    169         .assert()
    170         .success()
    171         .stdout(predicate::str::contains("Miami")); // 3000 = highest
    172 }
    173 
    174 #[test]
    175 fn sort_asc() {
    176     let (_dir, path) = setup();
    177     xlfilter()
    178         .arg(&path)
    179         .args(["--sort", "Amount:asc", "--limit", "1", "--cols", "City,Amount"])
    180         .assert()
    181         .success()
    182         .stdout(predicate::str::contains("Albany")); // 500 = lowest
    183 }
    184 
    185 #[test]
    186 fn sort_by_column_letter() {
    187     let (_dir, path) = setup();
    188     // C = Amount column
    189     xlfilter()
    190         .arg(&path)
    191         .args(["--sort", "C:desc", "--limit", "1", "--cols", "City,Amount"])
    192         .assert()
    193         .success()
    194         .stdout(predicate::str::contains("Miami")); // 3000 = highest
    195 }
    196 
    197 // === Limit, head, tail ===
    198 
    199 #[test]
    200 fn limit_caps_output() {
    201     let (_dir, path) = setup();
    202     xlfilter()
    203         .arg(&path)
    204         .args(["--limit", "3"])
    205         .assert()
    206         .success()
    207         .stderr(predicate::str::contains("3 rows"));
    208 }
    209 
    210 #[test]
    211 fn head_before_filter() {
    212     let (_dir, path) = setup();
    213     // First 3 rows: CA/LA, NY/NYC, CA/SF
    214     // Filter State=NY → only NYC
    215     xlfilter()
    216         .arg(&path)
    217         .args(["--head", "3", "--where", "State=NY"])
    218         .assert()
    219         .success()
    220         .stderr(predicate::str::contains("1 rows"));
    221 }
    222 
    223 #[test]
    224 fn tail_before_filter() {
    225     let (_dir, path) = setup();
    226     // Last 3 rows: TX/Houston, NY/Albany, FL/Miami
    227     // Filter State=NY → only Albany
    228     xlfilter()
    229         .arg(&path)
    230         .args(["--tail", "3", "--where", "State=NY"])
    231         .assert()
    232         .success()
    233         .stderr(predicate::str::contains("1 rows"));
    234 }
    235 
    236 #[test]
    237 fn head_and_tail_mutually_exclusive() {
    238     let (_dir, path) = setup();
    239     xlfilter()
    240         .arg(&path)
    241         .args(["--head", "3", "--tail", "3"])
    242         .assert()
    243         .failure()
    244         .stderr(predicate::str::contains("mutually exclusive"));
    245 }
    246 
    247 // === CSV output ===
    248 
    249 #[test]
    250 fn csv_output() {
    251     let (_dir, path) = setup();
    252     xlfilter()
    253         .arg(&path)
    254         .args(["--csv", "--where", "State=CA"])
    255         .assert()
    256         .success()
    257         .stdout(predicate::str::contains(",")) // CSV has commas
    258         .stdout(predicate::str::contains("|").not()); // no markdown pipes
    259 }
    260 
    261 // === Sheet selection ===
    262 
    263 #[test]
    264 fn sheet_by_name() {
    265     let (_dir, path) = setup();
    266     xlfilter()
    267         .arg(&path)
    268         .args(["--sheet", "Data"])
    269         .assert()
    270         .success();
    271 }
    272 
    273 #[test]
    274 fn sheet_not_found() {
    275     let (_dir, path) = setup();
    276     xlfilter()
    277         .arg(&path)
    278         .args(["--sheet", "NoSuchSheet"])
    279         .assert()
    280         .failure()
    281         .stderr(predicate::str::contains("not found"));
    282 }
    283 
    284 // === Error cases ===
    285 
    286 #[test]
    287 fn file_not_found() {
    288     xlfilter()
    289         .arg("nonexistent.xlsx")
    290         .assert()
    291         .failure()
    292         .stderr(predicate::str::contains("not found"));
    293 }
    294 
    295 #[test]
    296 fn bad_filter_expr() {
    297     let (_dir, path) = setup();
    298     xlfilter()
    299         .arg(&path)
    300         .args(["--where", "NoOperator"])
    301         .assert()
    302         .failure()
    303         .stderr(predicate::str::contains("no operator found"));
    304 }
    305 
    306 #[test]
    307 fn bad_sort_dir() {
    308     let (_dir, path) = setup();
    309     xlfilter()
    310         .arg(&path)
    311         .args(["--sort", "Amount:up"])
    312         .assert()
    313         .failure()
    314         .stderr(predicate::str::contains("invalid sort direction"));
    315 }
    316 
    317 #[test]
    318 fn unknown_column_in_where() {
    319     let (_dir, path) = setup();
    320     xlfilter()
    321         .arg(&path)
    322         .args(["--where", "Foo=bar"])
    323         .assert()
    324         .failure()
    325         .stderr(predicate::str::contains("not found"));
    326 }
    327 
    328 #[test]
    329 fn unknown_column_in_cols() {
    330     let (_dir, path) = setup();
    331     xlfilter()
    332         .arg(&path)
    333         .args(["--cols", "State,Foo"])
    334         .assert()
    335         .failure()
    336         .stderr(predicate::str::contains("not found"));
    337 }
    338 
    339 #[test]
    340 fn gt_with_non_numeric_value() {
    341     let (_dir, path) = setup();
    342     xlfilter()
    343         .arg(&path)
    344         .args(["--where", "Amount>abc"])
    345         .assert()
    346         .failure()
    347         .stderr(predicate::str::contains("numeric value"));
    348 }
    349 
    350 // === Skip rows ===
    351 
    352 #[test]
    353 fn skip_metadata_rows() {
    354     let dir = TempDir::new().unwrap();
    355     let path = dir.path().join("meta.xlsx");
    356     common::create_with_metadata(&path);
    357 
    358     xlfilter()
    359         .arg(&path)
    360         .args(["--skip", "2"])
    361         .assert()
    362         .success()
    363         .stdout(predicate::str::contains("Name"))
    364         .stdout(predicate::str::contains("Alice"))
    365         .stdout(predicate::str::contains("Bob"))
    366         .stdout(predicate::str::contains("Quarterly Report").not())
    367         .stderr(predicate::str::contains("2 rows"));
    368 }
    369 
    370 #[test]
    371 fn skip_with_filter() {
    372     let dir = TempDir::new().unwrap();
    373     let path = dir.path().join("meta.xlsx");
    374     common::create_with_metadata(&path);
    375 
    376     xlfilter()
    377         .arg(&path)
    378         .args(["--skip", "2", "--where", "Name=Alice"])
    379         .assert()
    380         .success()
    381         .stdout(predicate::str::contains("Alice"))
    382         .stdout(predicate::str::contains("Bob").not())
    383         .stderr(predicate::str::contains("1 rows"));
    384 }