Include e2e, api and performance tests in ci.yml (#45190)

Co-authored-by: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Co-authored-by: Ron Rennick <ron@ronandandrea.com>
This commit is contained in:
Adrian Moldovan 2024-03-27 17:21:49 +02:00 committed by GitHub
parent bb10ee5e57
commit 7d6d2c94dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 858 additions and 57 deletions

View File

@ -8,6 +8,7 @@ on:
concurrency: concurrency:
group: '${{ github.workflow }}-${{ github.ref }}' group: '${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
project-jobs: project-jobs:
# Since this is a monorepo, not every pull request or change is going to impact every project. # Since this is a monorepo, not every pull request or change is going to impact every project.
@ -18,9 +19,12 @@ jobs:
runs-on: 'ubuntu-20.04' runs-on: 'ubuntu-20.04'
outputs: outputs:
lint-jobs: ${{ steps.project-jobs.outputs.lint-jobs }} lint-jobs: ${{ steps.project-jobs.outputs.lint-jobs }}
test-jobs: ${{ steps.project-jobs.outputs.test-jobs }} default-test-jobs: ${{ steps.project-jobs.outputs.default-test-jobs }}
e2e-test-jobs: ${{ steps.project-jobs.outputs.e2e-test-jobs }}
api-test-jobs: ${{ steps.project-jobs.outputs.api-test-jobs }}
performance-test-jobs: ${{ steps.project-jobs.outputs.performance-test-jobs }}
steps: steps:
- uses: 'actions/checkout@v3' - uses: 'actions/checkout@v4'
name: 'Checkout' name: 'Checkout'
with: with:
fetch-depth: 0 fetch-depth: 0
@ -39,6 +43,7 @@ jobs:
} }
const child_process = require( 'node:child_process' ); const child_process = require( 'node:child_process' );
child_process.execSync( `pnpm utils ci-jobs ${ baseRef }` ); child_process.execSync( `pnpm utils ci-jobs ${ baseRef }` );
project-lint-jobs: project-lint-jobs:
name: 'Lint - ${{ matrix.projectName }}' name: 'Lint - ${{ matrix.projectName }}'
runs-on: 'ubuntu-20.04' runs-on: 'ubuntu-20.04'
@ -49,7 +54,7 @@ jobs:
matrix: matrix:
include: ${{ fromJSON( needs.project-jobs.outputs.lint-jobs ) }} include: ${{ fromJSON( needs.project-jobs.outputs.lint-jobs ) }}
steps: steps:
- uses: 'actions/checkout@v3' - uses: 'actions/checkout@v4'
name: 'Checkout' name: 'Checkout'
with: with:
fetch-depth: 0 fetch-depth: 0
@ -61,20 +66,19 @@ jobs:
build: '${{ matrix.projectName }}' build: '${{ matrix.projectName }}'
- name: 'Lint' - name: 'Lint'
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }}' run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }}'
project-test-jobs:
project-default-test-jobs:
name: 'Test - ${{ matrix.projectName }} - ${{ matrix.name }}' name: 'Test - ${{ matrix.projectName }} - ${{ matrix.name }}'
runs-on: 'ubuntu-20.04' runs-on: 'ubuntu-20.04'
needs: 'project-jobs' needs: 'project-jobs'
if: ${{ needs.project-jobs.outputs.test-jobs != '[]' }} if: ${{ needs.project-jobs.outputs.default-test-jobs != '[]' }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: ${{ fromJSON( needs.project-jobs.outputs.test-jobs ) }} include: ${{ fromJSON( needs.project-jobs.outputs.default-test-jobs ) }}
steps: steps:
- uses: 'actions/checkout@v3' - uses: 'actions/checkout@v4'
name: 'Checkout' name: 'Checkout'
with:
fetch-depth: 0
- uses: './.github/actions/setup-woocommerce-monorepo' - uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Setup Monorepo' name: 'Setup Monorepo'
id: 'setup-monorepo' id: 'setup-monorepo'
@ -88,10 +92,113 @@ jobs:
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.testEnv.start }}' run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.testEnv.start }}'
- name: 'Test' - name: 'Test'
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }}' run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }}'
project-e2e-test-jobs:
name: 'E2E - ${{ matrix.name }}'
runs-on: 'ubuntu-20.04'
needs: 'project-jobs'
if: ${{ needs.project-jobs.outputs.e2e-test-jobs != '[]' }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON( needs.project-jobs.outputs.e2e-test-jobs ) }}
steps:
- uses: 'actions/checkout@v4'
name: 'Checkout'
- uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Setup Monorepo'
id: 'setup-monorepo'
with:
install: '${{ matrix.projectName }}...'
build: '${{ matrix.projectName }}'
- name: 'Prepare Test Environment'
id: 'prepare-test-environment'
if: ${{ matrix.testEnv.shouldCreate }}
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.testEnv.start }}'
- name: 'Run tests'
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }}'
- name: 'Upload artifacts'
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: all-blob-reports-${{ matrix.shardNumber }}
path: ${{ matrix.projectPath }}/tests/e2e-pw/test-results/allure-results
retention-days: 1
compression-level: 9
project-api-test-jobs:
name: 'API - ${{ matrix.name }}'
runs-on: 'ubuntu-20.04'
needs: 'project-jobs'
if: ${{ needs.project-jobs.outputs.api-test-jobs != '[]' }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON( needs.project-jobs.outputs.api-test-jobs ) }}
steps:
- uses: 'actions/checkout@v4'
name: 'Checkout'
- uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Setup Monorepo'
id: 'setup-monorepo'
with:
install: '${{ matrix.projectName }}...'
build: '${{ matrix.projectName }}'
- name: 'Prepare Test Environment'
id: 'prepare-test-environment'
if: ${{ matrix.testEnv.shouldCreate }}
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.testEnv.start }}'
- name: 'Run tests'
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }}'
- name: 'Upload artifacts'
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: all-blob-reports-${{ matrix.shardNumber }}
path: ${{ matrix.projectPath }}/tests/api-core-tests/test-results/allure-results
retention-days: 1
compression-level: 9
project-performance-test-jobs:
name: 'Performance - ${{ matrix.name }}'
runs-on: 'ubuntu-20.04'
needs: 'project-jobs'
if: ${{ needs.project-jobs.outputs.performance-test-jobs != '[]' }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON( needs.project-jobs.outputs.performance-test-jobs ) }}
steps:
- uses: 'actions/checkout@v4'
name: 'Checkout'
- uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Setup Monorepo'
id: 'setup-monorepo'
with:
install: '${{ matrix.projectName }}...'
build: '${{ matrix.projectName }}'
- name: 'Prepare Test Environment'
id: 'prepare-test-environment'
if: ${{ matrix.testEnv.shouldCreate }}
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.testEnv.start }}'
- name: 'Run tests'
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }}'
evaluate-project-jobs: evaluate-project-jobs:
# In order to add a required status check we need a consistent job that we can grab onto. # In order to add a required status check we need a consistent job that we can grab onto.
# Since we are dynamically generating a matrix for the project jobs, however, we can't # Since we are dynamically generating a matrix for the project jobs, however, we can't
# rely on on any specific job being present. We can get around this limitation by # rely on any specific job being present. We can get around this limitation by
# using a job that runs after all the others and either passes or fails based # using a job that runs after all the others and either passes or fails based
# on the results of the other jobs in the workflow. # on the results of the other jobs in the workflow.
name: 'Evaluate Project Job Statuses' name: 'Evaluate Project Job Statuses'
@ -99,7 +206,9 @@ jobs:
needs: [ needs: [
'project-jobs', 'project-jobs',
'project-lint-jobs', 'project-lint-jobs',
'project-test-jobs' 'project-default-test-jobs',
'project-e2e-test-jobs',
'project-api-test-jobs'
] ]
if: ${{ always() }} if: ${{ always() }}
steps: steps:
@ -115,9 +224,150 @@ jobs:
echo "One or more lint jobs have failed." echo "One or more lint jobs have failed."
exit 1 exit 1
fi fi
result="${{ needs.project-test-jobs.result }}" result="${{ needs.project-default-test-jobs.result }}"
if [[ $result != "success" && $result != "skipped" ]]; then if [[ $result != "success" && $result != "skipped" ]]; then
echo "One or more test jobs have failed." echo "One or more test jobs have failed."
exit 1 exit 1
fi fi
result="${{ needs.project-e2e-test-jobs.result }}"
if [[ $result != "success" && $result != "skipped" ]]; then
echo "One or more e2e test jobs have failed."
exit 1
fi
result="${{ needs.project-api-test-jobs.result }}"
if [[ $result != "success" && $result != "skipped" ]]; then
echo "One or more api test jobs have failed."
exit 1
fi
echo "All jobs have completed successfully." echo "All jobs have completed successfully."
publish-e2e-test-reports:
name: 'Publish e2e test reports'
needs: [ project-e2e-test-jobs ]
if: ${{ contains( needs.*.result, 'success' ) || contains( needs.*.result, 'failure' ) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 'Install Allure CLI'
env:
DESTINATION_PATH: ../
run: ./.github/workflows/scripts/install-allure.sh
- name: 'Download blob reports from artifacts'
uses: actions/download-artifact@v4
with:
path: ./out/allure-results
pattern: all-blob-reports-*
run-id: project-e2e-test-jobs
merge-multiple: true
- name: 'Generate Allure report'
id: generate_allure_report
run: allure generate --clean ./out/allure-results --output ./out/allure-report
- name: 'Archive reports'
uses: actions/upload-artifact@v4
with:
name: e2e-test-report
path: ./out
if-no-files-found: ignore
retention-days: 5
- name: 'Publish reports'
env:
GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
RUN_ID: ${{ github.run_id }}
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [ "$GITHUB_EVENT_NAME" == pull_request ]; then
gh workflow run publish-test-reports-pr.yml \
-f run_id=$RUN_ID \
-f e2e_artifact=e2e-test-report \
-f pr_number=$PR_NUMBER \
-f commit_sha=$COMMIT_SHA \
-f s3_root=public \
--repo woocommerce/woocommerce-test-reports
else
gh workflow run publish-test-reports-trunk-merge.yml \
-f run_id=$RUN_ID \
-f artifact=e2e-test-report \
-f pr_number='' \
-f test_type="e2e" \
--repo woocommerce/woocommerce-test-reports
fi
slack-alert-e2e-test:
name: 'Send Slack alert for e2e tests'
needs: [ project-e2e-test-jobs ]
if: ${{ github.event_name != 'pull_request' && ( contains( needs.*.result, 'success' ) || contains( needs.*.result, 'failure' ) ) }}
runs-on: ubuntu-latest
steps:
- name: 'Send Slack notification'
uses: automattic/action-test-results-to-slack@v0.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
slack_token: ${{ secrets.E2E_SLACK_TOKEN }}
slack_channel: ${{ secrets.E2E_TRUNK_SLACK_CHANNEL }}
publish-api-test-reports:
name: 'Publish API test reports'
needs: [ project-api-test-jobs ]
if: ${{ contains( needs.*.result, 'success' ) || contains( needs.*.result, 'failure' ) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 'Install Allure CLI'
env:
DESTINATION_PATH: ../
run: ./.github/workflows/scripts/install-allure.sh
- name: 'Download blob reports from artifacts'
uses: actions/download-artifact@v4
with:
path: ./out/allure-results
pattern: all-blob-reports-*
run-id: project-api-test-jobs
merge-multiple: true
- name: 'Generate Allure report'
id: generate_allure_report
run: allure generate --clean ./out/allure-results --output ./out/allure-report
- name: 'Archive reports'
uses: actions/upload-artifact@v4
with:
name: api-test-report
path: ./out
if-no-files-found: ignore
retention-days: 5
- name: 'Publish reports'
env:
GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
RUN_ID: ${{ github.run_id }}
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [ "$GITHUB_EVENT_NAME" == pull_request ]; then
gh workflow run publish-test-reports-pr.yml \
-f run_id=$RUN_ID \
-f api_artifact=api-test-report \
-f pr_number=$PR_NUMBER \
-f commit_sha=$COMMIT_SHA \
-f s3_root=public \
--repo woocommerce/woocommerce-test-reports
else
gh workflow run publish-test-reports-trunk-merge.yml \
-f run_id=$RUN_ID \
-f artifact=api-test-report \
-f pr_number='' \
-f test_type="api" \
--repo woocommerce/woocommerce-test-reports
fi

View File

@ -1,9 +1,9 @@
name: Run tests against PR name: Run tests against PR
on: on:
workflow_dispatch: workflow_dispatch:
pull_request: #pull_request:
paths-ignore: #paths-ignore:
- '**/changelog/**' #- '**/changelog/**'
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}

29
.github/workflows/scripts/install-allure.sh vendored Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
set -eo pipefail
# Test Java installation
java -version
if [[ -z "$DESTINATION_PATH" ]]; then
echo "::error::DESTINATION_PATH must be set"
exit 1
fi
ALLURE_VERSION=2.27.0
ALLURE_DOWNLOAD_URL=https://github.com/allure-framework/allure2/releases/download/$ALLURE_VERSION/allure-$ALLURE_VERSION.zip
echo "Installing Allure $ALLURE_VERSION in $DESTINATION_PATH"
wget --no-verbose -O allure.zip $ALLURE_DOWNLOAD_URL \
&& unzip allure.zip -d "$DESTINATION_PATH" \
&& rm -rf allure.zip \
ALLURE_PATH=$(realpath "$DESTINATION_PATH"/allure-$ALLURE_VERSION/bin)
# Test Allure installation
echo "$ALLURE_PATH"
export PATH="$ALLURE_PATH:$PATH"
allure --version
# Add Allure in Github PATH to make it available to all subsequent actions in the current job
echo "$ALLURE_PATH" >> "$GITHUB_PATH"

View File

@ -1,8 +1,9 @@
name: Run tests against trunk after PR merge name: Run tests against trunk after PR merge
on: on:
pull_request: workflow_dispatch:
types: #pull_request:
- closed #types:
#- closed
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }} group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true cancel-in-progress: true

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
CI: adds e2e tests into ci-jobs and ci.yml

View File

@ -28,8 +28,10 @@
"env:restart": "pnpm wp-env destroy && pnpm wp-env start --update", "env:restart": "pnpm wp-env destroy && pnpm wp-env start --update",
"env:start": "pnpm wp-env start", "env:start": "pnpm wp-env start",
"env:stop": "pnpm wp-env stop", "env:stop": "pnpm wp-env stop",
"env:test": "WP_ENV_LIFECYCLE_SCRIPT_AFTER_START='./tests/e2e-pw/bin/test-env-setup.sh' && pnpm env:dev", "env:test": "WP_ENV_LIFECYCLE_SCRIPT_AFTER_START='./tests/e2e-pw/bin/test-env-setup.sh' && pnpm env:dev && pnpm playwright install chromium",
"env:test:cot": "WP_ENV_LIFECYCLE_SCRIPT_AFTER_START='ENABLE_HPOS=1 ./tests/e2e-pw/bin/test-env-setup.sh' && ENABLE_HPOS=1 pnpm env:dev", "env:test:cot": "WP_ENV_LIFECYCLE_SCRIPT_AFTER_START='ENABLE_HPOS=1 ./tests/e2e-pw/bin/test-env-setup.sh' && ENABLE_HPOS=1 pnpm env:dev",
"env:perf:install-k6": "curl https://github.com/grafana/k6/releases/download/v0.33.0/k6-v0.33.0-linux-amd64.tar.gz -L | tar xvz --strip-components 1",
"env:perf": "pnpm env:dev && pnpm env:performance-init && pnpm env:perf:install-k6",
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"postinstall": "composer install", "postinstall": "composer install",
"lint": "pnpm --if-present '/^lint:lang:.*$/'", "lint": "pnpm --if-present '/^lint:lang:.*$/'",
@ -48,6 +50,7 @@
"test:api": "API_TEST_REPORT_DIR=\"$PWD/tests/api\" pnpm exec wc-api-tests test api", "test:api": "API_TEST_REPORT_DIR=\"$PWD/tests/api\" pnpm exec wc-api-tests test api",
"test:api-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js", "test:api-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js",
"test:e2e-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js", "test:e2e-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js",
"test:perf": "./k6 run ./tests/performance/tests/gh-action-pr-requests.js",
"test:env:start": "pnpm env:test", "test:env:start": "pnpm env:test",
"test:php": "./vendor/bin/phpunit -c ./phpunit.xml", "test:php": "./vendor/bin/phpunit -c ./phpunit.xml",
"test:php:watch": "./vendor/bin/phpunit-watcher watch", "test:php:watch": "./vendor/bin/phpunit-watcher watch",
@ -165,6 +168,40 @@
"wpVersion": "latest-2" "wpVersion": "latest-2"
} }
} }
},
{
"name": "Core e2e tests",
"testType": "e2e",
"command": "test:e2e-pw",
"shardingArguments": [
"--shard=1/5",
"--shard=2/5",
"--shard=3/5",
"--shard=4/5",
"--shard=5/5"
],
"changes": [],
"testEnv": {
"start": "env:test"
}
},
{
"name": "Core API tests",
"testType": "api",
"command": "test:api-pw",
"changes": [],
"testEnv": {
"start": "env:test"
}
},
{
"name": "Core Performance tests (K6)",
"testType": "performance",
"command": "test:perf",
"changes": [],
"testEnv": {
"start": "env:perf"
}
} }
] ]
} }

