hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
291 lines • 12.6 kB
JavaScript
import chalk from "chalk";
// Use constants for the Uint8Array to improve memory usage (1 byte vs 8 bytes per item)
const STATUS_NOT_EXECUTED = 0; // equivalent to false
const STATUS_EXECUTED = 1; // equivalent to true
const STATUS_IGNORED = 2; // equivalent to null
/**
* Processes the raw EDR coverage information for a file and returns the executed and
* non-executed statements and lines.
*
* @param fileContent The original file content being analyzed
* @param metadata Coverage metadata received from EDR for this file
* @param hitTags The coverage tags recorded as executed during the test run
* for this specific file.
*
* @returns An object containing:
* - statements: the executed and not-executed statements
* - lines: the executed and not-executed line numbers
*/
export function getProcessedCoverageInfo(fileContent, metadata, hitTags) {
const statementsByExecution = partitionStatementsByExecution(metadata, hitTags);
const { start, end } = getCoverageBounds(statementsByExecution, fileContent.length);
const characterCoverage = buildCharacterCoverage(fileContent, start, end, statementsByExecution.unexecuted);
// printStatementsForDebugging(fileContent, statementsByExecution);
// printCharacterCoverageForDebugging(fileContent, characterCoverage);
return {
lines: partitionLinesByExecution(fileContent, characterCoverage),
};
}
function partitionStatementsByExecution(metadata, hitTags) {
const executed = [];
const unexecuted = [];
for (const node of metadata) {
if (hitTags.has(node.tag)) {
executed.push(node);
}
else {
unexecuted.push(node);
}
}
return {
executed,
unexecuted,
};
}
// Determine the minimum and maximum character indexes that define the range
// where coverage should be calculated, excluding parts that the tests never
// reach. This removes irrelevant sections from coverage analysis, such as the
// beginning or end of each Solidity file.
// Example:
// // SPDX-License-Identifier: MIT
// pragma solidity ^0.8.0;
function getCoverageBounds(statementsByExecution, fileLength) {
let start = fileLength;
let end = 0;
const { executed, unexecuted } = statementsByExecution;
for (const s of [...executed, ...unexecuted]) {
if (s.startUtf16 < start) {
start = s.startUtf16;
}
if (s.endUtf16 > end) {
end = s.endUtf16;
}
}
return { start, end };
}
// Return an array with the same length as the file content. Each position in
// the array corresponds to a character in the file. The value at each position
// indicates whether that character was executed during tests (STATUS_EXECUTED),
// not executed (STATUS_NOT_EXECUTED), or not relevant for coverage
// (STATUS_IGNORED). STATUS_IGNORED is used to indicate characters that are not
// executed during test, such as those found at the start or end of every Solidity file.
// Example:
// // SPDX-License-Identifier: MIT
// pragma solidity ^0.8.0;
function buildCharacterCoverage(fileContent, start, end, unexecutedStatements) {
// Use Uint8Array instead of Array<null | boolean> for memory efficiency.
// We initialize with STATUS_IGNORED (equivalent to filling with null)
const characterCoverage = new Uint8Array(fileContent.length).fill(STATUS_IGNORED);
// Initially mark all characters that may be executed during the tests as
// STATUS_EXECUTED. They will be set to false later if they are not executed.
// The coverage statement received from EDR will provide this information.
// Setting everything to true first simplifies the logic for extracting
// coverage data. Starting with false and toggling only covered characters
// would make statement processing more complex.
for (let i = start; i < end; i++) {
if (charMustBeIgnored(fileContent[i])) {
continue;
}
characterCoverage[i] = STATUS_EXECUTED;
}
for (const statement of unexecutedStatements) {
for (let i = statement.startUtf16; i < statement.endUtf16; i++) {
if (characterCoverage[i] === STATUS_IGNORED) {
continue;
}
characterCoverage[i] = STATUS_NOT_EXECUTED;
}
}
markNonExecutablePatterns(fileContent, characterCoverage);
return characterCoverage;
}
// The following characters are not relevant for the code coverage, so they
// must be ignored when marking characters as executed (true) or not executed (false)
function charMustBeIgnored(char) {
const code = char.charCodeAt(0);
return (code === 32 || // Space
(code >= 9 && code <= 13) || // \t (9), \n (10), \v (11), \f (12), \r (13)
code === 123 || // {
code === 125 // }
);
}
// Mark different types of substrings as not relevant for code coverage (set them to STATUS_IGNORED).
// For example, all "else" substrings or comments are not relevant for code coverage.
function markNonExecutablePatterns(fileContent, characterCoverage) {
// Comments that start with //
markMatchingCharsWithIgnored(fileContent, characterCoverage, /\/\/.*?(?=\n|$)/g);
// Comments wrapped between /* and */
markMatchingCharsWithIgnored(fileContent, characterCoverage, /\/\*[\s\S]*?\*\//g);
// Lines containing `else`, since they do not represent executable code by themselves.
// Keep in mind that `else` is preceded by a closing brace and followed by an opening brace.
// This can span multiple lines.
markMatchingCharsWithIgnored(fileContent, characterCoverage, /\}\s*\belse\b\s*\{/g);
// Lines containing the function signature. This can span multiple lines
markMatchingCharsWithIgnored(fileContent, characterCoverage, /^\s*(function\s+[A-Za-z_$][A-Za-z0-9_$]*\s*\([\s\S]*?\)[^{]*?)(?=\s*\{)/gm);
// Lines containing the catch signature. This can span multiple lines.
markMatchingCharsWithIgnored(fileContent, characterCoverage, /\bcatch\b(?:\s+[A-Za-z_][A-Za-z0-9_]*)?\s*(?:\([\s\S]*?\))?(?=\s*\{)/g);
}
function markMatchingCharsWithIgnored(fileContent, characterCoverage, regex) {
for (const match of fileContent.matchAll(regex)) {
for (let i = match.index; i < match.index + match[0].length; i++) {
characterCoverage[i] = STATUS_IGNORED;
}
}
}
// Generate non overlapping coverage statements. Every character between the start
// and end indexes is guaranteed to be either executed or not executed.
// Unlike the statements received from EDR, which may include nested sub
// statements, these are processed and have no sub statements or overlapping statements.
// Example:
// [
// { start: 12, end: 20, executed: true },
// { start: 15, end: 17, executed: true }
// ]
// ->
// [
// { start: 12, end: 20, executed: true }
// ]
/* eslint-disable-next-line @typescript-eslint/no-unused-vars
-- currently not used, but it will be used to create more detailed.
It will be used to return statements from the function `getProcessedCoverageInfo` */
function getProcessedExecutedStatements(characterCoverage) {
const executed = generateProcessedStatements(characterCoverage, STATUS_EXECUTED);
const unexecuted = generateProcessedStatements(characterCoverage, STATUS_NOT_EXECUTED);
return {
executed,
unexecuted,
};
}
// Based on the marked file, where each character is marked as either
// executed (STATUS_EXECUTED) or not executed (STATUS_NOT_EXECUTED),
// generate non-overlapping statements that indicate the start and
// end indices, along with whether they were executed or not.
function generateProcessedStatements(characterCoverage, targetStatus) {
const ranges = [];
const fileLength = characterCoverage.length;
let start = -1;
for (let i = 0; i < fileLength; i++) {
if (characterCoverage[i] === targetStatus) {
if (start === -1) {
start = i; // begin new range
}
}
else {
if (start !== -1) {
// Map back to boolean for the output object
ranges.push({
startUtf16: start,
endUtf16: i - 1,
executed: targetStatus === STATUS_EXECUTED,
});
start = -1;
}
}
}
// close last range if file ends inside a run
if (start !== -1) {
ranges.push({
startUtf16: start,
endUtf16: fileLength - 1,
executed: targetStatus === STATUS_EXECUTED,
});
}
return ranges;
}
// Return the executed and non executed lines based on the marked file.
// Some lines are excluded from the execution count, for example, comments.
function partitionLinesByExecution(fileContent, characterCoverage) {
const executed = new Map();
const unexecuted = new Map();
let lineStart = 0;
let isLineIgnored = true;
let isLineExecutedOrIgnored = true;
let lineNumber = 1; // File lines start at 1
// Helper to process a line and push to correct map
const processLine = (endIndex) => {
// Only process if the line isn't entirely ignored
if (!isLineIgnored) {
const lineText = fileContent.slice(lineStart, endIndex);
if (isLineExecutedOrIgnored) {
executed.set(lineNumber, lineText);
}
else {
unexecuted.set(lineNumber, lineText);
}
}
// Reset state for the next line
lineStart = endIndex + 1;
isLineIgnored = true;
isLineExecutedOrIgnored = true;
lineNumber++;
};
for (let i = 0; i < fileContent.length; i++) {
const char = fileContent[i];
const status = characterCoverage[i];
if (status !== STATUS_IGNORED) {
isLineIgnored = false;
}
if (status === STATUS_NOT_EXECUTED) {
isLineExecutedOrIgnored = false;
}
if (char === "\n") {
processLine(i);
}
}
// Handle the final line if the file doesn't end in a newline
if (lineStart < fileContent.length) {
processLine(fileContent.length);
}
return { executed, unexecuted };
}
// Enable this function while debugging to display the coverage for a file.
// The file will be printed with green characters when they are executed,
// red characters when they are not executed,
// and gray characters when they are irrelevant for code coverage.
/* eslint-disable-next-line @typescript-eslint/no-unused-vars
-- this function can be enabled for debugging purposes */
function printStatementsForDebugging(fileContent, statementsByExecution) {
const relativePath = statementsByExecution.executed.length > 0
? statementsByExecution.executed[0].relativePath
: statementsByExecution.unexecuted[0].relativePath;
console.debug("Statements for file: " + relativePath);
console.debug("Executed statements:");
let counter = 0;
for (const statement of statementsByExecution.executed) {
console.debug(counter++ + " ---");
for (let i = statement.startUtf16; i < statement.endUtf16; i++) {
process.stdout.write(chalk.gray(fileContent[i]));
}
console.debug();
}
console.debug();
console.debug("Unexecuted statements:");
counter = 0;
for (const statement of statementsByExecution.unexecuted) {
console.debug(counter++ + " ---");
for (let i = statement.startUtf16; i < statement.endUtf16; i++) {
process.stdout.write(chalk.gray(fileContent[i]));
}
console.debug();
}
}
// Enable this function while debugging to display the coverage for a file.
// The file will be printed with green characters when they are executed,
// red characters when they are not executed,
// and gray characters when they are irrelevant for code coverage.
/* eslint-disable-next-line @typescript-eslint/no-unused-vars
-- this function can be enabled for debugging purposes */
function printCharacterCoverageForDebugging(fileContent, characterCoverage) {
for (let i = 0; i < characterCoverage.length; i++) {
if (characterCoverage[i] === STATUS_IGNORED) {
process.stdout.write(chalk.gray(fileContent[i]));
}
else if (characterCoverage[i] === STATUS_EXECUTED) {
process.stdout.write(chalk.green(fileContent[i]));
}
else {
process.stdout.write(chalk.red(fileContent[i]));
}
}
}
//# sourceMappingURL=process-coverage.js.map