Merge branch 'main' of https://github.com/game-ci/unity-test-runner into game-ci-main

This commit is contained in:
Aaron Trudeau
2022-01-17 10:24:33 -05:00
64 changed files with 24667 additions and 3207 deletions

View File

@@ -1,6 +1,6 @@
import path from 'path';
import fs from 'fs';
import Action from './action';
import fs from 'fs';
import path from 'path';
describe('Action', () => {
describe('compatibility check', () => {
@@ -14,16 +14,16 @@ describe('Action', () => {
});
it('returns the root folder of the action', () => {
const { rootFolder, name } = Action;
const { rootFolder, canonicalName } = Action;
expect(path.basename(rootFolder)).toStrictEqual(name);
expect(path.basename(rootFolder)).toStrictEqual(canonicalName);
expect(fs.existsSync(rootFolder)).toStrictEqual(true);
});
it('returns the action folder', () => {
const { actionFolder } = Action;
expect(path.basename(actionFolder)).toStrictEqual('action');
expect(path.basename(actionFolder)).toStrictEqual('dist');
expect(fs.existsSync(actionFolder)).toStrictEqual(true);
});

View File

@@ -1,48 +1,48 @@
import path from 'path';
class Action {
static get supportedPlatforms() {
const Action = {
get supportedPlatforms() {
return ['linux'];
}
},
static get isRunningLocally() {
get isRunningLocally() {
return process.env.RUNNER_WORKSPACE === undefined;
}
},
static get isRunningFromSource() {
get isRunningFromSource() {
return path.basename(__dirname) === 'model';
}
},
static get name() {
get canonicalName() {
return 'unity-test-runner';
}
},
static get rootFolder() {
get rootFolder() {
if (Action.isRunningFromSource) {
return path.dirname(path.dirname(path.dirname(__filename)));
}
return path.dirname(path.dirname(__filename));
}
},
static get actionFolder() {
return `${Action.rootFolder}/action`;
}
get actionFolder() {
return `${Action.rootFolder}/dist`;
},
static get dockerfile() {
get dockerfile() {
return `${Action.actionFolder}/Dockerfile`;
}
},
static get workspace() {
get workspace() {
return process.env.GITHUB_WORKSPACE;
}
},
static checkCompatibility() {
checkCompatibility() {
const currentPlatform = process.platform;
if (!Action.supportedPlatforms.includes(currentPlatform)) {
throw new Error(`Currently ${currentPlatform}-platform is not supported`);
}
}
}
},
};
export default Action;

View File

@@ -1,25 +0,0 @@
import Action from './action';
import Docker from './docker';
import ImageTag from './image-tag';
describe('Docker', () => {
it('builds', async () => {
const path = Action.actionFolder;
const dockerfile = `${path}/Dockerfile`;
const image = new ImageTag({
repository: '',
name: 'ubuntu',
version: 'impish',
});
const baseImage = {
toString: () => image.toString().slice(0, image.toString().lastIndexOf('-base-0')),
version: image.version,
};
const tag = await Docker.build({ path, dockerfile, baseImage }, true);
expect(tag).toBeInstanceOf(ImageTag);
expect(tag.toString()).toStrictEqual('unity-action:impish-base-0');
}, 240000);
});

30
src/model/docker.test.ts Normal file
View File

@@ -0,0 +1,30 @@
import Action from './action';
import Docker from './docker';
import ImageTag from './image-tag';
describe('Docker', () => {
it.skip('builds', async () => {
const path = Action.actionFolder;
const dockerfile = `${path}/Dockerfile`;
const baseImage = new ImageTag({
repository: '',
name: 'alpine',
version: '3',
platform: 'Test',
});
const tag = await Docker.build({ path, dockerfile, baseImage }, true);
expect(tag).toBeInstanceOf(ImageTag);
expect(tag.toString()).toStrictEqual('unity-builder:3');
}, 240_000);
it.skip('runs', async () => {
const image = 'unity-builder:2019.2.11f1-webgl';
const parameters = {
workspace: Action.rootFolder,
projectPath: `${Action.rootFolder}/test-project`,
buildName: 'someBuildName',
buildsPath: 'build',
method: '',
};
await Docker.run(image, parameters);
});
});

View File

@@ -1,12 +1,12 @@
import { exec } from '@actions/exec';
import ImageTag from './image-tag';
import { exec } from '@actions/exec';
class Docker {
static async build(buildParameters, silent = false) {
const Docker = {
async build(buildParameters, silent = false) {
const { path, dockerfile, baseImage } = buildParameters;
const { version } = baseImage;
const tag = ImageTag.createForAction(version);
const tag = new ImageTag({ version });
const command = `docker build ${path} \
--file ${dockerfile} \
--build-arg IMAGE=${baseImage} \
@@ -15,19 +15,20 @@ class Docker {
await exec(command, undefined, { silent });
return tag;
}
},
static async run(image, parameters, silent = false) {
async run(image, parameters, silent = false) {
const {
unityVersion,
workspace,
projectPath,
customParameters,
testMode,
artifactsPath,
useHostNetwork,
customParameters,
sshAgent,
packageMode,
gitPrivateToken,
githubToken,
} = parameters;
@@ -41,6 +42,7 @@ class Docker {
--env UNITY_SERIAL \
--env UNITY_VERSION="${unityVersion}" \
--env PROJECT_PATH="${projectPath}" \
--env CUSTOM_PARAMETERS="${customParameters}" \
--env TEST_MODE="${testMode}" \
--env ARTIFACTS_PATH="${artifactsPath}" \
--env CUSTOM_PARAMETERS="${customParameters}" \
@@ -60,6 +62,7 @@ class Docker {
--env RUNNER_TOOL_CACHE \
--env RUNNER_TEMP \
--env RUNNER_WORKSPACE \
--env GIT_PRIVATE_TOKEN="${gitPrivateToken}" \
${sshAgent ? '--env SSH_AUTH_SOCK=/ssh-agent' : ''} \
--volume "/var/run/docker.sock":"/var/run/docker.sock" \
--volume "/home/runner/work/_temp/_github_home":"/root" \
@@ -72,7 +75,7 @@ class Docker {
${image}`;
await exec(command, undefined, { silent });
}
}
},
};
export default Docker;

View File

@@ -1,45 +0,0 @@
import { trimStart } from 'lodash-es';
class ImageTag {
static createForBase({ version, customImage }) {
const repository = 'unityci';
const name = 'editor';
return new this({ repository, name, version, customImage });
}
static createForAction(version) {
const repository = '';
const name = 'unity-action';
return new this({ repository, name, version });
}
constructor({ repository = '', name, version, customImage }) {
if (!ImageTag.versionPattern.test(version)) {
throw new Error(`Invalid version "${version}".`);
}
Object.assign(this, { repository, name, version, customImage });
}
static get versionPattern() {
return /^(20\d{2}\.\d\.\w{3,4}|3)|impish$/;
}
get tag() {
return this.version;
}
get image() {
return trimStart(`${this.repository}/${this.name}`, '/');
}
toString() {
if (this.customImage && this.customImage !== '') {
return this.customImage;
}
return `${this.image}:${this.tag}-base-0`;
}
}
export default ImageTag;

View File

@@ -1,47 +0,0 @@
import ImageTag from './image-tag';
describe('ImageTag', () => {
describe('constructor', () => {
const some = {
name: 'someName',
version: '2020.0.00f0',
};
it('can be called', () => {
expect(() => new ImageTag(some)).not.toThrow();
});
it('accepts parameters and sets the right properties', () => {
const image = new ImageTag(some);
expect(image.repository).toStrictEqual('');
expect(image.name).toStrictEqual(some.name);
expect(image.version).toStrictEqual(some.version);
});
test.each(['2000.0.0f0', '2011.1.11f1'])('accepts %p version format', version => {
expect(() => new ImageTag({ version })).not.toThrow();
});
test.each(['some version', '', 1, undefined])('throws for incorrect versions %p', version => {
expect(() => new ImageTag({ version })).toThrow();
});
});
describe('toString', () => {
it('returns the correct version', () => {
const image = ImageTag.createForBase({ version: '2099.1.1111' });
expect(image.toString()).toStrictEqual(`unityci/editor:2099.1.1111-base-0`);
});
it('returns customImage if given', () => {
const image = ImageTag.createForBase({
version: '2099.1.1111',
customImage: 'unityci/editor:2099.1.1111-base-0',
});
expect(image.toString()).toStrictEqual(image.customImage);
});
});
});

View File

@@ -0,0 +1,72 @@
import ImageTag from './image-tag';
describe('ImageTag', () => {
const some = {
repository: 'test1',
name: 'test2',
version: '2099.9.f9f9',
platform: 'Test',
builderPlatform: '',
};
const defaults = {
repository: 'unityci',
name: 'editor',
image: 'unityci/editor',
};
describe('constructor', () => {
it('can be called', () => {
const { platform } = some;
expect(() => new ImageTag({ platform })).not.toThrow();
});
it('accepts parameters and sets the right properties', () => {
const image = new ImageTag(some);
expect(image.repository).toStrictEqual(some.repository);
expect(image.name).toStrictEqual(some.name);
expect(image.version).toStrictEqual(some.version);
expect(image.platform).toStrictEqual(some.platform);
expect(image.builderPlatform).toStrictEqual(some.builderPlatform);
});
test.each(['2000.0.0f0', '2011.1.11f1'])('accepts %p version format', version => {
expect(() => new ImageTag({ version, platform: some.platform })).not.toThrow();
});
test.each(['some version', '', 1])('throws for incorrect versions %p', version => {
const { platform } = some;
expect(() => new ImageTag({ version, platform })).toThrow();
});
});
describe('toString', () => {
it('returns the correct version', () => {
const image = new ImageTag({ version: '2099.1.1111', platform: some.platform });
expect(image.toString()).toStrictEqual(`${defaults.image}:2099.1.1111-0`);
});
it('returns customImage if given', () => {
const image = new ImageTag({
version: '2099.1.1111',
platform: some.platform,
customImage: `${defaults.image}:2099.1.1111@347598437689743986`,
});
expect(image.toString()).toStrictEqual(image.customImage);
});
it('returns the specific build platform', () => {
const image = new ImageTag({ version: '2019.2.11f1', platform: 'WebGL' });
expect(image.toString()).toStrictEqual(`${defaults.image}:2019.2.11f1-webgl-0`);
});
it('returns no specific build platform for generic targetPlatforms', () => {
const image = new ImageTag({ platform: 'NoTarget' });
expect(image.toString()).toStrictEqual(`${defaults.image}:2019.2.11f1-0`);
});
});
});

130
src/model/image-tag.ts Normal file
View File

@@ -0,0 +1,130 @@
import Platform from './platform';
class ImageTag {
public repository: string;
public name: string;
public version: string;
public platform: any;
public builderPlatform: string;
public customImage: any;
constructor(imageProperties) {
const {
repository = 'unityci',
name = 'editor',
version = '2019.2.11f1',
platform = Platform.types.StandaloneLinux64,
customImage,
} = imageProperties;
if (!ImageTag.versionPattern.test(version)) {
throw new Error(`Invalid version "${version}".`);
}
const builderPlatform = ImageTag.getTargetPlatformToImageSuffixMap(platform, version);
this.repository = repository;
this.name = name;
this.version = version;
this.platform = platform;
this.builderPlatform = builderPlatform;
this.customImage = customImage;
}
static get versionPattern() {
return /^20\d{2}\.\d\.\w{3,4}|3$/;
}
static get imageSuffixes() {
return {
generic: '',
webgl: 'webgl',
mac: 'mac-mono',
windows: 'windows-mono',
linux: 'base',
linuxIl2cpp: 'linux-il2cpp',
android: 'android',
ios: 'ios',
facebook: 'facebook',
};
}
static getTargetPlatformToImageSuffixMap(platform, version) {
const { generic, webgl, mac, windows, linux, linuxIl2cpp, android, ios, facebook } =
ImageTag.imageSuffixes;
const [major, minor] = version.split('.').map(digit => Number(digit));
// @see: https://docs.unity3d.com/ScriptReference/BuildTarget.html
switch (platform) {
case Platform.types.StandaloneOSX:
return mac;
case Platform.types.StandaloneWindows:
return windows;
case Platform.types.StandaloneWindows64:
return windows;
case Platform.types.StandaloneLinux64: {
// Unity versions before 2019.3 do not support il2cpp
if (major >= 2020 || (major === 2019 && minor >= 3)) {
return linuxIl2cpp;
}
return linux;
}
case Platform.types.iOS:
return ios;
case Platform.types.Android:
return android;
case Platform.types.WebGL:
return webgl;
case Platform.types.WSAPlayer:
return windows;
case Platform.types.PS4:
return windows;
case Platform.types.XboxOne:
return windows;
case Platform.types.tvOS:
return windows;
case Platform.types.Switch:
return windows;
// Unsupported
case Platform.types.Lumin:
return windows;
case Platform.types.BJM:
return windows;
case Platform.types.Stadia:
return windows;
case Platform.types.Facebook:
return facebook;
case Platform.types.NoTarget:
return generic;
// Test specific
case Platform.types.Test:
return generic;
default:
throw new Error(`
Platform must be one of the ones described in the documentation.
"${platform}" is currently not supported.`);
}
}
get tag() {
return `${this.version}-${this.builderPlatform}`.replace(/-+$/, '');
}
get image() {
return `${this.repository}/${this.name}`.replace(/^\/+/, '');
}
toString() {
const { image, tag, customImage } = this;
if (customImage && customImage !== '') {
return customImage;
}
const dockerRepoVersion = 0;
return `${image}:${tag}-${dockerRepoVersion}`;
}
}
export default ImageTag;

View File

@@ -1,8 +0,0 @@
import Action from './action';
import Docker from './docker';
import Input from './input';
import ImageTag from './image-tag';
import Output from './output';
import ResultsCheck from './results-check';
export { Action, Docker, Input, ImageTag, Output, ResultsCheck };

View File

@@ -4,7 +4,7 @@ describe('Index', () => {
test.each(['Action', 'Docker', 'ImageTag', 'Input', 'Output', 'ResultsCheck'])(
'exports %s',
exportedModule => {
expect(typeof Index[exportedModule]).toStrictEqual('function');
expect(Index[exportedModule]).toBeDefined();
},
);
});

6
src/model/index.ts Normal file
View File

@@ -0,0 +1,6 @@
export { default as Action } from './action';
export { default as Docker } from './docker';
export { default as ImageTag } from './image-tag';
export { default as Input } from './input';
export { default as Output } from './output';
export { default as ResultsCheck } from './results-check';

View File

@@ -1,45 +1,45 @@
import { getInput } from '@actions/core';
import { includes } from 'lodash-es';
import UnityVersionParser from './unity-version-parser';
import { getInput } from '@actions/core';
class Input {
static get testModes() {
const Input = {
get testModes() {
return ['all', 'playmode', 'editmode'];
}
},
static isValidFolderName(folderName) {
const validFolderName = new RegExp(/^(\.|\.\/)?(\.?[\w~]+([_-]?[\w~]+)*\/?)*$/);
isValidFolderName(folderName) {
const validFolderName = new RegExp(/^(\.|\.\/)?(\.?[\w~]+([ _-]?[\w~]+)*\/?)*$/);
return validFolderName.test(folderName);
}
},
static getFromUser() {
getFromUser() {
// Input variables specified in workflow using "with" prop.
const rawUnityVersion = getInput('unityVersion') || 'auto';
const customImage = getInput('customImage') || '';
const testMode = (getInput('testMode') || 'all').toLowerCase();
const rawProjectPath = getInput('projectPath') || '.';
const customParameters = getInput('customParameters') || '';
const testMode = (getInput('testMode') || 'all').toLowerCase();
const rawArtifactsPath = getInput('artifactsPath') || 'artifacts';
const rawUseHostNetwork = getInput('useHostNetwork') || 'false';
const customParameters = getInput('customParameters') || '';
const sshAgent = getInput('sshAgent') || '';
const gitPrivateToken = getInput('gitPrivateToken') || '';
const githubToken = getInput('githubToken') || '';
const checkName = getInput('checkName') || 'Test Results';
const rawPackageMode = getInput('packageMode') || 'false';
// Validate input
if (!includes(this.testModes, testMode)) {
if (!this.testModes.includes(testMode)) {
throw new Error(`Invalid testMode ${testMode}`);
}
if (!this.isValidFolderName(rawArtifactsPath)) {
throw new Error(`Invalid artifactsPath "${rawArtifactsPath}"`);
}
if (!this.isValidFolderName(rawProjectPath)) {
throw new Error(`Invalid projectPath "${rawProjectPath}"`);
}
if (!this.isValidFolderName(rawArtifactsPath)) {
throw new Error(`Invalid artifactsPath "${rawArtifactsPath}"`);
}
if (rawUseHostNetwork !== 'true' && rawUseHostNetwork !== 'false') {
throw new Error(`Invalid useHostNetwork "${rawUseHostNetwork}"`);
}
@@ -61,16 +61,17 @@ class Input {
unityVersion,
customImage,
projectPath,
customParameters,
testMode,
artifactsPath,
useHostNetwork,
customParameters,
sshAgent,
gitPrivateToken,
githubToken,
checkName,
packageMode,
};
}
}
},
};
export default Input;

View File

@@ -1,9 +0,0 @@
const core = require('@actions/core');
class Output {
static async setArtifactsPath(artifactsPath) {
await core.setOutput('artifactsPath', artifactsPath);
}
}
export default Output;

View File

@@ -3,7 +3,7 @@ import Output from './output';
describe('Output', () => {
describe('setArtifactsPath', () => {
it('does not throw', async () => {
await expect(Output.setArtifactsPath()).resolves.not.toThrow();
await expect(Output.setArtifactsPath('')).resolves.not.toThrow();
});
});
});

9
src/model/output.ts Normal file
View File

@@ -0,0 +1,9 @@
import * as core from '@actions/core';
const Output = {
async setArtifactsPath(artifactsPath) {
await core.setOutput('artifactsPath', artifactsPath);
},
};
export default Output;

View File

@@ -0,0 +1,37 @@
import Platform from './platform';
describe('Platform', () => {
describe('default', () => {
it('does not throw', () => {
expect(() => Platform.default).not.toThrow();
});
it('returns a string', () => {
expect(typeof Platform.default).toStrictEqual('string');
});
it('returns a platform', () => {
expect(Object.values(Platform.types)).toContain(Platform.default);
});
});
describe('isWindows', () => {
it('returns true for windows', () => {
expect(Platform.isWindows(Platform.types.StandaloneWindows64)).toStrictEqual(true);
});
it('returns false for MacOS', () => {
expect(Platform.isWindows(Platform.types.StandaloneOSX)).toStrictEqual(false);
});
});
describe('isAndroid', () => {
it('returns true for Android', () => {
expect(Platform.isAndroid(Platform.types.Android)).toStrictEqual(true);
});
it('returns false for Windows', () => {
expect(Platform.isAndroid(Platform.types.StandaloneWindows64)).toStrictEqual(false);
});
});
});

51
src/model/platform.ts Normal file
View File

@@ -0,0 +1,51 @@
const Platform = {
get default() {
return Platform.types.StandaloneWindows64;
},
get types() {
return {
StandaloneOSX: 'StandaloneOSX',
StandaloneWindows: 'StandaloneWindows',
StandaloneWindows64: 'StandaloneWindows64',
StandaloneLinux64: 'StandaloneLinux64',
iOS: 'iOS',
Android: 'Android',
WebGL: 'WebGL',
WSAPlayer: 'WSAPlayer',
PS4: 'PS4',
XboxOne: 'XboxOne',
tvOS: 'tvOS',
Switch: 'Switch',
// Unsupported
Lumin: 'Lumin',
BJM: 'BJM',
Stadia: 'Stadia',
Facebook: 'Facebook',
NoTarget: 'NoTarget',
// Test specific
Test: 'Test',
};
},
isWindows(platform) {
switch (platform) {
case Platform.types.StandaloneWindows:
case Platform.types.StandaloneWindows64:
return true;
default:
return false;
}
},
isAndroid(platform) {
switch (platform) {
case Platform.types.Android:
return true;
default:
return false;
}
},
};
export default Platform;

View File

@@ -3,7 +3,7 @@ import ResultsCheck from './results-check';
describe('ResultsCheck', () => {
describe('createCheck', () => {
it('throws for missing input', () => {
expect(() => ResultsCheck.createCheck('', '', '')).rejects.toEqual(Error);
expect(() => ResultsCheck.createCheck('', '', '')).rejects;
});
});
});

View File

@@ -1,13 +1,13 @@
import * as core from '@actions/core';
import * as github from '@actions/github';
import * as fs from 'fs';
import path from 'path';
import * as github from '@actions/github';
import Handlebars from 'handlebars';
import ResultsParser from './results-parser';
import { RunMeta } from './ts/results-meta.ts';
import { RunMeta } from './results-meta';
import path from 'path';
class ResultsCheck {
static async createCheck(artifactsPath, githubToken, checkName) {
const ResultsCheck = {
async createCheck(artifactsPath, githubToken, checkName) {
// Validate input
if (!fs.existsSync(artifactsPath) || !githubToken || !checkName) {
throw new Error(
@@ -16,7 +16,7 @@ class ResultsCheck {
}
// Parse all results files
const runs = [];
const runs: RunMeta[] = [];
const files = fs.readdirSync(artifactsPath);
await Promise.all(
files.map(async filepath => {
@@ -30,16 +30,16 @@ class ResultsCheck {
// Combine all results into a single run summary
const runSummary = new RunMeta(checkName);
runs.forEach(run => {
for (const run of runs) {
runSummary.total += run.total;
runSummary.passed += run.passed;
runSummary.skipped += run.skipped;
runSummary.failed += run.failed;
runSummary.duration += run.duration;
run.suites.forEach(suite => {
for (const suite of run.suites) {
runSummary.addTests(suite.tests);
});
});
}
}
// Log
core.info('=================');
@@ -70,9 +70,9 @@ class ResultsCheck {
// Call GitHub API
await ResultsCheck.requestGitHubCheck(githubToken, checkName, output);
return runSummary.failed;
}
},
static async requestGitHubCheck(githubToken, checkName, output) {
async requestGitHubCheck(githubToken, checkName, output) {
const pullRequest = github.context.payload.pull_request;
const headSha = (pullRequest && pullRequest.head.sha) || github.context.sha;
@@ -87,18 +87,18 @@ class ResultsCheck {
};
const octokit = github.getOctokit(githubToken);
await octokit.checks.create(createCheckRequest);
}
await octokit.rest.checks.create(createCheckRequest);
},
static async renderSummary(runMetas) {
return ResultsCheck.render(`${__dirname}/../views/results-check-summary.hbs`, runMetas);
}
async renderSummary(runMetas) {
return ResultsCheck.render(`${__dirname}/results-check-summary.hbs`, runMetas);
},
static async renderDetails(runMetas) {
return ResultsCheck.render(`${__dirname}/../views/results-check-details.hbs`, runMetas);
}
async renderDetails(runMetas) {
return ResultsCheck.render(`${__dirname}/results-check-details.hbs`, runMetas);
},
static async render(viewPath, runMetas) {
async render(viewPath, runMetas) {
Handlebars.registerHelper('indent', toIndent =>
toIndent
.split('\n')
@@ -114,7 +114,7 @@ class ResultsCheck {
allowProtoPropertiesByDefault: true,
},
);
}
}
},
};
export default ResultsCheck;

View File

@@ -1,4 +1,4 @@
import { components } from '@octokit/openapi-types/dist-types/generated/types';
import { components } from '@octokit/openapi-types';
export function timeHelper(seconds: number): string {
return `${seconds.toFixed(3)}s`;
@@ -42,9 +42,9 @@ export class RunMeta extends Meta {
}
addTests(testSuite: TestMeta[]): void {
testSuite.forEach(test => {
for (const test of testSuite) {
this.addTest(test);
});
}
}
addTest(test: TestMeta): void {
@@ -93,6 +93,8 @@ export class TestMeta extends Meta {
constructor(suite: string, title: string) {
super(title);
this.suite = suite;
this.result = undefined;
this.duration = Number.NaN;
}
isSkipped(): boolean {
@@ -104,9 +106,7 @@ export class TestMeta extends Meta {
}
get summary(): string {
const dPart = this.isSkipped()
? ''
: ` in ${timeHelper(this.duration)}`;
const dPart = this.isSkipped() ? '' : ` in ${timeHelper(this.duration)}`;
return `${this.mark} **${this.title}** - ${this.result}${dPart}`;
}

View File

@@ -1,11 +1,12 @@
import * as xmljs from 'xml-js';
import * as fs from 'fs';
import * as xmljs from 'xml-js';
import ResultsParser from './results-parser';
import { TestMeta } from './results-meta';
describe('ResultsParser', () => {
describe('parseResults', () => {
it('throws for missing file', () => {
expect(() => ResultsParser.parseResults('')).rejects.toEqual(Error);
expect(() => ResultsParser.parseResults('')).rejects;
});
it('parses editmode-results.xml', () => {
@@ -19,14 +20,14 @@ describe('ResultsParser', () => {
describe('convertResults', () => {
it('converts editmode-results.xml', () => {
const file = fs.readFileSync('./artifacts/editmode-results.xml');
const file = fs.readFileSync('./artifacts/editmode-results.xml', 'utf8');
const filedata = xmljs.xml2js(file, { compact: true });
const result = ResultsParser.convertResults('editmode-results.xml', filedata);
expect(result.suites.length).toEqual(1);
});
it('converts playmode-results.xml', () => {
const file = fs.readFileSync('./artifacts/playmode-results.xml');
const file = fs.readFileSync('./artifacts/playmode-results.xml', 'utf8');
const filedata = xmljs.xml2js(file, { compact: true });
const result = ResultsParser.convertResults('playmode-results.xml', filedata);
expect(result.suites.length).toEqual(3);
@@ -53,27 +54,9 @@ describe('ResultsParser', () => {
const result = ResultsParser.convertSuite(targetSuite);
expect(result).toMatchObject([
{
annotation: undefined,
duration: Number.NaN,
result: undefined,
suite: 'Inner Suite Full Name',
title: 'testC',
},
{
annotation: undefined,
duration: Number.NaN,
result: undefined,
suite: 'Suite Full Name',
title: 'testA',
},
{
annotation: undefined,
duration: Number.NaN,
result: undefined,
suite: 'Suite Full Name',
title: 'testB',
},
new TestMeta('Inner Suite Full Name', 'testC'),
new TestMeta('Suite Full Name', 'testA'),
new TestMeta('Suite Full Name', 'testB'),
]);
});
});
@@ -149,7 +132,7 @@ describe('ResultsParser', () => {
expect(result.annotation).toBeUndefined();
});
test('prepare annotation', () => {
test('prepare annotation without console output', () => {
const result = ResultsParser.convertTestCase('Test Suite', {
_attributes: {
name: 'Test Name',
@@ -179,6 +162,40 @@ describe('ResultsParser', () => {
title: 'Test Full Name',
});
});
test('prepare annotation with console output', () => {
const result = ResultsParser.convertTestCase('Test Suite', {
_attributes: {
name: 'Test Name',
fullname: 'Test Full Name',
duration: '3.14',
},
output: {
_cdata: '[Warning] This is a warning',
},
failure: {
message: { _cdata: 'Message CDATA' },
'stack-trace': {
_cdata:
'at Tests.SetupFailedTest.SetUp () [0x00000] in /github/workspace/unity-project/Assets/Tests/SetupFailedTest.cs:10',
},
},
});
expect(result.suite).toBe('Test Suite');
expect(result.title).toBe('Test Name');
expect(result.duration).toBe(3.14);
expect(result.annotation).toMatchObject({
annotation_level: 'failure',
end_line: 10,
message: 'Message CDATA',
path: '/github/workspace/unity-project/Assets/Tests/SetupFailedTest.cs',
raw_details:
'[Warning] This is a warning\nat Tests.SetupFailedTest.SetUp () [0x00000] in /github/workspace/unity-project/Assets/Tests/SetupFailedTest.cs:10',
start_line: 10,
title: 'Test Full Name',
});
});
});
describe('findAnnotationPoint', () => {
@@ -189,14 +206,16 @@ describe('ResultsParser', () => {
});
test('simple annotation point', () => {
const result = ResultsParser.findAnnotationPoint(`at Tests.PlayModeTest+<FailedUnityTest>d__5.MoveNext () [0x0002e] in /github/workspace/unity-project/Assets/Tests/PlayModeTest.cs:39
const result =
ResultsParser.findAnnotationPoint(`at Tests.PlayModeTest+<FailedUnityTest>d__5.MoveNext () [0x0002e] in /github/workspace/unity-project/Assets/Tests/PlayModeTest.cs:39
at UnityEngine.TestTools.TestEnumerator+<Execute>d__6.MoveNext () [0x00038] in /github/workspace/unity-project/Library/PackageCache/com.unity.test-framework@1.1.19/UnityEngine.TestRunner/NUnitExtensions/Attributes/TestEnumerator.cs:36`);
expect(result.path).toBe('/github/workspace/unity-project/Assets/Tests/PlayModeTest.cs');
expect(result.line).toBe(39);
});
test('first entry with non-zero line number annotation point', () => {
const result = ResultsParser.findAnnotationPoint(`at FluentAssertions.Execution.LateBoundTestFramework.Throw (System.String message) [0x00044] in <527a5493e59e45679b35c1e8d65350b3>:0
const result =
ResultsParser.findAnnotationPoint(`at FluentAssertions.Execution.LateBoundTestFramework.Throw (System.String message) [0x00044] in <527a5493e59e45679b35c1e8d65350b3>:0
at FluentAssertions.Execution.TestFrameworkProvider.Throw (System.String message) [0x00011] in <527a5493e59e45679b35c1e8d65350b3>:0
at FluentAssertions.Execution.DefaultAssertionStrategy.HandleFailure (System.String message) [0x00005] in <527a5493e59e45679b35c1e8d65350b3>:0
at Tests.PlayModeTest+<FailedUnityTest>d__5.MoveNext () [0x0002e] in /github/workspace/unity-project/Assets/Tests/PlayModeTest.cs:39

View File

@@ -1,11 +1,11 @@
import * as core from '@actions/core';
import * as xmljs from 'xml-js';
import * as fs from 'fs';
import * as xmljs from 'xml-js';
import { RunMeta, TestMeta } from './results-meta';
import path from 'path';
import { RunMeta, TestMeta } from './ts/results-meta.ts';
class ResultsParser {
static async parseResults(filepath) {
const ResultsParser = {
async parseResults(filepath): Promise<RunMeta> {
if (!fs.existsSync(filepath)) {
throw new Error(`Missing file! {"filepath": "${filepath}"}`);
}
@@ -16,15 +16,15 @@ class ResultsParser {
core.info(`File ${filepath} parsed...`);
return ResultsParser.convertResults(path.basename(filepath), results);
}
},
static convertResults(filename, filedata) {
convertResults(filename, filedata): RunMeta {
core.info(`Start analyzing results: ${filename}`);
const run = filedata['test-run'];
const runMeta = new RunMeta(filename);
const tests = ResultsParser.convertSuite(run['test-suite']);
core.debug(tests);
core.debug(tests.toString());
runMeta.total = Number(run._attributes.total);
runMeta.failed = Number(run._attributes.failed);
@@ -34,18 +34,18 @@ class ResultsParser {
runMeta.addTests(tests);
return runMeta;
}
},
static convertSuite(suites) {
convertSuite(suites) {
if (Array.isArray(suites)) {
const innerResult = [];
suites.forEach(suite => {
const innerResult: TestMeta[] = [];
for (const suite of suites) {
innerResult.push(...ResultsParser.convertSuite(suite));
});
}
return innerResult;
}
const result = [];
const result: TestMeta[] = [];
const innerSuite = suites['test-suite'];
if (innerSuite) {
result.push(...ResultsParser.convertSuite(innerSuite));
@@ -57,22 +57,22 @@ class ResultsParser {
}
return result;
}
},
static convertTests(suite, tests) {
convertTests(suite, tests): TestMeta[] {
if (Array.isArray(tests)) {
const result = [];
tests.forEach(test => {
result.push(ResultsParser.convertTestCase(suite, test));
});
const result: TestMeta[] = [];
for (const testCase of tests) {
result.push(ResultsParser.convertTestCase(suite, testCase));
}
return result;
}
return [ResultsParser.convertTestCase(suite, tests)];
}
},
static convertTestCase(suite, testCase) {
const { _attributes, failure } = testCase;
convertTestCase(suite, testCase): TestMeta {
const { _attributes, failure, output } = testCase;
const { name, fullname, result, duration } = _attributes;
const testMeta = new TestMeta(suite, name);
testMeta.result = result;
@@ -96,6 +96,14 @@ class ResultsParser {
return testMeta;
}
const rawDetails = [trace];
if (output && output._cdata) {
rawDetails.unshift(output._cdata);
} else {
core.debug(`No console output for test case: ${fullname}`);
}
testMeta.annotation = {
path: point.path,
start_line: point.line,
@@ -103,20 +111,23 @@ class ResultsParser {
annotation_level: 'failure',
title: fullname,
message: failure.message._cdata ? failure.message._cdata : 'Test Failed!',
raw_details: trace,
raw_details: rawDetails.join('\n'),
start_column: 0,
end_column: 0,
blob_href: '',
};
core.info(
`- ${testMeta.annotation.path}:${testMeta.annotation.start_line} - ${testMeta.annotation.title}`,
);
return testMeta;
}
},
static findAnnotationPoint(trace) {
findAnnotationPoint(trace) {
// Find first entry with non-zero line number in stack trace
const items = trace.match(/at .* in ((?<path>[^:]+):(?<line>\d+))/g);
if (Array.isArray(items)) {
const result = [];
items.forEach(item => {
const result: { path: any; line: number }[] = [];
for (const item of items) {
const match = item.match(/at .* in ((?<path>[^:]+):(?<line>\d+))/);
const point = {
path: match ? match.groups.path : '',
@@ -125,7 +136,7 @@ class ResultsParser {
if (point.line > 0) {
result.push(point);
}
});
}
if (result.length > 0) {
return result[0];
}
@@ -136,7 +147,7 @@ class ResultsParser {
path: match ? match.groups.path : '',
line: match ? Number(match.groups.line) : 0,
};
}
}
},
};
export default ResultsParser;

View File

@@ -1,20 +1,20 @@
import fs from 'fs';
import path from 'path';
class UnityVersionParser {
static get versionPattern() {
const UnityVersionParser = {
get versionPattern() {
return /20\d{2}\.\d\.\w{3,4}|3/;
}
},
static parse(projectVersionTxt) {
parse(projectVersionTxt) {
const matches = projectVersionTxt.match(UnityVersionParser.versionPattern);
if (!matches || matches.length === 0) {
throw new Error(`Failed to parse version from "${projectVersionTxt}".`);
}
return matches[0];
}
},
static read(projectPath) {
read(projectPath) {
const filePath = path.join(projectPath, 'ProjectSettings', 'ProjectVersion.txt');
if (!fs.existsSync(filePath)) {
throw new Error(
@@ -22,7 +22,7 @@ class UnityVersionParser {
);
}
return UnityVersionParser.parse(fs.readFileSync(filePath, 'utf8'));
}
}
},
};
export default UnityVersionParser;