File diff suppressed because one or more lines are too long

View File

@ -12,6 +12,7 @@ import { buildProjectGraph } from './lib/project-graph';
import { getFileChanges } from './lib/file-changes'; import { getFileChanges } from './lib/file-changes';
import { createJobsForChanges } from './lib/job-processing'; import { createJobsForChanges } from './lib/job-processing';
import { isGithubCI } from '../core/environment'; import { isGithubCI } from '../core/environment';
import { testTypes } from './lib/config';
const program = new Command( 'ci-jobs' ) const program = new Command( 'ci-jobs' )
.description( .description(
@ -49,7 +50,13 @@ const program = new Command( 'ci-jobs' )
if ( isGithubCI() ) { if ( isGithubCI() ) {
setOutput( 'lint-jobs', JSON.stringify( jobs.lint ) ); setOutput( 'lint-jobs', JSON.stringify( jobs.lint ) );
setOutput( 'test-jobs', JSON.stringify( jobs.test ) );
testTypes.forEach( ( type ) => {
setOutput(
`${ type }-test-jobs`,
JSON.stringify( jobs[ `${ type }Test` ] )
);
} );
return; return;
} }
@ -62,14 +69,16 @@ const program = new Command( 'ci-jobs' )
Logger.notice( 'No lint jobs to run.' ); Logger.notice( 'No lint jobs to run.' );
} }
if ( jobs.test.length > 0 ) { testTypes.forEach( ( type ) => {
Logger.notice( 'Test Jobs' ); if ( jobs[ `${ type }Test` ].length > 0 ) {
for ( const job of jobs.test ) { Logger.notice( `${ type } test Jobs` );
for ( const job of jobs[ `${ type }Test` ] ) {
Logger.notice( `- ${ job.projectName } - ${ job.name }` ); Logger.notice( `- ${ job.projectName } - ${ job.name }` );
} }
} else { } else {
Logger.notice( 'No test jobs to run.' ); Logger.notice( `No ${ type } test jobs to run.` );
} }
} ); } );
} );
export default program; export default program;

