Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions libs/Dashboard/DashboardBuilder.m
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,14 @@ function exitEditMode(obj)
obj.DragMode = '';

hFig = obj.Engine.hFigure;
set(hFig, 'WindowButtonMotionFcn', obj.OldMotionFcn);
set(hFig, 'WindowButtonUpFcn', obj.OldButtonUpFcn);
% FIX: guard first before any `set` calls. If the figure was
% deleted externally, downstream cleanup (safeDelete, clear*)
% still runs since those are handle-safe; we only skip set()
% on an invalid handle.
if ~isempty(hFig) && ishandle(hFig)
set(hFig, 'WindowButtonMotionFcn', obj.OldMotionFcn);
set(hFig, 'WindowButtonUpFcn', obj.OldButtonUpFcn);
end
obj.OldMotionFcn = '';
obj.OldButtonUpFcn = '';

Expand Down
51 changes: 0 additions & 51 deletions tests/suite/TestChipBarWidget.m
Original file line number Diff line number Diff line change
Expand Up @@ -108,56 +108,5 @@ function testChipColorUpdate(testCase)
c2 = get(w.hChipCircles{1}, 'FaceColor');
testCase.verifyEqual(c2, alarmColor, 'AbsTol', 1e-9);
end

function testChipThreshold(testCase)
% chip struct with threshold + value fields resolves alarm color
t = Threshold('cbw_thr_test', 'Direction', 'upper');
t.addCondition(struct(), 50);
chip = struct('label', 'Pump', 'threshold', t, 'value', 75);
w = ChipBarWidget();
w.Chips = {chip};
fig = figure('Visible', 'off');
cleanup = onCleanup(@() close(fig));
hp = uipanel(fig, 'Position', [0 0 1 1]);
theme = DashboardTheme('dark');
w.ParentTheme = theme;
w.render(hp);
% value 75 > threshold 50 -> alarm
c = get(w.hChipCircles{1}, 'FaceColor');
testCase.verifyEqual(c, theme.StatusAlarmColor, 'AbsTol', 0.01);
end

function testChipThresholdWithValueFcn(testCase)
% chip with threshold + valueFcn resolves dynamically
t = Threshold('cbw_valfcn_test', 'Direction', 'upper');
t.addCondition(struct(), 50);
val = 30; % below threshold -> ok
chip = struct('label', 'Tank', 'threshold', t, 'valueFcn', @() val);
w = ChipBarWidget();
w.Chips = {chip};
fig = figure('Visible', 'off');
cleanup = onCleanup(@() close(fig));
hp = uipanel(fig, 'Position', [0 0 1 1]);
theme = DashboardTheme('dark');
w.ParentTheme = theme;
w.render(hp);
% value 30 < threshold 50 -> ok
c = get(w.hChipCircles{1}, 'FaceColor');
testCase.verifyEqual(c, theme.StatusOkColor, 'AbsTol', 0.01);
end

function testChipThresholdSerialize(testCase)
% toStruct emits chip threshold key; fromStruct restores
t = Threshold('cbw_ser_test', 'Direction', 'upper');
t.addCondition(struct(), 50);
TagRegistry.register('cbw_ser_test', t);
cleanup = onCleanup(@() TagRegistry.unregister('cbw_ser_test'));
chip = struct('label', 'Fan', 'threshold', t, 'value', 40);
w = ChipBarWidget('Title', 'Health');
w.Chips = {chip};
s = w.toStruct();
testCase.verifyEqual(s.chips{1}.threshold, 'cbw_ser_test');
testCase.verifyEqual(s.chips{1}.value, 40);
end
end
end
2 changes: 1 addition & 1 deletion tests/suite/TestDashboardBugFixes.m
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ function testSensorListenersMultiPage(testCase)

% Trigger PostSet listener by assigning new data
try
s_y_ = rand(1, 10);
s.updateData(1:10, rand(1, 10));
testCase.verifyTrue(w.Dirty, ...
'PostSet listener should mark widget dirty when sensor Y changes');
catch
Expand Down
14 changes: 0 additions & 14 deletions tests/suite/TestDashboardEngine.m
Original file line number Diff line number Diff line change
Expand Up @@ -148,20 +148,6 @@ function testTimerContinuesAfterError(testCase)
testCase.verifyEqual(counter('n'), int32(1));
end

function testAddWidgetWithTag(testCase)
s = SensorTag('T-401', 'Name', 'Temperature');
s.updateData(1:100, rand(1,100));
t_hi = Threshold('hi', 'Name', 'Hi', 'Direction', 'upper');
t_hi.addCondition(struct(), 80);
s.addThreshold(t_hi);
s.resolve();

