@salesforce/apex-node
Version:
Salesforce JS library for Apex
604 lines • 29.4 kB
JavaScript
;
/*
* Copyright (c) 2021, 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
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AsyncTests = void 0;
const core_1 = require("@salesforce/core");
const i18n_1 = require("../i18n");
const streaming_1 = require("../streaming");
const utils_1 = require("../utils");
const diagnosticUtil_1 = require("./diagnosticUtil");
const utils_2 = require("./utils");
const util = __importStar(require("util"));
const constants_1 = require("./constants");
const codeCoverage_1 = require("./codeCoverage");
const narrowing_1 = require("../narrowing");
const kit_1 = require("@salesforce/kit");
const node_fs_1 = require("node:fs");
const promises_1 = require("node:stream/promises");
const os = __importStar(require("node:os"));
const path_1 = __importDefault(require("path"));
const promises_2 = __importDefault(require("node:fs/promises"));
// eslint-disable-next-line @typescript-eslint/no-var-requires
const bfj = require('bfj');
const finishedStatuses = [
"Aborted" /* ApexTestRunResultStatus.Aborted */,
"Failed" /* ApexTestRunResultStatus.Failed */,
"Completed" /* ApexTestRunResultStatus.Completed */,
"Passed" /* ApexTestRunResultStatus.Passed */,
"Skipped" /* ApexTestRunResultStatus.Skipped */
];
const MIN_VERSION_TO_SUPPORT_TEST_SETUP_METHODS = 61.0;
const POLLING_FREQUENCY = kit_1.Duration.milliseconds(100);
const POLLING_TIMEOUT = kit_1.Duration.hours(4);
class AsyncTests {
connection;
codecoverage;
logger;
constructor(connection) {
this.connection = connection;
this.codecoverage = new codeCoverage_1.CodeCoverage(this.connection);
this.logger = core_1.Logger.childFromRoot('AsyncTests');
}
/**
* Asynchronous Test Runs
* @param options test options
* @param codeCoverage should report code coverage
* @param exitOnTestRunId should not wait for test run to complete, return test run id immediately
* @param progress progress reporter
* @param token cancellation token
* @param timeout Duration to wait before returning a TestRunIdResult. If the polling client times out,
* the method will return the test run ID so you can retrieve results later.
*/
async runTests(options, codeCoverage = false, exitOnTestRunId = false, progress, token, timeout) {
utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.runTests');
let testRunId;
try {
testRunId = await this.getTestRunRequestAction(options)();
if (exitOnTestRunId) {
return { testRunId };
}
if (token?.isCancellationRequested) {
return null;
}
const pollingClient = await core_1.PollingClient.create({
poll: async () => {
if (token?.isCancellationRequested) {
await this.abortTestRun(testRunId, progress);
return {
completed: true,
payload: {
done: true,
totalSize: 0,
records: []
}
};
}
progress?.report({
type: 'PollingClientProgress',
value: 'pollingProcessingTestRun',
message: i18n_1.nls.localize('pollingProcessingTestRun', testRunId),
testRunId
});
const hasTestSetupTimeField = await this.supportsTestSetupFeature();
const testRunSummaryQuery = hasTestSetupTimeField
? `SELECT AsyncApexJobId, Status, ClassesCompleted, ClassesEnqueued, MethodsEnqueued, StartTime, EndTime, TestTime, TestSetupTime, UserId FROM ApexTestRunResult WHERE AsyncApexJobId = '${testRunId}'`
: `SELECT AsyncApexJobId, Status, ClassesCompleted, ClassesEnqueued, MethodsEnqueued, StartTime, EndTime, TestTime, UserId FROM ApexTestRunResult WHERE AsyncApexJobId = '${testRunId}'`;
// Query for test run summary first to check overall status
const testRunSummary = await this.connection.tooling.query(testRunSummaryQuery);
if (!testRunSummary ||
!testRunSummary.records ||
testRunSummary.records.length === 0) {
throw new Error(`No test run summary found for test run ID: ${testRunId}`);
}
const summary = testRunSummary.records[0];
const isCompleted = finishedStatuses.includes(summary.Status);
// Query queue items to get detailed status
const queryResult = await this.connection.tooling.query(`SELECT Id, Status, ApexClassId, TestRunResultId, ParentJobId FROM ApexTestQueueItem WHERE ParentJobId = '${testRunId}'`);
if (!queryResult.records || queryResult.records.length === 0) {
throw new Error(`No test queue items found for test run ID: ${testRunId}`);
}
const queueItem = {
done: isCompleted,
totalSize: queryResult.records.length,
records: queryResult.records.map((record) => ({
Id: record.Id,
Status: record.Status,
ApexClassId: record.ApexClassId,
TestRunResultId: record.TestRunResultId
}))
};
return {
completed: isCompleted,
payload: queueItem
};
},
frequency: POLLING_FREQUENCY,
timeout: timeout ?? POLLING_TIMEOUT
});
const queueItem = (await pollingClient.subscribe());
const runResult = await this.checkRunStatus(testRunId);
const formattedResults = await this.formatAsyncResults({ runId: testRunId, queueItem }, (0, utils_1.getCurrentTime)(), codeCoverage, runResult.testRunSummary, progress);
await this.writeResultsToFile(formattedResults, testRunId);
return formattedResults;
}
catch (e) {
// If it's a PollingClientTimeout, return the test run ID so results can be retrieved later
if (e.name === 'PollingClientTimeout') {
this.logger.debug(`Polling client timed out for test run ${testRunId}. Returning test run ID for later result retrieval.`);
// Log the proper 'apex get test' command for the user to run later
const username = this.connection.getUsername();
this.logger.info(i18n_1.nls.localize('runTestReportCommand', [testRunId, username]));
return { testRunId };
}
throw (0, diagnosticUtil_1.formatTestErrors)(e);
}
finally {
utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.runTests');
}
}
async writeResultsToFile(formattedResults, runId) {
utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.writeResultsToFile');
try {
if (this.logger.shouldLog(core_1.LoggerLevel.DEBUG)) {
const rawResultsPath = path_1.default.join(os.tmpdir(), runId, 'rawResults.json');
await promises_2.default.mkdir(path_1.default.dirname(rawResultsPath), { recursive: true });
const writeStream = (0, node_fs_1.createWriteStream)(path_1.default.join(os.tmpdir(), runId, 'rawResults.json'));
this.logger.debug(`Raw results written to: ${writeStream.path}`);
const stringifyStream = bfj.stringify(formattedResults, {
bufferLength: (0, utils_2.getBufferSize)(),
iterables: 'ignore',
space: (0, utils_2.getJsonIndent)()
});
return await (0, promises_1.pipeline)(stringifyStream, writeStream);
}
}
finally {
utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.writeResultsToFile');
}
}
/**
* Report Asynchronous Test Run Results
* @param testRunId test run id
* @param codeCoverage should report code coverages
* @param token cancellation token
*/
async reportAsyncResults(testRunId, codeCoverage = false, token) {
utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.reportAsyncResults');
try {
const sClient = new streaming_1.StreamingClient(this.connection);
await sClient.init();
await sClient.handshake();
let queueItem;
let runResult = await this.checkRunStatus(testRunId);
if (runResult.testsComplete) {
queueItem = await sClient.handler(undefined, testRunId);
}
else {
queueItem = (await sClient.subscribe(undefined, testRunId)).queueItem;
runResult = await this.checkRunStatus(testRunId);
}
token?.onCancellationRequested(async () => {
sClient.disconnect();
});
if (token?.isCancellationRequested) {
return null;
}
const formattedResults = await this.formatAsyncResults({ queueItem, runId: testRunId }, (0, utils_1.getCurrentTime)(), codeCoverage, runResult.testRunSummary);
await this.writeResultsToFile(formattedResults, testRunId);
return formattedResults;
}
catch (e) {
throw (0, diagnosticUtil_1.formatTestErrors)(e);
}
finally {
utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.reportAsyncResults');
}
}
async checkRunStatus(testRunId, progress) {
if (!(0, narrowing_1.isValidTestRunID)(testRunId)) {
throw new Error(i18n_1.nls.localize('invalidTestRunIdErr', testRunId));
}
const hasTestSetupTimeField = await this.supportsTestSetupFeature();
const fields = [
'AsyncApexJobId',
'Status',
'ClassesCompleted',
'ClassesEnqueued',
'MethodsEnqueued',
'StartTime',
'EndTime',
'TestTime',
...(hasTestSetupTimeField ? ['TestSetupTime'] : []),
'UserId'
];
const testRunSummaryQuery = `SELECT ${fields.join(', ')} FROM ApexTestRunResult WHERE AsyncApexJobId = '${testRunId}'`;
progress?.report({
type: 'FormatTestResultProgress',
value: 'retrievingTestRunSummary',
message: i18n_1.nls.localize('retrievingTestRunSummary')
});
try {
const connection = await this.defineApiVersion();
const testRunSummaryResults = await connection.tooling.query(testRunSummaryQuery);
if (!testRunSummaryResults?.records) {
// If test run was aborted, return a dummy summary
if (progress?.report) {
return {
testsComplete: true,
testRunSummary: {
AsyncApexJobId: testRunId,
Status: "Aborted" /* ApexTestRunResultStatus.Aborted */,
StartTime: new Date().toISOString(),
TestTime: 0,
UserId: ''
}
};
}
throw new Error(`No test run summary found for test run ID: ${testRunId}. The test run may have been deleted or expired.`);
}
if (testRunSummaryResults.records.length > 1) {
throw new Error(`Multiple test run summaries found for test run ID: ${testRunId}. This is unexpected and may indicate a data integrity issue.`);
}
return {
testsComplete: finishedStatuses.includes(testRunSummaryResults.records[0].Status),
testRunSummary: testRunSummaryResults.records[0]
};
}
catch (e) {
if (e.message.includes('The requested resource does not exist')) {
throw new Error(`Test run with ID ${testRunId} does not exist. The test run may have been deleted, expired, or never created successfully.`);
}
throw new Error(i18n_1.nls.localize('noTestResultSummary', testRunId));
}
}
/**
* Format the results of a completed asynchronous test run
* @param asyncRunResult TestQueueItem and RunId for an async run
* @param commandStartTime start time for the async test run
* @param codeCoverage should report code coverages
* @param testRunSummary test run summary
* @param progress progress reporter
* @returns
*/
async formatAsyncResults(asyncRunResult, commandStartTime, codeCoverage = false, testRunSummary, progress) {
utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.formatAsyncResults');
try {
const apexTestResults = await this.getAsyncTestResults(asyncRunResult.queueItem);
const { apexTestClassIdSet, testResults, globalTests } = await this.buildAsyncTestResults(apexTestResults);
let outcome = testRunSummary.Status;
if (globalTests.failed > 0) {
outcome = "Failed" /* ApexTestRunResultStatus.Failed */;
}
else if (globalTests.passed === 0) {
outcome = "Skipped" /* ApexTestRunResultStatus.Skipped */;
}
else if (testRunSummary.Status === "Completed" /* ApexTestRunResultStatus.Completed */) {
outcome = "Passed" /* ApexTestRunResultStatus.Passed */;
}
const rawResult = {
summary: {
outcome,
testsRan: testResults.length,
passing: globalTests.passed,
failing: globalTests.failed,
skipped: globalTests.skipped,
passRate: (0, utils_2.calculatePercentage)(globalTests.passed, testResults.length),
failRate: (0, utils_2.calculatePercentage)(globalTests.failed, testResults.length),
skipRate: (0, utils_2.calculatePercentage)(globalTests.skipped, testResults.length),
testStartTime: (0, utils_1.formatStartTime)(testRunSummary.StartTime, 'ISO'),
testSetupTimeInMs: testRunSummary.TestSetupTime,
testExecutionTimeInMs: testRunSummary.TestTime ?? 0,
testTotalTimeInMs: testRunSummary.TestTime ?? 0,
commandTimeInMs: (0, utils_1.getCurrentTime)() - commandStartTime,
hostname: this.connection.instanceUrl,
orgId: this.connection.getAuthInfoFields().orgId,
username: this.connection.getUsername(),
testRunId: asyncRunResult.runId,
userId: testRunSummary.UserId
},
tests: testResults
};
await (0, utils_2.calculateCodeCoverage)(this.codecoverage, codeCoverage, apexTestClassIdSet, rawResult, true, progress);
return (0, utils_2.transformTestResult)(rawResult);
}
finally {
utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.formatAsyncResults');
}
}
async getAsyncTestResults(testQueueResult) {
utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.getAsyncTestResults');
const hasIsTestSetupField = await this.supportsTestSetupFeature();
try {
const resultIds = testQueueResult.records.map((record) => record.Id);
const isFlowRunTest = await this.isJobIdForFlowTestRun(resultIds[0]);
const testResultQuery = isFlowRunTest
? `SELECT Id, ApexTestQueueItemId, Result, TestStartDateTime,TestEndDateTime, FlowTest.DeveloperName, FlowDefinition.DeveloperName, FlowDefinition.NamespacePrefix FROM FlowTestResult WHERE ApexTestQueueItemId IN (%s)`
: hasIsTestSetupField
? `SELECT Id, QueueItemId, StackTrace, Message, RunTime, TestTimestamp, AsyncApexJobId, MethodName, Outcome, ApexLogId, IsTestSetup, ApexClass.Id, ApexClass.Name, ApexClass.NamespacePrefix FROM ApexTestResult WHERE QueueItemId IN (%s)`
: `SELECT Id, QueueItemId, StackTrace, Message, RunTime, TestTimestamp, AsyncApexJobId, MethodName, Outcome, ApexLogId, ApexClass.Id, ApexClass.Name, ApexClass.NamespacePrefix FROM ApexTestResult WHERE QueueItemId IN (%s)`;
// iterate thru ids, create query with id, & compare query length to char limit
const queries = [];
for (let i = 0; i < resultIds.length; i += constants_1.QUERY_RECORD_LIMIT) {
const recordSet = resultIds
.slice(i, i + constants_1.QUERY_RECORD_LIMIT)
.map((id) => `'${id}'`);
const query = util.format(testResultQuery, recordSet.join(','));
queries.push(query);
}
const connection = await this.defineApiVersion();
const queryPromises = queries.map(async (query) => (0, utils_2.queryAll)(connection, query, true));
const testResults = await Promise.all(queryPromises);
if (isFlowRunTest) {
return this.convertFlowTestResult(testResults);
}
return testResults;
}
finally {
utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.getAsyncTestResults');
}
}
/**
* @returns Convert FlowTest result to ApexTestResult type
*/
convertFlowTestResult(flowtestResults) {
return flowtestResults.map((flowtestResult) => {
const tmpRecords = flowtestResult.records.map((record) => ({
Id: record.Id,
QueueItemId: record.ApexTestQueueItemId,
StackTrace: '', // Default value
Message: '', // Default value
AsyncApexJobId: record.ApexTestQueueItemId, // Assuming this maps from ApexTestQueueItem
MethodName: record.FlowTest.DeveloperName,
Outcome: record.Result,
ApexLogId: '', // Default value
IsTestSetup: false,
ApexClass: {
Id: record.ApexTestQueueItemId,
Name: record.FlowDefinition.DeveloperName,
NamespacePrefix: record.FlowDefinition.NamespacePrefix,
FullName: record.FlowDefinition.NamespacePrefix
? `${record.FlowDefinition.NamespacePrefix}.${record.FlowTest.DeveloperName}`
: record.FlowTest.DeveloperName
},
RunTime: Number.isNaN(Number(record.TestEndDateTime))
? 0
: Number(record.TestEndDateTime) - Number(record.TestStartDateTime)
? 0
: Number(record.TestStartDateTime), // Default value, replace with actual runtime if available
TestTimestamp: record.TestStartDateTime
}));
return {
done: flowtestResult.done,
totalSize: tmpRecords.length,
records: tmpRecords
};
});
}
async buildAsyncTestResults(apexTestResults) {
utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.buildAsyncTestResults');
try {
const apexTestClassIdSet = new Set();
let passed = 0;
let failed = 0;
let skipped = 0;
// Iterate over test results, format and add them as results.tests
const testResults = [];
for (const result of apexTestResults) {
result.records.forEach((item) => {
switch (item.Outcome) {
case "Pass" /* ApexTestResultOutcome.Pass */:
passed++;
break;
case "Fail" /* ApexTestResultOutcome.Fail */:
case "CompileFail" /* ApexTestResultOutcome.CompileFail */:
failed++;
break;
case "Skip" /* ApexTestResultOutcome.Skip */:
skipped++;
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;
const diagnostic = item.Message || item.StackTrace ? (0, diagnosticUtil_1.getDiagnostic)(item) : null;
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,
isTestSetup: item.IsTestSetup,
apexClass: {
id: item.ApexClass.Id,
name: item.ApexClass.Name,
namespacePrefix: item.ApexClass.NamespacePrefix,
fullName: item.ApexClass.FullName
},
runTime: item.RunTime ?? 0,
testTimestamp: item.TestTimestamp, // TODO: convert timestamp
fullName: `${item.ApexClass.FullName}.${item.MethodName}`,
...(diagnostic ? { diagnostic } : {})
});
});
}
return {
apexTestClassIdSet,
testResults,
globalTests: { passed, failed, skipped }
};
}
finally {
utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.buildAsyncTestResults');
}
}
/**
* Abort test run with test run id
* @param testRunId
* @param progress
*/
async abortTestRun(testRunId, progress) {
progress?.report({
type: 'AbortTestRunProgress',
value: 'abortingTestRun',
message: i18n_1.nls.localize('abortingTestRun', testRunId),
testRunId
});
const testQueueItems = await this.connection.tooling.query(`SELECT Id, Status FROM ApexTestQueueItem WHERE ParentJobId = '${testRunId}'`);
for (const record of testQueueItems.records) {
record.Status = "Aborted" /* ApexTestQueueItemStatus.Aborted */;
}
await this.connection.tooling.update('ApexTestQueueItem', testQueueItems.records);
progress?.report({
type: 'AbortTestRunProgress',
value: 'abortingTestRunRequested',
message: i18n_1.nls.localize('abortingTestRunRequested', testRunId),
testRunId
});
}
getTestRunRequestAction(options) {
return async () => {
const url = `${this.connection.tooling._baseUrl()}/runTestsAsynchronous`;
try {
const testRunId = await this.connection.tooling.request({
method: 'POST',
url,
body: JSON.stringify(options),
headers: { 'content-type': 'application/json' }
});
return Promise.resolve(testRunId);
}
catch (e) {
return Promise.reject(e);
}
};
}
/**
* @returns A boolean indicating if the org's api version supports the test setup feature.
*/
async supportsTestSetupFeature() {
try {
return (parseFloat(await this.connection.retrieveMaxApiVersion()) >=
MIN_VERSION_TO_SUPPORT_TEST_SETUP_METHODS);
}
catch (e) {
throw new Error(`Error retrieving max api version`);
}
}
/**
* @returns A boolean indicating if this is running FlowTest.
*/
async isJobIdForFlowTestRun(testRunId) {
try {
const testRunApexIdResults = await this.connection.tooling.query(`SELECT ApexClassId FROM ApexTestQueueItem WHERE Id = '${testRunId}'`);
return testRunApexIdResults.records.some((record) => record.ApexClassId === null);
}
catch (e) {
return false;
}
}
/**
* @returns A connection based on the current api version and the max api version.
*/
async defineApiVersion() {
const maxApiVersion = await this.connection.retrieveMaxApiVersion();
if (parseFloat(this.connection.getApiVersion()) <
MIN_VERSION_TO_SUPPORT_TEST_SETUP_METHODS &&
this.supportsTestSetupFeature()) {
return await this.cloneConnectionWithNewVersion(maxApiVersion);
}
return this.connection;
}
/**
* @returns A new connection similar to the current one but with a new api version.
*/
async cloneConnectionWithNewVersion(newVersion) {
try {
const authInfo = await core_1.AuthInfo.create({
username: this.connection.getUsername()
});
const newConn = await core_1.Connection.create({
authInfo: authInfo,
connectionOptions: {
...this.connection.getConnectionOptions(),
version: newVersion
}
});
return newConn;
}
catch (e) {
throw new Error(`Error creating new connection with API version ${newVersion}: ${e.message}`);
}
}
}
exports.AsyncTests = AsyncTests;
__decorate([
(0, utils_1.elapsedTime)()
], AsyncTests.prototype, "runTests", null);
__decorate([
(0, utils_1.elapsedTime)()
], AsyncTests.prototype, "reportAsyncResults", null);
__decorate([
(0, utils_1.elapsedTime)()
], AsyncTests.prototype, "checkRunStatus", null);
__decorate([
(0, utils_1.elapsedTime)()
], AsyncTests.prototype, "formatAsyncResults", null);
__decorate([
(0, utils_1.elapsedTime)()
], AsyncTests.prototype, "getAsyncTestResults", null);
__decorate([
(0, utils_1.elapsedTime)()
], AsyncTests.prototype, "buildAsyncTestResults", null);
//# sourceMappingURL=asyncTests.js.map