dtcat.rs (9722B)
1 use assert_cmd::Command; 2 use predicates::prelude::*; 3 use std::io::Write; 4 use tempfile::NamedTempFile; 5 6 fn dtcat() -> Command { 7 Command::cargo_bin("dtcat").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 // ─── Basic viewing ─── 18 19 #[test] 20 fn shows_csv_data() { 21 let f = csv_file("name,value\nAlice,100\nBob,200\n"); 22 dtcat().arg(f.path()).assert().success() 23 .stdout(predicate::str::contains("Alice")) 24 .stdout(predicate::str::contains("Bob")); 25 } 26 27 #[test] 28 fn header_only_csv() { 29 let f = csv_file("name,value\n"); 30 dtcat().arg(f.path()).assert().success() 31 .stdout(predicate::str::contains("no data rows")); 32 } 33 34 #[test] 35 fn nonexistent_file_exits_1() { 36 dtcat().arg("/tmp/does_not_exist_12345.csv").assert().failure(); 37 } 38 39 // ─── Modes ─── 40 41 #[test] 42 fn schema_flag() { 43 let f = csv_file("name,value\nAlice,100\n"); 44 dtcat().arg(f.path()).arg("--schema").assert().success() 45 .stdout(predicate::str::contains("Column")) 46 .stdout(predicate::str::contains("Type")); 47 } 48 49 #[test] 50 fn describe_flag() { 51 let f = csv_file("name,value\nAlice,100\nBob,200\n"); 52 dtcat().arg(f.path()).arg("--describe").assert().success() 53 .stdout(predicate::str::contains("count")) 54 .stdout(predicate::str::contains("mean")); 55 } 56 57 #[test] 58 fn info_flag() { 59 let f = csv_file("name,value\nAlice,100\n"); 60 dtcat().arg(f.path()).arg("--info").assert().success() 61 .stdout(predicate::str::contains("File:")); 62 } 63 64 // ─── Row windowing ─── 65 66 #[test] 67 fn head_flag() { 68 let f = csv_file("x\n1\n2\n3\n4\n5\n"); 69 dtcat().arg(f.path()).arg("--head").arg("2").assert().success() 70 .stdout(predicate::str::contains("1")) 71 .stdout(predicate::str::contains("2")) 72 .stdout(predicate::str::contains("3").not()); 73 } 74 75 #[test] 76 fn tail_flag() { 77 let f = csv_file("x\n1\n2\n3\n4\n5\n"); 78 dtcat().arg(f.path()).arg("--tail").arg("2").assert().success() 79 .stdout(predicate::str::contains("4")) 80 .stdout(predicate::str::contains("5")); 81 } 82 83 #[test] 84 fn head_and_tail_combined() { 85 let f = csv_file("x\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n"); 86 dtcat().arg(f.path()).arg("--head").arg("2").arg("--tail").arg("2") 87 .assert().success() 88 .stdout(predicate::str::contains("1")) 89 .stdout(predicate::str::contains("2")) 90 .stdout(predicate::str::contains("9")) 91 .stdout(predicate::str::contains("10")); 92 } 93 94 // ─── Output format ─── 95 96 #[test] 97 fn csv_output_flag() { 98 let f = csv_file("name,value\nAlice,100\n"); 99 dtcat().arg(f.path()).arg("--csv").assert().success() 100 .stdout(predicate::str::contains("name,value")); 101 } 102 103 // ─── Format detection ─── 104 105 #[test] 106 fn format_override() { 107 let mut f = NamedTempFile::with_suffix(".txt").unwrap(); 108 write!(f, "a,b\n1,2\n").unwrap(); 109 f.flush().unwrap(); 110 dtcat().arg(f.path()).arg("--format").arg("csv").assert().success() 111 .stdout(predicate::str::contains("1")); 112 } 113 114 #[test] 115 fn tsv_detection() { 116 let mut f = NamedTempFile::with_suffix(".tsv").unwrap(); 117 write!(f, "name\tvalue\nAlice\t100\n").unwrap(); 118 f.flush().unwrap(); 119 dtcat().arg(f.path()).assert().success() 120 .stdout(predicate::str::contains("Alice")) 121 .stdout(predicate::str::contains("100")); 122 } 123 124 // ─── Skip rows ─── 125 126 #[test] 127 fn skip_metadata_rows() { 128 let f = csv_file("meta1\nmeta2\nname,value\nAlice,100\n"); 129 dtcat().arg(f.path()).arg("--skip").arg("2").assert().success() 130 .stdout(predicate::str::contains("Alice")); 131 } 132 133 // ─── All flag ─── 134 135 #[test] 136 fn all_flag_shows_every_row() { 137 // 60 rows > threshold of 50, so without --all we'd get head+tail 138 let mut content = String::from("x\n"); 139 for i in 1..=60 { 140 content.push_str(&format!("{}\n", i)); 141 } 142 let f = csv_file(&content); 143 // With --all, row 30 should appear (it would be omitted in head25+tail25) 144 dtcat().arg(f.path()).arg("--all").assert().success() 145 .stdout(predicate::str::contains("| 30 ")); 146 } 147 148 // ─── Sample ─── 149 150 #[test] 151 fn sample_returns_n_rows() { 152 let out = dtcat().arg("demo/sales.csv").arg("--sample").arg("5").arg("--csv") 153 .assert().success(); 154 let stdout = String::from_utf8(out.get_output().stdout.clone()).unwrap(); 155 let lines: Vec<&str> = stdout.trim().lines().collect(); 156 assert_eq!(lines.len(), 6, "expected header + 5 rows, got {}", lines.len()); 157 } 158 159 #[test] 160 fn sample_ge_total_returns_all() { 161 let f = csv_file("x\n1\n2\n3\n"); 162 dtcat().arg(f.path()).arg("--sample").arg("100").arg("--csv") 163 .assert().success(); 164 } 165 166 #[test] 167 fn sample_conflicts_with_head() { 168 let f = csv_file("x\n1\n"); 169 dtcat().arg(f.path()).arg("--sample").arg("1").arg("--head").arg("1") 170 .assert().code(2); 171 } 172 173 #[test] 174 fn sample_conflicts_with_tail() { 175 let f = csv_file("x\n1\n"); 176 dtcat().arg(f.path()).arg("--sample").arg("1").arg("--tail").arg("1") 177 .assert().code(2); 178 } 179 180 #[test] 181 fn sample_conflicts_with_all() { 182 let f = csv_file("x\n1\n"); 183 dtcat().arg(f.path()).arg("--sample").arg("1").arg("--all") 184 .assert().code(2); 185 } 186 187 #[test] 188 fn sample_conflicts_with_schema() { 189 let f = csv_file("x\n1\n"); 190 dtcat().arg(f.path()).arg("--sample").arg("1").arg("--schema") 191 .assert().code(2); 192 } 193 194 #[test] 195 fn sample_conflicts_with_describe() { 196 let f = csv_file("x\n1\n"); 197 dtcat().arg(f.path()).arg("--sample").arg("1").arg("--describe") 198 .assert().code(2); 199 } 200 201 #[test] 202 fn sample_conflicts_with_info() { 203 let f = csv_file("x\n1\n"); 204 dtcat().arg(f.path()).arg("--sample").arg("1").arg("--info") 205 .assert().code(2); 206 } 207 208 // ─── Parquet ─── 209 210 #[test] 211 fn parquet_view() { 212 dtcat().arg("tests/fixtures/data.parquet").assert().success() 213 .stdout(predicate::str::contains("Alice")) 214 .stdout(predicate::str::contains("Charlie")); 215 } 216 217 #[test] 218 fn parquet_schema() { 219 dtcat().arg("tests/fixtures/data.parquet").arg("--schema").assert().success() 220 .stdout(predicate::str::contains("name")) 221 .stdout(predicate::str::contains("value")); 222 } 223 224 // ─── Arrow/IPC ─── 225 226 #[test] 227 fn arrow_view() { 228 dtcat().arg("tests/fixtures/data.arrow").assert().success() 229 .stdout(predicate::str::contains("Alice")) 230 .stdout(predicate::str::contains("Charlie")); 231 } 232 233 #[test] 234 fn arrow_schema() { 235 dtcat().arg("tests/fixtures/data.arrow").arg("--schema").assert().success() 236 .stdout(predicate::str::contains("name")) 237 .stdout(predicate::str::contains("value")); 238 } 239 240 // ─── JSON ─── 241 242 #[test] 243 fn json_view() { 244 dtcat().arg("tests/fixtures/data.json").assert().success() 245 .stdout(predicate::str::contains("Alice")) 246 .stdout(predicate::str::contains("Charlie")); 247 } 248 249 // ─── NDJSON ─── 250 251 #[test] 252 fn ndjson_view() { 253 dtcat().arg("tests/fixtures/data.ndjson").assert().success() 254 .stdout(predicate::str::contains("Alice")) 255 .stdout(predicate::str::contains("Charlie")); 256 } 257 258 // ─── Excel ─── 259 260 #[test] 261 fn excel_view() { 262 dtcat().arg("demo/sales.xlsx").assert().success() 263 .stdout(predicate::str::contains("Revenue")); 264 } 265 266 #[test] 267 fn excel_schema() { 268 dtcat().arg("demo/sales.xlsx").arg("--schema").assert().success() 269 .stdout(predicate::str::contains("Column")) 270 .stdout(predicate::str::contains("Revenue")); 271 } 272 273 #[test] 274 fn excel_info() { 275 dtcat().arg("demo/sales.xlsx").arg("--info").assert().success() 276 .stdout(predicate::str::contains("Excel")) 277 .stdout(predicate::str::contains("Sheet1")); 278 } 279 280 // ─── Convert ─── 281 282 #[test] 283 fn convert_csv_to_parquet() { 284 let out = NamedTempFile::with_suffix(".parquet").unwrap(); 285 dtcat().arg("tests/fixtures/data.csv") 286 .arg("--convert").arg("parquet") 287 .arg("-o").arg(out.path()) 288 .assert().success(); 289 dtcat().arg(out.path()).arg("--csv") 290 .assert().success() 291 .stdout(predicate::str::contains("Alice")) 292 .stdout(predicate::str::contains("Charlie")); 293 } 294 295 #[test] 296 fn convert_parquet_to_csv_file() { 297 let out = NamedTempFile::with_suffix(".csv").unwrap(); 298 dtcat().arg("tests/fixtures/data.parquet") 299 .arg("--convert").arg("csv") 300 .arg("-o").arg(out.path()) 301 .assert().success(); 302 dtcat().arg(out.path()) 303 .assert().success() 304 .stdout(predicate::str::contains("Alice")); 305 } 306 307 #[test] 308 fn convert_csv_to_json_stdout() { 309 dtcat().arg("tests/fixtures/data.csv") 310 .arg("--convert").arg("json") 311 .assert().success() 312 .stdout(predicate::str::contains("Alice")); 313 } 314 315 #[test] 316 fn convert_csv_to_ndjson_stdout() { 317 dtcat().arg("tests/fixtures/data.csv") 318 .arg("--convert").arg("ndjson") 319 .assert().success() 320 .stdout(predicate::str::contains("Alice")); 321 } 322 323 #[test] 324 fn convert_parquet_no_output_errors() { 325 dtcat().arg("tests/fixtures/data.csv") 326 .arg("--convert").arg("parquet") 327 .assert().failure(); 328 } 329 330 #[test] 331 fn convert_arrow_no_output_errors() { 332 dtcat().arg("tests/fixtures/data.csv") 333 .arg("--convert").arg("arrow") 334 .assert().failure(); 335 } 336 337 #[test] 338 fn convert_conflicts_with_schema() { 339 let f = csv_file("x\n1\n"); 340 dtcat().arg(f.path()).arg("--convert").arg("csv").arg("--schema") 341 .assert().code(2); 342 } 343 344 #[test] 345 fn convert_with_skip() { 346 let f = csv_file("meta\nname,value\nAlice,100\n"); 347 dtcat().arg(f.path()).arg("--skip").arg("1").arg("--convert").arg("csv") 348 .assert().success() 349 .stdout(predicate::str::contains("Alice")); 350 }