diff --git a/.circleci/config.yml b/.circleci/config.yml index edaeb5d558..5aefeb2f90 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -230,15 +230,17 @@ jobs: auth: username: $DOCKERHUB_USERNAME password: $DOCKERHUB_TOKEN - resource_class: large steps: - checkout - attach_workspace: at: . - - run: npm run test:gas - - run: npx codechecks codechecks.unit.yml + - run: + name: Upload gas reports + command: | + npx hardhat test:merge-gas-reports gasReporterOutput-*.json + npx codechecks codechecks.unit.yml - store_artifacts: - path: test-gas-used.log + path: gasReporterOutput.json job-unit-tests: working_directory: ~/repo docker: @@ -259,11 +261,18 @@ jobs: set +e circleci tests glob 'test/contracts/*.js' | circleci tests split | - xargs npm test + xargs npm test -- --gas EXIT_CODE=$? - cat test-gas-used.log printf "\\n" exit $EXIT_CODE + - run: + name: Save gas report + command: | + mv gasReporterOutput.json ./gasReporterOutput-$CIRCLE_NODE_INDEX.json + - persist_to_workspace: + root: . + paths: + - gasReporterOutput-*.json job-validate-deployments: working_directory: ~/repo docker: @@ -324,6 +333,7 @@ workflows: - job-unit-tests-gas-report: requires: - job-prepare + - job-unit-tests - job-test-deploy-script: requires: - job-prepare diff --git a/.circleci/src/jobs/job-fork-tests.yml b/.circleci/src/jobs/job-fork-tests.yml index 0a69a95f97..f564a85595 100644 --- a/.circleci/src/jobs/job-fork-tests.yml +++ b/.circleci/src/jobs/job-fork-tests.yml @@ -8,5 +8,5 @@ steps: command: npm run fork background: true - cmd-wait-for-port: - port: 8545 + port: 8545 - run: npx hardhat test:integration:l1 --compile --deploy --use-fork diff --git a/.circleci/src/jobs/job-unit-tests-gas-report.yml b/.circleci/src/jobs/job-unit-tests-gas-report.yml index 920ef5de55..88e28de7fa 100644 --- a/.circleci/src/jobs/job-unit-tests-gas-report.yml +++ b/.circleci/src/jobs/job-unit-tests-gas-report.yml @@ -1,11 +1,13 @@ # Measures deployment and transaction gas usage in unit tests {{> job-header.yml}} -resource_class: large steps: - checkout - attach_workspace: at: . - - run: npm run test:gas - - run: npx codechecks codechecks.unit.yml + - run: + name: Upload gas reports + command: | + npx hardhat test:merge-gas-reports gasReporterOutput-*.json + npx codechecks codechecks.unit.yml - store_artifacts: - path: test-gas-used.log + path: gasReporterOutput.json diff --git a/.circleci/src/jobs/job-unit-tests.yml b/.circleci/src/jobs/job-unit-tests.yml index e8f268ead8..661351f23d 100644 --- a/.circleci/src/jobs/job-unit-tests.yml +++ b/.circleci/src/jobs/job-unit-tests.yml @@ -13,8 +13,15 @@ steps: set +e circleci tests glob 'test/contracts/*.js' | circleci tests split | - xargs npm test + xargs npm test -- --gas EXIT_CODE=$? - cat test-gas-used.log printf "\\n" exit $EXIT_CODE + - run: + name: Save gas report + command: | + mv gasReporterOutput.json ./gasReporterOutput-$CIRCLE_NODE_INDEX.json + - persist_to_workspace: + root: . + paths: + - gasReporterOutput-*.json diff --git a/.circleci/src/snippets/require-unit-tests.yml b/.circleci/src/snippets/require-unit-tests.yml new file mode 100644 index 0000000000..89b86a9c34 --- /dev/null +++ b/.circleci/src/snippets/require-unit-tests.yml @@ -0,0 +1,3 @@ +requires: + - job-prepare + - job-unit-tests diff --git a/.circleci/src/workflows/workflow-all.yml b/.circleci/src/workflows/workflow-all.yml index c2a70b65d0..f17c56f6a1 100644 --- a/.circleci/src/workflows/workflow-all.yml +++ b/.circleci/src/workflows/workflow-all.yml @@ -20,7 +20,7 @@ jobs: - job-unit-tests-coverage-report: {{> require-unit-tests-coverage.yml}} - job-unit-tests-gas-report: - {{> require-prepare.yml}} + {{> require-unit-tests.yml}} - job-test-deploy-script: {{> require-prepare.yml}} diff --git a/codechecks.unit.yml b/codechecks.unit.yml index 830467e36f..c57f204b79 100644 --- a/codechecks.unit.yml +++ b/codechecks.unit.yml @@ -1,7 +1,7 @@ checks: - name: eth-gas-reporter/codechecks options: - name: unit-test-gas-report + name: unit-test-gas-report-unoptimized settings: branches: - develop diff --git a/hardhat.config.js b/hardhat.config.js index 293d77457f..b38433747b 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -61,6 +61,7 @@ module.exports = { gasReporter: { enabled: false, showTimeSpent: true, + gasPrice: 20, currency: 'USD', maxMethodDiff: 25, // CI will fail if gas usage is > than this % outputFile: 'test-gas-used.log', diff --git a/hardhat/tasks/task-test-merge-gas-reports.js b/hardhat/tasks/task-test-merge-gas-reports.js new file mode 100644 index 0000000000..d0a9c9b09b --- /dev/null +++ b/hardhat/tasks/task-test-merge-gas-reports.js @@ -0,0 +1,125 @@ +const fs = require('fs'); +const path = require('path'); +const { gray } = require('chalk'); +const { task } = require('hardhat/config'); +const { globSync } = require('hardhat/internal/util/glob'); +const uniq = require('lodash.uniq'); + +/** + * Task for merging multiple gasReporterOuput.json files generated by eth-gas-reporter + * This task is necessary when we want to generate different parts of the reports + * parallelized on different jobs, then merge the results and upload it to codechecks. + * Gas Report JSON file schema: https://github.com/cgewecke/eth-gas-reporter/blob/master/docs/gasReporterOutput.md + */ + +task('test:merge-gas-reports', 'Merge several gasReporterOuput.json files into one') + .addOptionalParam('output', 'Target file to save the merged report', 'gasReporterOutput.json') + .addVariadicPositionalParam( + 'input', + 'A list of gasReporterOutput.json files generated by eth-gas-reporter. Files can be defined using glob patterns' + ) + .setAction(async taskArguments => { + const output = path.resolve(taskArguments.output); + + // Parse input files and calculate glob patterns + const input = uniq(taskArguments.input.map(globSync).flat()).map(inputFile => + path.resolve(inputFile) + ); + + if (input.length === 0) { + throw new Error(`No files found for the given input: ${taskArguments.input.join(' ')}`); + } + + console.log(gray(`Merging ${input.length} input files:`)); + input.forEach(inputFile => { + console.log(gray(' - ', inputFile)); + }); + + console.log(gray('\nOutput: ', output)); + + const result = { + namespace: null, + config: null, + info: { + methods: {}, + deployments: [], + blockLimit: null, + }, + }; + + input.forEach(inputFile => { + const report = JSON.parse(fs.readFileSync(inputFile, 'utf-8')); + + if (!report.config) { + throw new Error(`Missing "config" property on ${inputFile}`); + } + + if (!result.config) result.config = report.config; + + if (!result.namespace) { + result.namespace = report.namespace; + } + + if (result.namespace !== report.namespace) { + throw new Error('Cannot merge reports with different namespaces'); + } + + // Update config.gasPrice only if the newer one has a bigger number + if (typeof report.config.gasPrice === 'number') { + if ( + typeof result.config.gasPrice !== 'number' || + result.config.gasPrice < report.config.gasPrice + ) { + result.config.gasPrice = report.config.gasPrice; + } + } else { + result.config.gasPrice = report.config.gasPrice; + } + + if (!report.info || typeof report.info.blockLimit !== 'number') { + throw new Error(`Invalid "info" property on ${inputFile}`); + } + + if (!result.info.blockLimit) { + result.info.blockLimit = report.info.blockLimit; + } else if (result.info.blockLimit !== report.info.blockLimit) { + throw new Error('"info.blockLimit" should be the same on all reports'); + } + + if (!report.info.methods) { + throw new Error(`Missing "info.methods" property on ${inputFile}`); + } + + // Merge info.methods objects + Object.entries(report.info.methods).forEach(([key, value]) => { + if (!result.info.methods[key]) { + result.info.methods[key] = value; + return; + } + + result.info.methods[key].gasData = [ + ...result.info.methods[key].gasData, + ...report.info.methods[key].gasData, + ].sort((a, b) => a - b); + + result.info.methods[key].numberOfCalls += report.info.methods[key].numberOfCalls; + }); + + if (!Array.isArray(report.info.deployments)) { + throw new Error(`Invalid "info.deployments" property on ${inputFile}`); + } + + // Merge info.deployments objects + report.info.deployments.forEach(deployment => { + const current = result.info.deployments.find(d => d.name === deployment.name); + + if (current) { + current.gasData = [...current.gasData, ...deployment.gasData].sort((a, b) => a - b); + } else { + result.info.deployments.push(deployment); + } + }); + }); + + fs.writeFileSync(output, JSON.stringify(result), 'utf-8'); + }); diff --git a/package.json b/package.json index 3cf2d04d37..32786fb8b9 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "fork": "node --max-old-space-size=4096 ./node_modules/.bin/hardhat node --target-network mainnet", "test": "hardhat test", "describe": "hardhat describe", - "test:gas": "hardhat test --gas --optimizer || cat test-gas-used.log", "test:deployments": "mocha test/deployments -- --timeout 60000", "test:etherscan": "node test/etherscan", "test:publish": "concurrently --kill-others --success first \"npx hardhat node > /dev/null\" \"wait-port 8545 && mocha test/publish --bail --timeout 240000\""