diff --git a/src/runner.cpp b/src/runner.cpp index a44ee945..66350eee 100644 --- a/src/runner.cpp +++ b/src/runner.cpp @@ -363,8 +363,12 @@ run at the outermost level. } // init_reporter void init_runner(py::module& m) { + // Runner is polymorphic, but Reporter is not, so the Reporter base can + // have a nonzero offset. Force pybind11 to adjust base pointers so that + // inherited Reporter methods access the correct memory. py::class_ thing(m, "Runner", + py::multiple_inheritance(), R"pbdoc( Abstract class for derived [#sortof]_ classes that run an algorithm. diff --git a/tests/test_runner.py b/tests/test_runner.py index 31c3e7b1..d198b0a0 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -12,7 +12,15 @@ import pytest -from libsemigroups_pybind11 import Reporter +from libsemigroups_pybind11 import ( + FroidurePin, + KnuthBendix, + Presentation, + Reporter, + ToddCoxeter, + Transf, + congruence_kind, +) def test_reporter_000(): @@ -44,3 +52,65 @@ def test_reporter_000(): assert s.report_every() == timedelta(seconds=1) assert s.report_every() == timedelta(seconds=1) + + +@pytest.fixture( + name="reporter", + params=["Reporter", "FroidurePin", "ToddCoxeter", "KnuthBendixSet", "KnuthBendixTrie"], +) +def make_reporter(request): + """Return a reporter, including algorithms with inherited reporting methods.""" + if request.param == "Reporter": + return Reporter() + if request.param == "FroidurePin": + return FroidurePin(Transf([1, 0])) + if request.param == "ToddCoxeter": + return ToddCoxeter(congruence_kind.twosided, Presentation("ab")) + return KnuthBendix( + congruence_kind.twosided, + Presentation("ab"), + rewriting_system=request.param.removeprefix("KnuthBendix"), + ) + + +@pytest.mark.quick +def test_reporter_inherited_report_every(reporter): + """Inherited reporting methods must access the Reporter base subobject.""" + assert reporter.report_every() == timedelta(seconds=1) + reporter.report_every(timedelta(seconds=2)) + assert reporter.report_every() == timedelta(seconds=2) + + +@pytest.mark.quick +def test_reporter_inherited_report_prefix(reporter): + """Both prefix overloads must access the correct reporting prefix.""" + expected = "" if isinstance(reporter, Reporter) else type(reporter).__name__ + assert reporter.report_prefix() == expected + for prefix in ("a report prefix longer than a small string buffer", "foo", ""): + reporter.report_prefix(prefix) + assert reporter.report_prefix() == prefix + + +@pytest.mark.quick +def test_reporter_inherited_reset_last_report(reporter): + """Resetting the last report must preserve the start time.""" + start = reporter.start_time() + before = datetime.now() + reporter.reset_last_report() + with pytest.deprecated_call(): + last_report = reporter.last_report() + assert before <= last_report <= datetime.now() + assert abs(reporter.start_time() - start) < timedelta(milliseconds=1) + + +@pytest.mark.quick +def test_reporter_inherited_reset_start_time(reporter): + """Resetting the start time must also reset the last report time.""" + before = datetime.now() + reporter.reset_start_time() + start = reporter.start_time() + with pytest.deprecated_call(): + last_report = reporter.last_report() + assert before <= start <= datetime.now() + # Each conversion from high_resolution_clock samples system_clock anew. + assert abs(last_report - start) < timedelta(milliseconds=1)