|
15 | 15 |
|
16 | 16 | import os |
17 | 17 | import glob |
| 18 | +import io |
| 19 | +import contextlib |
| 20 | +import re |
18 | 21 | import json |
19 | 22 | import shutil |
20 | 23 | import unittest |
@@ -192,9 +195,76 @@ def test_ph_iter0(self): |
192 | 195 | scenario_denouement, |
193 | 196 | scenario_creator_kwargs={"scenario_count": 3}, |
194 | 197 | ) |
195 | | - |
| 198 | + |
196 | 199 | conv, obj, tbound = ph.ph_main() |
197 | 200 |
|
| 201 | + @unittest.skipIf(not solver_available, |
| 202 | + "no solver is available") |
| 203 | + def test_display_timing_emits_nonzero_solve_times(self): |
| 204 | + # End-to-end check that display_timing actually reaches the PH |
| 205 | + # solve loop and produces a non-zero solve-time report. Guards |
| 206 | + # the user-facing CLI flag (issue #290): if a future refactor |
| 207 | + # quietly drops the option from self.options or the print path, |
| 208 | + # this test fails. |
| 209 | + options = self._copy_of_base_options() |
| 210 | + options["PHIterLimit"] = 0 |
| 211 | + options["display_timing"] = True |
| 212 | + |
| 213 | + ph = mpisppy.opt.ph.PH( |
| 214 | + options, |
| 215 | + self.all3_scenario_names, |
| 216 | + scenario_creator, |
| 217 | + scenario_denouement, |
| 218 | + scenario_creator_kwargs={"scenario_count": 3}, |
| 219 | + ) |
| 220 | + |
| 221 | + buf = io.StringIO() |
| 222 | + with contextlib.redirect_stdout(buf): |
| 223 | + ph.ph_main() |
| 224 | + out = buf.getvalue() |
| 225 | + |
| 226 | + self.assertIn("Pyomo solve times (seconds):", out, |
| 227 | + msg=f"display_timing=True but timing header not in output:\n{out}") |
| 228 | + m = re.search( |
| 229 | + r"min=([0-9.]+)@\d+ mean=([0-9.]+) max=([0-9.]+)@\d+", |
| 230 | + out, |
| 231 | + ) |
| 232 | + self.assertIsNotNone( |
| 233 | + m, |
| 234 | + msg=f"display_timing stats line not found in output:\n{out}", |
| 235 | + ) |
| 236 | + mn, me, mx = float(m.group(1)), float(m.group(2)), float(m.group(3)) |
| 237 | + # max across scenarios must be strictly positive — any real |
| 238 | + # subproblem solve takes measurable wallclock time. min/mean |
| 239 | + # could round to 0.00 under %4.2f if subproblems are tiny. |
| 240 | + self.assertGreater( |
| 241 | + mx, 0.0, |
| 242 | + msg=f"max solve time reported as zero: min={mn} mean={me} max={mx}", |
| 243 | + ) |
| 244 | + |
| 245 | + @unittest.skipIf(not solver_available, |
| 246 | + "no solver is available") |
| 247 | + def test_display_timing_off_suppresses_solve_times(self): |
| 248 | + # Companion: confirm the timing report is NOT printed when the |
| 249 | + # flag is False, so we know the assertion above isn't picking up |
| 250 | + # output emitted unconditionally somewhere. |
| 251 | + options = self._copy_of_base_options() |
| 252 | + options["PHIterLimit"] = 0 |
| 253 | + options["display_timing"] = False |
| 254 | + |
| 255 | + ph = mpisppy.opt.ph.PH( |
| 256 | + options, |
| 257 | + self.all3_scenario_names, |
| 258 | + scenario_creator, |
| 259 | + scenario_denouement, |
| 260 | + scenario_creator_kwargs={"scenario_count": 3}, |
| 261 | + ) |
| 262 | + |
| 263 | + buf = io.StringIO() |
| 264 | + with contextlib.redirect_stdout(buf): |
| 265 | + ph.ph_main() |
| 266 | + self.assertNotIn("Pyomo solve times (seconds):", buf.getvalue()) |
| 267 | + |
198 | 268 | @unittest.skipIf(not solver_available, |
199 | 269 | "no solver is available") |
200 | 270 | def test_fix_ph_iter0(self): |
|
0 commit comments