UNPKG

@salesforce/apex-node

Version:

Salesforce JS library for Apex

528 lines 24.8 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 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; 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 */ async runTests(options, codeCoverage = false, exitOnTestRunId = false, progress, token, timeout) { utils_1.HeapMonitor.getInstance().checkHeapSize('asyncTests.runTests'); try { const sClient = new streaming_1.StreamingClient(this.connection, progress); await sClient.init(); await sClient.handshake(); token?.onCancellationRequested(async () => { const testRunId = await sClient.subscribedTestRunIdPromise; await this.abortTestRun(testRunId, progress); sClient.disconnect(); }); const testRunId = await this.getTestRunRequestAction(options)(); if (exitOnTestRunId) { return { testRunId }; } if (token?.isCancellationRequested) { return null; } const asyncRunResult = await sClient.subscribe(undefined, testRunId, timeout); if ('testRunId' in asyncRunResult) { // timeout, return the id return { testRunId }; } const runResult = await this.checkRunStatus(asyncRunResult.runId); const formattedResults = await this.formatAsyncResults(asyncRunResult, (0, utils_1.getCurrentTime)(), codeCoverage, runResult.testRunSummary, progress); await this.writeResultsToFile(formattedResults, asyncRunResult.runId); return formattedResults; } catch (e) { 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 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}'`; progress?.report({ type: 'FormatTestResultProgress', value: 'retrievingTestRunSummary', message: i18n_1.nls.localize('retrievingTestRunSummary') }); try { const testRunSummaryResults = await (await this.defineApiVersion()).singleRecordQuery(testRunSummaryQuery, { tooling: true }); return { testsComplete: finishedStatuses.includes(testRunSummaryResults.Status), testRunSummary: testRunSummaryResults }; } catch (e) { 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) => { return (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.getAsyncDiagnostic)(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) { const apexIdQuery = `SELECT ApexClassId FROM ApexTestQueueItem WHERE Id = '${testRunId}'`; try { const testRunApexIdResults = await this.connection.tooling.query(apexIdQuery); if (testRunApexIdResults.records.length > 0) { for (const record of testRunApexIdResults.records) { if (record.ApexClassId === null) { return true; } } return false; } return false; } 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