Coverage for src/csv_schema_validator/cli/tests/test_cli.py: 100%

218 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-12-23 15:34 +0100

1import pytest 

2import subprocess 

3import tempfile 

4import os 

5import json 

6from pathlib import Path 

7 

8 

9class TestCLI: 

10 """Test suite for the CLI interface using subprocess""" 

11 

12 @pytest.fixture 

13 def temp_dir(self): 

14 """Create a temporary directory for test files""" 

15 with tempfile.TemporaryDirectory() as tmpdir: 

16 yield tmpdir 

17 

18 @pytest.fixture 

19 def project_root(self): 

20 """Get the project root directory""" 

21 return Path(__file__).parent.parent.parent.parent.parent 

22 

23 @pytest.fixture 

24 def basic_schema(self, temp_dir): 

25 """Create a basic schema file for testing""" 

26 schema = { 

27 "name": "Test Schema", 

28 "description": "Basic test schema", 

29 "fields": [ 

30 { 

31 "name": "id", 

32 "type": "integer", 

33 "required": True, 

34 "description": "Unique identifier", 

35 }, 

36 { 

37 "name": "name", 

38 "type": "string", 

39 "required": True, 

40 "description": "Name field", 

41 }, 

42 { 

43 "name": "email", 

44 "type": "string", 

45 "required": True, 

46 "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", 

47 "description": "Email address", 

48 }, 

49 { 

50 "name": "department", 

51 "type": "string", 

52 "required": True, 

53 "enum": ["Engineering", "Marketing", "Sales"], 

54 "description": "Department", 

55 }, 

56 { 

57 "name": "salary", 

58 "type": "number", 

59 "required": True, 

60 "min": 30000, 

61 "max": 200000, 

62 "description": "Salary", 

63 }, 

64 { 

65 "name": "is_active", 

66 "type": "boolean", 

67 "required": True, 

68 "description": "Active status", 

69 }, 

70 ], 

71 } 

72 

73 schema_file = os.path.join(temp_dir, "schema.json") 

74 with open(schema_file, "w") as f: 

75 json.dump(schema, f, indent=2) 

76 return schema_file 

77 

78 @pytest.fixture 

79 def valid_csv(self, temp_dir): 

80 """Create a valid CSV file for testing""" 

81 csv_content = """id,name,email,department,salary,is_active 

821,John Doe,john.doe@company.com,Engineering,75000,true 

832,Jane Smith,jane.smith@company.com,Marketing,65000,false 

843,Bob Johnson,bob.johnson@company.com,Sales,55000,true""" 

85 

86 csv_file = os.path.join(temp_dir, "valid.csv") 

87 with open(csv_file, "w") as f: 

88 f.write(csv_content) 

89 return csv_file 

90 

91 @pytest.fixture 

92 def invalid_csv(self, temp_dir): 

93 """Create an invalid CSV file for testing""" 

94 csv_content = """id,name,email,department,salary,is_active 

951,John Doe,invalid-email,Engineering,75000,true 

962,Jane Smith,jane.smith@company.com,InvalidDept,65000,false 

973,invalid-id,Bob Johnson,bob@company.com,Sales,25000,maybe 

984,Alice Williams,alice@company.com,Marketing,300000,true""" 

99 

100 csv_file = os.path.join(temp_dir, "invalid.csv") 

101 with open(csv_file, "w") as f: 

102 f.write(csv_content) 

103 return csv_file 

104 

105 @pytest.fixture 

106 def empty_csv(self, temp_dir): 

107 """Create an empty CSV file for testing""" 

108 csv_file = os.path.join(temp_dir, "empty.csv") 

109 with open(csv_file, "w") as f: 

110 f.write("") 

111 return csv_file 

112 

113 @pytest.fixture 

114 def malformed_json_schema(self, temp_dir): 

115 """Create a malformed JSON schema file for testing""" 

116 schema_file = os.path.join(temp_dir, "malformed.json") 

117 with open(schema_file, "w") as f: 

118 f.write('{"name": "Test", "fields": [{"name": "id", "type": "integer"') # Missing closing braces 

