Skip to content

Commit b91a8f5

Browse files
Merge pull request #141 from salesforcecli/sm/use-array-flags
Sm/use-array-flags
2 parents db42532 + eeacd1b commit b91a8f5

File tree

3 files changed

+160
-58
lines changed

3 files changed

+160
-58
lines changed

messages/runtest.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ NOTE: The testRunCoverage value (JSON and JUnit result formats) is a percentage
2020

2121
- Run the specified Apex test classes in your default org and display results in human-readable form:
2222

23-
<%= config.bin %> <%= command.id %> --class-names "MyClassTest,MyOtherClassTest" --result-format human
23+
<%= config.bin %> <%= command.id %> --class-names MyClassTest --class-names MyOtherClassTest --result-format human
2424

2525
- Run the specified Apex test suites in your default org and include code coverage results and additional details:
2626

27-
<%= config.bin %> <%= command.id %> --suite-names "MySuite,MyOtherSuite" --code-coverage --detailed-coverage
27+
<%= config.bin %> <%= command.id %> --suite-names MySuite --suite-names MyOtherSuite --code-coverage --detailed-coverage
2828

2929
- Run the specified Apex tests in your default org and display results in human-readable output:
3030

31-
<%= config.bin %> <%= command.id %> --tests "MyClassTest.testCoolFeature,MyClassTest.testAwesomeFeature,AnotherClassTest,namespace.TheirClassTest.testThis" --result-format human
31+
<%= config.bin %> <%= command.id %> --tests MyClassTest.testCoolFeature --tests MyClassTest.testAwesomeFeature --tests AnotherClassTest --tests namespace.TheirClassTest.testThis --result-format human
3232

3333
- Run all tests in the org with the specified username with the specified test level; save the output to the specified directory:
3434

@@ -40,27 +40,33 @@ Format of the test results.
4040

4141
# flags.class-names.summary
4242

43-
Comma-separated list of Apex test class names to run; default is all classes.
43+
Apex test class names to run; default is all classes.
4444

4545
# flags.class-names.description
4646

4747
If you select --class-names, you can't specify --suite-names or --tests.
48+
For multiple classes, repeat the flag for each.
49+
--class-names Class1 --class-names Class2
4850

4951
# flags.suite-names.summary
5052

51-
Comma-separated list of Apex test suite names to run; default is all suites.
53+
Apex test suite names to run; default is all suites.
5254

5355
# flags.suite-names.description
5456

5557
If you select --suite-names, you can't specify --class-names or --tests.
58+
For multiple suites, repeat the flag for each.
59+
--suite-names Suite1 --suite-names Suite2
5660

5761
# flags.tests.summary
5862

59-
Comma-separated list of Apex test class names or IDs and, if applicable, test methods to run; default is all tests.
63+
Apex test class names or IDs and, if applicable, test methods to run; default is all tests.
6064

6165
# flags.tests.description
6266

6367
If you specify --tests, you can't specify --class-names or --suite-names
68+
For multiple tests, repeat the flag for each.
69+
--tests Test1 --tests Test2
6470

6571
# flags.code-coverage.summary
6672

@@ -98,10 +104,6 @@ Display detailed code coverage per test.
98104

99105
Run "%s apex get test -i %s -o %s" to retrieve test results
100106

101-
# classSuiteTestErr
102-
103-
Specify either classnames, suitenames, or tests
104-
105107
# syncClassErr
106108

107109
Synchronous test runs can include test methods from only one Apex class. Omit the --synchronous flag or include tests from only one class

