├── .gitignore ├── README.md ├── SECURITY.md ├── build-test-publish.pipeline.yml ├── images └── extension-icon.png ├── integ-test-promote-template.yml ├── integ-test-promote.pipeline.yml ├── license.txt ├── overview.md ├── package-lock.json ├── package.json ├── pre-integ-test.pipeline.yml ├── scripts ├── clean.js ├── compile.js ├── copyResources.js ├── preparePackage.js ├── project.js ├── setupdeps.sh ├── test.js └── util.js ├── tasks ├── install-matlab │ ├── v0 │ │ ├── icon.png │ │ ├── main.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── task.json │ │ ├── test │ │ │ ├── downloadAndExecuteLinux.ts │ │ │ ├── downloadAndExecutePrivate.ts │ │ │ ├── downloadAndExecuteWindows.ts │ │ │ ├── failDownload.ts │ │ │ ├── failExecute.ts │ │ │ ├── failSelfHosted.ts │ │ │ └── suite.ts │ │ ├── tsconfig.json │ │ └── utils.ts │ └── v1 │ │ ├── icon.png │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── src │ │ ├── install.ts │ │ ├── main.ts │ │ ├── matlab.ts │ │ ├── mpm.ts │ │ ├── script.ts │ │ └── utils.ts │ │ ├── task.json │ │ ├── test │ │ ├── install.test.ts │ │ ├── matlab.test.ts │ │ ├── mpm.test.ts │ │ ├── script.test.ts │ │ └── suite.ts │ │ └── tsconfig.json ├── run-matlab-build │ ├── v0 │ │ ├── .gitignore │ │ ├── icon.png │ │ ├── main.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── task.json │ │ ├── test │ │ │ ├── failRunBuild.ts │ │ │ ├── runBuildLinux.ts │ │ │ ├── runBuildWindows.ts │ │ │ ├── runBuildWithStartupOptsLinux.ts │ │ │ ├── runBuildWithStartupOptsWindows.ts │ │ │ ├── runDefaultTasks.ts │ │ │ └── suite.ts │ │ ├── tsconfig.json │ │ └── utils.ts │ └── v1 │ │ ├── .gitignore │ │ ├── buildtool.ts │ │ ├── icon.png │ │ ├── main.ts │ │ ├── matlab.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── task.json │ │ ├── test │ │ ├── buildtool.test.ts │ │ └── suite.ts │ │ └── tsconfig.json ├── run-matlab-command │ ├── v0 │ │ ├── .gitignore │ │ ├── icon.png │ │ ├── main.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── task.json │ │ ├── test │ │ │ ├── failRunCommand.ts │ │ │ ├── runCommandLinux.ts │ │ │ ├── runCommandWindows.ts │ │ │ ├── runCommandWithArgsLinux.ts │ │ │ ├── runCommandWithArgsWindows.ts │ │ │ └── suite.ts │ │ ├── tsconfig.json │ │ └── utils.ts │ └── v1 │ │ ├── .gitignore │ │ ├── icon.png │ │ ├── main.ts │ │ ├── matlab.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── task.json │ │ ├── test │ │ ├── matlab.test.ts │ │ └── suite.ts │ │ └── tsconfig.json └── run-matlab-tests │ ├── v0 │ ├── .gitignore │ ├── icon.png │ ├── main.ts │ ├── package-lock.json │ ├── package.json │ ├── task.json │ ├── test │ │ ├── common.ts │ │ ├── failRunTests.ts │ │ ├── runTestsLinux.ts │ │ ├── runTestsWindows.ts │ │ ├── runTestsWithStartupOptsLinux.ts │ │ ├── runTestsWithStartupOptsWindows.ts │ │ └── suite.ts │ ├── tsconfig.json │ └── utils.ts │ └── v1 │ ├── .gitignore │ ├── icon.png │ ├── main.ts │ ├── matlab.ts │ ├── package-lock.json │ ├── package.json │ ├── scriptgen.ts │ ├── task.json │ ├── test │ ├── scriptgen.test.ts │ └── suite.ts │ └── tsconfig.json ├── tsconfig.json ├── tslint.json └── vss-extension.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/_build/ 3 | **/_package/ 4 | 5 | **/*.map 6 | **/*.taskkey 7 | **/.DS_Store 8 | 9 | **/*.mltbx 10 | 11 | .idea 12 | .nyc_output 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Continuous Integration with MATLAB on Azure DevOps 2 | 3 | This extension provides tasks to build and test MATLAB projects in Azure DevOps environments. 4 | 5 | ## Usage 6 | 7 | Learn how to install and use the MATLAB extension on [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=MathWorks.matlab-azure-devops-extension). 8 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Vulnerabilities 2 | 3 | If you believe you have discovered a security vulnerability, please report it to 4 | [security@mathworks.com](mailto:security@mathworks.com). Please see 5 | [MathWorks Vulnerability Disclosure Policy for Security Researchers](https://www.mathworks.com/company/aboutus/policies_statements/vulnerability-disclosure-policy.html) 6 | for additional information. -------------------------------------------------------------------------------- /build-test-publish.pipeline.yml: -------------------------------------------------------------------------------- 1 | name: 1.12$(Rev:.r) 2 | 3 | trigger: 4 | - master 5 | 6 | variables: 7 | packageFolder: _package 8 | vsixFolder: '$(Build.ArtifactStagingDirectory)/vsix' 9 | vsixFile: '$(vsixFolder)/matlab-$(Build.BuildNumber).vsix' 10 | 11 | pool: 12 | vmImage: ubuntu-latest 13 | 14 | steps: 15 | - task: NodeTool@0 16 | displayName: Install node.js 17 | inputs: 18 | versionSpec: 18.x 19 | 20 | - script: npm install 21 | displayName: Install dependencies 22 | 23 | - script: npm run build 24 | displayName: Build 25 | 26 | - script: npm run test 27 | displayName: Test 28 | 29 | - script: npm run preparePackage 30 | displayName: Prepare package 31 | 32 | - task: TfxInstaller@2 33 | displayName: Install tfx-cli 34 | inputs: 35 | version: v0.16.x 36 | 37 | - task: PackageAzureDevOpsExtension@2 38 | displayName: Package 39 | inputs: 40 | rootFolder: '$(packageFolder)' 41 | outputPath: '$(vsixFile)' 42 | extensionVersion: '$(Build.BuildNumber)' 43 | updateTasksVersion: true 44 | updateTasksVersionType: minor 45 | 46 | - publish: '$(vsixFile)' 47 | displayName: Store extension 48 | artifact: extension 49 | 50 | - task: PublishAzureDevOpsExtension@2 51 | displayName: Publish dev 52 | inputs: 53 | connectTo: VsTeam 54 | connectedServiceName: MathWorks 55 | fileType: vsix 56 | vsixFile: '$(vsixFile)' 57 | extensionTag: -dev 58 | extensionVisibility: private 59 | updateTasksVersion: false 60 | shareWith: iat-ci-dev 61 | -------------------------------------------------------------------------------- /images/extension-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/matlab-azure-devops-extension/20cd6026fc706cc925710c25cd2d7f8528441a3e/images/extension-icon.png -------------------------------------------------------------------------------- /integ-test-promote-template.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - ${{ each version in parameters.TASK_VERSION }}: 3 | - job: test_install_v${{ version }} 4 | strategy: 5 | matrix: 6 | microsoft_hosted_linux: 7 | poolName: Azure Pipelines 8 | vmImage: ubuntu-22.04 9 | microsoft_hosted_macos: 10 | poolName: Azure Pipelines 11 | vmImage: macOS-latest 12 | microsoft_hosted_windows: 13 | poolName: Azure Pipelines 14 | vmImage: windows-latest 15 | pool: 16 | name: $(poolName) 17 | vmImage: $(vmImage) 18 | steps: 19 | - checkout: none 20 | - task: MathWorks.matlab-azure-devops-extension-dev.InstallMATLAB.InstallMATLAB@${{ version }} 21 | displayName: Install MATLAB 22 | - bash: | 23 | set -e 24 | os=$(uname) 25 | if [[ $os = CYGWIN* || $os = MINGW* || $os = MSYS* ]]; then 26 | mex.bat -h 27 | else 28 | mex -h 29 | fi 30 | displayName: Check mex 31 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 32 | displayName: Check matlab installation 33 | inputs: 34 | command: version 35 | 36 | 37 | - job: test_install_release_v${{ version }} 38 | strategy: 39 | matrix: 40 | microsoft_hosted_linux: 41 | poolName: Azure Pipelines 42 | vmImage: ubuntu-22.04 43 | microsoft_hosted_macos: 44 | poolName: Azure Pipelines 45 | vmImage: macOS-latest 46 | microsoft_hosted_windows: 47 | poolName: Azure Pipelines 48 | vmImage: windows-latest 49 | pool: 50 | name: $(poolName) 51 | vmImage: $(vmImage) 52 | steps: 53 | - checkout: none 54 | - task: MathWorks.matlab-azure-devops-extension-dev.InstallMATLAB.InstallMATLAB@${{ version }} 55 | displayName: Install MATLAB 56 | inputs: 57 | release: R2023a 58 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 59 | displayName: Check matlab release 60 | inputs: 61 | command: assert(strcmp(version('-release'),'2023a')) 62 | 63 | - job: test_install_latest_including_prerelease_v${{ version }} 64 | condition: not(eq(${{ version }}, '0')) 65 | strategy: 66 | matrix: 67 | microsoft_hosted_linux: 68 | poolName: Azure Pipelines 69 | vmImage: ubuntu-22.04 70 | pool: 71 | name: $(poolName) 72 | vmImage: $(vmImage) 73 | steps: 74 | - checkout: none 75 | - task: MathWorks.matlab-azure-devops-extension-dev.InstallMATLAB.InstallMATLAB@${{ version }} 76 | displayName: Install MATLAB 77 | inputs: 78 | release: latest-including-prerelease 79 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 80 | displayName: Print MATLAB version 81 | inputs: 82 | command: version 83 | 84 | - job: test_run_command_v${{ version }} 85 | strategy: 86 | matrix: 87 | microsoft_hosted_linux: 88 | poolName: Azure Pipelines 89 | vmImage: ubuntu-22.04 90 | microsoft_hosted_macos: 91 | poolName: Azure Pipelines 92 | vmImage: macOS-latest 93 | microsoft_hosted_windows: 94 | poolName: Azure Pipelines 95 | vmImage: windows-latest 96 | pool: 97 | name: $(poolName) 98 | vmImage: $(vmImage) 99 | steps: 100 | - checkout: none 101 | - task: MathWorks.matlab-azure-devops-extension-dev.InstallMATLAB.InstallMATLAB@${{ version }} 102 | displayName: Install MATLAB on Microsoft-hosted agents 103 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 104 | displayName: Run MATLAB statement 105 | inputs: 106 | command: f = fopen('myscript.m', 'w'); fwrite(f, 'assert(true)'); fclose(f); 107 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 108 | displayName: Run MATLAB script 109 | inputs: 110 | command: myscript 111 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 112 | displayName: Run MATLAB statement with quotes 1 113 | inputs: 114 | command: 'eval("a = 1+2"); assert(a == 3); eval(''b = 3+4''); assert(b == 7);' 115 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 116 | displayName: Run MATLAB statement with quotes 2 117 | inputs: 118 | command: "eval(\"a = 1+2\"); assert(a == 3); eval('b = 3+4'); assert(b == 7);" 119 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 120 | displayName: Run MATLAB statement with quotes 3 121 | inputs: 122 | command: a = """hello world"""; b = '"hello world"'; assert(strcmp(a,b)); 123 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 124 | displayName: Run MATLAB statement with symbols 125 | inputs: 126 | command: a = " !""#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; b = char([32:126]); assert(strcmp(a, b), a+b); 127 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 128 | displayName: Run MATLAB statement in working directory 129 | inputs: 130 | command: exp = getenv('SYSTEM_DEFAULTWORKINGDIRECTORY'); act = pwd; assert(strcmp(act, exp), strjoin({act exp}, '\n')); 131 | - bash: | 132 | echo 'myvar = 123' > startup.m 133 | displayName: Create startup.m 134 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 135 | displayName: MATLAB runs startup.m automatically 136 | inputs: 137 | command: assert(myvar==123, 'myvar was not set as expected by startup.m') 138 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 139 | displayName: Run MATLAB statement with startup options 140 | inputs: 141 | command: disp("Hello world!") 142 | startupOptions: -logfile mylog.log 143 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 144 | displayName: Run MATLAB statement with startup options 145 | inputs: 146 | command: assert(isfile("mylog.log"), 'mylog.log was not created as expected') 147 | - bash: | 148 | mkdir subdir 149 | echo 'onetyone = 11' > subdir/startup.m 150 | displayName: Create subdir/startup.m 151 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 152 | displayName: Run MATLAB statement with subdir/startup 153 | inputs: 154 | command: > 155 | assert(onetyone==11, 'onetyone was not set as expected by subdir/startup.m'); 156 | [~, folder] = fileparts(pwd()); 157 | assert(strcmp(folder, 'subdir')); 158 | startupOptions: -sd subdir 159 | 160 | - job: test_run_tests_v${{ version }} 161 | strategy: 162 | matrix: 163 | microsoft_hosted_linux: 164 | poolName: Azure Pipelines 165 | vmImage: ubuntu-22.04 166 | pool: 167 | name: $(poolName) 168 | vmImage: $(vmImage) 169 | workspace: 170 | clean: all 171 | steps: 172 | - checkout: none 173 | - task: MathWorks.matlab-azure-devops-extension-dev.InstallMATLAB.InstallMATLAB@${{ version }} 174 | displayName: Install MATLAB on Microsoft-hosted agents 175 | condition: eq(${{ version }}, '0') 176 | - task: MathWorks.matlab-azure-devops-extension-dev.InstallMATLAB.InstallMATLAB@${{ version }} 177 | inputs: 178 | products: Simulink Simulink_Test Simulink_Coverage Parallel_Computing_Toolbox 179 | displayName: Install MATLAB on Microsoft-hosted agents 180 | condition: eq(${{ version }}, '1') 181 | - bash: | 182 | echo 'myvar = 123' > startup.m 183 | mkdir src 184 | echo 'function c=add(a,b);c=a+b;' > src/add.m 185 | mkdir tests 186 | echo "%% StartupTest" > tests/mytest.m 187 | echo "evalin('base','assert(myvar==123)')" >> tests/mytest.m 188 | echo "%% FirstTest" >> tests/mytest.m 189 | echo "assert(add(1,2)==3)" >> tests/mytest.m 190 | mkdir tests/filteredTest 191 | echo "%% simpleTest" >> tests/filteredTest/filtertest.m 192 | echo "assert(2==2)" >> tests/filteredTest/filtertest.m 193 | echo "%% FilterByTag" >> tests/filteredTest/TaggedTest.m 194 | cat << EOF > tests/filteredTest/TaggedTest.m 195 | classdef (TestTags = {'FILTERED'}) TaggedTest < matlab.unittest.TestCase 196 | methods(Test) 197 | function testTag(testCase) 198 | assert(2==2); 199 | end 200 | end 201 | end 202 | EOF 203 | displayName: Make MATLAB tests 204 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABTests.RunMATLABTests@${{ version }} 205 | displayName: Run MATLAB tests with defaults 206 | inputs: 207 | sourceFolder: src 208 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABTests.RunMATLABTests@${{ version }} 209 | displayName: Run MATLAB tests producing artifacts 210 | inputs: 211 | testResultsJUnit: test-results/matlab/results.xml 212 | codeCoverageCobertura: code-coverage/coverage.xml 213 | sourceFolder: src 214 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABTests.RunMATLABTests@${{ version }} 215 | displayName: Run MATLAB tests filter by folder 216 | inputs: 217 | testResultsJUnit: test-results/matlab/selectbyfolder.xml 218 | selectByFolder: tests/filteredTest 219 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABTests.RunMATLABTests@${{ version }} 220 | displayName: Run MATLAB tests filter by folder 221 | inputs: 222 | testResultsJUnit: test-results/matlab/selectbytag.xml 223 | selectByTag: FILTERED 224 | - bash: | 225 | set -e 226 | grep -q FirstTest test-results/matlab/results.xml 227 | displayName: Verify test results file was created 228 | - bash: | 229 | set -e 230 | grep -q add code-coverage/coverage.xml 231 | displayName: Verify code coverage file was created 232 | - bash: | 233 | set -e 234 | grep -q simpleTest test-results/matlab/selectbyfolder.xml 235 | grep -v FirstTest test-results/matlab/selectbyfolder.xml 236 | displayName: Verify test filtered by folder 237 | - bash: | 238 | set -e 239 | grep -q TaggedTest test-results/matlab/selectbytag.xml 240 | grep -v FirstTest test-results/matlab/selectbytag.xml 241 | grep -v simpleTest test-results/matlab/selectbytag.xml 242 | displayName: Verify test filtered by tag name 243 | - bash: | 244 | echo 'diary console.log' >> startup.m 245 | displayName: Set up diary for logging 246 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABTests.RunMATLABTests@${{ version }} 247 | displayName: Run MATLAB tests with strict checks 248 | inputs: 249 | strict: true 250 | sourceFolder: src 251 | - bash: | 252 | set -e 253 | grep -q "runner.addPlugin(FailOnWarningsPlugin())" console.log 254 | rm console.log 255 | displayName: Verify tests ran with strict checks 256 | # Disable parallel tests until PCT bug is fixed g3416906 257 | # - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABTests.RunMATLABTests@${{ version }} 258 | # displayName: Run MATLAB tests in parallel 259 | # inputs: 260 | # useParallel: true 261 | # sourceFolder: src 262 | # condition: not(eq(${{ version }}, '0')) 263 | # - bash: | 264 | # set -e 265 | # grep -q "Starting parallel pool (parpool)" console.log 266 | # rm console.log 267 | # displayName: Verify tests ran in parallel 268 | # condition: not(eq(${{ version }}, '0')) 269 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABTests.RunMATLABTests@${{ version }} 270 | displayName: Run MATLAB tests with detailed display level for event details 271 | inputs: 272 | outputDetail: Detailed 273 | sourceFolder: src 274 | - bash: | 275 | set -e 276 | grep -q "TestRunner.withTextOutput('OutputDetail', 3)" console.log 277 | rm console.log 278 | displayName: Verify tests ran with detailed display level for event details 279 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABTests.RunMATLABTests@${{ version }} 280 | displayName: Run MATLAB tests with detailed verbosity level for logged diagnostics 281 | inputs: 282 | loggingLevel: Detailed 283 | sourceFolder: src 284 | - bash: | 285 | set -e 286 | grep -q "TestRunner.withTextOutput('LoggingLevel', 3)" console.log 287 | rm console.log 288 | displayName: Verify tests ran with detailed verbosity level for logged diagnostics 289 | - bash: | 290 | mkdir simtests 291 | cat << EOF > simtests/createModel.m 292 | % Create model 293 | model = 'new_temp_model'; 294 | new_system(model); 295 | load_system(model); 296 | 297 | % add blocks 298 | add_block('built-in/Inport', [model, '/InputPort'], ... 299 | 'Position', [50 50 100 100]); 300 | add_block('built-in/Gain', [model, '/Gain'], 'Gain', '1', ... 301 | 'Position', [150 50 200 100]); 302 | add_block('built-in/Outport', [model, '/OutputPort'], ... 303 | 'Position', [250 50 300 100]); 304 | 305 | % add lines 306 | add_line(model, 'InputPort/1', 'Gain/1'); 307 | add_line(model, 'Gain/1', 'OutputPort/1'); 308 | 309 | % save system 310 | save_system(model); 311 | close_system(model); 312 | 313 | % Create Simulink Test File 314 | sltest.testmanager.clear; 315 | sltest.testmanager.clearResults; 316 | tf = sltest.testmanager.TestFile('test.mldatx'); 317 | 318 | % Set coverage related options. 319 | cs = tf.getCoverageSettings; 320 | cs.RecordCoverage = true; 321 | cs.MdlRefCoverage = true; 322 | cs.MetricSettings = 'd'; 323 | 324 | % Set model and collect baseline. 325 | ts = tf.getTestSuites; 326 | tc = ts.getTestCases; 327 | tc.setProperty('model', model); 328 | captureBaselineCriteria(tc,'baseline_API.mat',true); 329 | 330 | % Save test file and close. 331 | tf.saveToFile; 332 | tf.close; 333 | sltest.testmanager.close; 334 | disp('Created Model and Simulink Test file to simulate the model.'); 335 | evalin('base', 'clear all'); 336 | EOF 337 | displayName: Create model file and Simulink test. 338 | condition: eq(variables['Agent.OS'], 'Linux') 339 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABCommand.RunMATLABCommand@${{ version }} 340 | displayName: Run model creator and test Simulink test generator 341 | inputs: 342 | command: "cd simtests;createModel" 343 | condition: eq(variables['Agent.OS'], 'Linux') 344 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABTests.RunMATLABTests@${{ version }} 345 | displayName: Run MATLAB tests and generate Simulink test artifacts 346 | inputs: 347 | selectByFolder: "simtests" 348 | modelCoverageCobertura: test-results/matlab/modelcoverage.xml 349 | testResultsSimulinkTest: test-results/matlab/stmResult.mldatx 350 | testResultsPDF: test-results/matlab/results.pdf 351 | condition: eq(variables['Agent.OS'], 'Linux') 352 | - bash: | 353 | set -e 354 | grep -q new_temp_model test-results/matlab/modelcoverage.xml 355 | displayName: Verify Model coverage was created 356 | condition: eq(variables['Agent.OS'], 'Linux') 357 | - bash: | 358 | set -e 359 | test -f test-results/matlab/stmResult.mldatx 360 | displayName: Verify STM report was created 361 | condition: eq(variables['Agent.OS'], 'Linux') 362 | - bash: | 363 | set -e 364 | test -f test-results/matlab/results.pdf 365 | displayName: Verify PDF report was created 366 | condition: eq(variables['Agent.OS'], 'Linux') 367 | 368 | - job: test_run_build_v${{ version }} 369 | strategy: 370 | matrix: 371 | microsoft_hosted_linux: 372 | poolName: Azure Pipelines 373 | vmImage: ubuntu-22.04 374 | setupTask: MathWorks.matlab-azure-devops-extension-dev.InstallMATLAB.InstallMATLAB@${{ version }} 375 | microsoft_hosted_macos: 376 | poolName: Azure Pipelines 377 | vmImage: macOS-latest 378 | setupTask: MathWorks.matlab-azure-devops-extension-dev.InstallMATLAB.InstallMATLAB@${{ version }} 379 | microsoft_hosted_windows: 380 | poolName: Azure Pipelines 381 | vmImage: windows-latest 382 | setupTask: MathWorks.matlab-azure-devops-extension-dev.InstallMATLAB.InstallMATLAB@${{ version }} 383 | pool: 384 | name: $(poolName) 385 | vmImage: $(vmImage) 386 | steps: 387 | - checkout: none 388 | - task: MathWorks.matlab-azure-devops-extension-dev.InstallMATLAB.InstallMATLAB@${{ version }} 389 | displayName: Install MATLAB on Microsoft-hosted agents 390 | condition: not(startsWith(variables['Agent.Name'], 'vmss')) 391 | - bash: | 392 | cat << EOF > buildfile.m 393 | function plan = buildfile 394 | plan = buildplan(localfunctions); 395 | plan("test").Dependencies = "build"; 396 | plan("deploy").Dependencies = "test"; 397 | plan.DefaultTasks = "test"; 398 | 399 | function buildTask(~) 400 | f = fopen('buildlog.txt', 'a+'); fprintf(f, 'building\n'); fclose(f); 401 | 402 | function testTask(~) 403 | f = fopen('buildlog.txt', 'a+'); fprintf(f, 'testing\n'); fclose(f); 404 | 405 | function deployTask(~) 406 | f = fopen('buildlog.txt', 'a+'); fprintf(f, 'deploying\n'); fclose(f); 407 | 408 | function checkTask(~) 409 | f = fopen('buildlog.txt', 'a+'); fprintf(f, 'checking\n'); fclose(f); 410 | 411 | function errorTask(~) 412 | f = fopen('buildlog.txt', 'a+'); fprintf(f, 'erroring\n'); fclose(f); 413 | error('Error occured in errorTask'); 414 | EOF 415 | displayName: Create buildfile.m in project root 416 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABBuild.RunMATLABBuild@${{ version }} 417 | displayName: Run build with default tasks 418 | - bash: | 419 | set -e 420 | grep "building" buildlog.txt 421 | grep "testing" buildlog.txt 422 | ! grep "deploying" buildlog.txt 423 | ! grep "checking" buildlog.txt 424 | rm buildlog.txt 425 | displayName: Verify that correct tasks appear in buildlog.txt 426 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABBuild.RunMATLABBuild@${{ version }} 427 | displayName: Run build with specified task 428 | inputs: 429 | tasks: deploy 430 | - bash: | 431 | set -e 432 | grep "building" buildlog.txt 433 | grep "testing" buildlog.txt 434 | grep "deploying" buildlog.txt 435 | ! grep "checking" buildlog.txt 436 | rm buildlog.txt 437 | displayName: Verify that correct tasks appear in buildlog.txt 438 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABBuild.RunMATLABBuild@${{ version }} 439 | displayName: Run build with multiple specified tasks 440 | inputs: 441 | tasks: deploy check 442 | - bash: | 443 | set -e 444 | grep "building" buildlog.txt 445 | grep "testing" buildlog.txt 446 | grep "deploying" buildlog.txt 447 | grep "checking" buildlog.txt 448 | rm buildlog.txt 449 | displayName: Verify that correct tasks appear in buildlog.txt 450 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABBuild.RunMATLABBuild@${{ version }} 451 | displayName: Run build with task skipping 452 | inputs: 453 | tasks: deploy 454 | buildOptions: -skip test 455 | condition: not(eq(${{ version }}, '0')) 456 | - bash: | 457 | set -e 458 | grep "building" buildlog.txt 459 | ! grep "testing" buildlog.txt 460 | grep "deploying" buildlog.txt 461 | rm buildlog.txt 462 | displayName: Verify that correct tasks appear in buildlog.txt 463 | condition: not(eq(${{ version }}, '0')) 464 | - task: MathWorks.matlab-azure-devops-extension-dev.RunMATLABBuild.RunMATLABBuild@${{ version }} 465 | displayName: Run build with continue on failure 466 | continueOnError: true 467 | inputs: 468 | tasks: error deploy 469 | buildOptions: -continueOnFailure 470 | condition: not(eq(${{ version }}, '0')) 471 | - bash: | 472 | set -e 473 | grep "erroring" buildlog.txt 474 | grep "building" buildlog.txt 475 | grep "testing" buildlog.txt 476 | grep "deploying" buildlog.txt 477 | rm buildlog.txt 478 | displayName: Verify that correct tasks appear in buildlog.txt 479 | condition: not(eq(${{ version }}, '0')) 480 | -------------------------------------------------------------------------------- /integ-test-promote.pipeline.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | pipelines: 3 | - pipeline: integ_test_promote 4 | source: pre-integ-test 5 | trigger: true 6 | 7 | trigger: none 8 | pr: none 9 | 10 | stages: 11 | - stage: integration_test 12 | displayName: Integration test 13 | jobs: 14 | - template: integ-test-promote-template.yml 15 | parameters: 16 | TASK_VERSION: ['0', '1'] 17 | 18 | - stage: publish_prerelease 19 | displayName: Publish prerelease 20 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) 21 | pool: 22 | vmImage: ubuntu-latest 23 | jobs: 24 | - deployment: publish_prerelease 25 | environment: prerelease 26 | strategy: 27 | runOnce: 28 | deploy: 29 | steps: 30 | - checkout: none 31 | 32 | - task: DownloadPipelineArtifact@2 33 | displayName: Get stored extension 34 | inputs: 35 | source: specific 36 | project: b3d35465-d584-454b-a4e9-e60757510c12 37 | pipeline: 6 38 | runVersion: latest 39 | artifact: extension 40 | path: $(Build.StagingDirectory) 41 | 42 | - task: TfxInstaller@2 43 | displayName: Install tfx-cli 44 | inputs: 45 | version: v0.7.x 46 | 47 | - task: PublishAzureDevOpsExtension@2 48 | displayName: Publish prerelease 49 | inputs: 50 | connectTo: VsTeam 51 | connectedServiceName: MathWorks 52 | fileType: vsix 53 | extensionTag: -prerelease 54 | extensionVisibility: private 55 | vsixFile: $(Build.StagingDirectory)/*.vsix 56 | updateTasksVersion: false 57 | 58 | - stage: publish_release 59 | displayName: Publish release 60 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) 61 | pool: 62 | vmImage: ubuntu-latest 63 | jobs: 64 | - deployment: publish_release 65 | environment: release 66 | strategy: 67 | runOnce: 68 | deploy: 69 | steps: 70 | - checkout: none 71 | 72 | - task: DownloadPipelineArtifact@2 73 | displayName: Get stored extension 74 | inputs: 75 | source: specific 76 | project: b3d35465-d584-454b-a4e9-e60757510c12 77 | pipeline: 6 78 | runVersion: latest 79 | artifact: extension 80 | path: $(Build.StagingDirectory) 81 | 82 | - task: TfxInstaller@2 83 | displayName: Install tfx-cli 84 | inputs: 85 | version: v0.7.x 86 | 87 | - task: PublishAzureDevOpsExtension@2 88 | displayName: Publish release 89 | inputs: 90 | connectTo: VsTeam 91 | connectedServiceName: MathWorks 92 | fileType: vsix 93 | vsixFile: $(Build.StagingDirectory)/*.vsix 94 | updateTasksVersion: false 95 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, The MathWorks, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. In all cases, the software is, and all modifications and derivatives of the 15 | software shall be, licensed to you solely for use in conjunction with 16 | MathWorks products and service offerings. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matlab-azure-devops-extension", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "bat": "npm run clean && npm run build && npm run test", 6 | "clean": "node ./scripts/clean.js", 7 | "lint": "tslint --project .", 8 | "compile": "node ./scripts/compile.js", 9 | "copyResources": "node ./scripts/copyResources.js", 10 | "build": "npm run lint && npm run compile && npm run copyResources", 11 | "test": "node ./scripts/test.js", 12 | "preparePackage": "node ./scripts/preparePackage.js" 13 | }, 14 | "devDependencies": { 15 | "@types/mocha": "^8.2.2", 16 | "@types/node": "^22.7.5", 17 | "@types/shelljs": "^0.8.15", 18 | "@types/sinon": "^10.0.13", 19 | "@types/uuid": "^9.0.8", 20 | "mocha": "^10.0.0", 21 | "nyc": "^15.1.0", 22 | "shelljs": "^0.8.5", 23 | "sinon": "^15.0.1", 24 | "sync-request": "^6.1.0", 25 | "tfx-cli": "^0.16.0", 26 | "tslint": "^5.20.1", 27 | "typescript": "^4.2.4" 28 | }, 29 | "nyc": { 30 | "exclude": "**/test/*" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pre-integ-test.pipeline.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | pipelines: 3 | - pipeline: pre_integ_test 4 | source: build-test-publish 5 | trigger: true 6 | 7 | trigger: none 8 | pr: none 9 | 10 | pool: 11 | vmImage: ubuntu-latest 12 | 13 | steps: 14 | - script: sleep 60 15 | displayName: Wait for published extension to propagate 16 | -------------------------------------------------------------------------------- /scripts/clean.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | "use strict"; 4 | 5 | const project = require("./project"); 6 | const sh = require("shelljs"); 7 | 8 | sh.config.fatal = true; 9 | 10 | sh.rm("-rf", project.buildPath); 11 | sh.rm("-rf", project.packagePath); 12 | -------------------------------------------------------------------------------- /scripts/compile.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The MathWorks, Inc. 2 | 3 | "use strict"; 4 | 5 | const project = require("./project"); 6 | const sh = require("shelljs"); 7 | 8 | sh.config.fatal = true; 9 | 10 | for (let task of project.taskList) { 11 | sh.echo(`> compiling ${task.fullName}`); 12 | sh.exec("npm install", { cwd: task.sourcePath }, "--omit=dev"); 13 | sh.exec("tsc --outDir " + task.buildPath + " --project " + task.sourcePath); 14 | } 15 | -------------------------------------------------------------------------------- /scripts/copyResources.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | "use strict"; 4 | 5 | const project = require("./project"); 6 | const sh = require("shelljs"); 7 | const path = require("path"); 8 | const util = require("./util"); 9 | 10 | sh.config.fatal = true; 11 | 12 | const ignored = [".ts", "tsconfig.json"]; 13 | 14 | for (let task of project.taskList) { 15 | sh.echo(`> copying resources for ${task.fullName}`); 16 | 17 | util.cpdir(task.sourcePath, task.buildPath, { 18 | filter: (file) => 19 | !file.includes("node_modules") && 20 | ignored.every(e => !file.endsWith(e)) 21 | }); 22 | 23 | sh.cp("-r", path.join(task.sourcePath, "node_modules"), path.join(task.buildPath, "node_modules")); 24 | } 25 | -------------------------------------------------------------------------------- /scripts/preparePackage.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | "use strict"; 4 | 5 | const project = require("./project"); 6 | const sh = require("shelljs"); 7 | const path = require("path"); 8 | const util = require("./util"); 9 | 10 | sh.config.fatal = true; 11 | 12 | sh.mkdir("-p", project.packagePath); 13 | 14 | sh.cp(path.join(project.rootPath, "vss-extension.json"), project.packagePath); 15 | sh.cp(path.join(project.rootPath, "license.txt"), project.packagePath); 16 | sh.cp(path.join(project.rootPath, "overview.md"), project.packagePath); 17 | sh.cp("-r", path.join(project.rootPath, "images"), project.packagePath); 18 | 19 | const ignored = [".taskkey", ".map"]; 20 | 21 | for (let task of project.taskList) { 22 | sh.mkdir("-p", task.packagePath); 23 | 24 | util.cpdir(task.buildPath, task.packagePath, { 25 | filter: (file) => 26 | !file.startsWith("node_modules") && 27 | !file.startsWith("test") && 28 | ignored.every(e => !file.endsWith(e)) 29 | }); 30 | 31 | sh.cp("-R", path.join(task.buildPath, "node_modules"), path.join(task.packagePath, "node_modules")); 32 | } 33 | -------------------------------------------------------------------------------- /scripts/project.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | "use strict"; 4 | 5 | const path = require("path"); 6 | const sh = require("shelljs"); 7 | 8 | const rootPath = path.dirname(__dirname); 9 | const buildPath = path.join(rootPath, "_build"); 10 | const packagePath = path.join(rootPath, "_package"); 11 | 12 | const tasksDir = "tasks"; 13 | const taskList = []; 14 | for (let task of sh.ls(path.join(rootPath, tasksDir))) { 15 | for (let ver of sh.ls(path.join(rootPath, tasksDir, task))) { 16 | taskList.push({ 17 | name: task, 18 | version: ver, 19 | fullName: task + " " + ver, 20 | sourcePath: path.join(rootPath, tasksDir, task, ver), 21 | buildPath: path.join(buildPath, tasksDir, task, ver), 22 | packagePath: path.join(packagePath, tasksDir, task, ver) 23 | }); 24 | } 25 | } 26 | 27 | module.exports = { 28 | rootPath: rootPath, 29 | buildPath: buildPath, 30 | packagePath: packagePath, 31 | taskList: taskList 32 | }; 33 | -------------------------------------------------------------------------------- /scripts/setupdeps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=${1:-v2} 4 | RMC_BASE_URL="https://ssd.mathworks.com/supportfiles/ci/run-matlab-command/$VERSION" 5 | SUPPORTED_OS=('win64' 'maci64' 'maca64' 'glnxa64') 6 | 7 | # Create dist directory if it doesn't already exist 8 | DISTDIR="$(pwd)/bin" 9 | rm -rf "$DISTDIR/" 10 | mkdir -p $DISTDIR 11 | 12 | # Download and extract in a temporary directory 13 | WORKINGDIR=$(mktemp -d -t rmc_build.XXXXXX) 14 | cd $WORKINGDIR 15 | 16 | wget -O "$WORKINGDIR/license.txt" "$RMC_BASE_URL/license.txt" 17 | wget -O "$WORKINGDIR/thirdpartylicenses.txt" "$RMC_BASE_URL/thirdpartylicenses.txt" 18 | 19 | for os in ${SUPPORTED_OS[@]} 20 | do 21 | if [[ $os == 'win64' ]] ; then 22 | bin_ext='.exe' 23 | else 24 | bin_ext='' 25 | fi 26 | mkdir -p "$WORKINGDIR/$os" 27 | wget -O "$WORKINGDIR/$os/run-matlab-command$bin_ext" "$RMC_BASE_URL/$os/run-matlab-command$bin_ext" 28 | zip -j "$WORKINGDIR/$os/run-matlab-command.zip" "$WORKINGDIR/$os/run-matlab-command$bin_ext" 29 | rm "$WORKINGDIR/$os/run-matlab-command$bin_ext" 30 | done 31 | 32 | mv -f ./* "$DISTDIR/" 33 | rm -rf $WORKINGDIR 34 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | "use strict"; 4 | 5 | const project = require("./project"); 6 | const sh = require("shelljs"); 7 | const path = require("path"); 8 | 9 | sh.config.fatal = true; 10 | 11 | for (let task of project.taskList) { 12 | sh.echo(`> testing ${task.fullName}`); 13 | sh.exec("nyc mocha " + path.join(task.buildPath, "test", "suite.js") + " --timeout 10000"); 14 | } 15 | -------------------------------------------------------------------------------- /scripts/util.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | "use strict"; 4 | 5 | const sh = require("shelljs"); 6 | const path = require("path"); 7 | 8 | function cpdir(src, dest, options) { 9 | if (!options) { 10 | options = {}; 11 | } 12 | 13 | let files = sh.find(src).map((f) => path.relative(src, f)); 14 | if (options.filter) { 15 | files = files.filter(options.filter); 16 | } 17 | 18 | for (let f of files) { 19 | const s = path.join(src, f); 20 | const d = path.join(dest, f); 21 | if (sh.test("-d", s)) { 22 | sh.mkdir("-p", d); 23 | } else { 24 | sh.cp(s, d); 25 | } 26 | } 27 | } 28 | exports.cpdir = cpdir; 29 | -------------------------------------------------------------------------------- /tasks/install-matlab/v0/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/matlab-azure-devops-extension/20cd6026fc706cc925710c25cd2d7f8528441a3e/tasks/install-matlab/v0/icon.png -------------------------------------------------------------------------------- /tasks/install-matlab/v0/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2022 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 5 | import * as fs from "fs"; 6 | import * as os from "os"; 7 | import * as path from "path"; 8 | import {platform} from "./utils"; 9 | 10 | async function run() { 11 | try { 12 | taskLib.setResourcePath(path.join( __dirname, "task.json")); 13 | const release = taskLib.getInput("release"); 14 | await install(release); 15 | } catch (err) { 16 | taskLib.setResult(taskLib.TaskResult.Failed, (err as Error).message); 17 | } 18 | } 19 | 20 | async function install(release?: string) { 21 | const serverType = taskLib.getVariable("System.ServerType"); 22 | if (!serverType || serverType.toLowerCase() !== "hosted") { 23 | throw new Error(taskLib.loc("InstallNotSupportedOnSelfHosted")); 24 | } 25 | 26 | let exitCode = 0; 27 | 28 | // install core system dependencies on Linux 29 | if (platform() === "linux") { 30 | const depArgs: string[] = []; 31 | if (release !== undefined) { 32 | depArgs.push(release); 33 | } 34 | exitCode = await curlsh("https://ssd.mathworks.com/supportfiles/ci/matlab-deps/v0/install.sh", depArgs); 35 | if (exitCode !== 0) { 36 | throw new Error(taskLib.loc("FailedToExecuteInstallScript", exitCode)); 37 | } 38 | } 39 | 40 | // install matlab-batch 41 | const batchInstallDir = installRoot("matlab-batch"); 42 | exitCode = await curlsh("https://ssd.mathworks.com/supportfiles/ci/matlab-batch/v0/install.sh", batchInstallDir); 43 | if (exitCode !== 0) { 44 | throw new Error(taskLib.loc("FailedToExecuteInstallScript", exitCode)); 45 | } 46 | try { 47 | toolLib.prependPath(batchInstallDir); 48 | } catch (err: any) { 49 | throw new Error(taskLib.loc("FailedToAddToPath", err.message)); 50 | } 51 | 52 | // install ephemeral version of MATLAB 53 | const installArgs: string[] = []; 54 | if (release !== undefined) { 55 | installArgs.push("--release", release); 56 | } 57 | const skipActivation = skipActivationFlag(process.env); 58 | if (skipActivation) { 59 | installArgs.push(skipActivation); 60 | } 61 | 62 | exitCode = await curlsh("https://ssd.mathworks.com/supportfiles/ci/ephemeral-matlab/v0/ci-install.sh", installArgs); 63 | if (exitCode !== 0) { 64 | throw new Error(taskLib.loc("FailedToExecuteInstallScript", exitCode)); 65 | } 66 | 67 | // prepend MATLAB to path 68 | let root: string; 69 | try { 70 | root = fs.readFileSync(path.join(os.tmpdir(), "ephemeral_matlab_root")).toString(); 71 | toolLib.prependPath(path.join(root, "bin")); 72 | } catch (err: any) { 73 | throw new Error(taskLib.loc("FailedToAddToPath", err.message)); 74 | } 75 | } 76 | 77 | function skipActivationFlag(env: NodeJS.ProcessEnv): string { 78 | return (env.MATHWORKS_TOKEN !== undefined && env.MATHWORKS_ACCOUNT !== undefined) ? "--skip-activation" : ""; 79 | } 80 | 81 | function installRoot(programName: string) { 82 | let installDir: string; 83 | if (platform() === "win32") { 84 | installDir = path.join("C:", "Program Files", programName); 85 | } else { 86 | installDir = path.join("/", "opt", programName); 87 | } 88 | return installDir; 89 | } 90 | 91 | // similar to "curl | sh" 92 | async function curlsh(url: string, args: string | string[]) { 93 | // download script 94 | const scriptPath = await toolLib.downloadToolWithRetries(url); 95 | 96 | // execute script 97 | const bashPath = taskLib.which("bash", true); 98 | let bash; 99 | if (platform() === "win32") { 100 | bash = taskLib.tool(bashPath); 101 | } else { 102 | bash = taskLib.tool("sudo").arg("-E").line(bashPath); 103 | } 104 | bash.arg(scriptPath); 105 | bash.arg(args); 106 | return bash.exec(); 107 | } 108 | 109 | run(); 110 | -------------------------------------------------------------------------------- /tasks/install-matlab/v0/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "install-matlab", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/node": "^22.7.5", 7 | "@types/q": "^1.5.4", 8 | "azure-pipelines-task-lib": "5.0.1-preview.0", 9 | "azure-pipelines-tool-lib": "^2.0.8" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tasks/install-matlab/v0/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json", 3 | "id": "553fa7ff-af12-4821-8ace-6bf3dc410e62", 4 | "name": "InstallMATLAB", 5 | "friendlyName": "Install MATLAB", 6 | "description": "Install MATLAB on a Linux-based Microsoft-hosted agent. Currently, this task is available only for public projects and does not include transformation products, such as MATLAB Coder and MATLAB Compiler.", 7 | "helpMarkDown": "", 8 | "category": "Tool", 9 | "author": "The MathWorks, Inc.", 10 | "version": { 11 | "Major": 0, 12 | "Minor": 1, 13 | "Patch": 0 14 | }, 15 | "inputs": [ 16 | { 17 | "name": "release", 18 | "type": "string", 19 | "label": "Release", 20 | "required": false, 21 | "defaultValue": "latest", 22 | "helpMarkDown": "MATLAB release to install. You can specify R2020a or a later release. If you do not specify `release`, the task installs the latest release of MATLAB." 23 | } 24 | ], 25 | "instanceNameFormat": "Install MATLAB", 26 | "execution": { 27 | "Node10": { 28 | "target": "main.js" 29 | } 30 | }, 31 | "messages": { 32 | "InstallNotSupportedOnSelfHosted": "Install MATLAB task is not supported on self-hosted agents. To install MATLAB on this machine, use the standard installer.", 33 | "FailedToDownloadInstallScript": "Failed to download install script: %s", 34 | "FailedToExecuteInstallScript": "Failed to execute install script. Exited with code '%s'.", 35 | "FailedToAddToPath": "Failed to add MATLAB to system path: %s" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tasks/install-matlab/v0/test/downloadAndExecuteLinux.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import * as fs from "fs"; 6 | import * as os from "os"; 7 | import path = require("path"); 8 | 9 | const tp = path.join(__dirname, "..", "main.js"); 10 | const tr = new mr.TaskMockRunner(tp); 11 | 12 | tr.setInput("release", "R2020a"); 13 | 14 | delete process.env.MATHWORKS_ACCOUNT; 15 | delete process.env.MATHWORKS_TOKEN; 16 | 17 | const matlabRoot = "path/to/matlab"; 18 | fs.writeFileSync(path.join(os.tmpdir(), "ephemeral_matlab_root"), matlabRoot); 19 | const batchInstallRoot = path.join("/", "opt", "matlab-batch"); 20 | 21 | // create assertAgent and getVariable mocks, support not added in this version of task-lib 22 | import tl = require("azure-pipelines-task-lib/mock-task"); 23 | const tlClone = Object.assign({}, tl); 24 | // @ts-ignore 25 | tlClone.getVariable = (variable: string) => { 26 | if (variable.toLocaleLowerCase() === "system.servertype") { 27 | return "hosted"; 28 | } 29 | return null; 30 | }; 31 | // @ts-ignore 32 | tlClone.assertAgent = (variable: string) => { 33 | return; 34 | }; 35 | tr.registerMock("azure-pipelines-task-lib/mock-task", tlClone); 36 | 37 | tr.registerMock("azure-pipelines-tool-lib/tool", { 38 | downloadToolWithRetries(url: string) { 39 | if (url === "https://ssd.mathworks.com/supportfiles/ci/matlab-deps/v0/install.sh") { 40 | return "install.sh"; 41 | } else if (url === "https://ssd.mathworks.com/supportfiles/ci/ephemeral-matlab/v0/ci-install.sh") { 42 | return "ci-install.sh"; 43 | } else if (url === "https://ssd.mathworks.com/supportfiles/ci/matlab-batch/v0/install.sh") { 44 | return "install.sh"; 45 | } else { 46 | throw new Error("Incorrect URL"); 47 | } 48 | }, 49 | prependPath(toolPath: string) { 50 | if ( toolPath !== path.join(matlabRoot, "bin") && toolPath !== batchInstallRoot) { 51 | throw new Error(`Unexpected path: ${toolPath}`); 52 | } 53 | }, 54 | }); 55 | 56 | tr.registerMock("./utils", { 57 | platform: () => "linux", 58 | }); 59 | 60 | const a: ma.TaskLibAnswers = { 61 | which: { 62 | bash: "/bin/bash", 63 | }, 64 | checkPath: { 65 | "/bin/bash": true, 66 | }, 67 | exec: { 68 | "sudo -E /bin/bash install.sh R2020a": { 69 | code: 0, 70 | stdout: "Installed MATLAB dependencies", 71 | }, 72 | "sudo -E /bin/bash ci-install.sh --release R2020a": { 73 | code: 0, 74 | stdout: "Installed MATLAB", 75 | }, 76 | "sudo -E /bin/bash ci-install.sh --release R2020a --skip-activation": { 77 | code: 0, 78 | stdout: "Installed MATLAB", 79 | }, 80 | "sudo -E /bin/bash install.sh /opt/matlab-batch": { 81 | code: 0, 82 | stdout: "Installed matlab-batch", 83 | }, 84 | }, 85 | } as ma.TaskLibAnswers; 86 | tr.setAnswers(a); 87 | 88 | tr.run(); 89 | -------------------------------------------------------------------------------- /tasks/install-matlab/v0/test/downloadAndExecutePrivate.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2022 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import * as fs from "fs"; 6 | import * as os from "os"; 7 | import path = require("path"); 8 | 9 | const tp = path.join(__dirname, "..", "main.js"); 10 | const tr = new mr.TaskMockRunner(tp); 11 | 12 | tr.setInput("release", "R2020a"); 13 | 14 | process.env.MATHWORKS_ACCOUNT = "euclid@mathworks.com"; 15 | process.env.MATHWORKS_TOKEN = "token123456"; 16 | 17 | const matlabRoot = "path/to/matlab"; 18 | fs.writeFileSync(path.join(os.tmpdir(), "ephemeral_matlab_root"), matlabRoot); 19 | const batchInstallRoot = path.join("/", "opt", "matlab-batch"); 20 | 21 | // create assertAgent and getVariable mocks, support not added in this version of task-lib 22 | import tl = require("azure-pipelines-task-lib/mock-task"); 23 | const tlClone = Object.assign({}, tl); 24 | // @ts-ignore 25 | tlClone.getVariable = (variable: string) => { 26 | if (variable.toLocaleLowerCase() === "system.servertype") { 27 | return "hosted"; 28 | } 29 | return null; 30 | }; 31 | // @ts-ignore 32 | tlClone.assertAgent = (variable: string) => { 33 | return; 34 | }; 35 | tr.registerMock("azure-pipelines-task-lib/mock-task", tlClone); 36 | 37 | tr.registerMock("azure-pipelines-tool-lib/tool", { 38 | downloadToolWithRetries(url: string) { 39 | if (url === "https://ssd.mathworks.com/supportfiles/ci/matlab-deps/v0/install.sh") { 40 | return "install.sh"; 41 | } else if (url === "https://ssd.mathworks.com/supportfiles/ci/ephemeral-matlab/v0/ci-install.sh") { 42 | return "ci-install.sh"; 43 | } else if (url === "https://ssd.mathworks.com/supportfiles/ci/matlab-batch/v0/install.sh") { 44 | return "install.sh"; 45 | } else { 46 | throw new Error("Incorrect URL"); 47 | } 48 | }, 49 | prependPath(toolPath: string) { 50 | if ( toolPath !== path.join(matlabRoot, "bin") && toolPath !== batchInstallRoot) { 51 | throw new Error(`Unexpected path: ${toolPath}`); 52 | } 53 | }, 54 | }); 55 | 56 | tr.registerMock("./utils", { 57 | platform: () => "linux", 58 | architecture: () => "x64", 59 | }); 60 | 61 | const a: ma.TaskLibAnswers = { 62 | which: { 63 | bash: "/bin/bash", 64 | }, 65 | checkPath: { 66 | "/bin/bash": true, 67 | }, 68 | exec: { 69 | "sudo -E /bin/bash install.sh R2020a": { 70 | code: 0, 71 | stdout: "Installed MATLAB dependencies", 72 | }, 73 | "sudo -E /bin/bash ci-install.sh --release R2020a --skip-activation": { 74 | code: 0, 75 | stdout: "Installed MATLAB without activating", 76 | }, 77 | "sudo -E /bin/bash install.sh /opt/matlab-batch": { 78 | code: 0, 79 | stdout: "Installed matlab-batch", 80 | }, 81 | }, 82 | } as ma.TaskLibAnswers; 83 | tr.setAnswers(a); 84 | 85 | tr.run(); 86 | -------------------------------------------------------------------------------- /tasks/install-matlab/v0/test/downloadAndExecuteWindows.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import * as fs from "fs"; 6 | import * as os from "os"; 7 | import path = require("path"); 8 | 9 | const tp = path.join(__dirname, "..", "main.js"); 10 | const tr = new mr.TaskMockRunner(tp); 11 | 12 | tr.setInput("release", "R2020a"); 13 | 14 | delete process.env.MATHWORKS_ACCOUNT; 15 | delete process.env.MATHWORKS_TOKEN; 16 | 17 | const matlabRoot = "C:\\path\\to\\matlab"; 18 | fs.writeFileSync(path.join(os.tmpdir(), "ephemeral_matlab_root"), matlabRoot); 19 | const batchInstallRoot = path.join("C:", "Program Files", "matlab-batch"); 20 | 21 | // create assertAgent and getVariable mocks, support not added in this version of task-lib 22 | import tl = require("azure-pipelines-task-lib/mock-task"); 23 | const tlClone = Object.assign({}, tl); 24 | // @ts-ignore 25 | tlClone.getVariable = (variable: string) => { 26 | if (variable.toLocaleLowerCase() === "system.servertype") { 27 | return "hosted"; 28 | } 29 | return null; 30 | }; 31 | // @ts-ignore 32 | tlClone.assertAgent = (variable: string) => { 33 | return; 34 | }; 35 | tr.registerMock("azure-pipelines-task-lib/mock-task", tlClone); 36 | 37 | tr.registerMock("azure-pipelines-tool-lib/tool", { 38 | downloadToolWithRetries(url: string) { 39 | if (url === "https://ssd.mathworks.com/supportfiles/ci/ephemeral-matlab/v0/ci-install.sh") { 40 | return "ci-install.sh"; 41 | } else if (url === "https://ssd.mathworks.com/supportfiles/ci/matlab-batch/v0/install.sh") { 42 | return "install.sh"; 43 | } else { 44 | throw new Error("Incorrect URL"); 45 | } 46 | }, 47 | prependPath(toolPath: string) { 48 | if ( toolPath !== path.join(matlabRoot, "bin") && toolPath !== batchInstallRoot) { 49 | throw new Error(`Unexpected path: ${toolPath}`); 50 | } 51 | }, 52 | }); 53 | 54 | tr.registerMock("./utils", { 55 | platform: () => "win32", 56 | architecture: () => "x64", 57 | }); 58 | 59 | const a: ma.TaskLibAnswers = { 60 | which: { 61 | bash: "bash.exe", 62 | }, 63 | checkPath: { 64 | "bash.exe": true, 65 | }, 66 | exec: { 67 | "bash.exe ci-install.sh --release R2020a": { 68 | code: 0, 69 | stdout: "Installed MATLAB", 70 | }, 71 | "bash.exe install.sh C:\\Program Files\\matlab-batch": { 72 | code: 0, 73 | stdout: "Installed matlab-batch", 74 | }, 75 | "bash.exe install.sh C:/Program Files/matlab-batch": { 76 | code: 0, 77 | stdout: "Installed matlab-batch", 78 | }, 79 | }, 80 | } as ma.TaskLibAnswers; 81 | tr.setAnswers(a); 82 | 83 | tr.run(); 84 | -------------------------------------------------------------------------------- /tasks/install-matlab/v0/test/failDownload.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | import mr = require("azure-pipelines-task-lib/mock-run"); 4 | import path = require("path"); 5 | 6 | const tp = path.join(__dirname, "..", "main.js"); 7 | const tr = new mr.TaskMockRunner(tp); 8 | 9 | tr.setInput("release", "R2020a"); 10 | 11 | // create assertAgent and getVariable mocks, support not added in this version of task-lib 12 | import tl = require("azure-pipelines-task-lib/mock-task"); 13 | const tlClone = Object.assign({}, tl); 14 | // @ts-ignore 15 | tlClone.getVariable = (variable: string) => { 16 | if (variable.toLocaleLowerCase() === "system.servertype") { 17 | return "hosted"; 18 | } 19 | return null; 20 | }; 21 | // @ts-ignore 22 | tlClone.assertAgent = (variable: string) => { 23 | return; 24 | }; 25 | tr.registerMock("azure-pipelines-task-lib/mock-task", tlClone); 26 | 27 | tr.registerMock("azure-pipelines-tool-lib/tool", { 28 | downloadToolWithRetries() { 29 | throw new Error("Download failed"); 30 | }, 31 | }); 32 | 33 | tr.run(); 34 | -------------------------------------------------------------------------------- /tasks/install-matlab/v0/test/failExecute.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | 7 | const tp = path.join(__dirname, "..", "main.js"); 8 | const tr = new mr.TaskMockRunner(tp); 9 | 10 | tr.setInput("release", "R2020a"); 11 | 12 | // create assertAgent and getVariable mocks, support not added in this version of task-lib 13 | import tl = require("azure-pipelines-task-lib/mock-task"); 14 | const tlClone = Object.assign({}, tl); 15 | // @ts-ignore 16 | tlClone.getVariable = (variable: string) => { 17 | if (variable.toLocaleLowerCase() === "system.servertype") { 18 | return "hosted"; 19 | } 20 | return null; 21 | }; 22 | // @ts-ignore 23 | tlClone.assertAgent = (variable: string) => { 24 | return; 25 | }; 26 | tr.registerMock("azure-pipelines-task-lib/mock-task", tlClone); 27 | 28 | tr.registerMock("azure-pipelines-tool-lib/tool", { 29 | downloadToolWithRetries(url: string) { 30 | if (url === "https://ssd.mathworks.com/supportfiles/ci/matlab-deps/v0/install.sh") { 31 | return "install.sh"; 32 | } else if (url === "https://ssd.mathworks.com/supportfiles/ci/ephemeral-matlab/v0/ci-install.sh") { 33 | return "ci-install.sh"; 34 | } else { 35 | throw new Error("Incorrect URL"); 36 | } 37 | }, 38 | }); 39 | 40 | tr.registerMock("./utils", { 41 | platform: () => "linux", 42 | architecture: () => "x64", 43 | }); 44 | 45 | const a: ma.TaskLibAnswers = { 46 | which: { 47 | bash: "/bin/bash", 48 | }, 49 | checkPath: { 50 | "/bin/bash": true, 51 | }, 52 | exec: { 53 | "sudo -E /bin/bash install.sh R2020a": { 54 | code: 1, 55 | stdout: "Failed to install MATLAB dependencies", 56 | }, 57 | }, 58 | } as ma.TaskLibAnswers; 59 | tr.setAnswers(a); 60 | 61 | tr.run(); 62 | -------------------------------------------------------------------------------- /tasks/install-matlab/v0/test/failSelfHosted.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | import mr = require("azure-pipelines-task-lib/mock-run"); 4 | import path = require("path"); 5 | 6 | const tp = path.join(__dirname, "..", "main.js"); 7 | const tr = new mr.TaskMockRunner(tp); 8 | 9 | tr.setInput("release", "R2020a"); 10 | 11 | // create assertAgent and getVariable mocks, support not added in this version of task-lib 12 | import tl = require("azure-pipelines-task-lib/mock-task"); 13 | const tlClone = Object.assign({}, tl); 14 | // @ts-ignore 15 | tlClone.getVariable = (variable: string) => { 16 | if (variable.toLocaleLowerCase() === "system.servertype") { 17 | return "self-hosted"; 18 | } 19 | return null; 20 | }; 21 | // @ts-ignore 22 | tlClone.assertAgent = (variable: string) => { 23 | return; 24 | }; 25 | tr.registerMock("azure-pipelines-task-lib/mock-task", tlClone); 26 | 27 | tr.run(); 28 | -------------------------------------------------------------------------------- /tasks/install-matlab/v0/test/suite.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The MathWorks, Inc. 2 | 3 | import * as assert from "assert"; 4 | import * as mt from "azure-pipelines-task-lib/mock-test"; 5 | import * as path from "path"; 6 | 7 | describe("InstallMATLAB V0 Suite", () => { 8 | it("should succeed downloading and executing install script on linux", async () => { 9 | const tp = path.join(__dirname, "downloadAndExecuteLinux.js"); 10 | const tr = new mt.MockTestRunner(tp); 11 | 12 | await tr.runAsync(); 13 | 14 | assert(tr.succeeded, "should have succeeded"); 15 | assert(tr.stdOutContained("Installed MATLAB"), "should have executed install script"); 16 | }); 17 | 18 | it("should succeed downloading and executing install script on windows", async () => { 19 | const tp = path.join(__dirname, "downloadAndExecuteWindows.js"); 20 | const tr = new mt.MockTestRunner(tp); 21 | 22 | await tr.runAsync(); 23 | 24 | assert(tr.succeeded, "should have succeeded"); 25 | assert(tr.stdOutContained("Installed MATLAB"), "should have executed install script"); 26 | }); 27 | 28 | it("should succeed with --skip-activation flag for private repos", async () => { 29 | const tp = path.join(__dirname, "downloadAndExecutePrivate.js"); 30 | const tr = new mt.MockTestRunner(tp); 31 | 32 | await tr.runAsync(); 33 | 34 | assert(tr.succeeded, "should have succeeded"); 35 | assert(tr.stdOutContained("Installed MATLAB"), "should have executed install script"); 36 | }); 37 | 38 | it("should fail when downloading install script fails", async () => { 39 | const tp = path.join(__dirname, "failDownload.js"); 40 | const tr = new mt.MockTestRunner(tp); 41 | 42 | await tr.runAsync(); 43 | 44 | assert(tr.failed, "should have failed"); 45 | assert(tr.stdOutContained("Download failed"), "should have failed download"); 46 | }); 47 | 48 | it("should fail when executing install script fails", async () => { 49 | const tp = path.join(__dirname, "failExecute.js"); 50 | const tr = new mt.MockTestRunner(tp); 51 | 52 | await tr.runAsync(); 53 | 54 | assert(tr.failed, "should have failed"); 55 | assert(tr.stdOutContained("Failed to install MATLAB"), "should have failed to install"); 56 | }); 57 | 58 | it("should fail on self-hosted agents", async () => { 59 | const tp = path.join(__dirname, "failSelfHosted.js"); 60 | const tr = new mt.MockTestRunner(tp); 61 | 62 | await tr.runAsync(); 63 | 64 | assert(tr.failed, "should have failed"); 65 | assert(tr.stdOutContained("InstallNotSupportedOnSelfHosted"), "should have failed to install"); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /tasks/install-matlab/v0/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "sourceMap": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tasks/install-matlab/v0/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | export function platform() { 4 | return process.platform; 5 | } 6 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/matlab-azure-devops-extension/20cd6026fc706cc925710c25cd2d7f8528441a3e/tasks/install-matlab/v1/icon.png -------------------------------------------------------------------------------- /tasks/install-matlab/v1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "install-matlab", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/node": "^22.7.5", 7 | "@types/q": "^1.5.4", 8 | "azure-pipelines-task-lib": "5.0.1-preview.0", 9 | "azure-pipelines-tool-lib": "^2.0.7" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/src/install.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import {AgentHostedMode, getAgentMode} from "azure-pipelines-task-lib/task"; 4 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 5 | import * as fs from "fs"; 6 | import * as path from "path"; 7 | import * as matlab from "./matlab"; 8 | import * as mpm from "./mpm"; 9 | 10 | export async function install(platform: string, architecture: string, release: string, products: string) { 11 | const parsedRelease: matlab.Release = await matlab.getReleaseInfo(release); 12 | if (parsedRelease.name < "r2020b") { 13 | throw new Error(`Release '${parsedRelease.name}' is not supported. Use 'R2020b' or a later release.`); 14 | } 15 | 16 | // install core system dependencies on Linux and Apple silicon on cloud-hosted agents 17 | if (getAgentMode() === AgentHostedMode.MsHosted) { 18 | await matlab.installSystemDependencies(platform, architecture, parsedRelease.name); 19 | } 20 | 21 | // Use Intel MATLAB for releases before R2023b 22 | let matlabArch = architecture; 23 | if (platform === "darwin" && architecture === "arm64" && parsedRelease.name < "r2023b") { 24 | matlabArch = "x64"; 25 | } 26 | 27 | // setup mpm 28 | const mpmPath: string = await mpm.setup(platform, matlabArch); 29 | 30 | // install MATLAB using mpm 31 | const [toolpath, alreadyExists] = await matlab.makeToolcacheDir(parsedRelease, platform); 32 | if (!alreadyExists) { 33 | await mpm.install(mpmPath, parsedRelease, toolpath, products); 34 | } 35 | 36 | // add MATLAB to system path 37 | try { 38 | toolLib.prependPath(path.join(toolpath, "bin")); 39 | } catch (err: any) { 40 | throw new Error("Failed to add MATLAB to system path."); 41 | } 42 | 43 | // install matlab-batch 44 | await matlab.setupBatch(platform, matlabArch); 45 | 46 | // add MATLAB Runtime to system path on Windows 47 | if (platform === "win32") { 48 | try { 49 | const runtimePath = path.join(toolpath, "runtime", matlabArch === "x86" ? "win32" : "win64"); 50 | if (fs.existsSync(runtimePath)) { 51 | toolLib.prependPath(runtimePath); 52 | } 53 | } catch (err: any) { 54 | throw new Error("Failed to add MATLAB Runtime to system path on windows."); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/src/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as path from "path"; 5 | import * as install from "./install"; 6 | 7 | async function run() { 8 | try { 9 | taskLib.setResourcePath(path.join( __dirname, "..", "task.json")); 10 | const release = taskLib.getInput("release") || "latest"; 11 | const products = taskLib.getInput("products") || "MATLAB"; 12 | await install.install(process.platform, process.arch, release, products); 13 | } catch (err) { 14 | taskLib.setResult(taskLib.TaskResult.Failed, (err as Error).message); 15 | } 16 | } 17 | 18 | run(); 19 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/src/matlab.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 5 | import * as fs from "fs"; 6 | import * as https from "https"; 7 | import * as path from "path"; 8 | import * as script from "./script"; 9 | import { downloadToolWithRetries } from "./utils"; 10 | 11 | export interface Release { 12 | name: string; 13 | version: string; 14 | update: string; 15 | isPrerelease: boolean; 16 | } 17 | 18 | export async function makeToolcacheDir(release: Release, platform: string): Promise<[string, boolean]> { 19 | let toolpath: string = toolLib.findLocalTool("MATLAB", release.version); 20 | let alreadyExists = false; 21 | if (toolpath) { 22 | alreadyExists = true; 23 | } else { 24 | if (platform === "win32") { 25 | toolpath = await windowsHostedToolpath(release).catch(async () => { 26 | return await defaultToolpath(release); 27 | }); 28 | } else { 29 | toolpath = await defaultToolpath(release); 30 | } 31 | } 32 | if (platform === "darwin") { 33 | toolpath = toolpath + "/MATLAB.app"; 34 | } 35 | return [toolpath, alreadyExists]; 36 | } 37 | 38 | export async function defaultToolpath(release: Release): Promise { 39 | fs.writeFileSync(".keep", ""); 40 | return await toolLib.cacheFile(".keep", ".keep", "MATLAB", release.version); 41 | } 42 | 43 | async function windowsHostedToolpath(release: Release): Promise { 44 | const defaultToolCacheRoot = taskLib.getVariable("Agent.ToolsDirectory"); 45 | 46 | // only apply optimization for microsoft hosted runners with a defined tool cache directory 47 | if (taskLib.getAgentMode() !== taskLib.AgentHostedMode.MsHosted || !defaultToolCacheRoot) { 48 | return Promise.reject(); 49 | } 50 | 51 | // make sure runner has expected directory structure 52 | if (!fs.existsSync("d:\\") || !fs.existsSync("c:\\")) { 53 | return Promise.reject(); 54 | } 55 | 56 | const defaultToolCacheDir = path.join(defaultToolCacheRoot, "MATLAB", release.version, "x64"); 57 | const actualToolCacheDir = defaultToolCacheDir.replace("C:", "D:").replace("c:", "d:"); 58 | 59 | // create install directory and link it to the toolcache directory 60 | fs.mkdirSync(actualToolCacheDir, {recursive: true}); 61 | fs.mkdirSync(path.dirname(defaultToolCacheDir), {recursive: true}); 62 | fs.symlinkSync(actualToolCacheDir, defaultToolCacheDir, "junction"); 63 | fs.writeFileSync(`${defaultToolCacheDir}.complete`, ""); 64 | return actualToolCacheDir; 65 | } 66 | 67 | export async function getReleaseInfo(release: string): Promise { 68 | // Get release name from input parameter 69 | let name: string; 70 | let isPrerelease: boolean = false; 71 | if (release.toLowerCase().trim() === "latest") { 72 | name = await fetchReleaseInfo("latest"); 73 | } else if (release.toLowerCase().trim() === "latest-including-prerelease") { 74 | name = await fetchReleaseInfo("latest-including-prerelease"); 75 | } else { 76 | const nameMatch = release.toLowerCase().match(/r[0-9]{4}[a-b]/); 77 | if (!nameMatch) { 78 | return Promise.reject(Error(`${release} is invalid or unsupported. Specify the value as R2020a or a later release.`)); 79 | } 80 | name = nameMatch[0]; 81 | } 82 | 83 | // create semantic version of format year.semiannual.update from release 84 | const year = name.slice(1, 5); 85 | const semiannual = name[5] === "a" ? "1" : "2"; 86 | const updateMatch = release.toLowerCase().match(/u[0-9]+/); 87 | let version = `${year}.${semiannual}`; 88 | let update: string; 89 | if (updateMatch) { 90 | update = updateMatch[0]; 91 | version += `.${update[1]}`; 92 | } else { 93 | update = "Latest"; 94 | version += ".999"; 95 | if (name.includes("prerelease")) { 96 | name = name.replace("prerelease", ""); 97 | version += "-prerelease"; 98 | isPrerelease = true; 99 | } 100 | } 101 | 102 | return { 103 | name, 104 | version, 105 | update, 106 | isPrerelease, 107 | }; 108 | } 109 | 110 | async function fetchReleaseInfo(name: string): Promise { 111 | return new Promise((resolve, reject) => { 112 | https.get(`https://ssd.mathworks.com/supportfiles/ci/matlab-release/v0/${name}`, (resp) => { 113 | if (resp.statusCode !== 200) { 114 | reject(Error(`Unable to retrieve latest MATLAB release information. Contact MathWorks at continuous-integration@mathworks.com if the problem persists.`)); 115 | } 116 | resp.on("data", (d) => { 117 | resolve(d.toString()); 118 | }); 119 | }); 120 | }); 121 | 122 | } 123 | 124 | export async function setupBatch(platform: string, architecture: string) { 125 | if (architecture !== "x64" && !(platform === "darwin" && architecture === "arm64")) { 126 | return Promise.reject(Error(`This task is not supported on ${platform} runners using the ${architecture} architecture.`)); 127 | } 128 | 129 | const matlabBatchRootUrl: string = "https://ssd.mathworks.com/supportfiles/ci/matlab-batch/v1/"; 130 | let matlabBatchUrl: string; 131 | let matlabBatchExt: string = ""; 132 | switch (platform) { 133 | case "win32": 134 | matlabBatchExt = ".exe"; 135 | matlabBatchUrl = matlabBatchRootUrl + "win64/matlab-batch.exe"; 136 | break; 137 | case "linux": 138 | matlabBatchUrl = matlabBatchRootUrl + "glnxa64/matlab-batch"; 139 | break; 140 | case "darwin": 141 | if (architecture === "x64") { 142 | matlabBatchUrl = matlabBatchRootUrl + "maci64/matlab-batch"; 143 | } else { 144 | matlabBatchUrl = matlabBatchRootUrl + "maca64/matlab-batch"; 145 | } 146 | break; 147 | default: 148 | return Promise.reject(Error(`This task is not supported on ${platform} runners.`)); 149 | } 150 | 151 | const tempPath = await downloadToolWithRetries(matlabBatchUrl, `matlab-batch${matlabBatchExt}`); 152 | const matlabBatchPath = await toolLib.cacheFile(tempPath, `matlab-batch${matlabBatchExt}`, "matlab-batch", "1.0.0"); 153 | try { 154 | toolLib.prependPath(matlabBatchPath); 155 | } catch (err: any) { 156 | throw new Error("Failed to add MATLAB to system path."); 157 | } 158 | if (platform !== "win32") { 159 | const exitCode = await taskLib.exec( 160 | "chmod", 161 | ["+x", path.join(matlabBatchPath, "matlab-batch" + matlabBatchExt)], 162 | ); 163 | if (exitCode !== 0) { 164 | return Promise.reject(Error("Unable to add execute permissions to matlab-batch binary.")); 165 | } 166 | } 167 | } 168 | 169 | export async function installSystemDependencies(platform: string, architecture: string, release: string) { 170 | if (platform === "linux") { 171 | const exitCode = await script.downloadAndRunScript(platform, "https://ssd.mathworks.com/supportfiles/ci/matlab-deps/v0/install.sh", [release]); 172 | if (exitCode !== 0) { 173 | return Promise.reject(Error("Unable to install core dependencies.")); 174 | } 175 | } else if (platform === "darwin" && architecture === "arm64") { 176 | if (release < "r2023b") { 177 | return installAppleSiliconRosetta(); 178 | } else { 179 | return installAppleSiliconJdk(); 180 | } 181 | } 182 | } 183 | 184 | async function installAppleSiliconRosetta() { 185 | const exitCode = await taskLib.exec("sudo", ["softwareupdate", "--install-rosetta", "--agree-to-licenses"]); 186 | if (exitCode !== 0) { 187 | return Promise.reject(Error("Unable to install Rosetta 2.")); 188 | } 189 | } 190 | 191 | async function installAppleSiliconJdk() { 192 | const jdk = await downloadToolWithRetries( 193 | "https://corretto.aws/downloads/resources/8.402.08.1/amazon-corretto-8.402.08.1-macosx-aarch64.pkg", 194 | "jdk.pkg", 195 | ); 196 | 197 | const exitCode = await taskLib.exec("sudo", ["installer", "-pkg", `"${jdk}"`, "-target", "/"]); 198 | if (exitCode !== 0) { 199 | return Promise.reject(Error("Unable to install Java runtime.")); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/src/mpm.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as matlab from "./matlab"; 5 | import { downloadToolWithRetries } from "./utils"; 6 | 7 | export async function setup(platform: string, architecture: string): Promise { 8 | const mpmRootUrl: string = "https://www.mathworks.com/mpm/"; 9 | let mpmUrl: string; 10 | if (architecture !== "x64" && !(platform === "darwin" && architecture === "arm64")) { 11 | return Promise.reject(Error(`This action is not supported on ${platform} runners using the ${architecture} architecture.`)); 12 | } 13 | let mpm: string; 14 | let exitCode: number; 15 | switch (platform) { 16 | case "win32": 17 | mpmUrl = mpmRootUrl + "win64/mpm"; 18 | mpm = await downloadToolWithRetries(mpmUrl, "mpm.exe"); 19 | break; 20 | case "linux": 21 | mpmUrl = mpmRootUrl + "glnxa64/mpm"; 22 | mpm = await downloadToolWithRetries(mpmUrl, "mpm"); 23 | exitCode = await taskLib.exec("chmod", ["+x", mpm]); 24 | if (exitCode !== 0) { 25 | return Promise.reject(Error("Unable to set up mpm.")); 26 | } 27 | break; 28 | case "darwin": 29 | if (architecture === "x64") { 30 | mpmUrl = mpmRootUrl + "maci64/mpm"; 31 | } else { 32 | mpmUrl = mpmRootUrl + "maca64/mpm"; 33 | } 34 | mpm = await downloadToolWithRetries(mpmUrl, "mpm"); 35 | exitCode = await taskLib.exec("chmod", ["+x", mpm]); 36 | if (exitCode !== 0) { 37 | return Promise.reject(Error("Unable to set up mpm.")); 38 | } 39 | break; 40 | default: 41 | return Promise.reject(Error(`This action is not supported on ${platform} runners using the ${architecture} architecture.`)); 42 | } 43 | return mpm; 44 | } 45 | 46 | export async function install( 47 | mpmPath: string, 48 | release: matlab.Release, 49 | destination: string, 50 | products: string, 51 | ): Promise { 52 | let parsedProducts = products.split(" "); 53 | // Add MATLAB by default 54 | parsedProducts.push("MATLAB"); 55 | // Remove duplicates 56 | parsedProducts = [...new Set(parsedProducts)]; 57 | let mpmArguments: string[] = [ 58 | "install", 59 | `--release=${release.name + release.update}`, 60 | `--destination=${destination}`, 61 | ]; 62 | 63 | if (release.isPrerelease) { 64 | mpmArguments = mpmArguments.concat("--release-status=Prerelease"); 65 | } 66 | mpmArguments = mpmArguments.concat("--products"); 67 | mpmArguments = mpmArguments.concat(parsedProducts); 68 | 69 | const exitCode = await taskLib.exec(mpmPath, mpmArguments); 70 | if (exitCode !== 0) { 71 | return Promise.reject(Error(`Failed to install MATLAB.`)); 72 | } 73 | return; 74 | } 75 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/src/script.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 5 | import * as path from "path"; 6 | 7 | export async function downloadAndRunScript(platform: string, url: string, args: string | string[]) { 8 | const scriptPath = await toolLib.downloadToolWithRetries(url); 9 | const bashPath = await taskLib.which("bash", true); 10 | const sudoPath = await taskLib.which("sudo", false); 11 | let bash; 12 | if (!sudoPath) { 13 | bash = taskLib.tool(bashPath); 14 | } else { 15 | bash = taskLib.tool("sudo").arg("-E").line(bashPath); 16 | } 17 | bash.arg(scriptPath); 18 | bash.arg(args); 19 | return bash.exec(); 20 | } 21 | 22 | export function defaultInstallRoot(platform: string, programName: string) { 23 | let installDir: string; 24 | if (platform === "win32") { 25 | installDir = path.join("C:", "Program Files", programName); 26 | } else { 27 | installDir = path.join("/", "opt", programName); 28 | } 29 | return installDir; 30 | } 31 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/src/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 5 | import * as path from "path"; 6 | 7 | export async function downloadToolWithRetries(url: string, fileName: string): Promise { 8 | let destPath: string; 9 | if (path.isAbsolute(fileName)) { 10 | destPath = fileName; 11 | } else { 12 | const tempDirectory = taskLib.getVariable("Agent.TempDirectory"); 13 | if (!tempDirectory) { 14 | throw new Error("Agent.TempDirectory is not set"); 15 | } 16 | destPath = path.join(tempDirectory, fileName); 17 | } 18 | 19 | taskLib.rmRF(destPath); 20 | await toolLib.downloadToolWithRetries(url, destPath); 21 | return destPath; 22 | } 23 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json", 3 | "id": "553fa7ff-af12-4821-8ace-6bf3dc410e62", 4 | "name": "InstallMATLAB", 5 | "friendlyName": "Install MATLAB", 6 | "description": "Set up your pipeline with a specific release of MATLAB.", 7 | "helpMarkDown": "", 8 | "category": "Tool", 9 | "author": "The MathWorks, Inc.", 10 | "version": { 11 | "Major": 1, 12 | "Minor": 0, 13 | "Patch": 0 14 | }, 15 | "inputs": [ 16 | { 17 | "name": "release", 18 | "type": "string", 19 | "label": "Release", 20 | "required": false, 21 | "defaultValue": "latest", 22 | "helpMarkDown": "MATLAB release to install (R2021a or later). If you do not specify `release`, the task installs the latest release of MATLAB." 23 | }, 24 | { 25 | "name": "products", 26 | "type": "string", 27 | "label": "Products", 28 | "required": false, 29 | "defaultValue": "MATLAB", 30 | "helpMarkDown": "Products to install in addition to MATLAB, specified as a list of product names separated by spaces." 31 | } 32 | ], 33 | "instanceNameFormat": "Install MATLAB", 34 | "execution": { 35 | "Node10": { 36 | "target": "src/main.js" 37 | }, 38 | "Node16": { 39 | "target": "src/main.js" 40 | }, 41 | "Node20": { 42 | "target": "src/main.js" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/test/install.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as assert from "assert"; 4 | import * as taskLib from "azure-pipelines-task-lib/task"; 5 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 6 | import * as fs from "fs"; 7 | import * as sinon from "sinon"; 8 | import * as install from "../src/install"; 9 | import * as matlab from "../src/matlab"; 10 | import * as mpm from "../src/mpm"; 11 | import * as script from "../src/script"; 12 | 13 | export default function suite() { 14 | describe("install.ts test suite", () => { 15 | let stubGetReleaseInfo: sinon.SinonStub; 16 | let stubGetAgentMode: sinon.SinonStub; 17 | let stubInstallSystemDependencies: sinon.SinonStub; 18 | let stubMakeToolcacheDir: sinon.SinonStub; 19 | let stubSetupBatch: sinon.SinonStub; 20 | let stubMpmSetup: sinon.SinonStub; 21 | let stubMpmInstall: sinon.SinonStub; 22 | let stubPrependPath: sinon.SinonStub; 23 | let stubExistsSync: sinon.SinonStub; 24 | 25 | const platform = "linux"; 26 | const architecture = "x64"; 27 | const release = "latest"; 28 | const products = "MATLAB"; 29 | const toolcacheDir = "/path/to/matlab"; 30 | const releaseInfo = {name: "r2022b", version: "2022.2.999", update: "latest"}; 31 | 32 | beforeEach(() => { 33 | stubGetReleaseInfo = sinon.stub(matlab, "getReleaseInfo"); 34 | stubGetReleaseInfo.callsFake((rel) => { 35 | return releaseInfo; 36 | }); 37 | stubGetAgentMode = sinon.stub(taskLib, "getAgentMode"); 38 | stubGetAgentMode.returns(taskLib.AgentHostedMode.MsHosted); 39 | stubInstallSystemDependencies = sinon.stub(matlab, "installSystemDependencies"); 40 | stubInstallSystemDependencies.resolves(0); 41 | stubMakeToolcacheDir = sinon.stub(matlab, "makeToolcacheDir"); 42 | stubMakeToolcacheDir.callsFake((rel) => { 43 | return [toolcacheDir, false]; 44 | }); 45 | stubSetupBatch = sinon.stub(matlab, "setupBatch"); 46 | stubMpmSetup = sinon.stub(mpm, "setup"); 47 | stubMpmInstall = sinon.stub(mpm, "install"); 48 | stubPrependPath = sinon.stub(toolLib, "prependPath"); 49 | stubExistsSync = sinon.stub(fs, "existsSync"); 50 | }); 51 | 52 | afterEach(() => { 53 | stubGetReleaseInfo.restore(); 54 | stubGetAgentMode.restore(); 55 | stubInstallSystemDependencies.restore(); 56 | stubMakeToolcacheDir.restore(); 57 | stubSetupBatch.restore(); 58 | stubMpmSetup.restore(); 59 | stubMpmInstall.restore(); 60 | stubPrependPath.restore(); 61 | stubExistsSync.restore(); 62 | }); 63 | 64 | it("ideally works", async () => { 65 | assert.doesNotReject(async () => { await install.install(platform, architecture, release, products); }); 66 | }); 67 | 68 | it("fails for unsupported release", async () => { 69 | stubGetReleaseInfo.callsFake((rel) => { 70 | return {name: "r2020a", version: "2020.1.999", update: "latest"}; 71 | }); 72 | assert.rejects(async () => { await install.install(platform, architecture, "r2020a", products); }); 73 | }); 74 | 75 | it("fails if setting up core deps fails", async () => { 76 | stubInstallSystemDependencies.rejects("bam"); 77 | assert.rejects(async () => { await install.install(platform, architecture, release, products); }); 78 | }); 79 | 80 | it("does not install core deps if self-hosted", async () => { 81 | stubGetAgentMode.returns(taskLib.AgentHostedMode.SelfHosted); 82 | await install.install(platform, architecture, release, products); 83 | assert(stubInstallSystemDependencies.notCalled); 84 | }); 85 | 86 | it("does not install if MATLAB already exists in toolcache", async () => { 87 | stubMakeToolcacheDir.callsFake((rel) => { 88 | return [toolcacheDir, true]; 89 | }); 90 | await assert.doesNotReject(async () => { 91 | await install.install(platform, architecture, release, products); 92 | }); 93 | assert(stubMpmInstall.notCalled); 94 | }); 95 | 96 | it("fails if add to path fails", async () => { 97 | stubPrependPath.callsFake((tool) => { 98 | throw new Error("BAM!"); 99 | }); 100 | assert.rejects(async () => { await install.install(platform, architecture, release, products); }); 101 | }); 102 | 103 | it("installs Intel version on Apple silicon prior to R2023b", async () => { 104 | await install.install("darwin", "arm64", release, products); 105 | assert(stubInstallSystemDependencies.calledWith("darwin", "arm64", "r2022b")); 106 | assert(stubSetupBatch.calledWith("darwin", "x64")); 107 | assert(stubMpmSetup.calledWith("darwin", "x64")); 108 | }); 109 | 110 | // Update the test cases 111 | it("adds MATLAB Runtime to system path on Windows if directory exists", async () => { 112 | stubExistsSync.returns(true); 113 | await install.install("win32", "x64", release, products); 114 | assert(stubPrependPath.calledWith(`${toolcacheDir}/runtime/win64`)); 115 | }); 116 | 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/test/matlab.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as assert from "assert"; 4 | import * as taskLib from "azure-pipelines-task-lib/task"; 5 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 6 | import * as fs from "fs"; 7 | import * as http from "http"; 8 | import * as https from "https"; 9 | import * as net from "net"; 10 | import * as path from "path"; 11 | import * as sinon from "sinon"; 12 | import * as matlab from "./../src/matlab"; 13 | import * as script from "./../src/script"; 14 | import * as utils from "./../src/utils"; 15 | 16 | export default function suite() { 17 | describe("matlab.ts test suite", () => { 18 | describe("makeToolcacheDir", () => { 19 | let stubCacheFile: sinon.SinonStub; 20 | let stubFindLocalTool: sinon.SinonStub; 21 | let writeFileStub: sinon.SinonStub; 22 | let stubDownloadAndRun: sinon.SinonStub; 23 | let platform: string; 24 | const defaultToolcacheLoc = "/opt/hostedtoolcache/matlab/2023.2.999/x64"; 25 | const releaseInfo = { 26 | name: "r2023b", 27 | version: "2023.2.999", 28 | update: "Latest", 29 | isPrerelease: false, 30 | }; 31 | 32 | beforeEach(() => { 33 | writeFileStub = sinon.stub(fs, "writeFileSync"); 34 | stubCacheFile = sinon.stub(toolLib, "cacheFile"); 35 | stubFindLocalTool = sinon.stub(toolLib, "findLocalTool"); 36 | stubFindLocalTool.callsFake((tool, ver) => { 37 | return ""; 38 | }); 39 | stubDownloadAndRun = sinon.stub(script, "downloadAndRunScript"); 40 | platform = "linux"; 41 | }); 42 | 43 | afterEach(() => { 44 | stubCacheFile.restore(); 45 | stubFindLocalTool.restore(); 46 | writeFileStub.restore(); 47 | stubDownloadAndRun.restore(); 48 | }); 49 | 50 | it("makeToolcacheDir returns toolpath if in toolcache", async () => { 51 | stubFindLocalTool.callsFake((tool, ver) => { 52 | return defaultToolcacheLoc; 53 | }); 54 | const [matlabPath, alreadyExists] = await matlab.makeToolcacheDir(releaseInfo, platform); 55 | assert(matlabPath === defaultToolcacheLoc); 56 | assert(alreadyExists); 57 | }); 58 | 59 | it("creates cache and returns default path for linux", async () => { 60 | stubCacheFile.callsFake((src, dest, tool, ver) => { 61 | return Promise.resolve(defaultToolcacheLoc); 62 | }); 63 | const [matlabPath, alreadyExists] = await matlab.makeToolcacheDir(releaseInfo, platform); 64 | assert(matlabPath === defaultToolcacheLoc); 65 | assert(!alreadyExists); 66 | }); 67 | 68 | it("creates cache and returns default path for mac", async () => { 69 | platform = "darwin"; 70 | stubCacheFile.callsFake((src, dest, tool, ver) => { 71 | return Promise.resolve(defaultToolcacheLoc); 72 | }); 73 | const [matlabPath, alreadyExists] = await matlab.makeToolcacheDir(releaseInfo, platform); 74 | assert(matlabPath === path.join(defaultToolcacheLoc, "MATLAB.app")); 75 | assert(!alreadyExists); 76 | }); 77 | 78 | it("finds existing cache and returns default path for mac", async () => { 79 | platform = "darwin"; 80 | stubFindLocalTool.callsFake((tool, ver) => { 81 | return defaultToolcacheLoc; 82 | }); 83 | const [matlabPath, alreadyExists] = await matlab.makeToolcacheDir(releaseInfo, platform); 84 | assert(matlabPath === path.join(defaultToolcacheLoc, "MATLAB.app")); 85 | assert(alreadyExists); 86 | }); 87 | 88 | describe("windows performance workaround", () => { 89 | let stubGetVariable: sinon.SinonStub; 90 | let stubGetAgentMode: sinon.SinonStub; 91 | let existsSyncStub: sinon.SinonStub; 92 | let mkdirStub: sinon.SinonStub; 93 | let symlinkStub: sinon.SinonStub; 94 | const defaultToolcacheWinDir = path.join("C:", "hostedtoolcache", "windows"); 95 | const defaultToolcacheWinLoc = path.join(defaultToolcacheWinDir, "MATLAB", "2023.2.999", "x64"); 96 | const optimizedToolcacheWinLoc = path.join("D:", "hostedtoolcache", "windows", "MATLAB", "2023.2.999", "x64"); 97 | 98 | beforeEach(() => { 99 | platform = "win32"; 100 | stubGetAgentMode = sinon.stub(taskLib, "getAgentMode"); 101 | stubGetAgentMode.callsFake(() => taskLib.AgentHostedMode.MsHosted); 102 | existsSyncStub = sinon.stub(fs, "existsSync"); 103 | existsSyncStub.callsFake((p) => true); 104 | mkdirStub = sinon.stub(fs, "mkdirSync"); 105 | symlinkStub = sinon.stub(fs, "symlinkSync"); 106 | stubGetVariable = sinon.stub(taskLib, "getVariable"); 107 | stubGetVariable.callsFake((v) => { 108 | if (v === "Agent.ToolsDirectory") { 109 | return defaultToolcacheWinDir; 110 | } 111 | }); 112 | stubCacheFile.callsFake((src, dest, tool, ver) => { 113 | return Promise.resolve(defaultToolcacheWinLoc); 114 | }); 115 | }); 116 | 117 | afterEach(() => { 118 | stubGetAgentMode.restore(); 119 | stubGetVariable.restore(); 120 | existsSyncStub.restore(); 121 | mkdirStub.restore(); 122 | symlinkStub.restore(); 123 | }); 124 | 125 | it("uses workaround if github-hosted", async () => { 126 | const [matlabPath, alreadyExists] = await matlab.makeToolcacheDir(releaseInfo, platform); 127 | assert(matlabPath === optimizedToolcacheWinLoc); 128 | assert(!alreadyExists); 129 | }); 130 | 131 | it("uses default toolcache directory if not github hosted", async () => { 132 | stubGetAgentMode.callsFake(() => taskLib.AgentHostedMode.SelfHosted); 133 | const [matlabPath, alreadyExists] = await matlab.makeToolcacheDir(releaseInfo, platform); 134 | assert(matlabPath === defaultToolcacheWinLoc); 135 | assert(!alreadyExists); 136 | }); 137 | 138 | it("uses default toolcache directory toolcache directory is not defined", async () => { 139 | stubGetVariable.callsFake((v) => { return; }); 140 | const [matlabPath, alreadyExists] = await matlab.makeToolcacheDir(releaseInfo, platform); 141 | assert(matlabPath === defaultToolcacheWinLoc); 142 | assert(!alreadyExists); 143 | }); 144 | 145 | it("uses default toolcache directory if d: drive doesn't exist", async () => { 146 | existsSyncStub.callsFake((p) => { 147 | return p !== "d:\\"; 148 | }); 149 | const [matlabPath, alreadyExists] = await matlab.makeToolcacheDir(releaseInfo, platform); 150 | assert(matlabPath === defaultToolcacheWinLoc); 151 | assert(!alreadyExists); 152 | }); 153 | 154 | it("uses default toolcache directory if c: drive doesn't exist", async () => { 155 | existsSyncStub.callsFake((p) => { 156 | return p !== "d:\\"; 157 | }); 158 | const [matlabPath, alreadyExists] = await matlab.makeToolcacheDir(releaseInfo, platform); 159 | assert(matlabPath === defaultToolcacheWinLoc); 160 | assert(!alreadyExists); 161 | }); 162 | }); 163 | }); 164 | 165 | describe("setupBatch", () => { 166 | let stubGetVariable: sinon.SinonStub; 167 | let stubCacheFile: sinon.SinonStub; 168 | let stubDownloadTool: sinon.SinonStub; 169 | let stubPrependPath: sinon.SinonStub; 170 | let stubExec: sinon.SinonStub; 171 | let platform: string; 172 | let architecture: string; 173 | const matlabBatchPath = "/path/to/downloaded/matlab-batch"; 174 | 175 | beforeEach(() => { 176 | stubGetVariable = sinon.stub(taskLib, "getVariable"); 177 | stubGetVariable.callsFake((v) => { 178 | if (v === "Agent.ToolsDirectory") { 179 | return "C:\\Program Files\\hostedtoolcache\\MATLAB\\r2022b"; 180 | } else if (v === "Agent.TempDirectory") { 181 | return "/home/agent/_tmp"; 182 | } 183 | return ""; 184 | }); 185 | stubExec = sinon.stub(taskLib, "exec"); 186 | stubExec.callsFake((tool, args) => { 187 | return Promise.resolve(0); 188 | }); 189 | stubCacheFile = sinon.stub(toolLib, "cacheFile"); 190 | stubCacheFile.callsFake((srcFile, desFile, tool, ver) => { 191 | return Promise.resolve(matlabBatchPath); 192 | }); 193 | stubDownloadTool = sinon.stub(toolLib, "downloadToolWithRetries"); 194 | stubDownloadTool.callsFake((url, name) => { 195 | return Promise.resolve(matlabBatchPath); 196 | }); 197 | stubPrependPath = sinon.stub(toolLib, "prependPath"); 198 | platform = "linux"; 199 | architecture = "x64"; 200 | }); 201 | 202 | afterEach(() => { 203 | stubGetVariable.restore(); 204 | stubExec.restore(); 205 | stubCacheFile.restore(); 206 | stubDownloadTool.restore(); 207 | stubPrependPath.restore(); 208 | }); 209 | 210 | describe("test on all supported platforms", () => { 211 | it(`works on linux`, async () => { 212 | platform = "linux"; 213 | assert.doesNotReject(async () => { 214 | await matlab.setupBatch(platform, architecture); 215 | }); 216 | }); 217 | 218 | it(`works on windows`, async () => { 219 | platform = "win32"; 220 | assert.doesNotReject(async () => { 221 | await matlab.setupBatch(platform, architecture); 222 | }); 223 | }); 224 | 225 | it(`works on mac`, async () => { 226 | platform = "darwin"; 227 | assert.doesNotReject(async () => { 228 | await matlab.setupBatch(platform, architecture); 229 | }); 230 | }); 231 | 232 | it(`works on mac with apple silicon`, async () => { 233 | platform = "darwin"; 234 | architecture = "arm64"; 235 | assert.doesNotReject(async () => { 236 | await matlab.setupBatch(platform, architecture); 237 | }); 238 | }); 239 | }); 240 | 241 | it("setupBatch rejects on unsupported platforms", async () => { 242 | platform = "sunos"; 243 | assert.rejects(async () => { 244 | await matlab.setupBatch(platform, architecture); 245 | }); 246 | }); 247 | 248 | it("setupBatch rejects on unsupported architectures", async () => { 249 | architecture = "x86"; 250 | assert.rejects(async () => { 251 | await matlab.setupBatch(platform, architecture); 252 | }); 253 | }); 254 | 255 | it("setupBatch rejects if chmod fails", async () => { 256 | stubExec.callsFake((tool, args) => { 257 | return Promise.resolve(1); 258 | }); 259 | assert.rejects(async () => { 260 | await matlab.setupBatch(platform, architecture); 261 | }); 262 | }); 263 | 264 | it("setupBatch rejects when the download fails", async () => { 265 | stubDownloadTool.callsFake((url, name) => { 266 | return Promise.reject(); 267 | }); 268 | assert.rejects(async () => { 269 | await matlab.setupBatch(platform, architecture); 270 | }); 271 | }); 272 | 273 | it("setupBatch rejects when adding to path fails", async () => { 274 | stubPrependPath.callsFake((p) => { 275 | throw Error("BAM!"); 276 | }); 277 | assert.rejects(async () => { 278 | await matlab.setupBatch(platform, architecture); 279 | }); 280 | }); 281 | }); 282 | 283 | describe("getReleaseInfo", () => { 284 | let stubHttpsGet: sinon.SinonStub; 285 | const releaseInfo = { 286 | name: "r2022b", 287 | version: "2022.2.999", 288 | update: "Latest", 289 | }; 290 | 291 | beforeEach(() => { 292 | stubHttpsGet = sinon.stub(https, "get"); 293 | }); 294 | 295 | afterEach(() => { 296 | stubHttpsGet.restore(); 297 | }); 298 | 299 | it("getReleaseInfo resolves latest", async () => { 300 | const mockResp = new http.IncomingMessage(new net.Socket()); 301 | mockResp.statusCode = 200; 302 | stubHttpsGet.callsFake((url, callback) => { 303 | callback(mockResp); 304 | mockResp.emit("data", "r2024b"); 305 | mockResp.emit("end"); 306 | return Promise.resolve(null); 307 | }); 308 | const release = await matlab.getReleaseInfo("latest"); 309 | assert(release.name === "r2024b"); 310 | assert(release.isPrerelease === false); 311 | }); 312 | 313 | it("getReleaseInfo resolves latest-including-prerelease", async () => { 314 | const mockResp = new http.IncomingMessage(new net.Socket()); 315 | mockResp.statusCode = 200; 316 | stubHttpsGet.callsFake((url, callback) => { 317 | callback(mockResp); 318 | mockResp.emit("data", "r2025aprerelease"); 319 | mockResp.emit("end"); 320 | return Promise.resolve(null); 321 | }); 322 | const release = await matlab.getReleaseInfo("latest"); 323 | assert(release.name === "r2025a"); 324 | assert(release.isPrerelease === true); 325 | }); 326 | 327 | it("getReleaseInfo is case insensitive", async () => { 328 | const release = await matlab.getReleaseInfo("R2022B"); 329 | assert(release.name === "r2022b"); 330 | assert(release.version === "2022.2.999"); 331 | assert(release.update === "Latest"); 332 | assert(release.isPrerelease === false); 333 | }); 334 | 335 | it("getReleaseInfo allows specifying update number", async () => { 336 | const release = await matlab.getReleaseInfo("R2022au2"); 337 | assert(release.name === "r2022a"); 338 | assert(release.version === "2022.1.2"); 339 | assert(release.update === "u2"); 340 | assert(release.isPrerelease === false); 341 | }); 342 | 343 | it("getReleaseInfo rejects for invalid release input", async () => { 344 | assert.rejects(async () => { 345 | await matlab.getReleaseInfo("NotMatlab"); 346 | }); 347 | }); 348 | 349 | it("getReleaseInfo rejects for bad http response", async () => { 350 | const mockResp = new http.IncomingMessage(new net.Socket()); 351 | // bad response 352 | mockResp.statusCode = 500; 353 | stubHttpsGet.callsFake((url, callback) => { 354 | callback(mockResp); 355 | mockResp.emit("end"); 356 | return Promise.resolve(null); 357 | }); 358 | assert.rejects(async () => { 359 | await matlab.getReleaseInfo("latest"); 360 | }); 361 | }); 362 | }); 363 | 364 | describe("installSystemDependencies", () => { 365 | let platform: string; 366 | let architecture: string; 367 | let release: string; 368 | 369 | let stubDownloadAndRun: sinon.SinonStub; 370 | let stubDownloadTool: sinon.SinonStub; 371 | let stubExec: sinon.SinonStub; 372 | 373 | beforeEach(() => { 374 | platform = "linux"; 375 | architecture = "x64"; 376 | release = "r2024a"; 377 | 378 | stubDownloadAndRun = sinon.stub(script, "downloadAndRunScript"); 379 | stubDownloadAndRun.resolves(0); 380 | stubDownloadTool = sinon.stub(utils, "downloadToolWithRetries"); 381 | stubDownloadTool.resolves("/path/to/jdk.pkg"); 382 | stubExec = sinon.stub(taskLib, "exec"); 383 | stubExec.resolves(0); 384 | }); 385 | 386 | afterEach(() => { 387 | stubDownloadAndRun.restore(); 388 | stubDownloadTool.restore(); 389 | stubExec.restore(); 390 | }); 391 | 392 | describe("test on all supported platforms", () => { 393 | it("works on Linux", async () => { 394 | await assert.doesNotReject(async () => { 395 | await matlab.installSystemDependencies(platform, architecture, release); 396 | }); 397 | assert(stubDownloadAndRun.calledOnce); 398 | }); 399 | 400 | it("works on Windows", async () => { 401 | platform = "windows"; 402 | await assert.doesNotReject(async () => { 403 | await matlab.installSystemDependencies(platform, architecture, release); 404 | }); 405 | assert(stubDownloadAndRun.notCalled); 406 | }); 407 | 408 | it("works on Mac", async () => { 409 | platform = "darwin"; 410 | await assert.doesNotReject(async () => { 411 | await matlab.installSystemDependencies(platform, architecture, release); 412 | }); 413 | assert(stubDownloadAndRun.notCalled); 414 | }); 415 | 416 | it("works on Mac with Apple silicon", async () => { 417 | platform = "darwin"; 418 | architecture = "arm64"; 419 | await assert.doesNotReject(async () => { 420 | await matlab.installSystemDependencies(platform, architecture, release); 421 | }); 422 | assert(stubDownloadAndRun.notCalled); 423 | assert(stubDownloadTool.calledOnce); 424 | assert(stubExec.calledOnce); 425 | }); 426 | 427 | it("works on Mac with Apple silicon < R2023b", async () => { 428 | platform = "darwin"; 429 | architecture = "arm64"; 430 | release = "r2022a"; 431 | await assert.doesNotReject(async () => { 432 | await matlab.installSystemDependencies(platform, architecture, release); 433 | }); 434 | assert(stubDownloadAndRun.notCalled); 435 | assert(stubDownloadTool.notCalled); 436 | assert(stubExec.calledOnce); 437 | }); 438 | }); 439 | 440 | it("rejects when the apple silicon JDK download fails", async () => { 441 | platform = "darwin"; 442 | architecture = "arm64"; 443 | stubDownloadTool.rejects("bam"); 444 | assert.rejects(async () => { 445 | await matlab.installSystemDependencies(platform, architecture, release); 446 | }); 447 | }); 448 | 449 | it("rejects when the apple silicon JDK fails to install", async () => { 450 | platform = "darwin"; 451 | architecture = "arm64"; 452 | stubExec.resolves(1); 453 | assert.rejects(async () => { 454 | await matlab.installSystemDependencies(platform, architecture, release); 455 | }); 456 | }); 457 | }); 458 | }); 459 | } 460 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/test/mpm.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as assert from "assert"; 4 | import * as taskLib from "azure-pipelines-task-lib/task"; 5 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 6 | import * as sinon from "sinon"; 7 | import * as mpm from "../src/mpm"; 8 | 9 | export default function suite() { 10 | const arch = "x64"; 11 | let stubDownloadTool: sinon.SinonStub; 12 | let stubExec: sinon.SinonStub; 13 | let stubGetVariable: sinon.SinonStub; 14 | const agentTemp: string = "/home/agent/_tmp"; 15 | 16 | describe("mpm.ts test suite", () => { 17 | beforeEach(() => { 18 | // setup stubs 19 | stubDownloadTool = sinon.stub(toolLib, "downloadToolWithRetries"); 20 | stubDownloadTool.callsFake((url, fileName) => { 21 | return Promise.resolve(`${agentTemp}/fileName`); 22 | }); 23 | stubExec = sinon.stub(taskLib, "exec"); 24 | stubExec.callsFake((bin, args) => { 25 | return Promise.resolve(0); 26 | }); 27 | stubGetVariable = sinon.stub(taskLib, "getVariable"); 28 | stubGetVariable.callsFake((v) => { 29 | if (v === "Agent.ToolsDirectory") { 30 | return "C:\\Program Files\\hostedtoolcache\\MATLAB\\r2022b"; 31 | } else if (v === "Agent.TempDirectory") { 32 | return agentTemp; 33 | } 34 | return ""; 35 | }); 36 | }); 37 | 38 | afterEach(() => { 39 | // restore stubs 40 | stubDownloadTool.restore(); 41 | stubExec.restore(); 42 | stubGetVariable.restore(); 43 | }); 44 | 45 | it(`setup works on linux`, async () => { 46 | const platform = "linux"; 47 | 48 | const mpmPath = await mpm.setup(platform, arch); 49 | assert(mpmPath === "/home/agent/_tmp/mpm"); 50 | assert(stubDownloadTool.calledOnce); 51 | assert(stubExec.calledOnce); 52 | }); 53 | 54 | it(`setup works on windows`, async () => { 55 | const platform = "win32"; 56 | 57 | const mpmPath = await mpm.setup(platform, arch); 58 | assert(mpmPath === "/home/agent/_tmp/mpm.exe"); 59 | assert(stubDownloadTool.calledOnce); 60 | assert(stubExec.notCalled); 61 | }); 62 | 63 | it(`setup works on mac`, async () => { 64 | const platform = "darwin"; 65 | 66 | const mpmPath = await mpm.setup(platform, arch); 67 | assert(mpmPath === "/home/agent/_tmp/mpm"); 68 | assert(stubDownloadTool.calledOnce); 69 | assert(stubExec.calledOnce); 70 | }); 71 | 72 | it(`setup works on mac with apple silicon`, async () => { 73 | const platform = "darwin"; 74 | 75 | const mpmPath = await mpm.setup(platform, "arm64"); 76 | assert(mpmPath === "/home/agent/_tmp/mpm"); 77 | assert(stubDownloadTool.calledOnce); 78 | assert(stubExec.calledOnce); 79 | }); 80 | 81 | it(`setup rejects on unsupported platforms`, async () => { 82 | const platform = "sunos"; 83 | assert.rejects(async () => { await mpm.setup(platform, arch); }); 84 | }); 85 | 86 | it(`setup rejects on unsupported architectures`, async () => { 87 | const platform = "win32"; 88 | assert.rejects(async () => { await mpm.setup(platform, "x86"); }); 89 | }); 90 | 91 | it("setup rejects when the download fails", async () => { 92 | const platform = "linux"; 93 | stubDownloadTool.callsFake((url, fileName?, handlers?) => { 94 | return Promise.reject(Error("BAM!")); 95 | }); 96 | assert.rejects(async () => { await mpm.setup(platform, arch); }); 97 | }); 98 | 99 | it("setup rejects on linux when the chmod fails", async () => { 100 | const platform = "linux"; 101 | stubExec.callsFake((bin, args?) => { 102 | // non-zero exit code 103 | return Promise.resolve(1); 104 | }); 105 | assert.rejects(async () => { await mpm.setup(platform, arch); }); 106 | }); 107 | 108 | it("setup rejects on macos when the chmod fails", async () => { 109 | const platform = "darwin"; 110 | stubExec.callsFake((bin, args?) => { 111 | // non-zero exit code 112 | return Promise.resolve(1); 113 | }); 114 | assert.rejects(async () => { await mpm.setup(platform, arch); }); 115 | }); 116 | 117 | it("install ideally works", async () => { 118 | const mpmPath = "mpm"; 119 | const releaseInfo = {name: "r2022b", version: "9.13.0", update: "Latest", isPrerelease: false}; 120 | const destination = "/opt/matlab"; 121 | const products = "MATLAB Compiler"; 122 | const expectedMpmArgs = [ 123 | "install", 124 | "--release=r2022bLatest", 125 | `--destination=${destination}`, 126 | "--products", 127 | "MATLAB", 128 | "Compiler", 129 | ]; 130 | assert.doesNotReject(async () => { mpm.install(mpmPath, releaseInfo, products, destination); }); 131 | mpm.install(mpmPath, releaseInfo, destination, products).then(() => { 132 | assert(stubExec.calledWithMatch(mpmPath, expectedMpmArgs)); 133 | }); 134 | }); 135 | 136 | it("add --release-status flag for prerelease", async () => { 137 | const mpmPath = "mpm"; 138 | const releaseInfo = {name: "r2025a", version: "9.13.0", update: "Latest", isPrerelease: true}; 139 | const destination = "/opt/matlab"; 140 | const products = "MATLAB Compiler"; 141 | const expectedMpmArgs = [ 142 | "install", 143 | "--release=r2025aLatest", 144 | `--destination=${destination}`, 145 | "--release-status=Prerelease", 146 | "--products", 147 | "MATLAB", 148 | "Compiler", 149 | ]; 150 | assert.doesNotReject(async () => { mpm.install(mpmPath, releaseInfo, products, destination); }); 151 | mpm.install(mpmPath, releaseInfo, destination, products).then(() => { 152 | assert(stubExec.calledWithMatch(mpmPath, expectedMpmArgs)); 153 | }); 154 | }); 155 | 156 | it("install rejects on failed install", async () => { 157 | const mpmPath = "mpm"; 158 | const releaseInfo = {name: "r2022b", version: "9.13.0", update: "latest", isPrerelease: false}; 159 | const destination = "/opt/matlab"; 160 | const products = "MATLAB Compiler"; 161 | stubExec.callsFake((bin, args?) => { 162 | // non-zero exit code 163 | return Promise.resolve(1); 164 | }); 165 | assert.rejects(async () => mpm.install(mpmPath, releaseInfo, destination, products)); 166 | }); 167 | }); 168 | } 169 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/test/script.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as assert from "assert"; 4 | import * as taskLib from "azure-pipelines-task-lib/task"; 5 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 6 | import * as sinon from "sinon"; 7 | import * as script from "../src/script"; 8 | 9 | const bashPath = "/bin/bash"; 10 | const mockToolRunner = { 11 | bin: bashPath, 12 | args: [] as string[], 13 | arg(val: string | string[]) { 14 | if (val instanceof Array) { 15 | this.args = this.args.concat(val); 16 | } else if (typeof (val) === "string") { 17 | this.args = this.args.concat(val.trim()); 18 | } 19 | return this; 20 | }, 21 | line() { return this; }, 22 | exec() { return 0; }, 23 | }; 24 | 25 | export default function suite() { 26 | let stubDownloadTool: sinon.SinonStub; 27 | let stubWhich: sinon.SinonStub; 28 | let stubTool: sinon.SinonStub; 29 | 30 | describe("script.ts test suite", () => { 31 | beforeEach(() => { 32 | // setup stubs 33 | stubDownloadTool = sinon.stub(toolLib, "downloadToolWithRetries"); 34 | stubDownloadTool.callsFake((url, fileName?, handlers?) => { 35 | return Promise.resolve("/path/to/script"); 36 | }); 37 | stubWhich = sinon.stub(taskLib, "which"); 38 | stubWhich.callsFake((file) => { 39 | return Promise.resolve("/bin/bash"); 40 | }); 41 | stubTool = sinon.stub(taskLib, "tool"); 42 | stubTool.callsFake((bin) => { 43 | return mockToolRunner; 44 | }); 45 | mockToolRunner.args = [] as string[]; 46 | }); 47 | 48 | afterEach(() => { 49 | // restore stubs 50 | stubDownloadTool.restore(); 51 | stubWhich.restore(); 52 | stubTool.restore(); 53 | }); 54 | 55 | it("downloadAndRunScript ideally works unix", async () => { 56 | stubWhich.callsFake((file) => { 57 | if (file === "bash") { 58 | return Promise.resolve("/bin/bash"); 59 | } else { 60 | return Promise.resolve("/bin/sudo"); 61 | } 62 | }); 63 | 64 | const platform = "linux"; 65 | const url = "https://mathworks.com/myscript"; 66 | const args = ["--release", "r2022b"]; 67 | const exitCode = await script.downloadAndRunScript(platform, url, args); 68 | assert(exitCode === 0); 69 | assert(mockToolRunner.args.includes("-E")); 70 | assert(args.every((arg) => mockToolRunner.args.includes(arg))); 71 | }); 72 | 73 | it("downloadAndRunScript ideally works windows", async () => { 74 | stubWhich.callsFake((file) => { 75 | if (file === "bash") { 76 | return Promise.resolve("/bin/bash"); 77 | } else { 78 | return Promise.resolve(""); 79 | } 80 | }); 81 | 82 | const platform = "win32"; 83 | const url = "https://mathworks.com/myscript"; 84 | const args = ["--release", "r2022b"]; 85 | const exitCode = await script.downloadAndRunScript(platform, url, args); 86 | 87 | assert(exitCode === 0); 88 | assert(!mockToolRunner.args.includes("-E")); 89 | assert(args.every((arg) => mockToolRunner.args.includes(arg))); 90 | }); 91 | 92 | // defaultInstallRoot test cases 93 | const testCase = (platform: string, subdirectory: string) => { 94 | it(`defaultInstallRoot sets correct install directory for ${platform}`, async () => { 95 | const installDir = script.defaultInstallRoot(platform, "matlab-batch"); 96 | assert(installDir.includes(subdirectory)); 97 | assert(installDir.includes("matlab-batch")); 98 | }); 99 | }; 100 | 101 | testCase("win32", "Program Files"); 102 | testCase("darwin", "opt"); 103 | testCase("linux", "opt"); 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/test/suite.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MathWorks, Inc. 2 | 3 | import installTests from "./install.test"; 4 | import matlabTests from "./matlab.test"; 5 | import mpmTests from "./mpm.test"; 6 | import scriptTests from "./script.test"; 7 | 8 | describe("InstallMATLAB V1 Suite", () => { 9 | installTests(); 10 | matlabTests(); 11 | mpmTests(); 12 | scriptTests(); 13 | }); 14 | -------------------------------------------------------------------------------- /tasks/install-matlab/v1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "sourceMap": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/.gitignore: -------------------------------------------------------------------------------- 1 | bin -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/matlab-azure-devops-extension/20cd6026fc706cc925710c25cd2d7f8528441a3e/tasks/run-matlab-build/v0/icon.png -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2023 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 5 | import { chmodSync } from "fs"; 6 | import * as path from "path"; 7 | import {architecture, platform} from "./utils"; 8 | 9 | async function run() { 10 | try { 11 | taskLib.setResourcePath(path.join( __dirname, "task.json")); 12 | const options: IRunBuildOptions = { 13 | Tasks: taskLib.getInput("tasks"), 14 | }; 15 | const startupOpts: string | undefined = taskLib.getInput("startupOptions"); 16 | await runBuild(options, startupOpts); 17 | } catch (err) { 18 | taskLib.setResult(taskLib.TaskResult.Failed, (err as Error).message); 19 | } 20 | } 21 | 22 | async function runBuild(options: IRunBuildOptions, args?: string) { 23 | if (architecture() !== "x64") { 24 | const msg = `This task is not supported on ${platform()} runners using the ${architecture()} architecture.`; 25 | throw new Error(msg); 26 | } 27 | let ext; 28 | let platformDir; 29 | switch (platform()) { 30 | case "win32": 31 | ext = ".exe"; 32 | platformDir = "win64"; 33 | break; 34 | case "darwin": 35 | ext = ""; 36 | platformDir = "maci64"; 37 | break; 38 | case "linux": 39 | ext = ""; 40 | platformDir = "glnxa64"; 41 | break; 42 | default: 43 | throw new Error(`This task is not supported on ${platform()} runners using the ${architecture()} architecture.`); 44 | } 45 | const binDir = path.join(__dirname, "bin", platformDir); 46 | const runToolPath = path.join(binDir, `run-matlab-command${ext}`); 47 | if (!taskLib.exist(runToolPath)) { 48 | const zipPath = path.join(binDir, "run-matlab-command.zip"); 49 | await toolLib.extractZip(zipPath, binDir); 50 | } 51 | 52 | chmodSync(runToolPath, "777"); 53 | const runTool = taskLib.tool(runToolPath); 54 | let buildtoolCommand: string = "buildtool"; 55 | if (options.Tasks) { 56 | buildtoolCommand = buildtoolCommand + " " + options.Tasks; 57 | } 58 | runTool.arg(buildtoolCommand); 59 | 60 | if (args) { 61 | runTool.arg(args.split(" ")); 62 | } 63 | 64 | const exitCode = await runTool.exec(); 65 | if (exitCode !== 0) { 66 | throw new Error(taskLib.loc("BuildFailed")); 67 | } 68 | } 69 | 70 | interface IRunBuildOptions { 71 | Tasks?: string; 72 | } 73 | 74 | run(); 75 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run-matlab-build", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "npm run getBinDep", 7 | "getBinDep": "../../../scripts/setupdeps.sh v1" 8 | }, 9 | "dependencies": { 10 | "@types/node": "^22.7.5", 11 | "@types/q": "^1.5.4", 12 | "azure-pipelines-task-lib": "5.0.1-preview.0", 13 | "azure-pipelines-tool-lib": "^2.0.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json", 3 | "id": "c61c6fcc-00f1-4aa0-8513-31aad3340512", 4 | "name": "RunMATLABBuild", 5 | "friendlyName": "Run MATLAB Build", 6 | "description": "Run a build using the MATLAB build tool. Use this task to run the MATLAB build tasks specified in a file named buildfile.m in the root of your repository. To use the run-build task, you need MATLAB R2022b or a later release.", 7 | "helpMarkDown": "", 8 | "category": "Build", 9 | "author": "The MathWorks, Inc.", 10 | "version": { 11 | "Major": 0, 12 | "Minor": 1, 13 | "Patch": 0 14 | }, 15 | "inputs": [ 16 | { 17 | "name": "tasks", 18 | "type": "string", 19 | "label": "Tasks", 20 | "required": false, 21 | "defaultValue": "", 22 | "helpMarkDown": "Space-separated list of tasks to run. If not specified, the task runs the default tasks in the file buildfile.m as well as all the tasks on which they depend." 23 | }, 24 | { 25 | "name": "startupOptions", 26 | "type": "string", 27 | "label": "Startup Options", 28 | "required": false, 29 | "defaultValue": "", 30 | "helpMarkDown": "Startup options to pass to MATLAB." 31 | } 32 | ], 33 | "instanceNameFormat": "Run MATLAB Build", 34 | "execution": { 35 | "Node10": { 36 | "target": "main.js" 37 | } 38 | }, 39 | "messages": { 40 | "BuildFailed": "Build failed with nonzero exit code." 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/test/failRunBuild.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | 7 | const tp = path.join(__dirname, "..", "main.js"); 8 | const tr = new mr.TaskMockRunner(tp); 9 | 10 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "glnxa64", "run-matlab-command"); 11 | 12 | tr.registerMock("./utils", { 13 | platform: () => "linux", 14 | architecture: () => "x64", 15 | }); 16 | 17 | tr.registerMock("fs", { 18 | chmodSync: () => Promise.resolve(0), 19 | }); 20 | 21 | const a: ma.TaskLibAnswers = { 22 | checkPath: { 23 | [runCmdPath]: true, 24 | }, 25 | exec: { 26 | [runCmdPath + " buildtool"]: { 27 | code: 1, 28 | stdout: "BAM!", 29 | }, 30 | }, 31 | exist: { 32 | [runCmdPath]: true, 33 | }, 34 | } as ma.TaskLibAnswers; 35 | tr.setAnswers(a); 36 | 37 | tr.run(); 38 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/test/runBuildLinux.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | 7 | const tp = path.join(__dirname, "..", "main.js"); 8 | const tr = new mr.TaskMockRunner(tp); 9 | 10 | tr.setInput("tasks", "test"); 11 | 12 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "glnxa64", "run-matlab-command"); 13 | 14 | tr.registerMock("./utils", { 15 | platform: () => "linux", 16 | architecture: () => "x64", 17 | }); 18 | 19 | tr.registerMock("fs", { 20 | chmodSync: () => Promise.resolve(0), 21 | }); 22 | 23 | const a: ma.TaskLibAnswers = { 24 | checkPath: { 25 | [runCmdPath]: true, 26 | }, 27 | exec: { 28 | [runCmdPath + " buildtool test"]: { 29 | code: 0, 30 | stdout: "ran test task", 31 | }, 32 | }, 33 | exist: { 34 | [runCmdPath]: true, 35 | }, 36 | } as ma.TaskLibAnswers; 37 | tr.setAnswers(a); 38 | 39 | tr.run(); 40 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/test/runBuildWindows.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | 7 | const tp = path.join(__dirname, "..", "main.js"); 8 | const tr = new mr.TaskMockRunner(tp); 9 | 10 | tr.setInput("tasks", "test"); 11 | 12 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "win64", "run-matlab-command.exe"); 13 | 14 | tr.registerMock("./utils", { 15 | platform: () => "win32", 16 | architecture: () => "x64", 17 | }); 18 | 19 | tr.registerMock("fs", { 20 | chmodSync: () => Promise.resolve(0), 21 | }); 22 | 23 | const a: ma.TaskLibAnswers = { 24 | checkPath: { 25 | [runCmdPath]: true, 26 | }, 27 | exec: { 28 | [runCmdPath + " buildtool test"]: { 29 | code: 0, 30 | stdout: "ran test task", 31 | }, 32 | }, 33 | exist: { 34 | [runCmdPath]: true, 35 | }, 36 | } as ma.TaskLibAnswers; 37 | tr.setAnswers(a); 38 | 39 | tr.run(); 40 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/test/runBuildWithStartupOptsLinux.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | 7 | const tp = path.join(__dirname, "..", "main.js"); 8 | const tr = new mr.TaskMockRunner(tp); 9 | 10 | tr.setInput("tasks", "test"); 11 | tr.setInput("startupOptions", "-nojvm -nodisplay"); 12 | 13 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "glnxa64", "run-matlab-command"); 14 | 15 | tr.registerMock("./utils", { 16 | platform: () => "linux", 17 | architecture: () => "x64", 18 | }); 19 | 20 | tr.registerMock("fs", { 21 | chmodSync: () => Promise.resolve(0), 22 | }); 23 | 24 | const a: ma.TaskLibAnswers = { 25 | checkPath: { 26 | [runCmdPath]: true, 27 | }, 28 | exec: { 29 | [runCmdPath + " buildtool test -nojvm -nodisplay"]: { 30 | code: 0, 31 | stdout: "ran test task", 32 | }, 33 | }, 34 | exist: { 35 | [runCmdPath]: true, 36 | }, 37 | } as ma.TaskLibAnswers; 38 | tr.setAnswers(a); 39 | 40 | tr.run(); 41 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/test/runBuildWithStartupOptsWindows.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | 7 | const tp = path.join(__dirname, "..", "main.js"); 8 | const tr = new mr.TaskMockRunner(tp); 9 | 10 | tr.setInput("tasks", "test"); 11 | tr.setInput("startupOptions", "-nojvm -nodisplay"); 12 | 13 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "win64", "run-matlab-command.exe"); 14 | 15 | tr.registerMock("./utils", { 16 | platform: () => "win32", 17 | architecture: () => "x64", 18 | }); 19 | 20 | tr.registerMock("fs", { 21 | chmodSync: () => Promise.resolve(0), 22 | }); 23 | 24 | const a: ma.TaskLibAnswers = { 25 | checkPath: { 26 | [runCmdPath]: true, 27 | }, 28 | exec: { 29 | [runCmdPath + " buildtool test -nojvm -nodisplay"]: { 30 | code: 0, 31 | stdout: "ran test task", 32 | }, 33 | }, 34 | exist: { 35 | [runCmdPath]: true, 36 | }, 37 | } as ma.TaskLibAnswers; 38 | tr.setAnswers(a); 39 | 40 | tr.run(); 41 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/test/runDefaultTasks.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | 7 | const tp = path.join(__dirname, "..", "main.js"); 8 | const tr = new mr.TaskMockRunner(tp); 9 | 10 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "glnxa64", "run-matlab-command"); 11 | 12 | tr.registerMock("./utils", { 13 | platform: () => "linux", 14 | architecture: () => "x64", 15 | }); 16 | 17 | tr.registerMock("fs", { 18 | chmodSync: () => Promise.resolve(0), 19 | }); 20 | 21 | const a: ma.TaskLibAnswers = { 22 | checkPath: { 23 | [runCmdPath]: true, 24 | }, 25 | exec: { 26 | [runCmdPath + " buildtool"]: { 27 | code: 0, 28 | stdout: "ran default tasks", 29 | }, 30 | }, 31 | exist: { 32 | [runCmdPath]: true, 33 | }, 34 | } as ma.TaskLibAnswers; 35 | tr.setAnswers(a); 36 | 37 | tr.run(); 38 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/test/suite.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The MathWorks, Inc. 2 | 3 | import * as assert from "assert"; 4 | import * as mt from "azure-pipelines-task-lib/mock-test"; 5 | import * as path from "path"; 6 | 7 | describe("RunMATLABBuild V0 Suite", () => { 8 | it("should succeed running MATLAB build on linux", async () => { 9 | const tp = path.join(__dirname, "runBuildLinux.js"); 10 | const tr = new mt.MockTestRunner(tp); 11 | 12 | await tr.runAsync(); 13 | 14 | assert(tr.succeeded, "should have succeeded"); 15 | assert(tr.stdOutContained("ran test task"), "should have run test task"); 16 | }); 17 | 18 | it("should succeed running MATLAB build on windows", async () => { 19 | const tp = path.join(__dirname, "runBuildWindows.js"); 20 | const tr = new mt.MockTestRunner(tp); 21 | 22 | await tr.runAsync(); 23 | 24 | assert(tr.succeeded, "should have succeeded"); 25 | assert(tr.stdOutContained("ran test task"), "should have run test task"); 26 | }); 27 | 28 | it("should succeed running MATLAB build on linux with startup options", async () => { 29 | const tp = path.join(__dirname, "runBuildWithStartupOptsLinux.js"); 30 | const tr = new mt.MockTestRunner(tp); 31 | 32 | await tr.runAsync(); 33 | 34 | assert(tr.succeeded, "should have succeeded"); 35 | assert(tr.stdOutContained("ran test task"), "should have run test task"); 36 | }); 37 | 38 | it("should succeed running MATLAB build on windows with startup options", async () => { 39 | const tp = path.join(__dirname, "runBuildWithStartupOptsWindows.js"); 40 | const tr = new mt.MockTestRunner(tp); 41 | 42 | await tr.runAsync(); 43 | 44 | assert(tr.succeeded, "should have succeeded"); 45 | assert(tr.stdOutContained("ran test task"), "should have run test task"); 46 | }); 47 | 48 | it("should run default tasks if no tasks input is supplied", async () => { 49 | const tp = path.join(__dirname, "runDefaultTasks.js"); 50 | const tr = new mt.MockTestRunner(tp); 51 | 52 | await tr.runAsync(); 53 | 54 | assert(tr.succeeded, "should have succeeded"); 55 | assert(tr.stdOutContained("ran default tasks"), "should have run default tasks"); 56 | }); 57 | 58 | it("should fail when running build fails", async () => { 59 | const tp = path.join(__dirname, "failRunBuild.js"); 60 | const tr = new mt.MockTestRunner(tp); 61 | 62 | await tr.runAsync(); 63 | 64 | assert(tr.failed, "should have failed"); 65 | assert(tr.stdOutContained("BAM!"), "should have executed build"); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "sourceMap": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v0/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The MathWorks, Inc. 2 | 3 | export function platform() { 4 | return process.platform; 5 | } 6 | 7 | export function architecture() { 8 | return process.arch; 9 | } 10 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v1/.gitignore: -------------------------------------------------------------------------------- 1 | bin -------------------------------------------------------------------------------- /tasks/run-matlab-build/v1/buildtool.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The MathWorks, Inc. 2 | 3 | export interface IRunBuildOptions { 4 | Tasks?: string; 5 | BuildOptions?: string; 6 | } 7 | 8 | export function generateCommand(options: IRunBuildOptions): string { 9 | let buildtoolCommand: string = "buildtool"; 10 | if (options.Tasks) { 11 | buildtoolCommand = buildtoolCommand + " " + options.Tasks; 12 | } 13 | if (options.BuildOptions) { 14 | buildtoolCommand = buildtoolCommand + " " + options.BuildOptions; 15 | } 16 | return buildtoolCommand; 17 | } 18 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v1/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/matlab-azure-devops-extension/20cd6026fc706cc925710c25cd2d7f8528441a3e/tasks/run-matlab-build/v1/icon.png -------------------------------------------------------------------------------- /tasks/run-matlab-build/v1/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as path from "path"; 5 | import * as buildtool from "./buildtool"; 6 | import * as matlab from "./matlab"; 7 | 8 | async function run() { 9 | try { 10 | taskLib.setResourcePath(path.join( __dirname, "task.json")); 11 | const options: buildtool.IRunBuildOptions = { 12 | Tasks: taskLib.getInput("tasks"), 13 | BuildOptions: taskLib.getInput("buildOptions"), 14 | }; 15 | const platform = process.platform; 16 | const architecture = process.arch; 17 | const startupOpts: string | undefined = taskLib.getInput("startupOptions"); 18 | const buildtoolCommand = buildtool.generateCommand(options); 19 | await matlab.runCommand(buildtoolCommand, platform, architecture, startupOpts); 20 | } catch (err) { 21 | taskLib.setResult(taskLib.TaskResult.Failed, (err as Error).message); 22 | } 23 | } 24 | 25 | run(); 26 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v1/matlab.ts: -------------------------------------------------------------------------------- 1 | ../../run-matlab-command/v1/matlab.ts -------------------------------------------------------------------------------- /tasks/run-matlab-build/v1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run-matlab-build", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "npm run getBinDep", 7 | "getBinDep": "../../../scripts/setupdeps.sh v2" 8 | }, 9 | "dependencies": { 10 | "@types/node": "^22.7.5", 11 | "@types/q": "^1.5.4", 12 | "azure-pipelines-task-lib": "5.0.1-preview.0", 13 | "azure-pipelines-tool-lib": "^2.0.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v1/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json", 3 | "id": "c61c6fcc-00f1-4aa0-8513-31aad3340512", 4 | "name": "RunMATLABBuild", 5 | "friendlyName": "Run MATLAB Build", 6 | "description": "Run a MATLAB build using the MATLAB build tool.", 7 | "helpMarkDown": "", 8 | "category": "Build", 9 | "author": "The MathWorks, Inc.", 10 | "version": { 11 | "Major": 1, 12 | "Minor": 0, 13 | "Patch": 0 14 | }, 15 | "inputs": [ 16 | { 17 | "name": "tasks", 18 | "type": "string", 19 | "label": "Tasks", 20 | "required": false, 21 | "defaultValue": "", 22 | "helpMarkDown": "Space-separated list of tasks to run. If not specified, the task runs the default tasks in the file buildfile.m as well as all the tasks on which they depend." 23 | }, 24 | { 25 | "name": "buildOptions", 26 | "type": "string", 27 | "label": "Build Options", 28 | "required": false, 29 | "defaultValue": "", 30 | "helpMarkDown": "Build options for MATLAB build tool." 31 | }, 32 | { 33 | "name": "startupOptions", 34 | "type": "string", 35 | "label": "Startup Options", 36 | "required": false, 37 | "defaultValue": "", 38 | "helpMarkDown": "Startup options for MATLAB." 39 | } 40 | ], 41 | "instanceNameFormat": "Run MATLAB Build", 42 | "execution": { 43 | "Node10": { 44 | "target": "main.js" 45 | }, 46 | "Node16": { 47 | "target": "main.js" 48 | }, 49 | "Node20": { 50 | "target": "main.js" 51 | } 52 | }, 53 | "messages": { 54 | "BuildFailed": "Build failed with nonzero exit code.", 55 | "GeneratingScript": "Generating MATLAB script with content:\n%s", 56 | "FailedToRunCommand": "Failed to run MATLAB command." 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v1/test/buildtool.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The MathWorks, Inc. 2 | 3 | import * as assert from "assert"; 4 | import * as buildtool from "../buildtool"; 5 | 6 | export default function suite() { 7 | describe("command generation", () => { 8 | it("buildtool invocation with unspecified tasks and build options", () => { 9 | const options: buildtool.IRunBuildOptions = { 10 | Tasks: "", 11 | BuildOptions: "", 12 | }; 13 | 14 | const actual = buildtool.generateCommand(options); 15 | assert(actual === "buildtool"); 16 | }); 17 | 18 | it("buildtool invocation with tasks specified", () => { 19 | const options: buildtool.IRunBuildOptions = { 20 | Tasks: "compile test", 21 | }; 22 | 23 | const actual = buildtool.generateCommand(options); 24 | assert(actual === "buildtool compile test"); 25 | }); 26 | 27 | it("buildtool invocation with only build options", () => { 28 | const options: buildtool.IRunBuildOptions = { 29 | Tasks: "", 30 | BuildOptions: "-continueOnFailure -skip check", 31 | }; 32 | 33 | const actual = buildtool.generateCommand(options); 34 | assert(actual === "buildtool -continueOnFailure -skip check"); 35 | }); 36 | 37 | it("buildtool invocation with specified tasks and build options", () => { 38 | const options: buildtool.IRunBuildOptions = { 39 | Tasks: "compile test", 40 | BuildOptions: "-continueOnFailure -skip check", 41 | }; 42 | 43 | const actual = buildtool.generateCommand(options); 44 | assert(actual === "buildtool compile test -continueOnFailure -skip check"); 45 | }); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v1/test/suite.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The MathWorks, Inc. 2 | 3 | import buildtoolTests from "./buildtool.test"; 4 | 5 | describe("RunMATLABBuild V1 Suite", () => { 6 | buildtoolTests(); 7 | }); 8 | -------------------------------------------------------------------------------- /tasks/run-matlab-build/v1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "sourceMap": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/.gitignore: -------------------------------------------------------------------------------- 1 | bin -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/matlab-azure-devops-extension/20cd6026fc706cc925710c25cd2d7f8528441a3e/tasks/run-matlab-command/v0/icon.png -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 5 | import { chmodSync } from "fs"; 6 | import * as fs from "fs"; 7 | import * as path from "path"; 8 | import { v4 as uuidV4 } from "uuid"; 9 | import { architecture, platform } from "./utils"; 10 | 11 | async function run() { 12 | try { 13 | taskLib.setResourcePath(path.join( __dirname, "task.json")); 14 | const command: string = taskLib.getInput("command", true) || ""; 15 | const startupOpts: string | undefined = taskLib.getInput("startupOptions"); 16 | 17 | await runCommand(command, startupOpts); 18 | } catch (err) { 19 | taskLib.setResult(taskLib.TaskResult.Failed, (err as Error).message); 20 | } 21 | } 22 | 23 | async function runCommand(command: string, args?: string) { 24 | // write command to script 25 | console.log(taskLib.loc("GeneratingScript", command)); 26 | taskLib.assertAgent("2.115.0"); 27 | const tempDirectory = taskLib.getVariable("agent.tempDirectory") || ""; 28 | taskLib.checkPath(tempDirectory, `${tempDirectory} (agent.tempDirectory)`); 29 | const scriptName = "command_" + uuidV4().replace(/-/g, "_"); 30 | const scriptPath = path.join(tempDirectory, scriptName + ".m"); 31 | fs.writeFileSync( 32 | scriptPath, 33 | "cd(getenv('MW_ORIG_WORKING_FOLDER'));\n" + command, 34 | { encoding: "utf8" }); 35 | 36 | // run script 37 | if (architecture() !== "x64") { 38 | const msg = `This task is not supported on ${platform()} runners using the ${architecture()} architecture.`; 39 | throw new Error(msg); 40 | } 41 | let ext; 42 | let platformDir; 43 | switch (platform()) { 44 | case "win32": 45 | ext = ".exe"; 46 | platformDir = "win64"; 47 | break; 48 | case "darwin": 49 | ext = ""; 50 | platformDir = "maci64"; 51 | break; 52 | case "linux": 53 | ext = ""; 54 | platformDir = "glnxa64"; 55 | break; 56 | default: 57 | throw new Error(`This task is not supported on ${platform()} runners using the ${architecture()} architecture.`); 58 | } 59 | console.log("========================== Starting Command Output ==========================="); 60 | const binDir = path.join(__dirname, "bin", platformDir); 61 | const runToolPath = path.join(binDir, `run-matlab-command${ext}`); 62 | if (!taskLib.exist(runToolPath)) { 63 | const zipPath = path.join(binDir, "run-matlab-command.zip"); 64 | await toolLib.extractZip(zipPath, binDir); 65 | } 66 | 67 | chmodSync(runToolPath, "777"); 68 | const runTool = taskLib.tool(runToolPath); 69 | runTool.arg("setenv('MW_ORIG_WORKING_FOLDER', cd('" + tempDirectory.replace(/'/g, "''") + "'));" + scriptName); 70 | 71 | if (args) { 72 | runTool.arg(args.split(" ")); 73 | } 74 | 75 | const exitCode = await runTool.exec(); 76 | if (exitCode !== 0) { 77 | throw new Error(taskLib.loc("FailedToRunCommand")); 78 | } 79 | } 80 | 81 | run(); 82 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run-matlab-command", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "npm run getBinDep", 7 | "getBinDep": "../../../scripts/setupdeps.sh v1" 8 | }, 9 | "dependencies": { 10 | "@types/node": "^22.7.5", 11 | "@types/q": "^1.5.4", 12 | "azure-pipelines-task-lib": "5.0.1-preview.0", 13 | "azure-pipelines-tool-lib": "^2.0.7" 14 | }, 15 | "devDependencies": { 16 | "@types/uuid": "^9.0.8" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json", 3 | "id": "28fdff80-51b4-4b6e-83e1-cfcf3f3b25a6", 4 | "name": "RunMATLABCommand", 5 | "friendlyName": "Run MATLAB Command", 6 | "description": "Execute a MATLAB script, function, or statement. MATLAB exits with exit code 0 if the specified script, function, or statement executes successfully without error. Otherwise, MATLAB terminates with a nonzero exit code, which causes the build to fail. You can use the assert or error functions in the command to ensure that builds fail when necessary. When you use this task, all of the required files must be on the MATLAB search path.", 7 | "helpMarkDown": "", 8 | "category": "Build", 9 | "author": "The MathWorks, Inc.", 10 | "version": { 11 | "Major": 0, 12 | "Minor": 1, 13 | "Patch": 0 14 | }, 15 | "inputs": [ 16 | { 17 | "name": "command", 18 | "type": "string", 19 | "label": "Command", 20 | "required": true, 21 | "defaultValue": "", 22 | "helpMarkDown": "Script, function, or statement to execute. If the value of `command` is the name of a MATLAB script or function, do not specify the file extension. If you specify more than one MATLAB command, use a comma or semicolon to separate the commands." 23 | }, 24 | { 25 | "name": "startupOptions", 26 | "type": "string", 27 | "label": "Startup Options", 28 | "required": false, 29 | "defaultValue": "", 30 | "helpMarkDown": "Startup options to pass to MATLAB." 31 | } 32 | ], 33 | "instanceNameFormat": "Run MATLAB Command", 34 | "execution": { 35 | "Node10": { 36 | "target": "main.js" 37 | } 38 | }, 39 | "messages": { 40 | "GeneratingScript": "Generating MATLAB script with content:\n%s", 41 | "FailedToRunCommand": "Failed to run command successfully." 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/test/failRunCommand.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | 7 | const tp = path.join(__dirname, "..", "main.js"); 8 | const tr = new mr.TaskMockRunner(tp); 9 | 10 | tr.setInput("command", "myscript"); 11 | 12 | tr.registerMock("./utils", { 13 | platform: () => "linux", 14 | architecture: () => "x64", 15 | }); 16 | 17 | // create assertAgent and getVariable mocks, support not added in this version of task-lib 18 | import tl = require("azure-pipelines-task-lib/mock-task"); 19 | const tlClone = Object.assign({}, tl); 20 | // @ts-ignore 21 | tlClone.getVariable = (variable: string) => { 22 | if (variable.toLowerCase() === "agent.tempdirectory") { 23 | return "temp/path"; 24 | } 25 | if (variable.toLowerCase() === "system.defaultworkingdirectory") { 26 | return "work/dir"; 27 | } 28 | return null; 29 | }; 30 | // @ts-ignore 31 | tlClone.assertAgent = (variable: string) => { 32 | return; 33 | }; 34 | tr.registerMock("azure-pipelines-task-lib/mock-task", tlClone); 35 | 36 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "glnxa64", "run-matlab-command"); 37 | const a: ma.TaskLibAnswers = { 38 | checkPath: { 39 | [runCmdPath]: true, 40 | "temp/path": true, 41 | }, 42 | exec: { 43 | [runCmdPath + ` setenv('MW_ORIG_WORKING_FOLDER', cd('temp/path'));command_1_2_3`]: { 44 | code: 1, 45 | stdout: "BAM!", 46 | }, 47 | }, 48 | exist: { 49 | [runCmdPath]: true, 50 | }, 51 | } as ma.TaskLibAnswers; 52 | tr.setAnswers(a); 53 | 54 | // mock fs 55 | tr.registerMock("fs", { 56 | chmodSync: () => Promise.resolve(0), 57 | writeFileSync: (filePath: any, contents: any, options: any) => { 58 | // tslint:disable-next-line:no-console 59 | console.log(`writing ${contents} to ${filePath}`); 60 | }, 61 | }); 62 | 63 | // mock uuidv4 64 | tr.registerMock("uuid", { 65 | v4: () => "1-2-3", 66 | }); 67 | 68 | tr.run(); 69 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/test/runCommandLinux.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | 7 | const tp = path.join(__dirname, "..", "main.js"); 8 | const tr = new mr.TaskMockRunner(tp); 9 | 10 | tr.setInput("command", "myscript"); 11 | 12 | tr.registerMock("./utils", { 13 | platform: () => "linux", 14 | architecture: () => "x64", 15 | }); 16 | 17 | // create assertAgent and getVariable mocks, support not added in this version of task-lib 18 | import tl = require("azure-pipelines-task-lib/mock-task"); 19 | const tlClone = Object.assign({}, tl); 20 | // @ts-ignore 21 | tlClone.getVariable = (variable: string) => { 22 | if (variable.toLowerCase() === "agent.tempdirectory") { 23 | return "temp's/path"; 24 | } 25 | if (variable.toLowerCase() === "system.defaultworkingdirectory") { 26 | return "work's/dir"; 27 | } 28 | return null; 29 | }; 30 | // @ts-ignore 31 | tlClone.assertAgent = (variable: string) => { 32 | return; 33 | }; 34 | tr.registerMock("azure-pipelines-task-lib/mock-task", tlClone); 35 | 36 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "glnxa64", "run-matlab-command"); 37 | const a: ma.TaskLibAnswers = { 38 | checkPath: { 39 | [runCmdPath]: true, 40 | "temp's/path": true, 41 | }, 42 | exec: { 43 | [runCmdPath + " setenv('MW_ORIG_WORKING_FOLDER', cd('temp''s/path'));command_1_2_3"]: { 44 | code: 0, 45 | stdout: "hello world", 46 | }, 47 | }, 48 | exist: { 49 | [runCmdPath]: true, 50 | }, 51 | } as ma.TaskLibAnswers; 52 | tr.setAnswers(a); 53 | 54 | // mock fs 55 | tr.registerMock("fs", { 56 | chmodSync: () => Promise.resolve(0), 57 | writeFileSync: (filePath: any, contents: any, options: any) => { 58 | // tslint:disable-next-line:no-console 59 | console.log(`writing ${contents} to ${filePath}`); 60 | }, 61 | }); 62 | 63 | // mock uuidv4 64 | tr.registerMock("uuid", { 65 | v4: () => "1-2-3", 66 | }); 67 | 68 | tr.run(); 69 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/test/runCommandWindows.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | 7 | const tp = path.join(__dirname, "..", "main.js"); 8 | const tr = new mr.TaskMockRunner(tp); 9 | 10 | tr.setInput("command", "myscript"); 11 | 12 | tr.registerMock("./utils", { 13 | platform: () => "win32", 14 | architecture: () => "x64", 15 | }); 16 | 17 | // create assertAgent and getVariable mocks, support not added in this version of task-lib 18 | import tl = require("azure-pipelines-task-lib/mock-task"); 19 | const tlClone = Object.assign({}, tl); 20 | // @ts-ignore 21 | tlClone.getVariable = (variable: string) => { 22 | if (variable.toLowerCase() === "agent.tempdirectory") { 23 | return "temp's\\path"; 24 | } 25 | if (variable.toLowerCase() === "system.defaultworkingdirectory") { 26 | return "work's\\dir"; 27 | } 28 | return null; 29 | }; 30 | // @ts-ignore 31 | tlClone.assertAgent = (variable: string) => { 32 | return; 33 | }; 34 | tr.registerMock("azure-pipelines-task-lib/mock-task", tlClone); 35 | 36 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "win64", "run-matlab-command.exe"); 37 | const a: ma.TaskLibAnswers = { 38 | checkPath: { 39 | [runCmdPath]: true, 40 | "temp's\\path": true, 41 | }, 42 | exec: { 43 | [runCmdPath + " setenv('MW_ORIG_WORKING_FOLDER', cd('temp''s\\path'));command_1_2_3"]: { 44 | code: 0, 45 | stdout: "hello world", 46 | }, 47 | }, 48 | exist: { 49 | [runCmdPath]: true, 50 | }, 51 | } as ma.TaskLibAnswers; 52 | tr.setAnswers(a); 53 | 54 | // mock fs 55 | tr.registerMock("fs", { 56 | chmodSync: () => Promise.resolve(0), 57 | writeFileSync: (filePath: any, contents: any, options: any) => { 58 | // tslint:disable-next-line:no-console 59 | console.log(`writing ${contents} to ${filePath}`); 60 | }, 61 | }); 62 | 63 | // mock uuidv4 64 | tr.registerMock("uuid", { 65 | v4: () => "1-2-3", 66 | }); 67 | 68 | tr.run(); 69 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/test/runCommandWithArgsLinux.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | 7 | const tp = path.join(__dirname, "..", "main.js"); 8 | const tr = new mr.TaskMockRunner(tp); 9 | 10 | const flags = "-nojvm -nodesktop -logfile file"; 11 | tr.setInput("command", "myscript"); 12 | tr.setInput("startupOptions", flags); 13 | 14 | tr.registerMock("./utils", { 15 | platform: () => "linux", 16 | architecture: () => "x64", 17 | }); 18 | 19 | // create assertAgent and getVariable mocks, support not added in this version of task-lib 20 | import tl = require("azure-pipelines-task-lib/mock-task"); 21 | const tlClone = Object.assign({}, tl); 22 | // @ts-ignore 23 | tlClone.getVariable = (variable: string) => { 24 | if (variable.toLowerCase() === "agent.tempdirectory") { 25 | return "temp's/path"; 26 | } 27 | if (variable.toLowerCase() === "system.defaultworkingdirectory") { 28 | return "work's/dir"; 29 | } 30 | return null; 31 | }; 32 | // @ts-ignore 33 | tlClone.assertAgent = (variable: string) => { 34 | return; 35 | }; 36 | tr.registerMock("azure-pipelines-task-lib/mock-task", tlClone); 37 | 38 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "glnxa64", "run-matlab-command"); 39 | const a: ma.TaskLibAnswers = { 40 | checkPath: { 41 | [runCmdPath]: true, 42 | "temp's/path": true, 43 | }, 44 | exec: { 45 | [runCmdPath + " setenv('MW_ORIG_WORKING_FOLDER', cd('temp''s/path'));command_1_2_3" + " " + flags]: { 46 | code: 0, 47 | stdout: "hello world", 48 | }, 49 | }, 50 | exist: { 51 | [runCmdPath]: true, 52 | }, 53 | } as ma.TaskLibAnswers; 54 | tr.setAnswers(a); 55 | 56 | // mock fs 57 | tr.registerMock("fs", { 58 | chmodSync: () => Promise.resolve(0), 59 | writeFileSync: (filePath: any, contents: any, options: any) => { 60 | // tslint:disable-next-line:no-console 61 | console.log(`writing ${contents} to ${filePath}`); 62 | }, 63 | }); 64 | 65 | // mock uuidv4 66 | tr.registerMock("uuid", { 67 | v4: () => "1-2-3", 68 | }); 69 | 70 | tr.run(); 71 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/test/runCommandWithArgsWindows.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | 7 | const tp = path.join(__dirname, "..", "main.js"); 8 | const tr = new mr.TaskMockRunner(tp); 9 | 10 | const flags = "-nojvm -nodesktop -logfile file"; 11 | tr.setInput("command", "myscript"); 12 | tr.setInput("startupOptions", flags); 13 | 14 | tr.registerMock("./utils", { 15 | platform: () => "win32", 16 | architecture: () => "x64", 17 | }); 18 | 19 | // create assertAgent and getVariable mocks, support not added in this version of task-lib 20 | import tl = require("azure-pipelines-task-lib/mock-task"); 21 | const tlClone = Object.assign({}, tl); 22 | // @ts-ignore 23 | tlClone.getVariable = (variable: string) => { 24 | if (variable.toLowerCase() === "agent.tempdirectory") { 25 | return "temp's\\path"; 26 | } 27 | if (variable.toLowerCase() === "system.defaultworkingdirectory") { 28 | return "work's\\dir"; 29 | } 30 | return null; 31 | }; 32 | // @ts-ignore 33 | tlClone.assertAgent = (variable: string) => { 34 | return; 35 | }; 36 | tr.registerMock("azure-pipelines-task-lib/mock-task", tlClone); 37 | 38 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "win64", "run-matlab-command.exe"); 39 | const a: ma.TaskLibAnswers = { 40 | checkPath: { 41 | [runCmdPath]: true, 42 | "temp's\\path": true, 43 | }, 44 | exec: { 45 | [runCmdPath + " setenv('MW_ORIG_WORKING_FOLDER', cd('temp''s\\path'));command_1_2_3" + " " + flags]: { 46 | code: 0, 47 | stdout: "hello world", 48 | }, 49 | }, 50 | exist: { 51 | [runCmdPath]: true, 52 | }, 53 | } as ma.TaskLibAnswers; 54 | tr.setAnswers(a); 55 | 56 | // mock fs 57 | tr.registerMock("fs", { 58 | chmodSync: () => Promise.resolve(0), 59 | writeFileSync: (filePath: any, contents: any, options: any) => { 60 | // tslint:disable-next-line:no-console 61 | console.log(`writing ${contents} to ${filePath}`); 62 | }, 63 | }); 64 | 65 | // mock uuidv4 66 | tr.registerMock("uuid", { 67 | v4: () => "1-2-3", 68 | }); 69 | 70 | tr.run(); 71 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/test/suite.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The MathWorks, Inc. 2 | 3 | import * as assert from "assert"; 4 | import * as mt from "azure-pipelines-task-lib/mock-test"; 5 | import * as path from "path"; 6 | 7 | describe("RunMATLABCommand V0 Suite", () => { 8 | it("should succeed running MATLAB command on linux", async () => { 9 | const tp = path.join(__dirname, "runCommandLinux.js"); 10 | const tr = new mt.MockTestRunner(tp); 11 | 12 | await tr.runAsync(); 13 | 14 | assert(tr.succeeded, "should have succeeded"); 15 | assert(tr.stdOutContained("hello world"), "should have executed command"); 16 | }); 17 | 18 | it("should succeed running MATLAB command with startup options on linux", async () => { 19 | const tp = path.join(__dirname, "runCommandWithArgsLinux.js"); 20 | const tr = new mt.MockTestRunner(tp); 21 | 22 | await tr.runAsync(); 23 | 24 | assert(tr.succeeded, "should have succeeded"); 25 | assert(tr.stdOutContained("hello world"), "should have executed command"); 26 | }); 27 | 28 | it("should succeed running MATLAB command on windows", async () => { 29 | const tp = path.join(__dirname, "runCommandWindows.js"); 30 | const tr = new mt.MockTestRunner(tp); 31 | 32 | await tr.runAsync(); 33 | 34 | assert(tr.succeeded, "should have succeeded"); 35 | assert(tr.stdOutContained("hello world"), "should have executed command"); 36 | }); 37 | 38 | it("should succeed running MATLAB command with startup options on windows", async () => { 39 | const tp = path.join(__dirname, "runCommandWithArgsWindows.js"); 40 | const tr = new mt.MockTestRunner(tp); 41 | 42 | await tr.runAsync(); 43 | 44 | assert(tr.succeeded, "should have succeeded"); 45 | assert(tr.stdOutContained("hello world"), "should have executed command"); 46 | }); 47 | 48 | it("should fail when running command fails", async () => { 49 | const tp = path.join(__dirname, "failRunCommand.js"); 50 | const tr = new mt.MockTestRunner(tp); 51 | 52 | await tr.runAsync(); 53 | 54 | assert(tr.failed, "should have failed"); 55 | assert(tr.stdOutContained("BAM!"), "should have executed command"); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "sourceMap": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v0/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | export function platform() { 4 | return process.platform; 5 | } 6 | 7 | export function architecture() { 8 | return process.arch; 9 | } 10 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v1/.gitignore: -------------------------------------------------------------------------------- 1 | bin -------------------------------------------------------------------------------- /tasks/run-matlab-command/v1/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/matlab-azure-devops-extension/20cd6026fc706cc925710c25cd2d7f8528441a3e/tasks/run-matlab-command/v1/icon.png -------------------------------------------------------------------------------- /tasks/run-matlab-command/v1/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as path from "path"; 5 | import * as matlab from "./matlab"; 6 | 7 | async function run() { 8 | try { 9 | taskLib.setResourcePath(path.join( __dirname, "task.json")); 10 | const command: string = taskLib.getInput("command", true) || ""; 11 | const startupOpts: string | undefined = taskLib.getInput("startupOptions"); 12 | 13 | await matlab.runCommand(command, process.platform, process.arch, startupOpts); 14 | } catch (err) { 15 | taskLib.setResult(taskLib.TaskResult.Failed, (err as Error).message); 16 | } 17 | } 18 | 19 | run(); 20 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v1/matlab.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 5 | import * as fs from "fs"; 6 | import * as path from "path"; 7 | import { v4 as uuidV4 } from "uuid"; 8 | 9 | export async function runCommand(command: string, platform: string, architecture: string, args?: string) { 10 | // write command to script 11 | console.log(taskLib.loc("GeneratingScript", command)); 12 | taskLib.assertAgent("2.115.0"); 13 | const tempDirectory = taskLib.getVariable("agent.tempDirectory") || ""; 14 | taskLib.checkPath(tempDirectory, `${tempDirectory} (agent.tempDirectory)`); 15 | const scriptName = "command_" + uuidV4().replace(/-/g, "_"); 16 | const scriptPath = path.join(tempDirectory, scriptName + ".m"); 17 | fs.writeFileSync( 18 | scriptPath, 19 | "cd(getenv('MW_ORIG_WORKING_FOLDER'));\n" + command, 20 | { encoding: "utf8" }, 21 | ); 22 | 23 | console.log("========================== Starting Command Output ==========================="); 24 | const runToolPath = await getRunMATLABCommandPath(platform, architecture); 25 | fs.chmodSync(runToolPath, "777"); 26 | const runTool = taskLib.tool(runToolPath); 27 | runTool.arg("setenv('MW_ORIG_WORKING_FOLDER', cd('" + tempDirectory.replace(/'/g, "''") + "'));" + scriptName); 28 | 29 | if (args) { 30 | runTool.arg(args.split(" ")); 31 | } 32 | 33 | const exitCode = await runTool.exec(); 34 | if (exitCode !== 0) { 35 | throw new Error(taskLib.loc("FailedToRunCommand")); 36 | } 37 | } 38 | 39 | export async function getRunMATLABCommandPath(platform: string, architecture: string): Promise { 40 | if (architecture !== "x64" && !(platform === "darwin" && architecture === "arm64")) { 41 | const msg = `This task is not supported on ${platform} runners using the ${architecture} architecture.`; 42 | return Promise.reject(Error(msg)); 43 | } 44 | let ext; 45 | let platformDir; 46 | switch (platform) { 47 | case "win32": 48 | ext = ".exe"; 49 | platformDir = "win64"; 50 | break; 51 | case "darwin": 52 | ext = ""; 53 | if (architecture === "x64") { 54 | platformDir = "maci64"; 55 | } else { 56 | platformDir = "maca64"; 57 | } 58 | break; 59 | case "linux": 60 | ext = ""; 61 | platformDir = "glnxa64"; 62 | break; 63 | default: 64 | const msg = `This task is not supported on ${platform} runners using the ${architecture} architecture.`; 65 | return Promise.reject(Error(msg)); 66 | } 67 | 68 | const binDir = path.join(__dirname, "bin", platformDir); 69 | const rmcPath = path.join(binDir, `run-matlab-command${ext}`); 70 | if (!taskLib.exist(rmcPath)) { 71 | const zipPath = path.join(binDir, "run-matlab-command.zip"); 72 | await toolLib.extractZip(zipPath, binDir); 73 | } 74 | return Promise.resolve(rmcPath); 75 | } 76 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run-matlab-command", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "npm run getBinDep", 7 | "getBinDep": "../../../scripts/setupdeps.sh v2" 8 | }, 9 | "dependencies": { 10 | "@types/node": "^22.7.5", 11 | "@types/q": "^1.5.4", 12 | "azure-pipelines-task-lib": "5.0.1-preview.0", 13 | "azure-pipelines-tool-lib": "^2.0.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v1/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json", 3 | "id": "28fdff80-51b4-4b6e-83e1-cfcf3f3b25a6", 4 | "name": "RunMATLABCommand", 5 | "friendlyName": "Run MATLAB Command", 6 | "description": "Run MATLAB scripts, functions, and statements.", 7 | "helpMarkDown": "", 8 | "category": "Build", 9 | "author": "The MathWorks, Inc.", 10 | "version": { 11 | "Major": 1, 12 | "Minor": 0, 13 | "Patch": 0 14 | }, 15 | "inputs": [ 16 | { 17 | "name": "command", 18 | "type": "string", 19 | "label": "Command", 20 | "required": true, 21 | "defaultValue": "", 22 | "helpMarkDown": "Script, function, or statement to execute." 23 | }, 24 | { 25 | "name": "startupOptions", 26 | "type": "string", 27 | "label": "Startup Options", 28 | "required": false, 29 | "defaultValue": "", 30 | "helpMarkDown": "Startup options for MATLAB." 31 | } 32 | ], 33 | "instanceNameFormat": "Run MATLAB Command", 34 | "execution": { 35 | "Node10": { 36 | "target": "main.js" 37 | }, 38 | "Node16": { 39 | "target": "main.js" 40 | }, 41 | "Node20": { 42 | "target": "main.js" 43 | } 44 | }, 45 | "messages": { 46 | "GeneratingScript": "Generating MATLAB script with content:\n%s", 47 | "FailedToRunCommand": "Failed to run MATLAB command." 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v1/test/matlab.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The MathWorks, Inc. 2 | 3 | import * as assert from "assert"; 4 | import * as taskLib from "azure-pipelines-task-lib/task"; 5 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 6 | import * as fs from "fs"; 7 | import * as path from "path"; 8 | import * as sinon from "sinon"; 9 | import * as matlab from "../matlab"; 10 | 11 | export default function suite() { 12 | describe("matlab.ts test suite", () => { 13 | describe("run command", () => { 14 | let stubWriteFileSync: sinon.SinonStub; 15 | let stubChmodSync: sinon.SinonStub; 16 | let stubCheckPath: sinon.SinonStub; 17 | let stubGetVariable: sinon.SinonStub; 18 | let stubTool: sinon.SinonStub; 19 | let stubExtractZip: sinon.SinonStub; 20 | let stubExist: sinon.SinonStub; 21 | let argsCapture: string[]; 22 | let execReturn: Promise; 23 | const platform = "linux"; 24 | const architecture = "x64"; 25 | 26 | before(() => { 27 | taskLib.setResourcePath(path.join( __dirname, "..", "task.json")); 28 | }); 29 | 30 | beforeEach(() => { 31 | stubWriteFileSync = sinon.stub(fs, "writeFileSync"); 32 | stubChmodSync = sinon.stub(fs, "chmodSync"); 33 | stubCheckPath = sinon.stub(taskLib, "checkPath"); 34 | stubGetVariable = sinon.stub(taskLib, "getVariable"); 35 | stubExtractZip = sinon.stub(toolLib, "extractZip"); 36 | stubTool = sinon.stub(taskLib, "tool"); 37 | // used to check if run-matlab-command has already been unzipped 38 | stubExist = sinon.stub(taskLib, "exist").get( 39 | () => (s: string) => false, 40 | ); 41 | argsCapture = []; 42 | execReturn = Promise.resolve(0); 43 | stubTool.callsFake((t) => { 44 | return { 45 | arg: (a: string) => { 46 | argsCapture.push(a); 47 | }, 48 | exec: () => { 49 | return execReturn; 50 | }, 51 | }; 52 | }); 53 | }); 54 | 55 | afterEach(() => { 56 | stubWriteFileSync.restore(); 57 | stubChmodSync.restore(); 58 | stubCheckPath.restore(); 59 | stubGetVariable.restore(); 60 | stubExtractZip.restore(); 61 | stubExist.restore(); 62 | stubTool.restore(); 63 | }); 64 | 65 | it("ideally works", async () => { 66 | await assert.doesNotReject(async () => await matlab.runCommand("myscript", platform, architecture)); 67 | // extract run-matlab-command binary 68 | assert(stubExtractZip.callCount === 1); 69 | // calls with myscript command as the only arg 70 | assert(argsCapture.length === 1); 71 | }); 72 | 73 | it("ideally works with arguments", async () => { 74 | await assert.doesNotReject( 75 | matlab.runCommand("myscript", platform, architecture, "-nojvm -logfile file"), 76 | ); 77 | // extract run-matlab-command binary 78 | assert(stubExtractZip.callCount === 1); 79 | // calls with myscript command and startup options 80 | assert(argsCapture.length === 2); 81 | // 3 startup options 82 | assert(argsCapture[1].length === 3); 83 | assert(argsCapture[1].includes("-nojvm")); 84 | assert(argsCapture[1].includes("-logfile")); 85 | assert(argsCapture[1].includes("file")); 86 | }); 87 | 88 | it("does not unzip if run-matlab-command is already there", async () => { 89 | // return true becuase run-matlab-command has already been unzipped 90 | stubExist = sinon.stub(taskLib, "exist").get( 91 | () => (s: string) => true, 92 | ); 93 | await assert.doesNotReject(async () => await matlab.runCommand("myscript", platform, architecture)); 94 | // check that unzip is skipped if run-matlab-command binary exists 95 | assert(stubExtractZip.callCount === 0); 96 | // calls with myscript command as the only arg 97 | assert(argsCapture.length === 1); 98 | }); 99 | 100 | it("fails when MATLAB returns a non-zero exit code", async () => { 101 | // return non-zero exit code 102 | execReturn = Promise.resolve(1); 103 | await assert.rejects(matlab.runCommand("myscript", platform, architecture)); 104 | }); 105 | 106 | it("fails when chmod fails", async () => { 107 | stubChmodSync.throws("BAM!"); 108 | await assert.rejects(matlab.runCommand("myscript", platform, architecture)); 109 | }); 110 | 111 | it("fails when the temporary directory doesn't exist", async () => { 112 | stubCheckPath.throws("BAM!"); 113 | await assert.rejects(matlab.runCommand("myscript", platform, architecture)); 114 | }); 115 | 116 | it("fails when there's an error writing to the file", async () => { 117 | stubWriteFileSync.throws("BAM!"); 118 | await assert.rejects(matlab.runCommand("myscript", platform, architecture)); 119 | }); 120 | }); 121 | 122 | describe("ci bin helper path", () => { 123 | let stubExtractZip: sinon.SinonStub; 124 | 125 | beforeEach(() => { 126 | stubExtractZip = sinon.stub(toolLib, "extractZip"); 127 | }); 128 | 129 | afterEach(() => { 130 | stubExtractZip.restore(); 131 | }); 132 | 133 | const testBin = (platform: string, architecture: string, subdirectory: string, ext: string) => { 134 | it(`considers the appropriate rmc bin on ${platform} ${architecture}`, async () => { 135 | const p = await matlab.getRunMATLABCommandPath(platform, architecture); 136 | // unzips run-matlab-command binary 137 | assert(stubExtractZip.callCount === 1); 138 | assert(path.extname(p) === ext); 139 | assert(p.includes(subdirectory)); 140 | }); 141 | }; 142 | 143 | testBin("linux", "x64", "glnxa64", ""); 144 | testBin("win32", "x64", "win64", ".exe"); 145 | testBin("darwin", "x64", "maci64", ""); 146 | testBin("darwin", "arm64", "maca64", ""); 147 | 148 | it("errors on unsupported platform", () => { 149 | assert.rejects(async () => await matlab.getRunMATLABCommandPath("sunos", "x64")); 150 | }); 151 | 152 | it("errors on unsupported architecture", () => { 153 | assert.rejects(async () => await matlab.getRunMATLABCommandPath("linux", "x86")); 154 | }); 155 | }); 156 | }); 157 | } 158 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v1/test/suite.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The MathWorks, Inc. 2 | 3 | import matlabTests from "./matlab.test"; 4 | 5 | describe("RunMATLABCommand V1 Suite", () => { 6 | matlabTests(); 7 | }); 8 | -------------------------------------------------------------------------------- /tasks/run-matlab-command/v1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "sourceMap": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | scriptgen -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/matlab-azure-devops-extension/20cd6026fc706cc925710c25cd2d7f8528441a3e/tasks/run-matlab-tests/v0/icon.png -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as toolLib from "azure-pipelines-tool-lib/tool"; 5 | import { chmodSync } from "fs"; 6 | import * as path from "path"; 7 | import { architecture, platform } from "./utils"; 8 | 9 | async function run() { 10 | try { 11 | taskLib.setResourcePath(path.join( __dirname, "task.json")); 12 | const options: IRunTestsOptions = { 13 | JUnitTestResults: taskLib.getInput("testResultsJUnit"), 14 | CoberturaCodeCoverage: taskLib.getInput("codeCoverageCobertura"), 15 | SourceFolder: taskLib.getInput("sourceFolder"), 16 | SelectByFolder: taskLib.getInput("selectByFolder"), 17 | SelectByTag: taskLib.getInput("selectByTag"), 18 | CoberturaModelCoverage: taskLib.getInput("modelCoverageCobertura"), 19 | SimulinkTestResults: taskLib.getInput("testResultsSimulinkTest"), 20 | PDFTestReport: taskLib.getInput("testResultsPDF"), 21 | Strict: taskLib.getBoolInput("strict"), 22 | UseParallel: taskLib.getBoolInput("useParallel"), 23 | OutputDetail: taskLib.getInput("outputDetail"), 24 | LoggingLevel: taskLib.getInput("loggingLevel")}; 25 | const startupOpts: string | undefined = taskLib.getInput("startupOptions"); 26 | await runTests(options, startupOpts); 27 | } catch (err) { 28 | taskLib.setResult(taskLib.TaskResult.Failed, (err as Error).message); 29 | } 30 | } 31 | 32 | async function runTests(options: IRunTestsOptions, args?: string) { 33 | if (architecture() !== "x64") { 34 | const msg = `This task is not supported on ${platform()} runners using the ${architecture()} architecture.`; 35 | throw new Error(msg); 36 | } 37 | let ext; 38 | let platformDir; 39 | switch (platform()) { 40 | case "win32": 41 | ext = ".exe"; 42 | platformDir = "win64"; 43 | break; 44 | case "darwin": 45 | ext = ""; 46 | platformDir = "maci64"; 47 | break; 48 | case "linux": 49 | ext = ""; 50 | platformDir = "glnxa64"; 51 | break; 52 | default: 53 | throw new Error(`This task is not supported on ${platform()} runners using the ${architecture()} architecture.`); 54 | } 55 | const binDir = path.join(__dirname, "bin", platformDir); 56 | const runToolPath = path.join(binDir, `run-matlab-command${ext}`); 57 | if (!taskLib.exist(runToolPath)) { 58 | const zipPath = path.join(binDir, "run-matlab-command.zip"); 59 | await toolLib.extractZip(zipPath, binDir); 60 | } 61 | 62 | chmodSync(runToolPath, "777"); 63 | const runTool = taskLib.tool(runToolPath); 64 | runTool.arg(`addpath('${path.join(__dirname, "scriptgen")}');` + 65 | `testScript = genscript('Test',` + 66 | `'JUnitTestResults','${options.JUnitTestResults || ""}',` + 67 | `'CoberturaCodeCoverage','${options.CoberturaCodeCoverage || ""}',` + 68 | `'SourceFolder','${options.SourceFolder || ""}',` + 69 | `'SelectByFolder','${options.SelectByFolder || ""}',` + 70 | `'SelectByTag','${options.SelectByTag || ""}',` + 71 | `'CoberturaModelCoverage','${options.CoberturaModelCoverage || ""}',` + 72 | `'SimulinkTestResults','${options.SimulinkTestResults || ""}',` + 73 | `'PDFTestReport','${options.PDFTestReport || ""}',` + 74 | `'Strict',${options.Strict || false},` + 75 | `'UseParallel',${options.UseParallel || false},` + 76 | `'OutputDetail','${options.OutputDetail || ""}',` + 77 | `'LoggingLevel','${options.LoggingLevel || ""}');` + 78 | `disp('Running MATLAB script with contents:');` + 79 | `disp(testScript.Contents);` + 80 | `fprintf('__________\\n\\n');` + 81 | `run(testScript);`); 82 | 83 | if (args) { 84 | runTool.arg(args.split(" ")); 85 | } 86 | 87 | const exitCode = await runTool.exec(); 88 | if (exitCode !== 0) { 89 | throw new Error(taskLib.loc("FailedToRunTests")); 90 | } 91 | } 92 | 93 | interface IRunTestsOptions { 94 | JUnitTestResults?: string; 95 | CoberturaCodeCoverage?: string; 96 | SourceFolder?: string; 97 | SelectByFolder?: string; 98 | SelectByTag?: string; 99 | CoberturaModelCoverage?: string; 100 | SimulinkTestResults?: string; 101 | PDFTestReport?: string; 102 | Strict?: boolean; 103 | UseParallel?: boolean; 104 | OutputDetail?: string; 105 | LoggingLevel?: string; 106 | } 107 | 108 | run(); 109 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run-matlab-command", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "npm run getBinDep && npm run getScriptgenDep", 7 | "getBinDep": "../../../scripts/setupdeps.sh v1", 8 | "getScriptgenDep": "wget -q https://ssd.mathworks.com/supportfiles/ci/matlab-script-generator/v0/matlab-script-generator.zip -O scriptgen.zip; unzip -qod scriptgen scriptgen.zip; rm scriptgen.zip" 9 | }, 10 | "dependencies": { 11 | "@types/node": "^22.7.5", 12 | "@types/q": "^1.5.4", 13 | "azure-pipelines-task-lib": "5.0.1-preview.0", 14 | "azure-pipelines-tool-lib": "^2.0.7" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json", 3 | "id": "d9f28863-c9b0-4133-9cb8-a6d4744f30ef", 4 | "name": "RunMATLABTests", 5 | "friendlyName": "Run MATLAB Tests", 6 | "description": "Run all tests in a MATLAB project and generate test artifacts. MATLAB includes any files in your project that have a Test label. If your pipeline does not leverage a MATLAB project, then MATLAB includes all tests in the the root of your repository including its subfolders.", 7 | "helpMarkDown": "", 8 | "category": "Test", 9 | "author": "The MathWorks, Inc.", 10 | "version": { 11 | "Major": 0, 12 | "Minor": 1, 13 | "Patch": 0 14 | }, 15 | "groups": [ 16 | { 17 | "name": "testArtifacts", 18 | "displayName": "Generate Test Artifacts" 19 | }, 20 | { 21 | "name": "coverageArtifacts", 22 | "displayName": "Generate Coverage Artifacts" 23 | }, 24 | { 25 | "name": "filterTests", 26 | "displayName": "Filter Tests" 27 | }, 28 | { 29 | "name": "customizeTestRun", 30 | "displayName": "Customize Test Run" 31 | } 32 | ], 33 | "inputs": [ 34 | { 35 | "name": "codeCoverageCobertura", 36 | "type": "string", 37 | "label": "Cobertura code coverage", 38 | "defaultValue": "", 39 | "required": false, 40 | "groupName": "coverageArtifacts", 41 | "helpMarkDown": "Path to write code coverage report in Cobertura XML format." 42 | }, 43 | { 44 | "name": "modelCoverageCobertura", 45 | "type": "string", 46 | "label": "Cobertura model coverage", 47 | "defaultValue": "", 48 | "groupName": "coverageArtifacts", 49 | "required": false, 50 | "helpMarkDown": "Path to write model coverage report in Cobertura XML format (requires Simulink Coverage™ license and is supported in MATLAB R2018b or later)." 51 | }, 52 | { 53 | "name": "testResultsSimulinkTest", 54 | "type": "string", 55 | "label": "Simulink test results", 56 | "defaultValue": "", 57 | "groupName": "testArtifacts", 58 | "required": false, 59 | "helpMarkDown": "Path to export Simulink Test Manager results in MLDATX format (requires Simulink Test license and is supported in MATLAB R2019a or later)." 60 | }, 61 | { 62 | "name": "testResultsPDF", 63 | "type": "string", 64 | "label": "PDF test report ", 65 | "defaultValue": "", 66 | "groupName": "testArtifacts", 67 | "required": false, 68 | "helpMarkDown": "Path to write test results report in PDF format (requires MATLAB R2020b or later on macOS platforms)." 69 | }, 70 | { 71 | "name": "selectByFolder", 72 | "type": "string", 73 | "label": "By folder", 74 | "defaultValue": "", 75 | "groupName": "filterTests", 76 | "required": false, 77 | "helpMarkDown": "Locations of the folders used to select test suite elements, relative to the project root folder. To create a test suite, the task uses only the tests in the specified folders and their subfolders. You can specify multiple folders using a colon-separated or a semicolon-separated list." 78 | }, 79 | { 80 | "name": "selectByTag", 81 | "type": "string", 82 | "label": "By tag", 83 | "defaultValue": "", 84 | "groupName": "filterTests", 85 | "required": false, 86 | "helpMarkDown": "Test tag used to select test suite elements. To create a test suite, the task uses only the test elements with the specified tag." 87 | }, 88 | { 89 | "name": "sourceFolder", 90 | "type": "string", 91 | "label": "Source folder", 92 | "defaultValue": "", 93 | "required": false, 94 | "helpMarkDown": "Location of the folder containing source code, relative to the project root folder. The specified folder and its subfolders are added to the top of the MATLAB search path. To generate a code coverage report, MATLAB uses only the source code in the specified folder and its subfolders. You can specify multiple folders using a colon-separated or a semicolon-separated list." 95 | }, 96 | { 97 | "name": "testResultsJUnit", 98 | "type": "string", 99 | "label": "JUnit-style test results", 100 | "defaultValue": "", 101 | "groupName": "testArtifacts", 102 | "required": false, 103 | "helpMarkDown": "Path to write test results report in JUnit XML format." 104 | }, 105 | { 106 | "name": "strict", 107 | "type": "boolean", 108 | "label": "Strict", 109 | "defaultValue": false, 110 | "required": false, 111 | "groupName": "customizeTestRun", 112 | "helpMarkDown": "Whether to apply strict checks when running the tests. For example, the task generates a qualification failure if a test issues a warning." 113 | }, 114 | { 115 | "name": "useParallel", 116 | "type": "boolean", 117 | "label": "Use Parallel", 118 | "defaultValue": false, 119 | "required": false, 120 | "groupName": "customizeTestRun", 121 | "helpMarkDown": "Whether to run tests in parallel on a self-hosted agent (requires Parallel Computing Toolbox). This feature might not be compatible with certain arguments, in which case, tests run in serial regardless of the specified value." 122 | }, 123 | { 124 | "name": "outputDetail", 125 | "type": "string", 126 | "label": "Output Detail", 127 | "defaultValue": "", 128 | "required": false, 129 | "groupName": "customizeTestRun", 130 | "helpMarkDown": "Display level for event details produced by the test run." 131 | }, 132 | { 133 | "name": "loggingLevel", 134 | "type": "string", 135 | "label": "Logging Level", 136 | "defaultValue": "", 137 | "required": false, 138 | "groupName": "customizeTestRun", 139 | "helpMarkDown": "Maximum verbosity level for logged diagnostics included for the test run." 140 | }, 141 | { 142 | "name": "startupOptions", 143 | "type": "string", 144 | "label": "Startup Options", 145 | "required": false, 146 | "defaultValue": "", 147 | "helpMarkDown": "Startup options to pass to MATLAB." 148 | } 149 | ], 150 | "instanceNameFormat": "Run MATLAB Tests", 151 | "execution": { 152 | "Node10": { 153 | "target": "main.js" 154 | } 155 | }, 156 | "messages": { 157 | "FailedToRunTests": "Failed to run tests successfully." 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/test/common.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | import * as path from "path"; 4 | 5 | export function runCmdArg( 6 | junit: string, 7 | cobertura: string, 8 | source: string, 9 | selectByFolder: string, 10 | selectByTag: string, 11 | modelCoverageCobertura: string, 12 | testResultsSimulinkTest: string, 13 | testResultsPDF: string, 14 | strict: boolean, 15 | useParallel: boolean, 16 | outputDetail: string, 17 | loggingLevel: string, 18 | ) { 19 | return "addpath('" + path.join(path.dirname(__dirname), "scriptgen") + "');" + 20 | "testScript = genscript('Test'," + 21 | "'JUnitTestResults','" + junit + "'," + 22 | "'CoberturaCodeCoverage','" + cobertura + "'," + 23 | "'SourceFolder','" + source + "'," + 24 | "'SelectByFolder','" + selectByFolder + "'," + 25 | "'SelectByTag','" + selectByTag + "'," + 26 | "'CoberturaModelCoverage','" + modelCoverageCobertura + "'," + 27 | "'SimulinkTestResults','" + testResultsSimulinkTest + "'," + 28 | "'PDFTestReport','" + testResultsPDF + "'," + 29 | "'Strict'," + strict + "," + 30 | "'UseParallel'," + useParallel + "," + 31 | "'OutputDetail','" + outputDetail + "'," + 32 | "'LoggingLevel','" + loggingLevel + "');" + 33 | `disp('Running MATLAB script with contents:');` + 34 | `disp(testScript.Contents);` + 35 | `fprintf('__________\\n\\n');` + 36 | `run(testScript);`; 37 | } 38 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/test/failRunTests.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | import { runCmdArg } from "./common"; 7 | 8 | const tp = path.join(__dirname, "..", "main.js"); 9 | const tr = new mr.TaskMockRunner(tp); 10 | 11 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "glnxa64", "run-matlab-command"); 12 | 13 | tr.registerMock("./utils", { 14 | platform: () => "linux", 15 | architecture: () => "x64", 16 | }); 17 | 18 | tr.registerMock("fs", { 19 | chmodSync: () => Promise.resolve(0), 20 | }); 21 | 22 | const a: ma.TaskLibAnswers = { 23 | checkPath: { 24 | [runCmdPath]: true, 25 | }, 26 | exec: { 27 | [runCmdPath + " " + runCmdArg("", "", "", "", "", "", "", "", false, false, "", "")]: { 28 | code: 1, 29 | stdout: "tests failed", 30 | }, 31 | }, 32 | exist: { 33 | [runCmdPath]: true, 34 | }, 35 | } as ma.TaskLibAnswers; 36 | tr.setAnswers(a); 37 | 38 | tr.run(); 39 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/test/runTestsLinux.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | import { runCmdArg } from "./common"; 7 | 8 | const tp = path.join(__dirname, "..", "main.js"); 9 | const tr = new mr.TaskMockRunner(tp); 10 | 11 | tr.setInput("testResultsJUnit", "results.xml"); 12 | tr.setInput("codeCoverageCobertura", "coverage.xml"); 13 | tr.setInput("sourceFolder", "source"); 14 | tr.setInput("selectByFolder", "tests/filteredTest"); 15 | tr.setInput("selectByTag", "FILTERED"); 16 | tr.setInput("modelCoverageCobertura", "modelcoverage.xml"); 17 | tr.setInput("testResultsSimulinkTest", "stmresults.mldatx"); 18 | tr.setInput("testResultsPDF", "results.pdf"); 19 | tr.setInput("strict", "true"); 20 | tr.setInput("useParallel", "true"); 21 | tr.setInput("outputDetail", "Verbose"); 22 | tr.setInput("loggingLevel", "Verbose"); 23 | 24 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "glnxa64", "run-matlab-command"); 25 | 26 | tr.registerMock("./utils", { 27 | platform: () => "linux", 28 | architecture: () => "x64", 29 | }); 30 | 31 | tr.registerMock("fs", { 32 | chmodSync: () => Promise.resolve(0), 33 | }); 34 | 35 | const a: ma.TaskLibAnswers = { 36 | checkPath: { 37 | [runCmdPath]: true, 38 | }, 39 | exec: { 40 | [runCmdPath + " " + runCmdArg("results.xml", "coverage.xml", "source", "tests/filteredTest", "FILTERED", "modelcoverage.xml", "stmresults.mldatx", "results.pdf", true, true, "Verbose", "Verbose")]: { 41 | code: 0, 42 | stdout: "ran tests", 43 | }, 44 | }, 45 | exist: { 46 | [runCmdPath]: true, 47 | }, 48 | } as ma.TaskLibAnswers; 49 | tr.setAnswers(a); 50 | 51 | tr.run(); 52 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/test/runTestsWindows.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | import { runCmdArg } from "./common"; 7 | 8 | const tp = path.join(__dirname, "..", "main.js"); 9 | const tr = new mr.TaskMockRunner(tp); 10 | 11 | tr.setInput("testResultsJUnit", "results.xml"); 12 | tr.setInput("codeCoverageCobertura", "coverage.xml"); 13 | tr.setInput("sourceFolder", "source"); 14 | tr.setInput("selectByFolder", "tests/filteredTest"); 15 | tr.setInput("selectByTag", "FILTERED"); 16 | tr.setInput("modelCoverageCobertura", "modelcoverage.xml"); 17 | tr.setInput("testResultsSimulinkTest", "stmresults.mldatx"); 18 | tr.setInput("testResultsPDF", "results.pdf"); 19 | tr.setInput("strict", "true"); 20 | tr.setInput("useParallel", "true"); 21 | tr.setInput("outputDetail", "Verbose"); 22 | tr.setInput("loggingLevel", "Verbose"); 23 | 24 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "win64", "run-matlab-command.exe"); 25 | 26 | tr.registerMock("./utils", { 27 | platform: () => "win32", 28 | architecture: () => "x64", 29 | }); 30 | 31 | tr.registerMock("fs", { 32 | chmodSync: () => Promise.resolve(0), 33 | }); 34 | 35 | const a: ma.TaskLibAnswers = { 36 | checkPath: { 37 | [runCmdPath]: true, 38 | }, 39 | exec: { 40 | [runCmdPath + " " + runCmdArg("results.xml", "coverage.xml", "source", "tests/filteredTest", "FILTERED", "modelcoverage.xml", "stmresults.mldatx", "results.pdf", true, true, "Verbose", "Verbose")]: { 41 | code: 0, 42 | stdout: "ran tests", 43 | }, 44 | }, 45 | exist: { 46 | [runCmdPath]: true, 47 | }, 48 | } as ma.TaskLibAnswers; 49 | tr.setAnswers(a); 50 | 51 | tr.run(); 52 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/test/runTestsWithStartupOptsLinux.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | import { runCmdArg } from "./common"; 7 | 8 | const tp = path.join(__dirname, "..", "main.js"); 9 | const tr = new mr.TaskMockRunner(tp); 10 | 11 | tr.setInput("testResultsJUnit", "results.xml"); 12 | tr.setInput("codeCoverageCobertura", "coverage.xml"); 13 | tr.setInput("sourceFolder", "source"); 14 | tr.setInput("selectByFolder", "tests/filteredTest"); 15 | tr.setInput("selectByTag", "FILTERED"); 16 | tr.setInput("modelCoverageCobertura", "modelcoverage.xml"); 17 | tr.setInput("testResultsSimulinkTest", "stmresults.mldatx"); 18 | tr.setInput("testResultsPDF", "results.pdf"); 19 | tr.setInput("strict", "true"); 20 | tr.setInput("useParallel", "true"); 21 | tr.setInput("outputDetail", "Verbose"); 22 | tr.setInput("loggingLevel", "Verbose"); 23 | tr.setInput("startupOptions", "-nojvm -nodisplay"); 24 | 25 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "glnxa64", "run-matlab-command"); 26 | 27 | tr.registerMock("./utils", { 28 | platform: () => "linux", 29 | architecture: () => "x64", 30 | }); 31 | 32 | tr.registerMock("fs", { 33 | chmodSync: () => Promise.resolve(0), 34 | }); 35 | 36 | const a: ma.TaskLibAnswers = { 37 | checkPath: { 38 | [runCmdPath]: true, 39 | }, 40 | exec: { 41 | [runCmdPath + " " + runCmdArg("results.xml", "coverage.xml", "source", "tests/filteredTest", "FILTERED", "modelcoverage.xml", "stmresults.mldatx", "results.pdf", true, true, "Verbose", "Verbose") + " -nojvm -nodisplay"]: { 42 | code: 0, 43 | stdout: "ran tests", 44 | }, 45 | }, 46 | exist: { 47 | [runCmdPath]: true, 48 | }, 49 | } as ma.TaskLibAnswers; 50 | tr.setAnswers(a); 51 | 52 | tr.run(); 53 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/test/runTestsWithStartupOptsWindows.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The MathWorks, Inc. 2 | 3 | import ma = require("azure-pipelines-task-lib/mock-answer"); 4 | import mr = require("azure-pipelines-task-lib/mock-run"); 5 | import path = require("path"); 6 | import { runCmdArg } from "./common"; 7 | 8 | const tp = path.join(__dirname, "..", "main.js"); 9 | const tr = new mr.TaskMockRunner(tp); 10 | 11 | tr.setInput("testResultsJUnit", "results.xml"); 12 | tr.setInput("codeCoverageCobertura", "coverage.xml"); 13 | tr.setInput("sourceFolder", "source"); 14 | tr.setInput("selectByFolder", "tests/filteredTest"); 15 | tr.setInput("selectByTag", "FILTERED"); 16 | tr.setInput("modelCoverageCobertura", "modelcoverage.xml"); 17 | tr.setInput("testResultsSimulinkTest", "stmresults.mldatx"); 18 | tr.setInput("testResultsPDF", "results.pdf"); 19 | tr.setInput("strict", "true"); 20 | tr.setInput("useParallel", "true"); 21 | tr.setInput("outputDetail", "Verbose"); 22 | tr.setInput("loggingLevel", "Verbose"); 23 | tr.setInput("startupOptions", "-nojvm -nodisplay"); 24 | 25 | const runCmdPath = path.join(path.dirname(__dirname), "bin", "win64", "run-matlab-command.exe"); 26 | 27 | tr.registerMock("./utils", { 28 | platform: () => "win32", 29 | architecture: () => "x64", 30 | }); 31 | 32 | tr.registerMock("fs", { 33 | chmodSync: () => Promise.resolve(0), 34 | }); 35 | 36 | const a: ma.TaskLibAnswers = { 37 | checkPath: { 38 | [runCmdPath]: true, 39 | }, 40 | exec: { 41 | [runCmdPath + " " + runCmdArg("results.xml", "coverage.xml", "source", "tests/filteredTest", "FILTERED", "modelcoverage.xml", "stmresults.mldatx", "results.pdf", true, true, "Verbose", "Verbose") + " -nojvm -nodisplay"]: { 42 | code: 0, 43 | stdout: "ran tests", 44 | }, 45 | }, 46 | exist: { 47 | [runCmdPath]: true, 48 | }, 49 | } as ma.TaskLibAnswers; 50 | tr.setAnswers(a); 51 | 52 | tr.run(); 53 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/test/suite.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The MathWorks, Inc. 2 | 3 | import * as assert from "assert"; 4 | import * as mt from "azure-pipelines-task-lib/mock-test"; 5 | import * as path from "path"; 6 | 7 | describe("RunMATLABTests V0 Suite", () => { 8 | it("should succeed running MATLAB tests on linux", async () => { 9 | const tp = path.join(__dirname, "runTestsLinux.js"); 10 | const tr = new mt.MockTestRunner(tp); 11 | 12 | await tr.runAsync(); 13 | 14 | assert(tr.succeeded, "should have succeeded"); 15 | assert(tr.stdOutContained("ran tests"), "should have run tests"); 16 | }); 17 | 18 | it("should succeed running MATLAB tests on windows", async () => { 19 | const tp = path.join(__dirname, "runTestsWindows.js"); 20 | const tr = new mt.MockTestRunner(tp); 21 | 22 | await tr.runAsync(); 23 | 24 | assert(tr.succeeded, "should have succeeded"); 25 | assert(tr.stdOutContained("ran tests"), "should have run tests"); 26 | }); 27 | 28 | it("should succeed running MATLAB tests with startup options on linux", async () => { 29 | const tp = path.join(__dirname, "runTestsWithStartupOptsLinux.js"); 30 | const tr = new mt.MockTestRunner(tp); 31 | 32 | await tr.runAsync(); 33 | 34 | assert(tr.succeeded, "should have succeeded"); 35 | assert(tr.stdOutContained("ran tests"), "should have run tests"); 36 | }); 37 | 38 | it("should succeed running MATLAB tests with startup options on windows", async () => { 39 | const tp = path.join(__dirname, "runTestsWithStartupOptsWindows.js"); 40 | const tr = new mt.MockTestRunner(tp); 41 | 42 | await tr.runAsync(); 43 | 44 | assert(tr.succeeded, "should have succeeded"); 45 | assert(tr.stdOutContained("ran tests"), "should have run tests"); 46 | }); 47 | 48 | it("should fail when running test fails", async () => { 49 | const tp = path.join(__dirname, "failRunTests.js"); 50 | const tr = new mt.MockTestRunner(tp); 51 | 52 | await tr.runAsync(); 53 | 54 | assert(tr.failed, "should have failed"); 55 | assert(tr.stdOutContained("tests failed"), "should have run tests"); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "sourceMap": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v0/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The MathWorks, Inc. 2 | 3 | export function platform() { 4 | return process.platform; 5 | } 6 | 7 | export function architecture() { 8 | return process.arch; 9 | } 10 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v1/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | scriptgen -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v1/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/matlab-azure-devops-extension/20cd6026fc706cc925710c25cd2d7f8528441a3e/tasks/run-matlab-tests/v1/icon.png -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v1/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The MathWorks, Inc. 2 | 3 | import * as taskLib from "azure-pipelines-task-lib/task"; 4 | import * as path from "path"; 5 | import * as matlab from "./matlab"; 6 | import * as scriptgen from "./scriptgen"; 7 | 8 | async function run() { 9 | try { 10 | taskLib.setResourcePath(path.join( __dirname, "task.json")); 11 | const options: scriptgen.IRunTestsOptions = { 12 | JUnitTestResults: taskLib.getInput("testResultsJUnit"), 13 | CoberturaCodeCoverage: taskLib.getInput("codeCoverageCobertura"), 14 | SourceFolder: taskLib.getInput("sourceFolder"), 15 | SelectByFolder: taskLib.getInput("selectByFolder"), 16 | SelectByTag: taskLib.getInput("selectByTag"), 17 | CoberturaModelCoverage: taskLib.getInput("modelCoverageCobertura"), 18 | SimulinkTestResults: taskLib.getInput("testResultsSimulinkTest"), 19 | PDFTestReport: taskLib.getInput("testResultsPDF"), 20 | Strict: taskLib.getBoolInput("strict"), 21 | UseParallel: taskLib.getBoolInput("useParallel"), 22 | OutputDetail: taskLib.getInput("outputDetail"), 23 | LoggingLevel: taskLib.getInput("loggingLevel")}; 24 | const startupOpts: string | undefined = taskLib.getInput("startupOptions"); 25 | const cmd = scriptgen.generateCommand(options); 26 | const platform = process.platform; 27 | const architecture = process.arch; 28 | await matlab.runCommand(cmd, platform, architecture, startupOpts); 29 | 30 | } catch (err) { 31 | taskLib.setResult(taskLib.TaskResult.Failed, (err as Error).message); 32 | } 33 | } 34 | 35 | run(); 36 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v1/matlab.ts: -------------------------------------------------------------------------------- 1 | ../../run-matlab-command/v1/matlab.ts -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run-matlab-tests", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "npm run getBinDep && npm run getScriptgenDep", 7 | "getBinDep": "../../../scripts/setupdeps.sh v2", 8 | "getScriptgenDep": "wget -q https://ssd.mathworks.com/supportfiles/ci/matlab-script-generator/v0/matlab-script-generator.zip -O scriptgen.zip; unzip -qod scriptgen scriptgen.zip; rm scriptgen.zip" 9 | }, 10 | "dependencies": { 11 | "@types/node": "^22.7.5", 12 | "@types/q": "^1.5.4", 13 | "azure-pipelines-task-lib": "5.0.1-preview.0", 14 | "azure-pipelines-tool-lib": "^2.0.7" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v1/scriptgen.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The MathWorks, Inc. 2 | 3 | import * as path from "path"; 4 | 5 | export interface IRunTestsOptions { 6 | JUnitTestResults?: string; 7 | CoberturaCodeCoverage?: string; 8 | SourceFolder?: string; 9 | PDFTestReport?: string; 10 | SimulinkTestResults?: string; 11 | CoberturaModelCoverage?: string; 12 | SelectByTag?: string; 13 | SelectByFolder?: string; 14 | Strict?: boolean; 15 | UseParallel?: boolean; 16 | OutputDetail?: string; 17 | LoggingLevel?: string; 18 | } 19 | 20 | export function generateCommand(options: IRunTestsOptions): string { 21 | return `addpath('${path.join(__dirname, "scriptgen")}');` + 22 | `testScript = genscript('Test',` + 23 | `'JUnitTestResults','${options.JUnitTestResults || ""}',` + 24 | `'CoberturaCodeCoverage','${options.CoberturaCodeCoverage || ""}',` + 25 | `'SourceFolder','${options.SourceFolder || ""}',` + 26 | `'PDFTestReport','${options.PDFTestReport || ""}',` + 27 | `'SimulinkTestResults','${options.SimulinkTestResults || ""}',` + 28 | `'CoberturaModelCoverage','${options.CoberturaModelCoverage || ""}',` + 29 | `'SelectByTag','${options.SelectByTag || ""}',` + 30 | `'SelectByFolder','${options.SelectByFolder || ""}',` + 31 | `'Strict',${options.Strict || false},` + 32 | `'UseParallel',${options.UseParallel || false},` + 33 | `'OutputDetail','${options.OutputDetail || ""}',` + 34 | `'LoggingLevel','${options.LoggingLevel || ""}');` + 35 | `disp('Running MATLAB script with contents:');` + 36 | `disp(testScript.Contents);` + 37 | `fprintf('__________\\n\\n');` + 38 | `run(testScript);` 39 | .replace(/$\n^\s*/gm, " ") 40 | .trim(); // replace ending newlines and starting spaces 41 | } 42 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v1/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json", 3 | "id": "d9f28863-c9b0-4133-9cb8-a6d4744f30ef", 4 | "name": "RunMATLABTests", 5 | "friendlyName": "Run MATLAB Tests", 6 | "description": "Run MATLAB and Simulink tests and generate artifacts.", 7 | "helpMarkDown": "", 8 | "category": "Test", 9 | "author": "The MathWorks, Inc.", 10 | "version": { 11 | "Major": 1, 12 | "Minor": 0, 13 | "Patch": 0 14 | }, 15 | "groups": [ 16 | { 17 | "name": "filterTests", 18 | "displayName": "Filter Tests" 19 | }, 20 | { 21 | "name": "customizeTestRun", 22 | "displayName": "Customize Test Run" 23 | }, 24 | { 25 | "name": "testArtifacts", 26 | "displayName": "Generate Test Artifacts" 27 | }, 28 | { 29 | "name": "coverageArtifacts", 30 | "displayName": "Generate Coverage Artifacts" 31 | } 32 | ], 33 | "inputs": [ 34 | { 35 | "name": "sourceFolder", 36 | "type": "string", 37 | "label": "Source folder", 38 | "defaultValue": "", 39 | "required": false, 40 | "helpMarkDown": "Location of the folder containing source code, relative to the project root folder. The specified folder and its subfolders are added to the top of the MATLAB search path. To generate a code coverage report, MATLAB uses only the source code in the specified folder and its subfolders." 41 | }, 42 | { 43 | "name": "selectByFolder", 44 | "type": "string", 45 | "label": "By folder", 46 | "defaultValue": "", 47 | "groupName": "filterTests", 48 | "required": false, 49 | "helpMarkDown": "Location of the folders used to select test suite elements, relative to the project root folder. To create a test suite, MATLAB uses only the tests in the specified folder and its subfolders." 50 | }, 51 | { 52 | "name": "selectByTag", 53 | "type": "string", 54 | "label": "By tag", 55 | "defaultValue": "", 56 | "groupName": "filterTests", 57 | "required": false, 58 | "helpMarkDown": "Test tag used to select test suite elements. To create a test suite, the task uses only the test elements with the specified tag." 59 | }, 60 | { 61 | "name": "strict", 62 | "type": "boolean", 63 | "label": "Strict", 64 | "defaultValue": false, 65 | "required": false, 66 | "groupName": "customizeTestRun", 67 | "helpMarkDown": "Option to apply strict checks when running the tests." 68 | }, 69 | { 70 | "name": "useParallel", 71 | "type": "boolean", 72 | "label": "Use Parallel", 73 | "defaultValue": false, 74 | "required": false, 75 | "groupName": "customizeTestRun", 76 | "helpMarkDown": "Option to run tests in parallel (requires Parallel Computing Toolbox)." 77 | }, 78 | { 79 | "name": "outputDetail", 80 | "type": "string", 81 | "label": "Output Detail", 82 | "defaultValue": "", 83 | "required": false, 84 | "groupName": "customizeTestRun", 85 | "helpMarkDown": "Display level for event details produced by the test run." 86 | }, 87 | { 88 | "name": "loggingLevel", 89 | "type": "string", 90 | "label": "Logging Level", 91 | "defaultValue": "", 92 | "required": false, 93 | "groupName": "customizeTestRun", 94 | "helpMarkDown": "Maximum verbosity level for logged diagnostics included for the test run." 95 | }, 96 | { 97 | "name": "testResultsPDF", 98 | "type": "string", 99 | "label": "PDF test report ", 100 | "defaultValue": "", 101 | "groupName": "testArtifacts", 102 | "required": false, 103 | "helpMarkDown": "Path to write the test results in PDF format (requires MATLAB R2020b or later on macOS platforms)." 104 | }, 105 | { 106 | "name": "testResultsJUnit", 107 | "type": "string", 108 | "label": "JUnit-style test results", 109 | "defaultValue": "", 110 | "groupName": "testArtifacts", 111 | "required": false, 112 | "helpMarkDown": "Path to write the test results in JUnit-style XML format." 113 | }, 114 | { 115 | "name": "testResultsSimulinkTest", 116 | "type": "string", 117 | "label": "Simulink test results", 118 | "defaultValue": "", 119 | "groupName": "testArtifacts", 120 | "required": false, 121 | "helpMarkDown": "Path to export Simulink Test Manager results in MLDATX format (requires Simulink Test and is supported in MATLAB R2019a or later)." 122 | }, 123 | { 124 | "name": "codeCoverageCobertura", 125 | "type": "string", 126 | "label": "Cobertura code coverage", 127 | "defaultValue": "", 128 | "required": false, 129 | "groupName": "coverageArtifacts", 130 | "helpMarkDown": "Path to write the code coverage results in Cobertura XML format." 131 | }, 132 | { 133 | "name": "modelCoverageCobertura", 134 | "type": "string", 135 | "label": "Cobertura model coverage", 136 | "defaultValue": "", 137 | "groupName": "coverageArtifacts", 138 | "required": false, 139 | "helpMarkDown": "Path to write the model coverage report in Cobertura XML format (requires Simulink Coverage and is supported in MATLAB R2018b or later)." 140 | }, 141 | { 142 | "name": "startupOptions", 143 | "type": "string", 144 | "label": "Startup Options", 145 | "required": false, 146 | "defaultValue": "", 147 | "helpMarkDown": "Startup options for MATLAB." 148 | } 149 | ], 150 | "instanceNameFormat": "Run MATLAB Tests", 151 | "execution": { 152 | "Node10": { 153 | "target": "main.js" 154 | }, 155 | "Node16": { 156 | "target": "main.js" 157 | }, 158 | "Node20": { 159 | "target": "main.js" 160 | } 161 | }, 162 | "messages": { 163 | "GeneratingScript": "Generating MATLAB script with content:\n%s", 164 | "FailedToRunCommand": "Failed to run MATLAB command.", 165 | "FailedToRunTests": "Tests failed with nonzero exit code." 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v1/test/scriptgen.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The MathWorks, Inc. 2 | 3 | import * as assert from "assert"; 4 | import * as scriptgen from "../scriptgen"; 5 | 6 | export default function suite() { 7 | describe("command generation", () => { 8 | it("contains genscript invocation with unspecified options", () => { 9 | const options: scriptgen.IRunTestsOptions = { 10 | JUnitTestResults: "", 11 | CoberturaCodeCoverage: "", 12 | SourceFolder: "", 13 | PDFTestReport: "", 14 | SimulinkTestResults: "", 15 | CoberturaModelCoverage: "", 16 | SelectByTag: "", 17 | SelectByFolder: "", 18 | Strict: false, 19 | UseParallel: false, 20 | OutputDetail: "", 21 | LoggingLevel: "", 22 | }; 23 | 24 | const actual = scriptgen.generateCommand(options); 25 | 26 | assert(actual.includes("genscript('Test'")); 27 | assert(actual.includes("'JUnitTestResults',''")); 28 | assert(actual.includes("'CoberturaCodeCoverage',''")); 29 | assert(actual.includes("'SourceFolder',''")); 30 | assert(actual.includes("'PDFTestReport',''")); 31 | assert(actual.includes("'SimulinkTestResults',''")); 32 | assert(actual.includes("'CoberturaModelCoverage',''")); 33 | assert(actual.includes("'SelectByTag',''")); 34 | assert(actual.includes("'SelectByFolder',''")); 35 | assert(actual.includes("'Strict',false")); 36 | assert(actual.includes("'UseParallel',false")); 37 | assert(actual.includes("'OutputDetail',''")); 38 | assert(actual.includes("'LoggingLevel',''")); 39 | 40 | const expected = `genscript('Test', 'JUnitTestResults','', 'CoberturaCodeCoverage','', 41 | 'SourceFolder','', 'PDFTestReport','', 'SimulinkTestResults','', 42 | 'CoberturaModelCoverage','', 'SelectByTag','', 'SelectByFolder','', 43 | 'Strict',false, 'UseParallel',false, 'OutputDetail','', 'LoggingLevel','')` 44 | .replace(/\s+/g, ""); 45 | assert(actual.replace(/\s+/g, "").includes(expected)); 46 | }); 47 | 48 | it("contains genscript invocation with all options specified", () => { 49 | const options: scriptgen.IRunTestsOptions = { 50 | JUnitTestResults: "test-results/results.xml", 51 | CoberturaCodeCoverage: "code-coverage/coverage.xml", 52 | SourceFolder: "source", 53 | PDFTestReport: "test-results/pdf-results.pdf", 54 | SimulinkTestResults: "test-results/simulinkTest.mldatx", 55 | CoberturaModelCoverage: "test-results/modelcoverage.xml", 56 | SelectByTag: "FeatureA", 57 | SelectByFolder: "test/tools;test/toolbox", 58 | Strict: true, 59 | UseParallel: true, 60 | OutputDetail: "Detailed", 61 | LoggingLevel: "Detailed", 62 | }; 63 | 64 | const actual = scriptgen.generateCommand(options); 65 | 66 | assert(actual.includes("genscript('Test'")); 67 | assert(actual.includes("'JUnitTestResults','test-results/results.xml'")); 68 | assert(actual.includes("'CoberturaCodeCoverage','code-coverage/coverage.xml'")); 69 | assert(actual.includes("'SourceFolder','source'")); 70 | assert(actual.includes("'PDFTestReport','test-results/pdf-results.pdf'")); 71 | assert(actual.includes("'SimulinkTestResults','test-results/simulinkTest.mldatx'")); 72 | assert(actual.includes("'CoberturaModelCoverage','test-results/modelcoverage.xml'")); 73 | assert(actual.includes("'SelectByTag','FeatureA'")); 74 | assert(actual.includes("'SelectByFolder','test/tools;test/toolbox'")); 75 | assert(actual.includes("'Strict',true")); 76 | assert(actual.includes("'UseParallel',true")); 77 | assert(actual.includes("'OutputDetail','Detailed'")); 78 | assert(actual.includes("'LoggingLevel','Detailed'")); 79 | 80 | const expected = `genscript('Test', 'JUnitTestResults','test-results/results.xml', 81 | 'CoberturaCodeCoverage','code-coverage/coverage.xml', 'SourceFolder','source', 82 | 'PDFTestReport','test-results/pdf-results.pdf', 'SimulinkTestResults','test-results/simulinkTest.mldatx', 83 | 'CoberturaModelCoverage','test-results/modelcoverage.xml', 'SelectByTag','FeatureA', 84 | 'SelectByFolder','test/tools;test/toolbox', 'Strict',true, 'UseParallel',true, 'OutputDetail','Detailed', 85 | 'LoggingLevel','Detailed' )` 86 | .replace(/\s+/g, ""); 87 | assert(actual.replace(/\s+/g, "").includes(expected)); 88 | }); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v1/test/suite.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The MathWorks, Inc. 2 | 3 | import scriptgenTests from "./scriptgen.test"; 4 | 5 | describe("RunMATLABCommand V1 Suite", () => { 6 | scriptgenTests(); 7 | }); 8 | -------------------------------------------------------------------------------- /tasks/run-matlab-tests/v1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "sourceMap": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "_build", 5 | "module": "commonjs", 6 | "strict": true, 7 | "sourceMap": true 8 | }, 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "object-literal-sort-keys": false, 9 | "no-console": false, 10 | "interface-name" : false, 11 | "prefer-const": [true, {"destructuring": "all"}] 12 | }, 13 | "rulesDirectory": [] 14 | } 15 | -------------------------------------------------------------------------------- /vss-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": 1, 3 | "id": "matlab-azure-devops-extension", 4 | "name": "MATLAB", 5 | "description": "Run MATLAB and Simulink as part of your build pipeline.", 6 | "public": false, 7 | "publisher": "MathWorks", 8 | "tags": [ 9 | "MATLAB", 10 | "Simulink", 11 | "MathWorks" 12 | ], 13 | "galleryFlags": [ 14 | "Public" 15 | ], 16 | "targets": [ 17 | { 18 | "id": "Microsoft.VisualStudio.Services" 19 | } 20 | ], 21 | "categories": [ 22 | "Azure Pipelines" 23 | ], 24 | "icons": { 25 | "default": "images/extension-icon.png" 26 | }, 27 | "content": { 28 | "details": { 29 | "path": "overview.md" 30 | }, 31 | "license": { 32 | "path": "license.txt" 33 | } 34 | }, 35 | "links": { 36 | "repository": { 37 | "uri": "https://github.com/mathworks/matlab-azure-devops-extension" 38 | }, 39 | "issues": { 40 | "uri": "https://github.com/mathworks/matlab-azure-devops-extension/issues" 41 | }, 42 | "support": { 43 | "uri": "mailto:continuous-integration@mathworks.com" 44 | }, 45 | "privacypolicy": { 46 | "uri": "https://www.mathworks.com/company/aboutus/policies_statements/privacy-policy.html" 47 | } 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "uri": "https://github.com/mathworks/matlab-azure-devops-extension" 52 | }, 53 | "files": [ 54 | { 55 | "path": "images", 56 | "addressable": true 57 | }, 58 | { 59 | "path": "tasks" 60 | } 61 | ], 62 | "contributions": [ 63 | { 64 | "id": "InstallMATLAB", 65 | "type": "ms.vss-distributed-task.task", 66 | "targets": [ 67 | "ms.vss-distributed-task.tasks" 68 | ], 69 | "properties": { 70 | "name": "tasks/install-matlab" 71 | } 72 | }, 73 | { 74 | "id": "RunMATLABBuild", 75 | "type": "ms.vss-distributed-task.task", 76 | "targets": [ 77 | "ms.vss-distributed-task.tasks" 78 | ], 79 | "properties": { 80 | "name": "tasks/run-matlab-build" 81 | } 82 | }, 83 | { 84 | "id": "RunMATLABCommand", 85 | "type": "ms.vss-distributed-task.task", 86 | "targets": [ 87 | "ms.vss-distributed-task.tasks" 88 | ], 89 | "properties": { 90 | "name": "tasks/run-matlab-command" 91 | } 92 | }, 93 | { 94 | "id": "RunMATLABTests", 95 | "type": "ms.vss-distributed-task.task", 96 | "targets": [ 97 | "ms.vss-distributed-task.tasks" 98 | ], 99 | "properties": { 100 | "name": "tasks/run-matlab-tests" 101 | } 102 | } 103 | ] 104 | } 105 | --------------------------------------------------------------------------------