UNPKG

@salesforce/apex-node

Version:

Salesforce JS library for Apex

629 lines 26.3 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; case types_1.ResultFormat.markdown: filePath = (0, path_1.join)(dirPath, `test-result-${testRunId || 'default'}.md`); readable = new reporters_1.MarkdownTextFormatTransformer(result, { bufferSize: (0, utils_1.getBufferSize)(), format: 'markdown', codeCoverage }); break; case types_1.ResultFormat.text: filePath = (0, path_1.join)(dirPath, `test-result-${testRunId || 'default'}.txt`); readable = new reporters_1.MarkdownTextFormatTransformer(result, { bufferSize: (0, utils_1.getBufferSize)(), format: 'text', codeCoverage }); 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; /** * Resolves namespace for a class name and returns a TestItem * @param className - Class name, potentially with namespace (e.g., "ns.ClassName" or "ClassName") * @returns TestItem with properly formatted namespace and className fields */ const resolveClassNamespace = (className) => { const classParts = className.split('.'); if (classParts.length > 1) { // Has namespace - always use full className format // This works for both installed packages and org namespaces const [namespace, classNamePart] = classParts; return { className: `${namespace}.${classNamePart}` }; } else { // No namespace - single class name if ((0, narrowing_1.isValidApexClassID)(className)) { return { classId: className }; } return { className }; } }; 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 { return await this.buildSyncClassPayload(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 this.buildClassPayloadForFlow(classNames, skipCodeCoverage); } else { return this.buildAsyncClassPayload(classNames, skipCodeCoverage); } } else { return { suiteNames, testLevel, ...(this.hasCategory(category) && { category: category.split(',') }), skipCodeCoverage }; } } catch (e) { throw (0, diagnosticUtil_1.formatTestErrors)(e); } } buildSyncClassPayload(className, testLevel, skipCodeCoverage) { const testItem = resolveClassNamespace(className); return { tests: [testItem], testLevel, skipCodeCoverage }; } buildAsyncClassPayload(classNames, skipCodeCoverage) { const classItems = classNames .split(',') .map((className) => resolveClassNamespace(className)); return { tests: classItems, testLevel: "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */, skipCodeCoverage }; } buildClassPayloadForFlow(classNames, skipCodeCoverage) { const classItems = classNames .split(',') .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 [flowTestingPrefix, namespace, flowName, methodName] = testParts; const fullClassName = `${flowTestingPrefix}.${namespace}.${flowName}`; if (!classes.includes(fullClassName)) { testItems.push({ namespace: `${flowTestingPrefix}.${namespace}`, className: fullClassName, testMethods: [methodName] }); classes.push(fullClassName); } else { testItems.forEach((element) => { if (element.className === fullClassName) { element.namespace = `${flowTestingPrefix}.${namespace}`; element.testMethods.push(methodName); } }); } } else { // Handle 3-part Flow tests: flowtesting.FlowName.testMethod const [flowTestingPrefix, flowOrNamespace, testMethodOrFlowName] = testParts; if (typeof namespaceInfos === 'undefined') { namespaceInfos = await (0, utils_1.queryNamespaces)(this.connection); } const currentNamespace = namespaceInfos.find((namespaceInfo) => namespaceInfo.namespace === flowOrNamespace); if (currentNamespace) { if (currentNamespace.installedNs) { testItems.push({ className: `${flowTestingPrefix}.${flowOrNamespace}.${testMethodOrFlowName}` }); } else { testItems.push({ namespace: `${flowTestingPrefix}.${flowOrNamespace}`, className: `${flowTestingPrefix}.${flowOrNamespace}.${testMethodOrFlowName}` }); } } else { const flowClassName = `${flowTestingPrefix}.${flowOrNamespace}`; if (!classes.includes(flowClassName)) { testItems.push({ className: flowClassName, testMethods: [testMethodOrFlowName] }); classes.push(flowClassName); } else { testItems.forEach((element) => { if (element.className === flowClassName) { element.testMethods.push(testMethodOrFlowName); } }); } } } return namespaceInfos; } async processApexTest(testParts, testItems, classes, namespaceInfos) { if (testParts.length === 3) { // namespace.ClassName.testMethod const [namespace, className, methodName] = testParts; const fullClassName = `${namespace}.${className}`; if (!classes.includes(fullClassName)) { testItems.push({ className: fullClassName, testMethods: [methodName] }); classes.push(fullClassName); } else { testItems.forEach((element) => { if (element.className === fullClassName) { element.testMethods.push(methodName); } }); } } else { // Handle 2-part Apex tests: namespace.Class or Class.method const [firstPart, secondPart] = testParts; // First check if this could be a namespace by seeing if we have any namespace info if (typeof namespaceInfos === 'undefined') { namespaceInfos = await (0, utils_1.queryNamespaces)(this.connection); } const currentNamespace = namespaceInfos.find((namespaceInfo) => namespaceInfo.namespace === firstPart); if (currentNamespace) { // This is namespace.Class - use full className format for all namespace types testItems.push({ className: `${firstPart}.${secondPart}` }); } else { // This is Class.method - firstPart is the class, secondPart is the method if (!classes.includes(firstPart)) { testItems.push({ className: firstPart, testMethods: [secondPart] }); classes.push(firstPart); } else { testItems.forEach((element) => { if (element.className === firstPart) { element.testMethods.push(secondPart); } }); } } } 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, "buildSyncClassPayload", 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