UNPKG

@salesforce/apex-node

Version:

Salesforce JS library for Apex

590 lines 25.8 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 = 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"); /** * The library jsonpath that bfj depends on cannot be bundled through esbuild. * Please pay attention whenever you deal with bfj */ // eslint-disable-next-line @typescript-eslint/no-var-requires const bfj = require('bfj'); 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) { utils_2.HeapMonitor.getInstance().startMonitoring(); try { return await this.asyncService.runTests(options, codeCoverage, immediatelyReturn, progress, token, timeout); } 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 { 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) { if (!(0, narrowing_1.isTestResult)(result)) { throw new Error(i18n_1.nls.localize('runIdFormatErr')); } 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; } if (filePath && readable) { filesWritten.push(await this.runPipeline(readable, filePath)); } } } if (codeCoverage) { if (!(0, narrowing_1.isTestResult)(result)) { throw new Error(i18n_1.nls.localize('covIdFormatErr')); } 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 this.runPipeline(bfj.stringify(c, { bufferLength: (0, utils_1.getBufferSize)(), iterables: 'ignore', space: (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]) : bfj.stringify(fileInfo.content, { bufferLength: (0, utils_1.getBufferSize)(), iterables: 'ignore', space: (0, utils_1.getJsonIndent)() }); filesWritten.push(await this.runPipeline(readable, filePath)); } } return filesWritten; } finally { utils_2.HeapMonitor.getInstance().checkHeapSize('testService.writeResultFiles'); utils_2.HeapMonitor.getInstance().stopMonitoring(); } } // utils to build test run payloads that may contain namespaces async buildSyncPayload(testLevel, tests, classnames, category) { try { if (tests) { let payload; if (this.isFlowTest(category)) { payload = await this.buildTestPayloadForFlow(tests); } else { payload = await this.buildTestPayload(tests); } 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.isFlowTest(category)) { const payload = await this.buildClassPayloadForFlow(classnames); 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 }; } } throw new Error(i18n_1.nls.localize('payloadErr')); } catch (e) { throw (0, diagnosticUtil_1.formatTestErrors)(e); } } async buildAsyncPayload(testLevel, tests, classNames, suiteNames, category) { try { if (tests) { if (this.isFlowTest(category)) { return (await this.buildTestPayloadForFlow(tests)); } else { return (await this.buildTestPayload(tests)); } } else if (classNames) { if (this.isFlowTest(category)) { return await this.buildClassPayloadForFlow(classNames); } else { return await this.buildAsyncClassPayload(classNames); } } else { if (this.isFlowTest(category)) { return { suiteNames, testLevel, category }; } return { suiteNames, testLevel }; } } catch (e) { throw (0, diagnosticUtil_1.formatTestErrors)(e); } } async buildAsyncClassPayload(classNames) { 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 */ }; } async buildClassPayloadForFlow(classNames) { const classNameArray = classNames.split(','); const classItems = classNameArray.map((item) => ({ className: item })); return { tests: classItems, testLevel: "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */ }; } async buildTestPayload(testNames) { const testNameArray = testNames.split(','); const testItems = []; const classes = []; let namespaceInfos; for (const test of testNameArray) { if (test.indexOf('.') > 0) { const testParts = test.split('.'); if (testParts.length === 3) { 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 { 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]); } }); } } } } else { const prop = (0, narrowing_1.isValidApexClassID)(test) ? 'classId' : 'className'; testItems.push({ [prop]: test }); } } return { tests: testItems, testLevel: "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */ }; } async buildTestPayloadForFlow(testNames) { const testNameArray = testNames.split(','); const testItems = []; const classes = []; let namespaceInfos; for (const test of testNameArray) { if (test.indexOf('.') > 0) { const testParts = test.split('.'); if (testParts.length === 4) { // for flow test, we will prefix flowtesting global namespace always. so, test string will be look like: // flowtesting.myNamespace.myFlow.myTest // the class name is always the full string including all the namespaces, eg: flowtesting.myNamespace.myFlow. if (!classes.includes(`${testParts[0]}.${testParts[1]}.${testParts[2]}`)) { testItems.push({ namespace: `${testParts[0]}.${testParts[1]}`, className: `${testParts[0]}.${testParts[1]}.${testParts[2]}`, testMethods: [testParts[3]] }); classes.push(`${testParts[0]}.${testParts[1]}.${testParts[2]}`); } else { testItems.forEach((element) => { if (element.className === `${testParts[0]}.${testParts[1]}.${testParts[2]}`) { element.namespace = `${testParts[0]}.${testParts[1]}`; element.testMethods.push(`${testParts[3]}`); } }); } } else { if (typeof namespaceInfos === 'undefined') { namespaceInfos = await (0, utils_1.queryNamespaces)(this.connection); } const currentNamespace = namespaceInfos.find((namespaceInfo) => namespaceInfo.namespace === testParts[1]); // 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) { // with installed namespace, no need to push namespace separately to tooling layer if (currentNamespace.installedNs) { testItems.push({ className: `${testParts[0]}.${testParts[1]}.${testParts[2]}` }); } else { // pushing namespace as part of the payload testItems.push({ namespace: `${testParts[0]}.${testParts[1]}`, className: `${testParts[0]}.${testParts[1]}.${testParts[2]}` }); } } else { if (!classes.includes(`${testParts[0]}.${testParts[1]}`)) { testItems.push({ className: `${testParts[0]}.${testParts[1]}`, testMethods: [testParts[2]] }); classes.push(`${testParts[0]}.${testParts[1]}`); } else { testItems.forEach((element) => { if (element.className === `${testParts[0]}.${testParts[1]}`) { element.testMethods.push(testParts[2]); } }); } } } } else { const prop = (0, narrowing_1.isValidApexClassID)(test) ? 'classId' : 'className'; testItems.push({ [prop]: test }); } } return { tests: testItems, testLevel: "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */ }; } 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'); } isFlowTest(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); __decorate([ (0, utils_2.elapsedTime)() ], TestService.prototype, "buildTestPayloadForFlow", null); //# sourceMappingURL=testService.js.map