119 return schema_file 

120 

121 def run_cli(self, csv_file, schema_file, project_root): 

122 """Helper method to run the CLI command""" 

123 import sys 

124 # Use absolute paths for the files 

125 abs_csv_file = os.path.abspath(csv_file) 

126 abs_schema_file = os.path.abspath(schema_file) 

127 

128 cmd = [ 

129 sys.executable, "-m", "csv_schema_validator.cli.cli", 

130 abs_csv_file, abs_schema_file 

131 ] 

132 return subprocess.run( 

133 cmd, 

134 capture_output=True, 

135 text=True, 

136 cwd=project_root, 

137 env={**os.environ, "PYTHONPATH": os.path.join(project_root, "src")} 

138 ) 

139 

140 # === SUCCESS CASES === 

141 

142 def test_cli_success_with_valid_files(self, valid_csv, basic_schema, project_root): 

143 """Test CLI with valid CSV and schema files""" 

144 result = self.run_cli(valid_csv, basic_schema, project_root) 

145 

146 assert result.returncode == 0 

147 assert "✅ Validation passed" in result.stdout 

148 assert "❌ Validation failed" not in result.stdout 

149 # Allow for RuntimeWarning about module import 

150 assert result.stderr == "" or "RuntimeWarning" in result.stderr 

151 

152 def test_cli_success_with_minimal_valid_data(self, temp_dir, basic_schema, project_root): 

153 """Test CLI with minimal valid data""" 

154 # Create CSV with just one valid row 

155 csv_content = """id,name,email,department,salary,is_active 

1561,John Doe,john.doe@company.com,Engineering,75000,true""" 

157 

158 csv_file = os.path.join(temp_dir, "minimal.csv") 

159 with open(csv_file, "w") as f: 

160 f.write(csv_content) 

161 

162 result = self.run_cli(csv_file, basic_schema, project_root) 

163 

164 assert result.returncode == 0 

165 assert "✅ Validation passed" in result.stdout 

166 

167 # === VALIDATION FAILURE CASES === 

168 

169 def test_cli_validation_failure_with_invalid_data(self, invalid_csv, basic_schema, project_root): 

170 """Test CLI with invalid CSV data""" 

171 result = self.run_cli(invalid_csv, basic_schema, project_root) 

172 

173 # The new exception system provides clear error messages instead of crashes 

174 assert result.returncode == 1 

175 assert "❌ Validation failed" in result.stdout 

176 assert "PatternValidationError" in result.stdout or "EnumValidationError" in result.stdout 

177 

178 def test_cli_validation_failure_with_empty_csv(self, empty_csv, basic_schema, project_root): 

179 """Test CLI with empty CSV file""" 

180 result = self.run_cli(empty_csv, basic_schema, project_root) 

181 

182 assert result.returncode == 1 # CLI now exits with error on validation failure 

183 assert "❌ Validation failed" in result.stdout 

184 assert "EmptyFileError" in result.stdout 

185 

186 # === FILE ERROR CASES === 

187 

188 def test_cli_missing_csv_file(self, basic_schema, project_root): 

189 """Test CLI with non-existent CSV file""" 

190 result = self.run_cli("nonexistent.csv", basic_schema, project_root) 

191 

192 assert result.returncode == 1 

193 assert "Error: CSV file" in result.stdout 

194 assert "nonexistent.csv does not exist" in result.stdout 

195 # Allow for RuntimeWarning about module import 

196 assert result.stderr == "" or "RuntimeWarning" in result.stderr 

197 

198 def test_cli_missing_schema_file(self, valid_csv, project_root): 

199 """Test CLI with non-existent schema file""" 

200 result = self.run_cli(valid_csv, "nonexistent.json", project_root) 

201 

202 assert result.returncode == 1 

203 assert "Error: Schema file" in result.stdout 

204 assert "nonexistent.json does not exist" in result.stdout 

205 # Allow for RuntimeWarning about module import 

206 assert result.stderr == "" or "RuntimeWarning" in result.stderr 

207 

208 def test_cli_missing_both_files(self, project_root): 

