feat: 实现干扰指标最新小时入库
This commit is contained in:
+121
-1
@@ -26,8 +26,9 @@ class PipelineTest(unittest.TestCase):
|
||||
for source_type in main.EXPECTED_TYPES:
|
||||
create_archive(root, source_type, COMPLETE_WINDOW)
|
||||
create_archive(root, main.EXPECTED_TYPES[0], INCOMPLETE_WINDOW)
|
||||
store = RecordingStore()
|
||||
|
||||
result = main.process(main.LocalSource(root), output, lookback_days=3)
|
||||
result = main.process(main.LocalSource(root), output, lookback_days=3, store=store)
|
||||
|
||||
self.assertEqual(result.name, COMPLETE_WINDOW)
|
||||
converted = sorted((result / "converted").glob("*.csv"))
|
||||
@@ -45,6 +46,8 @@ class PipelineTest(unittest.TestCase):
|
||||
self.assertEqual(manifest["summary_rows"], 7)
|
||||
self.assertEqual(len(manifest["warnings"]), 1)
|
||||
self.assertIn(INCOMPLETE_WINDOW, manifest["warnings"][0])
|
||||
self.assertEqual(manifest["database"]["metric_time"], "2026-07-31 10:00:00")
|
||||
self.assertEqual(len(store.rows), 7)
|
||||
|
||||
def test_requested_incomplete_window_is_rejected(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
@@ -69,6 +72,123 @@ class PipelineTest(unittest.TestCase):
|
||||
("2026-07-30 23:00:00", "2026-07-31 00:00:00"),
|
||||
)
|
||||
|
||||
def test_mysql_store_replaces_same_hour_and_deletes_other_hours(self) -> None:
|
||||
connection = FakeConnection(datetime(2026, 7, 31, 10), same_hour_rows=7, old_rows=20)
|
||||
store = main.MySQLSummaryStore(
|
||||
host="",
|
||||
port=3306,
|
||||
user="",
|
||||
password="",
|
||||
database="",
|
||||
connect_factory=lambda: connection,
|
||||
)
|
||||
|
||||
result = store.replace_latest([database_row()])
|
||||
|
||||
self.assertEqual(result["metric_time"], "2026-07-31 10:00:00")
|
||||
self.assertEqual(result["inserted_rows"], 1)
|
||||
self.assertEqual(result["refreshed_rows"], 7)
|
||||
self.assertEqual(result["old_rows_deleted"], 20)
|
||||
self.assertEqual(connection.cursor_instance.inserted[0][0], datetime(2026, 7, 31, 10))
|
||||
self.assertTrue(connection.committed)
|
||||
self.assertFalse(connection.rolled_back)
|
||||
self.assertTrue(connection.closed)
|
||||
|
||||
def test_mysql_store_refuses_to_replace_a_newer_hour(self) -> None:
|
||||
connection = FakeConnection(datetime(2026, 7, 31, 11))
|
||||
store = main.MySQLSummaryStore(
|
||||
host="",
|
||||
port=3306,
|
||||
user="",
|
||||
password="",
|
||||
database="",
|
||||
connect_factory=lambda: connection,
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(main.ProcessingError, "already contains newer metric time"):
|
||||
store.replace_latest([database_row()])
|
||||
|
||||
self.assertFalse(connection.committed)
|
||||
self.assertTrue(connection.rolled_back)
|
||||
self.assertEqual(connection.cursor_instance.inserted, [])
|
||||
self.assertTrue(connection.closed)
|
||||
|
||||
|
||||
class RecordingStore:
|
||||
def __init__(self) -> None:
|
||||
self.rows: list[dict[str, str]] = []
|
||||
|
||||
def replace_latest(self, rows: list[dict[str, str]]) -> dict[str, object]:
|
||||
self.rows = list(rows)
|
||||
return {
|
||||
"enabled": True,
|
||||
"table": main.DEFAULT_DB_TABLE,
|
||||
"metric_time": rows[0]["hour_start"],
|
||||
"inserted_rows": len(rows),
|
||||
"refreshed_rows": 0,
|
||||
"old_rows_deleted": 0,
|
||||
}
|
||||
|
||||
|
||||
class FakeCursor:
|
||||
def __init__(self, latest_time: datetime | None, same_hour_rows: int, old_rows: int) -> None:
|
||||
self.latest_time = latest_time
|
||||
self.same_hour_rows = same_hour_rows
|
||||
self.old_rows = old_rows
|
||||
self.rowcount = 0
|
||||
self.inserted: list[tuple[object, ...]] = []
|
||||
self.closed = False
|
||||
|
||||
def execute(self, query: str, params: tuple[object, ...] | None = None) -> None:
|
||||
normalized = " ".join(query.split())
|
||||
if normalized.startswith("DELETE") and "metric_time = %s" in normalized:
|
||||
self.rowcount = self.same_hour_rows
|
||||
elif normalized.startswith("DELETE") and "metric_time <> %s" in normalized:
|
||||
self.rowcount = self.old_rows
|
||||
|
||||
def executemany(self, query: str, values: list[tuple[object, ...]]) -> None:
|
||||
del query
|
||||
self.inserted = list(values)
|
||||
self.rowcount = len(values)
|
||||
|
||||
def fetchone(self) -> tuple[datetime | None]:
|
||||
return (self.latest_time,)
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
class FakeConnection:
|
||||
def __init__(self, latest_time: datetime | None, same_hour_rows: int = 0, old_rows: int = 0) -> None:
|
||||
self.cursor_instance = FakeCursor(latest_time, same_hour_rows, old_rows)
|
||||
self.committed = False
|
||||
self.rolled_back = False
|
||||
self.closed = False
|
||||
|
||||
def cursor(self) -> FakeCursor:
|
||||
return self.cursor_instance
|
||||
|
||||
def commit(self) -> None:
|
||||
self.committed = True
|
||||
|
||||
def rollback(self) -> None:
|
||||
self.rolled_back = True
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
def database_row() -> dict[str, str]:
|
||||
return {
|
||||
"hour_start": "2026-07-31 10:00:00",
|
||||
"hour_end": "2026-07-31 11:00:00",
|
||||
"source_type": "5G干扰监控",
|
||||
"cgi": "46000-100-1",
|
||||
"cell_name": "测试小区",
|
||||
"interference_dbm": "-100.5",
|
||||
"source_path": "/source.zip",
|
||||
}
|
||||
|
||||
|
||||
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]}"
|
||||
|
||||
Reference in New Issue
Block a user