diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6b84dbd..935d04f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: run-rockcraft-pack-action: strategy: matrix: - os: [ubuntu-24.04, ubuntu-22.04, ubuntu-20.04] + os: [ubuntu-24.04, ubuntu-22.04] revision: ['1206', ''] runs-on: ${{ matrix.os }} steps: diff --git a/dist/rockcraft-pack-action/index.js b/dist/rockcraft-pack-action/index.js index 48279b2..5ba2dbc 100644 --- a/dist/rockcraft-pack-action/index.js +++ b/dist/rockcraft-pack-action/index.js @@ -20002,6 +20002,9 @@ function expandHome(p) { function shellUser() { return os.userInfo().username; } +function fileExists(path2) { + return fs.existsSync(path2); +} async function haveExecutable(path2) { try { await fs.promises.access(path2, fs.constants.X_OK); @@ -20010,6 +20013,9 @@ async function haveExecutable(path2) { } return true; } +async function haveRockcraftTest() { + return await exec.exec("sudo", ["rockcraft", "test", "-h"]) === 0; +} async function ensureSnapd() { const haveSnapd = await haveExecutable("/usr/bin/snap"); if (!haveSnapd) { @@ -20093,10 +20099,12 @@ var RockcraftBuilder = class { rockcraftChannel; rockcraftPackVerbosity; rockcraftRevision; + runRockcraftTest; constructor(options) { this.projectRoot = expandHome(options.projectRoot); this.rockcraftChannel = options.rockcraftChannel; this.rockcraftRevision = options.rockcraftRevision; + this.runRockcraftTest = options.runRockcraftTest; if (allowedVerbosity.includes(options.rockcraftPackVerbosity)) { this.rockcraftPackVerbosity = options.rockcraftPackVerbosity; } else { @@ -20113,6 +20121,18 @@ var RockcraftBuilder = class { core2.endGroup(); let rockcraft = "rockcraft pack"; let rockcraftPackArgs = ""; + if (this.runRockcraftTest) { + const testFile = `${this.projectRoot}/spread.yaml`; + if (!fileExists(testFile)) { + throw new Error(`Cannot run tests. Missing ${testFile} file.`); + } else if (!await haveRockcraftTest()) { + throw new Error( + "Cannot run tests. rockcraft test is not a valid command." + ); + } else { + rockcraft = "rockcraft test"; + } + } if (this.rockcraftPackVerbosity) { rockcraftPackArgs = `${rockcraftPackArgs} --verbosity ${this.rockcraftPackVerbosity}`; } @@ -20150,6 +20170,7 @@ async function run() { core3.info(`Building rock in "${projectRoot}"...`); const rockcraftRevision = core3.getInput("revision"); const rockcraftChannel = core3.getInput("rockcraft-channel") || "stable"; + const runRockcraftTest = core3.getInput("test").toLowerCase() === "true"; if (rockcraftRevision.length < 1) { core3.warning( `Rockcraft revision not provided. Installing from ${rockcraftChannel}` @@ -20160,7 +20181,8 @@ async function run() { projectRoot, rockcraftChannel, rockcraftPackVerbosity, - rockcraftRevision + rockcraftRevision, + runRockcraftTest }); await builder.pack(); const rock = await builder.outputRock(); diff --git a/rockcraft-pack/action.yml b/rockcraft-pack/action.yml index 6a78c97..7cecfd8 100644 --- a/rockcraft-pack/action.yml +++ b/rockcraft-pack/action.yml @@ -27,6 +27,12 @@ inputs: If not provided, it defaults to whatever revision is in latest/stable. default: '' + test: + description: > + Run rockcraft test when packing the rock. + + If not provided, it defaults to false. + default: 'false' outputs: rock: description: 'The file name of the resulting rock.' diff --git a/src/rockcraft-pack-action.ts b/src/rockcraft-pack-action.ts index 54b1989..4e6d5e7 100644 --- a/src/rockcraft-pack-action.ts +++ b/src/rockcraft-pack-action.ts @@ -9,6 +9,7 @@ async function run(): Promise { core.info(`Building rock in "${projectRoot}"...`) const rockcraftRevision = core.getInput('revision') const rockcraftChannel = core.getInput('rockcraft-channel') || 'stable' + const runRockcraftTest = core.getInput('test').toLowerCase() === 'true' if (rockcraftRevision.length < 1) { core.warning( `Rockcraft revision not provided. Installing from ${rockcraftChannel}` @@ -20,7 +21,8 @@ async function run(): Promise { projectRoot, rockcraftChannel, rockcraftPackVerbosity, - rockcraftRevision + rockcraftRevision, + runRockcraftTest }) await builder.pack() const rock = await builder.outputRock() diff --git a/src/rockcraft-pack.ts b/src/rockcraft-pack.ts index 0bc1a96..9cf3299 100644 --- a/src/rockcraft-pack.ts +++ b/src/rockcraft-pack.ts @@ -13,6 +13,7 @@ interface RockcraftBuilderOptions { rockcraftChannel: string rockcraftPackVerbosity: string rockcraftRevision: string + runRockcraftTest: boolean } export class RockcraftBuilder { @@ -20,11 +21,13 @@ export class RockcraftBuilder { rockcraftChannel: string rockcraftPackVerbosity: string rockcraftRevision: string + runRockcraftTest: boolean constructor(options: RockcraftBuilderOptions) { this.projectRoot = tools.expandHome(options.projectRoot) this.rockcraftChannel = options.rockcraftChannel this.rockcraftRevision = options.rockcraftRevision + this.runRockcraftTest = options.runRockcraftTest if (allowedVerbosity.includes(options.rockcraftPackVerbosity)) { this.rockcraftPackVerbosity = options.rockcraftPackVerbosity } else { @@ -44,6 +47,21 @@ export class RockcraftBuilder { let rockcraft = 'rockcraft pack' let rockcraftPackArgs = '' + + if (this.runRockcraftTest) { + const testFile = `${this.projectRoot}/spread.yaml` + + if (!tools.fileExists(testFile)) { + throw new Error(`Cannot run tests. Missing ${testFile} file.`) + } else if (!(await tools.haveRockcraftTest())) { + throw new Error( + 'Cannot run tests. rockcraft test is not a valid command.' + ) + } else { + rockcraft = 'rockcraft test' + } + } + if (this.rockcraftPackVerbosity) { rockcraftPackArgs = `${rockcraftPackArgs} --verbosity ${this.rockcraftPackVerbosity}` } diff --git a/src/tools.ts b/src/tools.ts index f6cca46..575b61c 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -16,6 +16,10 @@ export function shellUser(): string { return os.userInfo().username } +export function fileExists(path: string): boolean { + return fs.existsSync(path) +} + async function haveExecutable(path: string): Promise { try { await fs.promises.access(path, fs.constants.X_OK) @@ -25,6 +29,10 @@ async function haveExecutable(path: string): Promise { return true } +export async function haveRockcraftTest(): Promise { + return (await exec.exec('sudo', ['rockcraft', 'test', '-h'])) === 0 +} + export async function ensureSnapd(): Promise { const haveSnapd = await haveExecutable('/usr/bin/snap') if (!haveSnapd) { diff --git a/tests/rockcraft-pack.test.ts b/tests/rockcraft-pack.test.ts index 5050842..73f2a50 100644 --- a/tests/rockcraft-pack.test.ts +++ b/tests/rockcraft-pack.test.ts @@ -3,6 +3,7 @@ import * as os from 'os' import * as path from 'path' import * as exec from '@actions/exec' +import * as core from '@actions/core' import * as build from '../src/rockcraft-pack' import * as tools from '../src/tools' import * as fs from 'fs' @@ -16,7 +17,8 @@ test('RockcraftBuilder expands tilde in project root', () => { projectRoot: '~', rockcraftChannel: 'edge', rockcraftPackVerbosity: 'trace', - rockcraftRevision: '1' + rockcraftRevision: '1', + runRockcraftTest: false }) expect(builder.projectRoot).toBe(os.homedir()) @@ -24,7 +26,8 @@ test('RockcraftBuilder expands tilde in project root', () => { projectRoot: '~/foo/bar', rockcraftChannel: 'stable', rockcraftPackVerbosity: 'trace', - rockcraftRevision: '1' + rockcraftRevision: '1', + runRockcraftTest: false }) expect(builder.projectRoot).toBe(path.join(os.homedir(), 'foo/bar')) }) @@ -59,7 +62,8 @@ test('RockcraftBuilder.pack runs a rock build', async () => { projectRoot: projectDir, rockcraftChannel: 'stable', rockcraftPackVerbosity: 'debug', - rockcraftRevision: '1' + rockcraftRevision: '1', + runRockcraftTest: false }) await builder.pack() @@ -108,7 +112,8 @@ test('RockcraftBuilder.build can set the Rockcraft channel', async () => { projectRoot: '.', rockcraftChannel: 'test-channel', rockcraftPackVerbosity: 'trace', - rockcraftRevision: '' + rockcraftRevision: '', + runRockcraftTest: false }) await builder.pack() @@ -139,7 +144,8 @@ test('RockcraftBuilder.build can set the Rockcraft revision', async () => { projectRoot: '.', rockcraftChannel: 'channel', rockcraftPackVerbosity: 'trace', - rockcraftRevision: '123' + rockcraftRevision: '123', + runRockcraftTest: false }) await builder.pack() @@ -175,7 +181,8 @@ test('RockcraftBuilder.build can pass known verbosity', async () => { projectRoot: '.', rockcraftChannel: 'stable', rockcraftPackVerbosity: 'trace', - rockcraftRevision: '1' + rockcraftRevision: '1', + runRockcraftTest: false }) await builder.pack() @@ -198,12 +205,153 @@ test('RockcraftBuilder.build can pass known verbosity', async () => { projectRoot: '.', rockcraftChannel: 'stable', rockcraftPackVerbosity: 'fake-verbosity', - rockcraftRevision: '1' + rockcraftRevision: '1', + runRockcraftTest: false }) } expect(badBuilder).toThrow() }) +test('RockcraftBuilder.pack runs a rock build and test', async () => { + expect.assertions(7) + + const user = 'ubuntu' + + const ensureSnapd = jest + .spyOn(tools, 'ensureSnapd') + .mockImplementation(async (): Promise => {}) + const ensureLXD = jest + .spyOn(tools, 'ensureLXD') + .mockImplementation(async (): Promise => {}) + const ensureRockcraft = jest + .spyOn(tools, 'ensureRockcraft') + .mockImplementation(async (channel): Promise => {}) + const shellUser = jest + .spyOn(tools, 'shellUser') + .mockImplementation((): string => user) + const haveRockcraftTest = jest + .spyOn(tools, 'haveRockcraftTest') + .mockImplementation(async (): Promise => true) + const fileExists = jest + .spyOn(tools, 'fileExists') + .mockImplementation((path: string) => true) + const execMock = jest + .spyOn(exec, 'exec') + .mockImplementation( + async (program: string, args?: string[]): Promise => { + return 0 + } + ) + + const projectDir = 'project-root' + const builder = new build.RockcraftBuilder({ + projectRoot: projectDir, + rockcraftChannel: 'stable', + rockcraftPackVerbosity: 'debug', + rockcraftRevision: '1', + runRockcraftTest: true + }) + await builder.pack() + + expect(ensureSnapd).toHaveBeenCalled() + expect(ensureLXD).toHaveBeenCalled() + expect(ensureRockcraft).toHaveBeenCalled() + expect(shellUser).toHaveBeenCalled() + expect(haveRockcraftTest).toHaveBeenCalled() + expect(fileExists).toHaveBeenCalledWith('project-root/spread.yaml') + expect(execMock).toHaveBeenCalledWith( + 'sudo', + [ + '--preserve-env', + '--user', + user, + 'rockcraft', + 'test', + '--verbosity', + 'debug' + ], + { + cwd: projectDir + } + ) +}) + +test('RockcraftBuilder.pack fails if test is set to true and no spread.yaml is found', async () => { + expect.assertions(5) + + const user = 'ubuntu' + + const ensureSnapd = jest + .spyOn(tools, 'ensureSnapd') + .mockImplementation(async (): Promise => {}) + const ensureLXD = jest + .spyOn(tools, 'ensureLXD') + .mockImplementation(async (): Promise => {}) + const ensureRockcraft = jest + .spyOn(tools, 'ensureRockcraft') + .mockImplementation(async (channel): Promise => {}) + const fileExists = jest + .spyOn(tools, 'fileExists') + .mockImplementation((path: string) => false) + + const projectDir = 'project-root' + const builder = new build.RockcraftBuilder({ + projectRoot: projectDir, + rockcraftChannel: 'stable', + rockcraftPackVerbosity: 'debug', + rockcraftRevision: '1', + runRockcraftTest: true + }) + + await expect(builder.pack()).rejects.toThrow( + 'Cannot run tests. Missing project-root/spread.yaml file.' + ) + expect(ensureSnapd).toHaveBeenCalled() + expect(ensureLXD).toHaveBeenCalled() + expect(ensureRockcraft).toHaveBeenCalled() + expect(fileExists).toHaveBeenCalledWith('project-root/spread.yaml') +}) + +test('RockcraftBuilder.pack fails if test is set to true and rockcraft test is invalid', async () => { + expect.assertions(6) + + const user = 'ubuntu' + + const ensureSnapd = jest + .spyOn(tools, 'ensureSnapd') + .mockImplementation(async (): Promise => {}) + const ensureLXD = jest + .spyOn(tools, 'ensureLXD') + .mockImplementation(async (): Promise => {}) + const ensureRockcraft = jest + .spyOn(tools, 'ensureRockcraft') + .mockImplementation(async (channel): Promise => {}) + const haveRockcraftTest = jest + .spyOn(tools, 'haveRockcraftTest') + .mockImplementation(async (): Promise => false) + const fileExists = jest + .spyOn(tools, 'fileExists') + .mockImplementation((path: string) => true) + + const projectDir = 'project-root' + const builder = new build.RockcraftBuilder({ + projectRoot: projectDir, + rockcraftChannel: 'stable', + rockcraftPackVerbosity: 'debug', + rockcraftRevision: '1', + runRockcraftTest: true + }) + + await expect(builder.pack()).rejects.toThrow( + 'Cannot run tests. rockcraft test is not a valid command.' + ) + expect(ensureSnapd).toHaveBeenCalled() + expect(ensureLXD).toHaveBeenCalled() + expect(ensureRockcraft).toHaveBeenCalled() + expect(haveRockcraftTest).toHaveBeenCalled() + expect(fileExists).toHaveBeenCalledWith('project-root/spread.yaml') +}) + test('RockcraftBuilder.outputRock fails if there are no rocks', async () => { expect.assertions(2) @@ -212,7 +360,8 @@ test('RockcraftBuilder.outputRock fails if there are no rocks', async () => { projectRoot: projectDir, rockcraftChannel: 'stable', rockcraftPackVerbosity: 'trace', - rockcraftRevision: '1' + rockcraftRevision: '1', + runRockcraftTest: false }) const readdir = jest @@ -233,7 +382,8 @@ test('RockcraftBuilder.outputRock returns the first rock', async () => { projectRoot: projectDir, rockcraftChannel: 'stable', rockcraftPackVerbosity: 'trace', - rockcraftRevision: '1' + rockcraftRevision: '1', + runRockcraftTest: false }) const readdir = jest