Skip to content

Commit

Permalink
feat: Check for query limits before getting apex test results (#92)
Browse files Browse the repository at this point in the history
@W-8379096@
  • Loading branch information
AnanyaJha authored and sfsholden committed Nov 20, 2020
1 parent d2b645d commit 0bf9940
Show file tree
Hide file tree
Showing 5 changed files with 705 additions and 233 deletions.
139 changes: 86 additions & 53 deletions packages/apex-node/src/tests/testService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<ApexTestResult[]> {
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<ApexTestResult>;
});
const apexTestResults = await Promise.all(queryPromises);
return apexTestResults;
}

public async getTestResultData(
testQueueResult: ApexTestQueueItem,
testRunId: string,
Expand All @@ -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;
Expand All @@ -237,46 +268,48 @@ export class TestService {
const coveredApexClassIdSet = new Set<string>();
// 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(
Expand Down
Loading

0 comments on commit 0bf9940

Please sign in to comment.