@salesforce/apex-node
Version:
Salesforce JS library for Apex
595 lines • 24.7 kB
JavaScript
;
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