From 926a2cd50b168919b78b5c701acf090aec2f91b2 Mon Sep 17 00:00:00 2001 From: Mukesh Angrakh Date: Thu, 9 Apr 2026 10:12:20 +0530 Subject: [PATCH 1/4] Add incremental build support to ExampleDrivenTesterTask - Set task Inputs to file globs (/**/*.m and /**/*.mlx) instead of folder paths so buildtool detects individual file changes - Always set task Outputs to a .last_run stamp file so buildtool can determine up-to-date status regardless of CreateTestReport setting - Write stamp file after successful test execution - Always create OutputPath directory since stamp file needs it - Add 7 new tests covering input tracking, output setup, stamp file creation, and incremental skip behavior Closes #10 Amp-Thread-ID: https://ampcode.com/threads/T-019d430c-3162-70ce-bbf0-6143f9221e1f Co-authored-by: Amp --- tests/tExampleDrivenTesterTask.m | 123 ++++++++++++++++++ .../examples/a.txt | 1 + .../examples/example.m | 9 ++ .../examples/example2.m | 8 ++ .../examples/exampleFunc.m | 11 ++ .../examples/exampleFuncInputs | 9 ++ .../examples/exampleMlx.mlx | Bin 0 -> 3025 bytes .../examples/new feature/example3.m | 8 ++ toolbox/internal/ExampleDrivenTesterTask.m | 21 +-- 9 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 tests/tExampleDrivenTesterTask.m create mode 100644 tests/tExampleDrivenTesterTask_files/examples/a.txt create mode 100644 tests/tExampleDrivenTesterTask_files/examples/example.m create mode 100644 tests/tExampleDrivenTesterTask_files/examples/example2.m create mode 100644 tests/tExampleDrivenTesterTask_files/examples/exampleFunc.m create mode 100644 tests/tExampleDrivenTesterTask_files/examples/exampleFuncInputs create mode 100644 tests/tExampleDrivenTesterTask_files/examples/exampleMlx.mlx create mode 100644 tests/tExampleDrivenTesterTask_files/examples/new feature/example3.m diff --git a/tests/tExampleDrivenTesterTask.m b/tests/tExampleDrivenTesterTask.m new file mode 100644 index 0000000..a0ab2d0 --- /dev/null +++ b/tests/tExampleDrivenTesterTask.m @@ -0,0 +1,123 @@ +classdef tExampleDrivenTesterTask < matlab.unittest.TestCase + % Test verifies ExampleDrivenTesterTask buildtool integration + % including incremental build support. + + properties (Access=private) + ExamplesFolder string + ToolboxPath string + InternalPath string + end + + methods (TestClassSetup) + function pathSetup(testCase) + import matlab.unittest.fixtures.PathFixture; + import matlab.unittest.fixtures.CurrentFolderFixture + testCase.ToolboxPath = fullfile(fileparts(pwd), "toolbox"); + testCase.InternalPath = fullfile(testCase.ToolboxPath, "internal"); + testCase.applyFixture(PathFixture(testCase.ToolboxPath)); + testCase.applyFixture(PathFixture(testCase.InternalPath)); + testCase.applyFixture(CurrentFolderFixture(mfilename + "_files")); + testCase.ExamplesFolder = fullfile(pwd, "examples"); + end + end + + methods (TestMethodSetup) + function ensurePath(testCase) %#ok + % buildplan/run may remove paths when it opens/closes the project. + % Re-add them before each test to ensure ExampleDrivenTesterTask + % is always on the path. + toolboxDir = fullfile(fileparts(fileparts(mfilename('fullpath'))), "toolbox"); + internalDir = fullfile(toolboxDir, "internal"); + addpath(toolboxDir); + addpath(internalDir); + end + end + + methods (Test) + + function verifyInputsTrackFiles(testCase) + % Verify that task Inputs use file globs, not just folder paths + task = ExampleDrivenTesterTask(testCase.ExamplesFolder); + inputPaths = task.Inputs.paths; + testCase.verifyGreaterThan(numel(inputPaths), 0, ... + "Task Inputs should resolve to actual files inside folders"); + end + + function verifyOutputsAlwaysSet(testCase) + % Verify that task Outputs is always set (stamp file) regardless + % of CreateTestReport value + task = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... + CreateTestReport=false); + testCase.verifyNotEmpty(task.Outputs, ... + "Task Outputs should always be set for incremental build support"); + end + + function verifyOutputsSetWhenReportEnabled(testCase) + % Verify that task Outputs is set when CreateTestReport is true + task = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... + CreateTestReport=true); + testCase.verifyNotEmpty(task.Outputs, ... + "Task Outputs should be set when CreateTestReport is true"); + end + + function verifyStampFileCreatedAfterRun(testCase) + % Verify that the stamp file is created after task execution + import matlab.unittest.fixtures.TemporaryFolderFixture + testCase.applyFixture(TemporaryFolderFixture); + + outputPath = testCase.createTemporaryFolder; + plan = buildplan; + plan("runExample") = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... + OutputPath=outputPath); + run(plan, "runExample"); + + stampFile = fullfile(outputPath, ".last_run"); + testCase.verifyEqual(exist(stampFile, "file"), 2, ... + "Stamp file should be created after task execution"); + end + + function verifyIncrementalBuildSkipsWhenUpToDate(testCase) + % Verify that the task is skipped on the second run when + % nothing has changed + import matlab.unittest.fixtures.TemporaryFolderFixture + testCase.applyFixture(TemporaryFolderFixture); + + outputPath = testCase.createTemporaryFolder; + plan = buildplan; + plan("runExample") = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... + OutputPath=outputPath); + + % First run — should execute + result1 = run(plan, "runExample"); + testCase.assertFalse(result1.TaskResults.Skipped, ... + "Task should run on first execution"); + + % Second run — should be skipped (up-to-date) + result2 = run(plan, "runExample"); + testCase.verifyTrue(result2.TaskResults.Skipped, ... + "Task should be skipped on second run when inputs are unchanged"); + end + + function verifyInputsIncludeMlxFiles(testCase) + % Verify that task Inputs include both .m and .mlx files + task = ExampleDrivenTesterTask(testCase.ExamplesFolder); + inputPaths = task.Inputs.paths; + hasMlx = any(endsWith(inputPaths, ".mlx")); + hasM = any(endsWith(inputPaths, ".m")); + testCase.verifyTrue(hasM, "Task Inputs should include .m files"); + testCase.verifyTrue(hasMlx, "Task Inputs should include .mlx files"); + end + + function verifyOutputStampFileLocation(testCase) + % Verify the stamp file output path is correctly set + outputPath = testCase.createTemporaryFolder; + task = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... + OutputPath=outputPath); + outputPaths = task.Outputs.paths; + testCase.verifyTrue(any(endsWith(outputPaths, ".last_run")), ... + "Task Outputs should contain the .last_run stamp file"); + end + + end + +end diff --git a/tests/tExampleDrivenTesterTask_files/examples/a.txt b/tests/tExampleDrivenTesterTask_files/examples/a.txt new file mode 100644 index 0000000..f5be726 --- /dev/null +++ b/tests/tExampleDrivenTesterTask_files/examples/a.txt @@ -0,0 +1 @@ +% Adding the file, to verify that Examples driven tester does not error out if examples folder has differnt file types \ No newline at end of file diff --git a/tests/tExampleDrivenTesterTask_files/examples/example.m b/tests/tExampleDrivenTesterTask_files/examples/example.m new file mode 100644 index 0000000..6ab114d --- /dev/null +++ b/tests/tExampleDrivenTesterTask_files/examples/example.m @@ -0,0 +1,9 @@ +% How to use addThem + +a = 10; +b = 20; +c = 30; + +% Call addThem with a and b as input + +c = addThem(a,b); \ No newline at end of file diff --git a/tests/tExampleDrivenTesterTask_files/examples/example2.m b/tests/tExampleDrivenTesterTask_files/examples/example2.m new file mode 100644 index 0000000..95d37b6 --- /dev/null +++ b/tests/tExampleDrivenTesterTask_files/examples/example2.m @@ -0,0 +1,8 @@ +% How to use addThem + +a = 80; +b = 20; +c= 10; +% Call addThem with a and b as input + +c = addThem(a,b); \ No newline at end of file diff --git a/tests/tExampleDrivenTesterTask_files/examples/exampleFunc.m b/tests/tExampleDrivenTesterTask_files/examples/exampleFunc.m new file mode 100644 index 0000000..42b194e --- /dev/null +++ b/tests/tExampleDrivenTesterTask_files/examples/exampleFunc.m @@ -0,0 +1,11 @@ +function exampleFunc() +% How to use addThem + +a = 10; +b = 20; +c = 30; + +% Call addThem with a and b as input + +c = addThem(a,b); +end \ No newline at end of file diff --git a/tests/tExampleDrivenTesterTask_files/examples/exampleFuncInputs b/tests/tExampleDrivenTesterTask_files/examples/exampleFuncInputs new file mode 100644 index 0000000..bac50a7 --- /dev/null +++ b/tests/tExampleDrivenTesterTask_files/examples/exampleFuncInputs @@ -0,0 +1,9 @@ +function exampleFuncInputs(a, b) +% How to use addThem + +c = 30; + +% Call addThem with a and b as input + +c = addThem(a,b); +end \ No newline at end of file diff --git a/tests/tExampleDrivenTesterTask_files/examples/exampleMlx.mlx b/tests/tExampleDrivenTesterTask_files/examples/exampleMlx.mlx new file mode 100644 index 0000000000000000000000000000000000000000..01650051fb38151b74ae80195cdfabccdeca0a36 GIT binary patch literal 3025 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 uhW Date: Tue, 5 May 2026 12:39:50 +0530 Subject: [PATCH 2/4] Fix incremental build by using static default OutputPath The timestamp-based default caused a new output folder on every buildfile evaluation, so the stamp file was never found on subsequent runs and the task always re-executed. Co-Authored-By: Claude Opus 4.6 --- toolbox/internal/ExampleDrivenTesterTask.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolbox/internal/ExampleDrivenTesterTask.m b/toolbox/internal/ExampleDrivenTesterTask.m index 53083c2..e2ce549 100644 --- a/toolbox/internal/ExampleDrivenTesterTask.m +++ b/toolbox/internal/ExampleDrivenTesterTask.m @@ -25,7 +25,7 @@ folders (1,:) string options.CreateTestReport (1,1) logical = true options.TestReportFormat (1,1) string {mustBeMember(options.TestReportFormat,["html", "pdf", "docx", "xml"])} = "html" - options.OutputPath(1,1) string = "reports_" + char(datetime('now', 'Format', 'yyyyMMdd_HHmmss')) + options.OutputPath(1,1) string = "test-report" options.CodeCoveragePlugin = [] options.CleanupFcn = [] end From b8ebe68438bc5d33e25216ec0cab11089eecf01d Mon Sep 17 00:00:00 2001 From: Shivam Lahoti Date: Wed, 13 May 2026 16:05:08 +0530 Subject: [PATCH 3/4] Add SourceFiles property to gate incremental build support Incremental build now only activates when SourceFiles is provided, matching MathWorks TestTask behavior. Without SourceFiles, the task always re-runs. This ensures examples are always exercised unless the user explicitly opts in to skipping via tracked source dependencies. Co-Authored-By: Claude Opus 4.6 --- buildfile.m | 4 +- tests/tExampleDrivenTesterTask.m | 124 +++++++++++++++--- .../source/myFunc.m | 3 + toolbox/internal/ExampleDrivenTesterTask.m | 39 +++--- 4 files changed, 135 insertions(+), 35 deletions(-) create mode 100644 tests/tExampleDrivenTesterTask_files/source/myFunc.m diff --git a/buildfile.m b/buildfile.m index 41d177f..eab405c 100644 --- a/buildfile.m +++ b/buildfile.m @@ -16,7 +16,9 @@ % Run MATLAB scripts from specified folder and generate a code coverage report reportFormat = matlab.unittest.plugins.codecoverage.CoverageReport('coverage-report'); covPlugin = matlab.unittest.plugins.CodeCoveragePlugin.forFolder("toolbox/sampleToolbox/code", "Producing", reportFormat); -plan("runExample") = ExampleDrivenTesterTask("toolbox/sampleToolbox/examples", CodeCoveragePlugin = covPlugin); +plan("runExample") = ExampleDrivenTesterTask("toolbox/sampleToolbox/examples", ... + SourceFiles = "toolbox/sampleToolbox/code", ... + CodeCoveragePlugin = covPlugin); plan.DefaultTasks = "test"; diff --git a/tests/tExampleDrivenTesterTask.m b/tests/tExampleDrivenTesterTask.m index a0ab2d0..744922d 100644 --- a/tests/tExampleDrivenTesterTask.m +++ b/tests/tExampleDrivenTesterTask.m @@ -4,6 +4,7 @@ properties (Access=private) ExamplesFolder string + SourceFolder string ToolboxPath string InternalPath string end @@ -18,6 +19,7 @@ function pathSetup(testCase) testCase.applyFixture(PathFixture(testCase.InternalPath)); testCase.applyFixture(CurrentFolderFixture(mfilename + "_files")); testCase.ExamplesFolder = fullfile(pwd, "examples"); + testCase.SourceFolder = fullfile(pwd, "source"); end end @@ -36,39 +38,42 @@ function ensurePath(testCase) %#ok methods (Test) function verifyInputsTrackFiles(testCase) - % Verify that task Inputs use file globs, not just folder paths - task = ExampleDrivenTesterTask(testCase.ExamplesFolder); + % Verify that task Inputs use file globs when SourceFiles is provided + task = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... + SourceFiles=testCase.SourceFolder); inputPaths = task.Inputs.paths; testCase.verifyGreaterThan(numel(inputPaths), 0, ... "Task Inputs should resolve to actual files inside folders"); end - function verifyOutputsAlwaysSet(testCase) - % Verify that task Outputs is always set (stamp file) regardless - % of CreateTestReport value - task = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... - CreateTestReport=false); - testCase.verifyNotEmpty(task.Outputs, ... - "Task Outputs should always be set for incremental build support"); + function verifyNoInputsOrOutputsWithoutSourceFiles(testCase) + % Verify that without SourceFiles, Inputs and Outputs are not set + % (task always runs — no incremental build) + task = ExampleDrivenTesterTask(testCase.ExamplesFolder); + testCase.verifyEmpty(task.Inputs, ... + "Task Inputs should be empty without SourceFiles"); + testCase.verifyEmpty(task.Outputs, ... + "Task Outputs should be empty without SourceFiles"); end - function verifyOutputsSetWhenReportEnabled(testCase) - % Verify that task Outputs is set when CreateTestReport is true + function verifyOutputsSetWhenSourceFilesProvided(testCase) + % Verify that Outputs is set when SourceFiles is provided task = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... - CreateTestReport=true); + SourceFiles=testCase.SourceFolder); testCase.verifyNotEmpty(task.Outputs, ... - "Task Outputs should be set when CreateTestReport is true"); + "Task Outputs should be set when SourceFiles is provided"); end function verifyStampFileCreatedAfterRun(testCase) % Verify that the stamp file is created after task execution + % when SourceFiles is provided import matlab.unittest.fixtures.TemporaryFolderFixture testCase.applyFixture(TemporaryFolderFixture); outputPath = testCase.createTemporaryFolder; plan = buildplan; plan("runExample") = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... - OutputPath=outputPath); + SourceFiles=testCase.SourceFolder, OutputPath=outputPath); run(plan, "runExample"); stampFile = fullfile(outputPath, ".last_run"); @@ -78,14 +83,14 @@ function verifyStampFileCreatedAfterRun(testCase) function verifyIncrementalBuildSkipsWhenUpToDate(testCase) % Verify that the task is skipped on the second run when - % nothing has changed + % SourceFiles is provided and nothing has changed import matlab.unittest.fixtures.TemporaryFolderFixture testCase.applyFixture(TemporaryFolderFixture); outputPath = testCase.createTemporaryFolder; plan = buildplan; plan("runExample") = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... - OutputPath=outputPath); + SourceFiles=testCase.SourceFolder, OutputPath=outputPath); % First run — should execute result1 = run(plan, "runExample"); @@ -98,9 +103,31 @@ function verifyIncrementalBuildSkipsWhenUpToDate(testCase) "Task should be skipped on second run when inputs are unchanged"); end + function verifyTaskAlwaysRunsWithoutSourceFiles(testCase) + % Verify that without SourceFiles the task runs every time + import matlab.unittest.fixtures.TemporaryFolderFixture + testCase.applyFixture(TemporaryFolderFixture); + + outputPath = testCase.createTemporaryFolder; + plan = buildplan; + plan("runExample") = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... + OutputPath=outputPath); + + % First run + result1 = run(plan, "runExample"); + testCase.assertFalse(result1.TaskResults.Skipped, ... + "Task should run on first execution"); + + % Second run — should still run (no incremental build) + result2 = run(plan, "runExample"); + testCase.verifyFalse(result2.TaskResults.Skipped, ... + "Task should always run when SourceFiles is not provided"); + end + function verifyInputsIncludeMlxFiles(testCase) % Verify that task Inputs include both .m and .mlx files - task = ExampleDrivenTesterTask(testCase.ExamplesFolder); + task = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... + SourceFiles=testCase.SourceFolder); inputPaths = task.Inputs.paths; hasMlx = any(endsWith(inputPaths, ".mlx")); hasM = any(endsWith(inputPaths, ".m")); @@ -112,12 +139,73 @@ function verifyOutputStampFileLocation(testCase) % Verify the stamp file output path is correctly set outputPath = testCase.createTemporaryFolder; task = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... - OutputPath=outputPath); + SourceFiles=testCase.SourceFolder, OutputPath=outputPath); outputPaths = task.Outputs.paths; testCase.verifyTrue(any(endsWith(outputPaths, ".last_run")), ... "Task Outputs should contain the .last_run stamp file"); end + function verifySourceFilesTrackedInInputs(testCase) + % Verify that SourceFiles are included in task Inputs + task = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... + SourceFiles=testCase.SourceFolder); + inputPaths = task.Inputs.paths; + hasSourceFile = any(contains(inputPaths, "source")); + testCase.verifyTrue(hasSourceFile, ... + "Task Inputs should include files from SourceFiles folders"); + end + + function verifySourceFilesPropertyStored(testCase) + % Verify that SourceFiles property is stored correctly + task = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... + SourceFiles=testCase.SourceFolder); + testCase.verifyEqual(task.SourceFiles, testCase.SourceFolder, ... + "SourceFiles property should store the provided value"); + end + + function verifyNoIncrementalBuildWithoutSourceFiles(testCase) + % Verify that when SourceFiles is not provided, no Inputs/Outputs + % are set (disabling incremental build) + task = ExampleDrivenTesterTask(testCase.ExamplesFolder); + testCase.verifyEmpty(task.Inputs, ... + "Task Inputs should be empty without SourceFiles"); + testCase.verifyEmpty(task.Outputs, ... + "Task Outputs should be empty without SourceFiles"); + end + + function verifySourceFileChangeTriggersRerun(testCase) + % Verify that modifying a source file triggers task re-run + import matlab.unittest.fixtures.TemporaryFolderFixture + testCase.applyFixture(TemporaryFolderFixture); + + outputPath = testCase.createTemporaryFolder; + srcFolder = testCase.createTemporaryFolder; + srcFile = fullfile(srcFolder, "helper.m"); + fid = fopen(srcFile, 'w'); + fprintf(fid, 'function out = helper(x)\n out = x;\nend\n'); + fclose(fid); + + plan = buildplan; + plan("runExample") = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... + SourceFiles=srcFolder, OutputPath=outputPath); + + % First run + result1 = run(plan, "runExample"); + testCase.assertFalse(result1.TaskResults.Skipped, ... + "Task should run on first execution"); + + % Modify source file + pause(1); + fid = fopen(srcFile, 'w'); + fprintf(fid, 'function out = helper(x)\n out = x + 1;\nend\n'); + fclose(fid); + + % Second run — should re-run due to source change + result2 = run(plan, "runExample"); + testCase.verifyFalse(result2.TaskResults.Skipped, ... + "Task should re-run when source files change"); + end + end end diff --git a/tests/tExampleDrivenTesterTask_files/source/myFunc.m b/tests/tExampleDrivenTesterTask_files/source/myFunc.m new file mode 100644 index 0000000..1343be2 --- /dev/null +++ b/tests/tExampleDrivenTesterTask_files/source/myFunc.m @@ -0,0 +1,3 @@ +function out = myFunc(x) + out = x + 1; +end diff --git a/toolbox/internal/ExampleDrivenTesterTask.m b/toolbox/internal/ExampleDrivenTesterTask.m index e2ce549..47f0f61 100644 --- a/toolbox/internal/ExampleDrivenTesterTask.m +++ b/toolbox/internal/ExampleDrivenTesterTask.m @@ -1,16 +1,18 @@ classdef ExampleDrivenTesterTask < matlab.buildtool.Task % Buildtool task to run example scripts with optional test & coverage reports. % Inputs: - % - Folders: string array of M-script locations + % - Folders: string array of M-script locations (test files) % Optional Inputs: - % - CreateTestReport (logical) - % - TestReportFormat (string) - % - ReportOutputFolder (string) - % - CodeCoveragePlugin (object) + % - SourceFiles (string) - Source code folders under test + % - CreateTestReport (logical) + % - TestReportFormat (string) + % - ReportOutputFolder (string) + % - CodeCoveragePlugin (object) % - CleanupFcn (function_handle) - Custom cleanup function executed after each test properties Folders (1,:) string + SourceFiles (1,:) string CreateTestReport (1,1) logical TestReportFormat (1,1) string OutputPath (1,1) string @@ -23,6 +25,7 @@ % Constructor arguments folders (1,:) string + options.SourceFiles (1,:) string = string.empty options.CreateTestReport (1,1) logical = true options.TestReportFormat (1,1) string {mustBeMember(options.TestReportFormat,["html", "pdf", "docx", "xml"])} = "html" options.OutputPath(1,1) string = "test-report" @@ -41,19 +44,21 @@ end task.Folders = folders; + task.SourceFiles = options.SourceFiles; task.CreateTestReport = options.CreateTestReport; task.TestReportFormat = options.TestReportFormat; task.OutputPath= options.OutputPath; task.CodeCoveragePlugin= options.CodeCoveragePlugin; task.CleanupFcn = options.CleanupFcn; - % Track actual files inside folders for incremental build - inputGlobs = [folders + "/**/*.m", folders + "/**/*.mlx"]; - task.Inputs = inputGlobs; - - % Always set output to a stamp file so buildtool can - % determine if the task is up-to-date - task.Outputs = fullfile(task.OutputPath, ".last_run"); + % Incremental build is only enabled when SourceFiles is provided. + % Without SourceFiles, the task always runs (matches TestTask behavior). + if ~isempty(options.SourceFiles) + inputGlobs = [folders + "/**/*.m", folders + "/**/*.mlx", ... + options.SourceFiles + "/**/*.m", options.SourceFiles + "/**/*.mlx"]; + task.Inputs = inputGlobs; + task.Outputs = fullfile(task.OutputPath, ".last_run"); + end end end @@ -73,10 +78,12 @@ function runExampleTests(task, ~) CleanupFcn = task.CleanupFcn); examplesRunner.executeTests; - % Write stamp file for incremental build tracking - stampFile = fullfile(task.OutputPath, ".last_run"); - fid = fopen(stampFile, 'w'); - fclose(fid); + % Write stamp file only when incremental build is enabled + if ~isempty(task.SourceFiles) + stampFile = fullfile(task.OutputPath, ".last_run"); + fid = fopen(stampFile, 'w'); + fclose(fid); + end end end end From 35f7179e895087e6597569a790626bb1cda8913e Mon Sep 17 00:00:00 2001 From: Shivam Lahoti Date: Thu, 21 May 2026 14:27:17 +0530 Subject: [PATCH 4/4] Refactor tests to share fixtures and document incremental build Remove duplicate tExampleDrivenTesterTask_files folder and reuse existing tExamplesTester_files fixtures. Add source/myFunc.m to shared fixtures for SourceFiles tests. Update README with incremental build usage example. Co-Authored-By: Claude Opus 4.6 --- README.md | 6 ++++ tests/tExampleDrivenTesterTask.m | 32 +++++------------- .../examples/a.txt | 1 - .../examples/example.m | 9 ----- .../examples/example2.m | 8 ----- .../examples/exampleFunc.m | 11 ------ .../examples/exampleFuncInputs | 9 ----- .../examples/exampleMlx.mlx | Bin 3025 -> 0 bytes .../examples/new feature/example3.m | 8 ----- .../source/myFunc.m | 0 toolbox/internal/ExampleDrivenTesterTask.m | 2 +- 11 files changed, 15 insertions(+), 71 deletions(-) delete mode 100644 tests/tExampleDrivenTesterTask_files/examples/a.txt delete mode 100644 tests/tExampleDrivenTesterTask_files/examples/example.m delete mode 100644 tests/tExampleDrivenTesterTask_files/examples/example2.m delete mode 100644 tests/tExampleDrivenTesterTask_files/examples/exampleFunc.m delete mode 100644 tests/tExampleDrivenTesterTask_files/examples/exampleFuncInputs delete mode 100644 tests/tExampleDrivenTesterTask_files/examples/exampleMlx.mlx delete mode 100644 tests/tExampleDrivenTesterTask_files/examples/new feature/example3.m rename tests/{tExampleDrivenTesterTask_files => tExamplesTester_files}/source/myFunc.m (100%) diff --git a/README.md b/README.md index 854dd2f..7821921 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,12 @@ plan("runExample") = ExampleDrivenTesterTask(["examples", "doc"], CodeCoveragePl plan("runExample") = ExampleDrivenTesterTask(["examples", "doc"], CleanupFcn = @() bdclose('all')); ``` +6. Enable incremental build support by specifying `SourceFiles`. The task will only re-run when source or example files change: +```matlab +plan("runExample") = ExampleDrivenTesterTask(["examples", "doc"], SourceFiles = "src"); +``` +Without `SourceFiles`, the task runs every time (same as the built-in `TestTask`). + ## License The license is available in the [LICENSE.txt](license.txt) file within this repository diff --git a/tests/tExampleDrivenTesterTask.m b/tests/tExampleDrivenTesterTask.m index 744922d..2138828 100644 --- a/tests/tExampleDrivenTesterTask.m +++ b/tests/tExampleDrivenTesterTask.m @@ -5,19 +5,16 @@ properties (Access=private) ExamplesFolder string SourceFolder string - ToolboxPath string - InternalPath string end methods (TestClassSetup) function pathSetup(testCase) import matlab.unittest.fixtures.PathFixture; import matlab.unittest.fixtures.CurrentFolderFixture - testCase.ToolboxPath = fullfile(fileparts(pwd), "toolbox"); - testCase.InternalPath = fullfile(testCase.ToolboxPath, "internal"); - testCase.applyFixture(PathFixture(testCase.ToolboxPath)); - testCase.applyFixture(PathFixture(testCase.InternalPath)); - testCase.applyFixture(CurrentFolderFixture(mfilename + "_files")); + testCase.applyFixture(PathFixture("../toolbox")); + testCase.applyFixture(PathFixture(fullfile("../toolbox", "internal"))); + testCase.applyFixture(CurrentFolderFixture("tExamplesTester_files")); + testCase.applyFixture(PathFixture("code")); testCase.ExamplesFolder = fullfile(pwd, "examples"); testCase.SourceFolder = fullfile(pwd, "source"); end @@ -46,14 +43,11 @@ function verifyInputsTrackFiles(testCase) "Task Inputs should resolve to actual files inside folders"); end - function verifyNoInputsOrOutputsWithoutSourceFiles(testCase) - % Verify that without SourceFiles, Inputs and Outputs are not set - % (task always runs — no incremental build) + function verifyNoIncrementalBuildWithoutSourceFiles(testCase) + % Verify that without SourceFiles, incremental build is disabled task = ExampleDrivenTesterTask(testCase.ExamplesFolder); - testCase.verifyEmpty(task.Inputs, ... - "Task Inputs should be empty without SourceFiles"); - testCase.verifyEmpty(task.Outputs, ... - "Task Outputs should be empty without SourceFiles"); + testCase.verifyEmpty(task.SourceFiles, ... + "SourceFiles should be empty when not provided"); end function verifyOutputsSetWhenSourceFilesProvided(testCase) @@ -163,16 +157,6 @@ function verifySourceFilesPropertyStored(testCase) "SourceFiles property should store the provided value"); end - function verifyNoIncrementalBuildWithoutSourceFiles(testCase) - % Verify that when SourceFiles is not provided, no Inputs/Outputs - % are set (disabling incremental build) - task = ExampleDrivenTesterTask(testCase.ExamplesFolder); - testCase.verifyEmpty(task.Inputs, ... - "Task Inputs should be empty without SourceFiles"); - testCase.verifyEmpty(task.Outputs, ... - "Task Outputs should be empty without SourceFiles"); - end - function verifySourceFileChangeTriggersRerun(testCase) % Verify that modifying a source file triggers task re-run import matlab.unittest.fixtures.TemporaryFolderFixture diff --git a/tests/tExampleDrivenTesterTask_files/examples/a.txt b/tests/tExampleDrivenTesterTask_files/examples/a.txt deleted file mode 100644 index f5be726..0000000 --- a/tests/tExampleDrivenTesterTask_files/examples/a.txt +++ /dev/null @@ -1 +0,0 @@ -% Adding the file, to verify that Examples driven tester does not error out if examples folder has differnt file types \ No newline at end of file diff --git a/tests/tExampleDrivenTesterTask_files/examples/example.m b/tests/tExampleDrivenTesterTask_files/examples/example.m deleted file mode 100644 index 6ab114d..0000000 --- a/tests/tExampleDrivenTesterTask_files/examples/example.m +++ /dev/null @@ -1,9 +0,0 @@ -% How to use addThem - -a = 10; -b = 20; -c = 30; - -% Call addThem with a and b as input - -c = addThem(a,b); \ No newline at end of file diff --git a/tests/tExampleDrivenTesterTask_files/examples/example2.m b/tests/tExampleDrivenTesterTask_files/examples/example2.m deleted file mode 100644 index 95d37b6..0000000 --- a/tests/tExampleDrivenTesterTask_files/examples/example2.m +++ /dev/null @@ -1,8 +0,0 @@ -% How to use addThem - -a = 80; -b = 20; -c= 10; -% Call addThem with a and b as input - -c = addThem(a,b); \ No newline at end of file diff --git a/tests/tExampleDrivenTesterTask_files/examples/exampleFunc.m b/tests/tExampleDrivenTesterTask_files/examples/exampleFunc.m deleted file mode 100644 index 42b194e..0000000 --- a/tests/tExampleDrivenTesterTask_files/examples/exampleFunc.m +++ /dev/null @@ -1,11 +0,0 @@ -function exampleFunc() -% How to use addThem - -a = 10; -b = 20; -c = 30; - -% Call addThem with a and b as input - -c = addThem(a,b); -end \ No newline at end of file diff --git a/tests/tExampleDrivenTesterTask_files/examples/exampleFuncInputs b/tests/tExampleDrivenTesterTask_files/examples/exampleFuncInputs deleted file mode 100644 index bac50a7..0000000 --- a/tests/tExampleDrivenTesterTask_files/examples/exampleFuncInputs +++ /dev/null @@ -1,9 +0,0 @@ -function exampleFuncInputs(a, b) -% How to use addThem - -c = 30; - -% Call addThem with a and b as input - -c = addThem(a,b); -end \ No newline at end of file diff --git a/tests/tExampleDrivenTesterTask_files/examples/exampleMlx.mlx b/tests/tExampleDrivenTesterTask_files/examples/exampleMlx.mlx deleted file mode 100644 index 01650051fb38151b74ae80195cdfabccdeca0a36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3025 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 uhW