| | """Unit tests for wall construction simulator.""" |
| |
|
| | from __future__ import annotations |
| |
|
| | from datetime import date |
| | from decimal import Decimal |
| | from pathlib import Path |
| |
|
| | import pytest |
| |
|
| | from apps.profiles.models import DailyProgress, Profile, WallSection |
| | from apps.profiles.services.simulator import ProfileConfig, SectionData, WallSimulator |
| |
|
| |
|
| | @pytest.mark.django_db |
| | class TestWallSimulator: |
| | """Test cases for WallSimulator.""" |
| |
|
| | def test_initialize_single_profile(self, tmp_path: Path) -> None: |
| | """Test initializing simulator with single profile.""" |
| | profiles_config = [ProfileConfig(heights=[21, 25, 28])] |
| |
|
| | simulator = WallSimulator(num_teams=2, log_dir=str(tmp_path)) |
| | section_data = simulator._initialize_profiles(profiles_config) |
| |
|
| | assert len(section_data) == 3 |
| | assert Profile.objects.count() == 1 |
| | assert WallSection.objects.count() == 3 |
| |
|
| | profile = Profile.objects.first() |
| | assert profile is not None |
| | assert profile.name == "Profile 1" |
| |
|
| | def test_initialize_multiple_profiles(self, tmp_path: Path) -> None: |
| | """Test initializing simulator with multiple profiles.""" |
| | profiles_config = [ |
| | ProfileConfig(heights=[21, 25, 28]), |
| | ProfileConfig(heights=[17]), |
| | ProfileConfig(heights=[17, 22, 17, 19, 17]), |
| | ] |
| |
|
| | simulator = WallSimulator(num_teams=4, log_dir=str(tmp_path)) |
| | section_data = simulator._initialize_profiles(profiles_config) |
| |
|
| | assert len(section_data) == 9 |
| | assert Profile.objects.count() == 3 |
| | assert WallSection.objects.count() == 9 |
| |
|
| | def test_assign_work_limits_by_teams(self, tmp_path: Path) -> None: |
| | """Test that work assignment respects team limit.""" |
| | section_data = [ |
| | SectionData( |
| | id=i, |
| | profile_id=1, |
| | profile_name="Profile 1", |
| | section_name=f"Section {i}", |
| | current_height=20, |
| | ) |
| | for i in range(10) |
| | ] |
| |
|
| | simulator = WallSimulator(num_teams=3, log_dir=str(tmp_path)) |
| | assigned = simulator._assign_work(section_data) |
| |
|
| | assert len(assigned) == 3 |
| |
|
| | def test_assign_work_skips_completed_sections(self, tmp_path: Path) -> None: |
| | """Test that completed sections are not assigned.""" |
| | section_data = [ |
| | SectionData(id=1, profile_id=1, profile_name="Profile 1", section_name="Section 1", current_height=30), |
| | SectionData(id=2, profile_id=1, profile_name="Profile 1", section_name="Section 2", current_height=25), |
| | SectionData(id=3, profile_id=1, profile_name="Profile 1", section_name="Section 3", current_height=30), |
| | SectionData(id=4, profile_id=1, profile_name="Profile 1", section_name="Section 4", current_height=28), |
| | ] |
| |
|
| | simulator = WallSimulator(num_teams=5, log_dir=str(tmp_path)) |
| | assigned = simulator._assign_work(section_data) |
| |
|
| | assert len(assigned) == 2 |
| | assert all(s.current_height < 30 for s in assigned) |
| |
|
| | def test_process_section_calculates_correctly(self, tmp_path: Path) -> None: |
| | """Test that section processing calculates ice and cost correctly.""" |
| | section = SectionData( |
| | id=1, |
| | profile_id=1, |
| | profile_name="Profile 1", |
| | section_name="Section 1", |
| | current_height=20, |
| | ) |
| |
|
| | simulator = WallSimulator(num_teams=1, log_dir=str(tmp_path)) |
| | result = simulator._process_section(section, day=1, team_id=0) |
| |
|
| | assert result.section_id == 1 |
| | assert result.feet_built == Decimal("1") |
| | assert result.ice == Decimal("195") |
| | assert result.cost == Decimal("370500") |
| |
|
| | def test_process_section_writes_log(self, tmp_path: Path) -> None: |
| | """Test that section processing writes to log file.""" |
| | section = SectionData( |
| | id=1, |
| | profile_id=1, |
| | profile_name="Profile 1", |
| | section_name="Section 1", |
| | current_height=20, |
| | ) |
| |
|
| | simulator = WallSimulator(num_teams=1, log_dir=str(tmp_path)) |
| | simulator._process_section(section, day=1, team_id=0) |
| |
|
| | log_file = tmp_path / "team_0.log" |
| | assert log_file.exists() |
| |
|
| | log_content = log_file.read_text() |
| | assert "Day 1" in log_content |
| | assert "Section 1" in log_content |
| |
|
| | def test_process_section_logs_completion(self, tmp_path: Path) -> None: |
| | """Test that section completion is logged.""" |
| | section = SectionData( |
| | id=1, |
| | profile_id=1, |
| | profile_name="Profile 1", |
| | section_name="Section 1", |
| | current_height=29, |
| | ) |
| |
|
| | simulator = WallSimulator(num_teams=1, log_dir=str(tmp_path)) |
| | simulator._process_section(section, day=1, team_id=0) |
| |
|
| | log_file = tmp_path / "team_0.log" |
| | log_content = log_file.read_text() |
| | assert "completed" in log_content |
| |
|
| | def test_simulate_simple_profile(self, tmp_path: Path) -> None: |
| | """Test complete simulation with simple profile.""" |
| | profiles_config = [ProfileConfig(heights=[28, 29])] |
| |
|
| | simulator = WallSimulator(num_teams=2, log_dir=str(tmp_path)) |
| | result = simulator.simulate(profiles_config, date(2025, 10, 20)) |
| |
|
| | assert result.total_days == 2 |
| | assert result.total_sections == 2 |
| |
|
| | |
| | assert DailyProgress.objects.count() == 3 |
| |
|
| | profile = Profile.objects.first() |
| | assert profile is not None |
| |
|
| | sections = WallSection.objects.filter(profile=profile) |
| | for section in sections: |
| | assert section.current_height == 30 |
| |
|
| | def test_simulate_creates_logs(self, tmp_path: Path) -> None: |
| | """Test that simulation creates team log files.""" |
| | profiles_config = [ProfileConfig(heights=[28])] |
| |
|
| | simulator = WallSimulator(num_teams=2, log_dir=str(tmp_path)) |
| | simulator.simulate(profiles_config, date(2025, 10, 20)) |
| |
|
| | assert (tmp_path / "team_0.log").exists() |
| | assert (tmp_path / "team_1.log").exists() |
| |
|
| | log_content = (tmp_path / "team_0.log").read_text() |
| | assert "relieved" in log_content |
| |
|
| | def test_simulate_respects_team_limit(self, tmp_path: Path) -> None: |
| | """Test that simulation respects team limit.""" |
| | profiles_config = [ProfileConfig(heights=[25, 25, 25, 25, 25])] |
| |
|
| | simulator = WallSimulator(num_teams=2, log_dir=str(tmp_path)) |
| | simulator.simulate(profiles_config, date(2025, 10, 20)) |
| |
|
| | progress_by_day: dict[str, int] = {} |
| | for progress in DailyProgress.objects.all(): |
| | day_key = progress.date.isoformat() |
| | progress_by_day[day_key] = progress_by_day.get(day_key, 0) + 1 |
| |
|
| | for count in progress_by_day.values(): |
| | assert count <= 2 |
| |
|
| | def test_simulate_with_varying_heights(self, tmp_path: Path) -> None: |
| | """Test simulation with varying initial heights.""" |
| | profiles_config = [ProfileConfig(heights=[21, 25, 28, 30, 17])] |
| |
|
| | simulator = WallSimulator(num_teams=3, log_dir=str(tmp_path)) |
| | result = simulator.simulate(profiles_config, date(2025, 10, 20)) |
| |
|
| | assert result.total_sections == 5 |
| |
|
| | sections_with_height_30 = WallSection.objects.filter(initial_height=30, current_height=30).count() |
| | assert sections_with_height_30 == 1 |
| |
|
| | all_sections = WallSection.objects.all() |
| | for section in all_sections: |
| | assert section.current_height == 30 |
| |
|