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

218 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-21 14:08 +0200

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 

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", 

130 abs_csv_file, abs_schema_file 

131 ] 

132 return subprocess.run( 

133 cmd, 

134 capture_output=True, 

135 text=True, 

136 cwd=os.path.join(project_root, "src") 

137 ) 

138 

139 # === SUCCESS CASES === 

140 

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

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

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

144 

145 assert result.returncode == 0 

146 assert "✅ Validation passed" in result.stdout 

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

148 assert result.stderr == "" 

149 

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

151 """Test CLI with minimal valid data""" 

152 # Create CSV with just one valid row 

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

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

155 

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

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

158 f.write(csv_content) 

159 

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

161 

162 assert result.returncode == 0 

163 assert "✅ Validation passed" in result.stdout 

164 

165 # === VALIDATION FAILURE CASES === 

166 

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

168 """Test CLI with invalid CSV data""" 

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

170 

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

172 assert result.returncode == 1 

173 assert "❌ Validation failed" in result.stdout 

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

175 

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

177 """Test CLI with empty CSV file""" 

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

179 

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

181 assert "❌ Validation failed" in result.stdout 

182 assert "EmptyFileError" in result.stdout 

183 

184 # === FILE ERROR CASES === 

185 

186 def test_cli_missing_csv_file(self, basic_schema, project_root): 

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

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

189 

190 assert result.returncode == 1 

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

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

193 assert result.stderr == "" 

194 

195 def test_cli_missing_schema_file(self, valid_csv, project_root): 

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

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

198 

199 assert result.returncode == 1 

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

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

202 assert result.stderr == "" 

203 

204 def test_cli_missing_both_files(self, project_root): 

205 """Test CLI with both files missing""" 

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

207 

208 assert result.returncode == 1 

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

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

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

212 

213 # === HELP AND VERSION OPTIONS === 

214 

215 def test_cli_help_long_flag(self, project_root): 

216 """Test CLI with --help flag""" 

217 import sys 

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

219 result = subprocess.run( 

220 cmd, 

221 capture_output=True, 

222 text=True, 

223 cwd=os.path.join(project_root, "src") 

224 ) 

225 

226 assert result.returncode == 0 

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

228 assert "USAGE:" in result.stdout 

229 assert "ARGUMENTS:" in result.stdout 

230 assert "OPTIONS:" in result.stdout 

231 assert "EXAMPLES:" in result.stdout 

232 assert "SCHEMA FORMAT:" in result.stdout 

233 assert "EXIT CODES:" in result.stdout 

234 assert result.stderr == "" 

235 

236 def test_cli_help_short_flag(self, project_root): 

237 """Test CLI with -h flag""" 

238 import sys 

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

240 result = subprocess.run( 

241 cmd, 

242 capture_output=True, 

243 text=True, 

244 cwd=os.path.join(project_root, "src") 

245 ) 

246 

247 assert result.returncode == 0 

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

249 assert "USAGE:" in result.stdout 

250 assert result.stderr == "" 

251 

252 def test_cli_version_long_flag(self, project_root): 

253 """Test CLI with --version flag""" 

254 import sys 

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

256 result = subprocess.run( 

257 cmd, 

258 capture_output=True, 

259 text=True, 

260 cwd=os.path.join(project_root, "src") 

261 ) 

262 

263 assert result.returncode == 0 

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

265 assert result.stderr == "" 

266 

267 def test_cli_version_short_flag(self, project_root): 

268 """Test CLI with -v flag""" 

269 import sys 

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

271 result = subprocess.run( 

272 cmd, 

273 capture_output=True, 

274 text=True, 

275 cwd=os.path.join(project_root, "src") 

276 ) 

277 

278 assert result.returncode == 0 

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

280 assert result.stderr == "" 

281 

282 def test_cli_help_with_other_args(self, project_root): 

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

284 import sys 

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

286 result = subprocess.run( 

287 cmd, 

288 capture_output=True, 

289 text=True, 

290 cwd=os.path.join(project_root, "src") 

291 ) 

292 

293 assert result.returncode == 0 

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

295 assert result.stderr == "" 

296 

297 def test_cli_version_with_other_args(self, project_root): 

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

299 import sys 

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

301 result = subprocess.run( 

302 cmd, 

303 capture_output=True, 

304 text=True, 

305 cwd=os.path.join(project_root, "src") 

306 ) 

307 

308 assert result.returncode == 0 

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

310 assert result.stderr == "" 

311 

312 # === ARGUMENT ERROR CASES === 

313 

314 def test_cli_no_arguments(self, project_root): 

315 """Test CLI with no arguments""" 

316 import sys 

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

318 result = subprocess.run( 

319 cmd, 

320 capture_output=True, 

321 text=True, 

322 cwd=os.path.join(project_root, "src") 

323 ) 

324 

325 assert result.returncode == 1 

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

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

328 

329 def test_cli_insufficient_arguments_one(self, project_root): 

330 """Test CLI with only one argument""" 

331 import sys 

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

333 result = subprocess.run( 

334 cmd, 

335 capture_output=True, 

336 text=True, 

337 cwd=os.path.join(project_root, "src") 

338 ) 

339 

340 assert result.returncode == 1 

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

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

343 

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

345 """Test CLI with too many arguments""" 

346 import sys 

347 cmd = [ 

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

349 valid_csv, basic_schema, "extra_arg" 

350 ] 

351 result = subprocess.run( 

352 cmd, 

353 capture_output=True, 

354 text=True, 

355 cwd=os.path.join(project_root, "src") 

356 ) 

357 

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

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

360 # This test documents the current behavior 

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

362 

363 # === JSON ERROR CASES === 

364 

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

366 """Test CLI with malformed JSON schema""" 

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

368 

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

370 # This test documents the current behavior 

371 assert result.returncode != 0 

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

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

374 

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

376 """Test CLI with empty schema file""" 

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

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

379 f.write("") 

380 

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

382 

383 # Empty JSON file should cause JSON decode error 

384 assert result.returncode != 0 

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

386 

387 # === OUTPUT FORMATTING TESTS === 

388 

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

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

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

392 

393 assert result.returncode == 0 

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

395 

396 # Should have success message 

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

398 

399 # Should not have error details 

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

401 

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

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

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

405 

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

407 assert result.returncode == 1 

408 assert "❌ Validation failed" in result.stdout 

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

410 

411 # === EDGE CASES === 

412 

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

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

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

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

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

418 

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

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

421 f.write(csv_content) 

422 

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

424 

425 assert result.returncode == 0 

426 assert "✅ Validation passed" in result.stdout 

427 

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

429 """Test CLI with larger CSV file""" 

430 # Create CSV with 100 rows 

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

432 for i in range(1, 101): 

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

434 

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

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

437 f.write(csv_content) 

438 

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

440 

441 assert result.returncode == 0 

442 assert "✅ Validation passed" in result.stdout 

443 

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

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

446 # Create files with spaces and special characters 

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

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

449 

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

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

452 f.write(csv_content) 

453 

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

455 

456 assert result.returncode == 0 

457 assert "✅ Validation passed" in result.stdout 

458 

459 # === PERFORMANCE TESTS === 

460 

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

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

463 import time 

464 

465 start_time = time.time() 

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

467 end_time = time.time() 

468 

469 execution_time = end_time - start_time 

470 

471 assert result.returncode == 0 

472 assert execution_time < 5.0 # Should complete within 5 seconds