@salesforce/apex-node
Version:
Salesforce JS library for Apex
590 lines • 25.8 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 = 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