diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c769019..893a46e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,4 +54,4 @@ jobs: - name: Test Local Action uses: ./ with: - include: ./fixtures/*.xml + include: ./__tests__/fixtures/*.xml diff --git a/fixtures/Sample.xml b/__tests__/fixtures/Sample.xml similarity index 100% rename from fixtures/Sample.xml rename to __tests__/fixtures/Sample.xml diff --git a/fixtures/Sample2.xml b/__tests__/fixtures/Sample2.xml similarity index 100% rename from fixtures/Sample2.xml rename to __tests__/fixtures/Sample2.xml diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts new file mode 100644 index 0000000..9eca35b --- /dev/null +++ b/__tests__/main.test.ts @@ -0,0 +1,159 @@ +/** + * Unit tests for the action's main functionality, src/main.ts + * + * These should be run as if the action was called from a workflow. + * Specifically, the inputs listed in `action.yml` should be set as environment + * variables following the pattern `INPUT_`. + */ + +import * as core from '@actions/core' +import * as main from '../src/main' + +// Mock the action's main function +const runMock = jest.spyOn(main, 'run') + +// Mock the GitHub Actions core library +let infoMock: jest.SpyInstance +let errorMock: jest.SpyInstance +let getInputMock: jest.SpyInstance +let setFailedMock: jest.SpyInstance + +describe('action', () => { + beforeEach(() => { + jest.clearAllMocks() + process.env.RUNNER_TEMP = '/tmp' + infoMock = jest.spyOn(core, 'info').mockImplementation() + errorMock = jest.spyOn(core, 'error').mockImplementation() + getInputMock = jest.spyOn(core, 'getInput').mockImplementation() + setFailedMock = jest.spyOn(core, 'setFailed').mockImplementation() + }) + + it('successfully completes', async () => { + await main.run() + expect(runMock).toHaveReturned() + expect(errorMock).not.toHaveBeenCalled() + expect(setFailedMock).not.toHaveBeenCalled() + expect(infoMock).toHaveBeenLastCalledWith( + 'junit-reducer exited successfully' + ) + }) + + it('successfully completes with a specific version', async () => { + getInputMock.mockImplementation((name: string): string => { + switch (name) { + case 'version': + return 'v1.2.0' + default: + return '' + } + }) + + await main.run() + expect(runMock).toHaveReturned() + expect(errorMock).not.toHaveBeenCalled() + expect(setFailedMock).not.toHaveBeenCalled() + expect(infoMock).toHaveBeenLastCalledWith( + 'junit-reducer exited successfully' + ) + }) + + describe('on mock Windows', () => { + beforeEach(() => { + process.env.OVERRIDE_PLATFORM = 'win32' + }) + it('successfully downloads the Windows binary', async () => { + await main.run() + expect(runMock).toHaveReturned() + expect(infoMock).toHaveBeenCalledWith('Windows archive downloaded') + expect(errorMock).toHaveBeenCalledWith( + "Can't install extension on mocked platform" + ) + }) + afterEach(() => { + delete process.env.OVERRIDE_PLATFORM + }) + }) + + describe('on mock Mac', () => { + beforeEach(() => { + process.env.OVERRIDE_PLATFORM = 'darwin' + }) + it('successfully downloads the Mac binary', async () => { + await main.run() + expect(runMock).toHaveReturned() + expect(infoMock).toHaveBeenCalledWith('Mac archive downloaded') + expect(errorMock).toHaveBeenCalledWith( + "Can't install extension on mocked platform" + ) + }) + afterEach(() => { + delete process.env.OVERRIDE_PLATFORM + }) + }) + + describe('on mock Linux', () => { + beforeEach(() => { + process.env.OVERRIDE_PLATFORM = 'linux' + }) + it('successfully downloads the Linux binary', async () => { + await main.run() + expect(runMock).toHaveReturned() + expect(infoMock).toHaveBeenCalledWith('Linux archive downloaded') + expect(errorMock).toHaveBeenCalledWith( + "Can't install extension on mocked platform" + ) + }) + afterEach(() => { + delete process.env.OVERRIDE_PLATFORM + }) + }) + + describe('on any other platform', () => { + beforeEach(() => { + process.env.OVERRIDE_PLATFORM = 'unknown' + }) + it('successfully downloads the Linux binary', async () => { + await main.run() + expect(runMock).toHaveReturned() + expect(infoMock).toHaveBeenCalledWith('Linux archive downloaded') + expect(errorMock).toHaveBeenCalledWith( + "Can't install extension on mocked platform" + ) + }) + afterEach(() => { + delete process.env.OVERRIDE_PLATFORM + }) + }) + + it('sets a failed status for invalid version', async () => { + // Set the action's inputs as return values from core.getInput() + getInputMock.mockImplementation((name: string): string => { + switch (name) { + case 'version': + return 'this is not a number' + default: + return '' + } + }) + + await main.run() + expect(runMock).toHaveReturned() + + // Verify that all of the core library functions were called correctly + expect(setFailedMock).toHaveBeenNthCalledWith( + 1, + 'invalid version string: this is not a number' + ) + expect(errorMock).not.toHaveBeenCalled() + }) + + it('sets a failed status for non-zero status code thrown by the binary', async () => { + // Set input variable 'include' to a non-existent file glob + process.env.INPUT_include = 'this-file-does-not-exist.xml' + + await main.run() + expect(runMock).toHaveReturned() + expect(errorMock).not.toHaveBeenCalled() + expect(setFailedMock).toHaveBeenCalled() + }) +}) diff --git a/badges/coverage.svg b/badges/coverage.svg index 89ac565..5bb55be 100644 --- a/badges/coverage.svg +++ b/badges/coverage.svg @@ -1 +1 @@ -Coverage: 14%Coverage14% \ No newline at end of file +Coverage: 100%Coverage100% \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index 8c1c42a..c527056 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6618,6 +6618,23 @@ const core = __importStar(__nccwpck_require__(2186)); const tc = __importStar(__nccwpck_require__(7784)); const exec = __importStar(__nccwpck_require__(1514)); const BASE_RELEASE_URL = 'https://github.com/willgeorgetaylor/junit-reducer/releases/'; +const PLATFORMS = { + win32: { + os: 'Windows', + suffix: 'Windows_x86_64.zip', + extractionFunction: tc.extractZip + }, + darwin: { + os: 'Mac', + suffix: 'Darwin_x86_64.tar.gz', + extractionFunction: tc.extractTar + }, + linux: { + os: 'Linux', + suffix: 'Linux_x86_64.tar.gz', + extractionFunction: tc.extractTar + } +}; // We're doing it this way so we can passthrough // inputs without needing to extract them individually. function enumerateInputs() { @@ -6633,10 +6650,17 @@ function enumerateInputs() { } return inputs; } +// For latest: https://github.com/willgeorgetaylor/junit-reducer/releases/latest/download/junit-reducer_Darwin_x86_64.tar.gz +// For a specific version: https://github.com/willgeorgetaylor/junit-reducer/releases/download/v1.2.0/junit-reducer_Darwin_x86_64.tar.gz function formatReleaseUrl(suffix, version) { if (version === 'latest') return `${BASE_RELEASE_URL}latest/download/junit-reducer_${suffix}`; - return `${BASE_RELEASE_URL}download/${version}/junit-reducer_${suffix}`; + else + return `${BASE_RELEASE_URL}download/${version}/junit-reducer_${suffix}`; +} +function getProcessPlatform() { + // This is a function so we can mock it in tests + return process.env.OVERRIDE_PLATFORM || process.platform; } /** * The main function for the action. @@ -6644,48 +6668,48 @@ function formatReleaseUrl(suffix, version) { */ async function run() { const version = core.getInput('version') || 'latest'; + const isPlatformOverride = process.env.OVERRIDE_PLATFORM !== undefined; + if (version !== 'latest' && !/^v\d+\.\d+\.\d+$/.test(version)) { + core.setFailed(`invalid version string: ${version}`); + return; + } try { - if (process.platform === 'win32') { - // Windows - const reducerReleaseUrl = await tc.downloadTool(formatReleaseUrl('Windows_x86_64.zip', version)); - const pathToCLI = await core.group('Downloading Windows binary (.zip)', async () => { - core.info(`Downloading from ${reducerReleaseUrl}...`); - const path = tc.extractZip(reducerReleaseUrl); - return path; - }); - core.addPath(pathToCLI); - } - else if (process.platform === 'darwin') { - // Mac - const reducerReleaseUrl = await tc.downloadTool(formatReleaseUrl('Darwin_x86_64.tar.gz', version)); - const pathToCLI = await core.group('Downloading Mac binary (.tar.gz)', async () => { - core.info(`Downloading from ${reducerReleaseUrl}...`); - const path = tc.extractTar(reducerReleaseUrl); - return path; - }); - core.addPath(pathToCLI); - } - else { - // Linux - const reducerReleaseUrl = await tc.downloadTool(formatReleaseUrl('Linux_x86_64.tar.gz', version)); - const pathToCLI = await core.group('Downloading Linux binary (.tar.gz)', async () => { - core.info(`Downloading from ${reducerReleaseUrl}...`); - const path = tc.extractTar(reducerReleaseUrl); - return path; - }); - core.addPath(pathToCLI); + const platform = PLATFORMS[getProcessPlatform()] || PLATFORMS.linux; + const tmp = await core.group(`Downloading ${platform.os} archive (${platform.suffix}})`, async () => { + const url = formatReleaseUrl(platform.suffix, version); + core.info(`Downloading from ${url}...`); + const download = await tc.downloadTool(url); + core.info(`${platform.os} archive downloaded`); + return download; + }); + // Only extract and install the extension if we're not + // mocking the OS, since it'll fail anyway. + if (isPlatformOverride) { + core.error("Can't install extension on mocked platform"); + return; } + const pathToCLI = await core.group(`Extracting ${platform.os} binary`, async () => { + core.info(`Extracting from ${tmp}`); + const path = await platform.extractionFunction(tmp); + core.info(`Extracted to ${path}`); + return path; + }); + core.addPath(pathToCLI); const inputs = enumerateInputs(); - const args = Object.entries(inputs).map(([key, value]) => `--${key}=${value}`); - core.startGroup(`Running junit-reducer with arguments: `); + const args = Object.entries(inputs).map(([key, value]) => `--${key} = ${value}`); + core.startGroup(`Running junit - reducer with arguments: `); core.info(Object.entries(inputs) - .map(([key, value]) => `${key}: ${value}`) + .map(([key, value]) => `${key}: ${value} `) .join('\n')); core.endGroup(); - await exec.exec('junit-reducer', args); + const exitCode = await exec.exec('junit-reducer', args); + if (exitCode === 0) { + core.info('junit-reducer exited successfully'); + } } catch (error) { // Fail the workflow run if an error occurs + console.log(error); if (error instanceof Error) core.setFailed(error.message); } diff --git a/output/Sample.xml b/output/Sample.xml new file mode 100644 index 0000000..48b2bb9 --- /dev/null +++ b/output/Sample.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index 8da4856..fb009c0 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "jest": { "preset": "ts-jest", "verbose": true, + "silent": false, "clearMocks": true, "testEnvironment": "node", "moduleFileExtensions": [ @@ -87,4 +88,4 @@ "ts-jest": "^29.1.1", "typescript": "^5.3.3" } -} +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 773c82a..ab4fa1c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,32 @@ import * as exec from '@actions/exec' const BASE_RELEASE_URL = 'https://github.com/willgeorgetaylor/junit-reducer/releases/' +type Platform = { + os: string + suffix: string + extractionFunction: (path: string) => Promise +} + +const PLATFORMS: { + [key: string]: Platform +} = { + win32: { + os: 'Windows', + suffix: 'Windows_x86_64.zip', + extractionFunction: tc.extractZip + }, + darwin: { + os: 'Mac', + suffix: 'Darwin_x86_64.tar.gz', + extractionFunction: tc.extractTar + }, + linux: { + os: 'Linux', + suffix: 'Linux_x86_64.tar.gz', + extractionFunction: tc.extractTar + } +} + // We're doing it this way so we can passthrough // inputs without needing to extract them individually. function enumerateInputs(): Record { @@ -21,10 +47,17 @@ function enumerateInputs(): Record { return inputs } +// For latest: https://github.com/willgeorgetaylor/junit-reducer/releases/latest/download/junit-reducer_Darwin_x86_64.tar.gz +// For a specific version: https://github.com/willgeorgetaylor/junit-reducer/releases/download/v1.2.0/junit-reducer_Darwin_x86_64.tar.gz function formatReleaseUrl(suffix: string, version: string): string { if (version === 'latest') return `${BASE_RELEASE_URL}latest/download/junit-reducer_${suffix}` - return `${BASE_RELEASE_URL}download/${version}/junit-reducer_${suffix}` + else return `${BASE_RELEASE_URL}download/${version}/junit-reducer_${suffix}` +} + +function getProcessPlatform(): string { + // This is a function so we can mock it in tests + return process.env.OVERRIDE_PLATFORM || process.platform } /** @@ -33,68 +66,66 @@ function formatReleaseUrl(suffix: string, version: string): string { */ export async function run(): Promise { const version = core.getInput('version') || 'latest' + const isPlatformOverride = process.env.OVERRIDE_PLATFORM !== undefined + + if (version !== 'latest' && !/^v\d+\.\d+\.\d+$/.test(version)) { + core.setFailed(`invalid version string: ${version}`) + return + } try { - if (process.platform === 'win32') { - // Windows - const reducerReleaseUrl = await tc.downloadTool( - formatReleaseUrl('Windows_x86_64.zip', version) - ) - const pathToCLI = await core.group( - 'Downloading Windows binary (.zip)', - async () => { - core.info(`Downloading from ${reducerReleaseUrl}...`) - const path = tc.extractZip(reducerReleaseUrl) - return path - } - ) - core.addPath(pathToCLI) - } else if (process.platform === 'darwin') { - // Mac - const reducerReleaseUrl = await tc.downloadTool( - formatReleaseUrl('Darwin_x86_64.tar.gz', version) - ) - const pathToCLI = await core.group( - 'Downloading Mac binary (.tar.gz)', - async () => { - core.info(`Downloading from ${reducerReleaseUrl}...`) - const path = tc.extractTar(reducerReleaseUrl) - return path - } - ) - core.addPath(pathToCLI) - } else { - // Linux - const reducerReleaseUrl = await tc.downloadTool( - formatReleaseUrl('Linux_x86_64.tar.gz', version) - ) - const pathToCLI = await core.group( - 'Downloading Linux binary (.tar.gz)', - async () => { - core.info(`Downloading from ${reducerReleaseUrl}...`) - const path = tc.extractTar(reducerReleaseUrl) - return path - } - ) - core.addPath(pathToCLI) + const platform = PLATFORMS[getProcessPlatform()] || PLATFORMS.linux + + const tmp = await core.group( + `Downloading ${platform.os} archive (${platform.suffix}})`, + async () => { + const url = formatReleaseUrl(platform.suffix, version) + core.info(`Downloading from ${url}...`) + const download = await tc.downloadTool(url) + core.info(`${platform.os} archive downloaded`) + return download + } + ) + + // Only extract and install the extension if we're not + // mocking the OS, since it'll fail anyway. + if (isPlatformOverride) { + core.error("Can't install extension on mocked platform") + return } + const pathToCLI = await core.group( + `Extracting ${platform.os} binary`, + async () => { + core.info(`Extracting from ${tmp}`) + const path = await platform.extractionFunction(tmp) + core.info(`Extracted to ${path}`) + return path + } + ) + core.addPath(pathToCLI) + const inputs = enumerateInputs() const args: string[] = Object.entries(inputs).map( - ([key, value]) => `--${key}=${value}` + ([key, value]) => `--${key} = ${value}` ) - core.startGroup(`Running junit-reducer with arguments: `) + core.startGroup(`Running junit - reducer with arguments: `) core.info( Object.entries(inputs) - .map(([key, value]) => `${key}: ${value}`) + .map(([key, value]) => `${key}: ${value} `) .join('\n') ) core.endGroup() - await exec.exec('junit-reducer', args) + const exitCode = await exec.exec('junit-reducer', args) + + if (exitCode === 0) { + core.info('junit-reducer exited successfully') + } } catch (error) { // Fail the workflow run if an error occurs + console.log(error) if (error instanceof Error) core.setFailed(error.message) } }