broker: a cancelled dark-mask collection is not a finished calibration

DarkMaskAnalysis::GetMask always returns a full-size array - a pixel that saw no frame reads as
good - so the dark_mask_result.size() check in TakeDarkMaskInternal accepted a mask measured on
nothing. The mask was adopted through LoadDarkBadPixelMask, the state went to Idle and the
sequence logged "Calibration sequence done" on a calibration the operator had just cancelled.
The dark mask is a single step, so unlike a pedestal sequence there is no later cancel_sequence
check to catch it.

Refuse it after Stop() the way the check at the top of the same function does: Inactive with the
"Mask sequence cancelled" error, matching what the pedestal steps report.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bew392LTGP2fkhfRJsMcB
This commit is contained in:
2026-09-11 18:53:53 +02:00
co-authored by Claude Opus 5
parent c40e0eb11d
commit 1da94bdfe2
3 changed files with 68 additions and 0 deletions
+55
View File
@@ -551,3 +551,58 @@ TEST_CASE("JFJochStateMachine_PreflightRefusesBeforeStarting") {
Catch::Matchers::ContainsSubstring("preflight_refused_2468"));
REQUIRE_FALSE(pusher.start_called);
}
namespace {
class NullPusher : public ImagePusher {
public:
void StartDataCollection(StartMessage &) override {}
bool EndDataCollection(const EndMessage &) override { return true; }
bool SendImage(const uint8_t *, size_t, int64_t) override { return true; }
bool SendCalibration(const CompressedImage &) override { return true; }
std::string PrintSetup() const override { return "NullPusher"; }
ImagePusherType GetType() const override { return ImagePusherType::Test; }
};
}
// Cancelling the dark-mask collection - the window between the receiver being started and Stop()
// returning, which is where a calibration spends its time - must abandon the sequence. The mask
// analysis always hands back a full-size array, every pixel that saw no frame reading as good, so
// the size check the sequence relies on accepts a mask measured on nothing; and the dark mask is a
// single step, so no later cancel_sequence check catches it. The calibration used to end Idle with
// "Calibration sequence done" on a mask it never measured.
TEST_CASE("JFJochStateMachine_DarkMaskCancelIsNotASuccess") {
Logger logger("JFJochStateMachine_DarkMaskCancelIsNotASuccess");
DarkMaskSettings mask_settings;
mask_settings.NumberOfFrames(100).MaxCounts(1).MaxFramesWithCounts(5);
// Nothing listens on the stream address, so no start message ever arrives and the collection
// stays in the window under test until it is cancelled.
DiffractionExperiment experiment(DetDECTRIS(1024, 1024, "Test", "127.0.0.1"));
experiment.ImportDarkMaskSettings(mask_settings);
AcquisitionDeviceGroup aq_devices;
NullPusher pusher;
JFJochReceiverService receiver_service(aq_devices, logger, pusher);
JFJochServices services(logger);
services.Receiver(&receiver_service);
JFJochStateMachine state_machine(experiment, services, logger);
state_machine.AddDetectorSetup(DetDECTRIS(1024, 1024, "Test", "127.0.0.1"));
state_machine.DebugOnly_SetState(JFJochState::Idle);
REQUIRE_NOTHROW(state_machine.Pedestal());
for (int i = 0; i < 400 && state_machine.GetStatus().state != JFJochState::Calibration; i++)
std::this_thread::sleep_for(25ms);
REQUIRE(state_machine.GetStatus().state == JFJochState::Calibration);
state_machine.Cancel();
REQUIRE_NOTHROW(state_machine.WaitTillMeasurementDone(std::chrono::seconds(30)));
auto status = state_machine.GetStatus();
REQUIRE(status.state != JFJochState::Idle);
REQUIRE(status.message_severity == BrokerStatus::MessageSeverity::Error);
REQUIRE_THAT(status.message.value_or(""), Catch::Matchers::ContainsSubstring("cancelled"));
}