import sqlite3 import tempfile import unittest from contextlib import closing from pathlib import Path from unittest.mock import patch import backward_backfill def make_plant(site_id="plant-a", start_date="2026-08-01"): return { "id": site_id, "name": site_id, "type": "fake", "start_date": start_date, "options": {}, } class BackwardBackfillTest(unittest.TestCase): def setUp(self): self.temp_dir = tempfile.TemporaryDirectory() self.db_path = str(Path(self.temp_dir.name) / "crawler_manager.db") self.db_path_patch = patch.object(backward_backfill, "DB_PATH", self.db_path) self.db_path_patch.start() def tearDown(self): self.db_path_patch.stop() self.temp_dir.cleanup() def read_state(self, site_id): with closing(sqlite3.connect(self.db_path)) as conn: return conn.execute( """ SELECT last_backfilled_date, consecutive_zero_count, status FROM backfill_state WHERE site_id = ? """, (site_id,), ).fetchone() def init_sites(self, plants, first_target="2026-08-05"): with patch.object(backward_backfill, "get_all_plants", return_value=plants): backward_backfill.init_backfill_states(first_target) def run_process(self, plants, crawler, save_result=True, days=1): with ( patch.object(backward_backfill, "get_all_plants", return_value=plants), patch.object(backward_backfill, "get_history_crawler", return_value=crawler), patch.object(backward_backfill, "save_history", return_value=save_result), ): backward_backfill.process_backfill( max_days_per_run=days, delay_sec=0, ) def test_first_requested_date_is_not_skipped(self): plant = make_plant() requested_dates = [] self.init_sites([plant]) self.assertEqual(self.read_state("plant-a")[0], "2026-08-06") def crawler(_plant, start_date, _end_date): requested_dates.append(start_date) return [{ "plant_id": "plant-a", "date": start_date, "generation_kwh": 12.0, }] self.run_process([plant], crawler) self.assertEqual(requested_dates, ["2026-08-05"]) self.assertEqual(self.read_state("plant-a"), ("2026-08-05", 0, "RUNNING")) def test_fetch_failure_keeps_cursor_and_zero_count(self): plant = make_plant() self.init_sites([plant]) backward_backfill.update_backfill_state("plant-a", "2026-08-06", 7, "RUNNING") self.run_process([plant], lambda *_args: None) self.assertEqual(self.read_state("plant-a"), ("2026-08-06", 7, "RUNNING")) def test_save_failure_keeps_cursor_and_zero_count(self): plant = make_plant() self.init_sites([plant]) def crawler(_plant, start_date, _end_date): return [{ "plant_id": "plant-a", "date": start_date, "generation_kwh": 5.0, }] self.run_process([plant], crawler, save_result=False) self.assertEqual(self.read_state("plant-a"), ("2026-08-06", 0, "RUNNING")) def test_successful_no_data_advances_without_counting_zero(self): plant = make_plant() self.init_sites([plant]) backward_backfill.update_backfill_state("plant-a", "2026-08-06", 4, "RUNNING") self.run_process([plant], lambda *_args: []) self.assertEqual(self.read_state("plant-a"), ("2026-08-05", 0, "RUNNING")) def test_thirtieth_actual_zero_completes_site(self): plant = make_plant(start_date="2026-01-01") self.init_sites([plant], first_target="2026-08-30") backward_backfill.update_backfill_state("plant-a", "2026-08-31", 29, "RUNNING") def crawler(_plant, start_date, _end_date): return [{ "plant_id": "plant-a", "date": start_date, "generation_kwh": 0.0, }] self.run_process([plant], crawler) self.assertEqual(self.read_state("plant-a"), ("2026-08-30", 30, "COMPLETED")) def test_one_site_failure_does_not_block_another_site(self): plants = [make_plant("plant-a"), make_plant("plant-b")] self.init_sites(plants) def crawler(plant, start_date, _end_date): if plant["id"] == "plant-a": return None return [{ "plant_id": "plant-b", "date": start_date, "generation_kwh": 9.0, }] self.run_process(plants, crawler) self.assertEqual(self.read_state("plant-a")[0], "2026-08-06") self.assertEqual(self.read_state("plant-b")[0], "2026-08-05") def test_start_date_is_processed_before_completion(self): plant = make_plant(start_date="2026-08-05") self.init_sites([plant]) def crawler(_plant, start_date, _end_date): return [{ "plant_id": "plant-a", "date": start_date, "generation_kwh": 1.0, }] self.run_process([plant], crawler, days=2) self.assertEqual(self.read_state("plant-a"), ("2026-08-05", 0, "COMPLETED")) if __name__ == "__main__": unittest.main()