209 """Test CLI with both files missing""" 

210 result = self.run_cli("missing.csv", "missing.json", project_root) 

211 

212 assert result.returncode == 1 

213 # Should fail on the first missing file (CSV) 

214 assert "Error: CSV file" in result.stdout 

215 assert "missing.csv does not exist" in result.stdout 

216 

217 # === HELP AND VERSION OPTIONS === 

218 

219 def test_cli_help_long_flag(self, project_root): 

220 """Test CLI with --help flag""" 

221 import sys 

222 cmd = [sys.executable, "-m", "csv_schema_validator.cli.cli", "--help"] 

223 result = subprocess.run( 

224 cmd, 

225 capture_output=True, 

226 text=True, 

227 cwd=project_root, 

228 env={**os.environ, "PYTHONPATH": os.path.join(project_root, "src")} 

229 ) 

230 

231 assert result.returncode == 0 

232 assert "csv-schema-validator - Validate CSV files against JSON schemas" in result.stdout 

233 assert "USAGE:" in result.stdout 

234 assert "ARGUMENTS:" in result.stdout 

235 assert "OPTIONS:" in result.stdout 

236 assert "EXAMPLES:" in result.stdout 

237 assert "SCHEMA FORMAT:" in result.stdout 

238 assert "EXIT CODES:" in result.stdout 

239 # Allow for RuntimeWarning about module import 

240 assert result.stderr == "" or "RuntimeWarning" in result.stderr 

241 

242 def test_cli_help_short_flag(self, project_root): 

243 """Test CLI with -h flag""" 

244 import sys 

245 cmd = [sys.executable, "-m", "csv_schema_validator.cli.cli", "-h"] 

246 result = subprocess.run( 

247 cmd, 

248 capture_output=True, 

249 text=True, 

250 cwd=project_root, 

251 env={**os.environ, "PYTHONPATH": os.path.join(project_root, "src")} 

252 ) 

253 

254 assert result.returncode == 0 

255 assert "csv-schema-validator - Validate CSV files against JSON schemas" in result.stdout 

256 assert "USAGE:" in result.stdout 

257 # Allow for RuntimeWarning about module import 

258 assert result.stderr == "" or "RuntimeWarning" in result.stderr 

259 

260 def test_cli_version_long_flag(self, project_root): 

261 """Test CLI with --version flag""" 

262 import sys 

263 cmd = [sys.executable, "-m", "csv_schema_validator.cli.cli", "--version"] 

264 result = subprocess.run( 

265 cmd, 

266 capture_output=True, 

267 text=True, 

268 cwd=project_root, 

269 env={**os.environ, "PYTHONPATH": os.path.join(project_root, "src")} 

270 ) 

271 

272 assert result.returncode == 0 

273 assert "csv-schema-validator 0.1.1" in result.stdout 

274 # Allow for RuntimeWarning about module import 

275 assert result.stderr == "" or "RuntimeWarning" in result.stderr 

276 

277 def test_cli_version_short_flag(self, project_root): 

278 """Test CLI with -v flag""" 

279 import sys 

280 cmd = [sys.executable, "-m", "csv_schema_validator.cli.cli", "-v"] 

281 result = subprocess.run( 

282 cmd, 

283 capture_output=True, 

284 text=True, 

285 cwd=project_root, 

286 env={**os.environ, "PYTHONPATH": os.path.join(project_root, "src")} 

287 ) 

288 

289 assert result.returncode == 0 

290 assert "csv-schema-validator 0.1.1" in result.stdout 

291 # Allow for RuntimeWarning about module import 

292 assert result.stderr == "" or "RuntimeWarning" in result.stderr 

293 

294 def test_cli_help_with_other_args(self, project_root): 

295 """Test CLI with help flag and other arguments (help should take precedence)""" 

296 import sys 

297 cmd = [sys.executable, "-m", "csv_schema_validator.cli.cli", "file.csv", "--help"] 

298 result = subprocess.run( 

299 cmd, 

300 capture_output=True, 

301 text=True, 

302 cwd=project_root, 

303 env={**os.environ, "PYTHONPATH": os.path.join(project_root, "src")} 

304 ) 

