diff --git a/tests/test_sentinel_dataset_validator.py b/tests/test_sentinel_dataset_validator.py index 6e77dd0a..b5cc1a56 100644 --- a/tests/test_sentinel_dataset_validator.py +++ b/tests/test_sentinel_dataset_validator.py @@ -257,4 +257,272 @@ def test_missing_dataset_file(tmp_path): "Dataset not found" in error for error in result.errors - ) \ No newline at end of file + ) + + +def test_multiple_duplicate_project_ids(tmp_path): + """Multiple duplicate IDs should all be reported.""" + + create_starter_file( + tmp_path, + "expense_tracker.py", + ) + + dataset = write_dataset( + tmp_path, + [ + create_project( + id=1, + title="Expense Tracker", + ), + create_project( + id=1, + title="Calculator", + ), + create_project( + id=2, + title="Weather App", + ), + create_project( + id=2, + title="Todo App", + ), + ], + ) + + result = run(dataset) + + assert result.passed is False + + assert result.errors == [ + "Duplicate project ID: 1", + "Duplicate project ID: 2", + ] + + +def test_multiple_duplicate_project_titles(tmp_path): + """Multiple duplicate titles should all be reported.""" + + create_starter_file( + tmp_path, + "expense_tracker.py", + ) + + dataset = write_dataset( + tmp_path, + [ + create_project( + id=1, + title="Calculator", + ), + create_project( + id=2, + title="Calculator", + ), + create_project( + id=3, + title="Todo App", + ), + create_project( + id=4, + title="Todo App", + ), + ], + ) + + result = run(dataset) + + assert result.passed is False + + assert result.errors == [ + 'Duplicate project title: "Calculator"', + 'Duplicate project title: "Todo App"', + ] + + +def test_whitespace_only_required_field(tmp_path): + """Whitespace-only required fields should fail validation.""" + + create_starter_file( + tmp_path, + "expense_tracker.py", + ) + + dataset = write_dataset( + tmp_path, + [ + create_project( + title=" ", + ), + ], + ) + + result = run(dataset) + + assert result.passed is False + + assert any( + "empty 'title'" + in error + for error in result.errors + ) + + +def test_empty_required_list_field(tmp_path): + """Empty required list fields should fail validation.""" + + create_starter_file( + tmp_path, + "expense_tracker.py", + ) + + dataset = write_dataset( + tmp_path, + [ + create_project( + skills=[], + ), + ], + ) + + result = run(dataset) + + assert result.passed is False + + assert any( + "empty 'skills'" + in error + for error in result.errors + ) + + +def test_multiple_missing_required_fields(tmp_path): + """Multiple missing required fields should all be reported.""" + + create_starter_file( + tmp_path, + "expense_tracker.py", + ) + + project = create_project() + + del project["description"] + del project["features"] + del project["resources"] + + dataset = write_dataset( + tmp_path, + [project], + ) + + result = run(dataset) + + assert result.passed is False + + assert len(result.errors) == 1 + + assert ( + "Project 1 is missing required fields: " + "description, features, resources" + in result.errors[0] + ) + + +def test_multiple_validation_failures(tmp_path): + """Multiple validation checks should be reported together.""" + + create_starter_file( + tmp_path, + "expense_tracker.py", + ) + + project_one = create_project( + id=1, + title="Calculator", + ) + + project_two = create_project( + id=1, + title="Calculator", + skills=[], + ) + + del project_two["description"] + + dataset = write_dataset( + tmp_path, + [ + project_one, + project_two, + ], + ) + + result = run(dataset) + + assert result.passed is False + + assert any( + "Duplicate project ID: 1" + in error + for error in result.errors + ) + + assert any( + 'Duplicate project title: "Calculator"' + in error + for error in result.errors + ) + + assert any( + "missing required fields: description" + in error + for error in result.errors + ) + + assert any( + "empty 'skills'" + in error + for error in result.errors + ) + + +def test_duplicate_values_are_reported_in_sorted_order(tmp_path): + """Duplicate IDs and titles should be reported deterministically.""" + + create_starter_file( + tmp_path, + "expense_tracker.py", + ) + + dataset = write_dataset( + tmp_path, + [ + create_project( + id=3, + title="Zebra App", + ), + create_project( + id=1, + title="Alpha App", + ), + create_project( + id=3, + title="Zebra App", + ), + create_project( + id=1, + title="Alpha App", + ), + ], + ) + + result = run(dataset) + + assert result.passed is False + + assert result.errors == [ + "Duplicate project ID: 1", + "Duplicate project ID: 3", + 'Duplicate project title: "Alpha App"', + 'Duplicate project title: "Zebra App"', + ] diff --git a/tests/test_starter_code_validator.py b/tests/test_starter_code_validator.py index ebe919d6..1b3b901a 100644 --- a/tests/test_starter_code_validator.py +++ b/tests/test_starter_code_validator.py @@ -364,4 +364,298 @@ def test_missing_starter_code_directory(tmp_path): "Starter code directory not found" in error for error in result.errors - ) \ No newline at end of file + ) + + +def test_nested_starter_code_file(tmp_path): + """Nested starter code files should be discovered and validated.""" + + nested_dir = tmp_path / "starter_code" / "python" + nested_dir.mkdir(parents=True) + + file_path = nested_dir / "expense_tracker.py" + file_path.write_text( + "# starter code", + encoding="utf-8", + ) + + dataset = write_dataset( + tmp_path, + [ + create_project( + starter_code="starter_code/python/expense_tracker.py", + ), + ], + ) + + result = run( + dataset_path=dataset, + starter_code_dir=tmp_path / "starter_code", + ) + + assert result.passed is True + assert result.errors == [] + assert result.warnings == [] + + assert result.details["checks"]["orphan_files"] == [] + + assert result.details["count"] == 1 + + +def test_nested_orphan_file(tmp_path): + """Nested unreferenced starter code files should be detected.""" + + nested_dir = tmp_path / "starter_code" / "python" + nested_dir.mkdir(parents=True) + + (nested_dir / "expense_tracker.py").write_text( + "# starter code", + encoding="utf-8", + ) + + (nested_dir / "calculator.py").write_text( + "# orphan starter code", + encoding="utf-8", + ) + + dataset = write_dataset( + tmp_path, + [ + create_project( + starter_code="starter_code/python/expense_tracker.py", + ), + ], + ) + + result = run( + dataset_path=dataset, + starter_code_dir=tmp_path / "starter_code", + ) + + assert result.passed is False + + assert result.details["checks"]["orphan_files"] == [ + "starter_code/python/calculator.py", + ] + + assert any( + "Orphan Files" + in error + for error in result.errors + ) + + +def test_multiple_empty_files(tmp_path): + """Multiple empty starter code files should all be detected.""" + + create_starter_file( + tmp_path, + "expense_tracker.py", + content="", + ) + + create_starter_file( + tmp_path, + "calculator.py", + content="", + ) + + dataset = write_dataset( + tmp_path, + [ + create_project( + starter_code="starter_code/expense_tracker.py", + ), + create_project( + id=2, + title="Calculator", + starter_code="starter_code/calculator.py", + ), + ], + ) + + result = run( + dataset_path=dataset, + starter_code_dir=tmp_path / "starter_code", + ) + + assert result.passed is False + + assert result.details["checks"]["empty_files"] == [ + "starter_code/calculator.py", + "starter_code/expense_tracker.py", + ] + + assert any( + "Empty Files" + in error + for error in result.errors + ) + + +def test_multiple_hidden_files(tmp_path): + """Multiple hidden files should all produce a warning.""" + + create_starter_file( + tmp_path, + ".gitkeep", + ) + + create_starter_file( + tmp_path, + ".config", + ) + + dataset = write_dataset( + tmp_path, + [create_project()], + ) + + result = run( + dataset_path=dataset, + starter_code_dir=tmp_path / "starter_code", + ) + + assert result.passed is False + + assert len(result.warnings) == 2 + + assert result.details["checks"]["hidden_files"] == [ + "starter_code/.config", + "starter_code/.gitkeep", + ] + + assert any( + "Hidden Files" + in warning + for warning in result.warnings + ) + + +def test_multiple_unsupported_extensions(tmp_path): + """Multiple unsupported file types should all produce warnings.""" + + create_starter_file( + tmp_path, + "expense_tracker.py", + ) + + create_starter_file( + tmp_path, + "notes.pdf", + ) + + create_starter_file( + tmp_path, + "archive.zip", + ) + + dataset = write_dataset( + tmp_path, + [create_project()], + ) + + result = run( + dataset_path=dataset, + starter_code_dir=tmp_path / "starter_code", + ) + + assert result.passed is False + + assert result.details["checks"]["unsupported_extensions"] == [ + "starter_code/archive.zip", + "starter_code/notes.pdf", + ] + + assert any( + "Unsupported Extensions" + in warning + for warning in result.warnings + ) + + +def test_all_supported_extensions(tmp_path): + """All configured supported extensions should pass validation.""" + + supported_extensions = [ + ".py", + ".js", + ".java", + ".html", + ".css", + ".yml", + ".yaml", + ".txt", + ".md", + ] + + starter_dir = tmp_path / "starter_code" + starter_dir.mkdir() + + projects = [] + + for index, extension in enumerate(supported_extensions, start=1): + filename = f"project_{index}{extension}" + + (starter_dir / filename).write_text( + "# starter code", + encoding="utf-8", + ) + + projects.append( + create_project( + id=index, + title=f"Project {index}", + starter_code=f"starter_code/{filename}", + ) + ) + + dataset = write_dataset( + tmp_path, + projects, + ) + + result = run( + dataset_path=dataset, + starter_code_dir=starter_dir, + ) + + assert result.passed is True + assert result.errors == [] + assert result.warnings == [] + + assert result.details["checks"]["unsupported_extensions"] == [] + assert result.details["checks"]["orphan_files"] == [] + + assert result.details["count"] == len(supported_extensions) + + +def test_dataset_must_be_json_array(tmp_path): + """A JSON object instead of a project list should fail validation.""" + + data_dir = tmp_path / "data" + data_dir.mkdir() + + dataset = data_dir / "projects.json" + + dataset.write_text( + json.dumps( + { + "project": create_project(), + } + ), + encoding="utf-8", + ) + + result = run( + dataset_path=dataset, + starter_code_dir=tmp_path / "starter_code", + ) + + assert result.passed is False + + assert any( + "Dataset must contain a JSON array of projects." + in error + for error in result.errors + )