View File

@ -6,7 +6,7 @@ import { makeRe } from 'minimatch';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { JobType, parseCIConfig } from '../config'; import { JobType, parseCIConfig, testTypes } from '../config';
describe( 'Config', () => { describe( 'Config', () => {
describe( 'parseCIConfig', () => { describe( 'parseCIConfig', () => {
@ -136,6 +136,8 @@ describe( 'Config', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType: 'default',
shardingArguments: [],
name: 'default', name: 'default',
changes: [ changes: [
/^package\.json$/, /^package\.json$/,
@ -222,5 +224,129 @@ describe( 'Config', () => {
], ],
} ); } );
} ); } );
it.each( testTypes )(
'should parse test config with expected testType',
( testType ) => {
const parsed = parseCIConfig( {
name: 'foo',
config: {
ci: {
tests: [
{
name: 'default',
testType,
changes: '/src/**/*.{js,jsx,ts,tsx}',
command: 'foo',
},
],
},
},
} );
expect( parsed ).toMatchObject( {
jobs: [
{
type: JobType.Test,
testType,
shardingArguments: [],
name: 'default',
changes: [
/^package\.json$/,
makeRe( '/src/**/*.{js,jsx,ts,tsx}' ),
],
command: 'foo',
},
],
} );
}
);
it.each( [
[ '', 'default' ],
[ 'bad', 'default' ],
[ 1, 'default' ],
[ undefined, 'default' ],
] )(
'should parse test config with unexpected testType',
( input, result ) => {
const parsed = parseCIConfig( {
name: 'foo',
config: {
ci: {
tests: [
{
name: 'default',
testType: input,
changes: '/src/**/*.{js,jsx,ts,tsx}',
command: 'foo',
},
],
},
},
} );
expect( parsed ).toMatchObject( {
jobs: [
{
type: JobType.Test,
testType: result,
shardingArguments: [],
name: 'default',
changes: [
/^package\.json$/,
makeRe( '/src/**/*.{js,jsx,ts,tsx}' ),
],
command: 'foo',
},
],
} );
}
);
it.each( [
[ [], [] ],
[ undefined, [] ],
[
[ 'a', 'b' ],
[ 'a', 'b' ],
],
] )(
'should parse test config with shards',
( shardingArguments: any, result: string[] ) => {
const parsed = parseCIConfig( {
name: 'foo',
config: {
ci: {
tests: [
{
name: 'default',
testType: 'e2e',
shardingArguments,
changes: '/src/**/*.{js,jsx,ts,tsx}',
command: 'foo',
},
],
},
},
} );
expect( parsed ).toMatchObject( {
jobs: [
{
type: JobType.Test,
testType: 'e2e',
shardingArguments: result,
name: 'default',
changes: [
/^package\.json$/,
makeRe( '/src/**/*.{js,jsx,ts,tsx}' ),
],
command: 'foo',
},
],
} );
}
);
} ); } );
} ); } );

