from __future__ import annotations import csv from datetime import datetime import io import json from pathlib import Path import tempfile import unittest import zipfile from openpyxl import Workbook import main COMPLETE_WINDOW = "2026073110001100" INCOMPLETE_WINDOW = "2026073111001200" class PipelineTest(unittest.TestCase): def test_latest_complete_window_is_converted_and_merged(self) -> None: with tempfile.TemporaryDirectory() as temp: root = Path(temp) / "source" output = Path(temp) / "output" for source_type in main.EXPECTED_TYPES: create_archive(root, source_type, COMPLETE_WINDOW) create_archive(root, main.EXPECTED_TYPES[0], INCOMPLETE_WINDOW) result = main.process(main.LocalSource(root), output, lookback_days=3) self.assertEqual(result.name, COMPLETE_WINDOW) converted = sorted((result / "converted").glob("*.csv")) self.assertEqual(len(converted), 7) with (result / f"interference_summary_{COMPLETE_WINDOW}.csv").open(encoding="utf-8-sig", newline="") as file: rows = list(csv.DictReader(file)) self.assertEqual(len(rows), 7) self.assertEqual({row["source_type"] for row in rows}, set(main.EXPECTED_TYPES)) self.assertIn("460-00-100-1", {row["cgi"] for row in rows}) self.assertIn("46000-100-1", {row["cgi"] for row in rows}) self.assertTrue(all(row["interference_dbm"] == "-100.5" for row in rows)) manifest = json.loads((result / "manifest.json").read_text(encoding="utf-8")) self.assertEqual(manifest["source_file_count"], 7) self.assertEqual(manifest["summary_rows"], 7) self.assertEqual(len(manifest["warnings"]), 1) self.assertIn(INCOMPLETE_WINDOW, manifest["warnings"][0]) def test_requested_incomplete_window_is_rejected(self) -> None: with tempfile.TemporaryDirectory() as temp: root = Path(temp) / "source" create_archive(root, main.EXPECTED_TYPES[0], INCOMPLETE_WINDOW) with self.assertRaisesRegex(main.ProcessingError, "Requested window is incomplete"): main.process(main.LocalSource(root), Path(temp) / "output", 3, INCOMPLETE_WINDOW) def test_schema_change_is_rejected(self) -> None: with tempfile.TemporaryDirectory() as temp: root = Path(temp) / "source" for source_type in main.EXPECTED_TYPES: create_archive(root, source_type, COMPLETE_WINDOW, bad_header=source_type == main.EXPECTED_TYPES[0]) with self.assertRaisesRegex(main.ProcessingError, "Unexpected Sheet0 header"): main.process(main.LocalSource(root), Path(temp) / "output", 3) def test_cross_midnight_window(self) -> None: self.assertEqual( main.window_bounds("2026073023000000"), ("2026-07-30 23:00:00", "2026-07-31 00:00:00"), ) def create_archive(root: Path, source_type: str, window: str, bad_header: bool = False) -> None: date_dir = root / f"{window[:4]}-{window[4:6]}-{window[6:8]}" date_dir.mkdir(parents=True, exist_ok=True) filename = f"{source_type}_LWP_每小时_过滤110_{window}" workbook = Workbook() sheet = workbook.active sheet.title = "Sheet0" header = list(main.EXPECTED_HEADERS[source_type]) if bad_header: header[-1] = "unexpected" sheet.append(header) sheet.append(mock_row(source_type, window)) metadata = workbook.create_sheet("指标(计数器)") metadata.append(["指标或计数器", "指标或计数器描述", "指标公式", "指标或计数器状态"]) content = io.BytesIO() workbook.save(content) workbook.close() with zipfile.ZipFile(date_dir / f"{filename}.zip", "w", zipfile.ZIP_DEFLATED) as archive: archive.writestr(f"{filename}.xlsx", content.getvalue()) def mock_row(source_type: str, window: str) -> list[object]: header = main.EXPECTED_HEADERS[source_type] values: dict[str, object] = {column: "mock" for column in header} values.update( { "开始时间": datetime.strptime(window[:12], "%Y%m%d%H%M"), "粒度": "1 小时", "eNodeBId": 100, "eNodeBID": 100, "cellId": 1, "小区ID": 1, "masterOperatorId": "46000-100-1", "E-UTRAN FDD小区名称": f"{source_type}-小区", "E-UTRAN TDD小区名称": f"{source_type}-小区", "CU小区配置名称": f"{source_type}-小区", "小区名称": f"{source_type}-小区", "载波平均噪声干扰(dBm)": -100.5, "小区上行平均干扰电平(dBm)": -100.5, } ) return [values[column] for column in header] if __name__ == "__main__": unittest.main()