@salesforce/plugin-apex
Version:
189 lines • 9.52 kB
JavaScript
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { CancellationTokenSource, TestService } from '@salesforce/apex-node';
import { arrayWithDeprecation, Flags, loglevel, orgApiVersionFlagWithDeprecations, requiredOrgFlagWithDeprecations, SfCommand, Ux, } from '@salesforce/sf-plugins-core';
import { Messages, SfError } from '@salesforce/core';
import { TestReporter } from '../../../reporters/index.js';
import { codeCoverageFlag, resultFormatFlag } from '../../../flags.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-apex', 'runtest');
export const TestLevelValues = ['RunLocalTests', 'RunAllTestsInOrg', 'RunSpecifiedTests'];
const exclusiveTestSpecifiers = ['class-names', 'suite-names', 'tests'];
export default class Test extends SfCommand {
static summary = messages.getMessage('summary');
static description = messages.getMessage('description');
static examples = messages.getMessages('examples');
static deprecateAliases = true;
static aliases = ['force:apex:test:run'];
static flags = {
'target-org': requiredOrgFlagWithDeprecations,
'api-version': orgApiVersionFlagWithDeprecations,
loglevel,
'code-coverage': codeCoverageFlag,
'output-dir': Flags.directory({
aliases: ['outputdir', 'output-directory'],
deprecateAliases: true,
char: 'd',
summary: messages.getMessage('flags.output-dir.summary'),
}),
'test-level': Flags.string({
deprecateAliases: true,
aliases: ['testlevel'],
char: 'l',
summary: messages.getMessage('flags.test-level.summary'),
description: messages.getMessage('flags.test-level.description'),
options: TestLevelValues,
}),
'class-names': arrayWithDeprecation({
deprecateAliases: true,
aliases: ['classnames'],
char: 'n',
summary: messages.getMessage('flags.class-names.summary'),
description: messages.getMessage('flags.class-names.description'),
exclusive: exclusiveTestSpecifiers.filter((specifier) => specifier !== 'class-names'),
}),
'result-format': resultFormatFlag,
'suite-names': arrayWithDeprecation({
deprecateAliases: true,
aliases: ['suitenames'],
char: 's',
summary: messages.getMessage('flags.suite-names.summary'),
description: messages.getMessage('flags.suite-names.description'),
exclusive: exclusiveTestSpecifiers.filter((specifier) => specifier !== 'suite-names'),
}),
tests: arrayWithDeprecation({
char: 't',
summary: messages.getMessage('flags.tests.summary'),
description: messages.getMessage('flags.tests.description'),
exclusive: exclusiveTestSpecifiers.filter((specifier) => specifier !== 'tests'),
}),
// we want to pass `undefined` to the API
// eslint-disable-next-line sf-plugin/flag-min-max-default
wait: Flags.duration({
unit: 'minutes',
char: 'w',
summary: messages.getMessage('flags.wait.summary'),
min: 0,
}),
synchronous: Flags.boolean({
char: 'y',
summary: messages.getMessage('flags.synchronous.summary'),
}),
'detailed-coverage': Flags.boolean({
deprecateAliases: true,
aliases: ['detailedcoverage'],
char: 'v',
summary: messages.getMessage('flags.detailed-coverage.summary'),
dependsOn: ['code-coverage'],
}),
concise: Flags.boolean({
summary: messages.getMessage('flags.concise.summary'),
}),
};
cancellationTokenSource = new CancellationTokenSource();
async run() {
const { flags } = await this.parse(Test);
const testLevel = await validateFlags(flags['class-names'], flags['suite-names'], flags.tests, flags.synchronous, flags['test-level']);
// graceful shutdown
const exitHandler = async () => {
await this.cancellationTokenSource.asyncCancel();
process.exit();
};
// eslint-disable-next-line @typescript-eslint/no-misused-promises
process.on('SIGINT', exitHandler);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
process.on('SIGTERM', exitHandler);
const conn = flags['target-org'].getConnection(flags['api-version']);
const testService = new TestService(conn);
// NOTE: This is a *bug*. Synchronous test runs should throw an error when multiple test classes are specified
// This was re-introduced due to https://github.com/forcedotcom/salesforcedx-vscode/issues/3154
// Address with W-9163533
const result = flags.synchronous && testLevel === "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */
? await this.runTest(testService, flags, testLevel)
: await this.runTestAsynchronous(testService, flags, testLevel);
if (this.cancellationTokenSource.token.isCancellationRequested) {
throw new SfError('Cancelled');
}
if ('summary' in result) {
const testReporter = new TestReporter(new Ux({ jsonEnabled: this.jsonEnabled() }), conn);
return testReporter.report(result, flags);
}
else {
// Tests were ran asynchronously or the --wait timed out.
// Log the proper 'apex get test' command for the user to run later
this.log(messages.getMessage('runTestReportCommand', [this.config.bin, result.testRunId, conn.getUsername()]));
this.info(messages.getMessage('runTestSyncInstructions'));
if (flags['output-dir']) {
// testService writes a file with just the test run id in it to test-run-id.txt
// github.com/forcedotcom/salesforcedx-apex/blob/c986abfabee3edf12f396f1d2e43720988fa3911/src/tests/testService.ts#L245-L246
await testService.writeResultFiles(result, { dirPath: flags['output-dir'] }, flags['code-coverage']);
}
return result;
}
}
async runTest(testService, flags, testLevel) {
const payload = {
...(await testService.buildSyncPayload(testLevel, flags.tests?.join(','), flags['class-names']?.join(','))),
skipCodeCoverage: !flags['code-coverage'],
};
try {
return (await testService.runTestSynchronous(payload, flags['code-coverage'], this.cancellationTokenSource.token));
}
catch (e) {
throw handleTestingServerError(SfError.wrap(e), flags, testLevel);
}
}
async runTestAsynchronous(testService, flags, testLevel) {
const payload = {
...(await testService.buildAsyncPayload(testLevel, flags.tests?.join(','), flags['class-names']?.join(','), flags['suite-names']?.join(','))),
skipCodeCoverage: !flags['code-coverage'],
};
try {
// cast as TestRunIdResult because we're building an async payload which will return an async result
return (await testService.runTestAsynchronous(payload, flags['code-coverage'], flags.wait && flags.wait.minutes > 0 ? false : !(flags.synchronous && !this.jsonEnabled()), undefined, this.cancellationTokenSource.token, flags.wait));
}
catch (e) {
throw handleTestingServerError(SfError.wrap(e), flags, testLevel);
}
}
}
function handleTestingServerError(error, flags, testLevel) {
if (!error.message.includes('Always provide a classes, suites, tests, or testLevel property')) {
return error;
}
// If error message condition is valid, return the original error.
const hasSpecifiedTestLevel = testLevel === "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */;
const hasNoTestNames = !flags.tests?.length;
const hasNoClassNames = !flags['class-names']?.length;
const hasNoSuiteNames = !flags['suite-names']?.length;
if (hasSpecifiedTestLevel && hasNoTestNames && hasNoClassNames && hasNoSuiteNames) {
return error;
}
// Otherwise, assume there are no Apex tests in the org and return clearer message.
return Object.assign(error, {
message: 'There are no Apex tests to run in this org.',
actions: ['Ensure Apex Tests exist in the org, and try again.'],
});
}
const validateFlags = async (classNames, suiteNames, tests, synchronous, testLevel) => {
if (synchronous && (Boolean(suiteNames) || (classNames?.length && classNames.length > 1))) {
return Promise.reject(new Error(messages.getMessage('syncClassErr')));
}
if ((Boolean(tests) || Boolean(classNames) || suiteNames) &&
testLevel &&
testLevel.toString() !== 'RunSpecifiedTests') {
return Promise.reject(new Error(messages.getMessage('testLevelErr')));
}
if (testLevel) {
return testLevel;
}
if (Boolean(classNames) || Boolean(suiteNames) || tests) {
return "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */;
}
return "RunLocalTests" /* TestLevel.RunLocalTests */;
};
//# sourceMappingURL=test.js.map