From 22aac7bc4494dd8bf34e3ee1bbb11d20036513a5 Mon Sep 17 00:00:00 2001 From: Thach Nguyen Date: Tue, 2 Aug 2022 20:51:49 +0700 Subject: [PATCH] Implement unit tests for the rest of `installer` methods --- __tests__/installer.test.ts | 135 +++++++++++++++++++++++++++++------- __tests__/main.test.ts | 97 ++++++++++++++++++++------ __tests__/utils.test.ts | 8 +-- jest.config.json | 3 +- 4 files changed, 192 insertions(+), 51 deletions(-) diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index 5b4ae00..0551c46 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -1,18 +1,28 @@ -/* eslint @typescript-eslint/consistent-type-imports: 0 */ import * as os from 'os'; import * as path from 'path'; -import { IncomingMessage } from 'http'; +import { existsSync, promises as fs } from 'fs'; import * as core from '@actions/core'; -import { HttpClient, HttpCodes } from '@actions/http-client'; +import * as hm from '@actions/http-client'; +import * as tc from '@actions/tool-cache'; import * as installer from '../src/installer'; +import { getVersionFromToolcachePath } from '../src/utils'; // Mocking modules jest.mock('@actions/core'); const CACHE_PATH = path.join(__dirname, 'runner'); +function mockHttpClientGet(responseBody: string, statusCode = hm.HttpCodes.OK): void { + jest.spyOn(hm, 'HttpClient').mockReturnValue(({ + get: jest.fn().mockResolvedValue({ + message: { statusCode, statusMessage: '' }, + readBody: jest.fn().mockResolvedValue(responseBody) + }) + } as unknown) as hm.HttpClient); +} + function createXmlManifest(...versions: readonly string[]): string { return versions.map(ver => `${ver}`).join(); } @@ -32,10 +42,7 @@ describe('getAvailableVersions', () => { }); it('failed to download versions manifest', async () => { - jest.spyOn(HttpClient.prototype, 'get').mockResolvedValue({ - message: ({ statusCode: 0 } as unknown) as IncomingMessage, - readBody: jest.fn().mockResolvedValue('') - }); + mockHttpClientGet('', 0); await expect(installer.getAvailableVersions()).rejects.toThrow( /Unable to get available versions from/i @@ -48,10 +55,7 @@ describe('getAvailableVersions', () => { [` bar${createXmlManifest('')} foo`, []], [` ${createXmlManifest(' 1.x', 'foo')}!`, [' 1.x', 'foo']] ])('%s -> %j', async (xml: string, expected: readonly string[]) => { - jest.spyOn(HttpClient.prototype, 'get').mockResolvedValue({ - message: ({ statusCode: HttpCodes.OK } as unknown) as IncomingMessage, - readBody: jest.fn().mockResolvedValue(xml) - }); + mockHttpClientGet(xml); const availableVersions = await installer.getAvailableVersions(); expect(availableVersions).toStrictEqual(expected); @@ -69,10 +73,7 @@ describe('findVersionForDownload', () => { ['* ', ['foo', ' ', ' 1.0.x ', '3.0']], [' >=3', [' 2.0.1', '!', ' 3.0.x ', '3.3']] ])('%s %j', async (spec: string, versions: readonly string[]) => { - jest.spyOn(HttpClient.prototype, 'get').mockResolvedValue({ - message: ({ statusCode: HttpCodes.OK } as unknown) as IncomingMessage, - readBody: jest.fn().mockResolvedValue(createXmlManifest(...versions)) - }); + mockHttpClientGet(createXmlManifest(...versions)); await expect(installer.findVersionForDownload(spec)).rejects.toThrow( new RegExp(`not find.* version for.* ${spec}`, 'i') @@ -86,10 +87,7 @@ describe('findVersionForDownload', () => { [' * ', ['!', '1.0.1', ' 3.1.0 ', '2.0.1 ', '3.3.0-alpha-1'], '3.1.0'], ['>=1 ', [' ', '1.1.0-beta-1', ' 1.0.1 ', ' 1.0.1-1'], '1.0.1'] ])('%s %j -> %s', async (spec: string, versions: readonly string[], expected: string) => { - jest.spyOn(HttpClient.prototype, 'get').mockResolvedValue({ - message: ({ statusCode: HttpCodes.OK } as unknown) as IncomingMessage, - readBody: jest.fn().mockResolvedValue(createXmlManifest(...versions)) - }); + mockHttpClientGet(createXmlManifest(...versions)); const resolvedVersion = await installer.findVersionForDownload(spec); expect(resolvedVersion).toBe(expected); @@ -98,13 +96,100 @@ describe('findVersionForDownload', () => { }); }); -process.env.RUNNER_TEMP = os.tmpdir(); -process.env.RUNNER_TOOL_CACHE = CACHE_PATH; +describe('download & setup Maven', () => { + process.env.RUNNER_TEMP = os.tmpdir(); + process.env.RUNNER_TOOL_CACHE = CACHE_PATH; -describe('downloadMaven', () => { - it.todo('download a real version of Maven'); + afterEach(async () => { + await fs.rmdir(CACHE_PATH, { recursive: true }); + }); - it.todo('raises error if download failed'); + describe('downloadMaven', () => { + const TEST_VERSION = '3.3.3'; - it.todo('raises error when extracting failed'); + it('download a real version of Maven', async () => { + const toolPath = await installer.downloadMaven(TEST_VERSION); + + expect(core.info).toHaveBeenCalledWith( + expect.stringMatching(new RegExp(`Downloading Maven ${TEST_VERSION} from`, 'i')) + ); + + expect(existsSync(`${toolPath}.complete`)).toBe(true); + expect(existsSync(path.join(CACHE_PATH, 'maven', TEST_VERSION))).toBe(true); + expect(getVersionFromToolcachePath(toolPath)).toBe(TEST_VERSION); + }); + + it('raises error if download failed', async () => { + mockHttpClientGet('', 1); + await expect(installer.downloadMaven(TEST_VERSION)).rejects.toThrow(/Unexpected HTTP.* 1/i); + + expect(core.info).toHaveBeenCalledTimes(1); + expect(core.debug).toHaveBeenCalledWith( + expect.stringMatching(/Failed to download.* Code\(1\)/i) + ); + }); + + it('raises error when extracting failed', async () => { + const spyDownload = jest.spyOn(tc, 'downloadTool').mockResolvedValue(__filename); + + await expect(installer.downloadMaven(TEST_VERSION)).rejects.toThrow(/failed.* exit code 1/i); + + expect(spyDownload).toHaveBeenCalledWith(expect.stringContaining(TEST_VERSION)); + expect(core.debug).toHaveBeenCalledWith(expect.stringContaining('tar')); + }); + }); + + describe('setupMaven', () => { + const TEST_VERSION = '3.2.5'; + const TOOL_PATH = path.join(CACHE_PATH, 'maven', TEST_VERSION, os.arch()); + + beforeEach(async () => { + await fs.mkdir(TOOL_PATH, { recursive: true }); + await fs.writeFile(`${TOOL_PATH}.complete`, ''); + }); + + describe('reuses the cached version of Maven', () => { + it.each([ + [TEST_VERSION, TEST_VERSION.replace(/\d+$/, 'x '), undefined], + [TEST_VERSION, TEST_VERSION.replace(/\.\d+$/, ''), TEST_VERSION.replace(/\d+$/, '0 ')] + ])('%s <- %s', async (expected: string, spec: string, active?: string) => { + const resolvedVersion = await installer.setupMaven(spec, active); + + expect(resolvedVersion).toBe(expected); + expect(core.addPath).toHaveBeenCalledWith( + expect.stringMatching(new RegExp(`\\b${expected}\\b.*[\\\\/]bin$`)) + ); + }); + }); + + describe('uses version of system Maven', () => { + it.each([ + [' 3.8', '3.8.2', ''], + ['3.x ', '3.3.9', TEST_VERSION] + ])('%s -> %s', async (spec: string, expected: string, resolved: string) => { + const resolvedVersion = await installer.setupMaven(spec, expected); + + expect(resolvedVersion).toBe(expected); + expect(core.info).toHaveBeenCalledWith( + expect.stringMatching(new RegExp(`Use.* ${expected} instead of .*\\b${resolved}`, 'i')) + ); + expect(core.addPath).not.toHaveBeenCalled(); + }); + }); + + it('install a new version of Maven', async () => { + const expected = '3.6.3'; + mockHttpClientGet(createXmlManifest('3.5.2 ', ` ${expected}`, '3.6.1')); + + jest.spyOn(tc, 'downloadTool').mockResolvedValue(''); + jest.spyOn(tc, 'extractTar').mockResolvedValue(''); + const spyCache = jest.spyOn(tc, 'cacheDir').mockResolvedValue('foo'); + + const resolvedVersion = await installer.setupMaven(' >3.5'); + + expect(spyCache).toHaveBeenCalledWith(expect.stringContaining(expected), 'maven', expected); + expect(resolvedVersion).toBe(expected); + expect(core.addPath).toHaveBeenCalledWith(path.join('foo', 'bin')); + }); + }); }); diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 43aea9c..7732ebf 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -1,15 +1,20 @@ +import * as os from 'os'; +import * as path from 'path'; +import { existsSync, promises as fs } from 'fs'; + import * as core from '@actions/core'; -import { getActiveMavenVersion } from '../src/utils'; -import { setupMaven } from '../src/installer'; +import * as utils from '../src/utils'; +import * as installer from '../src/installer'; import { run } from '../src/main'; // Mocking modules jest.mock('@actions/core'); -jest.mock('../src/utils'); -jest.mock('../src/installer'); +const MVN_PATH = path.join(__dirname, 'data'); const DEFAULT_VERSION = '3'; +const REAL_VERSION = '3.5.2'; +const CACHE_PATH = path.join(__dirname, 'runner'); describe('failed to run with invalid inputs', () => { it.each([ @@ -25,33 +30,83 @@ describe('failed to run with invalid inputs', () => { }); describe('run with valid inputs', () => { - it('setups default version when no Maven is installed', async () => { + it.each([ + // Default version + no Maven is installed + [{ setup: 'foo', spec: '' }, [DEFAULT_VERSION, undefined]], + [{ active: '', setup: '3.0', spec: ' * ' }, [' * ', '']], + // Installed version !~ version input + [{ active: '3.5.2', setup: DEFAULT_VERSION, spec: ' 3.3' }, [' 3.3', undefined]], + // Installed version =~ version input + [{ active: '3.3.9', setup: '', spec: '3.x ' }, ['3.x ', '3.3.9']] + ])( + '%o -> %j', + async ( + version: Readonly<{ spec: string; active?: string; setup: string }>, + expected: readonly (string | undefined)[] + ) => { + (core.getInput as jest.Mock).mockReturnValue(version.spec); + jest.spyOn(utils, 'getActiveMavenVersion').mockResolvedValue(version.active); + const spySetup = jest.spyOn(installer, 'setupMaven').mockResolvedValue(version.setup); + + await run(); + expect(spySetup).toHaveBeenCalledWith(...expected); + expect(core.setOutput).toHaveBeenCalledWith('version', version.setup); + } + ); +}); + +describe('integration tests', () => { + const ORIGINAL_PATH = process.env.PATH; + const TEST_VERSION = '3.1.1'; + const TOOL_PATH = path.join(CACHE_PATH, 'maven', TEST_VERSION, os.arch()); + + process.env.RUNNER_TEMP = os.tmpdir(); + process.env.RUNNER_TOOL_CACHE = CACHE_PATH; + + beforeEach(() => { + process.env.PATH = `${MVN_PATH}${path.delimiter}${ORIGINAL_PATH ?? ''}`; + }); + + afterEach(async () => { + process.env.PATH = ORIGINAL_PATH; + await fs.rmdir(CACHE_PATH, { recursive: true }); + }); + + it('uses system Maven if real version =~ default version', async () => { (core.getInput as jest.Mock).mockReturnValue(''); - (getActiveMavenVersion as jest.Mock).mockResolvedValue(undefined); - (setupMaven as jest.Mock).mockResolvedValue('foo'); await run(); - expect(setupMaven).toHaveBeenCalledWith(DEFAULT_VERSION, undefined); - expect(core.setOutput).toHaveBeenCalledWith('version', 'foo'); + expect(core.addPath).not.toHaveBeenCalled(); + expect(core.setOutput).toHaveBeenCalledWith('version', REAL_VERSION); }); - it('setups when installed Maven is different with version input', async () => { - (core.getInput as jest.Mock).mockReturnValue('3.3'); - (getActiveMavenVersion as jest.Mock).mockResolvedValue('3.5.2'); - (setupMaven as jest.Mock).mockResolvedValue(DEFAULT_VERSION); + it('install and cache a specific Maven version', async () => { + (core.getInput as jest.Mock).mockReturnValue(' ~3.1.0'); await run(); - expect(setupMaven).toHaveBeenCalledWith('3.3', undefined); - expect(core.setOutput).toHaveBeenCalledWith('version', DEFAULT_VERSION); + expect(core.info).toHaveBeenCalledWith( + expect.stringMatching(new RegExp(`Downloading Maven ${TEST_VERSION} from`, 'i')) + ); + + expect(core.addPath).toHaveBeenCalledWith(path.join(TOOL_PATH, 'bin')); + expect(core.setOutput).toHaveBeenCalledWith('version', TEST_VERSION); + + expect(existsSync(`${TOOL_PATH}.complete`)).toBe(true); }); - it('setups when installed Maven is correspond with version input', async () => { - (core.getInput as jest.Mock).mockReturnValue('3.x'); - (getActiveMavenVersion as jest.Mock).mockResolvedValue('3.3.9'); - (setupMaven as jest.Mock).mockResolvedValue(''); + it('uses system Maven if real version > cached version', async () => { + await fs.mkdir(TOOL_PATH, { recursive: true }); + await fs.writeFile(`${TOOL_PATH}.complete`, ''); + (core.getInput as jest.Mock).mockReturnValue('3.x '); await run(); - expect(setupMaven).toHaveBeenCalledWith('3.x', '3.3.9'); - expect(core.setOutput).toHaveBeenCalledWith('version', ''); + expect(core.info).toHaveBeenCalledWith( + expect.stringMatching( + new RegExp(`Use.* version ${REAL_VERSION} instead of.* ${TEST_VERSION}`, 'i') + ) + ); + + expect(core.addPath).not.toHaveBeenCalled(); + expect(core.setOutput).toHaveBeenCalledWith('version', REAL_VERSION); }); }); diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts index 70806d2..878df42 100644 --- a/__tests__/utils.test.ts +++ b/__tests__/utils.test.ts @@ -11,6 +11,7 @@ jest.mock('child_process'); jest.mock('@actions/core'); const MVN_PATH = path.join(__dirname, 'data'); +const REAL_VERSION = '3.5.2'; describe('getVersionFromToolcachePath', () => { it.each([ @@ -34,7 +35,6 @@ describe('getActiveMavenVersion', () => { }); it('gets real version by `mvn` command', async () => { - const expectedVersion = '3.5.2'; process.env.PATH = `${MVN_PATH}${path.delimiter}${ORIGINAL_PATH ?? ''}`; const cp = jest.requireActual('child_process'); @@ -42,9 +42,9 @@ describe('getActiveMavenVersion', () => { const installedVersion = await getActiveMavenVersion(); - expect(installedVersion).toBe(expectedVersion); + expect(installedVersion).toBe(REAL_VERSION); expect(core.debug).toHaveBeenCalledWith( - expect.stringMatching(new RegExp(`Retrieved.* version: ${expectedVersion}`, 'i')) + expect.stringMatching(new RegExp(`Retrieved.* version: ${REAL_VERSION}`, 'i')) ); }); @@ -75,7 +75,7 @@ describe('getActiveMavenVersion', () => { it('returns empty if `mvn` command is incorrect', async () => { process.env.PATH = MVN_PATH; - const cp = Object.create(new EventEmitter()) as EventEmitter & { stdout: EventEmitter }; + const cp = (new EventEmitter() as unknown) as EventEmitter & { stdout: EventEmitter }; (child.spawn as jest.Mock).mockReturnValue((cp.stdout = cp)); setTimeout(() => cp.emit('data', 'foo') && cp.emit('close', 0), EMIT_AT); diff --git a/jest.config.json b/jest.config.json index 0d7e67a..39ab3d7 100644 --- a/jest.config.json +++ b/jest.config.json @@ -1,11 +1,12 @@ { "clearMocks": true, - "resetMocks": true, + "restoreMocks": true, "moduleFileExtensions": ["ts", "js"], "testEnvironment": "node", "testMatch": ["**/*.test.ts"], "transform": { "\\.ts$": "ts-jest" }, + "testTimeout": 15000, "verbose": true }