View File

@ -1,8 +1,8 @@
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { JobType } from '../config'; import { JobType, testTypes } from '../config';
import { createJobsForChanges } from '../job-processing'; import { createJobsForChanges, getShardedJobs } from '../job-processing';
import { parseTestEnvConfig } from '../test-environment'; import { parseTestEnvConfig } from '../test-environment';
jest.mock( '../test-environment' ); jest.mock( '../test-environment' );
@ -49,6 +49,7 @@ describe( 'Job Processing', () => {
expect( jobs.lint ).toHaveLength( 1 ); expect( jobs.lint ).toHaveLength( 1 );
expect( jobs.lint ).toContainEqual( { expect( jobs.lint ).toContainEqual( {
projectName: 'test', projectName: 'test',
projectPath: 'test',
command: 'test-lint', command: 'test-lint',
} ); } );
expect( jobs.test ).toHaveLength( 0 ); expect( jobs.test ).toHaveLength( 0 );
@ -83,6 +84,7 @@ describe( 'Job Processing', () => {
expect( jobs.lint ).toHaveLength( 1 ); expect( jobs.lint ).toHaveLength( 1 );
expect( jobs.lint ).toContainEqual( { expect( jobs.lint ).toContainEqual( {
projectName: 'test', projectName: 'test',
projectPath: 'test',
command: 'test-lint test-base-ref', command: 'test-lint test-base-ref',
} ); } );
expect( jobs.test ).toHaveLength( 0 ); expect( jobs.test ).toHaveLength( 0 );
@ -220,10 +222,12 @@ describe( 'Job Processing', () => {
expect( jobs.lint ).toHaveLength( 2 ); expect( jobs.lint ).toHaveLength( 2 );
expect( jobs.lint ).toContainEqual( { expect( jobs.lint ).toContainEqual( {
projectName: 'test', projectName: 'test',
projectPath: 'test',
command: 'test-lint', command: 'test-lint',
} ); } );
expect( jobs.lint ).toContainEqual( { expect( jobs.lint ).toContainEqual( {
projectName: 'test-b', projectName: 'test-b',
projectPath: 'test-b',
command: 'test-lint-b', command: 'test-lint-b',
} ); } );
expect( jobs.test ).toHaveLength( 0 ); expect( jobs.test ).toHaveLength( 0 );
@ -276,16 +280,20 @@ describe( 'Job Processing', () => {
expect( jobs.lint ).toHaveLength( 2 ); expect( jobs.lint ).toHaveLength( 2 );
expect( jobs.lint ).toContainEqual( { expect( jobs.lint ).toContainEqual( {
projectName: 'test-a', projectName: 'test-a',
projectPath: 'test-a',
command: 'test-lint-a', command: 'test-lint-a',
} ); } );
expect( jobs.lint ).toContainEqual( { expect( jobs.lint ).toContainEqual( {
projectName: 'test-b', projectName: 'test-b',
projectPath: 'test-b',
command: 'test-lint-b', command: 'test-lint-b',
} ); } );
expect( jobs.test ).toHaveLength( 0 ); expect( jobs.test ).toHaveLength( 0 );
} ); } );
it( 'should trigger test job for single node', async () => { it( 'should trigger test job for single node', async () => {
const testType = 'default';
const jobs = await createJobsForChanges( const jobs = await createJobsForChanges(
{ {
name: 'test', name: 'test',
@ -294,6 +302,8 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType,
shardingArguments: [],
name: 'Default', name: 'Default',
changes: [ /test.js$/ ], changes: [ /test.js$/ ],
command: 'test-cmd', command: 'test-cmd',
@ -309,11 +319,13 @@ describe( 'Job Processing', () => {
); );
expect( jobs.lint ).toHaveLength( 0 ); expect( jobs.lint ).toHaveLength( 0 );
expect( jobs.test ).toHaveLength( 1 ); expect( jobs[ `${ testType }Test` ] ).toHaveLength( 1 );
expect( jobs.test ).toContainEqual( { expect( jobs[ `${ testType }Test` ] ).toContainEqual( {
projectName: 'test', projectName: 'test',
projectPath: 'test',
name: 'Default', name: 'Default',
command: 'test-cmd', command: 'test-cmd',
shardNumber: 0,
testEnv: { testEnv: {
shouldCreate: false, shouldCreate: false,
envVars: {}, envVars: {},
@ -322,6 +334,7 @@ describe( 'Job Processing', () => {
} ); } );
it( 'should replace vars in test command', async () => { it( 'should replace vars in test command', async () => {
const testType = 'default';
const jobs = await createJobsForChanges( const jobs = await createJobsForChanges(
{ {
name: 'test', name: 'test',
@ -330,7 +343,9 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType,
name: 'Default', name: 'Default',
shardingArguments: [],
changes: [ /test.js$/ ], changes: [ /test.js$/ ],
command: 'test-cmd <baseRef>', command: 'test-cmd <baseRef>',
}, },
@ -349,11 +364,13 @@ describe( 'Job Processing', () => {
); );
expect( jobs.lint ).toHaveLength( 0 ); expect( jobs.lint ).toHaveLength( 0 );
expect( jobs.test ).toHaveLength( 1 ); expect( jobs[ `${ testType }Test` ] ).toHaveLength( 1 );
expect( jobs.test ).toContainEqual( { expect( jobs[ `${ testType }Test` ] ).toContainEqual( {
projectName: 'test', projectName: 'test',
projectPath: 'test',
name: 'Default', name: 'Default',
command: 'test-cmd test-base-ref', command: 'test-cmd test-base-ref',
shardNumber: 0,
testEnv: { testEnv: {
shouldCreate: false, shouldCreate: false,
envVars: {}, envVars: {},
@ -362,6 +379,7 @@ describe( 'Job Processing', () => {
} ); } );
it( 'should not trigger a test job that has already been created', async () => { it( 'should not trigger a test job that has already been created', async () => {
const testType = 'default';
const jobs = await createJobsForChanges( const jobs = await createJobsForChanges(
{ {
name: 'test', name: 'test',
@ -370,7 +388,9 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType,
name: 'Default', name: 'Default',
shardingArguments: [],
changes: [ /test.js$/ ], changes: [ /test.js$/ ],
command: 'test-cmd', command: 'test-cmd',
jobCreated: true, jobCreated: true,
@ -386,10 +406,11 @@ describe( 'Job Processing', () => {
); );
expect( jobs.lint ).toHaveLength( 0 ); expect( jobs.lint ).toHaveLength( 0 );
expect( jobs.test ).toHaveLength( 0 ); expect( jobs[ `${ testType }Test` ] ).toHaveLength( 0 );
} ); } );
it( 'should not trigger test job for single node with no changes', async () => { it( 'should not trigger test job for single node with no changes', async () => {
const testType = 'default';
const jobs = await createJobsForChanges( const jobs = await createJobsForChanges(
{ {
name: 'test', name: 'test',
@ -398,7 +419,9 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType,
name: 'Default', name: 'Default',
shardingArguments: [],
changes: [ /test.js$/ ], changes: [ /test.js$/ ],
command: 'test-cmd', command: 'test-cmd',
}, },
@ -411,10 +434,11 @@ describe( 'Job Processing', () => {
); );
expect( jobs.lint ).toHaveLength( 0 ); expect( jobs.lint ).toHaveLength( 0 );
expect( jobs.test ).toHaveLength( 0 ); expect( jobs[ `${ testType }Test` ] ).toHaveLength( 0 );
} ); } );
it( 'should trigger test job for project graph', async () => { it( 'should trigger test job for project graph', async () => {
const testType = 'default';
const jobs = await createJobsForChanges( const jobs = await createJobsForChanges(
{ {
name: 'test', name: 'test',
@ -423,7 +447,9 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType,
name: 'Default', name: 'Default',
shardingArguments: [],
changes: [ /test.js$/ ], changes: [ /test.js$/ ],
command: 'test-cmd', command: 'test-cmd',
}, },
@ -437,7 +463,9 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType: 'default',
name: 'Default A', name: 'Default A',
shardingArguments: [],
changes: [ /test-b.js$/ ], changes: [ /test-b.js$/ ],
command: 'test-cmd-a', command: 'test-cmd-a',
}, },
@ -452,7 +480,9 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType: 'default',
name: 'Default B', name: 'Default B',
shardingArguments: [],
changes: [ /test-b.js$/ ], changes: [ /test-b.js$/ ],
command: 'test-cmd-b', command: 'test-cmd-b',
}, },
@ -471,20 +501,24 @@ describe( 'Job Processing', () => {
); );
expect( jobs.lint ).toHaveLength( 0 ); expect( jobs.lint ).toHaveLength( 0 );
expect( jobs.test ).toHaveLength( 2 ); expect( jobs[ `${ testType }Test` ] ).toHaveLength( 2 );
expect( jobs.test ).toContainEqual( { expect( jobs[ `${ testType }Test` ] ).toContainEqual( {
projectName: 'test', projectName: 'test',
projectPath: 'test',
name: 'Default', name: 'Default',
command: 'test-cmd', command: 'test-cmd',
shardNumber: 0,
testEnv: { testEnv: {
shouldCreate: false, shouldCreate: false,
envVars: {}, envVars: {},
}, },
} ); } );
expect( jobs.test ).toContainEqual( { expect( jobs[ `${ testType }Test` ] ).toContainEqual( {
projectName: 'test-b', projectName: 'test-b',
projectPath: 'test-b',
name: 'Default B', name: 'Default B',
command: 'test-cmd-b', command: 'test-cmd-b',
shardNumber: 0,
testEnv: { testEnv: {
shouldCreate: false, shouldCreate: false,
envVars: {}, envVars: {},
@ -492,7 +526,9 @@ describe( 'Job Processing', () => {
} ); } );
} ); } );
it( 'should trigger test job for dependent without changes when dependency has matching cascade key', async () => { it.each( testTypes )(
'should trigger %s test job for single node',
async ( testType ) => {
const jobs = await createJobsForChanges( const jobs = await createJobsForChanges(
{ {
name: 'test', name: 'test',
@ -501,7 +537,51 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType,
name: 'Default', name: 'Default',
shardingArguments: [],
changes: [ /test.js$/ ],
command: 'test-cmd',
},
],
},
dependencies: [],
},
{
test: [ 'test.js' ],
},
{}
);
expect( jobs.lint ).toHaveLength( 0 );
expect( jobs[ `${ testType }Test` ] ).toHaveLength( 1 );
expect( jobs[ `${ testType }Test` ] ).toContainEqual( {
projectName: 'test',
projectPath: 'test',
name: 'Default',
command: 'test-cmd',
shardNumber: 0,
testEnv: {
shouldCreate: false,
envVars: {},
},
} );
}
);
it( 'should trigger test job for dependent without changes when dependency has matching cascade key', async () => {
const testType = 'default';
const jobs = await createJobsForChanges(
{
name: 'test',
path: 'test',
ciConfig: {
jobs: [
{
type: JobType.Test,
testType,
name: 'Default',
shardingArguments: [],
changes: [ /test.js$/ ], changes: [ /test.js$/ ],
command: 'test-cmd', command: 'test-cmd',
cascadeKeys: [ 'test' ], cascadeKeys: [ 'test' ],
@ -516,7 +596,9 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType: 'default',
name: 'Default A', name: 'Default A',
shardingArguments: [],
changes: [ /test-a.js$/ ], changes: [ /test-a.js$/ ],
command: 'test-cmd-a', command: 'test-cmd-a',
cascadeKeys: [ 'test-a', 'test' ], cascadeKeys: [ 'test-a', 'test' ],
@ -534,20 +616,24 @@ describe( 'Job Processing', () => {
); );
expect( jobs.lint ).toHaveLength( 0 ); expect( jobs.lint ).toHaveLength( 0 );
expect( jobs.test ).toHaveLength( 2 ); expect( jobs[ `${ testType }Test` ] ).toHaveLength( 2 );
expect( jobs.test ).toContainEqual( { expect( jobs[ `${ testType }Test` ] ).toContainEqual( {
projectName: 'test', projectName: 'test',
projectPath: 'test',
name: 'Default', name: 'Default',
command: 'test-cmd', command: 'test-cmd',
shardNumber: 0,
testEnv: { testEnv: {
shouldCreate: false, shouldCreate: false,
envVars: {}, envVars: {},
}, },
} ); } );
expect( jobs.test ).toContainEqual( { expect( jobs[ `${ testType }Test` ] ).toContainEqual( {
projectName: 'test-a', projectName: 'test-a',
projectPath: 'test-a',
name: 'Default A', name: 'Default A',
command: 'test-cmd-a', command: 'test-cmd-a',
shardNumber: 0,
testEnv: { testEnv: {
shouldCreate: false, shouldCreate: false,
envVars: {}, envVars: {},
@ -556,6 +642,7 @@ describe( 'Job Processing', () => {
} ); } );
it( 'should isolate dependency cascade keys to prevent cross-dependency matching', async () => { it( 'should isolate dependency cascade keys to prevent cross-dependency matching', async () => {
const testType = 'default';
const jobs = await createJobsForChanges( const jobs = await createJobsForChanges(
{ {
name: 'test', name: 'test',
@ -564,7 +651,9 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType,
name: 'Default', name: 'Default',
shardingArguments: [],
changes: [ /test.js$/ ], changes: [ /test.js$/ ],
command: 'test-cmd', command: 'test-cmd',
cascadeKeys: [ 'test' ], cascadeKeys: [ 'test' ],
@ -579,7 +668,9 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType: 'default',
name: 'Default A', name: 'Default A',
shardingArguments: [],
changes: [ /test-a.js$/ ], changes: [ /test-a.js$/ ],
command: 'test-cmd-a', command: 'test-cmd-a',
cascadeKeys: [ 'test-a', 'test' ], cascadeKeys: [ 'test-a', 'test' ],
@ -595,7 +686,9 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType: 'default',
name: 'Default B', name: 'Default B',
shardingArguments: [],
changes: [ /test-b.js$/ ], changes: [ /test-b.js$/ ],
command: 'test-cmd-b', command: 'test-cmd-b',
cascadeKeys: [ 'test-b', 'test' ], cascadeKeys: [ 'test-b', 'test' ],
@ -613,20 +706,24 @@ describe( 'Job Processing', () => {
); );
expect( jobs.lint ).toHaveLength( 0 ); expect( jobs.lint ).toHaveLength( 0 );
expect( jobs.test ).toHaveLength( 2 ); expect( jobs[ `${ testType }Test` ] ).toHaveLength( 2 );
expect( jobs.test ).toContainEqual( { expect( jobs[ `${ testType }Test` ] ).toContainEqual( {
projectName: 'test', projectName: 'test',
projectPath: 'test',
name: 'Default', name: 'Default',
command: 'test-cmd', command: 'test-cmd',
shardNumber: 0,
testEnv: { testEnv: {
shouldCreate: false, shouldCreate: false,
envVars: {}, envVars: {},
}, },
} ); } );
expect( jobs.test ).toContainEqual( { expect( jobs[ `${ testType }Test` ] ).toContainEqual( {
projectName: 'test-a', projectName: 'test-a',
projectPath: 'test-a',
name: 'Default A', name: 'Default A',
command: 'test-cmd-a', command: 'test-cmd-a',
shardNumber: 0,
testEnv: { testEnv: {
shouldCreate: false, shouldCreate: false,
envVars: {}, envVars: {},
@ -635,6 +732,7 @@ describe( 'Job Processing', () => {
} ); } );
it( 'should trigger test job for single node and parse test environment config', async () => { it( 'should trigger test job for single node and parse test environment config', async () => {
const testType = 'default';
jest.mocked( parseTestEnvConfig ).mockResolvedValue( { jest.mocked( parseTestEnvConfig ).mockResolvedValue( {
WP_ENV_CORE: 'https://wordpress.org/latest.zip', WP_ENV_CORE: 'https://wordpress.org/latest.zip',
} ); } );
@ -647,7 +745,9 @@ describe( 'Job Processing', () => {
jobs: [ jobs: [
{ {
type: JobType.Test, type: JobType.Test,
testType,
name: 'Default', name: 'Default',
shardingArguments: [],
changes: [ /test.js$/ ], changes: [ /test.js$/ ],
command: 'test-cmd', command: 'test-cmd',
testEnv: { testEnv: {
@ -672,11 +772,13 @@ describe( 'Job Processing', () => {
); );
expect( jobs.lint ).toHaveLength( 0 ); expect( jobs.lint ).toHaveLength( 0 );
expect( jobs.test ).toHaveLength( 1 ); expect( jobs[ `${ testType }Test` ] ).toHaveLength( 1 );
expect( jobs.test ).toContainEqual( { expect( jobs[ `${ testType }Test` ] ).toContainEqual( {
projectName: 'test', projectName: 'test',
projectPath: 'test',
name: 'Default', name: 'Default',
command: 'test-cmd', command: 'test-cmd',
shardNumber: 0,
testEnv: { testEnv: {
shouldCreate: true, shouldCreate: true,
start: 'test-start test-base-ref', start: 'test-start test-base-ref',
@ -688,6 +790,7 @@ describe( 'Job Processing', () => {
} ); } );
it( 'should trigger all jobs for a single node with changes set to "true"', async () => { it( 'should trigger all jobs for a single node with changes set to "true"', async () => {
const testType = 'default';
const jobs = await createJobsForChanges( const jobs = await createJobsForChanges(
{ {
name: 'test', name: 'test',
@ -701,7 +804,9 @@ describe( 'Job Processing', () => {
}, },
{ {
type: JobType.Test, type: JobType.Test,
testType,
name: 'Default', name: 'Default',
shardingArguments: [],
changes: [ /test.js$/ ], changes: [ /test.js$/ ],
command: 'test-cmd', command: 'test-cmd',
}, },
@ -716,18 +821,174 @@ describe( 'Job Processing', () => {
expect( jobs.lint ).toHaveLength( 1 ); expect( jobs.lint ).toHaveLength( 1 );
expect( jobs.lint ).toContainEqual( { expect( jobs.lint ).toContainEqual( {
projectName: 'test', projectName: 'test',
projectPath: 'test',
command: 'test-lint', command: 'test-lint',
} ); } );
expect( jobs.test ).toHaveLength( 1 ); expect( jobs[ `${ testType }Test` ] ).toHaveLength( 1 );
expect( jobs.test ).toContainEqual( { expect( jobs[ `${ testType }Test` ] ).toContainEqual( {
projectName: 'test', projectName: 'test',
projectPath: 'test',
name: 'Default', name: 'Default',
command: 'test-cmd', command: 'test-cmd',
shardNumber: 0,
testEnv: { testEnv: {
shouldCreate: false, shouldCreate: false,
envVars: {}, envVars: {},
}, },
} ); } );
} ); } );
it( 'should trigger sharded test jobs for single node', async () => {
const testType = 'default';
const jobs = await createJobsForChanges(
{
name: 'test',
path: 'test',
ciConfig: {
jobs: [
{
type: JobType.Test,
testType,
name: 'Default',
shardingArguments: [
'--shard=1/2',
'--shard=2/2',
],
changes: [ /test.js$/ ],
command: 'test-cmd',
},
],
},
dependencies: [],
},
{
test: [ 'test.js' ],
},
{}
);
expect( jobs.lint ).toHaveLength( 0 );
expect( jobs[ `${ testType }Test` ] ).toHaveLength( 2 );
expect( jobs[ `${ testType }Test` ] ).toEqual(
expect.arrayContaining( [
{
projectName: 'test',
projectPath: 'test',
name: 'Default 1/2',
command: 'test-cmd --shard=1/2',
shardNumber: 1,
testEnv: {
shouldCreate: false,
envVars: {},
},
},
{
projectName: 'test',
projectPath: 'test',
name: 'Default 2/2',
command: 'test-cmd --shard=2/2',
shardNumber: 2,
testEnv: {
shouldCreate: false,
envVars: {},
},
},
] )
);
} );
} );
describe( 'getShardedJobs', () => {
it( 'should create sharded jobs', async () => {
const jobs = getShardedJobs(
{
projectName: 'test',
projectPath: 'test',
name: 'Default',
command: 'test-cmd',
shardNumber: 0,
testEnv: {
shouldCreate: false,
envVars: {},
},
},
{
type: JobType.Test,
testType: 'e2e',
name: 'Default',
shardingArguments: [ '--shard-arg-1', '--shard-arg-2' ],
changes: [ /test.js$/ ],
command: 'test-cmd',
}
);
expect( jobs ).toHaveLength( 2 );
expect( jobs ).toEqual(
expect.arrayContaining( [
{
projectName: 'test',
projectPath: 'test',
name: 'Default 1/2',
command: 'test-cmd --shard-arg-1',
shardNumber: 1,
testEnv: {
shouldCreate: false,
envVars: {},
},
},
{
projectName: 'test',
projectPath: 'test',
name: 'Default 2/2',
command: 'test-cmd --shard-arg-2',
shardNumber: 2,
testEnv: {
shouldCreate: false,
envVars: {},
},
},
] )
);
} );
it.each( [ [ [] ], [ [ '--sharding=1/1' ] ] ] )(
'should not create sharded jobs for shards',
async ( shardingArguments ) => {
const jobs = getShardedJobs(
{
projectName: 'test',
projectPath: 'test',
name: 'Default',
command: 'test-cmd',
shardNumber: 0,
testEnv: {
shouldCreate: false,
envVars: {},
},
},
{
type: JobType.Test,
testType: 'e2e',
name: 'Default',
shardingArguments,
changes: [ /test.js$/ ],
command: 'test-cmd',
}
);
expect( jobs ).toHaveLength( 1 );
expect( jobs ).toContainEqual( {
projectName: 'test',
projectPath: 'test',
name: 'Default',
command: 'test-cmd',
shardNumber: 0,
testEnv: {
shouldCreate: false,
envVars: {},
},
} );
}
);
} ); } );
} ); } );

View File

@ -21,6 +21,11 @@ export const enum JobType {
Test = 'test', Test = 'test',
} }
/**
* The type of the test job.
*/
export const testTypes = [ 'default', 'e2e', 'api', 'performance' ] as const;
/** /**
* The variables that can be used in tokens on command strings * The variables that can be used in tokens on command strings
* that will be replaced during job creation. * that will be replaced during job creation.
@ -251,11 +256,21 @@ export interface TestJobConfig extends BaseJobConfig {
*/ */
type: JobType.Test; type: JobType.Test;
/**
* The type of the test.
*/
testType: ( typeof testTypes )[ number ];
/** /**
* The name for the job. * The name for the job.
*/ */
name: string; name: string;
/**
* The number of shards to be created for this job.
*/
shardingArguments: string[];
/** /**
* The configuration for the test environment if one is needed. * The configuration for the test environment if one is needed.
*/ */
@ -320,10 +335,20 @@ function parseTestJobConfig( raw: any ): TestJobConfig {
); );
} }
let testType: ( typeof testTypes )[ number ] = 'default';
if (
raw.testType &&
testTypes.includes( raw.testType.toString().toLowerCase() )
) {
testType = raw.testType.toLowerCase();
}
validateCommandVars( raw.command ); validateCommandVars( raw.command );
const config: TestJobConfig = { const config: TestJobConfig = {
type: JobType.Test, type: JobType.Test,
testType,
shardingArguments: raw.shardingArguments || [],
name: raw.name, name: raw.name,
changes: parseChangesConfig( raw.changes, [ 'package.json' ] ), changes: parseChangesConfig( raw.changes, [ 'package.json' ] ),
command: raw.command, command: raw.command,

View File

@ -4,6 +4,7 @@
import { import {
CommandVarOptions, CommandVarOptions,
JobType, JobType,
testTypes,
LintJobConfig, LintJobConfig,
TestJobConfig, TestJobConfig,
} from './config'; } from './config';
@ -16,6 +17,7 @@ import { TestEnvVars, parseTestEnvConfig } from './test-environment';
*/ */
interface LintJob { interface LintJob {
projectName: string; projectName: string;
projectPath: string;
command: string; command: string;
} }
@ -33,9 +35,11 @@ interface TestJobEnv {
*/ */
interface TestJob { interface TestJob {
projectName: string; projectName: string;
projectPath: string;
name: string; name: string;
command: string; command: string;
testEnv: TestJobEnv; testEnv: TestJobEnv;
shardNumber: number;
} }
/** /**
@ -70,10 +74,42 @@ function replaceCommandVars( command: string, options: CreateOptions ): string {
} ); } );
} }
/**
* Multiplies a job based on the shards job config. It updates the job names and command - currently only supporting Playwright sharding.
*
* @param {TestJob} job The job to be multiplied.
* @param {TestJobConfig} jobConfig The job config.
* @return {TestJob[]} The list of sharded jobs.
*/
export function getShardedJobs(
job: TestJob,
jobConfig: TestJobConfig
): TestJob[] {
let createdJobs = [];
const shards = jobConfig.shardingArguments.length;
if ( shards <= 1 ) {
createdJobs.push( job );
} else {
createdJobs = Array( shards )
.fill( null )
.map( ( _, i ) => {
const jobCopy = JSON.parse( JSON.stringify( job ) );
jobCopy.shardNumber = i + 1;
jobCopy.name = `${ job.name } ${ i + 1 }/${ shards }`;
jobCopy.command = `${ job.command } ${ jobConfig.shardingArguments[ i ] }`;
return jobCopy;
} );
}
return createdJobs;
}
/** /**
* Checks the config against the changes and creates one if it should be run. * Checks the config against the changes and creates one if it should be run.
* *
* @param {string} projectName The name of the project that the job is for. * @param {string} projectName The name of the project that the job is for.
* @param {string} projectPath The path of the project that the job is for.
* @param {Object} config The config object for the lint job. * @param {Object} config The config object for the lint job.
* @param {Array.<string>|true} changes The file changes that have occurred for the project or true if all projects should be marked as changed. * @param {Array.<string>|true} changes The file changes that have occurred for the project or true if all projects should be marked as changed.
* @param {Object} options The options to use when creating the job. * @param {Object} options The options to use when creating the job.
@ -81,6 +117,7 @@ function replaceCommandVars( command: string, options: CreateOptions ): string {
*/ */
function createLintJob( function createLintJob(
projectName: string, projectName: string,
projectPath: string,
config: LintJobConfig, config: LintJobConfig,
changes: string[] | true, changes: string[] | true,
options: CreateOptions options: CreateOptions
@ -114,6 +151,7 @@ function createLintJob(
return { return {
projectName, projectName,
projectPath,
command: replaceCommandVars( config.command, options ), command: replaceCommandVars( config.command, options ),
}; };
} }
@ -122,18 +160,22 @@ function createLintJob(
* Checks the config against the changes and creates one if it should be run. * Checks the config against the changes and creates one if it should be run.
* *
* @param {string} projectName The name of the project that the job is for. * @param {string} projectName The name of the project that the job is for.
* @param {string} projectPath The path of the project that the job is for.
* @param {Object} config The config object for the test job. * @param {Object} config The config object for the test job.
* @param {Array.<string>|true} changes The file changes that have occurred for the project or true if all projects should be marked as changed. * @param {Array.<string>|true} changes The file changes that have occurred for the project or true if all projects should be marked as changed.
* @param {Object} options The options to use when creating the job. * @param {Object} options The options to use when creating the job.
* @param {Array.<string>} cascadeKeys The cascade keys that have been triggered in dependencies. * @param {Array.<string>} cascadeKeys The cascade keys that have been triggered in dependencies.
* @param {number} shardNumber The shard number for the job.
* @return {Promise.<Object|null>} The job that should be run or null if no job should be run. * @return {Promise.<Object|null>} The job that should be run or null if no job should be run.
*/ */
async function createTestJob( async function createTestJob(
projectName: string, projectName: string,
projectPath: string,
config: TestJobConfig, config: TestJobConfig,
changes: string[] | true, changes: string[] | true,
options: CreateOptions, options: CreateOptions,
cascadeKeys: string[] cascadeKeys: string[],
shardNumber: number
): Promise< TestJob | null > { ): Promise< TestJob | null > {
let triggered = false; let triggered = false;
@ -179,12 +221,14 @@ async function createTestJob(
const createdJob: TestJob = { const createdJob: TestJob = {
projectName, projectName,
projectPath,
name: config.name, name: config.name,
command: replaceCommandVars( config.command, options ), command: replaceCommandVars( config.command, options ),
testEnv: { testEnv: {
shouldCreate: false, shouldCreate: false,
envVars: {}, envVars: {},
}, },
shardNumber,
}; };
// We want to make sure that we're including the configuration for // We want to make sure that we're including the configuration for
@ -221,6 +265,10 @@ async function createJobsForProject(
test: [], test: [],
}; };
testTypes.forEach( ( type ) => {
newJobs[ `${ type }Test` ] = [];
} );
// In order to simplify the way that cascades work we're going to recurse depth-first and check our dependencies // In order to simplify the way that cascades work we're going to recurse depth-first and check our dependencies
// for jobs before ourselves. This lets any cascade keys created in dependencies cascade to dependents. // for jobs before ourselves. This lets any cascade keys created in dependencies cascade to dependents.
const newCascadeKeys = []; const newCascadeKeys = [];
@ -240,7 +288,12 @@ async function createJobsForProject(
dependencyCascade dependencyCascade
); );
newJobs.lint.push( ...dependencyJobs.lint ); newJobs.lint.push( ...dependencyJobs.lint );
newJobs.test.push( ...dependencyJobs.test );
testTypes.forEach( ( type ) => {
newJobs[ `${ type }Test` ].push(
...dependencyJobs[ `${ type }Test` ]
);
} );
// Track any new cascade keys added by the dependency. // Track any new cascade keys added by the dependency.
// Since we're filtering out duplicates after the // Since we're filtering out duplicates after the
@ -285,6 +338,7 @@ async function createJobsForProject(
case JobType.Lint: { case JobType.Lint: {
const created = createLintJob( const created = createLintJob(
node.name, node.name,
node.path,
jobConfig, jobConfig,
projectChanges, projectChanges,
options options
@ -301,17 +355,22 @@ async function createJobsForProject(
case JobType.Test: { case JobType.Test: {
const created = await createTestJob( const created = await createTestJob(
node.name, node.name,
node.path,
jobConfig, jobConfig,
projectChanges, projectChanges,
options, options,
cascadeKeys cascadeKeys,
0
); );
if ( ! created ) { if ( ! created ) {
break; break;
} }
jobConfig.jobCreated = true; jobConfig.jobCreated = true;
newJobs.test.push( created );
newJobs[ `${ jobConfig.testType }Test` ].push(
...getShardedJobs( created, jobConfig )
);
// We need to track any cascade keys that this job is associated with so that // We need to track any cascade keys that this job is associated with so that
// dependent projects can trigger jobs with matching keys. We are expecting // dependent projects can trigger jobs with matching keys. We are expecting