11import json
2+ import random
23from datetime import datetime , timedelta
34
4- from virtualship .make_realistic .problems .scenarios import GeneralProblem
5+ from virtualship .instruments .types import InstrumentType
6+ from virtualship .make_realistic .problems .scenarios import (
7+ GeneralProblem ,
8+ InstrumentProblem ,
9+ )
510from virtualship .make_realistic .problems .simulator import ProblemSimulator
611from virtualship .models .expedition import (
712 Expedition ,
1116 Waypoint ,
1217)
1318from virtualship .models .location import Location
14- from virtualship .utils import GENERAL_PROBLEM_REG
19+ from virtualship .utils import GENERAL_PROBLEM_REG , REPORT
1520
1621
1722def _make_simple_expedition (
18- num_waypoints : int = 2 , distance_scale : float = 1.0
23+ num_waypoints : int = 2 , distance_scale : float = 1.0 , no_instruments : bool = False
1924) -> Expedition :
25+ """Func. rather than fixture to allow for configurability in different tests."""
2026 sample_datetime = datetime (2024 , 1 , 1 , 0 , 0 , 0 )
27+ instruments_non_underway = [inst for inst in InstrumentType if not inst .is_underway ]
28+
2129 waypoints = []
2230 for i in range (num_waypoints ):
2331 wp = Waypoint (
2432 location = Location (
2533 latitude = 0.0 + i * distance_scale , longitude = 0.0 + i * distance_scale
2634 ),
2735 time = sample_datetime + timedelta (days = i ),
28- instrument = [], # ensure is list, not None
36+ instrument = []
37+ if no_instruments
38+ else random .sample (instruments_non_underway , 3 ),
2939 )
3040 waypoints .append (wp )
3141
@@ -39,8 +49,9 @@ def _make_simple_expedition(
3949
4050def test_select_problems_single_waypoint_returns_pre_departure (tmp_path ):
4151 expedition = _make_simple_expedition (num_waypoints = 1 )
52+ instruments_in_expedition = expedition .get_instruments ()
4253 simulator = ProblemSimulator (expedition , str (tmp_path ))
43- problems = simulator .select_problems (set () , prob_level = 2 )
54+ problems = simulator .select_problems (instruments_in_expedition , prob_level = 2 )
4455
4556 assert isinstance (problems , dict )
4657 assert len (problems ["problem_class" ]) == 1
@@ -51,11 +62,28 @@ def test_select_problems_single_waypoint_returns_pre_departure(tmp_path):
5162 assert getattr (problem_cls , "pre_departure" , False ) is True
5263
5364
65+ def test_no_instruments_no_instruments_problems (tmp_path ):
66+ expedition = _make_simple_expedition (num_waypoints = 2 , no_instruments = True )
67+ instruments_in_expedition = expedition .get_instruments ()
68+ assert len (instruments_in_expedition ) == 0 , "Expedition should have no instruments"
69+
70+ simulator = ProblemSimulator (expedition , str (tmp_path ))
71+ problems = simulator .select_problems (instruments_in_expedition , prob_level = 2 )
72+
73+ has_instrument_problems = any (
74+ issubclass (cls , InstrumentProblem ) for cls in problems ["problem_class" ]
75+ )
76+ assert not has_instrument_problems , (
77+ "Should not select instrument problems when no instruments are present"
78+ )
79+
80+
5481def test_select_problems_prob_level_zero ():
5582 expedition = _make_simple_expedition (num_waypoints = 2 )
83+ instruments_in_expedition = expedition .get_instruments ()
5684 simulator = ProblemSimulator (expedition , "." )
5785
58- problems = simulator .select_problems (set () , prob_level = 0 )
86+ problems = simulator .select_problems (instruments_in_expedition , prob_level = 0 )
5987 assert problems is None
6088
6189
@@ -64,10 +92,10 @@ def test_cache_and_load_selected_problems_roundtrip(tmp_path):
6492 simulator = ProblemSimulator (expedition , str (tmp_path ))
6593
6694 # pick two general problems (registry should contain entries)
67- cls1 = GENERAL_PROBLEM_REG [0 ]
68- cls2 = GENERAL_PROBLEM_REG [1 ] if len (GENERAL_PROBLEM_REG ) > 1 else cls1
95+ problem1 = GENERAL_PROBLEM_REG [0 ]
96+ problem2 = GENERAL_PROBLEM_REG [1 ] if len (GENERAL_PROBLEM_REG ) > 1 else problem1
6997
70- problems = {"problem_class" : [cls1 , cls2 ], "waypoint_i" : [None , 0 ]}
98+ problems = {"problem_class" : [problem1 , problem2 ], "waypoint_i" : [None , 0 ]}
7199
72100 sel_fpath = tmp_path / "subdir" / "selected_problems.json"
73101 simulator .cache_selected_problems (problems , str (sel_fpath ))
@@ -89,11 +117,11 @@ def test_hash_to_json(tmp_path):
89117 expedition = _make_simple_expedition (num_waypoints = 2 )
90118 simulator = ProblemSimulator (expedition , str (tmp_path ))
91119
92- cls = GENERAL_PROBLEM_REG [0 ]
120+ any_problem = GENERAL_PROBLEM_REG [0 ]
93121
94122 hash_path = tmp_path / "problem_hash.json"
95123 simulator ._hash_to_json (
96- cls , "deadbeef" , None , hash_path
124+ any_problem , "deadbeef" , None , hash_path
97125 ) # "deadbeef" as sub for hex in test
98126
99127 assert hash_path .exists ()
@@ -107,18 +135,27 @@ def test_hash_to_json(tmp_path):
107135def test_has_contingency_pre_departure (tmp_path ):
108136 expedition = _make_simple_expedition (num_waypoints = 2 )
109137 simulator = ProblemSimulator (expedition , str (tmp_path ))
110- cls = GENERAL_PROBLEM_REG [0 ]
111138
112- # _has_contingency should return False for pre-departure (None)
113- assert simulator ._has_contingency (cls , None ) is False
139+ pre_departure_problem = next (
140+ gp for gp in GENERAL_PROBLEM_REG if getattr (gp , "pre_departure" , False )
141+ )
142+ assert pre_departure_problem is not None , (
143+ "Need at least one pre-departure problem class in the general problem registry"
144+ )
145+
146+ # _has_contingency should return False for pre-departure (waypoint = None)
147+ assert simulator ._has_contingency (pre_departure_problem , None ) is False
114148
115149
116150def test_select_problems_prob_levels (tmp_path ):
117151 expedition = _make_simple_expedition (num_waypoints = 3 )
152+ instruments_in_expedition = expedition .get_instruments ()
118153 simulator = ProblemSimulator (expedition , str (tmp_path ))
119154
120155 for level in range (3 ): # prob levels 0, 1, 2
121- problems = simulator .select_problems (set (), prob_level = level )
156+ problems = simulator .select_problems (
157+ instruments_in_expedition , prob_level = level
158+ )
122159 if level == 0 :
123160 assert problems is None
124161 else :
@@ -132,13 +169,22 @@ def test_select_problems_prob_levels(tmp_path):
132169def test_prob_level_two_more_problems (tmp_path ):
133170 prob_level = 2
134171
135- short_expedition = _make_simple_expedition (num_waypoints = 2 )
172+ short_expedition = _make_simple_expedition (
173+ num_waypoints = 2
174+ ) # short in terms of number of waypoints
175+ instruments_in_short_expedition = short_expedition .get_instruments ()
136176 simulator_short = ProblemSimulator (short_expedition , str (tmp_path ))
177+
137178 long_expedition = _make_simple_expedition (num_waypoints = 12 )
179+ instruments_in_long_expedition = long_expedition .get_instruments ()
138180 simulator_long = ProblemSimulator (long_expedition , str (tmp_path ))
139181
140- problems_short = simulator_short .select_problems (set (), prob_level = prob_level )
141- problems_long = simulator_long .select_problems (set (), prob_level = prob_level )
182+ problems_short = simulator_short .select_problems (
183+ instruments_in_short_expedition , prob_level = prob_level
184+ )
185+ problems_long = simulator_long .select_problems (
186+ instruments_in_long_expedition , prob_level = prob_level
187+ )
142188
143189 assert len (problems_long ["problem_class" ]) >= len (
144190 problems_short ["problem_class" ]
@@ -165,3 +211,31 @@ def test_has_contingency_during_expedition(tmp_path):
165211 # short distance expedition should have contingency, long distance should not (given time between waypoints and ship speed is constant)
166212 assert short_simulator ._has_contingency (problem_cls , problem_waypoint_i = 0 ) is True
167213 assert long_simulator ._has_contingency (problem_cls , problem_waypoint_i = 0 ) is False
214+
215+
216+ def test_post_expedition_report (tmp_path ):
217+ expedition = _make_simple_expedition (
218+ num_waypoints = 12
219+ ) # longer expedition to increase likelihood of multiple problems at prob_level=2
220+ instruments_in_expedition = expedition .get_instruments ()
221+
222+ simulator = ProblemSimulator (expedition , str (tmp_path ))
223+ problems = simulator .select_problems (instruments_in_expedition , prob_level = 2 )
224+
225+ report_path = tmp_path / REPORT
226+ simulator .post_expedition_report (problems , report_path )
227+
228+ assert report_path .exists ()
229+ with open (report_path , encoding = "utf-8" ) as f :
230+ content = f .read ()
231+
232+ assert content .count ("Problem:" ) == len (problems ["problem_class" ]), (
233+ "Number of reported problems should match number of selected problems."
234+ )
235+ assert content .count ("Delay caused:" ) == len (problems ["problem_class" ]), (
236+ "Number of reported delay durations should match number of selected problems."
237+ )
238+ for problem in problems ["problem_class" ]:
239+ assert problem .message in content , (
240+ "Problem messages in report should match those of selected problems."
241+ )
0 commit comments