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
« 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
9class TestCLI:
10 """Test suite for the CLI interface using subprocess"""
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
18 @pytest.fixture
19 def project_root(self):
20 """Get the project root directory"""
21 return Path(__file__).parent.parent.parent.parent
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 }
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
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"""
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
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"""
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
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
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
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)
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 )
139 # === SUCCESS CASES ===
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)
145 assert result.returncode == 0
146 assert "✅ Validation passed" in result.stdout
147 assert "❌ Validation failed" not in result.stdout
148 assert result.stderr == ""
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"""
156 csv_file = os.path.join(temp_dir, "minimal.csv")
157 with open(csv_file, "w") as f:
158 f.write(csv_content)
160 result = self.run_cli(csv_file, basic_schema, project_root)
162 assert result.returncode == 0
163 assert "✅ Validation passed" in result.stdout
165 # === VALIDATION FAILURE CASES ===
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)
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
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)
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
184 # === FILE ERROR CASES ===
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)
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 == ""
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)
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 == ""
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)
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
213 # === HELP AND VERSION OPTIONS ===
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 )
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 == ""
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 )
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 == ""
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 )
263 assert result.returncode == 0
264 assert "csv-schema-validator 0.1.1" in result.stdout
265 assert result.stderr == ""
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 )
278 assert result.returncode == 0
279 assert "csv-schema-validator 0.1.1" in result.stdout
280 assert result.stderr == ""
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 )
293 assert result.returncode == 0
294 assert "csv-schema-validator - Validate CSV files against JSON schemas" in result.stdout
295 assert result.stderr == ""
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 )
308 assert result.returncode == 0
309 assert "csv-schema-validator 0.1.1" in result.stdout
310 assert result.stderr == ""
312 # === ARGUMENT ERROR CASES ===
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 )
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
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 )
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
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 )
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
363 # === JSON ERROR CASES ===
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)
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()
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("")
381 result = self.run_cli(valid_csv, empty_schema, project_root)
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()
387 # === OUTPUT FORMATTING TESTS ===
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)
393 assert result.returncode == 0
394 output_lines = result.stdout.strip().split('\n')
396 # Should have success message
397 assert any("✅ Validation passed" in line for line in output_lines)
399 # Should not have error details
400 assert not any("Row" in line and "Column" in line for line in output_lines)
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)
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
411 # === EDGE CASES ===
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"""
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)
423 result = self.run_cli(csv_file, basic_schema, project_root)
425 assert result.returncode == 0
426 assert "✅ Validation passed" in result.stdout
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"
435 csv_file = os.path.join(temp_dir, "large.csv")
436 with open(csv_file, "w") as f:
437 f.write(csv_content)
439 result = self.run_cli(csv_file, basic_schema, project_root)
441 assert result.returncode == 0
442 assert "✅ Validation passed" in result.stdout
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"""
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)
454 result = self.run_cli(csv_file, basic_schema, project_root)
456 assert result.returncode == 0
457 assert "✅ Validation passed" in result.stdout
459 # === PERFORMANCE TESTS ===
461 def test_cli_execution_time(self, valid_csv, basic_schema, project_root):
462 """Test that CLI executes within reasonable time"""
463 import time
465 start_time = time.time()
466 result = self.run_cli(valid_csv, basic_schema, project_root)
467 end_time = time.time()
469 execution_time = end_time - start_time
471 assert result.returncode == 0
472 assert execution_time < 5.0 # Should complete within 5 seconds