diff --git a/tests/test_face_tracker.cpp b/tests/test_face_tracker.cpp index 587a7b1..ca8cc32 100644 --- a/tests/test_face_tracker.cpp +++ b/tests/test_face_tracker.cpp @@ -192,6 +192,13 @@ TEST_CASE("a track within the extinction window is still a candidate", Embedding person = at_sim(0, 1, 0.99f); int id_pre = r.track_of(frame(0.0, 10, 10, person)); + + // Identify it while it is still on screen. Since 1477c53 a DORMANT track is + // only associable if it was owned -- an anonymous one has no actor to + // re-attach to, so it is dropped from the pool a line before the horizon is + // ever consulted. Without this the test cannot reach what it is about. + r.reg->observe(id_pre, /*actor=*/0, /*posterior=*/0.99f, person); + r.track_of(frame(1.0, 300, 300, axis(7), /*is_cut=*/true)); // Back inside the window: the same person continues the same track, so the diff --git a/tests/test_track_registry.cpp b/tests/test_track_registry.cpp index 4c99178..20a9deb 100644 --- a/tests/test_track_registry.cpp +++ b/tests/test_track_registry.cpp @@ -415,15 +415,21 @@ TEST_CASE("a track is not reaped until the evidence clock passes it", } /// TRACES: UT-001 | AR-008, AR-013 | SR-002 -TEST_CASE("a track retired from association is still open to evidence", +TEST_CASE("association and reaping retire a track on the same clock", "[registry][AR-008]") { - // The two clocks answer different questions and must not share an answer. - // Association asks "may this detection link to that track?" on the tracker's - // clock; reaping asks "is that track finished?" and cannot answer until the - // votes are in. Deferring both to the evidence clock was the second half of - // this bug: retired tracks lingered in the candidate pool for as long as the - // matcher lagged, so a new face re-associated onto a long-dead track and two - // people merged into one window. + // Renamed and rewritten for the unified clock. This test used to assert the + // opposite arrangement -- association answered on the tracker's clock while + // reaping answered on the evidence watermark -- and it was written when that + // was true. It is not true now: candidates() and reap_locked() take the SAME + // clock and the SAME threshold, so "retired from association" and "reaped" + // are one event, and the interval the old name described does not exist. + // + // Both halves of the AR-013 bug are visible here. Offering on a LOOSER + // horizon than the reap left retired tracks in the candidate pool for as + // long as the matcher lagged, so a new face re-associated onto a long-dead + // track and two people merged into one window. Reaping on a looser horizon + // than the offer killed tracks whose votes were still in flight. One clock + // makes both unrepresentable. TrackRegistry reg(cfg(/*extinction=*/5.0), disc()); Sink sink; sink.attach(reg); reg.expect_evidence(); @@ -432,21 +438,37 @@ TEST_CASE("a track retired from association is still open to evidence", { auto s = reg.begin_frame(0.0); id = s.create(0.0, axis(1)); + } + // Owned before it goes dormant, for the reason above: an unidentified + // dormant track is not a candidate at any horizon (1477c53). + reg.observe(id, 7, 0.99f, axis(1)); + { + auto s = reg.begin_frame(0.0); s.mark_lost(id, 0.0); } { - auto s = reg.begin_frame(3.0); // inside the window + auto s = reg.begin_frame(3.0); // watermark inside the window CHECK(s.candidates().size() == 1); // still associable } + // The tracker races far past the horizon while the matcher lags. This + // retires nothing, and that is the point of the watermark: the clock that + // decides has not moved, so the track is still offered. { - auto s = reg.begin_frame(50.0); // far outside it - CHECK(s.candidates().empty()); // retired from association... + auto s = reg.begin_frame(50.0); + CHECK(s.candidates().size() == 1); } - // ...but not gone, and still able to receive the votes in flight for it. + // Which is what keeps it able to receive the votes still in flight for it. reg.observe(id, 7, 0.99f, axis(1)); CHECK(reg.dropped_votes() == 0); + + // Only the watermark passing last_seen + extinction retires it -- and the + // same call reaps and emits it, so there is no interval in between. reg.advance_evidence(50.0); + { + auto s = reg.begin_frame(50.0); + CHECK(s.candidates().empty()); + } REQUIRE(sink.claims.size() == 1); CHECK(sink.claims[0].actor_idx == 7); }