From b9a5460410e5b2932f659028c8de795fa35fcd5d Mon Sep 17 00:00:00 2001 From: Xiaoyi Chen Date: Mon, 15 Mar 2021 11:54:04 -0700 Subject: [PATCH] feat: implement progress api (#159) * feat: implement progress api * feat: localized progress message * feat: retrievingTestRunSummary,queryingForAggregateCodeCoverage progress --- packages/apex-node/src/i18n/i18n.ts | 3 + packages/apex-node/src/index.ts | 1 + .../src/streaming/streamingClient.ts | 21 +++ packages/apex-node/src/tests/index.ts | 1 + packages/apex-node/src/tests/testService.ts | 18 ++- packages/apex-node/src/tests/types.ts | 22 ++- .../test/streaming/streamingClient.test.ts | 118 +++++++++++++- .../apex-node/test/tests/asyncTests.test.ts | 152 +++++++++++++++++- 8 files changed, 329 insertions(+), 7 deletions(-) diff --git a/packages/apex-node/src/i18n/i18n.ts b/packages/apex-node/src/i18n/i18n.ts index 2827ce7f..f99da793 100644 --- a/packages/apex-node/src/i18n/i18n.ts +++ b/packages/apex-node/src/i18n/i18n.ts @@ -36,6 +36,9 @@ export const messages = { streamingTransportUp: 'Listening for streaming state changes...', streamingTransportDown: 'Faye client generated a transport:down event.', streamingProcessingTestRun: 'Processing test run %s', + retrievingTestRunSummary: 'Retrieving test run summary record', + queryingForAggregateCodeCoverage: + 'Querying for aggregate code coverage results', failRate: 'Fail Rate', testsRan: 'Tests Ran', orgId: 'Org Id', diff --git a/packages/apex-node/src/index.ts b/packages/apex-node/src/index.ts index 5aa1cf6e..3c87a841 100644 --- a/packages/apex-node/src/index.ts +++ b/packages/apex-node/src/index.ts @@ -14,6 +14,7 @@ export { export { LogService, ApexLogGetOptions, LogRecord, LogResult } from './logs'; export { JUnitReporter, TapReporter, HumanReporter } from './reporters'; export { + ApexTestProgressValue, ApexTestResultData, ApexTestResultOutcome, AsyncTestArrayConfiguration, diff --git a/packages/apex-node/src/streaming/streamingClient.ts b/packages/apex-node/src/streaming/streamingClient.ts index 7b1e6440..d7ff966d 100644 --- a/packages/apex-node/src/streaming/streamingClient.ts +++ b/packages/apex-node/src/streaming/streamingClient.ts @@ -59,10 +59,20 @@ export class StreamingClient { }); this.client.on('transport:up', () => { + this.progress?.report({ + type: 'StreamingClientProgress', + value: 'streamingTransportUp', + message: nls.localize('streamingTransportUp') + }); console.log(nls.localize('streamingTransportUp')); }); this.client.on('transport:down', () => { + this.progress?.report({ + type: 'StreamingClientProgress', + value: 'streamingTransportDown', + message: nls.localize('streamingTransportDown') + }); console.log(nls.localize('streamingTransportDown')); }); @@ -167,6 +177,12 @@ export class StreamingClient { return result; } + this.progress?.report({ + type: 'StreamingClientProgress', + value: 'streamingProcessingTestRun', + message: nls.localize('streamingProcessingTestRun', testRunId), + testRunId + }); console.log(nls.localize('streamingProcessingTestRun', testRunId)); return null; } @@ -183,6 +199,11 @@ export class StreamingClient { throw new Error(nls.localize('noTestQueueResults', testRunId)); } + this.progress?.report({ + type: 'TestQueueProgress', + value: result + }); + for (let i = 0; i < result.records.length; i++) { const item = result.records[i]; if ( diff --git a/packages/apex-node/src/tests/index.ts b/packages/apex-node/src/tests/index.ts index 65434357..a2dcf533 100644 --- a/packages/apex-node/src/tests/index.ts +++ b/packages/apex-node/src/tests/index.ts @@ -7,6 +7,7 @@ export { TestService } from './testService'; export { + ApexTestProgressValue, AsyncTestConfiguration, AsyncTestArrayConfiguration, SyncTestConfiguration, diff --git a/packages/apex-node/src/tests/testService.ts b/packages/apex-node/src/tests/testService.ts index 5230bfde..4ab0d014 100644 --- a/packages/apex-node/src/tests/testService.ts +++ b/packages/apex-node/src/tests/testService.ts @@ -405,7 +405,8 @@ export class TestService { asyncRunResult.queueItem, asyncRunResult.runId, getCurrentTime(), - codeCoverage + codeCoverage, + progress ); } @@ -439,7 +440,8 @@ export class TestService { testQueueResult: ApexTestQueueItem, testRunId: string, commandStartTime: number, - codeCoverage = false + codeCoverage = false, + progress?: Progress ): Promise { if (!this.isValidTestRunID(testRunId)) { throw new Error(nls.localize('invalidTestRunIdErr', testRunId)); @@ -450,6 +452,13 @@ export class TestService { testRunSummaryQuery += 'MethodsEnqueued, StartTime, EndTime, TestTime, UserId '; testRunSummaryQuery += `FROM ApexTestRunResult WHERE AsyncApexJobId = '${testRunId}'`; + + progress?.report({ + type: 'FormatTestResultProgress', + value: 'retrievingTestRunSummary', + message: nls.localize('retrievingTestRunSummary') + }); + const testRunSummaryResults = (await this.connection.tooling.query( testRunSummaryQuery )) as ApexTestRunResult; @@ -526,6 +535,11 @@ export class TestService { } }); + progress?.report({ + type: 'FormatTestResultProgress', + value: 'queryingForAggregateCodeCoverage', + message: nls.localize('queryingForAggregateCodeCoverage') + }); const { codeCoverageResults, totalLines, diff --git a/packages/apex-node/src/tests/types.ts b/packages/apex-node/src/tests/types.ts index 9951abb7..b3a57809 100644 --- a/packages/apex-node/src/tests/types.ts +++ b/packages/apex-node/src/tests/types.ts @@ -462,4 +462,24 @@ export type NamespaceQueryResult = { records: NamespaceRecord[]; }; -export type ApexTestProgressValue = ApexTestRunResultRecord; +export type ApexTestProgressValue = + | { + type: 'StreamingClientProgress'; + value: 'streamingTransportUp' | 'streamingTransportDown'; + message: string; + } + | { + type: 'StreamingClientProgress'; + value: 'streamingProcessingTestRun'; + testRunId: string; + message: string; + } + | { + type: 'TestQueueProgress'; + value: ApexTestQueueItem; + } + | { + type: 'FormatTestResultProgress'; + value: 'retrievingTestRunSummary' | 'queryingForAggregateCodeCoverage'; + message: string; + }; diff --git a/packages/apex-node/test/streaming/streamingClient.test.ts b/packages/apex-node/test/streaming/streamingClient.test.ts index d5ff562d..d4a2b122 100644 --- a/packages/apex-node/test/streaming/streamingClient.test.ts +++ b/packages/apex-node/test/streaming/streamingClient.test.ts @@ -7,14 +7,19 @@ import { AuthInfo, Connection } from '@salesforce/core'; import { MockTestOrgData, testSetup } from '@salesforce/core/lib/testSetup'; -import { createSandbox, SinonSandbox } from 'sinon'; +import { assert, createSandbox, SinonSandbox } from 'sinon'; import { StreamingClient } from '../../src/streaming'; import { expect } from 'chai'; import { Client as FayeClient, Subscription } from 'faye'; import { fail } from 'assert'; +import { Progress } from '../../src'; import { TestResultMessage } from '../../src/streaming/types'; -import { ApexTestQueueItemStatus } from '../../src/tests/types'; +import { + ApexTestQueueItemStatus, + ApexTestProgressValue +} from '../../src/tests/types'; import { nls } from '../../src/i18n'; +import { EventEmitter } from 'events'; const $$ = testSetup(); let mockConnection: Connection; @@ -233,4 +238,113 @@ describe('Streaming API Client', () => { expect(mockToolingQuery.calledOnce).to.equal(true); expect(results).to.equal(null); }); + + it('should report streamingTransportUp progress', () => { + const reportStub = sandboxStub.stub(); + const progressReporter: Progress = { + report: reportStub + }; + const mockFayeClient = new EventEmitter(); + const stubOn = sandboxStub.stub(FayeClient.prototype, 'on'); + stubOn.callsFake(mockFayeClient.on.bind(mockFayeClient)); + + new StreamingClient(mockConnection, progressReporter); + mockFayeClient.emit('transport:up'); + + assert.calledOnce(reportStub); + assert.calledWith(reportStub, { + type: 'StreamingClientProgress', + value: 'streamingTransportUp', + message: nls.localize('streamingTransportUp') + }); + }); + + it('should report streamingTransportDown progress', () => { + const reportStub = sandboxStub.stub(); + const progressReporter: Progress = { + report: reportStub + }; + const mockFayeClient = new EventEmitter(); + const stubOn = sandboxStub.stub(FayeClient.prototype, 'on'); + stubOn.callsFake(mockFayeClient.on.bind(mockFayeClient)); + + new StreamingClient(mockConnection, progressReporter); + mockFayeClient.emit('transport:down'); + + assert.calledOnce(reportStub); + assert.calledWith(reportStub, { + type: 'StreamingClientProgress', + value: 'streamingTransportDown', + message: nls.localize('streamingTransportDown') + }); + }); + + it('should report streamingProcessingTestRun progress', async () => { + const mockToolingQuery = sandboxStub.stub(mockConnection.tooling, 'query'); + mockToolingQuery.resolves({ + done: true, + totalSize: 0, + records: [ + { + Id: '707xx0000AGQ3jbQQD', + Status: ApexTestQueueItemStatus.Processing, + ApexClassId: '01pxx00000O6tXZQAx', + TestRunResultId: '05mxx000000TgYuQAw' + } + ] + }); + const reportStub = sandboxStub.stub(); + const progressReporter: Progress = { + report: reportStub + }; + + const streamClient = new StreamingClient(mockConnection, progressReporter); + await streamClient.handler(testResultMsg); + + assert.calledWith(reportStub, { + type: 'StreamingClientProgress', + value: 'streamingProcessingTestRun', + message: nls.localize('streamingProcessingTestRun', '707xx0000AGQ3jbQQD'), + testRunId: '707xx0000AGQ3jbQQD' + }); + }); + + it('should report test queue progress', async () => { + const mockToolingQuery = sandboxStub.stub(mockConnection.tooling, 'query'); + mockToolingQuery.resolves({ + done: true, + totalSize: 0, + records: [ + { + Id: '707xx0000AGQ3jbQQD', + Status: ApexTestQueueItemStatus.Processing, + ApexClassId: '01pxx00000O6tXZQAx', + TestRunResultId: '05mxx000000TgYuQAw' + } + ] + }); + const reportStub = sandboxStub.stub(); + const progressReporter: Progress = { + report: reportStub + }; + + const streamClient = new StreamingClient(mockConnection, progressReporter); + await streamClient.handler(testResultMsg); + + assert.calledWith(reportStub, { + type: 'TestQueueProgress', + value: { + done: true, + totalSize: 0, + records: [ + { + Id: '707xx0000AGQ3jbQQD', + Status: ApexTestQueueItemStatus.Processing, + ApexClassId: '01pxx00000O6tXZQAx', + TestRunResultId: '05mxx000000TgYuQAw' + } + ] + } + }); + }); }); diff --git a/packages/apex-node/test/tests/asyncTests.test.ts b/packages/apex-node/test/tests/asyncTests.test.ts index e9c94209..a5931a2f 100644 --- a/packages/apex-node/test/tests/asyncTests.test.ts +++ b/packages/apex-node/test/tests/asyncTests.test.ts @@ -7,7 +7,13 @@ import { AuthInfo, Connection } from '@salesforce/core'; import { MockTestOrgData, testSetup } from '@salesforce/core/lib/testSetup'; import { assert, expect } from 'chai'; -import { createSandbox, SinonSandbox, SinonSpy, SinonStub } from 'sinon'; +import { + assert as sinonAssert, + createSandbox, + SinonSandbox, + SinonSpy, + SinonStub +} from 'sinon'; import { TestService, OutputDirConfig } from '../../src/tests'; import { AsyncTestConfiguration, @@ -42,7 +48,12 @@ import { import { join } from 'path'; import * as stream from 'stream'; import * as fs from 'fs'; -import { JUnitReporter, TapReporter } from '../../src'; +import { + JUnitReporter, + TapReporter, + Progress, + ApexTestProgressValue +} from '../../src'; const $$ = testSetup(); let mockConnection: Connection; @@ -216,6 +227,67 @@ describe('Run Apex tests asynchronously', () => { expect(getTestResultData).to.deep.equals(missingTimeTestData); }); + it('should report progress for formatting async results', async () => { + const testSrv = new TestService(mockConnection); + const mockToolingQuery = sandboxStub.stub(mockConnection.tooling, 'query'); + mockToolingQuery.onFirstCall().resolves({ + done: true, + totalSize: 1, + records: [ + { + AsyncApexJobId: testRunId, + Status: ApexTestRunResultStatus.Completed, + StartTime: testStartTime, + TestTime: null, + UserId: '005xx000000abcDAAU' + } + ] + } as ApexTestRunResult); + mockToolingQuery.onSecondCall().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: null, + TestTimestamp: '3' + } + ] + } as ApexTestResult); + const reportStub = sandboxStub.stub(); + const progressReporter: Progress = { + report: reportStub + }; + + await testSrv.formatAsyncResults( + pollResponse, + testRunId, + new Date().getTime(), + false, + progressReporter + ); + + sinonAssert.calledOnce(reportStub); + sinonAssert.calledWith(reportStub, { + type: 'FormatTestResultProgress', + value: 'retrievingTestRunSummary', + message: nls.localize('retrievingTestRunSummary') + }); + }); + it('should return correct summary outcome for single skipped test', async () => { skippedTestData.summary.orgId = mockConnection.getAuthInfoFields().orgId; skippedTestData.summary.username = mockConnection.getUsername(); @@ -532,6 +604,82 @@ describe('Run Apex tests asynchronously', () => { expect(getTestResultData.codecoverage.length).to.equal(3); }); + it('should report progress for aggregating code coverage', () => { + it('should return formatted test results with code coverage', async () => { + const testSrv = new TestService(mockConnection); + const mockToolingQuery = sandboxStub.stub( + mockConnection.tooling, + 'query' + ); + mockToolingQuery.onCall(0).resolves({ + done: true, + totalSize: 1, + records: [ + { + AsyncApexJobId: testRunId, + Status: ApexTestRunResultStatus.Completed, + StartTime: '2020-07-12T02:54:47.000+0000', + TestTime: 1765, + UserId: '005xx000000abcDAAU' + } + ] + } as ApexTestRunResult); + + mockToolingQuery.onCall(1).resolves({ + done: true, + totalSize: 6, + records: mixedTestResults + } as ApexTestResult); + + mockToolingQuery.onCall(2).resolves({ + done: true, + totalSize: 3, + records: mixedPerClassCodeCoverage + } as ApexCodeCoverage); + + mockToolingQuery.onCall(3).resolves({ + done: true, + totalSize: 3, + records: codeCoverageQueryResult + } as ApexCodeCoverageAggregate); + + mockToolingQuery.onCall(4).resolves({ + done: true, + totalSize: 1, + records: [ + { + PercentCovered: '57' + } + ] + } as ApexOrgWideCoverage); + + const reportStub = sandboxStub.stub(); + const progressReporter: Progress = { + report: reportStub + }; + + await testSrv.formatAsyncResults( + pollResponse, + testRunId, + new Date().getTime(), + true, + progressReporter + ); + + sinonAssert.calledTwice(reportStub); + sinonAssert.calledWith(reportStub, { + type: 'FormatTestResultProgress', + value: 'retrievingTestRunSummary', + message: nls.localize('retrievingTestRunSummary') + }); + sinonAssert.calledWith(reportStub, { + type: 'FormatTestResultProgress', + value: 'queryingForAggregateCodeCoverage', + message: nls.localize('queryingForAggregateCodeCoverage') + }); + }); + }); + 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 ';