Files
Wyndham-RSVN-0918/scripts/tests/test_rate_room_directory_generator.py
鲨鱼辣椒 31849411a8
verify / booking-verify (push) Has been cancelled
建立5178独立项目基线
2026-09-08 15:03:45 +08:00

182 lines
8.1 KiB
Python

"""Deterministic and mutation tests for the Rate/Room directory foundation."""
from __future__ import annotations
import copy
import importlib.util
import json
import tempfile
import unittest
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
GENERATOR_PATH = REPOSITORY_ROOT / "scripts" / "generate_rate_room_directory.py"
DIRECTORY_PATH = (
REPOSITORY_ROOT
/ "server"
/ "src"
/ "main"
/ "resources"
/ "reservation-booking"
/ "catalog"
/ "rate-room-directory-v20260810.json"
)
MANIFEST_PATH = DIRECTORY_PATH.with_name("rate-room-directory-v20260810.manifest.json")
GOLDEN_PATH = (
REPOSITORY_ROOT
/ "server"
/ "src"
/ "test"
/ "resources"
/ "reservation-booking"
/ "catalog"
/ "rate-room-directory-v20260810-golden.json"
)
def load_generator():
spec = importlib.util.spec_from_file_location("rate_room_directory_generator", GENERATOR_PATH)
if spec is None or spec.loader is None:
raise RuntimeError("Rate/Room generator module cannot be loaded")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class RateRoomDirectoryGeneratorTest(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.generator = load_generator()
cls.directory_bytes = DIRECTORY_PATH.read_bytes()
cls.directory = json.loads(cls.directory_bytes)
cls.manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
cls.golden = json.loads(GOLDEN_PATH.read_text(encoding="utf-8"))
def test_directory_manifest_and_independent_golden_match(self) -> None:
self.generator.validate_directory(self.directory)
self.generator.validate_manifest(self.directory_bytes, self.manifest)
self.assertEqual(self.golden["directory_version"], self.directory["directory_version"])
self.assertEqual(self.golden["source_excel_sha256"], self.directory["source"]["sha256"])
self.assertEqual(self.golden["source_word_sha256"], self.directory["source"]["word"]["sha256"])
self.assertEqual(self.golden["semantic_fingerprints"], self.generator.semantic_fingerprints(self.directory))
self.assertEqual(self.golden["semantic_fingerprints"], self.manifest["semantic_fingerprints"])
self.assertEqual(self.golden["room_types"], [item["code"] for item in self.directory["room_types"]])
self.assertEqual(
self.golden["rate_codes"],
[
{
"code": item["code"],
"breakfast_included": item["breakfast_included"],
"breakfast_restaurant_code": item["breakfast_restaurant_code"],
}
for item in self.directory["rate_codes"]
],
)
def test_golden_word_corrections_are_bound_to_exact_source_rows(self) -> None:
rows = {item["source_row"]: item for item in self.directory["mapping_rows"]}
projected = []
for expected in self.golden["word_corrections"]:
row = rows[expected["source_row"]]
projected.append({key: row[key] for key in expected})
self.assertEqual(self.golden["word_corrections"], projected)
def test_room_type_and_rate_code_projections_are_independently_fingerprinted(self) -> None:
fingerprints = self.generator.semantic_fingerprints(self.directory)
self.assertEqual(
"f1bfce959809ac47b65d7a0291f036e43ef9f99b80a774bbd6af9603e9d06eac",
fingerprints["room_type_mapping_projection"],
)
self.assertEqual(
"2810b5c812858da4be694dfa7259b8cdd1c98c1e8133f368e4a4ad9fe5e02885",
fingerprints["rate_code_mapping_projection"],
)
self.assertEqual(50, len(self.generator.room_type_mapping_projection(self.directory)))
self.assertEqual(165, len(self.generator.rate_code_mapping_projection(self.directory)))
roomtype_only = copy.deepcopy(self.directory)
roomtype_only["mapping_rows"][0]["room_type"] = "RM4"
roomtype_only_fingerprints = self.generator.semantic_fingerprints(roomtype_only)
self.assertNotEqual(
fingerprints["room_type_mapping_projection"],
roomtype_only_fingerprints["room_type_mapping_projection"],
)
self.assertEqual(
fingerprints["rate_code_mapping_projection"],
roomtype_only_fingerprints["rate_code_mapping_projection"],
)
def test_semantic_duplicate_ignores_source_provenance(self) -> None:
mutated = copy.deepcopy(self.directory)
duplicate = copy.deepcopy(mutated["mapping_rows"][0])
duplicate["source_row"] = mutated["mapping_rows"][1]["source_row"]
mutated["mapping_rows"][1] = duplicate
with self.assertRaisesRegex(ValueError, "Duplicate semantic source mappings"):
self.generator.validate_directory(mutated)
def test_duplicate_source_row_is_rejected(self) -> None:
mutated = copy.deepcopy(self.directory)
mutated["mapping_rows"][1]["source_row"] = mutated["mapping_rows"][0]["source_row"]
with self.assertRaisesRegex(ValueError, "Duplicate source_row"):
self.generator.validate_directory(mutated)
def test_unknown_room_type_reference_is_rejected(self) -> None:
mutated = copy.deepcopy(self.directory)
mutated["mapping_rows"][0]["room_type"] = "UNKNOWN"
with self.assertRaisesRegex(ValueError, "references unknown Roomtype"):
self.generator.validate_directory(mutated)
def test_breakfast_cross_reference_is_rejected(self) -> None:
mutated = copy.deepcopy(self.directory)
mutated["mapping_rows"][0]["breakfast_restaurant_code"] = "LEELA"
with self.assertRaisesRegex(ValueError, "breakfast state differs"):
self.generator.validate_directory(mutated)
def test_bad_alias_target_is_rejected(self) -> None:
mutated = copy.deepcopy(self.directory)
mutated["legacy_rate_code_aliases"][0]["base_rate_code"] = "UNKNOWN"
with self.assertRaisesRegex(ValueError, "points outside"):
self.generator.validate_directory(mutated)
def test_candidate_scenario_drift_is_rejected(self) -> None:
mutated = copy.deepcopy(self.directory)
mutated["candidate_scenarios"][0]["candidate_room_types"] = ["RM2"]
with self.assertRaisesRegex(ValueError, "Candidate scenarios are stale"):
self.generator.validate_directory(mutated)
def test_missing_price_default_must_be_canonical_grpa1(self) -> None:
mutated = copy.deepcopy(self.directory)
mutated["policies"]["rate_code"]["missing_price_default_base_rate_code"] = "GRPA1-900"
with self.assertRaisesRegex(ValueError, "canonical base Rate Code"):
self.generator.validate_directory(mutated)
def test_company_specific_segment_override_is_rejected(self) -> None:
mutated = copy.deepcopy(self.directory)
mutated["policies"]["segment"]["fixed_by_company"]["Q.B.D"] = "Group"
with self.assertRaisesRegex(ValueError, "Unexpected segment policy"):
self.generator.validate_directory(mutated)
def test_manifest_hash_mismatch_is_rejected(self) -> None:
with self.assertRaisesRegex(ValueError, "Manifest directory SHA-256 mismatch"):
self.generator.validate_manifest(self.directory_bytes + b"\n", self.manifest)
def test_check_mode_never_creates_or_rewrites_files(self) -> None:
with tempfile.TemporaryDirectory(prefix="rate-room-generator-test-") as temporary_directory:
target = Path(temporary_directory) / "artifact.txt"
target.write_text("stable", encoding="utf-8")
before = target.stat().st_mtime_ns
self.generator.write_or_check(target, "stable", True)
self.assertEqual(before, target.stat().st_mtime_ns)
missing = Path(temporary_directory) / "missing.txt"
with self.assertRaisesRegex(ValueError, "stale or missing"):
self.generator.write_or_check(missing, "expected", True)
self.assertFalse(missing.exists())
if __name__ == "__main__":
unittest.main()