305 

306 assert result.returncode == 0 

307 assert "csv-schema-validator - Validate CSV files against JSON schemas" in result.stdout 

308 # Allow for RuntimeWarning about module import 

309 assert result.stderr == "" or "RuntimeWarning" in result.stderr 

310 

311 def test_cli_version_with_other_args(self, project_root): 

312 """Test CLI with version flag and other arguments (version should take precedence)""" 

313 import sys 

314 cmd = [sys.executable, "-m", "csv_schema_validator.cli.cli", "file.csv", "-v"] 

315 result = subprocess.run( 

316 cmd, 

317 capture_output=True, 

318 text=True, 

319 cwd=project_root, 

320 env={**os.environ, "PYTHONPATH": os.path.join(project_root, "src")} 

321 ) 

322 

323 assert result.returncode == 0 

324 assert "csv-schema-validator 0.1.1" in result.stdout 

325 # Allow for RuntimeWarning about module import 

326 assert result.stderr == "" or "RuntimeWarning" in result.stderr 

327 

328 # === ARGUMENT ERROR CASES === 

329 

330 def test_cli_no_arguments(self, project_root): 

331 """Test CLI with no arguments""" 

332 import sys 

333 cmd = [sys.executable, "-m", "csv_schema_validator.cli.cli"] 

334 result = subprocess.run( 

335 cmd, 

336 capture_output=True, 

337 text=True, 

338 cwd=project_root, 

339 env={**os.environ, "PYTHONPATH": os.path.join(project_root, "src")} 

340 ) 

341 

342 assert result.returncode == 1 

343 assert "Usage: csv-schema-validator <csv_file> <schema_file>" in result.stderr 

344 assert "Use --help for more information." in result.stderr 

345 

346 def test_cli_insufficient_arguments_one(self, project_root): 

347 """Test CLI with only one argument""" 

348 import sys 

349 cmd = [sys.executable, "-m", "csv_schema_validator.cli.cli", "test.csv"] 

350 result = subprocess.run( 

351 cmd, 

352 capture_output=True, 

353 text=True, 

354 cwd=project_root, 

355 env={**os.environ, "PYTHONPATH": os.path.join(project_root, "src")} 

356 ) 

357 

358 assert result.returncode == 1 

359 assert "Usage: csv-schema-validator <csv_file> <schema_file>" in result.stderr 

360 assert "Use --help for more information." in result.stderr 

361 

362 def test_cli_too_many_arguments(self, valid_csv, basic_schema, project_root): 

363 """Test CLI with too many arguments""" 

364 import sys 

365 cmd = [ 

366 sys.executable, "-m", "csv_schema_validator.cli.cli", 

367 valid_csv, basic_schema, "extra_arg" 

368 ] 

369 result = subprocess.run( 

370 cmd, 

371 capture_output=True, 

372 text=True, 

373 cwd=project_root, 

374 env={**os.environ, "PYTHONPATH": os.path.join(project_root, "src")} 

375 ) 

376 

377 # Your current CLI doesn't validate argument count, so this might work 

378 # or might fail depending on how sys.argv is handled 

379 # This test documents the current behavior 

380 assert result.returncode in [0, 1] # Either success or failure is acceptable 

381 

382 # === JSON ERROR CASES === 

383 

384 def test_cli_malformed_json_schema(self, valid_csv, malformed_json_schema, project_root): 

385 """Test CLI with malformed JSON schema""" 

386 result = self.run_cli(valid_csv, malformed_json_schema, project_root) 

387 

388 # Your current CLI will crash with a JSON decode error 

389 # This test documents the current behavior 

390 assert result.returncode != 0 

391 # The error will be in stderr as a Python traceback 

392 assert "JSONDecodeError" in result.stderr or "json" in result.stderr.lower() 

393 

394 def test_cli_empty_schema_file(self, valid_csv, temp_dir, project_root): 

395 """Test CLI with empty schema file""" 

396 empty_schema = os.path.join(temp_dir, "empty.json") 

397 with open(empty_schema, "w") as f: 

