UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

276 lines 11.4 kB
import path from "node:path"; import { HardhatError } from "@nomicfoundation/hardhat-errors"; import { ensureError } from "@nomicfoundation/hardhat-utils/error"; import { FileNotFoundError, readdir, readJsonFile, remove, writeJsonFile, } from "@nomicfoundation/hardhat-utils/fs"; import chalk from "chalk"; import { getFullyQualifiedName, parseFullyQualifiedName, } from "../../../utils/contract-names.js"; import { getUserFqn } from "./gas-analytics-manager.js"; import { formatSectionHeader } from "./helpers.js"; export const SNAPSHOT_CHEATCODES_DIR = "snapshots"; export function getSnapshotCheatcodesPath(basePath, filename) { return path.join(basePath, SNAPSHOT_CHEATCODES_DIR, filename); } export function extractSnapshotCheatcodes(suiteResults) { const snapshots = new Map(); for (const { id: suiteId, testResults } of suiteResults) { for (const { valueSnapshotGroups: snapshotGroups } of testResults) { if (snapshotGroups === undefined) { continue; } const userFqn = getUserFqn(getFullyQualifiedName(suiteId.source, suiteId.name)); for (const group of snapshotGroups) { let snapshot = snapshots.get(group.name); if (snapshot === undefined) { snapshot = {}; snapshots.set(group.name, snapshot); } for (const entry of group.entries) { snapshot[entry.name] = { value: entry.value, metadata: { source: parseFullyQualifiedName(userFqn).sourceName, }, }; } } } } return snapshots; } async function deleteOrphanedSnapshotFiles(snapshotsDir, currentGroups) { let dirEntries; try { dirEntries = await readdir(snapshotsDir); for (const entry of dirEntries) { if (entry.endsWith(".json")) { const groupName = entry.slice(0, -5); // remove .json if (!currentGroups.has(groupName)) { const filePath = path.join(snapshotsDir, entry); await remove(filePath); } } } } catch (error) { ensureError(error); // Directory doesn't exist yet, nothing to clean up if (error instanceof FileNotFoundError) { return; } throw new HardhatError(HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_WRITE_ERROR, { snapshotsPath: snapshotsDir, error: error.message }, error); } } export async function writeSnapshotCheatcodes(basePath, snapshotCheatcodes) { const snapshotsDir = path.join(basePath, SNAPSHOT_CHEATCODES_DIR); // Delete old files that are no longer in the map const currentGroups = new Set(snapshotCheatcodes.keys()); await deleteOrphanedSnapshotFiles(snapshotsDir, currentGroups); // Write current snapshot files for (const [snapshotGroup, snapshot] of snapshotCheatcodes) { const snapshotCheatcodesPath = getSnapshotCheatcodesPath(basePath, `${snapshotGroup}.json`); const snapshotWithoutMetadata = {}; for (const [name, entry] of Object.entries(snapshot)) { snapshotWithoutMetadata[name] = entry.value; } try { await writeJsonFile(snapshotCheatcodesPath, snapshotWithoutMetadata); } catch (error) { ensureError(error); throw new HardhatError(HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_WRITE_ERROR, { snapshotsPath: snapshotCheatcodesPath, error: error.message }, error); } } } export async function readSnapshotCheatcodes(basePath) { const snapshots = new Map(); const snapshotsDir = path.join(basePath, SNAPSHOT_CHEATCODES_DIR); let dirEntries; try { dirEntries = await readdir(snapshotsDir); } catch (error) { ensureError(error); // Re-throw as-is to allow the caller to handle this case specifically if (error instanceof FileNotFoundError) { throw error; } throw new HardhatError(HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_READ_ERROR, { snapshotsPath: snapshotsDir, error: error.message }, error); } for (const entry of dirEntries) { if (entry.endsWith(".json")) { const snapshotGroup = entry.slice(0, -5); // remove .json extension const snapshotCheatcodesPath = getSnapshotCheatcodesPath(basePath, entry); let snapshot; try { snapshot = await readJsonFile(snapshotCheatcodesPath); } catch (error) { ensureError(error); throw new HardhatError(HardhatError.ERRORS.CORE.SOLIDITY_TESTS.SNAPSHOT_READ_ERROR, { snapshotsPath: snapshotCheatcodesPath, error: error.message }, error); } snapshots.set(snapshotGroup, snapshot); } } return snapshots; } export function stringifySnapshotCheatcodes(snapshots) { const lines = []; for (const { group, name, value } of snapshots) { lines.push(`${group}#${name}: ${value}`); } return lines.sort((a, b) => a.localeCompare(b)).join("\n"); } export function compareSnapshotCheatcodes(previousSnapshotsMap, currentSnapshotsMap) { const added = []; const removed = []; const changed = []; const seenPreviousEntries = new Set(); for (const [group, currentSnapshots] of currentSnapshotsMap) { const previousSnapshots = previousSnapshotsMap.get(group); for (const [name, currentEntry] of Object.entries(currentSnapshots)) { const key = `${group}#${name}`; if (previousSnapshots === undefined || !Object.hasOwn(previousSnapshots, name)) { added.push({ group, name, value: currentEntry.value }); } else { seenPreviousEntries.add(key); const previousValue = previousSnapshots[name]; if (previousValue !== currentEntry.value) { changed.push({ group, name, expected: Number(previousValue), actual: Number(currentEntry.value), source: currentEntry.metadata.source, }); } } } } for (const [group, previousSnapshots] of previousSnapshotsMap) { for (const [name, previousValue] of Object.entries(previousSnapshots)) { const key = `${group}#${name}`; if (!seenPreviousEntries.has(key)) { removed.push({ group, name, value: previousValue }); } } } const sortByKey = (a, b) => `${a.group}#${a.name}`.localeCompare(`${b.group}#${b.name}`); // Sort the results for consistent output return { added: added.sort(sortByKey), removed: removed.sort(sortByKey), changed: changed.sort(sortByKey), }; } export async function checkSnapshotCheatcodes(basePath, suiteResults) { const snapshotCheatcodes = extractSnapshotCheatcodes(suiteResults); let previousSnapshotCheatcodes; try { previousSnapshotCheatcodes = await readSnapshotCheatcodes(basePath); } catch (error) { if (error instanceof FileNotFoundError) { // Only write if there are cheatcodes to save const written = snapshotCheatcodes.size > 0; if (written) { await writeSnapshotCheatcodes(basePath, snapshotCheatcodes); } return { passed: true, comparison: { added: [], removed: [], changed: [], }, written, }; } throw error; } const comparison = compareSnapshotCheatcodes(previousSnapshotCheatcodes, snapshotCheatcodes); // Update snapshots when functions are added or removed (but not changed) const hasAddedOrRemoved = comparison.added.length > 0 || comparison.removed.length > 0; if (comparison.changed.length === 0 && hasAddedOrRemoved) { await writeSnapshotCheatcodes(basePath, snapshotCheatcodes); } return { passed: comparison.changed.length === 0, comparison, written: hasAddedOrRemoved, }; } export function logSnapshotCheatcodesSection(result, logger = console.log) { const { comparison, written } = result; const changedLength = comparison.changed.length; const addedLength = comparison.added.length; const removedLength = comparison.removed.length; const hasChanges = changedLength > 0; const hasAdded = addedLength > 0; const hasRemoved = removedLength > 0; const hasAnyDifferences = hasChanges || hasAdded || hasRemoved; const isFirstTimeWrite = written && !hasAnyDifferences; // Nothing to report if (!isFirstTimeWrite && !hasAnyDifferences) { return; } logger(formatSectionHeader("Snapshot cheatcodes", { changedLength, addedLength, removedLength, })); if (isFirstTimeWrite) { logger(); logger(chalk.green(" No existing snapshots found. Snapshot cheatcodes written successfully")); logger(); return; } if (hasChanges) { logger(); printSnapshotCheatcodeChanges(comparison.changed, logger); } if (hasAdded) { logger(); logger(` Added ${comparison.added.length} snapshot(s):`); const addedLines = stringifySnapshotCheatcodes(comparison.added).split("\n"); for (const line of addedLines) { logger(chalk.green(` + ${line}`)); } } if (hasRemoved) { logger(); logger(` Removed ${comparison.removed.length} snapshot(s):`); const removedLines = stringifySnapshotCheatcodes(comparison.removed).split("\n"); for (const line of removedLines) { logger(chalk.red(` - ${line}`)); } } logger(); } export function printSnapshotCheatcodeChanges(changes, logger = console.log) { for (let i = 0; i < changes.length; i++) { const change = changes[i]; const isLast = i === changes.length - 1; logger(` ${change.group}#${change.name}`); logger(chalk.grey(` (in ${change.source})`)); const diff = change.actual - change.expected; const formattedDiff = diff > 0 ? `Δ+${diff}` : ${diff}`; let gasChange = `${formattedDiff}`; if (change.expected > 0) { const percent = (diff / change.expected) * 100; const formattedPercent = percent >= 0 ? `+${percent.toFixed(2)}%` : `${percent.toFixed(2)}%`; gasChange = `${formattedPercent}, ${formattedDiff}`; } // Color: green for decrease (improvement), red for increase (regression) const formattedGasChange = diff < 0 ? chalk.green(gasChange) : chalk.red(gasChange); logger(chalk.grey(` Expected: ${change.expected}`)); logger(chalk.grey(` Actual: ${change.actual} (`) + formattedGasChange + chalk.grey(")")); if (!isLast) { logger(); } } } //# sourceMappingURL=snapshot-cheatcodes.js.map