diff --git a/README.md b/README.md index 7b47b37..93935bf 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,6 @@ See [`problem_statement.md`](problem_statement.md) for the official participant ## Submission -Build `code.zip` with `python3 code/package_submission.py`, submit the root `output.csv`, and upload `log.txt` as the chat transcript. The challenge submission page is: +Build `code.zip` with `python3 code/package_submission.py`, submit the root `output.csv`, and upload `log.txt` as the chat transcript. The archive includes the required reports at the exact paths `evaluation/evaluation_report.md` and `evaluation/usage_report.md`; their source-of-truth copies remain beside the evaluator under `code/evaluation/`. The challenge submission page is: https://www.hackerrank.com/contests/hackerrank-orchestrate-october26/challenges/ghost-data-still-exists diff --git a/code/package_submission.py b/code/package_submission.py index 8336eb6..ecce120 100644 --- a/code/package_submission.py +++ b/code/package_submission.py @@ -12,6 +12,14 @@ EXCLUDED_PARTS = {"__pycache__", ".venv", "venv", "node_modules", ".git"} EXCLUDED_SUFFIXES = {".pyc", ".pyo", ".zip", ".tar", ".gz", ".tgz", ".7z", ".rar"} FIXED_ZIP_TIMESTAMP = (1980, 1, 1, 0, 0, 0) +REPORT_ALIASES = { + ROOT / "code" / "evaluation" / "evaluation_report.md": ( + "evaluation/evaluation_report.md" + ), + ROOT / "code" / "evaluation" / "usage_report.md": ( + "evaluation/usage_report.md" + ), +} def included_files() -> list[Path]: @@ -25,13 +33,27 @@ def included_files() -> list[Path]: ) -def build_archive(output: Path = OUTPUT) -> list[Path]: +def archive_entries() -> list[tuple[Path, str]]: + """Return source files and their contract-compliant archive paths.""" + entries = [ + (path, path.relative_to(ROOT).as_posix()) + for path in included_files() + ] + entries.extend(REPORT_ALIASES.items()) + entries = sorted(entries, key=lambda item: item[1]) + names = [name for _path, name in entries] + if len(names) != len(set(names)): + raise ValueError("Submission archive contains duplicate paths") + return entries + + +def build_archive(output: Path = OUTPUT) -> list[tuple[Path, str]]: """Write a byte-reproducible archive independent of checkout timestamps.""" - files = included_files() + entries = archive_entries() with zipfile.ZipFile(output, "w") as archive: - for path in files: + for path, archive_path in entries: info = zipfile.ZipInfo( - path.relative_to(ROOT).as_posix(), + archive_path, date_time=FIXED_ZIP_TIMESTAMP, ) info.compress_type = zipfile.ZIP_DEFLATED @@ -43,12 +65,12 @@ def build_archive(output: Path = OUTPUT) -> list[Path]: compress_type=zipfile.ZIP_DEFLATED, compresslevel=9, ) - return files + return entries def main() -> int: - files = build_archive() - print(f"Created {OUTPUT} with {len(files)} files") + entries = build_archive() + print(f"Created {OUTPUT} with {len(entries)} files") return 0 diff --git a/code/tests/test_engine.py b/code/tests/test_engine.py index 1239d88..ad36cbf 100644 --- a/code/tests/test_engine.py +++ b/code/tests/test_engine.py @@ -298,6 +298,24 @@ def test_submission_archive_is_reproducible_and_dataset_free(self) -> None: with zipfile.ZipFile(first) as archive: names = archive.namelist() self.assertEqual(archive.testzip(), None) + self.assertEqual(len(names), len(set(names))) + required_reports = { + "evaluation/evaluation_report.md": ( + REPO_ROOT + / "code" + / "evaluation" + / "evaluation_report.md" + ), + "evaluation/usage_report.md": ( + REPO_ROOT / "code" / "evaluation" / "usage_report.md" + ), + } + self.assertTrue(required_reports.keys() <= set(names)) + for archive_path, source_path in required_reports.items(): + self.assertEqual( + archive.read(archive_path), + source_path.read_bytes(), + ) self.assertFalse( any( name == "log.txt"