UNPKG

sfdx-hardis

Version:

Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards

284 lines (280 loc) 13.9 kB
import { SfCommand, Flags, requiredOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; import c from 'chalk'; import fs from 'fs-extra'; import * as path from 'path'; import { execCommand, extractRegexMatchesMultipleGroups, uxLog } from '../../../../common/utils/index.js'; import { getNotificationButtons, getOrgMarkdown } from '../../../../common/utils/notifUtils.js'; import { CONSTANTS, getConfig, getEnvVar, getReportDirectory } from '../../../../config/index.js'; import { NotifProvider } from '../../../../common/notifProvider/index.js'; import { generateApexCoverageOutputFile } from '../../../../common/utils/deployUtils.js'; import { setConnectionVariables } from '../../../../common/utils/orgUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); export default class OrgTestApex extends SfCommand { static title = 'Run apex tests'; static description = `Run apex tests in Salesforce org If following configuration is defined, it will fail if apex coverage target is not reached: - Env \`APEX_TESTS_MIN_COVERAGE_ORG_WIDE\` or \`.sfdx-hardis\` property \`apexTestsMinCoverageOrgWide\` - Env \`APEX_TESTS_MIN_COVERAGE_ORG_WIDE\` or \`.sfdx-hardis\` property \`apexTestsMinCoverageOrgWide\` You can override env var SFDX_TEST_WAIT_MINUTES to wait more than 120 minutes. This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/salesforce-monitoring-apex-tests/) and can output Grafana, Slack and MsTeams Notifications. `; static examples = ['$ sf hardis:org:test:apex']; static flags = { testlevel: Flags.string({ char: 'l', default: 'RunLocalTests', options: ['NoTestRun', 'RunSpecifiedTests', 'RunLocalTests', 'RunAllTestsInOrg'], description: messages.getMessage('testLevel'), }), debug: Flags.boolean({ char: 'd', default: false, description: messages.getMessage('debugMode'), }), websocket: Flags.string({ description: messages.getMessage('websocket'), }), skipauth: Flags.boolean({ description: 'Skip authentication check when a default username is required', }), 'target-org': requiredOrgFlagWithDeprecations, }; // Set this to true if your command requires a project workspace; 'requiresProject' is false by default // protected static requiresProject = true; configInfo = {}; testRunOutcome; testRunOutputString; statusMessage; coverageTarget = 75.0; coverageValue = 0.0; failingTestClasses = []; notifSeverity = 'log'; notifText; notifAttachments = []; notifAttachedFiles = []; orgMarkdown = ''; notifButtons = []; /* jscpd:ignore-start */ async run() { const { flags } = await this.parse(OrgTestApex); const testlevel = flags.testlevel || 'RunLocalTests'; const debugMode = flags.debug || false; this.configInfo = await getConfig('branch'); const orgInstanceUrl = flags['target-org']?.getConnection()?.instanceUrl || ''; this.orgMarkdown = await getOrgMarkdown(orgInstanceUrl); this.notifButtons = await getNotificationButtons(); /* jscpd:ignore-end */ uxLog("action", this, c.cyan(`Running Apex tests in org ${orgInstanceUrl} with test level: ${testlevel}`)); await this.runApexTests(testlevel, debugMode, flags['target-org']?.getUsername()); uxLog("action", this, c.cyan(`Apex tests completed with outcome: ${this.testRunOutcome}`)); // No Apex if (this.testRunOutcome === 'NoApex') { this.notifSeverity = 'log'; this.statusMessage = 'No Apex found in the org'; this.notifText = `No Apex found in org ${this.orgMarkdown}`; uxLog("log", this, c.grey(this.statusMessage)); } // Failed tests else if (this.testRunOutcome === 'Failed') { await this.processApexTestsFailure(); } else if (this.testRunOutcome === 'Passed') { uxLog("success", this, c.green(`Apex tests passed (${this.testRunOutcome})`)); } // Get test coverage (and fail if not reached) await this.checkOrgWideCoverage(); await this.checkTestRunCoverage(); if (this.testRunOutcome !== 'NoApex') { uxLog("other", this, `Apex coverage: ${this.coverageValue}% (target: ${this.coverageTarget}%)`); } await setConnectionVariables(flags['target-org']?.getConnection()); // Required for some notifications providers like Email await NotifProvider.postNotifications({ type: 'APEX_TESTS', text: this.notifText, attachments: this.notifAttachments, buttons: this.notifButtons, severity: this.notifSeverity, attachedFiles: this.notifAttachedFiles, logElements: this.failingTestClasses, data: { metric: this.failingTestClasses.length, coverageTarget: this.coverageTarget, coverageValue: this.coverageValue, }, metrics: { ApexTestsFailingClasses: this.failingTestClasses.length, ApexTestsCodeCoverage: this.coverageValue, }, }); // Handle output message & exit code if (this.notifSeverity === 'error') { process.exitCode = 1; uxLog("error", this, c.red(this.statusMessage)); } else { uxLog("success", this, c.green(this.statusMessage)); } return { orgId: flags['target-org'].getOrgId(), outputString: this.statusMessage, statusCode: process.exitCode }; } async runApexTests(testlevel, debugMode, orgUsername) { // Run tests with SFDX commands const reportDir = await getReportDirectory(); const testCommand = 'sf apex run test' + ' --code-coverage' + ' --result-format human' + ` --output-dir ${reportDir}` + ` --wait ${getEnvVar("SFDX_TEST_WAIT_MINUTES") || '60'}` + ` --test-level ${testlevel}` + (orgUsername ? ` --target-org ${orgUsername}` : '') + (debugMode ? ' --verbose' : ''); try { const execCommandRes = await execCommand(testCommand, this, { output: true, debug: debugMode, fail: true, }); // Parse outcome value from logs with Regex this.testRunOutcome = (/Outcome *(.*) */.exec(execCommandRes.stdout + execCommandRes.stderr) || '')[1].trim(); this.testRunOutputString = execCommandRes.stdout + execCommandRes.stderr; await generateApexCoverageOutputFile(); } catch (e) { // No Apex in the org const errorMessage = e.message; if (errorMessage.includes('Toujours fournir une propriété classes, suites, tests ou testLevel') || errorMessage.includes('Always provide a classes, suites, tests, or testLevel property') || errorMessage.includes('No tests found for category') || errorMessage.includes('Aucun test trouvé pour category') || errorMessage.includes('Error (INVALID_INPUT)')) { this.testRunOutcome = 'NoApex'; } else { // Failing Apex tests this.testRunOutputString = e.message; this.testRunOutcome = 'Failed'; await generateApexCoverageOutputFile(); } } } async processApexTestsFailure() { this.notifSeverity = 'error'; const reportDir = await getReportDirectory(); // Parse log from external file const sfReportFile = path.join(reportDir, '/test-result.txt'); if (fs.existsSync(sfReportFile)) { this.notifAttachedFiles = [sfReportFile]; } // Parse failing test classes const failuresRegex = /(.*) Fail (.*)/gm; const regexMatches = await extractRegexMatchesMultipleGroups(failuresRegex, this.testRunOutputString); for (const match of regexMatches) { this.failingTestClasses.push({ name: match[1].trim(), error: match[2].trim() }); } this.notifAttachments = [ { text: this.failingTestClasses .map((failingTestClass) => { return '• *' + failingTestClass.name + '*: ' + failingTestClass.error; }) .join('\n'), }, ]; this.statusMessage = `Apex tests failed (${this.failingTestClasses.length}). (Outcome: ${this.testRunOutcome})`; this.notifText = `Apex tests failed (${this.failingTestClasses.length}) in org ${this.orgMarkdown} (Outcome: ${this.testRunOutcome})`; const failedTestsString = this.failingTestClasses .map((failingTestClass) => { return `- ${failingTestClass.name}: ${failingTestClass.error}`; }) .join('\n'); uxLog("warning", this, c.yellow("Failing Apex tests:\n" + failedTestsString)); } async checkOrgWideCoverage() { if (this.testRunOutcome === 'NoApex') { return; } // Safely extract org-wide coverage from output to avoid crashes when regex doesn't match const orgWideMatch = (/Org Wide Coverage\s*(.*)/.exec(this.testRunOutputString) || []); const coverageOrgWide = orgWideMatch[1] && typeof orgWideMatch[1] === 'string' ? (Number.isFinite(parseFloat(orgWideMatch[1].replace('%', ''))) ? parseFloat(orgWideMatch[1].replace('%', '')) : 0.0) : 0.0; if (coverageOrgWide === 0.0) { this.notifSeverity = 'error'; uxLog("warning", this, c.yellow('Warning: Unable to extract org wide coverage from test run output.\n' + this.testRunOutputString)); } const minCoverageOrgWide = parseFloat(process.env.APEX_TESTS_MIN_COVERAGE_ORG_WIDE || process.env.APEX_TESTS_MIN_COVERAGE || this.configInfo.apexTestsMinCoverageOrgWide || this.configInfo.apexTestsMinCoverage || 75.0); this.coverageTarget = minCoverageOrgWide; this.coverageValue = coverageOrgWide; // Do not test if tests failed if (this.testRunOutcome !== 'Passed') { return; } // Developer tried to cheat in config ^^ if (minCoverageOrgWide < 75.0) { this.notifSeverity = 'error'; this.statusMessage = `Don't try to cheat with configuration: Minimum org wide coverage must be 75% ;)`; this.notifText = this.statusMessage; } // Min coverage not reached else if (coverageOrgWide < minCoverageOrgWide) { this.notifSeverity = 'error'; this.statusMessage = `Test run coverage (org wide) *${coverageOrgWide}%* should be > to ${minCoverageOrgWide}%`; this.notifText = `${this.statusMessage} in ${this.orgMarkdown}`; } // We are good ! else { this.notifSeverity = 'log'; this.statusMessage = `Test run coverage (org wide) *${coverageOrgWide}%* is > to ${minCoverageOrgWide}%`; this.notifText = `${this.statusMessage} in ${this.orgMarkdown}`; } } async checkTestRunCoverage() { if (this.testRunOutcome === 'NoApex') { return; } if (this.testRunOutputString.includes('Test Run Coverage')) { // const coverageTestRun = parseFloat(testRes.result.summary.testRunCoverage.replace('%', '')); const testRunMatch = /Test Run Coverage\s*(.*)/.exec(this.testRunOutputString); const coverageTestRunRaw = testRunMatch && testRunMatch[1] ? testRunMatch[1].replace('%', '').trim() : '0'; const coverageTestRun = parseFloat(coverageTestRunRaw) || 0.0; if (coverageTestRun === 0.0) { this.notifSeverity = 'error'; uxLog("warning", this, c.yellow('Warning: Unable to extract test run coverage from test run output.\n' + this.testRunOutputString)); } const minCoverageTestRun = parseFloat(process.env.APEX_TESTS_MIN_COVERAGE_TEST_RUN || process.env.APEX_TESTS_MIN_COVERAGE || this.configInfo.apexTestsMinCoverage || this.coverageTarget); this.coverageTarget = minCoverageTestRun; this.coverageValue = coverageTestRun; // Do not test if tests failed if (this.testRunOutcome !== 'Passed') { return; } // Developer tried to cheat in config ^^ if (minCoverageTestRun < 75.0) { this.notifSeverity = 'error'; this.statusMessage = `Don't try to cheat with configuration: Minimum test run coverage must be 75% ;)`; this.notifText = this.statusMessage; } // Min coverage not reached else if (coverageTestRun < minCoverageTestRun) { this.notifSeverity = 'error'; this.statusMessage = `Test run coverage *${coverageTestRun}%* should be > to ${minCoverageTestRun}%`; this.notifText = `${this.statusMessage} in ${this.orgMarkdown}`; } // We are good ! else { this.notifSeverity = 'log'; this.statusMessage = `Test run coverage *${coverageTestRun}%* is > to ${minCoverageTestRun}%`; this.notifText = `${this.statusMessage} in ${this.orgMarkdown}`; } } } } //# sourceMappingURL=apex.js.map