src/commands/apex/run/test.ts

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
import { CancellationTokenSource, TestLevel, TestResult, TestRunIdResult, TestService } from '@salesforce/apex-node';
88
import {
9+
arrayWithDeprecation,
910
Flags,
1011
loglevel,
1112
orgApiVersionFlagWithDeprecations,
@@ -24,6 +25,7 @@ const messages = Messages.loadMessages('@salesforce/plugin-apex', 'runtest');
2425

2526
export const TestLevelValues = ['RunLocalTests', 'RunAllTestsInOrg', 'RunSpecifiedTests'];
2627
export type RunCommandResult = RunResult | TestRunIdResult;
28+
const exclusiveTestSpecifiers = ['class-names', 'suite-names', 'tests'];
2729
export default class Test extends SfCommand<RunCommandResult> {
2830
public static readonly summary = messages.getMessage('summary');
2931
public static readonly description = messages.getMessage('description');
@@ -55,12 +57,13 @@ export default class Test extends SfCommand<RunCommandResult> {
5557
description: messages.getMessage('flags.test-level.description'),
5658
options: TestLevelValues,
5759
}),
58-
'class-names': Flags.string({
60+
'class-names': arrayWithDeprecation({
5961
deprecateAliases: true,
6062
aliases: ['classnames'],
6163
char: 'n',
6264
summary: messages.getMessage('flags.class-names.summary'),
6365
description: messages.getMessage('flags.class-names.description'),
66+
exclusive: exclusiveTestSpecifiers.filter((specifier) => specifier !== 'class-names'),
6467
}),
6568
'result-format': Flags.string({
6669
deprecateAliases: true,
@@ -70,17 +73,19 @@ export default class Test extends SfCommand<RunCommandResult> {
7073
options: resultFormat,
7174
default: 'human',
7275
}),
73-
'suite-names': Flags.string({
76+
'suite-names': arrayWithDeprecation({
7477
deprecateAliases: true,
7578
aliases: ['suitenames'],
7679
char: 's',
7780
summary: messages.getMessage('flags.suite-names.summary'),
7881
description: messages.getMessage('flags.suite-names.description'),
82+
exclusive: exclusiveTestSpecifiers.filter((specifier) => specifier !== 'suite-names'),
7983
}),
80-
tests: Flags.string({
84+
tests: arrayWithDeprecation({
8185
char: 't',
8286
summary: messages.getMessage('flags.tests.summary'),
8387
description: messages.getMessage('flags.tests.description'),
88+
exclusive: exclusiveTestSpecifiers.filter((specifier) => specifier !== 'tests'),
8489
}),
8590
// we want to pass `undefined` to the API
8691
// eslint-disable-next-line sf-plugin/flag-min-max-default
@@ -108,9 +113,7 @@ export default class Test extends SfCommand<RunCommandResult> {
108113
public async run(): Promise<RunCommandResult> {
109114
const { flags } = await this.parse(Test);
110115

111-
const testLevel = await this.validateFlags(
112-
flags['code-coverage'],
113-
flags['result-format'],
116+
const testLevel = await validateFlags(
114117
flags['class-names'],
115118
flags['suite-names'],
116119
flags.tests,
@@ -159,48 +162,17 @@ export default class Test extends SfCommand<RunCommandResult> {
159162
}
160163
}
161164

162-
// eslint-disable-next-line class-methods-use-this
163-
public async validateFlags(
164-
codeCoverage?: boolean,
165-
resultFormatFlag?: string,
166-
classNames?: string,
167-
suiteNames?: string,
168-
tests?: string,
169-
synchronous?: boolean,
170-
testLevel?: TestLevel
171-
): Promise<TestLevel> {
172-
if ((classNames && (suiteNames || tests)) || (suiteNames && tests)) {
173-
return Promise.reject(new Error(messages.getMessage('classSuiteTestErr')));
174-
}
175-
176-
if (synchronous && (suiteNames || (classNames && classNames.split(',').length > 1))) {
177-
return Promise.reject(new Error(messages.getMessage('syncClassErr')));
178-
}
179-
180-
if ((tests || classNames || suiteNames) && testLevel && testLevel !== 'RunSpecifiedTests') {
181-
return Promise.reject(new Error(messages.getMessage('testLevelErr')));
182-
}
183-
184-
if (testLevel) {
185-
return testLevel;
186-
}
187-
if (classNames || suiteNames || tests) {
188-
return TestLevel.RunSpecifiedTests;
189-
}
190-
return TestLevel.RunLocalTests;
191-
}
192-
193165
private async runTest(
194166
testService: TestService,
195167
flags: {
196-
tests?: string;
197-
'class-names'?: string;
168+
tests?: string[];
169+
'class-names'?: string[];
198170
'code-coverage'?: boolean;
199171
},
200172
testLevel: TestLevel
201173
): Promise<TestResult> {
202174
const payload = {
203-
...(await testService.buildSyncPayload(testLevel, flags.tests, flags['class-names'])),
175+
...(await testService.buildSyncPayload(testLevel, flags.tests?.join(','), flags['class-names']?.join(','))),
204176
skipCodeCoverage: !flags['code-coverage'],
205177
};
206178
return testService.runTestSynchronous(
@@ -213,9 +185,9 @@ export default class Test extends SfCommand<RunCommandResult> {
213185
private async runTestAsynchronous(
214186
testService: TestService,
215187
flags: {
216-
tests?: string;
217-
'class-names'?: string;
218-
'suite-names'?: string;
188+
tests?: string[];
189+
'class-names'?: string[];
190+
'suite-names'?: string[];
219191
'code-coverage'?: boolean;
220192
synchronous?: boolean;
221193
'result-format'?: string;
@@ -225,7 +197,12 @@ export default class Test extends SfCommand<RunCommandResult> {
225197
testLevel: TestLevel
226198
): Promise<TestRunIdResult> {
227199
const payload = {
228-
...(await testService.buildAsyncPayload(testLevel, flags.tests, flags['class-names'], flags['suite-names'])),
200+
...(await testService.buildAsyncPayload(
201+
testLevel,
202+
flags.tests?.join(','),
203+
flags['class-names']?.join(','),
204+
flags['suite-names']?.join(',')
205+
)),
229206
skipCodeCoverage: !flags['code-coverage'],
230207
};
231208

@@ -239,3 +216,28 @@ export default class Test extends SfCommand<RunCommandResult> {
239216
) as Promise<TestRunIdResult>;
240217
}
241218
}
219+
220+
// eslint-disable-next-line class-methods-use-this
221+
const validateFlags = async (
222+
classNames?: string[],
223+
suiteNames?: string[],
224+
tests?: string[],
225+
synchronous?: boolean,
226+
testLevel?: TestLevel
227+
): Promise<TestLevel> => {
228+
if (synchronous && (suiteNames || (classNames?.length && classNames.length > 1))) {
229+
return Promise.reject(new Error(messages.getMessage('syncClassErr')));
230+
}
231+
232+
if ((tests || classNames || suiteNames) && testLevel && testLevel !== 'RunSpecifiedTests') {
233+
return Promise.reject(new Error(messages.getMessage('testLevelErr')));
234+
}
235+
236+
if (testLevel) {
237+
return testLevel;
238+
}
239+
if (classNames || suiteNames || tests) {
240+
return TestLevel.RunSpecifiedTests;
241+
}
242+
return TestLevel.RunLocalTests;
243+
};

test/commands/apex/run/test.test.ts

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Messages, Org } from '@salesforce/core';
1010
import { createSandbox, SinonSandbox } from 'sinon';
1111
import { Ux } from '@salesforce/sf-plugins-core';
1212
import { Config } from '@oclif/core';
13-
import { expect } from 'chai';
13+
import { assert, expect } from 'chai';
1414
import { TestService } from '@salesforce/apex-node';
1515
import Test from '../../../../src/commands/apex/run/test';
1616
import {
@@ -224,6 +224,100 @@ describe('apex:test:run', () => {
224224
expect(logStub.firstCall.args[0]).to.include('MyApexTests.testConfig Pass 53');
225225
});
226226

227+
it('should parse tests flags correctly comma separated', async () => {
228+
const apexStub = sandbox.stub(TestService.prototype, 'runTestAsynchronous').resolves(testRunSimple);
229+
230+
await new Test(['--tests', 'MyApexTests,MySecondTest', '--result-format', 'human'], config).run();
231+
expect(apexStub.firstCall.args[0]).to.deep.equal({
232+
skipCodeCoverage: true,
233+
testLevel: 'RunSpecifiedTests',
234+
tests: [
235+
{
236+
className: 'MyApexTests',
237+
},
238+
{
239+
className: 'MySecondTest',
240+
},
241+
],
242+
});
243+
});
244+
245+
it('should parse tests flags correctly multi-flag', async () => {
246+
const apexStub = sandbox.stub(TestService.prototype, 'runTestAsynchronous').resolves(testRunSimple);
247+
248+
await new Test(['--tests', 'MyApexTests', '--tests', 'MySecondTest', '--result-format', 'human'], config).run();
249+
expect(apexStub.firstCall.args[0]).to.deep.equal({
250+
skipCodeCoverage: true,
251+
testLevel: 'RunSpecifiedTests',
252+
tests: [
253+
{
254+
className: 'MyApexTests',
255+
},
256+
{
257+
className: 'MySecondTest',
258+
},
259+
],
260+
});
261+
});
262+
263+
it('should parse class-names flags correctly comma separated', async () => {
264+
const apexStub = sandbox.stub(TestService.prototype, 'runTestAsynchronous').resolves(testRunSimple);
265+
266+
await new Test(['--class-names', 'MyApexTests,MySecondTest', '--result-format', 'human'], config).run();
267+
expect(apexStub.firstCall.args[0]).to.deep.equal({
268+
skipCodeCoverage: true,
269+
testLevel: 'RunSpecifiedTests',
270+
tests: [
271+
{
272+
className: 'MyApexTests',
273+
},
274+
{
275+
className: 'MySecondTest',
276+
},
277+
],
278+
});
279+
});
280+
281+
it('should parse class-names (-n) flags correctly multi-flag', async () => {
282+
const apexStub = sandbox.stub(TestService.prototype, 'runTestAsynchronous').resolves(testRunSimple);
283+
284+
await new Test(['-n', 'MyApexTests', '-n', 'MySecondTest', '--result-format', 'human'], config).run();
285+
expect(apexStub.firstCall.args[0]).to.deep.equal({
286+
skipCodeCoverage: true,
287+
testLevel: 'RunSpecifiedTests',
288+
tests: [
289+
{
290+
className: 'MyApexTests',
291+
},
292+
{
293+
className: 'MySecondTest',
294+
},
295+
],
296+
});
297+
});
298+
299+
it('should parse suite-names flags correctly comma separated', async () => {
300+
const apexStub = sandbox.stub(TestService.prototype, 'runTestAsynchronous').resolves(testRunSimple);
301+
302+
await new Test(['--suite-names', 'MyApexTests,MySecondTest', '--result-format', 'human'], config).run();
303+
expect(apexStub.firstCall.args[0]).to.deep.equal({
304+
skipCodeCoverage: true,
305+
testLevel: 'RunSpecifiedTests',
306+
suiteNames: 'MyApexTests,MySecondTest',
307+
});
308+
});
309+
310+
it('should parse suite-names (-s) flags correctly multi-flag', async () => {
311+
const apexStub = sandbox.stub(TestService.prototype, 'runTestAsynchronous').resolves(testRunSimple);
312+
313+
await new Test(['-s', 'MyApexTests', '-s', 'MySecondTest', '--result-format', 'human'], config).run();
314+
expect(apexStub.firstCall.args[0]).to.deep.equal({
315+
skipCodeCoverage: true,
316+
testLevel: 'RunSpecifiedTests',
317+
suiteNames: 'MyApexTests,MySecondTest',
318+
});
319+
});
320+
227321
it('should return a success tap format message with async', async () => {
228322
sandbox.stub(TestService.prototype, 'runTestAsynchronous').resolves(testRunSimple);
229323

@@ -401,22 +495,26 @@ describe('apex:test:run', () => {
401495
});
402496

403497
it('rejects classname/suitnames/test variations', async () => {
498+
// uses oclif exclusive now
404499
try {
405500
await new Test(['--class-names', 'myApex', '--suite-names', 'testsuite'], config).run();
406501
} catch (e) {
407-
expect((e as Error).message).to.equal(messages.getMessage('classSuiteTestErr'));
502+
assert(e instanceof Error);
503+
expect(e.message).to.include('cannot also be provided when using');
408504
}
409505

410506
try {
411507
await new Test(['--class-names', 'myApex', '--tests', 'testsuite'], config).run();
412508
} catch (e) {
413-
expect((e as Error).message).to.equal(messages.getMessage('classSuiteTestErr'));
509+
assert(e instanceof Error);
510+
expect(e.message).to.include('cannot also be provided when using');
414511
}
415512

416513
try {
417514
await new Test(['--suite-names', 'myApex', '--tests', 'testsuite'], config).run();
418515
} catch (e) {
419-
expect((e as Error).message).to.equal(messages.getMessage('classSuiteTestErr'));
516+
assert(e instanceof Error);
517+
expect(e.message).to.include('cannot also be provided when using');
420518
}
421519
});
422520
});

0 commit comments

Comments
 (0)