diff --git a/.github/workflows/bat.yml b/.github/workflows/bat.yml index b54564f..cc5fdf2 100644 --- a/.github/workflows/bat.yml +++ b/.github/workflows/bat.yml @@ -1,35 +1,34 @@ -name: Build and Test -on: [push] -permissions: - contents: read - -jobs: - plugin-tests: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - - os: windows-latest - - os: macos-latest - steps: - - uses: actions/checkout@v6 - - uses: matlab-actions/setup-matlab@v2 - with: - release: latest-including-prerelease - - uses: matlab-actions/run-tests@v2 - with: - source-folder: plugins - - bat: - name: Build and Test - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version: 24 - - name: Perform npm tasks - run: npm run ci - +name: Build and Test +on: [push] +permissions: + contents: read + +jobs: + plugin-tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + - os: windows-latest + - os: macos-latest + steps: + - uses: actions/checkout@v6 + - uses: matlab-actions/setup-matlab@v3 + with: + release: latest-including-prerelease + - uses: matlab-actions/run-tests@v3 + with: + source-folder: plugins + + bat: + name: Build and Test + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + - name: Perform npm tasks + run: npm run ci diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2b73fcd..9683fc6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,77 +1,77 @@ -name: Publish -on: - release: - types: published -permissions: - contents: write - -jobs: - build: - name: Build - runs-on: ubuntu-latest - outputs: - tag: ${{ steps.update-package-version.outputs.version }} - steps: - - uses: actions/checkout@v6 - - name: Configure git - run: | - git config user.name 'Release Action' - git config user.email '<>' - - uses: actions/setup-node@v6 - with: - node-version: 24 - - # Call `npm version`. It increments the version and commits the changes. - # We'll save the output (new version string) for use in the following - # steps - - name: Update package version - id: update-package-version - run: | - git tag -d "${{ github.event.release.tag_name }}" - VERSION=$(npm version "${{ github.event.release.tag_name }}" --no-git-tag-version) - git add package.json package-lock.json - git commit -m "[skip ci] Bump $VERSION" - git push origin HEAD:main - - # Now carry on, business as usual - - name: Perform npm tasks - run: npm run ci - - # Finally, create a detached commit containing the built artifacts and tag - # it with the release. Note: the fact that the branch is locally updated - # will not be relayed (pushed) to origin - - name: Commit to release branch - id: release_info - run: | - # Check for semantic versioning - longVersion="${{github.event.release.tag_name}}" - echo "Preparing release for version $longVersion" - [[ $longVersion == v[0-9]*.[0-9]*.[0-9]* ]] || (echo "must follow semantic versioning" && exit 1) - majorVersion=$(echo ${longVersion%.*.*}) - minorVersion=$(echo ${longVersion%.*}) - - # Add the built artifacts. Using --force because dist/lib should be in - # .gitignore - git add --force dist lib - - # Make the commit - MESSAGE="Build for $(git rev-parse --short HEAD)" - git commit --allow-empty -m "$MESSAGE" - git tag -f -a -m "Release $longVersion" $longVersion - - # Get the commit of the tag you just released - commitHash=$(git rev-list -n 1 $longVersion) - - # Delete the old major and minor version tags locally - git tag -d $majorVersion || true - git tag -d $minorVersion || true - - # Make new major and minor version tags locally that point to the commit you got from the "git rev-list" above - git tag -f $majorVersion $commitHash - git tag -f $minorVersion $commitHash - - # Force push the new minor version tag to overwrite the old tag remotely - echo "Pushing new tags" - git push -f origin $longVersion - git push -f origin $majorVersion - git push -f origin $minorVersion +name: Publish +on: + release: + types: published +permissions: + contents: write + +jobs: + build: + name: Build + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.update-package-version.outputs.version }} + steps: + - uses: actions/checkout@v6 + - name: Configure git + run: | + git config user.name 'Release Action' + git config user.email '<>' + - uses: actions/setup-node@v6 + with: + node-version: 24 + + # Call `npm version`. It increments the version and commits the changes. + # We'll save the output (new version string) for use in the following + # steps + - name: Update package version + id: update-package-version + run: | + git tag -d "${{ github.event.release.tag_name }}" + VERSION=$(npm version "${{ github.event.release.tag_name }}" --no-git-tag-version) + git add package.json package-lock.json + git commit -m "[skip ci] Bump $VERSION" + git push origin HEAD:main + + # Now carry on, business as usual + - name: Perform npm tasks + run: npm run ci + + # Finally, create a detached commit containing the built artifacts and tag + # it with the release. Note: the fact that the branch is locally updated + # will not be relayed (pushed) to origin + - name: Commit to release branch + id: release_info + run: | + # Check for semantic versioning + longVersion="${{github.event.release.tag_name}}" + echo "Preparing release for version $longVersion" + [[ $longVersion == v[0-9]*.[0-9]*.[0-9]* ]] || (echo "must follow semantic versioning" && exit 1) + majorVersion=$(echo ${longVersion%.*.*}) + minorVersion=$(echo ${longVersion%.*}) + + # Add the built artifacts. Using --force because dist/lib should be in + # .gitignore + git add --force dist lib + + # Make the commit + MESSAGE="Build for $(git rev-parse --short HEAD)" + git commit --allow-empty -m "$MESSAGE" + git tag -f -a -m "Release $longVersion" $longVersion + + # Get the commit of the tag you just released + commitHash=$(git rev-list -n 1 $longVersion) + + # Delete the old major and minor version tags locally + git tag -d $majorVersion || true + git tag -d $minorVersion || true + + # Make new major and minor version tags locally that point to the commit you got from the "git rev-list" above + git tag -f $majorVersion $commitHash + git tag -f $minorVersion $commitHash + + # Force push the new minor version tag to overwrite the old tag remotely + echo "Pushing new tags" + git push -f origin $longVersion + git push -f origin $majorVersion + git push -f origin $minorVersion diff --git a/devel/contributing.md b/devel/contributing.md new file mode 100644 index 0000000..018b52c --- /dev/null +++ b/devel/contributing.md @@ -0,0 +1,44 @@ +## Contributing + +Verify changes by running tests and building locally with the following command: + +``` +npm run ci +``` + +## Creating a New Release + +Familiarize yourself with the best practices for [releasing and maintaining GitHub actions](https://docs.github.com/en/actions/creating-actions/releasing-and-maintaining-actions). + +Changes should be made on a new branch. The new branch should be merged to the main branch via a pull request. Ensure that all of the CI pipeline checks and tests have passed for your changes. + +After the pull request has been approved and merged to main, follow the Github process for [creating a new release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository). The release must follow semantic versioning (ex: vX.Y.Z). This will kick off a new pipeline execution, and the action will automatically be published to the GitHub Actions Marketplace if the pipeline finishes successfully. Check the [GitHub Marketplace](https://github.com/marketplace/actions/setup-matlab) and check the major version in the repository (ex: v1 for v1.0.0) to ensure that the new semantically versioned tag is available. + +## Adding a Pre-Commit Hook + +You can run all CI checks before each commit by adding a pre-commit hook. To do so, navigate to the repository root folder and run the following commands: + +_bash (Linux/macOS)_ + +```sh +echo '#!/bin/sh' > .git/hooks/pre-commit +echo 'npm run ci' >> .git/hooks/pre-commit +chmod +x .git/hooks/pre-commit +``` + +_Command Prompt (Windows)_ + +```cmd +echo #!/bin/sh > .git\hooks\pre-commit +echo npm run ci >> .git\hooks\pre-commit +``` + +_PowerShell (Windows)_ + +```pwsh +Set-Content .git\hooks\pre-commit '#!/bin/sh' +Add-Content .git\hooks\pre-commit 'npm run ci' +``` + +> **Note:** +> Git hooks are not version-controlled, so you need to set up this hook for each fresh clone of the repository. diff --git a/jest.config.ts b/jest.config.ts index 5551fb3..864e6f1 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -14,7 +14,7 @@ export default { }, ], }, - extensionsToTreatAsEsm: ['.ts'], + extensionsToTreatAsEsm: [".ts"], transformIgnorePatterns: ["node_modules/(?!(@actions)/)"], moduleNameMapper: { "^(\\.{1,2}/.*)\\.js$": "$1", diff --git a/package-lock.json b/package-lock.json index 49a5c30..114d28c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1956,9 +1956,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -4559,9 +4559,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ecaf282..325a6c0 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "prepare": "npm run build", "package": "ncc build --minify", "test": "NODE_OPTIONS='--experimental-vm-modules' jest", - "all": "npm test && npm run build && npm run package", + "all": "npm run format-check && npm test && npm run build && npm run package", "ci": "npm run clean && npm ci --ignore-scripts && npm run all" }, "files": [ diff --git a/src/buildSummary.ts b/src/buildSummary.ts index 30803b4..02d7530 100644 --- a/src/buildSummary.ts +++ b/src/buildSummary.ts @@ -1,7 +1,7 @@ // Copyright 2024-25 The MathWorks, Inc. import * as core from "@actions/core"; -import { join } from 'path'; -import { readFileSync, unlinkSync, existsSync } from 'fs'; +import { join } from "path"; +import { readFileSync, unlinkSync, existsSync } from "fs"; export function addSummary(taskSummaryTableRows: string[][], actionName: string) { try { @@ -9,25 +9,30 @@ export function addSummary(taskSummaryTableRows: string[][], actionName: string) .addHeading("MATLAB Build Results (" + actionName + ") ") .addTable(taskSummaryTableRows); } catch (e) { - console.error('An error occurred while adding the build results table to the summary:', e); + console.error("An error occurred while adding the build results table to the summary:", e); } } export function getSummaryRows(buildSummary: string): any[] { const rows = JSON.parse(buildSummary).map((t: any) => { if (t.failed) { - return [t.name, '🔴 Failed', t.description, t.duration]; + return [t.name, "🔴 Failed", t.description, t.duration]; } else if (t.skipped) { - return [t.name, 'đŸ”ĩ Skipped' + ' (' + interpretSkipReason(t.skipReason) + ')', t.description, t.duration]; + return [ + t.name, + "đŸ”ĩ Skipped" + " (" + interpretSkipReason(t.skipReason) + ")", + t.description, + t.duration, + ]; } else { - return [t.name, 'đŸŸĸ Successful', t.description, t.duration]; + return [t.name, "đŸŸĸ Successful", t.description, t.duration]; } }); return rows; } -export function interpretSkipReason(skipReason: string){ - switch(skipReason) { +export function interpretSkipReason(skipReason: string) { + switch (skipReason) { case "UpToDate": return "up-to-date"; case "UserSpecified": @@ -40,28 +45,32 @@ export function interpretSkipReason(skipReason: string){ } } -export function processAndAddBuildSummary( - runnerTemp: string, - runId: string, - actionName: string -) { - const header = [{ data: 'MATLAB Task', header: true }, { data: 'Status', header: true }, { data: 'Description', header: true }, { data: 'Duration (HH:mm:ss)', header: true }]; +export function processAndAddBuildSummary(runnerTemp: string, runId: string, actionName: string) { + const header = [ + { data: "MATLAB Task", header: true }, + { data: "Status", header: true }, + { data: "Description", header: true }, + { data: "Duration (HH:mm:ss)", header: true }, + ]; const filePath: string = join(runnerTemp, `buildSummary${runId}.json`); let taskSummaryTable; if (existsSync(filePath)) { try { - const buildSummary = readFileSync(filePath, { encoding: 'utf8' }); + const buildSummary = readFileSync(filePath, { encoding: "utf8" }); const rows = getSummaryRows(buildSummary); taskSummaryTable = [header, ...rows]; } catch (e) { - console.error('An error occurred while reading the build summary file:', e); + console.error("An error occurred while reading the build summary file:", e); return; } finally { try { unlinkSync(filePath); } catch (e) { - console.error(`An error occurred while trying to delete the build summary file ${filePath}:`, e); + console.error( + `An error occurred while trying to delete the build summary file ${filePath}:`, + e, + ); } } addSummary(taskSummaryTable, actionName); diff --git a/src/buildSummary.unit.test.ts b/src/buildSummary.unit.test.ts index d8454bb..d6d80cd 100644 --- a/src/buildSummary.unit.test.ts +++ b/src/buildSummary.unit.test.ts @@ -2,7 +2,7 @@ import { jest, describe, it, expect, beforeEach } from "@jest/globals"; -jest.unstable_mockModule('@actions/core', () => ({ +jest.unstable_mockModule("@actions/core", () => ({ summary: { addTable: jest.fn().mockReturnThis(), addHeading: jest.fn().mockReturnThis(), @@ -19,33 +19,66 @@ beforeEach(() => { (core.summary.write as jest.Mock).mockReturnThis(); }); -describe('summaryGeneration', () => { - it('should process and return summary rows for valid JSON with different task statuses', () => { +describe("summaryGeneration", () => { + it("should process and return summary rows for valid JSON with different task statuses", () => { const mockBuildSummary = JSON.stringify([ - { name: 'Task 1', failed: true, skipped: false, description: 'Task 1 description', duration: '00:00:10' }, - { name: 'Task 2', failed: false, skipped: true, skipReason: 'UserSpecified', description: 'Task 2 description', duration: '00:00:20' }, - { name: 'Task 3', failed: false, skipped: true, skipReason: 'DependencyFailed', description: 'Task 3 description', duration: '00:00:20' }, - { name: 'Task 4', failed: false, skipped: true, skipReason: 'UpToDate', description: 'Task 4 description', duration: '00:00:20' }, - { name: 'Task 5', failed: false, skipped: false, description: 'Task 5 description', duration: '00:00:30' } + { + name: "Task 1", + failed: true, + skipped: false, + description: "Task 1 description", + duration: "00:00:10", + }, + { + name: "Task 2", + failed: false, + skipped: true, + skipReason: "UserSpecified", + description: "Task 2 description", + duration: "00:00:20", + }, + { + name: "Task 3", + failed: false, + skipped: true, + skipReason: "DependencyFailed", + description: "Task 3 description", + duration: "00:00:20", + }, + { + name: "Task 4", + failed: false, + skipped: true, + skipReason: "UpToDate", + description: "Task 4 description", + duration: "00:00:20", + }, + { + name: "Task 5", + failed: false, + skipped: false, + description: "Task 5 description", + duration: "00:00:30", + }, ]); const result = buildSummary.getSummaryRows(mockBuildSummary); expect(result).toEqual([ - ['Task 1', '🔴 Failed', 'Task 1 description', '00:00:10'], - ['Task 2', 'đŸ”ĩ Skipped (user requested)', 'Task 2 description', '00:00:20'], - ['Task 3', 'đŸ”ĩ Skipped (dependency failed)', 'Task 3 description', '00:00:20'], - ['Task 4', 'đŸ”ĩ Skipped (up-to-date)', 'Task 4 description', '00:00:20'], - ['Task 5', 'đŸŸĸ Successful', 'Task 5 description', '00:00:30'] + ["Task 1", "🔴 Failed", "Task 1 description", "00:00:10"], + ["Task 2", "đŸ”ĩ Skipped (user requested)", "Task 2 description", "00:00:20"], + ["Task 3", "đŸ”ĩ Skipped (dependency failed)", "Task 3 description", "00:00:20"], + ["Task 4", "đŸ”ĩ Skipped (up-to-date)", "Task 4 description", "00:00:20"], + ["Task 5", "đŸŸĸ Successful", "Task 5 description", "00:00:30"], ]); }); - it('writes the summary correctly', () => { + it("writes the summary correctly", () => { const mockTableRows = [ - ['MATLAB Task', 'Status', 'Description', 'Duration (HH:mm:ss)'], - ['Test Task', '🔴 Failed', 'A test task', '00:00:10'], + ["MATLAB Task", "Status", "Description", "Duration (HH:mm:ss)"], + ["Test Task", "🔴 Failed", "A test task", "00:00:10"], ]; - const actionName = 'run-build'; + const actionName = "run-build"; buildSummary.addSummary(mockTableRows, actionName); diff --git a/src/codeCoverageSummary.ts b/src/codeCoverageSummary.ts index b33300d..6193fec 100644 --- a/src/codeCoverageSummary.ts +++ b/src/codeCoverageSummary.ts @@ -11,26 +11,23 @@ interface CoverageMetric { export interface CoverageData { MetricLevel?: string; - FunctionCoverage?: CoverageMetric; - StatementCoverage?: CoverageMetric; - DecisionCoverage?: CoverageMetric; - ConditionCoverage?: CoverageMetric; - MCDCCoverage?: CoverageMetric; + FunctionCoverage?: CoverageMetric; + StatementCoverage?: CoverageMetric; + DecisionCoverage?: CoverageMetric; + ConditionCoverage?: CoverageMetric; + MCDCCoverage?: CoverageMetric; } -export function getCoverageResults( - runnerTemp: string, - runId: string, -): CoverageData | null { +export function getCoverageResults(runnerTemp: string, runId: string): CoverageData | null { let coverageData = null; const coveragePath = path.join(runnerTemp, `matlabCoverageResults${runId}.json`); - + if (existsSync(coveragePath)) { try { const coverageArray: CoverageData[] = JSON.parse(readFileSync(coveragePath, "utf8")); if (coverageArray.length !== 0) { coverageData = coverageArray[coverageArray.length - 1]; - } + } } catch (e) { console.error( `An error occurred while reading the code coverage summary file ${coveragePath}:`, @@ -52,42 +49,41 @@ export function getCoverageResults( function formatPercentage(percentage: number): string { if (percentage === null || percentage === undefined || isNaN(percentage)) { - return '0.00%'; + return "0.00%"; } - return percentage.toFixed(2) + '%'; + return percentage.toFixed(2) + "%"; } export function getCoverageTable(coverage: CoverageData): string { - // Define all possible columns const allColumns = [ - { name: 'Function', data: coverage.FunctionCoverage }, - { name: 'Statement', data: coverage.StatementCoverage }, - { name: 'Decision', data: coverage.DecisionCoverage }, - { name: 'Condition', data: coverage.ConditionCoverage }, - { name: 'MC/DC', data: coverage.MCDCCoverage } + { name: "Function", data: coverage.FunctionCoverage }, + { name: "Statement", data: coverage.StatementCoverage }, + { name: "Decision", data: coverage.DecisionCoverage }, + { name: "Condition", data: coverage.ConditionCoverage }, + { name: "MC/DC", data: coverage.MCDCCoverage }, ]; // Filter to only include columns where data actually exists - const visibleColumns = allColumns.filter(col => col.data !== undefined && col.data !== null); + const visibleColumns = allColumns.filter((col) => col.data !== undefined && col.data !== null); // Build header row - const headers = visibleColumns.map(col => `${col.name}`).join(''); + const headers = visibleColumns.map((col) => `${col.name}`).join(""); const headerRow = `Metric${headers}`; // Build percentage row - const percentages = visibleColumns.map(col => - `${formatPercentage(col.data!.Percentage)}` - ).join(''); + const percentages = visibleColumns + .map((col) => `${formatPercentage(col.data!.Percentage)}`) + .join(""); const percentageRow = `Percentage${percentages}`; // Build covered/total row - const coveredTotals = visibleColumns.map(col => - `${col.data!.Executed}/${col.data!.Total}` - ).join(''); + const coveredTotals = visibleColumns + .map((col) => `${col.data!.Executed}/${col.data!.Total}`) + .join(""); const coveredTotalRow = `Covered/Total${coveredTotals}`; const tableHTML = `${headerRow}${percentageRow}${coveredTotalRow}
`; - + return tableHTML; } diff --git a/src/codeCoverageSummary.unit.test.ts b/src/codeCoverageSummary.unit.test.ts index 116b2c6..d250f9f 100644 --- a/src/codeCoverageSummary.unit.test.ts +++ b/src/codeCoverageSummary.unit.test.ts @@ -36,7 +36,7 @@ describe("Coverage Data Retrieval Tests", () => { expect(result).toBeNull(); expect(fs.existsSync).toHaveBeenCalledWith( - path.join(runnerTemp, `matlabCoverageResults${runId}.json`) + path.join(runnerTemp, `matlabCoverageResults${runId}.json`), ); expect(mockUnlinkSync).not.toHaveBeenCalled(); }); @@ -48,14 +48,14 @@ describe("Coverage Data Retrieval Tests", () => { StatementCoverage: { Executed: 80, Total: 100, - Percentage: 80.0 + Percentage: 80.0, }, FunctionCoverage: { Executed: 15, Total: 20, - Percentage: 75.0 - } - } + Percentage: 75.0, + }, + }, ]; (fs.existsSync as jest.Mock).mockReturnValue(true); @@ -66,10 +66,10 @@ describe("Coverage Data Retrieval Tests", () => { expect(result).toEqual(mockCoverageData[0]); expect(fs.readFileSync).toHaveBeenCalledWith( path.join(runnerTemp, `matlabCoverageResults${runId}.json`), - "utf8" + "utf8", ); expect(mockUnlinkSync).toHaveBeenCalledWith( - path.join(runnerTemp, `matlabCoverageResults${runId}.json`) + path.join(runnerTemp, `matlabCoverageResults${runId}.json`), ); }); @@ -80,17 +80,17 @@ describe("Coverage Data Retrieval Tests", () => { StatementCoverage: { Executed: 70, Total: 100, - Percentage: 70.0 - } + Percentage: 70.0, + }, }, { MetricLevel: "decision", DecisionCoverage: { Executed: 90, Total: 100, - Percentage: 90.0 - } - } + Percentage: 90.0, + }, + }, ]; (fs.existsSync as jest.Mock).mockReturnValue(true); @@ -120,7 +120,9 @@ describe("Coverage Data Retrieval Tests", () => { expect(result).toBeNull(); expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining("An error occurred while reading the code coverage summary file"), + expect.stringContaining( + "An error occurred while reading the code coverage summary file", + ), expect.anything(), ); expect(mockUnlinkSync).toHaveBeenCalled(); @@ -136,7 +138,9 @@ describe("Coverage Data Retrieval Tests", () => { expect(result).toBeNull(); expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining("An error occurred while reading the code coverage summary file"), + expect.stringContaining( + "An error occurred while reading the code coverage summary file", + ), expect.anything(), ); expect(mockUnlinkSync).toHaveBeenCalled(); @@ -144,13 +148,17 @@ describe("Coverage Data Retrieval Tests", () => { it("should handle file deletion errors gracefully", () => { (fs.existsSync as jest.Mock).mockReturnValue(true); - (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify([{ - StatementCoverage: { - Executed: 80, - Total: 100, - Percentage: 80.0 - } - }])); + (fs.readFileSync as jest.Mock).mockReturnValue( + JSON.stringify([ + { + StatementCoverage: { + Executed: 80, + Total: 100, + Percentage: 80.0, + }, + }, + ]), + ); mockUnlinkSync.mockImplementationOnce(() => { throw new Error("Permission denied - cannot delete file"); }); @@ -159,7 +167,9 @@ describe("Coverage Data Retrieval Tests", () => { expect(result).toBeDefined(); expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining("An error occurred while trying to delete the code coverage summary file"), + expect.stringContaining( + "An error occurred while trying to delete the code coverage summary file", + ), expect.anything(), ); expect(mockUnlinkSync).toHaveBeenCalled(); @@ -177,28 +187,28 @@ describe("Coverage Table HTML Generation Tests", () => { FunctionCoverage: { Executed: 15, Total: 20, - Percentage: 75.0 + Percentage: 75.0, }, StatementCoverage: { Executed: 80, Total: 100, - Percentage: 80.55 + Percentage: 80.55, }, DecisionCoverage: { Executed: 45, Total: 50, - Percentage: 90.0 + Percentage: 90.0, }, ConditionCoverage: { Executed: 30, Total: 40, - Percentage: 75.0 + Percentage: 75.0, }, MCDCCoverage: { Executed: 20, Total: 25, - Percentage: 80.0 - } + Percentage: 80.0, + }, }; const html = codeCoverageSummary.getCoverageTable(mockCoverageData); @@ -249,8 +259,8 @@ describe("Coverage Table HTML Generation Tests", () => { StatementCoverage: { Executed: 85, Total: 100, - Percentage: 85.0 - } + Percentage: 85.0, + }, }; const html = codeCoverageSummary.getCoverageTable(mockCoverageData); @@ -282,18 +292,18 @@ describe("Coverage Table HTML Generation Tests", () => { StatementCoverage: { Executed: 80, Total: 100, - Percentage: 80.0 + Percentage: 80.0, }, DecisionCoverage: { Executed: 45, Total: 50, - Percentage: 90.0 + Percentage: 90.0, }, MCDCCoverage: { Executed: 20, Total: 25, - Percentage: 80.0 - } + Percentage: 80.0, + }, }; const html = codeCoverageSummary.getCoverageTable(mockCoverageData); @@ -322,8 +332,8 @@ describe("Coverage Table HTML Generation Tests", () => { StatementCoverage: { Executed: 0, Total: 100, - Percentage: 0 - } + Percentage: 0, + }, }; const html = codeCoverageSummary.getCoverageTable(mockCoverageData); @@ -336,8 +346,8 @@ describe("Coverage Table HTML Generation Tests", () => { StatementCoverage: { Executed: 100, Total: 100, - Percentage: 100 - } + Percentage: 100, + }, }; const html = codeCoverageSummary.getCoverageTable(mockCoverageData); @@ -350,8 +360,8 @@ describe("Coverage Table HTML Generation Tests", () => { StatementCoverage: { Executed: 333, Total: 1000, - Percentage: 33.333333 - } + Percentage: 33.333333, + }, }; const html = codeCoverageSummary.getCoverageTable(mockCoverageData); @@ -366,8 +376,8 @@ describe("HTML Structure and Alignment Tests", () => { StatementCoverage: { Executed: 80, Total: 100, - Percentage: 80.0 - } + Percentage: 80.0, + }, }; const html = codeCoverageSummary.getCoverageTable(mockCoverageData); @@ -376,7 +386,7 @@ describe("HTML Structure and Alignment Tests", () => { const document = dom.window.document; const rows = document.querySelectorAll("tr"); - rows.forEach(row => { + rows.forEach((row) => { expect(row.getAttribute("align")).toBe("center"); }); }); @@ -386,13 +396,13 @@ describe("HTML Structure and Alignment Tests", () => { StatementCoverage: { Executed: 80, Total: 100, - Percentage: 80.0 + Percentage: 80.0, }, FunctionCoverage: { Executed: 15, Total: 20, - Percentage: 75.0 - } + Percentage: 75.0, + }, }; const html = codeCoverageSummary.getCoverageTable(mockCoverageData); @@ -410,8 +420,8 @@ describe("Edge Cases and Special Values", () => { StatementCoverage: { Executed: 0, Total: 0, - Percentage: NaN - } + Percentage: NaN, + }, }; const html = codeCoverageSummary.getCoverageTable(mockCoverageData); @@ -424,8 +434,8 @@ describe("Edge Cases and Special Values", () => { StatementCoverage: { Executed: 0, Total: 0, - Percentage: undefined as any - } + Percentage: undefined as any, + }, }; const html = codeCoverageSummary.getCoverageTable(mockCoverageData); @@ -438,8 +448,8 @@ describe("Edge Cases and Special Values", () => { StatementCoverage: { Executed: 0, Total: 0, - Percentage: null as any - } + Percentage: null as any, + }, }; const html = codeCoverageSummary.getCoverageTable(mockCoverageData); @@ -452,8 +462,8 @@ describe("Edge Cases and Special Values", () => { StatementCoverage: { Executed: 999999, Total: 1000000, - Percentage: 99.9999 - } + Percentage: 99.9999, + }, }; const html = codeCoverageSummary.getCoverageTable(mockCoverageData); diff --git a/src/matlab.ts b/src/matlab.ts index 02dd1fe..f2403e0 100644 --- a/src/matlab.ts +++ b/src/matlab.ts @@ -37,9 +37,9 @@ export async function generateScript(workspaceDir: string, command: string): Pro encoding: "utf8", }); - return { - dir: temporaryDirectory, - command: scriptName + return { + dir: temporaryDirectory, + command: scriptName, }; } @@ -53,7 +53,13 @@ export async function generateScript(workspaceDir: string, command: string): Pro * @param architecture Architecture of the runner (e.g., "x64") * @param fn ExecFn that will execute a command on the runner */ -export async function runCommand(hs: HelperScript, platform: string, architecture: string, fn: ExecFn, args?: string[]): Promise { +export async function runCommand( + hs: HelperScript, + platform: string, + architecture: string, + fn: ExecFn, + args?: string[], +): Promise { const rmcPath = getRunMATLABCommandScriptPath(platform, architecture); await fs.chmod(rmcPath, 0o777); @@ -62,7 +68,7 @@ export async function runCommand(hs: HelperScript, platform: string, architectur let execArgs = [rmcArg]; if (args) { - execArgs = execArgs.concat(args); + execArgs = execArgs.concat(args); } const exitCode = await fn(rmcPath, execArgs); @@ -76,10 +82,12 @@ export async function runCommand(hs: HelperScript, platform: string, architectur * * @param platform Operating system of the runner (e.g., "win32" or "linux") * @param architecture Architecture of the runner (e.g., "x64") -*/ + */ export function getRunMATLABCommandScriptPath(platform: string, architecture: string): string { if (architecture != "x64" && !(platform == "darwin" && architecture == "arm64")) { - throw new Error(`This action is not supported on ${platform} runners using the ${architecture} architecture.`); + throw new Error( + `This action is not supported on ${platform} runners using the ${architecture} architecture.`, + ); } let ext; let platformDir; @@ -101,9 +109,10 @@ export function getRunMATLABCommandScriptPath(platform: string, architecture: st platformDir = "glnxa64"; break; default: - throw new Error(`This action is not supported on ${platform} runners using the ${architecture} architecture.`); + throw new Error( + `This action is not supported on ${platform} runners using the ${architecture} architecture.`, + ); } const rmcPath = path.join(import.meta.dirname, "bin", platformDir, `run-matlab-command${ext}`); return rmcPath; - } diff --git a/src/matlab.unit.test.ts b/src/matlab.unit.test.ts index 3bca266..93d891a 100644 --- a/src/matlab.unit.test.ts +++ b/src/matlab.unit.test.ts @@ -60,7 +60,7 @@ describe("script generation", () => { describe("run command", () => { const helperScript = { dir: "/home/sweet/home", command: "disp('hello, world');" }; const platform = "win32"; - const architecture = "x64" + const architecture = "x64"; it("ideally works", async () => { const chmod = jest.spyOn(fs, "chmod"); @@ -80,7 +80,11 @@ describe("run command", () => { chmod.mockResolvedValue(undefined); execFn.mockResolvedValue(0); - const actual = matlab.runCommand(helperScript, platform, architecture, execFn, ["-nojvm", "-logfile", "file"]); + const actual = matlab.runCommand(helperScript, platform, architecture, execFn, [ + "-nojvm", + "-logfile", + "file", + ]); await expect(actual).resolves.toBeUndefined(); expect(execFn.mock.calls[0][1]![1]).toBe("-nojvm"); expect(execFn.mock.calls[0][1]![2]).toBe("-logfile"); @@ -127,8 +131,8 @@ describe("run command", () => { }); describe("ci helper path", () => { - const platform = "linux" - const architecture = "x64" + const platform = "linux"; + const architecture = "x64"; const testExtension = (platform: string, ext: string) => { it(`considers the appropriate script on ${platform}`, () => { const actualPath = matlab.getRunMATLABCommandScriptPath(platform, architecture); @@ -143,7 +147,7 @@ describe("ci helper path", () => { expect(actualPath).toContain(subdirectory); }); }; - + testExtension("win32", "exe"); testExtension("darwin", ""); testExtension("linux", ""); @@ -154,10 +158,10 @@ describe("ci helper path", () => { testDirectory("linux", "x64", "glnxa64"); it("errors on unsupported platform", () => { - expect(() => matlab.getRunMATLABCommandScriptPath('sunos',architecture)).toThrow(); - }) + expect(() => matlab.getRunMATLABCommandScriptPath("sunos", architecture)).toThrow(); + }); it("errors on unsupported architecture", () => { - expect(() => matlab.getRunMATLABCommandScriptPath(platform, 'x86')).toThrow(); - }) + expect(() => matlab.getRunMATLABCommandScriptPath(platform, "x86")).toThrow(); + }); }); diff --git a/src/script.ts b/src/script.ts index 5b3e185..78d10d7 100644 --- a/src/script.ts +++ b/src/script.ts @@ -10,10 +10,8 @@ import * as path from "path"; * @returns MATLAB command. */ export function prepare(command: string): string { - const pluginsPath = path.join(import.meta.dirname, "plugins").replaceAll("'","''"); - return `cd(getenv('MW_ORIG_WORKING_FOLDER')); ` + - `addpath('` + pluginsPath + `'); ` - + command; + const pluginsPath = path.join(import.meta.dirname, "plugins").replaceAll("'", "''"); + return `cd(getenv('MW_ORIG_WORKING_FOLDER')); ` + `addpath('` + pluginsPath + `'); ` + command; } /** diff --git a/src/script.unit.test.ts b/src/script.unit.test.ts index 79fe7f8..1c8e4a0 100644 --- a/src/script.unit.test.ts +++ b/src/script.unit.test.ts @@ -8,8 +8,9 @@ describe("call generation", () => { it("ideally works", () => { // I know what your thinking const testCommand = "disp('hello world')"; - const pluginsPath = path.join(import.meta.dirname, "plugins").replaceAll("'","''"); - const expectedString = `cd(getenv('MW_ORIG_WORKING_FOLDER')); addpath('` + pluginsPath + `'); ${testCommand}`; + const pluginsPath = path.join(import.meta.dirname, "plugins").replaceAll("'", "''"); + const expectedString = + `cd(getenv('MW_ORIG_WORKING_FOLDER')); addpath('` + pluginsPath + `'); ${testCommand}`; expect(script.prepare(testCommand)).toMatch(expectedString); }); diff --git a/src/testResultsSummary.ts b/src/testResultsSummary.ts index 714efa8..a835b19 100644 --- a/src/testResultsSummary.ts +++ b/src/testResultsSummary.ts @@ -70,7 +70,7 @@ export function processAndAddTestSummary( ) { const testResultsData = getTestResults(runnerTemp, runId, workspace); const coverageResultsData = getCoverageResults(runnerTemp, runId); - if(testResultsData || coverageResultsData) { + if (testResultsData || coverageResultsData) { addSummary(testResultsData, coverageResultsData, actionName); } } @@ -143,8 +143,8 @@ export function addSummary( // Add test results table if available if (testResultsData) { const helpLink = - `â„šī¸`; + `â„šī¸`; const header = getTestHeader(testResultsData.Stats); core.summary @@ -161,9 +161,7 @@ export function addSummary( // Add detailed test results if (testResultsData) { const detailedResults = getDetailedResults(testResultsData.TestResults); - core.summary - .addHeading("All tests", 3) - .addRaw(detailedResults, true); + core.summary.addHeading("All tests", 3).addRaw(detailedResults, true); } } catch (e) { console.error("An error occurred while adding the test results to the summary:", e); @@ -175,19 +173,39 @@ export function getTestHeader(stats: TestStatistics): string { ` - - - - + + + + - - - - - - + + + + + +
Total testsPassed ` + getStatusEmoji(MatlabTestStatus.PASSED) + `Failed ` + getStatusEmoji(MatlabTestStatus.FAILED) + `Incomplete ` + getStatusEmoji(MatlabTestStatus.INCOMPLETE) + `Not Run ` + getStatusEmoji(MatlabTestStatus.NOT_RUN) + `Passed ` + + getStatusEmoji(MatlabTestStatus.PASSED) + + `Failed ` + + getStatusEmoji(MatlabTestStatus.FAILED) + + `Incomplete ` + + getStatusEmoji(MatlabTestStatus.INCOMPLETE) + + `Not Run ` + + getStatusEmoji(MatlabTestStatus.NOT_RUN) + + ` Duration(s) ⌛
` + stats.Total + `` + stats.Passed + `` + stats.Failed + `` + stats.Incomplete + `` + stats.NotRun + `` + stats.Duration.toFixed(2) + `` + + stats.Total + + `` + + stats.Passed + + `` + + stats.Failed + + `` + + stats.Incomplete + + `` + + stats.NotRun + + `` + + stats.Duration.toFixed(2) + + `
` ); @@ -200,10 +218,10 @@ export function getDetailedResults(testResults: MatlabTestFile[][]): string { Test File Duration(s) ` + - testResults - .flat() - .map((file) => generateTestFileRow(file)) - .join("") + + testResults + .flat() + .map((file) => generateTestFileRow(file)) + .join("") + `` ); } @@ -216,11 +234,17 @@ function generateTestFileRow(file: MatlabTestFile): string { return ( ` - + - ` + - statusEmoji + ` ` + file.Name + - ` + ` + + statusEmoji + + ` ` + + file.Name + + `
@@ -229,15 +253,15 @@ function generateTestFileRow(file: MatlabTestFile): string { ` + - file.TestCases.map((tc) => generateTestCaseRow(tc)).join("") + - `
Diagnostics Duration(s)
+ file.TestCases.map((tc) => generateTestCaseRow(tc)).join("") + + ` ` + - `` + - file.Duration.toFixed(2) + - `` + - ` + `` + + file.Duration.toFixed(2) + + `` + + ` ` ); } @@ -246,26 +270,32 @@ function generateTestCaseRow(testCase: MatlabTestCase): string { const statusEmoji = getStatusEmoji(testCase.Status); const diagnosticsColumn = testCase.Diagnostics.length > 0 - ? testCase.Diagnostics - .map( - (diagnostic) => - `
` + - `` + - diagnostic.Event + - `` + - `
` +
-                                diagnostic.Report.replace(/\n/g, "
").trim() + - `
` + - `
`, - ) - .join("") + ? testCase.Diagnostics.map( + (diagnostic) => + `
` + + `` + + diagnostic.Event + + `` + + `
` +
+                      diagnostic.Report.replace(/\n/g, "
").trim() + + `
` + + `
`, + ).join("") : ""; return ( `` + - `` + statusEmoji + ` ` + testCase.Name + `` + - `` + diagnosticsColumn + `` + - `` + testCase.Duration.toFixed(2) + `` + + `` + + statusEmoji + + ` ` + + testCase.Name + + `` + + `` + + diagnosticsColumn + + `` + + `` + + testCase.Duration.toFixed(2) + + `` + `` ); } @@ -337,7 +367,9 @@ function determineTestStatus(testResult: MatlabTestResultJson): MatlabTestStatus } } -function processDiagnostics(diagnostics: MatlabTestDiagnostics | MatlabTestDiagnostics[] | undefined): MatlabTestDiagnostics[] { +function processDiagnostics( + diagnostics: MatlabTestDiagnostics | MatlabTestDiagnostics[] | undefined, +): MatlabTestDiagnostics[] { if (!diagnostics) return []; return Array.isArray(diagnostics) ? diagnostics : [diagnostics]; diff --git a/src/testResultsSummary.unit.test.ts b/src/testResultsSummary.unit.test.ts index 700f57d..73fddf1 100644 --- a/src/testResultsSummary.unit.test.ts +++ b/src/testResultsSummary.unit.test.ts @@ -1,6 +1,6 @@ // Copyright 2025-2026 The MathWorks, Inc. -import {jest, describe, it, expect, beforeAll} from "@jest/globals"; +import { jest, describe, it, expect, beforeAll } from "@jest/globals"; import * as path from "path"; import * as os from "os"; import * as nodeFs from "fs"; @@ -29,7 +29,7 @@ jest.unstable_mockModule("fs", () => ({ const core = await import("@actions/core"); const fs = await import("fs"); const testResultsSummary = await import("./testResultsSummary.js"); -const {MatlabTestStatus} = testResultsSummary; +const { MatlabTestStatus } = testResultsSummary; describe("Artifact Processing Tests", () => { // Shared test data @@ -59,11 +59,17 @@ describe("Artifact Processing Tests", () => { return { osName: "windows", workspaceParent: "C:\\" }; if (platform.includes("linux") || platform.includes("unix") || platform.includes("aix")) return { osName: "linux", workspaceParent: "/home/user/" }; - if (platform.includes("darwin")) return { osName: "mac", workspaceParent: "/Users/username/" }; + if (platform.includes("darwin")) + return { osName: "mac", workspaceParent: "/Users/username/" }; throw new Error(`Unsupported OS: ${platform}`); } - function copyTestDataFile(osName: string, runnerTemp: string, runId: string, actionName: string) { + function copyTestDataFile( + osName: string, + runnerTemp: string, + runId: string, + actionName: string, + ) { const sourceFilePath = path.join( import.meta.dirname, "test-data", @@ -120,18 +126,10 @@ describe("Artifact Processing Tests", () => { expect(testResults[0][0].TestCases[8].Name).toBe("testInvalidDateFormat"); expect(testResults[1][0].TestCases[0].Name).toBe("testNonLeapYear"); - expect(testResults[0][0].TestCases[0].Status).toBe( - MatlabTestStatus.PASSED, - ); - expect(testResults[0][0].TestCases[4].Status).toBe( - MatlabTestStatus.FAILED, - ); - expect(testResults[0][0].TestCases[8].Status).toBe( - MatlabTestStatus.NOT_RUN, - ); - expect(testResults[1][0].TestCases[0].Status).toBe( - MatlabTestStatus.INCOMPLETE, - ); + expect(testResults[0][0].TestCases[0].Status).toBe(MatlabTestStatus.PASSED); + expect(testResults[0][0].TestCases[4].Status).toBe(MatlabTestStatus.FAILED); + expect(testResults[0][0].TestCases[8].Status).toBe(MatlabTestStatus.NOT_RUN); + expect(testResults[1][0].TestCases[0].Status).toBe(MatlabTestStatus.INCOMPLETE); expect(testResults[0][0].TestCases[0].Duration).toBeCloseTo(0.1); expect(testResults[0][0].TestCases[1].Duration).toBeCloseTo(0.11); @@ -382,7 +380,11 @@ describe("Error Handling Tests", () => { fs.writeFileSync(invalidJsonPath, "{ invalid json content"); try { - const result = testResultsSummary.getTestResults(process.env.RUNNER_TEMP, process.env.GITHUB_RUN_ID, ""); + const result = testResultsSummary.getTestResults( + process.env.RUNNER_TEMP, + process.env.GITHUB_RUN_ID, + "", + ); expect(result).toBeNull(); // Verify error was logged @@ -419,9 +421,13 @@ describe("Error Handling Tests", () => { fs.writeFileSync(validJsonPath, "[]"); // Empty array - valid JSON try { - const result = testResultsSummary.getTestResults(process.env.RUNNER_TEMP, process.env.GITHUB_RUN_ID, ""); + const result = testResultsSummary.getTestResults( + process.env.RUNNER_TEMP, + process.env.GITHUB_RUN_ID, + "", + ); - if(result){ + if (result) { // Should still return results even if deletion fails expect(result).toBeDefined(); expect(result.TestResults).toEqual([]); @@ -450,4 +456,4 @@ describe("Error Handling Tests", () => { } } }); -}); \ No newline at end of file +});