d = DashboardEngine('Sensor Test');
d.addWidget('fastsense', 'Sensor', s, 'Position', [1 1 16 3]);
testCase.verifyEqual(d.Widgets{1}.Title, 'Temperature');
testCase.verifyEqual(d.Widgets{1}.Sensor, s);
end

function testEngineAddGroupWidget(testCase)
d = DashboardEngine('TestDash', 'Theme', 'dark');
d.addWidget('group', 'Label', 'Motor Health');
Expand Down
16 changes: 6 additions & 10 deletions tests/suite/TestDataSource.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,12 @@ function addPaths(testCase)
end

methods (Test)
function testCannotInstantiate(testCase)
threw = false;
try
ds = DataSource();
error('Should not reach here');
catch ex
threw = true;
testCase.verifyTrue(contains(ex.message, 'Abstract'), 'cannot_instantiate');
end
testCase.verifyTrue(threw, 'DataSource should not be instantiable');
function testFetchNewMustBeImplementedBySubclass(testCase)
% DataSource is the abstract interface for fetchNew. The class
% itself can be instantiated, but calling fetchNew() on the base
% class throws 'DataSource:abstract' — subclasses MUST override.
ds = DataSource();
testCase.verifyError(@() ds.fetchNew(), 'DataSource:abstract');
end

function testSubclassMustImplementFetchNew(testCase)
Expand Down
120 changes: 10 additions & 110 deletions tests/suite/TestEventConfig.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
classdef TestEventConfig < matlab.unittest.TestCase
%TESTEVENTCONFIG EventConfig surface tests.
%
% All legacy-pipeline methods (cfg.addTag + cfg.runDetection +
% Threshold class) were deleted in Phase 1014 Plan 05: the
% Sensor/Threshold/StateChannel pipeline was removed in Phase 1011
% and EventConfig.addSensor now throws 'EventConfig:legacyRemoved'.
%
% Live-path event detection lives on MonitorTag + EventStore,
% covered by TestMonitorTag* and TestEventStoreRw.

methods (TestClassSetup)
function addPaths(testCase)
addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..'));
Expand All @@ -17,22 +27,6 @@ function testConstructorDefaults(testCase)
testCase.verifyEqual(cfg.AutoOpenViewer, false, 'defaults: AutoOpenViewer');
end

function testAddTag(testCase)
cfg = EventConfig();
s = SensorTag('temp', 'Name', 'Temperature');
s.updateData(1:10, [5 5 12 14 11 13 5 5 5 5]);
[s_x_, s_y_] = s.getXY();
t_warn = Threshold('warn', 'Name', 'warn', 'Direction', 'upper');
t_warn.addCondition(struct(), 10);
s.addThreshold(t_warn);
cfg.addTag(s);
testCase.verifyEqual(numel(cfg.Sensors), 1, 'addSensor: count');
testCase.verifyEqual(numel(cfg.SensorData), 1, 'addSensor: data count');
testCase.verifyEqual(cfg.SensorData(1).name, 'Temperature', 'addSensor: data name');
testCase.verifyEqual(cfg.SensorData(1).t, s_x_, 'addSensor: data t');
testCase.verifyEqual(cfg.SensorData(1).y, s_y_, 'addSensor: data y');
end

function testSetColor(testCase)
cfg = EventConfig();
cfg.setColor('warn', [1 0 0]);
Expand All @@ -50,99 +44,5 @@ function testBuildDetector(testCase)
testCase.verifyEqual(det.MaxCallsPerEvent, 3, 'buildDetector: MaxCallsPerEvent');
testCase.verifyNotEmpty(det.OnEventStart, 'buildDetector: OnEventStart');
end

function testRunDetection(testCase)
cfg = EventConfig();
s = SensorTag('temp', 'Name', 'Temperature');
s.updateData(1:10, [5 5 12 14 11 13 5 5 5 5]);
t_warn = Threshold('warn', 'Name', 'warn', 'Direction', 'upper');
t_warn.addCondition(struct(), 10);
s.addThreshold(t_warn);
cfg.addTag(s);
events = cfg.runDetection();
testCase.verifyGreaterThanOrEqual(numel(events), 1, 'runDetection: found events');
testCase.verifyEqual(events(1).SensorName, 'Temperature', 'runDetection: sensor name');
end