398 f.write("") 

399 

400 result = self.run_cli(valid_csv, empty_schema, project_root) 

401 

402 # Empty JSON file should cause JSON decode error 

403 assert result.returncode != 0 

404 assert "JSONDecodeError" in result.stderr or "json" in result.stderr.lower() 

405 

406 # === OUTPUT FORMATTING TESTS === 

407 

408 def test_cli_output_formatting_success(self, valid_csv, basic_schema, project_root): 

409 """Test CLI output formatting for successful validation""" 

410 result = self.run_cli(valid_csv, basic_schema, project_root) 

411 

412 assert result.returncode == 0 

413 output_lines = result.stdout.strip().split('\n') 

414 

415 # Should have success message 

416 assert any("✅ Validation passed" in line for line in output_lines) 

417 

418 # Should not have error details 

419 assert not any("Row" in line and "Column" in line for line in output_lines) 

420 

421 def test_cli_output_formatting_failure(self, invalid_csv, basic_schema, project_root): 

422 """Test CLI output formatting for validation failure""" 

423 result = self.run_cli(invalid_csv, basic_schema, project_root) 

424 

425 # The new exception system provides clear error messages instead of crashes 

426 assert result.returncode == 1 

427 assert "❌ Validation failed" in result.stdout 

428 assert "PatternValidationError" in result.stdout or "EnumValidationError" in result.stdout 

429 

430 # === EDGE CASES === 

431 

432 def test_cli_with_unicode_data(self, temp_dir, basic_schema, project_root): 

433 """Test CLI with Unicode data in CSV""" 

434 csv_content = """id,name,email,department,salary,is_active 

4351,José García,jose.garcia@company.com,Engineering,75000,true 

4362,François Dupont,francois.dupont@company.com,Marketing,65000,false""" 

437 

438 csv_file = os.path.join(temp_dir, "unicode.csv") 

439 with open(csv_file, "w", encoding="utf-8") as f: 

440 f.write(csv_content) 

441 

442 result = self.run_cli(csv_file, basic_schema, project_root) 

443 

444 assert result.returncode == 0 

445 assert "✅ Validation passed" in result.stdout 

446 

447 def test_cli_with_large_csv(self, temp_dir, basic_schema, project_root): 

448 """Test CLI with larger CSV file""" 

449 # Create CSV with 100 rows 

450 csv_content = "id,name,email,department,salary,is_active\n" 

451 for i in range(1, 101): 

452 csv_content += f"{i},Person {i},person{i}@company.com,Engineering,{50000 + i},true\n" 

453 

454 csv_file = os.path.join(temp_dir, "large.csv") 

455 with open(csv_file, "w") as f: 

456 f.write(csv_content) 

457 

458 result = self.run_cli(csv_file, basic_schema, project_root) 

459 

460 assert result.returncode == 0 

461 assert "✅ Validation passed" in result.stdout 

462 

463 def test_cli_with_special_characters_in_paths(self, temp_dir, basic_schema, project_root): 

464 """Test CLI with special characters in file paths""" 

465 # Create files with spaces and special characters 

466 csv_content = """id,name,email,department,salary,is_active 

4671,John Doe,john.doe@company.com,Engineering,75000,true""" 

468 

469 csv_file = os.path.join(temp_dir, "file with spaces.csv") 

470 with open(csv_file, "w") as f: 

471 f.write(csv_content) 

472 

473 result = self.run_cli(csv_file, basic_schema, project_root) 

474 

475 assert result.returncode == 0 

476 assert "✅ Validation passed" in result.stdout 

477 

478 # === PERFORMANCE TESTS === 

479 

480 def test_cli_execution_time(self, valid_csv, basic_schema, project_root): 

481 """Test that CLI executes within reasonable time""" 

482 import time 

483 

484 start_time = time.time() 

485 result = self.run_cli(valid_csv, basic_schema, project_root) 

486 end_time = time.time() 

487 

488 execution_time = end_time - start_time 

489 

490 assert result.returncode == 0 

491 assert execution_time < 5.0 # Should complete within 5 seconds