diff --git a/README.md b/README.md index 493475e..854dd2f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Examples driven tester is a tool for users which uses MATLAB® scripts which * **TestReportFormat** - Format of test report. ***Possible values:** "pdf", "docx" , ["html”], “xml”* * **OutputPath** - Directory where reports will be generated. Default is pwd. * **CodeCoveragePlugin** - [MATLAB Code Coverage plugin](https://www.mathworks.com/help/matlab/ref/matlab.unittest.plugins.codecoverageplugin-class.html). +* **CleanupFcn** - Function handle executed after each test method for custom cleanup (e.g., closing Simulink models). Default is empty. ***Note:** Values enclosed in square braces are default values.* @@ -67,6 +68,25 @@ covPlugin = matlab.unittest.plugins.CodeCoveragePlugin.forFolder("code", "Produc obj = examplesTester(["examples", "doc"], CodeCoveragePlugin = covPlugin); obj.executeTests; ``` + +Run MATLAB scripts from specified folders and close all Simulink models after each test. + +```matlab +obj = examplesTester(["examples", "doc"], CleanupFcn = @() bdclose('all')); +obj.executeTests; +``` + +Run MATLAB scripts with a custom cleanup function that performs multiple cleanup steps. + +```matlab +obj = examplesTester(["examples", "doc"], CleanupFcn = @myCleanup); +obj.executeTests; + +function myCleanup() + bdclose('all'); % Close Simulink models + Simulink.sdi.clear; % Clear Signal Data Inspector +end +``` ## Integration with MATLAB's BuildTool From MATLAB R2025a and onwards, users can use the `ExampleDrivenTesterTask`, a ready-to-use buildtool task shipped with ExamplesDrivenTester for automated example testing. @@ -97,6 +117,11 @@ covPlugin = matlab.unittest.plugins.CodeCoveragePlugin.forFolder("code", "Produc plan("runExample") = ExampleDrivenTesterTask(["examples", "doc"], CodeCoveragePlugin = covPlugin); ``` +5. Run MATLAB scripts with a custom cleanup function (e.g., close Simulink models after each test): +```matlab +plan("runExample") = ExampleDrivenTesterTask(["examples", "doc"], CleanupFcn = @() bdclose('all')); +``` + ## License The license is available in the [LICENSE.txt](license.txt) file within this repository diff --git a/tests/tExamplesTester.m b/tests/tExamplesTester.m index 1882fd8..fae6248 100644 --- a/tests/tExamplesTester.m +++ b/tests/tExamplesTester.m @@ -198,6 +198,43 @@ function nonStringTestFolders(testCase) expectedError = "examplesTester:NonStringTestFolder"; testCase.verifyError(@()examplesTester(23), expectedError); end + + function verifyCleanupFcnIsCalled(testCase) + % Test verifies that a user-provided CleanupFcn is called after + % each test method execution + + import matlab.unittest.fixtures.TemporaryFolderFixture + testCase.applyFixture(TemporaryFolderFixture); + + cleanupCallCount = 0; + function incrementCount() + cleanupCallCount = cleanupCallCount + 1; + end + + obj = examplesTester("examples", ... + CleanupFcn=@incrementCount); + obj.OutputPath = testCase.createTemporaryFolder; + obj.executeTests(); + + testCase.verifyGreaterThan(cleanupCallCount, 0, ... + "CleanupFcn was not called during test execution"); + testCase.verifyTrue(all([obj.TestResults.Passed]), 'All tests did not pass'); + end + + function verifyCleanupFcnDefaultIsEmpty(testCase) + % Test verifies that CleanupFcn defaults to empty + obj = examplesTester("examples"); + testCase.verifyEmpty(obj.CleanupFcn, ... + "Default value of CleanupFcn should be empty"); + end + + function verifyInvalidCleanupFcn(testCase) + % Verify appropriate error is thrown when CleanupFcn is not a + % function handle + expectedError = "examplesTester:InvalidCleanupFcn"; + testCase.verifyError(@()examplesTester("examples", CleanupFcn="notAFunction"), ... + expectedError); + end end end \ No newline at end of file diff --git a/toolbox/examplesTester.m b/toolbox/examplesTester.m index 9de4815..abca2b5 100644 --- a/toolbox/examplesTester.m +++ b/toolbox/examplesTester.m @@ -18,6 +18,7 @@ % TestReportFormat - Format of test report. Possible values: "pdf", "docx" , ["html”], “xml” % OutputPath - Directory where reports will be generated. Default is "test-report" % CodeCoveragePlugin - MATLAB CodeCoverage plugin. Default value is empty +% CleanupFcn - Function handle executed after each test method. Default is empty % % Description % ------------ @@ -28,6 +29,8 @@ % % obj = examplesTester(["test", "doc"], TestReportFormat = "pdf"); creates a code coverage report in PDF format % +% obj = examplesTester(["test", "doc"], CleanupFcn = @() bdclose('all')); closes all Simulink models after each test +% % Copyright 2023 The MathWorks, Inc. properties @@ -37,6 +40,7 @@ TestFolders (1, :) {examplesTester.validateTestFolders(TestFolders)} = pwd TestResults CodeCoveragePlugin {examplesTester.validateCodeCoveragePlugin} = [] + CleanupFcn {examplesTester.validateCleanupFcn} = [] end properties (Access=private) @@ -58,12 +62,14 @@ args.TestReportFormat = "html" args.OutputPath = pwd args.CodeCoveragePlugin = [] + args.CleanupFcn = [] end obj.CreateTestReport = args.CreateTestReport; obj.TestReportFormat = args.TestReportFormat; obj.OutputPath = args.OutputPath; obj.CodeCoveragePlugin = args.CodeCoveragePlugin; + obj.CleanupFcn = args.CleanupFcn; if examplesTester.isJsonPath(TestFolders) obj.readTestFiles(TestFolders); @@ -94,6 +100,8 @@ function executeTests(obj) testFilesAndFolders = Parameter.fromData('tests', obj.testFiles); suite = TestSuite.fromPackage(obj.testPackage, 'ExternalParameters', testFilesAndFolders); + setappdata(0, 'ExamplesTester_CleanupFcn', obj.CleanupFcn); + cleanupObj = onCleanup(@() rmappdata(0, 'ExamplesTester_CleanupFcn')); obj.TestResults = obj.Runner.run(suite); end @@ -302,6 +310,14 @@ function validateCodeCoveragePlugin(codeCoveragePlugin) error("Invalid value for CodeCoveragePlugin"); end end + + function validateCleanupFcn(cleanupFcn) + % Method validates the value of CleanupFcn property + if ~isempty(cleanupFcn) && ~isa(cleanupFcn, 'function_handle') + error("examplesTester:InvalidCleanupFcn", ... + "CleanupFcn must be a function handle or empty"); + end + end end % Get set methods diff --git a/toolbox/internal/+tests/wrapperTest.m b/toolbox/internal/+tests/wrapperTest.m index 964610b..5cfc81a 100644 --- a/toolbox/internal/+tests/wrapperTest.m +++ b/toolbox/internal/+tests/wrapperTest.m @@ -18,6 +18,13 @@ function closeAllFigures(~) figHandles = findall(groot,'Type','figure'); close(figHandles, "force"); end + + function runCustomCleanup(~) + cleanupFcn = getappdata(0, 'ExamplesTester_CleanupFcn'); + if ~isempty(cleanupFcn) + cleanupFcn(); + end + end end methods (Test) diff --git a/toolbox/internal/ExampleDrivenTesterTask.m b/toolbox/internal/ExampleDrivenTesterTask.m index 95fd45c..26282bb 100644 --- a/toolbox/internal/ExampleDrivenTesterTask.m +++ b/toolbox/internal/ExampleDrivenTesterTask.m @@ -7,6 +7,7 @@ % - TestReportFormat (string) % - ReportOutputFolder (string) % - CodeCoveragePlugin (object) + % - CleanupFcn (function_handle) - Custom cleanup function executed after each test properties Folders (1,:) string @@ -14,6 +15,7 @@ TestReportFormat (1,1) string OutputPath (1,1) string CodeCoveragePlugin + CleanupFcn end methods @@ -25,6 +27,7 @@ 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.CodeCoveragePlugin = [] + options.CleanupFcn = [] end task.Description = "Run published examples"; @@ -44,6 +47,7 @@ task.TestReportFormat = options.TestReportFormat; task.OutputPath= options.OutputPath; task.CodeCoveragePlugin= options.CodeCoveragePlugin; + task.CleanupFcn = options.CleanupFcn; if task.CreateTestReport task.Outputs = task.OutputPath; @@ -65,7 +69,8 @@ function runExampleTests(task, ~) task.Folders, ... CreateTestReport = task.CreateTestReport, ... TestReportFormat = task.TestReportFormat, ... - OutputPath = task.OutputPath); + OutputPath = task.OutputPath, ... + CleanupFcn = task.CleanupFcn); else % Pass CodeCoveragePlugin through when provided examplesRunner = examplesTester( ... @@ -73,7 +78,8 @@ function runExampleTests(task, ~) CreateTestReport = task.CreateTestReport, ... TestReportFormat = task.TestReportFormat, ... OutputPath = task.OutputPath, ... - CodeCoveragePlugin = task.CodeCoveragePlugin); + CodeCoveragePlugin = task.CodeCoveragePlugin, ... + CleanupFcn = task.CleanupFcn); end examplesRunner.executeTests; end