UNPKG

@salesforce/apex-node

Version:

Salesforce JS library for Apex

595 lines 24.7 kB
"use strict"; 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; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestService = exports.writeResultFiles = void 0; const types_1 = require("./types"); const path_1 = require("path"); const i18n_1 = require("../i18n"); const reporters_1 = require("../reporters"); const utils_1 = require("./utils"); const asyncTests_1 = require("./asyncTests"); const syncTests_1 = require("./syncTests"); const diagnosticUtil_1 = require("./diagnosticUtil"); const promises_1 = require("node:fs/promises"); const node_stream_1 = require("node:stream"); const streaming_1 = require("../streaming"); const utils_2 = require("../utils"); const narrowing_1 = require("../narrowing"); const promises_2 = require("node:stream/promises"); const node_fs_1 = require("node:fs"); const json_stream_stringify_1 = require("json-stream-stringify"); /** * Standalone function for writing test result files - easier to test */ const writeResultFiles = async (result, outputDirConfig, codeCoverage = false, runPipeline) => { const filesWritten = []; const { dirPath, resultFormats, fileInfos } = outputDirConfig; if (resultFormats && !resultFormats.every((format) => format in types_1.ResultFormat)) { throw new Error(i18n_1.nls.localize('resultFormatErr')); } await (0, promises_1.mkdir)(dirPath, { recursive: true }); const testRunId = (0, narrowing_1.isTestResult)(result) ? result.summary.testRunId : result.testRunId; try { await (0, promises_1.writeFile)((0, path_1.join)(dirPath, 'test-run-id.txt'), testRunId); filesWritten.push((0, path_1.join)(dirPath, 'test-run-id.txt')); } catch (err) { console.error(`Error writing file: ${err}`); } if (resultFormats && (0, narrowing_1.isTestResult)(result)) { for (const format of resultFormats) { let filePath; let readable; switch (format) { case types_1.ResultFormat.json: filePath = (0, path_1.join)(dirPath, `test-result-${testRunId || 'default'}.json`); readable = streaming_1.TestResultStringifyStream.fromTestResult(result, { bufferSize: (0, utils_1.getBufferSize)() }); break; case types_1.ResultFormat.tap: filePath = (0, path_1.join)(dirPath, `test-result-${testRunId}-tap.txt`); readable = new reporters_1.TapFormatTransformer(result, undefined, { bufferSize: (0, utils_1.getBufferSize)() }); break; case types_1.ResultFormat.junit: filePath = (0, path_1.join)(dirPath, `test-result-${testRunId || 'default'}-junit.xml`); readable = new reporters_1.JUnitFormatTransformer(result, { bufferSize: (0, utils_1.getBufferSize)() }); break; default: throw new Error(i18n_1.nls.localize('resultFormatErr')); } if (filePath && readable) { filesWritten.push(await runPipeline(readable, filePath)); } } } if (codeCoverage && (0, narrowing_1.isTestResult)(result)) { const filePath = (0, path_1.join)(dirPath, `test-result-${testRunId}-codecoverage.json`); const c = result.tests .map((record) => record.perClassCoverage) .filter((pcc) => pcc?.length); filesWritten.push(await runPipeline(new json_stream_stringify_1.JsonStreamStringify(c, null, (0, utils_1.getJsonIndent)()), filePath)); } if (fileInfos) { for (const fileInfo of fileInfos) { const filePath = (0, path_1.join)(dirPath, fileInfo.filename); const readable = typeof fileInfo.content === 'string' ? node_stream_1.Readable.from([fileInfo.content]) : new json_stream_stringify_1.JsonStreamStringify(fileInfo.content, null, (0, utils_1.getJsonIndent)()); filesWritten.push(await runPipeline(readable, filePath)); } } return filesWritten; }; exports.writeResultFiles = writeResultFiles; class TestService { connection; asyncService; syncService; constructor(connection) { this.connection = connection; this.syncService = new syncTests_1.SyncTests(connection); this.asyncService = new asyncTests_1.AsyncTests(connection); } /** * Retrieve all suites in org * @returns list of Suites in org */ async retrieveAllSuites() { const testSuiteRecords = (await this.connection.tooling.query(`SELECT id, TestSuiteName FROM ApexTestSuite`)); return testSuiteRecords.records; } async retrieveSuiteId(suitename) { const suiteResult = (await this.connection.tooling.query(`SELECT id FROM ApexTestSuite WHERE TestSuiteName = '${suitename}'`)); if (suiteResult.records.length === 0) { return undefined; } return suiteResult.records[0].Id; } /** * Retrive the ids for the given suites * @param suitenames names of suites * @returns Ids associated with each suite */ async getOrCreateSuiteIds(suitenames) { const suiteIds = suitenames.map(async (suite) => { const suiteId = await this.retrieveSuiteId(suite); if (suiteId === undefined) { const result = (await this.connection.tooling.create('ApexTestSuite', { TestSuiteName: suite })); return result.id; } return suiteId; }); return await Promise.all(suiteIds); } /** * Retrieves the test classes in a given suite * @param suitename name of suite * @param suiteId id of suite * @returns list of test classes in the suite */ async getTestsInSuite(suitename, suiteId) { if (suitename === undefined && suiteId === undefined) { throw new Error(i18n_1.nls.localize('suitenameErr')); } if (suitename) { suiteId = await this.retrieveSuiteId(suitename); if (suiteId === undefined) { throw new Error(i18n_1.nls.localize('missingSuiteErr')); } } const classRecords = (await this.connection.tooling.query(`SELECT ApexClassId FROM TestSuiteMembership WHERE ApexTestSuiteId = '${suiteId}'`)); return classRecords.records; } /** * Returns the associated Ids for each given Apex class * @param testClasses list of Apex class names * @returns the associated ids for each Apex class */ async getApexClassIds(testClasses) { const classIds = testClasses.map(async (testClass) => { const apexClass = (await this.connection.tooling.query(`SELECT id, name FROM ApexClass WHERE Name = '${testClass}'`)); if (apexClass.records.length === 0) { throw new Error(i18n_1.nls.localize('missingTestClassErr', testClass)); } return apexClass.records[0].Id; }); return await Promise.all(classIds); } /** * Builds a test suite with the given test classes. Creates the test suite if it doesn't exist already * @param suitename name of suite * @param testClasses */ async buildSuite(suitename, testClasses) { const testSuiteId = (await this.getOrCreateSuiteIds([suitename]))[0]; const classesInSuite = await this.getTestsInSuite(undefined, testSuiteId); const testClassIds = await this.getApexClassIds(testClasses); await Promise.all(testClassIds.map(async (classId) => { const existingClass = classesInSuite.filter((rec) => rec.ApexClassId === classId); const testClass = testClasses[testClassIds.indexOf(classId)]; if (existingClass.length > 0) { console.log(i18n_1.nls.localize('testSuiteMsg', [testClass, suitename])); } else { await this.connection.tooling.create('TestSuiteMembership', { ApexClassId: classId, ApexTestSuiteId: testSuiteId }); console.log(i18n_1.nls.localize('classSuiteMsg', [testClass, suitename])); } })); } /** * Synchronous Test Runs * @param options Synchronous Test Runs configuration * @param codeCoverage should report code coverage * @param token cancellation token */ async runTestSynchronous(options, codeCoverage = false, token) { utils_2.HeapMonitor.getInstance().startMonitoring(); try { return await this.syncService.runTests(options, codeCoverage, token); } finally { utils_2.HeapMonitor.getInstance().stopMonitoring(); } } /** * Asynchronous Test Runs * @param options test options * @param codeCoverage should report code coverage * @param immediatelyReturn should not wait for test run to complete, return test run id immediately * @param progress progress reporter * @param token cancellation token * @param timeout */ async runTestAsynchronous(options, codeCoverage = false, immediatelyReturn = false, progress, token, timeout, interval) { utils_2.HeapMonitor.getInstance().startMonitoring(); try { return await this.asyncService.runTests(options, codeCoverage, immediatelyReturn, progress, token, timeout, interval); } finally { utils_2.HeapMonitor.getInstance().stopMonitoring(); } } /** * 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_2.HeapMonitor.getInstance().startMonitoring(); try { return await this.asyncService.reportAsyncResults(testRunId, codeCoverage, token); } finally { utils_2.HeapMonitor.getInstance().stopMonitoring(); } } /** * * @param result test result * @param outputDirConfig config for result files * @param codeCoverage should report code coverage * @returns list of result files created */ async writeResultFiles(result, outputDirConfig, codeCoverage = false) { utils_2.HeapMonitor.getInstance().startMonitoring(); utils_2.HeapMonitor.getInstance().checkHeapSize('testService.writeResultFiles'); try { return await (0, exports.writeResultFiles)(result, outputDirConfig, codeCoverage, this.runPipeline.bind(this)); } finally { utils_2.HeapMonitor.getInstance().checkHeapSize('testService.writeResultFiles'); utils_2.HeapMonitor.getInstance().stopMonitoring(); } } /** * Utility function to help build payload for https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/intro_rest_resources_testing_runner_sync.htm */ async buildSyncPayload(testLevel, tests, classnames, /** * Category for this run, for Flow or Apex. Can be a single category or an array of categories. */ category, /** * Specifies whether to opt out of collecting code coverage information during the test run ("true") or to collect code coverage information ("false"). * @default false */ skipCodeCoverage = false) { try { if (tests) { const payload = await this.buildTestPayload(tests, skipCodeCoverage); const classes = payload.tests ?.filter((testItem) => testItem.className) .map((testItem) => testItem.className); if (new Set(classes).size !== 1) { throw new Error(i18n_1.nls.localize('syncClassErr')); } return payload; } else if (classnames) { if (this.hasCategory(category)) { const payload = await this.buildClassPayloadForFlow(classnames, skipCodeCoverage); const classes = (payload.tests || []) .filter((testItem) => testItem.className) .map((testItem) => testItem.className); if (new Set(classes).size !== 1) { throw new Error(i18n_1.nls.localize('syncClassErr')); } return payload; } else { const prop = (0, narrowing_1.isValidApexClassID)(classnames) ? 'classId' : 'className'; return { tests: [{ [prop]: classnames }], testLevel, skipCodeCoverage }; } } else if (this.hasCategory(category)) { return { testLevel, category: category.split(','), skipCodeCoverage }; } throw new Error(i18n_1.nls.localize('payloadErr')); } catch (e) { throw (0, diagnosticUtil_1.formatTestErrors)(e); } } /** * Utility function to help build payload for https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/intro_rest_resources_testing_runner_async.htm */ async buildAsyncPayload(testLevel, tests, classNames, suiteNames, /** * Category for this run, for Flow or Apex. Can be a single category or an array of categories. */ category, /** * Specifies whether to opt out of collecting code coverage information during the test run ("true") or to collect code coverage information ("false"). * @default false */ skipCodeCoverage = false) { try { if (tests) { return (await this.buildTestPayload(tests, skipCodeCoverage)); } else if (classNames) { if (this.hasCategory(category)) { return await this.buildClassPayloadForFlow(classNames, skipCodeCoverage); } else { return await this.buildAsyncClassPayload(classNames, skipCodeCoverage); } } else { return { suiteNames, testLevel, ...(this.hasCategory(category) && { category: category.split(',') }), skipCodeCoverage }; } } catch (e) { throw (0, diagnosticUtil_1.formatTestErrors)(e); } } async buildAsyncClassPayload(classNames, skipCodeCoverage) { const classNameArray = classNames.split(','); const classItems = classNameArray.map((item) => { const classParts = item.split('.'); if (classParts.length > 1) { return { className: `${classParts[0]}.${classParts[1]}` }; } const prop = (0, narrowing_1.isValidApexClassID)(item) ? 'classId' : 'className'; return { [prop]: item }; }); return { tests: classItems, testLevel: "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */, skipCodeCoverage }; } async buildClassPayloadForFlow(classNames, skipCodeCoverage) { const classNameArray = classNames.split(','); const classItems = classNameArray.map((item) => ({ className: item })); return { tests: classItems, testLevel: "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */, skipCodeCoverage }; } async buildTestPayload(testNames, skipCodeCoverage) { const testNameArray = testNames.split(','); const testItems = []; const classes = []; let namespaceInfos; for (const test of testNameArray) { if (test.indexOf('.') > 0) { const testParts = test.split('.'); const isFlow = (0, utils_1.isFlowTest)(test); if (isFlow) { namespaceInfos = await this.processFlowTest(testParts, testItems, classes, namespaceInfos); } else { namespaceInfos = await this.processApexTest(testParts, testItems, classes, namespaceInfos); } } else { const prop = (0, narrowing_1.isValidApexClassID)(test) ? 'classId' : 'className'; testItems.push({ [prop]: test }); } } return { tests: testItems, testLevel: "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */, skipCodeCoverage }; } async processFlowTest(testParts, testItems, classes, namespaceInfos) { if (testParts.length === 4) { // flowtesting.namespace.FlowName.testMethod const fullClassName = `${testParts[0]}.${testParts[1]}.${testParts[2]}`; if (!classes.includes(fullClassName)) { testItems.push({ namespace: `${testParts[0]}.${testParts[1]}`, className: fullClassName, testMethods: [testParts[3]] }); classes.push(fullClassName); } else { testItems.forEach((element) => { if (element.className === fullClassName) { element.namespace = `${testParts[0]}.${testParts[1]}`; element.testMethods.push(testParts[3]); } }); } } else { // Handle 3-part Flow tests: flowtesting.FlowName.testMethod if (typeof namespaceInfos === 'undefined') { namespaceInfos = await (0, utils_1.queryNamespaces)(this.connection); } const currentNamespace = namespaceInfos.find((namespaceInfo) => namespaceInfo.namespace === testParts[1]); if (currentNamespace) { if (currentNamespace.installedNs) { testItems.push({ className: `${testParts[0]}.${testParts[1]}.${testParts[2]}` }); } else { testItems.push({ namespace: `${testParts[0]}.${testParts[1]}`, className: `${testParts[0]}.${testParts[1]}.${testParts[2]}` }); } } else { const flowClassName = `${testParts[0]}.${testParts[1]}`; if (!classes.includes(flowClassName)) { testItems.push({ className: flowClassName, testMethods: [testParts[2]] }); classes.push(flowClassName); } else { testItems.forEach((element) => { if (element.className === flowClassName) { element.testMethods.push(testParts[2]); } }); } } } return namespaceInfos; } async processApexTest(testParts, testItems, classes, namespaceInfos) { if (testParts.length === 3) { // namespace.ClassName.testMethod if (!classes.includes(testParts[1])) { testItems.push({ namespace: `${testParts[0]}`, className: `${testParts[1]}`, testMethods: [testParts[2]] }); classes.push(testParts[1]); } else { testItems.forEach((element) => { if (element.className === `${testParts[1]}`) { element.namespace = `${testParts[0]}`; element.testMethods.push(`${testParts[2]}`); } }); } } else { // Handle 2-part Apex tests or namespace resolution if (typeof namespaceInfos === 'undefined') { namespaceInfos = await (0, utils_1.queryNamespaces)(this.connection); } const currentNamespace = namespaceInfos.find((namespaceInfo) => namespaceInfo.namespace === testParts[0]); // NOTE: Installed packages require the namespace to be specified as part of the className field // The namespace field should not be used with subscriber orgs if (currentNamespace) { if (currentNamespace.installedNs) { testItems.push({ className: `${testParts[0]}.${testParts[1]}` }); } else { testItems.push({ namespace: `${testParts[0]}`, className: `${testParts[1]}` }); } } else { if (!classes.includes(testParts[0])) { testItems.push({ className: testParts[0], testMethods: [testParts[1]] }); classes.push(testParts[0]); } else { testItems.forEach((element) => { if (element.className === testParts[0]) { element.testMethods.push(testParts[1]); } }); } } } return namespaceInfos; } async runPipeline(readable, filePath, transform) { const writable = this.createStream(filePath); if (transform) { await (0, promises_2.pipeline)(readable, transform, writable); } else { await (0, promises_2.pipeline)(readable, writable); } return filePath; } createStream(filePath) { return (0, node_fs_1.createWriteStream)(filePath, 'utf8'); } hasCategory(category) { return category && category.length !== 0; } } exports.TestService = TestService; __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "retrieveAllSuites", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "retrieveSuiteId", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "getOrCreateSuiteIds", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "getTestsInSuite", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "getApexClassIds", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "buildSuite", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "runTestSynchronous", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "runTestAsynchronous", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "reportAsyncResults", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "writeResultFiles", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "buildSyncPayload", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "buildAsyncPayload", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "buildAsyncClassPayload", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "buildClassPayloadForFlow", null); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "buildTestPayload", null); //# sourceMappingURL=testService.js.map