-
Notifications
You must be signed in to change notification settings - Fork 0
Add incremental build support to ExampleDrivenTesterTask #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
slahoti1
wants to merge
4
commits into
main
Choose a base branch
from
feature/incremental-build-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
926a2cd
Add incremental build support to ExampleDrivenTesterTask
5d964fc
Fix incremental build by using static default OutputPath
slahoti1 b8ebe68
Add SourceFiles property to gate incremental build support
slahoti1 35f7179
Refactor tests to share fixtures and document incremental build
slahoti1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| classdef tExampleDrivenTesterTask < matlab.unittest.TestCase | ||
| % Test verifies ExampleDrivenTesterTask buildtool integration | ||
| % including incremental build support. | ||
|
|
||
| properties (Access=private) | ||
| ExamplesFolder string | ||
| SourceFolder string | ||
| end | ||
|
|
||
| methods (TestClassSetup) | ||
| function pathSetup(testCase) | ||
| import matlab.unittest.fixtures.PathFixture; | ||
| import matlab.unittest.fixtures.CurrentFolderFixture | ||
| 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 | ||
| end | ||
|
|
||
| methods (TestMethodSetup) | ||
| function ensurePath(testCase) %#ok<MANU> | ||
| % 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 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 verifyNoIncrementalBuildWithoutSourceFiles(testCase) | ||
| % Verify that without SourceFiles, incremental build is disabled | ||
| task = ExampleDrivenTesterTask(testCase.ExamplesFolder); | ||
| testCase.verifyEmpty(task.SourceFiles, ... | ||
| "SourceFiles should be empty when not provided"); | ||
| end | ||
|
|
||
| function verifyOutputsSetWhenSourceFilesProvided(testCase) | ||
| % Verify that Outputs is set when SourceFiles is provided | ||
| task = ExampleDrivenTesterTask(testCase.ExamplesFolder, ... | ||
| SourceFiles=testCase.SourceFolder); | ||
| testCase.verifyNotEmpty(task.Outputs, ... | ||
| "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, ... | ||
| SourceFiles=testCase.SourceFolder, 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 | ||
| % 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, ... | ||
| SourceFiles=testCase.SourceFolder, 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 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, ... | ||
| SourceFiles=testCase.SourceFolder); | ||
| 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, ... | ||
| 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 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| function out = myFunc(x) | ||
| out = x + 1; | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; enables incremental build when provided | ||
| % - 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,18 +25,17 @@ | |
| % 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 = "reports_" + char(datetime('now', 'Format', 'yyyyMMdd_HHmmss')) | ||
| options.OutputPath(1,1) string = "test-report" | ||
| options.CodeCoveragePlugin = [] | ||
| options.CleanupFcn = [] | ||
| end | ||
|
|
||
| task.Description = "Run published examples"; | ||
| task.Inputs = folders; | ||
|
|
||
| % Basic validation | ||
| % mustBeMember(options.TestReportFormat, ["html", "pdf", "docx", "xml"]); | ||
| for f = folders | ||
| if ~isfolder(f) | ||
| error("ExampleDrivenTesterTask:FolderNotFound", ... | ||
|
|
@@ -43,24 +44,28 @@ | |
| 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; | ||
|
|
||
| if task.CreateTestReport | ||
| task.Outputs = task.OutputPath; | ||
| else | ||
| task.Outputs = string.empty; | ||
| % 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"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we introducing this? |
||
| end | ||
| end | ||
| end | ||
|
|
||
| methods (TaskAction, Sealed, Hidden) | ||
|
|
||
| function runExampleTests(task, ~) | ||
| if task.CreateTestReport && ~isfolder(task.OutputPath) | ||
| if ~isfolder(task.OutputPath) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so does this reports folder will always be created even if the CreateTestReport is passed as false? |
||
| mkdir(task.OutputPath); | ||
| end | ||
|
|
||
|
|
@@ -72,6 +77,13 @@ function runExampleTests(task, ~) | |
| CodeCoveragePlugin = task.CodeCoveragePlugin, ... | ||
| CleanupFcn = task.CleanupFcn); | ||
| examplesRunner.executeTests; | ||
|
|
||
| % 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 | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will overwrite developers older test reports without giving warning which could be risky, Why is this change required?