Add Feature to test Unity Packages (#164)

* (should fail) add jq install to docker image

* (might fail) remove rm rf call

* move things around to try to fix test

* Revert "(might fail) remove rm rf call"

This reverts commit 22f74ebca7.

* remove silent setting from docker test

* Fix failing test's Docker image

* Add new input and basic test

* Add test package; start using jq cli

* Use test package in test workflow

* Create temporary Unity Project and run tests

* Test removing jq install from Dockerfile

* Revert "Test removing jq install from Dockerfile"

This reverts commit 6aa7a6f443.

* Remove mkdir call

* remove duplicate command

* add packageMode option back in

* build changes

* check for apt-get before installing jq

* change apt-get version check message

* spelling and documentation fixes

* add working example unity package with tests

* add temp pwd call to help figure out absolute pathing

* fix workflow package location

* add jq to add package to temp project packages

* try fixing jq calls

* clean up jq calls, fix variable expansion

* try renaming jq args

* try using different arg syntax for jq

* try wrapping args in parentheses

* try using double quotes only

* try changing up quoting

* try properly using string interpolation

* try removing colon

* add string interpolation to key

* omit double quotes from jq call to retrieve package name

* clean up logging

* add rest of workflow tests

* Revert "add rest of workflow tests"

This reverts commit c0bb008b2c.

* add play mode test without cache

* add package mode all mode workflow step

* add consecutive ppackage mode workflow step

* add package mode "like in the readme" test

* fix workflow syntax error

* try to fix syntax error again

* use correct folder

* *hopefully actually* use correct package path

* try adding caching to "readme" test

* remove caching/mentions of caching from package mode tests

* fix artifacts paths

* fix artifacts pathing and names

* fix combined artifacts for package mode

* clean up documentation and exit code

* clarify allowed docker images for packageMode

* update README to mention Unity packages

* move package name validation to TS part of action

* improve logging for temp project creation failure

* make husky hook executable

* add error for missing tests folder

* update docs to reflect unsupported packages

* remove jq install

* Revert "remove jq install"

This reverts commit bd35ac8f6f.

* TEMP log image in use

* Revert "TEMP log image in use"

This reverts commit 95722dcab4.

* Revert "Revert "remove jq install""

This reverts commit e3bac048b1.

* TEMP list installed packages

* Revert "TEMP list installed packages"

This reverts commit db9c07da38.

* TEMP log project's manifest

* add code coverage package to generated project

* remove temp project manifest log

* add coverage to package mode tests

* update name of package coverage steps

* add codecoverage dependency to test package

* Revert "add codecoverage dependency to test package"

This reverts commit 4b2c03069d.

* add assembly filters for coverage

* TEMP console log project folder

* Revert "TEMP console log project folder"

This reverts commit 411ec51817.

* add logic to copy package to folder without activation file

* fix false positive activation file detection

* fix improper bash "if" formatting

* TEMP remove conditional for package copying

* Revert "TEMP remove conditional for package copying"

This reverts commit 4f12d83889.

* Revert "fix improper bash "if" formatting"

This reverts commit acb975bcea.

* Revert "fix false positive activation file detection"

This reverts commit 580c9c14a0.

* Revert "add logic to copy package to folder without activation file"

This reverts commit b20d994b5d.

* run yarn build

* move package mode check lower in the file

* throw error if unity version is auto in package mode

* fix unity version error wording

* try deleting activate license file

* try logging hidden package files

* try deleting all non-package files

* fix license activation files deletion

* scrap file removals and print dir permissions

* log permissions for package folder

* Add packageMode inputs to main

* fix fs mocks and run yarn build

* fix documentation and add error message for missing jq

* add clarification on package mode caveats

* fix line endings problem (?)

* Revert "fix line endings problem (?)"

This reverts commit 1cba302bc4.

* Revert "add clarification on package mode caveats"

This reverts commit fb62d36ba1.

* Revert "fix documentation and add error message for missing jq"

This reverts commit 0df3ab6b88.

* Redo the input docs fixes

* Redo the jq presence test

* update readme to indicate package mode caveats

* fix wording on coverageOptions

* one more wording fix on coverageOptions

* move sample package to example.com domain

---------

Co-authored-by: Aaron Trudeau <120415438+trudeaua-vividream-software@users.noreply.github.com>
pull/229/head v2
Aaron Trudeau 2023-07-03 11:09:20 -04:00 committed by GitHub
parent 31086d9859
commit 7787abf249
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1058 additions and 11 deletions

View File

@ -399,3 +399,226 @@ jobs:
name: Test results (combined)
path: artifacts/
retention-days: 14
testAllPackageModesLikeInTheReadme:
name: Test package mode 📦 in ${{ matrix.testMode }} on version ${{ matrix.unityVersion }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
projectPath:
- unity-package-with-correct-tests/com.example.testpackage
unityVersion:
- 2019.2.11f1
testMode:
- playmode
- editmode
steps:
###########################
# Checkout #
###########################
- name: Checkout
uses: actions/checkout@v2
with:
lfs: true
- uses: ./
id: packageTests
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
testMode: ${{ matrix.testMode }}
artifactsPath: ${{ matrix.testMode }}-packageArtifacts
customParameters: -profile SomeProfile -someBoolean -someValue exampleValue
packageMode: true
- uses: actions/upload-artifact@v2
with:
name: Package test results for ${{ matrix.testMode }}
path: ${{ steps.packageTests.outputs.artifactsPath }}
retention-days: 14
testPackageRunnerInAllModes:
name: Test package mode in all modes 📦✨
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
projectPath:
- unity-package-with-correct-tests/com.example.testpackage
unityVersion:
- 2019.2.11f1
steps:
###########################
# Checkout #
###########################
- uses: actions/checkout@v2
with:
lfs: true
# Configure test runner
- name: Run tests
id: packageAllTests
uses: ./
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
testMode: all
coverageOptions: 'generateAdditionalMetrics;generateHtmlReport;generateBadgeReport;assemblyFilters:+example.testpackage.*,-*Tests*'
packageMode: true
# Test implicit artifactsPath, by not setting it
# Upload artifacts
- name: Upload test results
uses: actions/upload-artifact@v2
with:
name: Package test results (all)
path: ${{ steps.packageAllTests.outputs.artifactsPath }}
retention-days: 14
# Upload coverage
- name: Upload coverage results
uses: actions/upload-artifact@v3
with:
name: Package Coverage results (all)
path: ${{ steps.packageAllTests.outputs.coveragePath }}
retention-days: 14
testPackageRunnerInEditMode:
name: Test package mode in edit mode 📦📝
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
unityVersion:
- 2019.2.11f1
projectPath:
- unity-package-with-correct-tests/com.example.testpackage
steps:
###########################
# Checkout #
###########################
- uses: actions/checkout@v2
with:
lfs: true
# Configure test runner
- name: Run tests
id: packageEditMode
uses: ./
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
testMode: editmode
coverageOptions: 'generateAdditionalMetrics;generateHtmlReport;generateBadgeReport;assemblyFilters:+example.testpackage.*,-*Tests*'
artifactsPath: artifacts/packageeditmode
packageMode: true
# Upload artifacts
- name: Upload test results
uses: actions/upload-artifact@v2
with:
name: Package test results (edit mode)
path: ${{ steps.packageEditMode.outputs.artifactsPath }}
retention-days: 14
# Upload coverage
- name: Upload coverage results
uses: actions/upload-artifact@v3
with:
name: Package Coverage results (edit mode)
path: ${{ steps.packageEditMode.outputs.coveragePath }}
retention-days: 14
testPackageRunnerInPlayMode:
name: Test package mode in play mode 📦📺
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
projectPath:
- unity-package-with-correct-tests/com.example.testpackage
unityVersion:
- 2019.2.11f1
steps:
###########################
# Checkout #
###########################
- uses: actions/checkout@v2
with:
lfs: true
# Configure test runner
- name: Run tests
id: packagePlayMode
uses: ./
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
testMode: playmode
coverageOptions: 'generateAdditionalMetrics;generateHtmlReport;generateBadgeReport;assemblyFilters:+example.testpackage.*,-*Tests*'
artifactsPath: artifacts/packageplaymode
packageMode: true
# Upload artifacts
- name: Upload test results
uses: actions/upload-artifact@v2
with:
name: Package test results (play mode)
path: ${{ steps.packagePlayMode.outputs.artifactsPath }}
retention-days: 14
# Upload coverage
- name: Upload coverage results
uses: actions/upload-artifact@v3
with:
name: Package Coverage results (play mode)
path: ${{ steps.packagePlayMode.outputs.coveragePath }}
retention-days: 14
testPackageModeEachModeSequentially:
name: Test package mode in each mode sequentially 📦 👩‍👩‍👧‍👦 # don't try this at home (it's much slower)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
unityVersion:
- 2019.2.11f1
projectPath:
- unity-package-with-correct-tests/com.example.testpackage
steps:
###########################
# Checkout #
###########################
- uses: actions/checkout@v2
with:
lfs: true
# Configure first test runner
- name: Test package mode in editmode 📦📝
uses: ./
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
testMode: editmode
artifactsPath: packageArtifacts/editmode
packageMode: true
# Configure second test runner
- name: Test package mode in playmode 📦📺
uses: ./
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
testMode: playmode
artifactsPath: packageArtifacts/playmode
packageMode: true
# Upload combined artifacts
- name: Upload combined test results
uses: actions/upload-artifact@v2
with:
name: Package test results (combined)
path: packageArtifacts/
retention-days: 14

0
.husky/pre-commit 100644 → 100755
View File

View File

@ -4,7 +4,7 @@
GitHub Action to
[run tests](https://github.com/marketplace/actions/unity-test-runner)
for any Unity project.
for any Unity project and _some_ Unity packages.
Part of the <a href="https://game.ci">GameCI</a> open source project.
<br />

View File

@ -5,14 +5,14 @@ inputs:
unityVersion:
required: false
default: 'auto'
description: 'Version of unity to use for testing the project. Use "auto" to get from your ProjectSettings/ProjectVersion.txt'
description: 'Version of unity to use for testing the project. Use "auto" to get from your ProjectSettings/ProjectVersion.txt. ⚠️ If testing a Unity Package, this field is required and cannot be set to "auto".'
customImage:
required: false
default: ''
description: 'Specific docker image that should be used for testing the project'
description: 'Specific docker image that should be used for testing the project. If packageMode is true, this image must have jq installed.'
projectPath:
required: false
description: 'Path to the Unity project to be tested.'
description: 'Path to the Unity project or package to be tested.'
customParameters:
required: false
description: 'Extra parameters to configure the Unity editor run.'
@ -23,7 +23,7 @@ inputs:
coverageOptions:
required: false
default: 'generateAdditionalMetrics;generateHtmlReport;generateBadgeReport;dontClear'
description: 'Optional coverage parameters for the -coverageOptions argument.'
description: 'Optional coverage parameters for the -coverageOptions argument. To get coverage in Package Mode, pass assemblies from the package you want covered to the assemblyFilters option.'
artifactsPath:
required: false
default: 'artifacts'
@ -48,6 +48,10 @@ inputs:
required: false
default: 'Test Results'
description: 'Name for the check run that is created when a github token is provided.'
packageMode:
required: false
default: false
description: 'Whether the tests are being run for a Unity package instead of a Unity project. If true, the action can only be run on Linux runners, and any custom docker image passed to this action must have `jq` installed. NOTE: may not work properly for packages with dependencies outside of the Unity Registry.'
chownFilesTo:
required: false
default: ''

68
dist/index.js generated vendored
View File

@ -98,7 +98,7 @@ function run() {
try {
model_1.Action.checkCompatibility();
const { workspace, actionFolder } = model_1.Action;
const { editorVersion, customImage, projectPath, customParameters, testMode, coverageOptions, artifactsPath, useHostNetwork, sshAgent, gitPrivateToken, githubToken, checkName, chownFilesTo, unityLicensingServer, } = model_1.Input.getFromUser();
const { editorVersion, customImage, projectPath, customParameters, testMode, coverageOptions, artifactsPath, useHostNetwork, sshAgent, gitPrivateToken, githubToken, checkName, packageMode, packageName, chownFilesTo, unityLicensingServer, } = model_1.Input.getFromUser();
const baseImage = new model_1.ImageTag({ editorVersion, customImage });
const runnerContext = model_1.Action.runnerContext();
try {
@ -112,6 +112,8 @@ function run() {
artifactsPath,
useHostNetwork,
sshAgent,
packageMode,
packageName,
gitPrivateToken,
githubToken,
chownFilesTo,
@ -260,7 +262,7 @@ const Docker = {
});
},
getLinuxCommand(image, parameters) {
const { actionFolder, editorVersion, workspace, projectPath, customParameters, testMode, coverageOptions, artifactsPath, useHostNetwork, sshAgent, gitPrivateToken, githubToken, runnerTemporaryPath, chownFilesTo, unityLicensingServer, } = parameters;
const { actionFolder, editorVersion, workspace, projectPath, customParameters, testMode, coverageOptions, artifactsPath, useHostNetwork, sshAgent, packageMode, packageName, gitPrivateToken, githubToken, runnerTemporaryPath, chownFilesTo, unityLicensingServer, } = parameters;
const githubHome = path_1.default.join(runnerTemporaryPath, '_github_home');
if (!(0, fs_1.existsSync)(githubHome))
(0, fs_1.mkdirSync)(githubHome);
@ -286,6 +288,8 @@ const Docker = {
--env COVERAGE_OPTIONS="${coverageOptions}" \
--env COVERAGE_RESULTS_PATH="CodeCoverage" \
--env ARTIFACTS_PATH="${artifactsPath}" \
--env PACKAGE_MODE="${packageMode}" \
--env PACKAGE_NAME="${packageName}" \
--env GITHUB_REF \
--env GITHUB_SHA \
--env GITHUB_REPOSITORY \
@ -319,7 +323,7 @@ const Docker = {
/bin/bash -c /entrypoint.sh`;
},
getWindowsCommand(image, parameters) {
const { actionFolder, editorVersion, workspace, projectPath, customParameters, testMode, coverageOptions, artifactsPath, useHostNetwork, sshAgent, gitPrivateToken, githubToken, runnerTemporaryPath, chownFilesTo, unityLicensingServer, } = parameters;
const { actionFolder, editorVersion, workspace, projectPath, customParameters, testMode, coverageOptions, artifactsPath, useHostNetwork, sshAgent, packageMode, packageName, gitPrivateToken, githubToken, runnerTemporaryPath, chownFilesTo, unityLicensingServer, } = parameters;
const githubHome = path_1.default.join(runnerTemporaryPath, '_github_home');
if (!(0, fs_1.existsSync)(githubHome))
(0, fs_1.mkdirSync)(githubHome);
@ -345,6 +349,8 @@ const Docker = {
--env COVERAGE_OPTIONS="${coverageOptions}" \
--env COVERAGE_RESULTS_PATH="CodeCoverage" \
--env ARTIFACTS_PATH="${artifactsPath}" \
--env PACKAGE_MODE="${packageMode}" \
--env PACKAGE_NAME="${packageName}" \
--env GITHUB_REF \
--env GITHUB_SHA \
--env GITHUB_REPOSITORY \
@ -556,6 +562,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const unity_version_parser_1 = __importDefault(__nccwpck_require__(7049));
const fs_1 = __importDefault(__nccwpck_require__(7147));
const core_1 = __nccwpck_require__(2186);
const Input = {
get testModes() {
@ -565,6 +572,41 @@ const Input = {
const validFolderName = new RegExp(/^(\.|\.\/)?(\.?[\w~]+([ _-]?[\w~]+)*\/?)*$/);
return validFolderName.test(folderName);
},
/**
* When in package mode, we need to scrape the package's name from its package.json file
*/
getPackageNameFromPackageJson(packagePath) {
const packageJsonPath = `${packagePath}/package.json`;
if (!fs_1.default.existsSync(packageJsonPath)) {
throw new Error(`Invalid projectPath - Cannot find package.json at ${packageJsonPath}`);
}
let packageJson;
try {
packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString());
}
catch (error) {
if (error instanceof SyntaxError) {
throw new SyntaxError(`Unable to parse package.json contents as JSON - ${error.message}`);
}
throw new Error(`Unable to parse package.json contents as JSON - unknown error ocurred`);
}
const rawPackageName = packageJson.name;
if (typeof rawPackageName !== 'string') {
throw new TypeError(`Unable to parse package name from package.json - package name should be string, but was ${typeof rawPackageName}`);
}
if (rawPackageName.length === 0) {
throw new Error(`Package name from package.json is a string, but is empty`);
}
return rawPackageName;
},
/**
* When in package mode, we need to ensure that the Tests folder is present
*/
verifyTestsFolderIsPresent(packagePath) {
if (!fs_1.default.existsSync(`${packagePath}/Tests`)) {
throw new Error(`Invalid projectPath - Cannot find package tests folder at ${packagePath}/Tests`);
}
},
getFromUser() {
// Input variables specified in workflow using "with" prop.
const unityVersion = (0, core_1.getInput)('unityVersion') || 'auto';
@ -580,6 +622,8 @@ const Input = {
const gitPrivateToken = (0, core_1.getInput)('gitPrivateToken') || '';
const githubToken = (0, core_1.getInput)('githubToken') || '';
const checkName = (0, core_1.getInput)('checkName') || 'Test Results';
const rawPackageMode = (0, core_1.getInput)('packageMode') || 'false';
let packageName = '';
const chownFilesTo = (0, core_1.getInput)('chownFilesTo') || '';
// Validate input
if (!this.testModes.includes(testMode)) {
@ -594,8 +638,22 @@ const Input = {
if (rawUseHostNetwork !== 'true' && rawUseHostNetwork !== 'false') {
throw new Error(`Invalid useHostNetwork "${rawUseHostNetwork}"`);
}
// Sanitise input
if (rawPackageMode !== 'true' && rawPackageMode !== 'false') {
throw new Error(`Invalid packageMode "${rawPackageMode}"`);
}
// sanitize packageMode input and projectPath input since they are needed
// for input validation
const packageMode = rawPackageMode === 'true';
const projectPath = rawProjectPath.replace(/\/$/, '');
// if in package mode, attempt to get the package's name, and ensure tests are present
if (packageMode) {
if (unityVersion === 'auto') {
throw new Error('Package Mode is enabled, but unityVersion is set to "auto". unityVersion must manually be set in Package Mode.');
}
packageName = this.getPackageNameFromPackageJson(projectPath);
this.verifyTestsFolderIsPresent(projectPath);
}
// Sanitise other input
const artifactsPath = rawArtifactsPath.replace(/\/$/, '');
const useHostNetwork = rawUseHostNetwork === 'true';
const editorVersion = unityVersion === 'auto' ? unity_version_parser_1.default.read(projectPath) : unityVersion;
@ -613,6 +671,8 @@ const Input = {
gitPrivateToken,
githubToken,
checkName,
packageMode,
packageName,
chownFilesTo,
unityLicensingServer,
};

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@ -36,6 +36,76 @@ echo "Using custom parameters $CUSTOM_PARAMETERS."
echo "Using Unity version \"$UNITY_VERSION\" to test."
#
# Create an empty project for testing if in package mode
#
if [ "$PACKAGE_MODE" = "true" ]; then
echo "Running tests on a Unity package rather than a Unity project."
if ! command -v jq &> /dev/null
then
echo "jq could not be found. This is required for package mode, and is likely the result of using a custom Docker image. Please use the default image or install jq to your custom image."
exit 1
fi
echo ""
echo "###########################"
echo "# Package Folder #"
echo "###########################"
echo ""
ls -la "$UNITY_PROJECT_PATH"
echo ""
echo "Creating an empty Unity project to add the package $PACKAGE_NAME to."
TEMP_PROJECT_PATH="./TempProject"
unity-editor \
-batchmode \
-createProject "$TEMP_PROJECT_PATH" \
-quit
# use jq to add the package to the temp project through manually modifying Packages/manifest.json
echo "Adding package to the temporary project's dependencies and testables..."
echo ""
PACKAGE_MANIFEST_PATH="$TEMP_PROJECT_PATH/Packages/manifest.json"
if [ ! -f "$PACKAGE_MANIFEST_PATH" ]; then
echo "Packages/mainfest.json was not created properly. This indicates a problem with the Action, not with your package. Logging directories and aborting..."
echo ""
echo "###########################"
echo "# Temp Project Folder #"
echo "###########################"
echo ""
ls -a "$TEMP_PROJECT_PATH"
echo ""
echo "################################"
echo "# Temp Project Packages Folder #"
echo "################################"
echo ""
ls -a "$TEMP_PROJECT_PATH/Packages"
exit 1
fi
PACKAGE_MANIFEST_JSON=$(cat "$PACKAGE_MANIFEST_PATH")
echo "$PACKAGE_MANIFEST_JSON" | \
jq \
--arg packageName "$PACKAGE_NAME" \
--arg projectPath "$UNITY_PROJECT_PATH" \
'.dependencies += {"com.unity.testtools.codecoverage": "1.1.1"} | .dependencies += {"\($packageName)": "file:\($projectPath)"} | . += {testables: ["\($packageName)"]}' \
> "$PACKAGE_MANIFEST_PATH"
UNITY_PROJECT_PATH="$TEMP_PROJECT_PATH"
fi
#
# Overall info
#

View File

@ -19,6 +19,8 @@ export async function run() {
gitPrivateToken,
githubToken,
checkName,
packageMode,
packageName,
chownFilesTo,
unityLicensingServer,
} = Input.getFromUser();
@ -37,6 +39,8 @@ export async function run() {
artifactsPath,
useHostNetwork,
sshAgent,
packageMode,
packageName,
gitPrivateToken,
githubToken,
chownFilesTo,

View File

@ -62,6 +62,8 @@ const Docker = {
artifactsPath,
useHostNetwork,
sshAgent,
packageMode,
packageName,
gitPrivateToken,
githubToken,
runnerTemporaryPath,
@ -95,6 +97,8 @@ const Docker = {
--env COVERAGE_OPTIONS="${coverageOptions}" \
--env COVERAGE_RESULTS_PATH="CodeCoverage" \
--env ARTIFACTS_PATH="${artifactsPath}" \
--env PACKAGE_MODE="${packageMode}" \
--env PACKAGE_NAME="${packageName}" \
--env GITHUB_REF \
--env GITHUB_SHA \
--env GITHUB_REPOSITORY \
@ -142,6 +146,8 @@ const Docker = {
artifactsPath,
useHostNetwork,
sshAgent,
packageMode,
packageName,
gitPrivateToken,
githubToken,
runnerTemporaryPath,
@ -175,6 +181,8 @@ const Docker = {
--env COVERAGE_OPTIONS="${coverageOptions}" \
--env COVERAGE_RESULTS_PATH="CodeCoverage" \
--env ARTIFACTS_PATH="${artifactsPath}" \
--env PACKAGE_MODE="${packageMode}" \
--env PACKAGE_NAME="${packageName}" \
--env GITHUB_REF \
--env GITHUB_SHA \
--env GITHUB_REPOSITORY \

View File

@ -1,7 +1,11 @@
import Input from './input';
import fs from 'fs';
jest.mock('./unity-version-parser');
const mockedFsExistsSync = jest.spyOn(fs, 'existsSync');
const mockedFsReadFileSync = jest.spyOn(fs, 'readFileSync');
describe('Input', () => {
describe('getFromUser', () => {
it('does not throw', () => {
@ -33,4 +37,79 @@ describe('Input', () => {
expect(Input.isValidFolderName(folderName)).toStrictEqual(false);
});
});
describe('getPackageNameFromPackageJson', () => {
it('throws error if package.json cannot be found at the given project path', () => {
mockedFsExistsSync.mockReturnValue(false);
expect(() => Input.getPackageNameFromPackageJson('some/path')).toThrow(
'Invalid projectPath - Cannot find package.json at some/path/package.json',
);
});
it('throws error if package.json contents cannot be parsed', () => {
mockedFsExistsSync.mockReturnValue(true);
mockedFsReadFileSync.mockReturnValue(Buffer.from('DefinitelyNotJSON'));
expect(() => Input.getPackageNameFromPackageJson('some/path')).toThrow(
/Unable to parse package.json contents as JSON/,
);
});
it('throws error if name field in package.json is not present', () => {
mockedFsExistsSync.mockReturnValue(true);
mockedFsReadFileSync.mockReturnValue(
Buffer.from(JSON.stringify({ notName: 'some-package', alsoNotName: 'some-package' })),
);
expect(() => Input.getPackageNameFromPackageJson('some/path')).toThrow(
'Unable to parse package name from package.json - package name should be string, but was undefined',
);
});
it('throws error if name field in package.json is present but not a string', () => {
mockedFsExistsSync.mockReturnValue(true);
mockedFsReadFileSync.mockReturnValue(
Buffer.from(JSON.stringify({ name: 3, notName: 'some-package' })),
);
expect(() => Input.getPackageNameFromPackageJson('some/path')).toThrow(
'Unable to parse package name from package.json - package name should be string, but was number',
);
});
it('throws error if name field in package.json is present but empty', () => {
mockedFsExistsSync.mockReturnValue(true);
mockedFsReadFileSync.mockReturnValue(Buffer.from(JSON.stringify({ name: '', notName: 3 })));
expect(() => Input.getPackageNameFromPackageJson('some/path')).toThrow(
'Package name from package.json is a string, but is empty',
);
});
it('returns the name field in package.json if it is present as a non-empty string', () => {
mockedFsExistsSync.mockReturnValue(true);
mockedFsReadFileSync.mockReturnValue(
Buffer.from(JSON.stringify({ name: 'some-package', notName: 'not-what-we-want' })),
);
expect(Input.getPackageNameFromPackageJson('some/path')).toStrictEqual('some-package');
});
});
describe('verifyTestsFolderIsPresent', () => {
it('throws error if tests folder is not present', () => {
mockedFsExistsSync.mockReturnValue(false);
expect(() => Input.verifyTestsFolderIsPresent('some/path')).toThrow(
'Invalid projectPath - Cannot find package tests folder at some/path/Tests',
);
});
it('does not throw if tests folder is present', () => {
mockedFsExistsSync.mockReturnValue(true);
expect(() => Input.verifyTestsFolderIsPresent('some/path')).not.toThrow();
});
});
});

View File

@ -1,4 +1,5 @@
import UnityVersionParser from './unity-version-parser';
import fs from 'fs';
import { getInput } from '@actions/core';
const Input = {
@ -12,6 +13,53 @@ const Input = {
return validFolderName.test(folderName);
},
/**
* When in package mode, we need to scrape the package's name from its package.json file
*/
getPackageNameFromPackageJson(packagePath) {
const packageJsonPath = `${packagePath}/package.json`;
if (!fs.existsSync(packageJsonPath)) {
throw new Error(`Invalid projectPath - Cannot find package.json at ${packageJsonPath}`);
}
let packageJson;
try {
packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString());
} catch (error) {
if (error instanceof SyntaxError) {
throw new SyntaxError(`Unable to parse package.json contents as JSON - ${error.message}`);
}
throw new Error(`Unable to parse package.json contents as JSON - unknown error ocurred`);
}
const rawPackageName = packageJson.name;
if (typeof rawPackageName !== 'string') {
throw new TypeError(
`Unable to parse package name from package.json - package name should be string, but was ${typeof rawPackageName}`,
);
}
if (rawPackageName.length === 0) {
throw new Error(`Package name from package.json is a string, but is empty`);
}
return rawPackageName;
},
/**
* When in package mode, we need to ensure that the Tests folder is present
*/
verifyTestsFolderIsPresent(packagePath) {
if (!fs.existsSync(`${packagePath}/Tests`)) {
throw new Error(
`Invalid projectPath - Cannot find package tests folder at ${packagePath}/Tests`,
);
}
},
getFromUser() {
// Input variables specified in workflow using "with" prop.
const unityVersion = getInput('unityVersion') || 'auto';
@ -27,6 +75,8 @@ const Input = {
const gitPrivateToken = getInput('gitPrivateToken') || '';
const githubToken = getInput('githubToken') || '';
const checkName = getInput('checkName') || 'Test Results';
const rawPackageMode = getInput('packageMode') || 'false';
let packageName = '';
const chownFilesTo = getInput('chownFilesTo') || '';
// Validate input
@ -46,8 +96,28 @@ const Input = {
throw new Error(`Invalid useHostNetwork "${rawUseHostNetwork}"`);
}
// Sanitise input
if (rawPackageMode !== 'true' && rawPackageMode !== 'false') {
throw new Error(`Invalid packageMode "${rawPackageMode}"`);
}
// sanitize packageMode input and projectPath input since they are needed
// for input validation
const packageMode = rawPackageMode === 'true';
const projectPath = rawProjectPath.replace(/\/$/, '');
// if in package mode, attempt to get the package's name, and ensure tests are present
if (packageMode) {
if (unityVersion === 'auto') {
throw new Error(
'Package Mode is enabled, but unityVersion is set to "auto". unityVersion must manually be set in Package Mode.',
);
}
packageName = this.getPackageNameFromPackageJson(projectPath);
this.verifyTestsFolderIsPresent(projectPath);
}
// Sanitise other input
const artifactsPath = rawArtifactsPath.replace(/\/$/, '');
const useHostNetwork = rawUseHostNetwork === 'true';
const editorVersion =
@ -67,6 +137,8 @@ const Input = {
gitPrivateToken,
githubToken,
checkName,
packageMode,
packageName,
chownFilesTo,
unityLicensingServer,
};

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a62b511ba12825d4d9f992b4ed37a533
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e3a65787d84893340b9dc38af5b7c31f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,15 @@
using UnityEngine;
using System.Collections;
using UnityEditor;
[CustomEditor(typeof(TimerComponent))]
public class LevelScriptEditor : Editor
{
public override void OnInspectorGUI()
{
TimerComponent myTarget = (TimerComponent)target;
EditorGUILayout.LabelField("Timer", myTarget.Timer.ToString());
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f0a715d2f35ea4c40a6f1cdae355c61c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,16 @@
{
"name": "example.testpackage.Editor",
"rootNamespace": "",
"references": [
"example.testpackage.Runtime"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8223b1b52474b674a87c6113b6384f10
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e472ec5749e60ca4db87f10cec905d2c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 21861106477d38342a589fc525c4e0bb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6c6729c46a2a6594da2ce1182420ab81
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
using System;
public class BasicCounter
{
public const int MaxCount = 10;
public BasicCounter(int count = 0)
{
Count = count;
}
public void Increment()
{
Count = Math.Min(MaxCount, Count + 1);
}
public int Count { get; private set; }
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0bd8dfbd5c7fc9e439246091668234b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,17 @@
using UnityEngine;
public class SampleComponent : MonoBehaviour
{
public BasicCounter Counter;
void Start()
{
Counter = new BasicCounter(5);
}
// Update is called once per frame
void Update()
{
Counter.Increment();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 121f2ede62657a84082c012941df22d5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
using UnityEngine;
public class TimerComponent : MonoBehaviour
{
public BasicCounter Counter = new BasicCounter();
public float Timer = 1f;
void Update()
{
Timer -= Time.deltaTime;
if (Timer > 0)
return;
Counter.Increment();
Timer = 1f;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 563e4fb514abf6141b80ca1b71c08889
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,14 @@
{
"name": "example.testpackage.Runtime",
"rootNamespace": "",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b20629d7e725e1e449076020f132df2a
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d0f3a0ff2938264498234e4aaa66cf5f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7644cfe4cdc2d0f4ebc7ab351323a576
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,38 @@
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace Tests
{
public class SampleEditModeTest
{
[Test]
public void TestIncrement()
{
// Given
var counter = new BasicCounter(0);
// When
counter.Increment();
// Then
Assert.AreEqual(1, counter.Count);
}
[Test]
public void TestMaxCount()
{
// Given
var counter = new BasicCounter(BasicCounter.MaxCount);
// When
counter.Increment();
// Then
Assert.AreEqual(BasicCounter.MaxCount, counter.Count);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88de94cc1489d83488ce54f71b512989
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,25 @@
{
"name": "example.testpackage.EditorTests",
"rootNamespace": "",
"references": [
"UnityEngine.TestRunner",
"UnityEditor.TestRunner",
"example.testpackage.Editor",
"example.testpackage.Runtime"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b5712d2009ce3b34a8ca077667b16764
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b4f774583b1374a4abe450c7100726bd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,31 @@
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace Tests
{
public class SampleComponentTest
{
private GameObject target;
private SampleComponent component;
[SetUp]
public void Setup()
{
target = GameObject.Instantiate(new GameObject());
component = target.AddComponent<SampleComponent>();
}
[UnityTest]
public IEnumerator TestIncrementOnUpdateAfterNextFrame()
{
// Save the current value, since it was updated after component Start() method called
var count = component.Counter.Count;
// Skip frame and assert the new value
yield return null;
Assert.AreEqual(count + 1, component.Counter.Count);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b551b84934711564eb78aab8c16425ac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,42 @@
using System.Collections;
using NUnit.Framework;
using UnityEngine.TestTools;
namespace Tests
{
public class SamplePlayModeTest
{
// A Test behaves as an ordinary method
[Test]
public void NewTestScriptSimplePasses()
{
// Given
var counter = new BasicCounter(0);
// When
counter.Increment();
// Then
Assert.AreEqual(1, counter.Count);
}
// A UnityTest behaves like a coroutine in Play Mode. In Edit Mode you can use
// `yield return null;` to skip a frame.
[UnityTest]
public IEnumerator NewTestScriptWithEnumeratorPasses()
{
// Given
var counter = new BasicCounter(3);
// Use the Assert class to test conditions.
// Use yield to skip a frame.
yield return null;
// When
counter.Increment();
// Then
Assert.AreEqual(4, counter.Count);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4dbe2d2dc79550c4d81602bcf94a9824
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,66 @@
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace Tests
{
public class TimerComponentTest
{
private GameObject target;
private TimerComponent component;
[SetUp]
public void Setup()
{
target = GameObject.Instantiate(new GameObject());
component = target.AddComponent<TimerComponent>();
}
[UnityTest]
public IEnumerator TestIncrementAfterSomeTime()
{
// Save the current value, since it was updated after component Start() method called
var count = component.Counter.Count;
// Skip frame and assert the new value
yield return null;
Assert.AreEqual(count, component.Counter.Count);
yield return new WaitForSeconds(1.1f);
Assert.AreEqual(count + 1, component.Counter.Count);
yield return new WaitForSeconds(1.1f);
Assert.AreEqual(count + 2, component.Counter.Count);
}
[UnityTest]
public IEnumerator TestTimeScaleIsAffectingIncrement()
{
// Save the current value, since it was updated after component Start() method called
var count = component.Counter.Count;
Time.timeScale = .5f;
// Skip frame and assert the new value
yield return null;
Assert.AreEqual(count, component.Counter.Count);
yield return WaitForRealSeconds(1.1f);
Assert.AreEqual(count, component.Counter.Count);
yield return WaitForRealSeconds(1.1f);
Assert.AreEqual(count + 1, component.Counter.Count);
}
// Skipping time ignoring Time.scale
// https://answers.unity.com/questions/301868/yield-waitforseconds-outside-of-timescale.html
public static IEnumerator WaitForRealSeconds(float time)
{
float start = Time.realtimeSinceStartup;
while (Time.realtimeSinceStartup < start + time)
{
yield return null;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 010121a56a70d60428dc89307eb77b54
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,22 @@
{
"name": "example.testpackage.RuntimeTests",
"rootNamespace": "",
"references": [
"UnityEngine.TestRunner",
"UnityEditor.TestRunner",
"example.testpackage.Runtime"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 902aaaf7a59149243b2f4e38fc9f388e
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
{
"name": "com.example.testpackage",
"version": "0.0.1",
"displayName": "Test Package",
"description": "Test Package",
"unity": "2019.2",
"unityRelease": "11f1",
"keywords": [
"nothing"
],
"author": {
"name": "Example Author",
"email": "author@example.com",
"url": "example.com"
},
"type": "tool",
"hideInEditor": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4232dbd3889ab6a4393e846291288fb0
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: