diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f1a3c7..6a213d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,11 @@ # This is a basic workflow to help you get started with MATLAB Actions -name: CI +name: MATLAB Build # Controls when the action will run. on: - # Triggers the workflow on pull request events, but only for the main branch + # Triggers the workflow on push or pull request events, but only for the main branch + push: + branches: [ main ] pull_request: branches: [ main ] @@ -11,7 +13,7 @@ on: workflow_dispatch: env: - PRODUCT_LIST: MATLAB MATLAB_Test SimBiology Statistics_and_Machine_Learning_Toolbox + PRODUCT_LIST: MATLAB MATLAB_Test # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: @@ -22,15 +24,20 @@ permissions: # Only allow one build of this type to run at a time # Ensure results publishing completes without being interrupted/overwritten concurrency: - group: "test" + group: "test and publish results" cancel-in-progress: false jobs: + # This workflow contains a single job called "build" + build: - test: + # Set up URLs for GitHub Pages report + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} # The type of runner that the job will run on - runs-on: windows-latest + runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -50,3 +57,36 @@ jobs: uses: matlab-actions/run-build@v2 with: tasks: test + + # Configure GitHub Pages to accept your artifact uploads + - name: Setup Pages + uses: actions/configure-pages@v5 + + # Upload testing and code coverage reports to your repository + - name: Upload pages + uses: actions/upload-pages-artifact@v4 + with: + path: results # Upload results + + # Publish reports to GitHub Pages so they can be viewed in a browser + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + + + + # ==================================== # + # Alternate ways to run commands in CI # + # ==================================== # + + ## Runs your tests using `runtests` command + #- name: Run all tests + # uses: matlab-actions/run-tests@v2 + # with: + # source-folder: code + + ## Executes custom MATLAB scripts, functions, or statements + #- name: Run custom testing procedure + # uses: matlab-actions/run-command@v2 + # with: + # command: disp('Running my custom testing procedure!'); addpath('code'); results = runtests('IncludeSubfolders', true); assertSuccess(results); diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 623cdd1..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,99 +0,0 @@ -# This is a basic workflow to help you get started with MATLAB Actions -name: MATLAB Build - -# Controls when the action will run. -on: - # Triggers the workflow on push events, but only for the main branch - push: - branches: [ main ] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -env: - PRODUCT_LIST: MATLAB MATLAB_Test SimBiology Statistics_and_Machine_Learning_Toolbox - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Only allow one build of this type to run at a time -# Ensure results publishing completes without being interrupted/overwritten -concurrency: - group: "test and publish results" - cancel-in-progress: false - -jobs: - - build: - - # Set up URLs for GitHub Pages report - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - # The type of runner that the job will run on - runs-on: windows-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - # Check out your repository - - uses: actions/checkout@v5 - - # Set up MATLAB on a GitHub-hosted runner - - name: Setup MATLAB - uses: matlab-actions/setup-matlab@v2 - with: - products: ${{ env.PRODUCT_LIST }} - cache: true - - # Run the MATLAB build tool to build and test your code - - name: Run buildtool - uses: matlab-actions/run-build@v2 - with: - tasks: test - - # Configure GitHub Pages to accept your artifact uploads - - name: Setup Pages - if: always() - uses: actions/configure-pages@v5 - - # Upload testing and code coverage reports to your repository - - name: Upload pages - if: always() - uses: actions/upload-pages-artifact@v4 - with: - path: results # Upload results - - # # Upload compiled CTF file to deploy Web App - # - name: Upload CTF file - # uses: actions/upload-pages-artifact@v4 - # with: - # name: WebApp_CTF - # path: WebAppArchive/*.ctf - - # Publish reports to GitHub Pages so they can be viewed in a browser - - name: Deploy to GitHub Pages - id: deployment - if: always() - uses: actions/deploy-pages@v4 - - - # ==================================== # - # Alternate ways to run commands in CI # - # ==================================== # - - ## Runs your tests using `runtests` command - #- name: Run all tests - # uses: matlab-actions/run-tests@v2 - # with: - # source-folder: code - - ## Executes custom MATLAB scripts, functions, or statements - #- name: Run custom testing procedure - # uses: matlab-actions/run-command@v2 - # with: - # command: disp('Running my custom testing procedure!'); addpath('code'); results = runtests('IncludeSubfolders', true); assertSuccess(results); diff --git a/README.md b/README.md index b14faec..dfd68bc 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -[![MATLAB](https://github.com/ChezJe/MATLAB-SimBiology-DevOps-Workflow-Example/actions/workflows/ci.yml/badge.svg)](https://github.com/ChezJe/MATLAB-SimBiology-DevOps-Workflow-Example/actions/workflows/deploy.yml) +[![MATLAB](https://github.com/ChezJe/MATLAB-SimBiology-DevOps-Workflow-Example/actions/workflows/ci.yml/badge.svg)](https://github.com/ChezJe/MATLAB-SimBiology-DevOps-Workflow-Example/actions/workflows/ci.yml) [![Tests](https://img.shields.io/badge/Tests-Open_Test_Report-blue)](https://ChezJe.github.io/MATLAB-SimBiology-DevOps-Workflow-Example/tests/) [![Coverage](https://img.shields.io/badge/Coverage-Open_Code_Coverage_Report-orange)](https://ChezJe.github.io/MATLAB-SimBiology-DevOps-Workflow-Example/coverage/) -# MATLAB®/SimBiology® DevOps Workflow Example +# Generating Tests for Your MATLAB® Code Workshop This workshop provides hands-on experience using some of MATLAB's powerful software testing and automation features.