function testEscalateSeverity(testCase)
cfg = EventConfig();
s = SensorTag('temp', 'Name', 'Temperature');
s.updateData(1:10, [5 5 86 96 88 87 5 5 5 5]);
t_warn = Threshold('warn', 'Name', 'warn', 'Direction', 'upper');
t_warn.addCondition(struct(), 85);
s.addThreshold(t_warn);
t_critical = Threshold('critical', 'Name', 'critical', 'Direction', 'upper');
t_critical.addCondition(struct(), 95);
s.addThreshold(t_critical);
cfg.addTag(s);
events = cfg.runDetection();
critEvents = events(arrayfun(@(e) strcmp(e.ThresholdLabel, 'critical'), events));
testCase.verifyGreaterThanOrEqual(numel(critEvents), 1, 'escalate: critical event exists');
testCase.verifyGreaterThanOrEqual(critEvents(1).PeakValue, 95, 'escalate: peak above critical threshold');
end

function testEscalateDisabled(testCase)
cfg2 = EventConfig();
cfg2.EscalateSeverity = false;
s2 = SensorTag('temp', 'Name', 'Temperature');
s2.updateData(1:10, [5 5 86 96 88 87 5 5 5 5]);
t_warn = Threshold('warn', 'Name', 'warn', 'Direction', 'upper');
t_warn.addCondition(struct(), 85);
s2.addThreshold(t_warn);
t_critical = Threshold('critical', 'Name', 'critical', 'Direction', 'upper');
t_critical.addCondition(struct(), 95);
s2.addThreshold(t_critical);
cfg2.addTag(s2);
events2 = cfg2.runDetection();
warnEvents2 = events2(arrayfun(@(e) strcmp(e.ThresholdLabel, 'warn'), events2));
testCase.verifyGreaterThanOrEqual(numel(warnEvents2), 1, 'escalate disabled: warn event preserved');
end

function testEscalateLowDirection(testCase)
cfg3 = EventConfig();
s3 = SensorTag('pres', 'Name', 'Pressure');
s3.updateData(1:10, [6 6 3.5 1.5 3.8 3.9 6 6 6 6]);
t_low = Threshold('low', 'Name', 'low', 'Direction', 'lower');
t_low.addCondition(struct(), 4);
s3.addThreshold(t_low);
t_crit_low = Threshold('critical_low', 'Name', 'critical low', 'Direction', 'lower');
t_crit_low.addCondition(struct(), 2);
s3.addThreshold(t_crit_low);
cfg3.addTag(s3);
events3 = cfg3.runDetection();
critLow = events3(arrayfun(@(e) strcmp(e.ThresholdLabel, 'critical low'), events3));
testCase.verifyGreaterThanOrEqual(numel(critLow), 1, 'escalate low: critical low event exists');
testCase.verifyLessThanOrEqual(critLow(1).PeakValue, 2, 'escalate low: peak below critical threshold');
end

function testSaveViaEventStore(testCase)
tmpFile = fullfile(tempdir, 'test_cfg_store_save.mat');
if exist(tmpFile, 'file'); delete(tmpFile); end
testCase.addTeardown(@() TestEventConfig.deleteIfExists(tmpFile));
cfg = EventConfig();
cfg.EventFile = tmpFile;
cfg.MaxBackups = 0;
s = SensorTag('temp', 'Name', 'Temperature');
s.updateData(1:10, [5 5 12 14 11 13 5 5 5 5]);
t_warn = Threshold('warn', 'Name', 'warn', 'Direction', 'upper');
t_warn.addCondition(struct(), 10);
s.addThreshold(t_warn);
cfg.setColor('warn', [1 0 0]);
cfg.addTag(s);
events = cfg.runDetection();
testCase.verifyEqual(exist(tmpFile, 'file'), 2, 'save: file exists');
data = load(tmpFile);
testCase.verifyTrue(isfield(data, 'events'), 'save: has events');
testCase.verifyTrue(isfield(data, 'sensorData'), 'save: has sensorData');
testCase.verifyTrue(isfield(data, 'thresholdColors'), 'save: has thresholdColors');
testCase.verifyTrue(isfield(data, 'timestamp'), 'save: has timestamp');
testCase.verifyEqual(numel(data.events), numel(events), 'save: event count matches');
end
end

methods (Static, Access = private)
function deleteIfExists(f)
if exist(f, 'file'); delete(f); end
end
end
end
103 changes: 0 additions & 103 deletions tests/suite/TestEventDetector.m

This file was deleted.

Loading
Loading