From 0bf9940c96dee32c10bce5570e292bce93ee8749 Mon Sep 17 00:00:00 2001 From: AnanyaJha Date: Thu, 19 Nov 2020 14:42:22 -0600 Subject: [PATCH] feat: Check for query limits before getting apex test results (#92) @W-8379096@ --- packages/apex-node/src/tests/testService.ts | 139 ++++-- .../apex-node/test/tests/testService.test.ts | 320 +++++++++++- .../src/commands/force/apex/test/run.ts | 10 +- .../test/commands/force/apex/test/run.test.ts | 461 +++++++++++------- packages/plugin-apex/yarn.lock | 8 +- 5 files changed, 705 insertions(+), 233 deletions(-) diff --git a/packages/apex-node/src/tests/testService.ts b/packages/apex-node/src/tests/testService.ts index 93721cfc..066c87eb 100644 --- a/packages/apex-node/src/tests/testService.ts +++ b/packages/apex-node/src/tests/testService.ts @@ -27,6 +27,10 @@ import * as util from 'util'; import { nls } from '../i18n'; import { StreamingClient } from '../streaming'; +// Tooling API query char limit is 100,000 after v48; REST API limit for uri + headers is 16,348 bytes +// local testing shows query char limit to be closer to ~12,400 +const QUERY_CHAR_LIMIT = 12400; + export class TestService { public readonly connection: Connection; @@ -195,6 +199,47 @@ export class TestService { return percentage; } + private addIdToQuery(formattedIds: string, id: string): string { + return formattedIds.length === 0 ? id : `${formattedIds}','${id}`; + } + + public async getApexTestResults( + testQueueResult: ApexTestQueueItem + ): Promise { + let apexTestResultQuery = 'SELECT Id, QueueItemId, StackTrace, Message, '; + apexTestResultQuery += + 'RunTime, TestTimestamp, AsyncApexJobId, MethodName, Outcome, ApexLogId, '; + apexTestResultQuery += + 'ApexClass.Id, ApexClass.Name, ApexClass.NamespacePrefix '; + apexTestResultQuery += 'FROM ApexTestResult WHERE QueueItemId IN (%s)'; + + const apexResultIds = testQueueResult.records.map(record => record.Id); + let formattedIds = ''; + const queries = []; + + // iterate thru ids, create query with id, & compare query length to char limit + for (const id of apexResultIds) { + const newIds = this.addIdToQuery(formattedIds, id); + const query = util.format(apexTestResultQuery, `'${newIds}'`); + + if (query.length > QUERY_CHAR_LIMIT) { + queries.push(util.format(apexTestResultQuery, `'${formattedIds}'`)); + formattedIds = ''; + } + formattedIds = this.addIdToQuery(formattedIds, id); + } + + if (formattedIds.length > 0) { + queries.push(util.format(apexTestResultQuery, `'${formattedIds}'`)); + } + + const queryPromises = queries.map(query => { + return this.connection.tooling.query(query) as Promise; + }); + const apexTestResults = await Promise.all(queryPromises); + return apexTestResults; + } + public async getTestResultData( testQueueResult: ApexTestQueueItem, testRunId: string, @@ -214,21 +259,7 @@ export class TestService { } const summaryRecord = testRunSummaryResults.records[0]; - - let apexTestResultQuery = 'SELECT Id, QueueItemId, StackTrace, Message, '; - apexTestResultQuery += - 'RunTime, TestTimestamp, AsyncApexJobId, MethodName, Outcome, ApexLogId, '; - apexTestResultQuery += - 'ApexClass.Id, ApexClass.Name, ApexClass.NamespacePrefix '; - apexTestResultQuery += 'FROM ApexTestResult WHERE QueueItemId IN (%s)'; - - // TODO: this needs to check for query length - const apexResultIds = testQueueResult.records - .map(record => record.Id) - .join("','"); - const apexTestResults = (await this.connection.tooling.query( - util.format(apexTestResultQuery, `'${apexResultIds}'`) - )) as ApexTestResult; + const apexTestResults = await this.getApexTestResults(testQueueResult); let globalTestPassed = 0; let globalTestFailed = 0; @@ -237,46 +268,48 @@ export class TestService { const coveredApexClassIdSet = new Set(); // Iterate over test results, format and add them as results.tests const testResults: ApexTestResultData[] = []; - apexTestResults.records.forEach(item => { - switch (item.Outcome) { - case ApexTestResultOutcome.Pass: - globalTestPassed++; - break; - case ApexTestResultOutcome.Fail: - case ApexTestResultOutcome.CompileFail: - globalTestFailed++; - break; - case ApexTestResultOutcome.Skip: - globalTestSkipped++; - break; - } - - apexTestClassIdSet.add(item.ApexClass.Id); - // Can only query the FullName field if a single record is returned, so manually build the field - item.ApexClass.FullName = item.ApexClass.NamespacePrefix - ? `${item.ApexClass.NamespacePrefix}__${item.ApexClass.Name}` - : item.ApexClass.Name; + for (const result of apexTestResults) { + result.records.forEach(item => { + switch (item.Outcome) { + case ApexTestResultOutcome.Pass: + globalTestPassed++; + break; + case ApexTestResultOutcome.Fail: + case ApexTestResultOutcome.CompileFail: + globalTestFailed++; + break; + case ApexTestResultOutcome.Skip: + globalTestSkipped++; + break; + } - testResults.push({ - id: item.Id, - queueItemId: item.QueueItemId, - stackTrace: item.StackTrace, - message: item.Message, - asyncApexJobId: item.AsyncApexJobId, - methodName: item.MethodName, - outcome: item.Outcome, - apexLogId: item.ApexLogId, - apexClass: { - id: item.ApexClass.Id, - name: item.ApexClass.Name, - namespacePrefix: item.ApexClass.NamespacePrefix, - fullName: item.ApexClass.FullName - }, - runTime: item.RunTime, - testTimestamp: item.TestTimestamp, // TODO: convert timestamp - fullName: `${item.ApexClass.FullName}.${item.MethodName}` + apexTestClassIdSet.add(item.ApexClass.Id); + // Can only query the FullName field if a single record is returned, so manually build the field + item.ApexClass.FullName = item.ApexClass.NamespacePrefix + ? `${item.ApexClass.NamespacePrefix}__${item.ApexClass.Name}` + : item.ApexClass.Name; + + testResults.push({ + id: item.Id, + queueItemId: item.QueueItemId, + stackTrace: item.StackTrace, + message: item.Message, + asyncApexJobId: item.AsyncApexJobId, + methodName: item.MethodName, + outcome: item.Outcome, + apexLogId: item.ApexLogId, + apexClass: { + id: item.ApexClass.Id, + name: item.ApexClass.Name, + namespacePrefix: item.ApexClass.NamespacePrefix, + fullName: item.ApexClass.FullName + }, + runTime: item.RunTime, + testTimestamp: item.TestTimestamp, // TODO: convert timestamp + fullName: `${item.ApexClass.FullName}.${item.MethodName}` + }); }); - }); + } if (codeCoverage) { const perClassCoverageMap = await this.getPerClassCodeCoverage( diff --git a/packages/apex-node/test/tests/testService.test.ts b/packages/apex-node/test/tests/testService.test.ts index 1147722f..fd90a28f 100644 --- a/packages/apex-node/test/tests/testService.test.ts +++ b/packages/apex-node/test/tests/testService.test.ts @@ -21,7 +21,8 @@ import { ApexTestResult, ApexOrgWideCoverage, ApexCodeCoverageAggregate, - ApexCodeCoverage + ApexCodeCoverage, + ApexTestQueueItemRecord } from '../../src/tests/types'; import { AsyncTestRun, StreamingClient } from '../../src/streaming'; import { fail } from 'assert'; @@ -448,4 +449,321 @@ describe('Run Apex tests asynchronously', () => { expect(getTestResultData.tests.length).to.equal(6); expect(getTestResultData.codecoverage.length).to.equal(3); }); + + describe('Check Query Limits', async () => { + const queryStart = + 'SELECT Id, QueueItemId, StackTrace, Message, RunTime, TestTimestamp, AsyncApexJobId, MethodName, Outcome, ApexLogId, ApexClass.Id, ApexClass.Name, ApexClass.NamespacePrefix FROM ApexTestResult WHERE QueueItemId IN '; + + const record = { + Id: '7092M000000Vt94QAC', + Status: ApexTestQueueItemStatus.Completed, + ApexClassId: '01p2M00000O6tXZQAZ', + TestRunResultId: '05m2M000000TgYuQAK' + }; + const records: ApexTestQueueItemRecord[] = []; + const queryIds: string[] = []; + let count = 700; + while (count > 0) { + records.push(record); + queryIds.push(record.Id); + count--; + } + + const testQueueItems: ApexTestQueueItem = { + done: true, + totalSize: 700, + records + }; + + it('should split into multiple queries if query is longer than char limit', async () => { + const mockToolingQuery = sandboxStub.stub( + mockConnection.tooling, + 'query' + ); + mockToolingQuery.onFirstCall().resolves({ + done: true, + totalSize: 600, + records: [ + { + Id: '07Mxx00000F2Xx6UAF', + QueueItemId: '7092M000000Vt94QAC', + StackTrace: null, + Message: null, + AsyncApexJobId: testRunId, + MethodName: 'testLoggerLog', + Outcome: ApexTestResultOutcome.Pass, + ApexLogId: null, + ApexClass: { + Id: '01pxx00000O6tXZQAZ', + Name: 'TestLogger', + NamespacePrefix: 't3st', + FullName: 't3st__TestLogger' + }, + RunTime: 8, + TestTimestamp: '3' + } + ] + } as ApexTestResult); + mockToolingQuery.onSecondCall().resolves({ + done: true, + totalSize: 100, + records: [ + { + Id: '07Mxx00000F2Xx6UAF', + QueueItemId: '7092M000000Vt94QAC', + StackTrace: null, + Message: null, + AsyncApexJobId: testRunId, + MethodName: 'testLoggerLog', + Outcome: ApexTestResultOutcome.Pass, + ApexLogId: null, + ApexClass: { + Id: '01pxx00000O6tXZQAZ', + Name: 'TestLogger', + NamespacePrefix: 't3st', + FullName: 't3st__TestLogger' + }, + RunTime: 8, + TestTimestamp: '3' + } + ] + } as ApexTestResult); + + const testSrv = new TestService(mockConnection); + const result = await testSrv.getApexTestResults(testQueueItems); + + expect(mockToolingQuery.calledTwice).to.be.true; + expect(result.length).to.eql(2); + }); + + it('should make a single api call if query is under char limit', async () => { + const mockToolingQuery = sandboxStub.stub( + mockConnection.tooling, + 'query' + ); + mockToolingQuery.onFirstCall().resolves({ + done: true, + totalSize: 1, + records: [ + { + Id: '07Mxx00000F2Xx6UAF', + QueueItemId: '7092M000000Vt94QAC', + StackTrace: null, + Message: null, + AsyncApexJobId: testRunId, + MethodName: 'testLoggerLog', + Outcome: ApexTestResultOutcome.Pass, + ApexLogId: null, + ApexClass: { + Id: '01pxx00000O6tXZQAZ', + Name: 'TestLogger', + NamespacePrefix: 't3st', + FullName: 't3st__TestLogger' + }, + RunTime: 8, + TestTimestamp: '3' + } + ] + } as ApexTestResult); + + const testSrv = new TestService(mockConnection); + const result = await testSrv.getApexTestResults(pollResponse); + + expect(mockToolingQuery.calledOnce).to.be.true; + expect(result.length).to.eql(1); + }); + + it('should format multiple queries correctly', async () => { + const queryOneIds = queryIds.slice(0, 120).join("','"); + const queryOne = `${queryStart}('${queryOneIds}')`; + const queryTwoIds = queryIds.slice(120).join("','"); + const queryTwo = `${queryStart}('${queryTwoIds}')`; + + const testQueueItems: ApexTestQueueItem = { + done: true, + totalSize: 700, + records + }; + + const mockToolingQuery = sandboxStub.stub( + mockConnection.tooling, + 'query' + ); + mockToolingQuery.onFirstCall().resolves({ + done: true, + totalSize: 600, + records: [ + { + Id: '07Mxx00000F2Xx6UAF', + QueueItemId: '7092M000000Vt94QAC', + StackTrace: null, + Message: null, + AsyncApexJobId: testRunId, + MethodName: 'testLoggerLog', + Outcome: ApexTestResultOutcome.Pass, + ApexLogId: null, + ApexClass: { + Id: '01pxx00000O6tXZQAZ', + Name: 'TestLogger', + NamespacePrefix: 't3st', + FullName: 't3st__TestLogger' + }, + RunTime: 8, + TestTimestamp: '3' + } + ] + } as ApexTestResult); + mockToolingQuery.onSecondCall().resolves({ + done: true, + totalSize: 100, + records: [ + { + Id: '07Mxx00000F2Xx6UAF', + QueueItemId: '7092M000000Vt94QAC', + StackTrace: null, + Message: null, + AsyncApexJobId: testRunId, + MethodName: 'testLoggerLog', + Outcome: ApexTestResultOutcome.Pass, + ApexLogId: null, + ApexClass: { + Id: '01pxx00000O6tXZQAZ', + Name: 'TestLogger', + NamespacePrefix: 't3st', + FullName: 't3st__TestLogger' + }, + RunTime: 8, + TestTimestamp: '3' + } + ] + } as ApexTestResult); + + const testSrv = new TestService(mockConnection); + const result = await testSrv.getApexTestResults(testQueueItems); + + expect(mockToolingQuery.calledTwice).to.be.true; + expect(result.length).to.eql(2); + expect(mockToolingQuery.calledWith(queryOne)).to.be.true; + expect(mockToolingQuery.calledWith(queryTwo)).to.be.true; + }); + + it('should format query at query limit correctly', async () => { + const record = { + Id: '7092M000000Vt94QAC', + Status: ApexTestQueueItemStatus.Completed, + ApexClassId: '01p2M00000O6tXZQAZ', + TestRunResultId: '05m2M000000TgYuQAK' + }; + + const queryOneIds = queryIds.slice(0, 120).join("','"); + const queryOne = `${queryStart}('${queryOneIds}')`; + + const testQueueItems: ApexTestQueueItem = { + done: true, + totalSize: 700, + records + }; + + const mockToolingQuery = sandboxStub.stub( + mockConnection.tooling, + 'query' + ); + mockToolingQuery.onFirstCall().resolves({ + done: true, + totalSize: 600, + records: [ + { + Id: '07Mxx00000F2Xx6UAF', + QueueItemId: '7092M000000Vt94QAC', + StackTrace: null, + Message: null, + AsyncApexJobId: testRunId, + MethodName: 'testLoggerLog', + Outcome: ApexTestResultOutcome.Pass, + ApexLogId: null, + ApexClass: { + Id: '01pxx00000O6tXZQAZ', + Name: 'TestLogger', + NamespacePrefix: 't3st', + FullName: 't3st__TestLogger' + }, + RunTime: 8, + TestTimestamp: '3' + } + ] + } as ApexTestResult); + mockToolingQuery.onSecondCall().resolves({ + done: true, + totalSize: 100, + records: [ + { + Id: '07Mxx00000F2Xx6UAF', + QueueItemId: '7092M000000Vt94QAC', + StackTrace: null, + Message: null, + AsyncApexJobId: testRunId, + MethodName: 'testLoggerLog', + Outcome: ApexTestResultOutcome.Pass, + ApexLogId: null, + ApexClass: { + Id: '01pxx00000O6tXZQAZ', + Name: 'TestLogger', + NamespacePrefix: 't3st', + FullName: 't3st__TestLogger' + }, + RunTime: 8, + TestTimestamp: '3' + } + ] + } as ApexTestResult); + + const testSrv = new TestService(mockConnection); + const result = await testSrv.getApexTestResults(testQueueItems); + + expect(mockToolingQuery.calledTwice).to.be.true; + expect(result.length).to.eql(2); + expect(mockToolingQuery.calledWith(queryOne)).to.be.true; + expect(mockToolingQuery.calledWith(`${queryStart}('${record.Id}')`)); + }); + + it('should format single query correctly', async () => { + const mockToolingQuery = sandboxStub.stub( + mockConnection.tooling, + 'query' + ); + const id = '7092M000000Vt94QAC'; + mockToolingQuery.onFirstCall().resolves({ + done: true, + totalSize: 1, + records: [ + { + Id: '07Mxx00000F2Xx6UAF', + QueueItemId: id, + StackTrace: null, + Message: null, + AsyncApexJobId: testRunId, + MethodName: 'testLoggerLog', + Outcome: ApexTestResultOutcome.Pass, + ApexLogId: null, + ApexClass: { + Id: '01pxx00000O6tXZQAZ', + Name: 'TestLogger', + NamespacePrefix: 't3st', + FullName: 't3st__TestLogger' + }, + RunTime: 8, + TestTimestamp: '3' + } + ] + } as ApexTestResult); + const singleQuery = `${queryStart}('${id}')`; + + const testSrv = new TestService(mockConnection); + const result = await testSrv.getApexTestResults(pollResponse); + + expect(mockToolingQuery.calledOnce).to.be.true; + expect(mockToolingQuery.calledWith(singleQuery)).to.be.true; + expect(result.length).to.eql(1); + }); + }); }); diff --git a/packages/plugin-apex/src/commands/force/apex/test/run.ts b/packages/plugin-apex/src/commands/force/apex/test/run.ts index ceaa39ba..cf047201 100644 --- a/packages/plugin-apex/src/commands/force/apex/test/run.ts +++ b/packages/plugin-apex/src/commands/force/apex/test/run.ts @@ -29,6 +29,8 @@ export const TestLevel = [ export const resultFormat = ['human', 'tap', 'junit', 'json']; +const CLASS_ID_PREFIX = '01p'; + export function buildTestItem(testNames: string): TestItem[] { const testNameArray = testNames.split(','); const tItems = testNameArray.map(item => { @@ -153,9 +155,13 @@ export default class Run extends SfdxCommand { ); } } else { + const prop = this.flags.classnames + .toLowerCase() + .startsWith(CLASS_ID_PREFIX) + ? 'classId' + : 'className'; testOptions = { - tests: [], - classNames: this.flags.classnames, + tests: [{ [prop]: this.flags.classnames }], testLevel }; } diff --git a/packages/plugin-apex/test/commands/force/apex/test/run.test.ts b/packages/plugin-apex/test/commands/force/apex/test/run.test.ts index 313cf14b..9f2e6b34 100644 --- a/packages/plugin-apex/test/commands/force/apex/test/run.test.ts +++ b/packages/plugin-apex/test/commands/force/apex/test/run.test.ts @@ -247,66 +247,29 @@ describe('force:apex:test:run', () => { root: __dirname }) .stub(process, 'cwd', () => projectPath) - .stub(TestService.prototype, 'runTestAsynchronous', () => testRunSimple) + .do(ctx => { + ctx.myStub = sandboxStub.stub( + TestService.prototype, + 'runTestSynchronous' + ); + }) .stdout() .stderr() .command([ 'force:apex:test:run', - '--tests', - 'MyApexTests.testMethodOne', '--classnames', 'MyApexTests', - '--resultformat', - 'human' - ]) - .it('should throw an error if classnames and tests are specified', ctx => { - expect(ctx.stderr).to.contain(messages.getMessage('classSuiteTestErr')); - }); - - test - .withOrg({ username: TEST_USERNAME }, true) - .loadConfig({ - root: __dirname - }) - .stub(process, 'cwd', () => projectPath) - .stub(TestService.prototype, 'runTestAsynchronous', () => testRunSimple) - .stdout() - .stderr() - .command([ - 'force:apex:test:run', - '--tests', - 'MyApexTests.testMethodOne', - '--suitenames', - 'MyApexSuite', - '--resultformat', - 'human' - ]) - .it('should throw an error if suitenames and tests are specified', ctx => { - expect(ctx.stderr).to.contain(messages.getMessage('classSuiteTestErr')); - }); - - test - .withOrg({ username: TEST_USERNAME }, true) - .loadConfig({ - root: __dirname - }) - .stub(process, 'cwd', () => projectPath) - .stub(TestService.prototype, 'runTestAsynchronous', () => testRunSimple) - .stdout() - .stderr() - .command([ - 'force:apex:test:run', - '--tests', - 'MyApexTests.testMethodOne', - '--suitenames', - 'MyApexSuite', - '--resultformat', - 'human' + '--synchronous' ]) .it( - 'should throw an error if suitenames and classnames are specified', + 'should format request with correct properties for sync run with class name', ctx => { - expect(ctx.stderr).to.contain(messages.getMessage('classSuiteTestErr')); + expect( + ctx.myStub.calledWith({ + tests: [{ className: 'MyApexTests' }], + testLevel: 'RunSpecifiedTests' + }) + ).to.be.true; } ); @@ -316,43 +279,29 @@ describe('force:apex:test:run', () => { root: __dirname }) .stub(process, 'cwd', () => projectPath) - .stub(TestService.prototype, 'runTestAsynchronous', () => testRunSimple) - .stdout() - .stderr() - .command([ - 'force:apex:test:run', - '--tests', - 'MyApexTests.testMethodOne', - '-c' - ]) - .it( - 'should throw an error if code coverage is specified but reporter is missing', - ctx => { - expect(ctx.stderr).to.contain( - messages.getMessage('missingReporterErr') - ); - } - ); - - test - .withOrg({ username: TEST_USERNAME }, true) - .loadConfig({ - root: __dirname + .do(ctx => { + ctx.myStub = sandboxStub.stub( + TestService.prototype, + 'runTestSynchronous' + ); }) - .stub(process, 'cwd', () => projectPath) - .stub(TestService.prototype, 'runTestSynchronous', () => testRunSimple) .stdout() .stderr() .command([ 'force:apex:test:run', - '--suitenames', - 'MyApexSuite', + '--classnames', + '01p45678x123456', '--synchronous' ]) .it( - 'should throw an error if suitenames is specifed with sync run', + 'should format request with correct properties for sync run with class id', ctx => { - expect(ctx.stderr).to.contain(messages.getMessage('syncClassErr')); + expect( + ctx.myStub.calledWith({ + tests: [{ classId: '01p45678x123456' }], + testLevel: 'RunSpecifiedTests' + }) + ).to.be.true; } ); @@ -362,112 +311,278 @@ describe('force:apex:test:run', () => { root: __dirname }) .stub(process, 'cwd', () => projectPath) - .stub(TestService.prototype, 'runTestSynchronous', () => testRunSimple) + .do(ctx => { + ctx.myStub = sandboxStub.stub( + TestService.prototype, + 'runTestSynchronous' + ); + }) .stdout() .stderr() .command([ 'force:apex:test:run', - '--classnames', - 'MyApexClass,MySecondClass', + '--tests', + 'MyApexTests.testMethodOne', '--synchronous' ]) .it( - 'should throw an error if multiple classnames are specifed with sync run', + 'should format request with correct properties for sync run with tests', ctx => { - expect(ctx.stderr).to.contain(messages.getMessage('syncClassErr')); + expect( + ctx.myStub.calledWith({ + tests: [ + { + className: 'MyApexTests', + testMethods: ['testMethodOne'] + } + ], + testLevel: 'RunSpecifiedTests' + }) + ).to.be.true; } ); - test - .withOrg({ username: TEST_USERNAME }, true) - .loadConfig({ - root: __dirname - }) - .stub(process, 'cwd', () => projectPath) - .stub(TestService.prototype, 'runTestSynchronous', () => testRunSimple) - .stdout() - .stderr() - .command([ - 'force:apex:test:run', - '--suitenames', - 'MyApexSuite', - '--testlevel', - 'RunLocalTests' - ]) - .it( - 'should throw an error if test level is not "Run Specified Tests" for run with suites', - ctx => { - expect(ctx.stderr).to.contain(messages.getMessage('testLevelErr')); - } - ); + describe('Error checking', async () => { + test + .withOrg({ username: TEST_USERNAME }, true) + .loadConfig({ + root: __dirname + }) + .stub(process, 'cwd', () => projectPath) + .stub(TestService.prototype, 'runTestAsynchronous', () => testRunSimple) + .stdout() + .stderr() + .command([ + 'force:apex:test:run', + '--tests', + 'MyApexTests.testMethodOne', + '--classnames', + 'MyApexTests', + '--resultformat', + 'human' + ]) + .it( + 'should throw an error if classnames and tests are specified', + ctx => { + expect(ctx.stderr).to.contain( + messages.getMessage('classSuiteTestErr') + ); + } + ); - test - .withOrg({ username: TEST_USERNAME }, true) - .loadConfig({ - root: __dirname - }) - .stub(process, 'cwd', () => projectPath) - .stub(TestService.prototype, 'runTestSynchronous', () => testRunSimple) - .stdout() - .stderr() - .command([ - 'force:apex:test:run', - '--classnames', - 'MyApexClass', - '--synchronous', - '--testlevel', - 'RunAllTestsInOrg' - ]) - .it( - 'should throw an error if test level is not "Run Specified Tests" for run with classnames', - ctx => { - expect(ctx.stderr).to.contain(messages.getMessage('testLevelErr')); - } - ); + test + .withOrg({ username: TEST_USERNAME }, true) + .loadConfig({ + root: __dirname + }) + .stub(process, 'cwd', () => projectPath) + .stub(TestService.prototype, 'runTestAsynchronous', () => testRunSimple) + .stdout() + .stderr() + .command([ + 'force:apex:test:run', + '--tests', + 'MyApexTests.testMethodOne', + '--suitenames', + 'MyApexSuite', + '--resultformat', + 'human' + ]) + .it( + 'should throw an error if suitenames and tests are specified', + ctx => { + expect(ctx.stderr).to.contain( + messages.getMessage('classSuiteTestErr') + ); + } + ); - test - .withOrg({ username: TEST_USERNAME }, true) - .loadConfig({ - root: __dirname - }) - .stub(process, 'cwd', () => projectPath) - .stub(TestService.prototype, 'runTestSynchronous', () => testRunSimple) - .stdout() - .stderr() - .command([ - 'force:apex:test:run', - '--tests', - 'MyApexClass.testInsertTrigger', - '--synchronous', - '--testlevel', - 'RunAllTestsInOrg' - ]) - .it( - 'should throw an error if test level is not "Run Specified Tests" for run with tests', - ctx => { - expect(ctx.stderr).to.contain(messages.getMessage('testLevelErr')); - } - ); + test + .withOrg({ username: TEST_USERNAME }, true) + .loadConfig({ + root: __dirname + }) + .stub(process, 'cwd', () => projectPath) + .stub(TestService.prototype, 'runTestAsynchronous', () => testRunSimple) + .stdout() + .stderr() + .command([ + 'force:apex:test:run', + '--tests', + 'MyApexTests.testMethodOne', + '--suitenames', + 'MyApexSuite', + '--resultformat', + 'human' + ]) + .it( + 'should throw an error if suitenames and classnames are specified', + ctx => { + expect(ctx.stderr).to.contain( + messages.getMessage('classSuiteTestErr') + ); + } + ); - test - .withOrg({ username: TEST_USERNAME }, true) - .loadConfig({ - root: __dirname - }) - .stub(process, 'cwd', () => projectPath) - .stub(TestService.prototype, 'runTestSynchronous', () => testRunSimple) - .stdout() - .stderr() - .command([ - 'force:apex:test:run', - '--tests', - 'MyApexClass.testInsertTrigger,MySecondClass.testAfterTrigger', - '--synchronous' - ]) - .it( - 'should throw an error if test level is not "Run Specified Tests" for run with tests', - ctx => { - expect(ctx.stderr).to.contain(messages.getMessage('syncClassErr')); - } - ); + test + .withOrg({ username: TEST_USERNAME }, true) + .loadConfig({ + root: __dirname + }) + .stub(process, 'cwd', () => projectPath) + .stub(TestService.prototype, 'runTestAsynchronous', () => testRunSimple) + .stdout() + .stderr() + .command([ + 'force:apex:test:run', + '--tests', + 'MyApexTests.testMethodOne', + '-c' + ]) + .it( + 'should throw an error if code coverage is specified but reporter is missing', + ctx => { + expect(ctx.stderr).to.contain( + messages.getMessage('missingReporterErr') + ); + } + ); + + test + .withOrg({ username: TEST_USERNAME }, true) + .loadConfig({ + root: __dirname + }) + .stub(process, 'cwd', () => projectPath) + .stub(TestService.prototype, 'runTestSynchronous', () => testRunSimple) + .stdout() + .stderr() + .command([ + 'force:apex:test:run', + '--suitenames', + 'MyApexSuite', + '--synchronous' + ]) + .it( + 'should throw an error if suitenames is specifed with sync run', + ctx => { + expect(ctx.stderr).to.contain(messages.getMessage('syncClassErr')); + } + ); + + test + .withOrg({ username: TEST_USERNAME }, true) + .loadConfig({ + root: __dirname + }) + .stub(process, 'cwd', () => projectPath) + .stub(TestService.prototype, 'runTestSynchronous', () => testRunSimple) + .stdout() + .stderr() + .command([ + 'force:apex:test:run', + '--classnames', + 'MyApexClass,MySecondClass', + '--synchronous' + ]) + .it( + 'should throw an error if multiple classnames are specifed with sync run', + ctx => { + expect(ctx.stderr).to.contain(messages.getMessage('syncClassErr')); + } + ); + + test + .withOrg({ username: TEST_USERNAME }, true) + .loadConfig({ + root: __dirname + }) + .stub(process, 'cwd', () => projectPath) + .stub(TestService.prototype, 'runTestSynchronous', () => testRunSimple) + .stdout() + .stderr() + .command([ + 'force:apex:test:run', + '--suitenames', + 'MyApexSuite', + '--testlevel', + 'RunLocalTests' + ]) + .it( + 'should throw an error if test level is not "Run Specified Tests" for run with suites', + ctx => { + expect(ctx.stderr).to.contain(messages.getMessage('testLevelErr')); + } + ); + + test + .withOrg({ username: TEST_USERNAME }, true) + .loadConfig({ + root: __dirname + }) + .stub(process, 'cwd', () => projectPath) + .stub(TestService.prototype, 'runTestSynchronous', () => testRunSimple) + .stdout() + .stderr() + .command([ + 'force:apex:test:run', + '--classnames', + 'MyApexClass', + '--synchronous', + '--testlevel', + 'RunAllTestsInOrg' + ]) + .it( + 'should throw an error if test level is not "Run Specified Tests" for run with classnames', + ctx => { + expect(ctx.stderr).to.contain(messages.getMessage('testLevelErr')); + } + ); + + test + .withOrg({ username: TEST_USERNAME }, true) + .loadConfig({ + root: __dirname + }) + .stub(process, 'cwd', () => projectPath) + .stub(TestService.prototype, 'runTestSynchronous', () => testRunSimple) + .stdout() + .stderr() + .command([ + 'force:apex:test:run', + '--tests', + 'MyApexClass.testInsertTrigger', + '--synchronous', + '--testlevel', + 'RunAllTestsInOrg' + ]) + .it( + 'should throw an error if test level is not "Run Specified Tests" for run with tests', + ctx => { + expect(ctx.stderr).to.contain(messages.getMessage('testLevelErr')); + } + ); + + test + .withOrg({ username: TEST_USERNAME }, true) + .loadConfig({ + root: __dirname + }) + .stub(process, 'cwd', () => projectPath) + .stub(TestService.prototype, 'runTestSynchronous', () => testRunSimple) + .stdout() + .stderr() + .command([ + 'force:apex:test:run', + '--tests', + 'MyApexClass.testInsertTrigger,MySecondClass.testAfterTrigger', + '--synchronous' + ]) + .it( + 'should throw an error if test level is not "Run Specified Tests" for run with tests', + ctx => { + expect(ctx.stderr).to.contain(messages.getMessage('syncClassErr')); + } + ); + }); }); diff --git a/packages/plugin-apex/yarn.lock b/packages/plugin-apex/yarn.lock index 54387dd0..36f6e8b5 100644 --- a/packages/plugin-apex/yarn.lock +++ b/packages/plugin-apex/yarn.lock @@ -247,10 +247,10 @@ dependencies: fancy-test "^1.4.3" -"@salesforce/apex-node@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@salesforce/apex-node/-/apex-node-0.1.2.tgz#87ed9e2dfa1fcfec4af302666908aa71f3fe92c3" - integrity sha512-6kHCChtpsLo1vntm0LJSvkg2N3fBmDge0CY5XtRCzJTXyhH1wkPXQevtLqxLGm4neXK4jp//+9iNAzWEaBRHPg== +"@salesforce/apex-node@0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@salesforce/apex-node/-/apex-node-0.1.4.tgz#2c81757f564702f037e8f26bc2a1bcd5d3bfd912" + integrity sha512-J+Ib3wOl/kXAeUibb+XvA7uM/x0PuJLfGd9RZQ7DQifE7tEIRifxTN3JD4yHxYTdAxhyt/UqTPY1xilgoVf84w== dependencies: "@salesforce/core" "2.11.0" faye "1.1.3"