UNPKG

@vlocode/apex

Version:
175 lines 8.09 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; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestIdentifier = void 0; const core_1 = require("@vlocode/core"); const util_1 = require("@vlocode/util"); const path_1 = __importDefault(require("path")); const parser_1 = require("./parser"); /** * This class can be used to identify test classes that cover a given Apex class, it * does so by parsing the source files and identifying test classes. * * The class is injectable and depends on the `FileSystem` and `Logger` services. These services can be injected * by the dependency injection container see {@link container} * * @example * ```typescript * const testIdentifier = container.create(TestIdentifier); * await testIdentifier.loadApexClasses([ 'path/to/apex/classes' ]); * const testClasses = testIdentifier.getTestClasses('MyClass'); * ``` */ let TestIdentifier = exports.TestIdentifier = class TestIdentifier { fileSystem; logger; /** * Full path of the file to the apex class name. */ fileToApexClass = new Map(); /** * Map of Apex class information by name class name (lowercase) */ apexClassesByName = new Map(); /** * Set of test class names */ testClasses = new Map(); constructor(fileSystem, logger = core_1.LogManager.get('apex-test-identifier')) { this.fileSystem = fileSystem; this.logger = logger; } /** * Loads the Apex classes from the specified folders and populates the testClasses map. * * @param folders - An array of folder paths containing the Apex classes. * @returns A promise that resolves when the Apex classes are loaded and testClasses map is populated. */ async loadApexClasses(folders) { const timerAll = new util_1.Timer(); const loadedFiles = await this.parseSourceFiles(folders); const testClasses = loadedFiles.filter((info) => info.classStructure.methods.some(method => method.isTest)); testClasses.forEach(testClass => this.testClasses.set(testClass.name.toLowerCase(), { name: testClass.name, file: testClass.file, classCoverage: testClass.classStructure.methods .filter(method => method.isTest) .flatMap(method => method.refs.filter(ref => !ref.isSystemType)) .map(ref => ref.name.toLowerCase()), testMethods: testClass.classStructure.methods .filter(method => method.isTest) .map(method => ({ methodName: method.name, classCoverage: method.refs.filter(ref => !ref.isSystemType).map(ref => ref.name.toLowerCase()) })), })); this.logger.info(`Loaded ${loadedFiles.length} sources (${testClasses.length} test classes) in ${timerAll.toString('ms')}`); } /** * Retrieves the test classes that cover a given class, optionally include test classes for classes that reference the given class. * The depth parameter controls how many levels of references to include, if not specified only direct test classes are returned. * * If the class is not found, undefined is returned; if no test classes are found, an empty array is returned. * * @param className - The name of the class to retrieve test classes for. * @param options - Optional parameters for controlling the depth of the search. * @returns An array of test class names that cover the specified class. */ getTestClasses(className, options) { const classInfo = this.apexClassesByName.get(className.toLowerCase()); if (!classInfo) { return undefined; } const testClasses = new Set(); for (const testClass of this.testClasses.values()) { if (testClass.classCoverage.includes(className.toLowerCase())) { testClasses.add(testClass.name); } } if (options?.depth) { for (const referenceClassName of this.getClassReferences(className)) { this.getTestClasses(referenceClassName, { depth: options.depth - 1 }) ?.forEach(testClass => testClasses.add(testClass)); } } return [...testClasses]; } /** * Retrieves an Array class that references the given class. * @param className The name of the class to retrieve references for. * @returns An array of class names that reference the given class. */ getClassReferences(className) { const references = new Set(); for (const classInfo of this.apexClassesByName.values()) { if (classInfo.classStructure.refs.some(ref => (0, util_1.stringEqualsIgnoreCase)(ref.name, className))) { references.add(classInfo.name); } } return [...references]; } async parseSourceFiles(folders) { const sourceFiles = new Array(); for (const folder of folders) { this.logger.verbose(`Parsing source files in: ${folder}`); for await (const { buffer, file } of this.readSourceFiles(folder)) { const apexClassName = this.fileToApexClass.get(file); if (apexClassName) { sourceFiles.push(this.apexClassesByName.get(apexClassName.toLowerCase())); continue; } const parseTimer = new util_1.Timer(); const parser = new parser_1.Parser(buffer); const struct = parser.getCodeStructure(); for (const classInfo of struct.classes) { const sourceData = { classStructure: classInfo, name: classInfo.name, file, isAbstract: !!classInfo.isAbstract, isTest: !!classInfo.isTest, }; this.fileToApexClass.set(file, classInfo.name); this.apexClassesByName.set(classInfo.name.toLowerCase(), sourceData); sourceFiles.push(sourceData); } this.logger.verbose(`Parsed: ${file} (${parseTimer.toString('ms')})`); } } return sourceFiles; } async *readSourceFiles(folder) { for (const file of await this.fileSystem.readDirectory(folder)) { const fullPath = path_1.default.join(folder, file.name); if (file.isDirectory()) { yield* this.readSourceFiles(fullPath); } if (file.isFile() && file.name.endsWith('.cls') /* || file.name.endsWith('.trigger') */) { yield { buffer: await this.fileSystem.readFile(fullPath), fullPath, file: file.name }; } } } }; exports.TestIdentifier = TestIdentifier = __decorate([ (0, core_1.injectable)({ lifecycle: core_1.LifecyclePolicy.transient }), __metadata("design:typeinfo", { paramTypes: () => [core_1.FileSystem, core_1.Logger] }) ], TestIdentifier); //# sourceMappingURL=testIdentifier.js.map