UNPKG

@salesforce/apex-node

Version:

Salesforce JS library for Apex

604 lines 29.4 kB
"use strict"; /* * 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