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 }