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-09-21 14:35 +0200
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-21 14:35 +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.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.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 )
140 # === SUCCESS CASES ===
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)
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
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"""
158 csv_file = os.path.join(temp_dir, "minimal.csv")
159 with open(csv_file, "w") as f:
160 f.write(csv_content)
162 result = self.run_cli(csv_file, basic_schema, project_root)
164 assert result.returncode == 0
165 assert "✅ Validation passed" in result.stdout
167 # === VALIDATION FAILURE CASES ===
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)
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
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)
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
186 # === FILE ERROR CASES ===
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)
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
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)
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
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)
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
217 # === HELP AND VERSION OPTIONS ===
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 )
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
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 )
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
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 )
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
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 )
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
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 )
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
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 )
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
328 # === ARGUMENT ERROR CASES ===
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 )
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
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 )
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
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 )
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
382 # === JSON ERROR CASES ===
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)
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()
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("")
400 result = self.run_cli(valid_csv, empty_schema, project_root)
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()
406 # === OUTPUT FORMATTING TESTS ===
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)
412 assert result.returncode == 0
413 output_lines = result.stdout.strip().split('\n')
415 # Should have success message
416 assert any("✅ Validation passed" in line for line in output_lines)
418 # Should not have error details
419 assert not any("Row" in line and "Column" in line for line in output_lines)
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)
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
430 # === EDGE CASES ===
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"""
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)
442 result = self.run_cli(csv_file, basic_schema, project_root)
444 assert result.returncode == 0
445 assert "✅ Validation passed" in result.stdout
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"
454 csv_file = os.path.join(temp_dir, "large.csv")
455 with open(csv_file, "w") as f:
456 f.write(csv_content)
458 result = self.run_cli(csv_file, basic_schema, project_root)
460 assert result.returncode == 0
461 assert "✅ Validation passed" in result.stdout
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"""
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)
473 result = self.run_cli(csv_file, basic_schema, project_root)
475 assert result.returncode == 0
476 assert "✅ Validation passed" in result.stdout
478 # === PERFORMANCE TESTS ===
480 def test_cli_execution_time(self, valid_csv, basic_schema, project_root):
481 """Test that CLI executes within reasonable time"""
482 import time
484 start_time = time.time()
485 result = self.run_cli(valid_csv, basic_schema, project_root)
486 end_time = time.time()
488 execution_time = end_time - start_time
490 assert result.returncode == 0
491 assert execution_time < 5.0 # Should complete within 5 seconds