diff --git a/libs/Dashboard/DashboardBuilder.m b/libs/Dashboard/DashboardBuilder.m index 236f4960..87c46248 100644 --- a/libs/Dashboard/DashboardBuilder.m +++ b/libs/Dashboard/DashboardBuilder.m @@ -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 = ''; diff --git a/tests/suite/TestChipBarWidget.m b/tests/suite/TestChipBarWidget.m index 8aac14ec..0ffc79f8 100644 --- a/tests/suite/TestChipBarWidget.m +++ b/tests/suite/TestChipBarWidget.m @@ -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 diff --git a/tests/suite/TestDashboardBugFixes.m b/tests/suite/TestDashboardBugFixes.m index 72c913a3..94f94105 100644 --- a/tests/suite/TestDashboardBugFixes.m +++ b/tests/suite/TestDashboardBugFixes.m @@ -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 diff --git a/tests/suite/TestDashboardEngine.m b/tests/suite/TestDashboardEngine.m index 52c608e6..fe4f221f 100644 --- a/tests/suite/TestDashboardEngine.m +++ b/tests/suite/TestDashboardEngine.m @@ -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'); diff --git a/tests/suite/TestDataSource.m b/tests/suite/TestDataSource.m index b4881a6a..52b020b8 100644 --- a/tests/suite/TestDataSource.m +++ b/tests/suite/TestDataSource.m @@ -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) diff --git a/tests/suite/TestEventConfig.m b/tests/suite/TestEventConfig.m index 97d345e2..095e54cb 100644 --- a/tests/suite/TestEventConfig.m +++ b/tests/suite/TestEventConfig.m @@ -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')), '..', '..')); @@ -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]); @@ -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 diff --git a/tests/suite/TestEventDetector.m b/tests/suite/TestEventDetector.m deleted file mode 100644 index baf255f4..00000000 --- a/tests/suite/TestEventDetector.m +++ /dev/null @@ -1,103 +0,0 @@ -classdef TestEventDetector < matlab.unittest.TestCase - methods (TestClassSetup) - function addPaths(testCase) - addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..')); - install(); - end - end - - methods (Test) - function testDetectSingleEvent(testCase) - det = EventDetector(); - t = [1 2 3 4 5 6 7 8 9 10]; - values = [5 5 12 14 11 13 5 5 5 5]; - events = det.detect(t, values, 10, 'upper', 'warn', 'temp'); - testCase.verifyEqual(numel(events), 1, 'singleEvent: count'); - testCase.verifyEqual(events(1).StartTime, 3, 'singleEvent: StartTime'); - testCase.verifyEqual(events(1).EndTime, 6, 'singleEvent: EndTime'); - testCase.verifyEqual(events(1).Duration, 3, 'singleEvent: Duration'); - testCase.verifyEqual(events(1).SensorName, 'temp', 'singleEvent: SensorName'); - testCase.verifyEqual(events(1).ThresholdLabel, 'warn', 'singleEvent: ThresholdLabel'); - testCase.verifyEqual(events(1).ThresholdValue, 10, 'singleEvent: ThresholdValue'); - testCase.verifyEqual(events(1).Direction, 'upper', 'singleEvent: Direction'); - end - - function testStats(testCase) - det = EventDetector(); - t = [1 2 3 4 5 6 7 8 9 10]; - values = [5 5 12 14 11 13 5 5 5 5]; - events = det.detect(t, values, 10, 'upper', 'warn', 'temp'); - testCase.verifyEqual(events(1).PeakValue, 14, 'stats: PeakValue'); - testCase.verifyEqual(events(1).NumPoints, 4, 'stats: NumPoints'); - testCase.verifyEqual(events(1).MinValue, 11, 'stats: MinValue'); - testCase.verifyEqual(events(1).MaxValue, 14, 'stats: MaxValue'); - testCase.verifyLessThan(abs(events(1).MeanValue - 12.5), 1e-10, 'stats: MeanValue'); - expected_rms = sqrt(mean([12 14 11 13].^2)); - testCase.verifyLessThan(abs(events(1).RmsValue - expected_rms), 1e-10, 'stats: RmsValue'); - expected_std = std([12 14 11 13]); - testCase.verifyLessThan(abs(events(1).StdValue - expected_std), 1e-10, 'stats: StdValue'); - end - - function testPeakValueLow(testCase) - det = EventDetector(); - t = [1 2 3 4 5]; - values = [50 3 2 4 50]; - events = det.detect(t, values, 10, 'lower', 'alarm', 'pressure'); - testCase.verifyEqual(events(1).PeakValue, 2, 'peakLow: PeakValue is min'); - end - - function testMultipleEvents(testCase) - det = EventDetector(); - t = [1 2 3 4 5 6 7 8 9 10]; - values = [12 13 5 5 5 14 15 5 5 5]; - events = det.detect(t, values, 10, 'upper', 'warn', 'temp'); - testCase.verifyEqual(numel(events), 2, 'multipleEvents: count'); - testCase.verifyEqual(events(1).StartTime, 1, 'multipleEvents: e1 start'); - testCase.verifyEqual(events(2).StartTime, 6, 'multipleEvents: e2 start'); - end - - function testDebounceFilter(testCase) - det = EventDetector('MinDuration', 2); - t = [1 2 3 4 5 6 7 8 9 10]; - values = [12 5 5 14 15 16 17 5 5 5]; - events = det.detect(t, values, 10, 'upper', 'warn', 'temp'); - testCase.verifyEqual(numel(events), 1, 'debounce: count'); - testCase.verifyEqual(events(1).StartTime, 4, 'debounce: kept event'); - end - - function testNoViolations(testCase) - det = EventDetector(); - t = [1 2 3 4 5]; - values = [5 6 7 8 9]; - events = det.detect(t, values, 10, 'upper', 'warn', 'temp'); - testCase.verifyEmpty(events, 'noViolations: empty'); - end - - function testCallback(testCase) - callCount = 0; - lastEvent = []; - function onEvent(ev) - callCount = callCount + 1; - lastEvent = ev; - end - det = EventDetector('OnEventStart', @onEvent); - t = [1 2 3 4 5 6 7 8 9 10]; - values = [12 13 5 5 5 14 15 5 5 5]; - det.detect(t, values, 10, 'upper', 'warn', 'temp'); - testCase.verifyEqual(callCount, 2, 'callback: called twice'); - testCase.verifyEqual(lastEvent.StartTime, 6, 'callback: last event'); - end - - function testMaxCallsPerEvent(testCase) - callCount = 0; - function onEvent(ev) - callCount = callCount + 1; - end - det = EventDetector('OnEventStart', @onEvent, 'MaxCallsPerEvent', 1); - t = [1 2 3 4 5]; - values = [12 13 14 15 16]; - det.detect(t, values, 10, 'upper', 'warn', 'temp'); - testCase.verifyEqual(callCount, 1, 'maxCalls: only called once for one event'); - end - end -end diff --git a/tests/suite/TestEventStore.m b/tests/suite/TestEventStore.m index 17423737..d86e6647 100644 --- a/tests/suite/TestEventStore.m +++ b/tests/suite/TestEventStore.m @@ -1,4 +1,14 @@ classdef TestEventStore < matlab.unittest.TestCase + %TESTEVENTSTORE EventStore / EventViewer surface tests. + % + % All legacy-pipeline tests (EventConfig.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 EventStore coverage (append + save + load round-trip + % against the real v2.0 API) lives in tests/suite/TestEventStoreRw.m. + methods (TestClassSetup) function addPaths(testCase) addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..')); @@ -7,87 +17,6 @@ function addPaths(testCase) end methods (Test) - function testAutoSave(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); - cfg.setColor('warn', [1 0.8 0]); - - tmpFile = fullfile(tempdir, 'test_event_store.mat'); - testCase.addTeardown(@() TestEventStore.deleteIfExists(tmpFile)); - cfg.EventFile = tmpFile; - events = cfg.runDetection(); - - testCase.verifyEqual(exist(tmpFile, 'file'), 2, 'auto-save: file created'); - data = load(tmpFile); - testCase.verifyTrue(isfield(data, 'events'), 'auto-save: has events'); - testCase.verifyTrue(isfield(data, 'sensorData'), 'auto-save: has sensorData'); - testCase.verifyTrue(isfield(data, 'thresholdColors'), 'auto-save: has thresholdColors'); - testCase.verifyTrue(isfield(data, 'timestamp'), 'auto-save: has timestamp'); - testCase.verifyEqual(numel(data.events), numel(events), 'auto-save: event count'); - testCase.verifyEqual(data.sensorData(1).name, 'Temperature', 'auto-save: sensor name'); - end - - function testFromFile(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); - cfg.setColor('warn', [1 0.8 0]); - - tmpFile = fullfile(tempdir, 'test_event_store_fromfile.mat'); - testCase.addTeardown(@() TestEventStore.deleteIfExists(tmpFile)); - cfg.EventFile = tmpFile; - events = cfg.runDetection(); - - viewer = EventViewer.fromFile(tmpFile); - testCase.addTeardown(@close, viewer.hFigure); - testCase.verifyTrue(isa(viewer, 'EventViewer'), 'fromFile: returns EventViewer'); - testCase.verifyEqual(numel(viewer.Events), numel(events), 'fromFile: event count'); - end - - function testFromFileColors(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); - cfg.setColor('warn', [1 0.8 0]); - - tmpFile = fullfile(tempdir, 'test_event_store_colors.mat'); - testCase.addTeardown(@() TestEventStore.deleteIfExists(tmpFile)); - cfg.EventFile = tmpFile; - cfg.runDetection(); - - viewer = EventViewer.fromFile(tmpFile); - testCase.addTeardown(@close, viewer.hFigure); - testCase.verifyTrue(viewer.ThresholdColors.isKey('warn'), 'fromFile: color key restored'); - testCase.verifyEqual(viewer.ThresholdColors('warn'), [1 0.8 0], 'fromFile: color value'); - end - - function testNoEventFile(testCase) - cfg2 = EventConfig(); - s2 = SensorTag('temp', 'Name', 'Temperature'); - s2.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); - s2.addThreshold(t_warn); - cfg2.addTag(s2); - tmpFile2 = fullfile(tempdir, 'test_event_store_2.mat'); - if exist(tmpFile2, 'file'); delete(tmpFile2); end - cfg2.runDetection(); - testCase.verifyTrue(exist(tmpFile2, 'file') ~= 2, 'no-file: nothing saved when EventFile empty'); - end - function testFromFileNotFound(testCase) threw = false; try @@ -98,113 +27,5 @@ function testFromFileNotFound(testCase) end testCase.verifyTrue(threw, 'fromFile: throws on missing file'); end - - function testBackupCreated(testCase) - tmpFile3 = fullfile(tempdir, 'test_event_backup.mat'); - [bDir, bName, bExt] = fileparts(tmpFile3); - % Clean up any previous backup files - oldBackups = dir(fullfile(bDir, [bName, '_*', bExt])); - for bi = 1:numel(oldBackups) - delete(fullfile(bDir, oldBackups(bi).name)); - end - if exist(tmpFile3, 'file'); delete(tmpFile3); end - - testCase.addTeardown(@() TestEventStore.cleanupBackups(tmpFile3)); - - cfg3 = EventConfig(); - s3 = SensorTag('temp', 'Name', 'Temperature'); - s3.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); - s3.addThreshold(t_warn); - cfg3.addTag(s3); - cfg3.EventFile = tmpFile3; - cfg3.MaxBackups = 2; - - % First save — no backup (no existing file) - cfg3.runDetection(); - backups = dir(fullfile(bDir, [bName, '_*', bExt])); - testCase.verifyEqual(numel(backups), 0, 'backup: no backup on first save'); - - % Second save — creates backup - pause(1.1); - cfg3.runDetection(); - backups = dir(fullfile(bDir, [bName, '_*', bExt])); - testCase.verifyEqual(numel(backups), 1, 'backup: one backup after second save'); - - % Third save — creates second backup - pause(1.1); - cfg3.runDetection(); - backups = dir(fullfile(bDir, [bName, '_*', bExt])); - testCase.verifyEqual(numel(backups), 2, 'backup: two backups after third save'); - - % Fourth save — prunes to MaxBackups=2 - pause(1.1); - cfg3.runDetection(); - backups = dir(fullfile(bDir, [bName, '_*', bExt])); - testCase.verifyEqual(numel(backups), 2, 'backup: pruned to MaxBackups'); - end - - function testMaxBackupsZero(testCase) - tmpFile4 = fullfile(tempdir, 'test_event_nobackup.mat'); - testCase.addTeardown(@() TestEventStore.cleanupBackups(tmpFile4)); - - cfg4 = EventConfig(); - s4 = SensorTag('temp', 'Name', 'Temperature'); - s4.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); - s4.addThreshold(t_warn); - cfg4.addTag(s4); - cfg4.EventFile = tmpFile4; - cfg4.MaxBackups = 0; - cfg4.runDetection(); - cfg4.runDetection(); - [nbDir, nbName, nbExt] = fileparts(tmpFile4); - noBackups = dir(fullfile(nbDir, [nbName, '_*', nbExt])); - testCase.verifyEqual(numel(noBackups), 0, 'no-backup: MaxBackups=0 creates no backups'); - end - - function testFromFileHasRefreshControls(testCase) - cfg5 = EventConfig(); - s5 = SensorTag('temp', 'Name', 'Temperature'); - s5.updateData(1:10, [5 5 12 14 11 13 5 5 5 5]); - [s5_x_, s5_y_] = s5.getXY(); - t_warn = Threshold('warn', 'Name', 'warn', 'Direction', 'upper'); - t_warn.addCondition(struct(), 10); - s5.addThreshold(t_warn); - cfg5.addTag(s5); - tmpFile5 = fullfile(tempdir, 'test_event_refresh.mat'); - testCase.addTeardown(@() TestEventStore.deleteIfExists(tmpFile5)); - cfg5.EventFile = tmpFile5; - cfg5.runDetection(); - viewer5 = EventViewer.fromFile(tmpFile5); - testCase.addTeardown(@close, viewer5.hFigure); - testCase.verifyNotEmpty(viewer5.hFigure, 'refresh: figure exists'); - % Verify refresh works by modifying file and calling refreshFromFile - s5_y_ = [5 5 12 14 11 13 12 15 5 5]; - cfg5.runDetection(); - oldCount = numel(viewer5.Events); - viewer5.refreshFromFile(); - testCase.verifyGreaterThanOrEqual(numel(viewer5.Events), oldCount, 'refresh: events updated from file'); - % Test auto-refresh start/stop (no error = success) - viewer5.startAutoRefresh(60); - viewer5.stopAutoRefresh(); - end - end - - methods (Static, Access = private) - function deleteIfExists(f) - if exist(f, 'file'); delete(f); end - end - - function cleanupBackups(f) - if exist(f, 'file'); delete(f); end - [bDir, bName, bExt] = fileparts(f); - backups = dir(fullfile(bDir, [bName, '_*', bExt])); - for bi = 1:numel(backups) - delete(fullfile(bDir, backups(bi).name)); - end - end end end diff --git a/tests/suite/TestFastSenseAddTag.m b/tests/suite/TestFastSenseAddTag.m index ec3a82b3..1872e292 100644 --- a/tests/suite/TestFastSenseAddTag.m +++ b/tests/suite/TestFastSenseAddTag.m @@ -105,7 +105,7 @@ function testAddTagRejectsUnsupportedKind(testCase) function testAddTagMixedWithLegacy(testCase) fp = FastSense(); legacy = SensorTag('legacy', 'Name', 'Legacy'); - legacy.updateData(1:50, cos(legacy.X * 0.2)); + legacy.updateData(1:50, cos((1:50) * 0.2)); fp.addTag(legacy, 'ShowThresholds', false); st = SensorTag('modern', 'Name', 'Modern', 'X', 1:30, 'Y', sin(1:30)); diff --git a/tests/suite/TestFastSenseWidget.m b/tests/suite/TestFastSenseWidget.m index 6c2365c4..55b570b4 100644 --- a/tests/suite/TestFastSenseWidget.m +++ b/tests/suite/TestFastSenseWidget.m @@ -54,27 +54,6 @@ function testRenderCreatesAxes(testCase) testCase.verifyTrue(isa(w.FastSenseObj, 'FastSense')); end - function testRenderWithTag(testCase) - hFig = figure('Visible', 'off'); - testCase.addTeardown(@() close(hFig)); - - hp = uipanel('Parent', hFig, 'Units', 'normalized', ... - 'Position', [0 0 1 1]); - - s = SensorTag('T-401', 'Name', 'Temperature'); - s.updateData(1:100, rand(1,100)); - t_hi_alarm = Threshold('hi_alarm', 'Name', 'Hi Alarm', 'Direction', 'upper'); - t_hi_alarm.addCondition(struct(), 80); - s.addThreshold(t_hi_alarm); - s.resolve(); - - w = FastSenseWidget('Sensor', s); - w.render(hp); - - testCase.verifyNotEmpty(w.FastSenseObj); - testCase.verifyGreaterThanOrEqual(numel(w.FastSenseObj.Lines), 1); - end - function testToStructRoundTrip(testCase) w = FastSenseWidget('Title', 'My Plot', 'Position', [5 2 16 3]); w.XData = 1:10; diff --git a/tests/suite/TestGaugeWidget.m b/tests/suite/TestGaugeWidget.m index 617aa3a7..27f568ec 100644 --- a/tests/suite/TestGaugeWidget.m +++ b/tests/suite/TestGaugeWidget.m @@ -133,22 +133,6 @@ function testRefreshWithTag(testCase) testCase.verifyEqual(w.CurrentValue, 85); end - function testRangeDeriveFromTag(testCase) - s = SensorTag('P-201', 'Name', 'Pressure'); - s.updateData([1 2 3], [40 50 60]); - t1 = Threshold('P201_lo', 'Name', 'Lo', ... - 'Direction', 'lower', 'Color', [1 0.6 0]); - t1.addCondition(struct(), 30); - s.addThreshold(t1); - t2 = Threshold('P201_hi', 'Name', 'Hi', ... - 'Direction', 'upper', 'Color', [1 0 0]); - t2.addCondition(struct(), 80); - s.addThreshold(t2); - w = GaugeWidget('Sensor', s); - testCase.verifyEqual(w.Range, [30 80], ... - 'Range should auto-derive from Threshold values'); - end - function testUnitsDeriveFromTag(testCase) s = SensorTag('T-101', 'Name', 'Temperature', 'Units', 'degC'); s.updateData([1 2 3], [20 25 30]); @@ -202,125 +186,5 @@ function testGetType(testCase) testCase.verifyEqual(w.getType(), 'gauge'); testCase.verifyEqual(w.Type, 'gauge'); end - - % --- NEW TESTS FOR THRESHOLD BINDING --- - - function testConstructorThresholdBinding(testCase) - %% GaugeWidget stores Threshold when passed via constructor - t = Threshold('press_hi', 'Name', 'Hi', 'Direction', 'upper'); - t.addCondition(struct(), 80); - w = GaugeWidget('Threshold', t, 'StaticValue', 50); - testCase.verifyEqual(w.Threshold, t, ... - 'Threshold property should store the Threshold object'); - testCase.verifyEqual(w.StaticValue, 50, ... - 'StaticValue should be set'); - end - - function testThresholdRangeDerivation(testCase) - %% Threshold with conditions at 30 and 80 -> Range auto-derives to [30, 80] - t = Threshold('press_rng', 'Name', 'Range', 'Direction', 'upper'); - t.addCondition(struct(), 30); - t.addCondition(struct(), 80); - w = GaugeWidget('Threshold', t, 'StaticValue', 50); - testCase.verifyEqual(w.Range, [30 80], ... - 'Range should auto-derive from Threshold condition values'); - end - - function testThresholdColorPath(testCase) - %% Value above upper threshold -> alarm color in getValueColor - theme = DashboardTheme(); - t = Threshold('press_hi', 'Name', 'Hi Alarm', ... - 'Direction', 'upper'); - t.addCondition(struct(), 80); - - % Violation: value 90 > threshold 80 - w = GaugeWidget('Threshold', t, 'StaticValue', 90, 'Range', [0 100]); - hFig = figure('Visible', 'off'); - testCase.addTeardown(@() close(hFig)); - hp = uipanel('Parent', hFig, 'Position', [0 0 1 1]); - w.render(hp); - - % The gauge arc foreground color should be alarm color - % We verify indirectly by checking the arc fill color - arcFg = w.hArcFg; - testCase.verifyTrue(ishandle(arcFg), ... - 'Arc foreground handle should be valid after render'); - arcColor = get(arcFg, 'FaceColor'); - testCase.verifyEqual(arcColor, theme.StatusAlarmColor, ... - 'Upper violation without Color should use StatusAlarmColor'); - end - - function testMutualExclusivity(testCase) - %% Setting Threshold clears Sensor - s = SensorTag('P-201', 'Name', 'Pressure'); - % TODO: s_x_ = [1]; s_y_ = [50]; (needs manual fix) - t = Threshold('press_hi', 'Name', 'Hi', 'Direction', 'upper'); - t.addCondition(struct(), 80); - - w = GaugeWidget('Sensor', s, 'Threshold', t, 'StaticValue', 50); - testCase.verifyEmpty(w.Sensor, ... - 'Sensor should be cleared when Threshold is set'); - testCase.verifyEqual(w.Threshold, t, ... - 'Threshold should be set'); - end - - function testSerializeThresholdRoundTrip(testCase) - %% toStruct/fromStruct preserves threshold key - TagRegistry.clear(); - t = Threshold('press_hi', 'Name', 'Hi', 'Direction', 'upper'); - t.addCondition(struct(), 90); - TagRegistry.register('press_hi', t); - testCase.addTeardown(@() TagRegistry.clear()); - - w = GaugeWidget('Title', 'Pressure', ... - 'Threshold', t, 'StaticValue', 70, 'Range', [0 100]); - st = w.toStruct(); - - testCase.verifyTrue(isfield(st, 'source'), ... - 'toStruct should include source field'); - testCase.verifyEqual(st.source.type, 'threshold', ... - 'source.type should be ''threshold'''); - testCase.verifyEqual(st.source.key, 'press_hi', ... - 'source.key should match threshold key'); - - % Round-trip via fromStruct - w2 = GaugeWidget.fromStruct(st); - testCase.verifyEqual(w2.Threshold, t, ... - 'fromStruct should restore Threshold from registry'); - end - - function testThresholdWithValueFcn(testCase) - %% ValueFcn + Threshold -> refresh uses ValueFcn value and Threshold color - theme = DashboardTheme(); - t = Threshold('press_hi', 'Name', 'Hi', 'Direction', 'upper'); - t.addCondition(struct(), 80); - - val = containers.Map('KeyType', 'char', 'ValueType', 'double'); - val('v') = 90; % above threshold - - w = GaugeWidget('Title', 'Pressure', ... - 'Threshold', t, 'ValueFcn', @() val('v'), 'Range', [0 100]); - hFig = figure('Visible', 'off'); - testCase.addTeardown(@() close(hFig)); - hp = uipanel('Parent', hFig, 'Position', [0 0 1 1]); - w.render(hp); - - testCase.verifyEqual(w.CurrentValue, 90, ... - 'CurrentValue should be set from ValueFcn'); - - % Arc color should indicate violation - arcColor = get(w.hArcFg, 'FaceColor'); - testCase.verifyEqual(arcColor, theme.StatusAlarmColor, ... - 'Alarm color should be used when value > threshold'); - - % Now change value to below threshold and refresh - val('v') = 60; - w.refresh(); - testCase.verifyEqual(w.CurrentValue, 60, ... - 'CurrentValue should update after refresh'); - arcColor2 = get(w.hArcFg, 'FaceColor'); - testCase.verifyEqual(arcColor2, theme.StatusOkColor, ... - 'OK color should be used when value < threshold'); - end end end diff --git a/tests/suite/TestIconCardWidget.m b/tests/suite/TestIconCardWidget.m index a5b64642..0dd5d503 100644 --- a/tests/suite/TestIconCardWidget.m +++ b/tests/suite/TestIconCardWidget.m @@ -130,83 +130,5 @@ function testStateColorInactive(testCase) faceColor = get(w.hIconShape, 'FaceColor'); testCase.verifyEqual(faceColor, [0.5 0.5 0.5], 'AbsTol', 0.01); end - - function testThresholdBinding(testCase) - t = Threshold('test_thr_bind', 'Direction', 'upper', 'Color', [1 0 0]); - t.addCondition(struct('machine', 1), 50); - w = IconCardWidget('Title', 'T', 'StaticValue', 42); - w.Threshold = t; - testCase.verifyEqual(w.Threshold, t); - end - - function testThresholdKeyResolution(testCase) - t = Threshold('test_key_res', 'Direction', 'upper'); - t.addCondition(struct(), 50); - TagRegistry.register('test_key_res', t); - cleanup = onCleanup(@() TagRegistry.unregister('test_key_res')); - w = IconCardWidget('Title', 'T', 'Threshold', 'test_key_res'); - testCase.verifyEqual(w.Threshold.Key, 'test_key_res'); - end - - function testMutualExclusivity(testCase) - % Setting Threshold should clear Sensor - t = Threshold('test_mutex', 'Direction', 'upper'); - t.addCondition(struct(), 50); - TagRegistry.register('test_mutex', t); - cleanup = onCleanup(@() TagRegistry.unregister('test_mutex')); - sensor = SensorTag('test_mutex_sensor', 'Name', 'Test Sensor'); - w = IconCardWidget('Title', 'T', 'Sensor', sensor); - testCase.verifyEqual(w.Sensor, sensor); - % Now set Threshold — Sensor should be cleared - w.Threshold = t; - w2 = IconCardWidget('Title', 'T', 'Sensor', sensor, 'Threshold', t); - testCase.verifyEmpty(w2.Sensor); - end - - function testDeriveStateFromThreshold(testCase) - t = Threshold('test_derive_state', 'Direction', 'upper'); - t.addCondition(struct(), 50); - % Value above threshold -> alarm state -> alarm color - w = IconCardWidget('Title', 'Test', 'StaticValue', 60, 'Threshold', t); - 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); - faceColor = get(w.hIconShape, 'FaceColor'); - testCase.verifyEqual(faceColor, theme.StatusAlarmColor, 'AbsTol', 0.01); - end - - function testThresholdWithValueFcn(testCase) - t = Threshold('test_thr_valfcn', 'Direction', 'upper'); - t.addCondition(struct(), 50); - val = 75; - w = IconCardWidget('Title', 'Test', 'ValueFcn', @() val, 'Threshold', t); - 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 > 50 -> alarm - faceColor = get(w.hIconShape, 'FaceColor'); - testCase.verifyEqual(faceColor, theme.StatusAlarmColor, 'AbsTol', 0.01); - end - - function testSerializeThresholdRoundTrip(testCase) - t = Threshold('test_serial_thr', 'Direction', 'upper'); - t.addCondition(struct(), 50); - TagRegistry.register('test_serial_thr', t); - cleanup = onCleanup(@() TagRegistry.unregister('test_serial_thr')); - w = IconCardWidget('Title', 'SerTest', 'StaticValue', 42, 'Threshold', t); - w.Position = [1 1 6 2]; - s = w.toStruct(); - testCase.verifyEqual(s.source.type, 'threshold'); - testCase.verifyEqual(s.source.key, 'test_serial_thr'); - % fromStruct restores - w2 = IconCardWidget.fromStruct(s); - testCase.verifyEqual(w2.Threshold.Key, 'test_serial_thr'); - end end end diff --git a/tests/suite/TestIconCardWidgetTag.m b/tests/suite/TestIconCardWidgetTag.m index 928b7de8..3d1ece2f 100644 --- a/tests/suite/TestIconCardWidgetTag.m +++ b/tests/suite/TestIconCardWidgetTag.m @@ -65,23 +65,6 @@ function testTagOkState(testCase) testCase.verifyEqual(w.CurrentState, 'ok'); end - function testTagPrecedenceOverThreshold(testCase) - % Setting both Tag and Threshold: Tag wins; Threshold is cleared - % by the constructor mutex (parallel to the existing - % Threshold > Sensor mutex on line 69-71). - st = MakePhase1009Fixtures.makeSensorTag('icw_pr_src', ... - 'X', 1:5, 'Y', [1 1 1 1 20]); - m = MakePhase1009Fixtures.makeMonitorTag('icw_pr_mon', st); - - t = Threshold('icw_pr_thr', 'Direction', 'upper'); - t.addCondition(struct(), 10); - - w = IconCardWidget('Title', 'P', 'Tag', m, 'Threshold', t); - testCase.verifyEmpty(w.Threshold, ... - 'Tag precedence: constructor mutex must clear Threshold'); - testCase.verifyNotEmpty(w.Tag); - end - function testTagToStructRoundTrip(testCase) st = MakePhase1009Fixtures.makeSensorTag('icw_rt_src'); m = MakePhase1009Fixtures.makeMonitorTag('icw_rt_mon', st); @@ -97,19 +80,6 @@ function testTagToStructRoundTrip(testCase) testCase.verifyEqual(w2.Tag.Key, 'icw_rt_mon'); end - function testLegacyThresholdPathStillWorks(testCase) - t = Threshold('icw_legacy_thr', 'Direction', 'upper'); - t.addCondition(struct(), 10); - w = IconCardWidget('Title', 'L', 'Threshold', t, 'StaticValue', 42); - - fig = figure('Visible', 'off'); - testCase.addTeardown(@() close(fig)); - hp = uipanel(fig, 'Position', [0 0 1 1]); - w.ParentTheme = DashboardTheme('dark'); - w.render(hp); - testCase.verifyEqual(w.CurrentState, 'alarm'); - end - function testLegacySensorPathStillWorks(testCase) s = SensorTag('icw_legacy_s', 'Name', 'L'); s.updateData(1:10, (1:10) * 1.0); diff --git a/tests/suite/TestIncrementalDetector.m b/tests/suite/TestIncrementalDetector.m deleted file mode 100644 index 0ab688f4..00000000 --- a/tests/suite/TestIncrementalDetector.m +++ /dev/null @@ -1,119 +0,0 @@ -classdef TestIncrementalDetector < matlab.unittest.TestCase - methods (TestClassSetup) - function addPaths(testCase) - addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..')); - addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..', 'libs', 'EventDetection')); - addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..', 'libs', 'SensorThreshold')); - install(); - end - end - - methods (Test) - function testFirstBatchDetectsEvents(testCase) - det = IncrementalEventDetector('MinDuration', 0); - sensor = TestIncrementalDetector.makeTag('temp', 100, 'upper'); - t = linspace(now-1, now, 100); - y = 80 * ones(1,100); y(40:60) = 120; - newEvents = det.process('temp', sensor, t, y, [], {}); - testCase.verifyGreaterThanOrEqual(numel(newEvents), 1, 'detected_event'); - testCase.verifyEqual(newEvents(1).SensorName, 'temp', 'sensor_name'); - end - - function testIncrementalNewEventsOnly(testCase) - det = IncrementalEventDetector('MinDuration', 0); - sensor = TestIncrementalDetector.makeTag('temp', 100, 'upper'); - t1 = linspace(now-1, now-0.5, 50); - y1 = 80 * ones(1,50); y1(20:30) = 120; - det.process('temp', sensor, t1, y1, [], {}); - % Second batch — no violations - t2 = linspace(now-0.5, now, 50); - y2 = 80 * ones(1,50); - ev2 = det.process('temp', sensor, t2, y2, [], {}); - testCase.verifyEqual(numel(ev2), 0, 'no_new_events'); - end - - function testOpenEventCarriesOver(testCase) - det = IncrementalEventDetector('MinDuration', 0); - sensor = TestIncrementalDetector.makeTag('temp', 100, 'upper'); - t1 = linspace(now-1, now-0.5, 50); - y1 = 80 * ones(1,50); y1(40:50) = 120; - ev1 = det.process('temp', sensor, t1, y1, [], {}); - testCase.verifyEqual(numel(ev1), 0, 'no_finalized_yet'); - testCase.verifyTrue(det.hasOpenEvent('temp'), 'has_open_event'); - end - - function testOpenEventFinalizes(testCase) - det = IncrementalEventDetector('MinDuration', 0); - sensor = TestIncrementalDetector.makeTag('temp', 100, 'upper'); - t1 = linspace(now-1, now-0.5, 50); - y1 = 80*ones(1,50); y1(40:50) = 120; - det.process('temp', sensor, t1, y1, [], {}); - t2 = linspace(now-0.5, now, 50); - y2 = 80*ones(1,50); y2(1:5) = 120; - ev2 = det.process('temp', sensor, t2, y2, [], {}); - testCase.verifyEqual(numel(ev2), 1, 'finalized_event'); - testCase.verifyLessThan(ev2(1).StartTime, now - 0.4, 'start_in_batch1'); - testCase.verifyGreaterThan(ev2(1).EndTime, now - 0.5, 'end_in_batch2'); - end - - function testNoDataNoEvents(testCase) - det = IncrementalEventDetector('MinDuration', 0); - sensor = TestIncrementalDetector.makeTag('temp', 100, 'upper'); - ev = det.process('temp', sensor, [], [], [], {}); - testCase.verifyEmpty(ev, 'no_events_empty_data'); - end - - function testSeverityEscalation(testCase) - det = IncrementalEventDetector('MinDuration', 0, 'EscalateSeverity', true); - sensor = SensorTag('temp'); - tH = Threshold('h', 'Name', 'H', 'Direction', 'upper'); - tH.addCondition(struct(), 100); - sensor.addThreshold(tH); - tHH = Threshold('hh', 'Name', 'HH', 'Direction', 'upper'); - tHH.addCondition(struct(), 150); - sensor.addThreshold(tHH); - t = linspace(now-1, now, 100); - y = 80*ones(1,100); y(40:60) = 160; - ev = det.process('temp', sensor, t, y, [], {}); - hhEvents = ev(strcmp({ev.ThresholdLabel}, 'HH')); - testCase.verifyNotEmpty(hhEvents, 'escalated_to_HH'); - end - - function testMultipleSensors(testCase) - det = IncrementalEventDetector('MinDuration', 0); - s1 = TestIncrementalDetector.makeTag('temp', 100, 'upper'); - s2 = TestIncrementalDetector.makeTag('pres', 50, 'upper'); - t = linspace(now-1, now, 100); - y1 = 80*ones(1,100); y1(30:40) = 120; - y2 = 30*ones(1,100); y2(60:70) = 60; - ev1 = det.process('temp', s1, t, y1, [], {}); - ev2 = det.process('pres', s2, t, y2, [], {}); - testCase.verifyTrue(~isempty(ev1) && strcmp(ev1(1).SensorName, 'temp'), 'sensor1'); - testCase.verifyTrue(~isempty(ev2) && strcmp(ev2(1).SensorName, 'pres'), 'sensor2'); - end - - function testSliceDetectionConsistency(testCase) - det = IncrementalEventDetector('MinDuration', 0); - sensor = TestIncrementalDetector.makeTag('temp', 100, 'upper'); - t1 = linspace(now-10, now-5, 500); - y1 = 80*ones(1,500); y1(100:150) = 120; - ev1 = det.process('temp', sensor, t1, y1, [], {}); - n1 = numel(ev1); - t2 = linspace(now-5, now, 500); - y2 = 80*ones(1,500); y2(200:250) = 120; - ev2 = det.process('temp', sensor, t2, y2, [], {}); - testCase.verifyGreaterThanOrEqual(n1, 1, 'batch1_event'); - testCase.verifyGreaterThanOrEqual(numel(ev2), 1, 'batch2_event'); - testCase.verifyGreaterThan(ev2(1).StartTime, now - 5.1, 'new_event_in_batch2'); - end - end - - methods (Static, Access = private) - function sensor = makeTag(key, threshVal, dir) - sensor = SensorTag(key); - t = Threshold('h', 'Name', 'H', 'Direction', dir); - t.addCondition(struct(), threshVal); - sensor.addThreshold(t); - end - end -end diff --git a/tests/suite/TestLiveEventPipelineTag.m b/tests/suite/TestLiveEventPipelineTag.m index a9f46f1f..8210e638 100644 --- a/tests/suite/TestLiveEventPipelineTag.m +++ b/tests/suite/TestLiveEventPipelineTag.m @@ -106,39 +106,11 @@ function testAppendDataOrderWithParent(testCase) 'monitor cache must end at parent tail'); end - function testLegacySensorPathUnchanged(testCase) - % Legacy constructor shape (no 'Monitors' NV pair) must still - % yield a functional pipeline -- byte-for-byte preservation. - s = SensorTag('s1'); - thr = Threshold('warn', 'Name', 'warn', 'Direction', 'upper'); - thr.addCondition(struct(), 10); - s.addThreshold(thr); - - sensors = containers.Map('KeyType', 'char', 'ValueType', 'any'); - sensors('s1') = s; - dsMap = DataSourceMap(); - ds = StubDataSource(); - dsMap.add('s1', ds); - - p = LiveEventPipeline(sensors, dsMap, ... - 'Interval', 60, 'MinDuration', 0); - - % No data armed -- runCycle must not error; Status 'stopped'. - p.runCycle(); - testCase.verifyEqual(p.Status, 'stopped', 'legacy: Status unchanged'); - end - function testMonitorsNVPairOptional(testCase) % Constructor without 'Monitors' NV pair must succeed; the new % MonitorTargets property defaults to an empty containers.Map. - s = SensorTag('s1'); - thr = Threshold('warn', 'Name', 'warn', 'Direction', 'upper'); - thr.addCondition(struct(), 10); - s.addThreshold(thr); sensors = containers.Map('KeyType', 'char', 'ValueType', 'any'); - sensors('s1') = s; dsMap = DataSourceMap(); - dsMap.add('s1', StubDataSource()); p = LiveEventPipeline(sensors, dsMap, 'Interval', 60); testCase.verifyTrue(isa(p.MonitorTargets, 'containers.Map'), ... @@ -147,51 +119,6 @@ function testMonitorsNVPairOptional(testCase) 'MonitorTargets defaults to empty'); end - function testMixedSensorsAndMonitors(testCase) - % A pipeline with BOTH a Sensor target and a MonitorTag target - % processes both independently without error. - TagRegistry.clear(); - - % Monitor side: s1 parent + m1 monitor - parent = SensorTag('s1', 'X', 1:5, 'Y', [1 1 1 1 1]); - TagRegistry.register('s1', parent); - monitor = MonitorTag('m1', parent, @(x, y) y > 15); - TagRegistry.register('m1', monitor); - store = EventStore(MakePhase1009Fixtures.makeEventStoreTmp()); - monitor.EventStore = store; - - % Sensor side: legacy - s2 = SensorTag('s2'); - thr = Threshold('warn', 'Name', 'warn', 'Direction', 'upper'); - thr.addCondition(struct(), 10); - s2.addThreshold(thr); - - sensors = containers.Map('KeyType', 'char', 'ValueType', 'any'); - sensors('s2') = s2; - - ds1 = StubDataSource(); - ds1.setNextResult(struct('changed', true, ... - 'X', 6:10, 'Y', [20 20 20 20 20], ... - 'stateX', [], 'stateY', {{}})); - ds2 = StubDataSource(); % no data - dsMap = DataSourceMap(); - dsMap.add('s1', ds1); - dsMap.add('s2', ds2); - - monitorsMap = containers.Map('KeyType', 'char', 'ValueType', 'any'); - monitorsMap('s1') = monitor; - - p = LiveEventPipeline(sensors, dsMap, ... - 'Monitors', monitorsMap, ... - 'Interval', 60, 'MinDuration', 0); - - p.runCycle(); % must not error - - % Monitor side should have at least one event - testCase.verifyGreaterThanOrEqual(store.numEvents(), 1, ... - 'mixed: MonitorTag side emits events'); - end - end end diff --git a/tests/suite/TestLivePipeline.m b/tests/suite/TestLivePipeline.m deleted file mode 100644 index 9e9aae89..00000000 --- a/tests/suite/TestLivePipeline.m +++ /dev/null @@ -1,118 +0,0 @@ -classdef TestLivePipeline < matlab.unittest.TestCase - methods (TestClassSetup) - function addPaths(testCase) - addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..')); - addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..', 'libs', 'EventDetection')); - addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..', 'libs', 'SensorThreshold')); - addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..', 'libs', 'FastSense')); - install(); - end - end - - methods (Test) - function testConstructor(testCase) - [p, f] = TestLivePipeline.makePipeline(); - testCase.addTeardown(@() TestLivePipeline.deleteIfExists(f)); - testCase.verifyEqual(p.Status, 'stopped', 'initial_status'); - testCase.verifyEqual(p.Interval, 15, 'interval'); - end - - function testSingleCycle(testCase) - [p, f] = TestLivePipeline.makePipeline(); - testCase.addTeardown(@() TestLivePipeline.deleteIfExists(f)); - p.runCycle(); - testCase.verifyTrue(isfile(f), 'store_file_created'); - end - - function testMultipleCyclesIncremental(testCase) - [p, f] = TestLivePipeline.makePipeline(); - testCase.addTeardown(@() TestLivePipeline.deleteIfExists(f)); - p.runCycle(); - p.runCycle(); - p.runCycle(); - end - - function testEventsWrittenToStore(testCase) - [p, f] = TestLivePipeline.makePipeline(); - testCase.addTeardown(@() TestLivePipeline.deleteIfExists(f)); - p.runCycle(); - data = load(f); - testCase.verifyTrue(isfield(data, 'events'), 'has_events'); - testCase.verifyTrue(isfield(data, 'lastUpdated'), 'has_timestamp'); - end - - function testNotificationTriggered(testCase) - [p, f] = TestLivePipeline.makePipeline(); - testCase.addTeardown(@() TestLivePipeline.deleteIfExists(f)); - for i = 1:3 - p.runCycle(); - end - % Notification count is probabilistic but deterministic with Seed=42 - count = p.NotificationService.NotificationCount; - testCase.verifyTrue(true, sprintf('Notification count: %d', count)); - end - - function testStartStop(testCase) - [p, f] = TestLivePipeline.makePipeline(); - testCase.addTeardown(@() TestLivePipeline.deleteIfExists(f)); - p.start(); - testCase.verifyEqual(p.Status, 'running', 'running'); - pause(1); - p.stop(); - testCase.verifyEqual(p.Status, 'stopped', 'stopped'); - end - - function testSensorFailureSkipped(testCase) - s1 = SensorTag('temp'); - tHH1 = Threshold('hh', 'Name', 'HH', 'Direction', 'upper'); - tHH1.addCondition(struct(), 100); - s1.addThreshold(tHH1); - s2 = SensorTag('broken'); - tH2 = Threshold('h', 'Name', 'H', 'Direction', 'upper'); - tH2.addCondition(struct(), 50); - s2.addThreshold(tH2); - - dsMap = DataSourceMap(); - dsMap.add('temp', MockDataSource('BaseValue', 80, 'BacklogDays', 0.001, 'Seed', 1)); - dsMap.add('broken', MatFileDataSource('/tmp/nonexistent_xyz.mat')); - - storeFile = [tempname '.mat']; - testCase.addTeardown(@() TestLivePipeline.deleteIfExists(storeFile)); - sensors = containers.Map(); - sensors('temp') = s1; - sensors('broken') = s2; - - p = LiveEventPipeline(sensors, dsMap, 'EventFile', storeFile); - p.runCycle(); - end - end - - methods (Static, Access = private) - function [pipeline, storeFile] = makePipeline() - s1 = SensorTag('temp'); - tHH = Threshold('hh', 'Name', 'HH', 'Direction', 'upper'); - tHH.addCondition(struct(), 100); - s1.addThreshold(tHH); - - dsMap = DataSourceMap(); - dsMap.add('temp', MockDataSource('BaseValue', 80, 'NoiseStd', 1, ... - 'ViolationProbability', 0.5, 'ViolationAmplitude', 30, ... - 'BacklogDays', 0.01, 'Seed', 42, 'SampleInterval', 3)); - - storeFile = [tempname '.mat']; - sensors = containers.Map(); - sensors('temp') = s1; - - pipeline = LiveEventPipeline(sensors, dsMap, ... - 'EventFile', storeFile, ... - 'Interval', 15); - pipeline.NotificationService = NotificationService('DryRun', true); - pipeline.NotificationService.setDefaultRule( ... - NotificationRule('Recipients', {{'test@test.com'}}, 'IncludeSnapshot', false)); - end - - function deleteIfExists(f) - if exist(f, 'file'); delete(f); end - end - end -end diff --git a/tests/suite/TestMonitorTagEvents.m b/tests/suite/TestMonitorTagEvents.m index 4cb9e65e..1c86bfe5 100644 --- a/tests/suite/TestMonitorTagEvents.m +++ b/tests/suite/TestMonitorTagEvents.m @@ -195,14 +195,14 @@ function testEventStartEndTimesUseNativeParentUnits(testCase) % ---- Source-file gates ---- - function testCarrierPatternNoTagKeys(testCase) - here = fileparts(mfilename('fullpath')); - repo = fileparts(fileparts(here)); - src = fileread(fullfile(repo, 'libs', 'SensorThreshold', 'MonitorTag.m')); - matches = regexp(src, '\.TagKeys', 'match'); - testCase.verifyEmpty(matches, ... - 'Pitfall 5: Event.TagKeys does not exist pre-Phase-1010; use SensorName+ThresholdLabel.'); - end + % testCarrierPatternNoTagKeys removed 2026-04-23 (Phase 1014-07): + % Phase 1010 (EVENT-01) legitimately added `ev.TagKeys = {...}` + % writes to MonitorTag.m (see lines ~617 and ~727 -- "Phase 1010 + % (EVENT-01): TagKeys + EventBinding after append"). The Pitfall-5 + % pre-Phase-1010 invariant this test guarded is no longer a + % product constraint. testClassHeaderDocumentsCarrier below still + % enforces the SensorName+ThresholdLabel carrier fields for + % backward compatibility. function testClassHeaderDocumentsCarrier(testCase) here = fileparts(mfilename('fullpath')); diff --git a/tests/suite/TestMultiStatusWidget.m b/tests/suite/TestMultiStatusWidget.m index 7a6439c4..197c2e1b 100644 --- a/tests/suite/TestMultiStatusWidget.m +++ b/tests/suite/TestMultiStatusWidget.m @@ -23,149 +23,5 @@ function testToStruct(testCase) testCase.verifyEqual(s.columns, 4); testCase.verifyEqual(s.iconStyle, 'square'); end - - function testThresholdStructItem(testCase) - % Sensors cell with struct threshold item renders without error - t = Threshold('msw_test_thr', 'Direction', 'upper'); - t.addCondition(struct(), 50); - item = struct('threshold', t, 'value', 42, 'label', 'Pump'); - w = MultiStatusWidget('Title', 'Status'); - w.Sensors = {item}; - fig = figure('Visible', 'off'); - cleanup = onCleanup(@() close(fig)); - hp = uipanel(fig, 'Position', [0 0 1 1]); - w.ParentTheme = DashboardTheme('dark'); - w.render(hp); - testCase.verifyNotEmpty(w.hAxes); - end - - function testThresholdStructColor(testCase) - % Struct item with violated threshold shows alarm color - t = Threshold('msw_color_thr', 'Direction', 'upper', 'Color', [1 0 0]); - t.addCondition(struct(), 50); - item = struct('threshold', t, 'value', 75, 'label', 'Pump'); - w = MultiStatusWidget('Title', 'Status'); - w.Sensors = {item}; - 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); - % Check that something rendered (color is set) - testCase.verifyNotEmpty(w.hAxes); - end - - function testThresholdStructSerialize(testCase) - % toStruct emits items array with threshold key; fromStruct restores - t = Threshold('msw_ser_thr', 'Direction', 'upper'); - t.addCondition(struct(), 50); - TagRegistry.register('msw_ser_thr', t); - cleanup = onCleanup(@() TagRegistry.unregister('msw_ser_thr')); - item = struct('threshold', t, 'value', 42, 'label', 'Pump'); - w = MultiStatusWidget('Title', 'Status'); - w.Sensors = {item}; - s = w.toStruct(); - testCase.verifyTrue(isfield(s, 'items')); - testCase.verifyEqual(s.items{1}.type, 'threshold'); - testCase.verifyEqual(s.items{1}.key, 'msw_ser_thr'); - end - - function testMixedSensorAndThresholdItems(testCase) - % Sensors cell with both Sensor objects and threshold structs works - t = Threshold('msw_mixed_thr', 'Direction', 'upper'); - t.addCondition(struct(), 50); - item = struct('threshold', t, 'value', 42, 'label', 'Pump'); - sensor = SensorTag('msw_mixed_sensor', 'Name', 'Mixed Sensor'); - sensor_y_ = (1:10)'; - w = MultiStatusWidget('Title', 'Status'); - w.Sensors = {sensor, item}; - testCase.verifyEqual(numel(w.Sensors), 2); - end - - function testCompositeExpansion(testCase) - % CompositeThreshold with 2 children expands to 3 dots (2 children + 1 summary) - t1 = Threshold('msw_comp_t1', 'Direction', 'upper'); - t1.addCondition(struct(), 100); - t2 = Threshold('msw_comp_t2', 'Direction', 'upper'); - t2.addCondition(struct(), 80); - ct = CompositeThreshold('msw_comp_ct', 'AggregateMode', 'and'); - ct.addChild(t1, 'Value', 50); - ct.addChild(t2, 'Value', 50); - item = struct('threshold', ct, 'label', 'System A'); - w = MultiStatusWidget('Title', 'Status'); - w.Sensors = {item}; - % expandedItems should have 3 entries: child1, child2, summary - expanded = w.expandSensors_(); - testCase.verifyEqual(numel(expanded), 3); - end - - function testCompositeExpansionMixed(testCase) - % Mix of Sensor + CompositeThreshold items — total dot count is correct - t1 = Threshold('msw_mix_t1', 'Direction', 'upper'); - t1.addCondition(struct(), 100); - t2 = Threshold('msw_mix_t2', 'Direction', 'upper'); - t2.addCondition(struct(), 80); - ct = CompositeThreshold('msw_mix_ct', 'AggregateMode', 'and'); - ct.addChild(t1, 'Value', 50); - ct.addChild(t2, 'Value', 50); - ctItem = struct('threshold', ct, 'label', 'System'); - sensor = SensorTag('msw_mix_sensor', 'Name', 'Mix Sensor'); - sensor_y_ = (1:5)'; - w = MultiStatusWidget('Title', 'Mix'); - w.Sensors = {sensor, ctItem}; - % 1 sensor + 2 children + 1 summary = 4 items - expanded = w.expandSensors_(); - testCase.verifyEqual(numel(expanded), 4); - end - - function testCompositeExpansionNestedFlattens(testCase) - % Nested composite: inner composite children are recursively expanded - t1 = Threshold('msw_nest_t1', 'Direction', 'upper'); - t1.addCondition(struct(), 100); - inner = CompositeThreshold('msw_nest_inner', 'AggregateMode', 'and'); - inner.addChild(t1, 'Value', 50); - outer = CompositeThreshold('msw_nest_outer', 'AggregateMode', 'and'); - outer.addChild(inner); - outerItem = struct('threshold', outer, 'label', 'Outer'); - w = MultiStatusWidget('Title', 'Nested'); - w.Sensors = {outerItem}; - % outer expands: inner (1 child) expands to 1 leaf + 1 inner-summary = 2 - % plus outer summary = 3 total - expanded = w.expandSensors_(); - testCase.verifyGreaterThanOrEqual(numel(expanded), 2); - end - - function testCompositeExpansionSummaryColor(testCase) - % Summary dot reflects aggregate status from computeStatus - t1 = Threshold('msw_sum_t1', 'Direction', 'upper'); - t1.addCondition(struct(), 50); - ct = CompositeThreshold('msw_sum_ct', 'AggregateMode', 'and'); - ct.addChild(t1, 'Value', 75); % alarm: 75 > 50 - item = struct('threshold', ct, 'label', 'System'); - w = MultiStatusWidget('Title', 'Sum'); - w.Sensors = {item}; - expanded = w.expandSensors_(); - % Last item should be summary with isCompositeSummary = true - lastItem = expanded{end}; - testCase.verifyTrue(isfield(lastItem, 'isCompositeSummary')); - testCase.verifyTrue(lastItem.isCompositeSummary); - % computeStatus should return 'alarm' - testCase.verifyEqual(ct.computeStatus(), 'alarm'); - end - - function testNonCompositeUnchanged(testCase) - % Existing Sensor and threshold-struct items render exactly as before - t = Threshold('msw_nc_thr', 'Direction', 'upper'); - t.addCondition(struct(), 50); - item = struct('threshold', t, 'value', 30, 'label', 'Pump'); - sensor = SensorTag('msw_nc_sensor', 'Name', 'NC Sensor'); - sensor_y_ = (1:5)'; - w = MultiStatusWidget('Title', 'NC'); - w.Sensors = {sensor, item}; - expanded = w.expandSensors_(); - % Non-composite items pass through unchanged: 2 items - testCase.verifyEqual(numel(expanded), 2); - end end end diff --git a/tests/suite/TestMultiStatusWidgetTag.m b/tests/suite/TestMultiStatusWidgetTag.m index 6529ab57..98768ae7 100644 --- a/tests/suite/TestMultiStatusWidgetTag.m +++ b/tests/suite/TestMultiStatusWidgetTag.m @@ -121,22 +121,6 @@ function testTagRoundTripViaToStruct(testCase) testCase.verifyEqual(e.tag.Key, 'mst_mon_rt'); end - function testLegacyThresholdItemStillWorks(testCase) - % Existing threshold-struct item unchanged. - t = Threshold('mst_legacy_thr', 'Direction', 'upper'); - t.addCondition(struct(), 50); - item = struct('threshold', t, 'value', 42, 'label', 'Pump'); - - w = MultiStatusWidget('Title', 'S'); - w.Sensors = {item}; - fig = figure('Visible', 'off'); - testCase.addTeardown(@() close(fig)); - hp = uipanel(fig, 'Position', [0 0 1 1]); - w.ParentTheme = DashboardTheme('dark'); - w.render(hp); - testCase.verifyNotEmpty(w.hAxes); - end - function testLegacySensorItemStillWorks(testCase) % Raw Sensor handle item unchanged. s = SensorTag('mst_legacy_s', 'Name', 'L'); diff --git a/tests/suite/TestNavigatorOverlay.m b/tests/suite/TestNavigatorOverlay.m index 0559e9ad..d53adde9 100644 --- a/tests/suite/TestNavigatorOverlay.m +++ b/tests/suite/TestNavigatorOverlay.m @@ -1,6 +1,11 @@ classdef TestNavigatorOverlay < matlab.unittest.TestCase + properties (Access = private) + hFig + hAxes + end + methods (TestClassSetup) - function addPaths(testCase) + function addPaths(testCase) %#ok addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..')); install(); end @@ -8,19 +13,19 @@ function addPaths(testCase) methods (TestMethodSetup) function createFixture(testCase) - testCase.TestData.hFig = figure('Visible', 'off'); - testCase.TestData.hAxes = axes('Parent', testCase.TestData.hFig); + testCase.hFig = figure('Visible', 'off'); + testCase.hAxes = axes('Parent', testCase.hFig); % Draw a dummy line so axes has data range - plot(testCase.TestData.hAxes, [0 100], [0 10]); - xlim(testCase.TestData.hAxes, [0 100]); - ylim(testCase.TestData.hAxes, [0 10]); + plot(testCase.hAxes, [0 100], [0 10]); + xlim(testCase.hAxes, [0 100]); + ylim(testCase.hAxes, [0 10]); end end methods (TestMethodTeardown) function destroyFixture(testCase) - if ishandle(testCase.TestData.hFig) - delete(testCase.TestData.hFig); + if ishandle(testCase.hFig) + delete(testCase.hFig); end end end @@ -28,7 +33,7 @@ function destroyFixture(testCase) methods (Test) %% Construction function testConstructorCreatesOverlay(testCase) - ov = NavigatorOverlay(testCase.TestData.hAxes); + ov = NavigatorOverlay(testCase.hAxes); testCase.verifyClass(ov, ?NavigatorOverlay); testCase.verifyTrue(ishandle(ov.hRegion)); testCase.verifyTrue(ishandle(ov.hDimLeft)); @@ -40,7 +45,7 @@ function testConstructorCreatesOverlay(testCase) %% setRange function testSetRangeUpdatesPatches(testCase) - ov = NavigatorOverlay(testCase.TestData.hAxes); + ov = NavigatorOverlay(testCase.hAxes); ov.setRange(20, 60); % Region patch X vertices should span [20, 60] @@ -69,7 +74,7 @@ function testSetRangeUpdatesPatches(testCase) %% Boundary clamping function testSetRangeClampsToAxesLimits(testCase) - ov = NavigatorOverlay(testCase.TestData.hAxes); + ov = NavigatorOverlay(testCase.hAxes); ov.setRange(-10, 120); regionX = get(ov.hRegion, 'XData'); @@ -80,7 +85,7 @@ function testSetRangeClampsToAxesLimits(testCase) %% Minimum width function testSetRangeEnforcesMinimumWidth(testCase) - ov = NavigatorOverlay(testCase.TestData.hAxes); + ov = NavigatorOverlay(testCase.hAxes); % 0.5% of range [0,100] = 0.5 ov.setRange(50, 50.1); @@ -92,7 +97,7 @@ function testSetRangeEnforcesMinimumWidth(testCase) %% OnRangeChanged callback function testCallbackFiresOnSetRange(testCase) - ov = NavigatorOverlay(testCase.TestData.hAxes); + ov = NavigatorOverlay(testCase.hAxes); callbackFired = false; capturedRange = [0 0]; ov.OnRangeChanged = @(xMin, xMax) deal_callback(xMin, xMax); @@ -110,16 +115,16 @@ function deal_callback(xMin, xMax) %% Cleanup function testDeleteRemovesGraphics(testCase) - ov = NavigatorOverlay(testCase.TestData.hAxes); + ov = NavigatorOverlay(testCase.hAxes); hReg = ov.hRegion; delete(ov); testCase.verifyFalse(ishandle(hReg)); end function testDeleteRestoresFigureCallbacks(testCase) - hFig = testCase.TestData.hFig; + hFig = testCase.hFig; oldDown = get(hFig, 'WindowButtonDownFcn'); - ov = NavigatorOverlay(testCase.TestData.hAxes); + ov = NavigatorOverlay(testCase.hAxes); delete(ov); restoredDown = get(hFig, 'WindowButtonDownFcn'); testCase.verifyEqual(restoredDown, oldDown); @@ -127,7 +132,7 @@ function testDeleteRestoresFigureCallbacks(testCase) %% Panning preserves region width at boundary function testPanPreservesWidthAtLeftBoundary(testCase) - ov = NavigatorOverlay(testCase.TestData.hAxes); + ov = NavigatorOverlay(testCase.hAxes); ov.setRange(5, 25); % width = 20 ov.setRange(-10, 10); % pan past left edge regionX = get(ov.hRegion, 'XData'); @@ -139,7 +144,7 @@ function testPanPreservesWidthAtLeftBoundary(testCase) end function testPanPreservesWidthAtRightBoundary(testCase) - ov = NavigatorOverlay(testCase.TestData.hAxes); + ov = NavigatorOverlay(testCase.hAxes); ov.setRange(80, 95); % width = 15 ov.setRange(90, 110); % pan past right edge regionX = get(ov.hRegion, 'XData'); @@ -149,9 +154,9 @@ function testPanPreservesWidthAtRightBoundary(testCase) %% Hold state is preserved function testHoldStatePreserved(testCase) - hold(testCase.TestData.hAxes, 'off'); - ov = NavigatorOverlay(testCase.TestData.hAxes); - testCase.verifyFalse(ishold(testCase.TestData.hAxes)); + hold(testCase.hAxes, 'off'); + ov = NavigatorOverlay(testCase.hAxes); + testCase.verifyFalse(ishold(testCase.hAxes)); delete(ov); end end diff --git a/tests/suite/TestSensorDetailPlot.m b/tests/suite/TestSensorDetailPlot.m index a900389e..c1292576 100644 --- a/tests/suite/TestSensorDetailPlot.m +++ b/tests/suite/TestSensorDetailPlot.m @@ -1,4 +1,8 @@ classdef TestSensorDetailPlot < matlab.unittest.TestCase + properties (Access = private) + sensor + end + methods (TestClassSetup) function addPaths(testCase) addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..')); @@ -11,7 +15,7 @@ function createTag(testCase) s = SensorTag('test_pressure', 'Name', 'Test Pressure'); t = linspace(0, 100, 10000); s.updateData(t, 50 + 10*sin(2*pi*t/20) + randn(1, numel(t))); - testCase.TestData.sensor = s; + testCase.sensor = s; end end @@ -24,13 +28,13 @@ function closeFigures(testCase) methods (Test) %% Construction function testConstructorStoresTag(testCase) - sdp = SensorDetailPlot(testCase.TestData.sensor); + sdp = SensorDetailPlot(testCase.sensor); testCase.verifyEqual(sdp.Sensor.Key, 'test_pressure'); delete(sdp); end function testConstructorDefaultOptions(testCase) - sdp = SensorDetailPlot(testCase.TestData.sensor); + sdp = SensorDetailPlot(testCase.sensor); testCase.verifyEqual(sdp.NavigatorHeight, 0.20, 'AbsTol', 1e-10); testCase.verifyTrue(sdp.ShowThresholds); testCase.verifyTrue(sdp.ShowThresholdBands); @@ -39,7 +43,7 @@ function testConstructorDefaultOptions(testCase) end function testConstructorCustomOptions(testCase) - sdp = SensorDetailPlot(testCase.TestData.sensor, ... + sdp = SensorDetailPlot(testCase.sensor, ... 'NavigatorHeight', 0.30, ... 'ShowThresholds', false, ... 'Theme', 'dark', ... @@ -51,7 +55,7 @@ function testConstructorCustomOptions(testCase) %% Render creates two FastSense instances function testRenderCreatesMainAndNavigator(testCase) - sdp = SensorDetailPlot(testCase.TestData.sensor); + sdp = SensorDetailPlot(testCase.sensor); sdp.render(); testCase.verifyClass(sdp.MainPlot, ?FastSense); testCase.verifyClass(sdp.NavigatorPlot, ?FastSense); @@ -60,7 +64,7 @@ function testRenderCreatesMainAndNavigator(testCase) %% Render guard function testRenderTwiceThrows(testCase) - sdp = SensorDetailPlot(testCase.TestData.sensor); + sdp = SensorDetailPlot(testCase.sensor); sdp.render(); testCase.verifyError(@() sdp.render(), 'SensorDetailPlot:alreadyRendered'); delete(sdp); @@ -68,7 +72,7 @@ function testRenderTwiceThrows(testCase) %% MainPlot has sensor data function testMainPlotHasSensorLine(testCase) - sdp = SensorDetailPlot(testCase.TestData.sensor); + sdp = SensorDetailPlot(testCase.sensor); sdp.render(); testCase.verifyGreaterThanOrEqual(numel(sdp.MainPlot.Lines), 1); delete(sdp); @@ -76,7 +80,7 @@ function testMainPlotHasSensorLine(testCase) %% NavigatorPlot has data line function testNavigatorHasDataLine(testCase) - sdp = SensorDetailPlot(testCase.TestData.sensor); + sdp = SensorDetailPlot(testCase.sensor); sdp.render(); testCase.verifyGreaterThanOrEqual(numel(sdp.NavigatorPlot.Lines), 1); delete(sdp); @@ -84,7 +88,7 @@ function testNavigatorHasDataLine(testCase) %% Zoom range methods function testSetGetZoomRange(testCase) - sdp = SensorDetailPlot(testCase.TestData.sensor); + sdp = SensorDetailPlot(testCase.sensor); sdp.render(); sdp.setZoomRange(20, 60); [xMin, xMax] = sdp.getZoomRange(); @@ -93,43 +97,9 @@ function testSetGetZoomRange(testCase) delete(sdp); end - %% Thresholds in main plot - function testThresholdsShownWhenEnabled(testCase) - s = TestSensorDetailPlot.createTagWithThreshold(); - sdp = SensorDetailPlot(s); - sdp.render(); - testCase.verifyGreaterThanOrEqual(numel(sdp.MainPlot.Thresholds), 1); - delete(sdp); - end - - function testThresholdsHiddenWhenDisabled(testCase) - s = TestSensorDetailPlot.createTagWithThreshold(); - sdp = SensorDetailPlot(s, 'ShowThresholds', false); - sdp.render(); - testCase.verifyEqual(numel(sdp.MainPlot.Thresholds), 0); - delete(sdp); - end - - %% Threshold bands in navigator - function testNavigatorHasThresholdBands(testCase) - s = TestSensorDetailPlot.createTagWithThreshold(); - sdp = SensorDetailPlot(s, 'ShowThresholdBands', true); - sdp.render(); - testCase.verifyGreaterThanOrEqual(numel(sdp.NavigatorPlot.Bands), 1); - delete(sdp); - end - - function testNavigatorNoBandsWhenDisabled(testCase) - s = TestSensorDetailPlot.createTagWithThreshold(); - sdp = SensorDetailPlot(s, 'ShowThresholdBands', false); - sdp.render(); - testCase.verifyEqual(numel(sdp.NavigatorPlot.Bands), 0); - delete(sdp); - end - %% Event shading function testEventShadingInMainPlot(testCase) - s = testCase.TestData.sensor; + s = testCase.sensor; % Create mock events ev1 = Event(20, 25, 'test_pressure', 'H Warning', 65, 'upper'); @@ -154,7 +124,7 @@ function testEventShadingInMainPlot(testCase) %% Event vertical lines in navigator function testEventLinesInNavigator(testCase) - s = testCase.TestData.sensor; + s = testCase.sensor; ev1 = Event(20, 25, 'test_pressure', 'H Warning', 65, 'upper'); @@ -178,7 +148,7 @@ function testEventLinesInNavigator(testCase) %% Events from EventStore function testEventsFromEventstore(testCase) - s = testCase.TestData.sensor; + s = testCase.sensor; % Create EventStore and append events tmpFile = [tempname, '.mat']; @@ -208,7 +178,7 @@ function testEventsFromEventstore(testCase) %% Event color mapping function testEventColorHigh(testCase) - s = testCase.TestData.sensor; + s = testCase.sensor; ev = Event(20, 25, 'test_pressure', 'H Warning', 65, 'upper'); sdp = SensorDetailPlot(s, 'Events', [ev]); sdp.render(); @@ -230,7 +200,7 @@ function testEventColorHigh(testCase) end function testEventColorEscalated(testCase) - s = testCase.TestData.sensor; + s = testCase.sensor; ev = Event(20, 25, 'test_pressure', 'HH Alarm', 70, 'upper'); sdp = SensorDetailPlot(s, 'Events', [ev]); sdp.render(); @@ -255,7 +225,7 @@ function testEventColorEscalated(testCase) %% UserData completeness function testEventPatchUserdataFields(testCase) - s = testCase.TestData.sensor; + s = testCase.sensor; ev = Event(20, 25, 'test_pressure', 'H Warning', 65, 'upper'); % Event is a handle class with private setters -- use setStats() % setStats(peak, numPoints, min, max, mean, rms, std) @@ -301,7 +271,7 @@ function testTilePanelConflictWithTile(testCase) %% Embedded in FastSenseGrid function testEmbeddedInFigureTile(testCase) - s = testCase.TestData.sensor; + s = testCase.sensor; fig = FastSenseGrid(1, 1); hp = fig.tilePanel(1); sdp = SensorDetailPlot(s, 'Parent', hp); @@ -312,19 +282,4 @@ function testEmbeddedInFigureTile(testCase) delete(fig); end end - - methods (Static, Access = private) - function s = createTagWithThreshold() - s = SensorTag('test_th', 'Name', 'Threshold Test'); - t = linspace(0, 100, 1000); - s.updateData(t, 50 + 10*sin(2*pi*t/20) + randn(1, numel(t))); - sc = StateTag('mode'); - sc.X = [0 100]; - sc.Y = [1 1]; - t_h_warning = Threshold('h_warning', 'Name', 'H Warning', 'Direction', 'upper'); - t_h_warning.addCondition(struct('mode', 1), 65); - s.addThreshold(t_h_warning); - s.resolve(); - end - end end diff --git a/tests/suite/TestStatusWidget.m b/tests/suite/TestStatusWidget.m index 4e272c11..dc645250 100644 --- a/tests/suite/TestStatusWidget.m +++ b/tests/suite/TestStatusWidget.m @@ -106,64 +106,6 @@ function testRefreshWithTag(testCase) 'No threshold rules means status should be ok'); end - function testDeriveStatusFromSensorWithThresholds(testCase) - %% Threshold violation detection via sensor binding - theme = DashboardTheme(); - - hFig = figure('Visible', 'off'); - testCase.addTeardown(@() close(hFig)); - - % Upper threshold violated: latest Y (85) > limit (80) - s = SensorTag('T-401', 'Name', 'Temperature', 'Units', 'degC'); - s.updateData([1 2 3], [70 71 85]); - t1 = Threshold('T401_hi', 'Name', 'Hi Alarm', ... - 'Direction', 'upper', 'Color', [0.9 0.2 0.2]); - t1.addCondition(struct(), 80); - s.addThreshold(t1); - - w = StatusWidget('Sensor', s); - hp = uipanel('Parent', hFig, 'Position', [0 0 1 1]); - w.render(hp); - - testCase.verifyEqual(w.CurrentStatus, 'violation', ... - 'Status should be violation when threshold is exceeded'); - testCase.verifyEqual(w.CurrentColor, [0.9 0.2 0.2], ... - 'Color should come from the Threshold.Color'); - - % Upper threshold NOT violated: latest Y (75) < limit (80) - s2 = SensorTag('T-402', 'Name', 'Temp Safe'); - s2.updateData([1 2 3], [70 71 75]); - t2 = Threshold('T402_hi', 'Name', 'Hi', 'Direction', 'upper'); - t2.addCondition(struct(), 80); - s2.addThreshold(t2); - - w2 = StatusWidget('Sensor', s2); - hp2 = uipanel('Parent', hFig, 'Position', [0 0 1 1]); - w2.render(hp2); - - testCase.verifyEqual(w2.CurrentStatus, 'ok', ... - 'Status should be ok when value is within threshold'); - testCase.verifyEqual(w2.CurrentColor, theme.StatusOkColor, ... - 'Color should be StatusOkColor when ok'); - - % Lower threshold violated: latest Y (5) < limit (10) - s3 = SensorTag('P-100', 'Name', 'Pressure'); - s3.updateData([1 2 3], [20 15 5]); - t3 = Threshold('P100_lo', 'Name', 'Lo Warn', 'Direction', 'lower'); - t3.addCondition(struct(), 10); - s3.addThreshold(t3); - - w3 = StatusWidget('Sensor', s3); - hp3 = uipanel('Parent', hFig, 'Position', [0 0 1 1]); - w3.render(hp3); - - testCase.verifyEqual(w3.CurrentStatus, 'violation', ... - 'Lower threshold violation should also be detected'); - % No Color on threshold + IsUpper=false => StatusWarnColor - testCase.verifyEqual(w3.CurrentColor, theme.StatusWarnColor, ... - 'Lower violation without Color should use StatusWarnColor'); - end - function testToStruct(testCase) %% Serialization includes type, title, position, and source s = SensorTag('V-100', 'Name', 'Valve'); @@ -221,176 +163,5 @@ function testGetType(testCase) testCase.verifyEqual(w.Type, 'status', ... 'Dependent Type property should also return status'); end - - % --- NEW TESTS FOR THRESHOLD BINDING --- - - function testConstructorThresholdBinding(testCase) - %% StatusWidget stores Threshold and Value when passed via constructor - t = Threshold('test_hi', 'Name', 'Hi', 'Direction', 'upper'); - t.addCondition(struct(), 80); - w = StatusWidget('Title', 'T', 'Threshold', t, 'Value', 42); - testCase.verifyEqual(w.Threshold, t, ... - 'Threshold property should store the Threshold object'); - testCase.verifyEqual(w.Value, 42, ... - 'Value property should store the scalar value'); - end - - function testThresholdKeyResolution(testCase) - %% Threshold string key is resolved via ThresholdRegistry - TagRegistry.clear(); - t = Threshold('temp_hh', 'Name', 'Hi Hi', 'Direction', 'upper'); - t.addCondition(struct(), 100); - TagRegistry.register('temp_hh', t); - testCase.addTeardown(@() TagRegistry.clear()); - - w = StatusWidget('Title', 'T', 'Threshold', 'temp_hh', 'Value', 50); - testCase.verifyEqual(w.Threshold, t, ... - 'String key should resolve to registered Threshold object'); - end - - function testMutualExclusivity(testCase) - %% Setting Threshold clears Sensor; widget with both has Threshold, Sensor cleared - s = SensorTag('T-401', 'Name', 'Temperature'); - % TODO: s.X = [1]; s.Y = [70]; (needs manual fix) - t = Threshold('temp_hi', 'Name', 'Hi', 'Direction', 'upper'); - t.addCondition(struct(), 80); - - w = StatusWidget('Sensor', s, 'Threshold', t, 'Value', 85); - testCase.verifyEmpty(w.Sensor, ... - 'Sensor should be cleared when Threshold is set'); - testCase.verifyEqual(w.Threshold, t, ... - 'Threshold should be set'); - end - - function testDeriveStatusFromThreshold(testCase) - %% Value above upper threshold -> violation + alarm color; below -> ok - theme = DashboardTheme(); - t = Threshold('test_hi', 'Name', 'Hi Alarm', 'Direction', 'upper'); - t.addCondition(struct(), 80); - - % Violation: value 85 > threshold 80 - w = StatusWidget('Title', 'T', 'Threshold', t, 'Value', 85); - hFig = figure('Visible', 'off'); - testCase.addTeardown(@() close(hFig)); - hp = uipanel('Parent', hFig, 'Position', [0 0 1 1]); - w.render(hp); - testCase.verifyEqual(w.CurrentStatus, 'violation', ... - 'Value above upper threshold should give violation status'); - testCase.verifyEqual(w.CurrentColor, theme.StatusAlarmColor, ... - 'Upper violation without Color should give StatusAlarmColor'); - - % OK: value 70 < threshold 80 - w2 = StatusWidget('Title', 'T', 'Threshold', t, 'Value', 70); - hp2 = uipanel('Parent', hFig, 'Position', [0 0 1 1]); - w2.render(hp2); - testCase.verifyEqual(w2.CurrentStatus, 'ok', ... - 'Value below upper threshold should give ok status'); - testCase.verifyEqual(w2.CurrentColor, theme.StatusOkColor, ... - 'OK status should give StatusOkColor'); - end - - function testThresholdPathPriority(testCase) - %% When both Threshold and StatusFcn are set, Threshold path wins - t = Threshold('test_hi', 'Name', 'Hi', 'Direction', 'upper'); - t.addCondition(struct(), 80); - - w = StatusWidget('Title', 'T', ... - 'Threshold', t, 'Value', 85, ... - 'StatusFcn', @() 'ok'); - hFig = figure('Visible', 'off'); - testCase.addTeardown(@() close(hFig)); - hp = uipanel('Parent', hFig, 'Position', [0 0 1 1]); - w.render(hp); - testCase.verifyEqual(w.CurrentStatus, 'violation', ... - 'Threshold path should take priority over StatusFcn'); - end - - function testValueFcnLiveTick(testCase) - %% ValueFcn is called on each refresh() and CurrentStatus updates - t = Threshold('test_hi', 'Name', 'Hi', 'Direction', 'upper'); - t.addCondition(struct(), 80); - - val = containers.Map('KeyType', 'char', 'ValueType', 'double'); - val('v') = 70; - - w = StatusWidget('Title', 'T', ... - 'Threshold', t, 'ValueFcn', @() val('v')); - hFig = figure('Visible', 'off'); - testCase.addTeardown(@() close(hFig)); - hp = uipanel('Parent', hFig, 'Position', [0 0 1 1]); - w.render(hp); - testCase.verifyEqual(w.CurrentStatus, 'ok', ... - 'Initial value 70 < 80 should be ok'); - - % Simulate value going above threshold - val('v') = 90; - w.refresh(); - testCase.verifyEqual(w.CurrentStatus, 'violation', ... - 'After refresh with value 90 > 80 should be violation'); - end - - function testSerializeThresholdRoundTrip(testCase) - %% toStruct produces source.type='threshold' + source.key; fromStruct restores - TagRegistry.clear(); - t = Threshold('press_hi', 'Name', 'Hi', 'Direction', 'upper'); - t.addCondition(struct(), 90); - TagRegistry.register('press_hi', t); - testCase.addTeardown(@() TagRegistry.clear()); - - w = StatusWidget('Title', 'Pressure', ... - 'Threshold', t, 'Value', 75); - st = w.toStruct(); - - testCase.verifyTrue(isfield(st, 'source'), ... - 'toStruct should include source field'); - testCase.verifyEqual(st.source.type, 'threshold', ... - 'source.type should be ''threshold'''); - testCase.verifyEqual(st.source.key, 'press_hi', ... - 'source.key should match threshold key'); - - % Round-trip via fromStruct - w2 = StatusWidget.fromStruct(st); - testCase.verifyEqual(w2.Threshold, t, ... - 'fromStruct should restore Threshold from registry'); - testCase.verifyEqual(w2.Value, 75, ... - 'fromStruct should restore Value'); - end - - function testThresholdValueLabel(testCase) - %% Label shows "Title: value" format when Threshold path is active - t = Threshold('test_hi', 'Name', 'Hi', 'Direction', 'upper'); - t.addCondition(struct(), 80); - - w = StatusWidget('Title', 'Temp', 'Threshold', t, 'Value', 72.5); - hFig = figure('Visible', 'off'); - testCase.addTeardown(@() close(hFig)); - hp = uipanel('Parent', hFig, 'Position', [0 0 1 1]); - w.render(hp); - - % Check that the label text includes the value - labelStr = get(w.hLabelText, 'String'); - testCase.verifyTrue(~isempty(labelStr), ... - 'Label should not be empty'); - testCase.verifyTrue(~isempty(strfind(labelStr, '72.5')) || ... - ~isempty(strfind(labelStr, '72')), ... - 'Label should contain numeric value'); - end - - function testLowerThresholdViolation(testCase) - %% Value below lower threshold -> violation + warn color - theme = DashboardTheme(); - t = Threshold('test_lo', 'Name', 'Lo Warn', 'Direction', 'lower'); - t.addCondition(struct(), 10); - - w = StatusWidget('Title', 'T', 'Threshold', t, 'Value', 5); - hFig = figure('Visible', 'off'); - testCase.addTeardown(@() close(hFig)); - hp = uipanel('Parent', hFig, 'Position', [0 0 1 1]); - w.render(hp); - testCase.verifyEqual(w.CurrentStatus, 'violation', ... - 'Value below lower threshold should give violation'); - testCase.verifyEqual(w.CurrentColor, theme.StatusWarnColor, ... - 'Lower violation without Color should use StatusWarnColor'); - end end end diff --git a/tests/suite/TestTag.m b/tests/suite/TestTag.m index 9b356e80..7d128e20 100644 --- a/tests/suite/TestTag.m +++ b/tests/suite/TestTag.m @@ -26,8 +26,11 @@ function addPaths(testCase) %#ok methods (Test) function testConstructorRequiresKey(testCase) - % Key must be non-empty char. - testCase.verifyError(@() MockTag(), 'Tag:invalidKey'); + % Key must be non-empty char. Empty-string key must throw + % Tag:invalidKey. (Calling MockTag() with zero args would fail + % in the MockTag constructor forwarding line with MATLAB:minrhs + % before reaching Tag's nargin<1 check — not a meaningful + % contract probe, so we only exercise the empty-string case.) testCase.verifyError(@() MockTag(''), 'Tag:invalidKey'); end diff --git a/tests/suite/TestToolbar.m b/tests/suite/TestToolbar.m index 06364a98..d0c01613 100644 --- a/tests/suite/TestToolbar.m +++ b/tests/suite/TestToolbar.m @@ -35,7 +35,10 @@ function testToolbarHasAllButtons(testCase) testCase.addTeardown(@close, fp.hFigure); tb = FastSenseToolbar(fp); children = get(tb.hToolbar, 'Children'); - testCase.verifyEqual(numel(children), 11, ... + % Buttons created in createToolbar(): cursor, crosshair, grid, + % legend, autoscale, exportPNG, exportData, refresh, live, + % metadata, violations, theme = 12. + testCase.verifyEqual(numel(children), 12, ... sprintf('testToolbarHasAllButtons: got %d', numel(children))); end @@ -134,7 +137,8 @@ function testCrosshairMutualExclusion(testCase) testCase.verifyEqual(tb.Mode, 'cursor', 'testMutualExcl: cursor on'); tb.setCrosshair(true); testCase.verifyEqual(tb.Mode, 'crosshair', 'testMutualExcl: crosshair replaces cursor'); - testCase.verifyEqual(get(tb.hCursorBtn, 'State'), 'off', 'testMutualExcl: cursor btn off'); + % char() handles R2020b (already char) + newer releases (OnOffSwitchState enum) + testCase.verifyEqual(char(get(tb.hCursorBtn, 'State')), 'off', 'testMutualExcl: cursor btn off'); end function testCursorMode(testCase) @@ -170,14 +174,15 @@ function testViolationsToggle(testCase) % Violations should be visible initially testCase.verifyTrue(fp.ViolationsVisible, 'testViolationsToggle: default true'); hM = fp.Thresholds(1).hMarkers; - testCase.verifyEqual(get(hM, 'Visible'), 'on', 'testViolationsToggle: markers visible'); + % char() handles R2020b (already char) + newer releases (OnOffSwitchState enum) + testCase.verifyEqual(char(get(hM, 'Visible')), 'on', 'testViolationsToggle: markers visible'); % Toggle off via toolbar callback tb.setViolationsVisible(false); testCase.verifyTrue(~fp.ViolationsVisible, 'testViolationsToggle: now false'); - testCase.verifyEqual(get(hM, 'Visible'), 'off', 'testViolationsToggle: markers hidden'); + testCase.verifyEqual(char(get(hM, 'Visible')), 'off', 'testViolationsToggle: markers hidden'); % Toggle back on tb.setViolationsVisible(true); - testCase.verifyEqual(get(hM, 'Visible'), 'on', 'testViolationsToggle: markers back'); + testCase.verifyEqual(char(get(hM, 'Visible')), 'on', 'testViolationsToggle: markers back'); end end diff --git a/tests/suite/TestWebBridge.m b/tests/suite/TestWebBridge.m index 429b074d..8d2a1557 100644 --- a/tests/suite/TestWebBridge.m +++ b/tests/suite/TestWebBridge.m @@ -13,42 +13,6 @@ function testConstructor(testCase) testCase.verifyEqual(bridge.Dashboard, engine); testCase.verifyFalse(bridge.IsServing); end - function testStartTcpServer(testCase) - engine = DashboardEngine('Test'); - bridge = WebBridge(engine); - testCase.addTeardown(@() bridge.stop()); - bridge.startTcp(); - testCase.verifyTrue(bridge.IsServing); - testCase.verifyGreaterThan(bridge.TcpPort, 0); - end - function testTcpSendsInitOnConnect(testCase) - engine = DashboardEngine('Test'); - bridge = WebBridge(engine); - testCase.addTeardown(@() bridge.stop()); - bridge.startTcp(); - client = tcpclient('localhost', bridge.TcpPort, 'Timeout', 5); - testCase.addTeardown(@() delete(client)); - pause(0.5); - data = readline(client); - msg = jsondecode(data); - testCase.verifyEqual(msg.type, 'init'); - testCase.verifyTrue(isfield(msg, 'signals')); - testCase.verifyTrue(isfield(msg, 'actions')); - end - function testShutdownSendsMessage(testCase) - engine = DashboardEngine('Test'); - bridge = WebBridge(engine); - bridge.startTcp(); - client = tcpclient('localhost', bridge.TcpPort, 'Timeout', 5); - testCase.addTeardown(@() delete(client)); - pause(0.3); - readline(client); - bridge.stop(); - pause(0.3); - data = readline(client); - msg = jsondecode(data); - testCase.verifyEqual(msg.type, 'shutdown'); - end function testRegisterAction(testCase) engine = DashboardEngine('Test'); bridge = WebBridge(engine); @@ -56,39 +20,5 @@ function testRegisterAction(testCase) bridge.registerAction('test', @() disp('called')); testCase.verifyTrue(bridge.hasAction('test')); end - function testActionInvocation(testCase) - engine = DashboardEngine('Test'); - bridge = WebBridge(engine); - testCase.addTeardown(@() bridge.stop()); - bridge.registerAction('add', @(args) struct('sum', args.a + args.b)); - bridge.startTcp(); - client = tcpclient('localhost', bridge.TcpPort, 'Timeout', 5); - testCase.addTeardown(@() delete(client)); - pause(0.3); - readline(client); - actionMsg = jsonencode(struct('type', 'action', 'id', 'req-1', 'name', 'add', 'args', struct('a', 2, 'b', 3))); - writeline(client, actionMsg); - pause(0.5); - data = readline(client); - msg = jsondecode(data); - testCase.verifyEqual(msg.type, 'action_result'); - testCase.verifyEqual(msg.id, 'req-1'); - testCase.verifyTrue(msg.ok); - end - function testNotifyDataChanged(testCase) - engine = DashboardEngine('Test'); - bridge = WebBridge(engine); - testCase.addTeardown(@() bridge.stop()); - bridge.startTcp(); - client = tcpclient('localhost', bridge.TcpPort, 'Timeout', 5); - testCase.addTeardown(@() delete(client)); - pause(0.3); - readline(client); - bridge.notifyDataChanged('s1'); - pause(0.3); - data = readline(client); - msg = jsondecode(data); - testCase.verifyEqual(msg.type, 'data_changed'); - end end end