## About the workshop This hands-on workshop will guide you through: -* forking your own copy of the "MATLAB-SimBiology-DevOps-Workflow-Example" repository on GitHub +* forking your own copy of the "Generating Tests for Your MATLAB Code Workshop" repository on GitHub * generating tests using your command history and MATLAB Copilot * automatically finding and running existing tests * measuring and exploring code coverage metrics for your code @@ -26,4 +26,6 @@ Step-by-step workshop instructions can be found in: * [WorkshopGuide.m](WorkshopGuide.m)

+ + Copyright 2025 The MathWorks, Inc. diff --git a/buildfile.m b/buildfile.m index 523d623..ba5686a 100644 --- a/buildfile.m +++ b/buildfile.m @@ -7,6 +7,7 @@ % CodeIssues task plan("check") = CodeIssuesTask(Results=["results/codeissues.sarif"; ... "results/codeissues.mat"]); + % Test task tTask = TestTask("tests", ... SourceFiles = "code", ... @@ -33,7 +34,7 @@ plan("generateSimFun").Inputs = fullfile(proj.RootFolder,"code","*.sbproj"); plan("generateSimFun").Outputs = fullfile(proj.RootFolder,"code","*.mat"); plan("test").Inputs = fullfile(proj.RootFolder,"code","*"); -plan("compile").Inputs = fullfile(proj.RootFolder,"code",["*.mat","*.mlapp","*.m"]); +plan("compile").Inputs = fullfile(proj.RootFolder,"code",["*.mat","*.mlapp","graystyle.m"]); plan("compile").Outputs = fullfile(proj.RootFolder,"WebAppArchive"); % Set default task @@ -61,9 +62,9 @@ function compileTask(~) MATfilename = dir(fullfile(rootFolder,"code","*.mat")); MATfilename = fullfile(rootFolder,"code",MATfilename.name); - s = load(MATfilename,"dependenciesSimFun"); + load(MATfilename,"dependenciesSimFun"); - appDependencies = [MATfilename; s.dependenciesSimFun; ... + appDependencies = [MATfilename; dependenciesSimFun; ... codeFiles; imgFiles]; appfilename = fullfile(rootFolder,"code","TMDDApp.mlapp"); diff --git a/code/ConcTimecourseView.m b/code/ConcTimecourseView.m index 2cbe5d4..a341078 100644 --- a/code/ConcTimecourseView.m +++ b/code/ConcTimecourseView.m @@ -2,6 +2,7 @@ properties ( Access = private ) Model + Axes ConcColors = [0.30,0.75,0.93;... 0.86,0.55,0.41;... @@ -9,11 +10,7 @@ FontName = "Helvetica"; end - properties ( Hidden ) - % Leave these properties Hidden but public to enable access for any test generated - % with Copilot during workshop - Axes - + properties ( SetAccess=private, GetAccess={?tTMDDApp} ) % line handles lhDrug lhReceptor @@ -38,16 +35,13 @@ xlabel(ax, "Time (hours)", 'FontName',obj.FontName); ylabel(ax, "Concentrations (nanomole/liter)",'FontName',obj.FontName); - obj.lhDrug = plot(ax, NaN, NaN, '-','Linewidth',2,'Color',obj.ConcColors(1,:),'DisplayName','Drug'); + obj.lhDrug = plot(ax, NaN, NaN, '-','Linewidth',2,'Color',obj.ConcColors(1,:)); hold(ax,'on'); - obj.lhReceptor = plot(ax, NaN, NaN, '-','Linewidth',2,'Color',obj.ConcColors(2,:),'DisplayName','Receptor'); - obj.lhComplex= plot(ax, NaN, NaN, '-','Linewidth',2,'Color',obj.ConcColors(3,:),'DisplayName','Complex'); + obj.lhReceptor = plot(ax, NaN, NaN, '-','Linewidth',2,'Color',obj.ConcColors(2,:)); + obj.lhComplex= plot(ax, NaN, NaN, '-','Linewidth',2,'Color',obj.ConcColors(3,:)); hold(ax,'off'); - lgd = legend(ax,'show','FontName',obj.FontName,'Color','none'); - lgd.Box = "off"; - - ax.XLimitMethod = "padded"; - ax.YLimitMethod = "padded"; + lh = legend(ax,{'Drug','Receptor','Complex'},'FontName',obj.FontName); + lh.Box = 'off'; % instantiate listener dataListener = event.listener( model, 'DataChanged', ... diff --git a/code/LampView.m b/code/LampView.m index 736d3b4..c457ec1 100644 --- a/code/LampView.m +++ b/code/LampView.m @@ -9,9 +9,7 @@ LampColorFailure = [0.85, 0.33, 0.10] % color of lamp if RO not between thresholds after day 1 end - properties ( Hidden ) - % Leave these properties Hidden but public to enable access for any test generated - % with Copilot during workshop + properties ( SetAccess=private, GetAccess={?tTMDDApp} ) LampObj end diff --git a/code/NCAView.m b/code/NCAView.m index c4f3d47..9f05d46 100644 --- a/code/NCAView.m +++ b/code/NCAView.m @@ -7,9 +7,7 @@ end - properties ( Hidden ) - % Leave these properties Hidden but public to enable access for any test generated - % with Copilot during workshop + properties ( SetAccess=private, GetAccess={?tTMDDApp} ) NCAtable end diff --git a/code/ROTimecourseView.m b/code/ROTimecourseView.m index e8048c7..84d9e50 100644 --- a/code/ROTimecourseView.m +++ b/code/ROTimecourseView.m @@ -8,10 +8,7 @@ 'FontWeight','bold','LabelVerticalAlignment','middle'}; % style for threshold lines end - properties ( Hidden, SetAccess=private) - % Leave these properties Hidden but public to enable access for any test generated - % with Copilot during workshop - + properties ( GetAccess = {?tTMDDApp} ) % line handles lhRO end @@ -42,12 +39,12 @@ ylabel(ax, "RO (%)",'FontName',obj.FontName); obj.lhRO = plot(ax, NaN, NaN, 'Color', obj.ROColors,'Linewidth',2); - % yline(ax,model.ThresholdValues(1), '--','efficacy','FontName',obj.FontName,obj.ThresholdStyle{:}); - % yline(ax,model.ThresholdValues(2), '--','safety','FontName',obj.FontName,obj.ThresholdStyle{:}); + yline(ax,model.ThresholdValues(1), '--','efficacy','FontName',obj.FontName,obj.ThresholdStyle{:}); + yline(ax,model.ThresholdValues(2), '--','safety','FontName',obj.FontName,obj.ThresholdStyle{:}); % set limits - ax.XLimitMethod = "padded"; + xlim(ax,'auto'); ylim(ax,[-5, 105]); % instantiate listener diff --git a/code/SimulationModel.m b/code/SimulationModel.m index 498c127..d4b4811 100644 --- a/code/SimulationModel.m +++ b/code/SimulationModel.m @@ -1,6 +1,17 @@ classdef SimulationModel < handle % Class to simulate the TMDD model + properties ( SetAccess = private ) + DoseTable % daily dose to apply to simulate + SimFun % exported SimFunction + + SimData + SimDataTable + + ThresholdValues = [20, 80] % threshold values + + end + properties % original values for resetting Amount0 (1,1) double @@ -9,28 +20,10 @@ Kel0 (1,1) double Kdeg0 (1,1) double Interval0 (1,1) double - end - - properties ( Dependent ) ROIsBetweenThresholds (1,1) logical end - properties ( Hidden ) - % Leave these properties Hidden but public to enable access for any test generated - % with Copilot during workshop - - DoseTable % daily dose to apply to simulate - SimFun % exported SimFunction - - SimDataTable - SimData - - ThresholdValues = [20, 80] % threshold values - end - - events ( NotifyAccess = public ) - % Leave this notification public to enable access for any test generated - % with Copilot during workshop + events ( NotifyAccess = private ) DataChanged end @@ -80,6 +73,10 @@ function simulate(obj, parameters) idxNotIncreasing = diff(t.Time)<=0; % remove duplicates t(idxNotIncreasing,:) = []; + % logical value to check whether or not RO remains between thresholds after day 1 + aboveThreshold1 = all(t.RO(t.Time >= 24) >= obj.ThresholdValues(1)/100); + belowThreshold2 = all(t.RO(t.Time >= 24) <= obj.ThresholdValues(2)/100); + obj.ROIsBetweenThresholds = aboveThreshold1 && belowThreshold2; obj.SimData = sd; obj.SimDataTable = t; @@ -88,13 +85,6 @@ function simulate(obj, parameters) end % simulate - function value = get.ROIsBetweenThresholds(obj) - % logical value to check whether or not RO remains between thresholds after day 1 - timeAfter24h = obj.SimDataTable.Time >= 24; - ROAfter24h = obj.SimDataTable.RO(timeAfter24h); - value = all(ROAfter24h >= obj.ThresholdValues(1)/100) && ... - all(ROAfter24h <= obj.ThresholdValues(2)/100); - end % get.ROIsBetweenThresholds() end % public methods diff --git a/code/TMDD.sbproj b/code/TMDD.sbproj index 3480cba..be0de33 100644 Binary files a/code/TMDD.sbproj and b/code/TMDD.sbproj differ diff --git a/code/TMDDApp.mlapp b/code/TMDDApp.mlapp index 6c987c4..1a5add8 100644 Binary files a/code/TMDDApp.mlapp and b/code/TMDDApp.mlapp differ diff --git a/code/simFunction_Dose.mat b/code/simFunction_Dose.mat new file mode 100644 index 0000000..c9b533b Binary files /dev/null and b/code/simFunction_Dose.mat differ diff --git a/resources/project/Project.xml b/resources/project/Project.xml index addadd3..e7171c4 100644 --- a/resources/project/Project.xml +++ b/resources/project/Project.xml @@ -152,67 +152,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -227,19 +166,8 @@ - - - - - - - - - - - diff --git a/tests/tTMDDApp.m b/tests/tTMDDApp.m index b0794e1..1957107 100644 --- a/tests/tTMDDApp.m +++ b/tests/tTMDDApp.m @@ -30,45 +30,44 @@ function testStartup(testCase) end % testStartup - % function testChangeDosingAmountAutomaticUpdate(testCase) - % - % % Deactivate automatic plot update - % testCase.App.AutomaticupdateCheckBox.Value = false; - % - % % Simulate for drug=100 - % testCase.App.DosingAmountField.Value = 100; - % testCase.App.updateApp(); - % - % oldlhRO_XData = testCase.App.ROViewObj.lhRO.XData; - % oldlhRO_YData = testCase.App.ROViewObj.lhRO.YData; - % oldlhDrug_XData = testCase.App.ConcViewObj.lhDrug.XData; - % oldlhDrug_YData = testCase.App.ConcViewObj.lhDrug.YData; - % oldlhReceptor_XData = testCase.App.ConcViewObj.lhReceptor.XData; - % oldlhReceptor_YData = testCase.App.ConcViewObj.lhReceptor.YData; - % oldlhComplex_XData = testCase.App.ConcViewObj.lhComplex.XData; - % oldlhComplex_YData = testCase.App.ConcViewObj.lhComplex.YData; - % - % % Activate automatic plot update - % testCase.App.AutomaticupdateCheckBox.Value = true; - % - % % Drag slider - % % testCase.drag(testCase.App.DosingAmountSlider,100,200); % requires display (does not work on github) - % testCase.App.DosingAmountField.Value = 200; % BUT this does not trigger ValueChangedFcn callback ... - % - % % Check plot update - % testCase.verifyNotEqual(oldlhRO_XData, testCase.App.ROViewObj.lhRO.XData, "x values for RO not updated"); - % testCase.verifyNotEqual(oldlhRO_YData, testCase.App.ROViewObj.lhRO.YData, "y values for RO not updated"); - % testCase.verifyNotEqual(oldlhDrug_XData, testCase.App.ConcViewObj.lhDrug.XData, "x values for Drug not updated"); - % testCase.verifyNotEqual(oldlhDrug_YData, testCase.App.ConcViewObj.lhDrug.YData, "y values for Drug not updated"); - % testCase.verifyNotEqual(oldlhReceptor_XData, testCase.App.ConcViewObj.lhReceptor.XData, "x values for Receptor not updated"); - % testCase.verifyNotEqual(oldlhReceptor_YData, testCase.App.ConcViewObj.lhReceptor.YData, "y values for Receptor not updated"); - % testCase.verifyNotEqual(oldlhComplex_XData, testCase.App.ConcViewObj.lhComplex.XData, "x values for Complex not updated"); - % testCase.verifyNotEqual(oldlhComplex_YData, testCase.App.ConcViewObj.lhComplex.YData, "y values for Complex not updated"); - % - % % Check that lamp is set to false - % testCase.verifyFalse(testCase.App.LampViewObj.IsOn); - % - % end % testChangeDosingAmountAutomaticUpdate + function testChangeDosingAmountAutomaticUpdate(testCase) + + % Deactivate automatic plot update + testCase.App.AutomaticupdateCheckBox.Value = false; + + % Simulate for drug=100 + testCase.App.DosingAmountField.Value = 100; + testCase.App.updateApp(); + + oldlhRO_XData = testCase.App.ROViewObj.lhRO.XData; + oldlhRO_YData = testCase.App.ROViewObj.lhRO.YData; + oldlhDrug_XData = testCase.App.ConcViewObj.lhDrug.XData; + oldlhDrug_YData = testCase.App.ConcViewObj.lhDrug.YData; + oldlhReceptor_XData = testCase.App.ConcViewObj.lhReceptor.XData; + oldlhReceptor_YData = testCase.App.ConcViewObj.lhReceptor.YData; + oldlhComplex_XData = testCase.App.ConcViewObj.lhComplex.XData; + oldlhComplex_YData = testCase.App.ConcViewObj.lhComplex.YData; + + % Activate automatic plot update + testCase.App.AutomaticupdateCheckBox.Value = true; + + % Drag slider + testCase.drag(testCase.App.DosingAmountSlider,100,200); + + % Check plot update + testCase.verifyNotEqual(oldlhRO_XData, testCase.App.ROViewObj.lhRO.XData, "x values for RO not updated"); + testCase.verifyNotEqual(oldlhRO_YData, testCase.App.ROViewObj.lhRO.YData, "y values for RO not updated"); + testCase.verifyNotEqual(oldlhDrug_XData, testCase.App.ConcViewObj.lhDrug.XData, "x values for Drug not updated"); + testCase.verifyNotEqual(oldlhDrug_YData, testCase.App.ConcViewObj.lhDrug.YData, "y values for Drug not updated"); + testCase.verifyNotEqual(oldlhReceptor_XData, testCase.App.ConcViewObj.lhReceptor.XData, "x values for Receptor not updated"); + testCase.verifyNotEqual(oldlhReceptor_YData, testCase.App.ConcViewObj.lhReceptor.YData, "y values for Receptor not updated"); + testCase.verifyNotEqual(oldlhComplex_XData, testCase.App.ConcViewObj.lhComplex.XData, "x values for Complex not updated"); + testCase.verifyNotEqual(oldlhComplex_YData, testCase.App.ConcViewObj.lhComplex.YData, "y values for Complex not updated"); + + % Check that lamp is set to false + testCase.verifyFalse(testCase.App.LampViewObj.IsOn); + + end % testChangeDosingAmountAutomaticUpdate function testChangeDosingAmountManualUpdate(testCase) @@ -85,22 +84,18 @@ function testChangeDosingAmountManualUpdate(testCase) oldlhComplex_YData = testCase.App.ConcViewObj.lhComplex.YData; % Drag slider - if batchStartupOptionUsed() - testCase.App.DosingAmountField.Value = 200; - else - testCase.drag(testCase.App.DosingAmountSlider,100,200); % requires display (does not work on github) - end + testCase.drag(testCase.App.DosingAmountSlider,100,200); % Check plot update - testCase.verifyEqual(oldlhRO_XData, testCase.App.ROViewObj.lhRO.XData, "x values for RO were updated"); - testCase.verifyEqual(oldlhRO_YData, testCase.App.ROViewObj.lhRO.YData, "y values for RO were updated"); - testCase.verifyEqual(oldlhDrug_XData, testCase.App.ConcViewObj.lhDrug.XData, "x values for Drug were updated"); - testCase.verifyEqual(oldlhDrug_YData, testCase.App.ConcViewObj.lhDrug.YData, "y values for Drug were updated"); - testCase.verifyEqual(oldlhReceptor_XData, testCase.App.ConcViewObj.lhReceptor.XData, "x values for Receptor were updated"); - testCase.verifyEqual(oldlhReceptor_YData, testCase.App.ConcViewObj.lhReceptor.YData, "y values for Receptor were updated"); - testCase.verifyEqual(oldlhComplex_XData, testCase.App.ConcViewObj.lhComplex.XData, "x values for Complex were updated"); - testCase.verifyEqual(oldlhComplex_YData, testCase.App.ConcViewObj.lhComplex.YData, "y values for Complex were updated"); - + testCase.verifyEqual(oldlhRO_XData, testCase.App.ROViewObj.lhRO.XData, "x values for RO not updated"); + testCase.verifyEqual(oldlhRO_YData, testCase.App.ROViewObj.lhRO.YData, "y values for RO not updated"); + testCase.verifyEqual(oldlhDrug_XData, testCase.App.ConcViewObj.lhDrug.XData, "x values for Drug not updated"); + testCase.verifyEqual(oldlhDrug_YData, testCase.App.ConcViewObj.lhDrug.YData, "y values for Drug not updated"); + testCase.verifyEqual(oldlhReceptor_XData, testCase.App.ConcViewObj.lhReceptor.XData, "x values for Receptor not updated"); + testCase.verifyEqual(oldlhReceptor_YData, testCase.App.ConcViewObj.lhReceptor.YData, "y values for Receptor not updated"); + testCase.verifyEqual(oldlhComplex_XData, testCase.App.ConcViewObj.lhComplex.XData, "x values for Complex not updated"); + testCase.verifyEqual(oldlhComplex_YData, testCase.App.ConcViewObj.lhComplex.YData, "y values for Complex not updated"); + % Check that lamp is set to false testCase.verifyTrue(testCase.App.LampViewObj.IsOn);