Remove ganache-cli related code from API & tests (#849)

pull/851/head
cgewecke 9 months ago committed by GitHub
parent 6739db6265
commit c6eea4b130
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      README.md
  2. 2
      docs/advanced.md
  3. 69
      docs/api.md
  4. 109
      lib/api.js
  5. 12
      lib/ui.js
  6. 2
      package.json
  7. 21
      plugins/hardhat.plugin.js
  8. 18
      plugins/resources/nomiclabs.ui.js
  9. 58
      plugins/resources/nomiclabs.utils.js
  10. 31
      plugins/resources/plugin.utils.js
  11. 34
      test/integration/errors.js
  12. 33
      test/integration/flags.js
  13. 88
      test/integration/standard.js
  14. 1
      test/sources/projects/ganache-solcoverjs/.gitignore
  15. 5
      test/sources/projects/ganache-solcoverjs/.solcover.js
  16. 17
      test/sources/projects/ganache-solcoverjs/contracts/ContractA.sol
  17. 17
      test/sources/projects/ganache-solcoverjs/contracts/ContractB.sol
  18. 17
      test/sources/projects/ganache-solcoverjs/contracts/ContractC.sol
  19. 23
      test/sources/projects/ganache-solcoverjs/contracts/Migrations.sol
  20. 14
      test/sources/projects/ganache-solcoverjs/hardhat.config.js
  21. 15
      test/sources/projects/ganache-solcoverjs/test/contracta.js
  22. 15
      test/sources/projects/ganache-solcoverjs/test/contractb.js
  23. 20
      test/sources/projects/ganache-solcoverjs/test/contractc.js
  24. 32
      test/units/api.js
  25. 2
      test/units/validator.js
  26. 113
      yarn.lock

@ -46,7 +46,6 @@ npx hardhat coverage [command-options]
| testfiles | `--testfiles "test/registry/*.ts"` | Test file(s) to run. (Globs must be enclosed by quotes and use [globby matching patterns][38])|
| sources | `--sources myFolder` or `--sources myFile.sol` | Path to *single* folder or file to target for coverage. Path is relative to Hardhat's `paths.sources` (usually `contracts/`) |
| solcoverjs | `--solcoverjs ./../.solcover.js` | Relative path from working directory to config. Useful for monorepo packages that share settings. (Path must be "./" prefixed) |
| network | `--network development` | Use network settings defined in the Hardhat config |
| temp[<sup>*</sup>][14] | `--temp build` | :warning: **Caution** :warning: Path to a *disposable* folder to store compilation artifacts in. Useful when your test setup scripts include hard-coded paths to a build directory. [More...][14] |
| matrix | `--matrix` | Generate a JSON object that maps which mocha tests hit which lines of code. (Useful as an input for some fuzzing, mutation testing and fault-localization algorithms.) [More...][39]|
@ -83,11 +82,9 @@ module.exports = {
| onPreCompile[<sup>*</sup>][14] | *Function* | | Hook run *after* filesystem and compiler configuration is applied, *before* the compiler is run. Can be used with the other hooks to be able to generate coverage reports on non-standard / customized directory structures, as well as contracts with absolute import paths. [More...][23] |
| onCompileComplete[<sup>*</sup>][14] | *Function* | | Hook run *after* compilation completes, *before* tests are run. Useful if you have secondary compilation steps or need to modify built artifacts. [More...][23]|
| onTestsComplete[<sup>*</sup>][14] | *Function* | | Hook run *after* the tests complete, *before* Istanbul reports are generated. [More...][23]|
| onIstanbulComplete[<sup>*</sup>][14] | *Function* | | Hook run *after* the Istanbul reports are generated, *before* the ganache server is shut down. Useful if you need to clean resources up. [More...][23]|
| onIstanbulComplete[<sup>*</sup>][14] | *Function* | | Hook run *after* the Istanbul reports are generated, *before* the coverage task completes. Useful if you need to clean resources up. [More...][23]|
| configureYulOptimizer | *Boolean* | false | (Experimental) Setting to `true` should resolve "stack too deep" compiler errors in large projects using ABIEncoderV2 |
| solcOptimizerDetails | *Object* | `undefined` |(Experimental) Must be used in combination with `configureYulOptimizer`. Allows you configure solc's [optimizer details][1001]. Useful if the default remedy for stack-too-deep errors doesn't work in your case (See FAQ below). |
| client | *Object* | `require("ganache-core")` | Ganache only: useful if you need a specific ganache version |
| providerOptions | *Object* | `{ }` | Ganache only: [ganache-core options][1] |
[<sup>*</sup> Advanced use][14]
@ -143,7 +140,6 @@ $ git clone https://github.com/sc-forks/solidity-coverage.git
$ yarn
```
[1]: https://github.com/trufflesuite/ganache-core#options
[2]: https://istanbul.js.org/docs/advanced/alternative-reporters/
[3]: https://mochajs.org/api/mocha
[4]: https://github.com/sc-forks/solidity-coverage/blob/master/docs/faq.md#running-out-of-gas

@ -27,7 +27,7 @@ module.exports = {
The plugin exposes a set of workflow hooks that let you run arbitrary async logic between the main
stages of the coverage generation process. These are useful for tasks like launching secondary
services which need to connect to a running ganache instance (ex: the Oraclize/Provable bridge),
services which need to connect to a running ethereum client instance (ex: the Oraclize/Provable bridge),
or reading data from the compilation artifacts to run special preparatory steps for your tests.
The stages/hooks are (in order of execution):

@ -8,9 +8,9 @@ table below shows how its core methods relate to the stages of a test run:
| Test Stage <img width=200/> | API Method <img width=200/> | Description <img width=800/> |
|---------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| compilation | `instrument` | A **pre-compilation** step: Rewrites contracts and generates an instrumentation data map. |
| client launch | `ganache` | A **substitute** step: Launches a ganache client with coverage collection enabled in its VM. As the client runs it will mark line/branch hits on the instrumentation data map. |
| client launch | `attachToHardhatVM` | A **pre-test** step: Enables coverage collection enabled in a HardhatEVM client. As the client runs it will mark line/branch hits on the instrumentation data map. |
| test | `report` | A **post-test** step: Generates a coverage report from the data collected by the VM after tests complete. |
| exit | `finish` | A **substitute** step: Shuts client down |
[3]: https://github.com/gotwarlost/istanbul
@ -20,8 +20,8 @@ table below shows how its core methods relate to the stages of a test run:
disposable set of contracts/artifacts which coverage must use in lieu of the 'real' (uninstrumented)
contracts.
+ there are two complete [coverage tool/plugin implementations][5] (for Hardhat and Truffle)
which can be used as sources if you're building something similar.
+ there is a complete [coverage tool/plugin implementation][5] for Hardhat
which can be used as a source if you're building something similar.
[5]: https://github.com/sc-forks/solidity-coverage/tree/master/plugins
@ -31,9 +31,8 @@ which can be used as sources if you're building something similar.
- [API Methods](#api)
* [constructor](#constructor)
* [instrument](#instrument)
* [ganache](#ganache)
* [attachToHardhatVM](#attachToHardhatVM)
* [report](#report)
* [finish](#finish)
* [getInstrumentationData](#getinstrumentationdata)
* [setInstrumentationData](#setinstrumentationdata)
- [Utils Methods](#utils)
@ -64,13 +63,10 @@ Creates a coverage API instance. Configurable.
| ------ | ---- | ------- | ----------- |
| port | *Number* | 8555 | Port to launch client on |
| silent | *Boolean* | false | Suppress logging output |
| client | *Object* | `require("ganache-core")` | JS Ethereum client |
| providerOptions | *Object* | `{ }` | [ganache-core options][1] |
| skipFiles | *Array* | `[]` | Array of contracts or folders (with paths expressed relative to the `contracts` directory) that should be skipped when doing instrumentation. |
| istanbulFolder | *String* | `./coverage` | Folder location for Istanbul coverage reports. |
| istanbulReporter | *Array* | `['html', 'lcov', 'text', 'json']` | [Istanbul coverage reporters][2] |
[1]: https://github.com/trufflesuite/ganache-core#options
[2]: https://istanbul.js.org/docs/advanced/alternative-reporters/
--------------
@ -100,33 +96,37 @@ const instrumented = api.instrument(contracts)
--------------
## ganache
## attachToHardhatVM
Enables coverage data collection on an in-process ganache server. By default, this method launches
the server, begins listening on the port specified in the [config](#constructor) (or 8555 if unspecified), and
returns a url string. When `autoLaunchServer` is false, method returns `ganache.server` so you can control
the `server.listen` invocation yourself.
Enables coverage data collection on a HardhatEVM provider. (You will need to create a hardhat provider with the correct VM settings as shown below before invoking this method.)
**Parameters**
- `client` **Object**: (*Optional*) ganache module
- `autoLaunchServer` **Boolean**: (*Optional*)
- `provider` **Object**: Hardhat provider
Returns **Promise** Address of server to connect to, or initialized, unlaunched server
Returns **Promise**
**Example**
```javascript
const client = require('ganache-cli');
const { createProvider } = require("hardhat/internal/core/providers/construction");
const { resolveConfig } = require("hardhat/internal/core/config/config-resolution");
const { HARDHAT_NETWORK_NAME } = require("hardhat/plugins")
const api = new CoverageAPI( { client: client } );
const address = await api.ganache();
const api = new CoverageAPI( { ... } );
const config = resolveConfig("./", {});
> http://127.0.0.1:8555
config.networks[HARDHAT_NETWORK_NAME].allowUnlimitedContractSize = true;
config.networks[HARDHAT_NETWORK_NAME].blockGasLimit = api.gasLimitNumber;
config.networks[HARDHAT_NETWORK_NAME].gas = api.gasLimit;
config.networks[HARDHAT_NETWORK_NAME].gasPrice = api.gasPrice;
config.networks[HARDHAT_NETWORK_NAME].initialBaseFeePerGas = 0;
// Alternatively...
const provider = await createProvider(
config,
HARDHAT_NETWORK_NAME
)
const server = await api.ganache(client, false);
await pify(server.listen)(8545));
await api.attachToHardhatVM(provider);
```
--------------
@ -148,22 +148,6 @@ await api.report('./coverage_4A3cd2b'); // Default folder name is 'coverage'
-------------
## finish
Shuts down coverage-enabled ganache server instance
Returns **Promise**
**Example**
```javascript
const client = require('ganache-cli');
await api.ganache(client); // Server listening...
await api.finish(); // Server shut down.
```
-------------
## getInstrumentationData
Returns a copy of the hit map created during instrumentation. Useful if you'd like to delegate
@ -193,7 +177,7 @@ const data = load(data); // Pseudo-code
api.setIntrumentationData(data);
// Client will collect data for the loaded map
const address = await api.ganache(client);
await api.attachToHardhatVM(provider);
// Or to `report` instrumentation data which was collected in a different process.
const data = load(data); // Pseudo-code
@ -348,8 +332,7 @@ utils.save(instrumented, config.contractsDir, tempContractsDir);
## finish
Deletes temporary folders and shuts the ganache server down. Is tolerant - if folders or ganache
server don't exist it will return silently.
Deletes temporary folders. Is tolerant - if folders don't exist it will return silently.
**Parameters**

@ -4,7 +4,6 @@ const fs = require('fs');
const path = require('path');
const istanbul = require('sc-istanbul');
const assert = require('assert');
const detect = require('detect-port');
const _ = require('lodash/lang');
const ConfigValidator = require('./validator');
@ -45,20 +44,10 @@ class API {
this.onIstanbulComplete = config.onIstanbulComplete || this.defaultHook;
this.onPreCompile = config.onPreCompile || this.defaultHook;
this.server = null;
this.defaultPort = 8555;
this.client = config.client;
this.defaultNetworkName = 'soliditycoverage';
this.port = config.port || this.defaultPort;
this.host = config.host || "127.0.0.1";
this.providerOptions = config.providerOptions || {};
this.autoLaunchServer = config.autoLaunchServer === false ? false : true;
this.skipFiles = config.skipFiles || [];
this.log = config.log || console.log;
this.gasLimit = 0xffffffffff // default "gas sent" with transactions
this.gasLimitString = "0x1fffffffffffff"; // block gas limit for ganache (higher than "gas sent")
this.gasLimitNumber = 0x1fffffffffffff; // block gas limit for Hardhat
this.gasPrice = 0x01;
@ -138,56 +127,6 @@ class API {
this.instrumenter.instrumentationData = _.cloneDeep(data);
}
/**
* Enables coverage collection on in-process ethereum client server, hooking the DataCollector
* to its VM. By default, method will return a url after server has begun listening on the port
* specified in the config. When `autoLaunchServer` is false, method returns`ganache.server` so
* the consumer can control the 'server.listen' invocation themselves.
* @param {Object} client ganache client
* @param {Boolean} autoLaunchServer boolean
* @return {<Promise> (String | Server) } address of server to connect to, or initialized, unlaunched server.
*/
async ganache(client, autoLaunchServer){
// Check for port-in-use
if (await detect(this.port) !== this.port){
throw new Error(this.ui.generate('server-fail', [this.port]))
}
this.collector = new DataCollector(this.instrumenter.instrumentationData);
this.providerOptions.gasLimit =
'gasLimit' in this.providerOptions
? this.providerOptions.gasLimit
: this.gasLimitString;
this.providerOptions.allowUnlimitedContractSize =
'allowUnlimitedContractSize' in this.providerOptions
? this.providerOptions.allowUnlimitedContractSize
: true;
// Attach to vm step of supplied client
try {
if (this.config.forceBackupServer) throw new Error()
await this.attachToGanacheVM(client)
}
// Fallback to ganache-cli)
catch(err) {
const _ganache = require('ganache-cli');
this.ui.report('vm-fail', [_ganache.version]);
await this.attachToGanacheVM(_ganache);
}
if (autoLaunchServer === false || this.autoLaunchServer === false){
return this.server;
}
await pify(this.server.listen)(this.port);
const address = `http://${this.host}:${this.port}`;
this.ui.report('server', [address]);
return address;
}
/**
* Generate coverage / write coverage report / run istanbul
*/
@ -223,60 +162,16 @@ class API {
})
}
/**
* Removes coverage build artifacts, kills testrpc.
*/
async finish() {
if (this.server && this.server.close){
this.ui.report('finish');
await pify(this.server.close)();
}
}
async finish() { /* Just a stub now - used to shutdown ganache */}
// ------------------------------------------ Utils ----------------------------------------------
// ========
// Provider
// ========
async attachToGanacheVM(client){
const self = this;
// Fallback to client from options
if(!client) client = this.client;
this.server = client.server(this.providerOptions);
this.assertHasBlockchain(this.server.provider);
await this.vmIsResolved(this.server.provider);
const blockchain = this.server.provider.engine.manager.state.blockchain;
const createVM = blockchain.createVMFromStateTrie;
// Attach to VM which ganache has already created for transactions
blockchain.vm.on('step', self.collector.step.bind(self.collector));
// Hijack createVM method which ganache runs for each `eth_call`
blockchain.createVMFromStateTrie = function(state, activatePrecompiles) {
const vm = createVM.apply(blockchain, arguments);
vm.on('step', self.collector.step.bind(self.collector));
return vm;
}
}
assertHasBlockchain(provider){
assert(provider.engine.manager.state.blockchain !== undefined);
assert(provider.engine.manager.state.blockchain.createVMFromStateTrie !== undefined);
}
async vmIsResolved(provider){
return new Promise(resolve => {
const interval = setInterval(() => {
if (provider.engine.manager.state.blockchain.vm !== undefined){
clearInterval(interval);
resolve();
}
});
})
}
// Hardhat
async attachToHardhatVM(provider){

@ -56,10 +56,6 @@ class AppUI extends UI {
const w = ":warning:";
const kinds = {
'vm-fail': `${w} ${c.red('There was a problem attaching to the ganache VM.')}\n` +
`${w} ${c.red('For help, see the "client" & "providerOptions" syntax in solidity-coverage docs.')}\n`+
`${w} ${c.red(`Using ganache-cli (v${args[0]}) instead.`)}\n`,
'instr-start': `\n${c.bold('Instrumenting for coverage...')}` +
`\n${c.bold('=============================')}\n`,
@ -69,10 +65,6 @@ class AppUI extends UI {
'istanbul': `${ct} ${c.grey('Istanbul reports written to')} ./coverage/ ` +
`${c.grey('and')} ./coverage.json`,
'finish': `${ct} ${c.grey('solidity-coverage cleaning up, shutting down ganache server')}`,
'server': `${ct} ${c.bold('server: ')} ${c.grey(args[0])}`,
'command': `\n${w} ${c.red.bold('solidity-coverage >= 0.7.0 is no longer a shell command.')} ${w}\n` +
`${c.bold('=============================================================')}\n\n` +
`Instead, you should use the plugin produced for your development stack\n` +
@ -103,10 +95,6 @@ class AppUI extends UI {
'istanbul-fail': `${c.red('Istanbul coverage reports could not be generated. ')}`,
'sources-fail': `${c.red('Cannot locate expected contract sources folder: ')} ${args[0]}`,
'server-fail': `${c.red('Port')} ${args[0]} ${c.red('is already in use.\n')}` +
`${c.red('\tRun: "lsof -i" to find the pid of the process using it.\n')}` +
`${c.red('\tRun: "kill -9 <pid>" to kill it.\n')}`
}
return this._format(kinds[kind])

@ -26,7 +26,6 @@
"@solidity-parser/parser": "^0.18.0",
"chalk": "^2.4.2",
"death": "^1.1.0",
"detect-port": "^1.3.0",
"difflib": "^0.2.4",
"fs-extra": "^8.1.0",
"ghost-testrpc": "^0.0.2",
@ -52,7 +51,6 @@
"decache": "^4.5.1",
"ethereum-waffle": "^3.4.0",
"ethers": "^5.5.3",
"ganache-cli": "6.12.2",
"hardhat": "^2.19.5",
"hardhat-gas-reporter": "^1.0.1",
"nyc": "^14.1.1",

@ -111,7 +111,6 @@ task("coverage", "Generates a code coverage report for tests")
let ui;
let api;
let config;
let client;
let address;
let failedTests = 0;
@ -143,6 +142,11 @@ task("coverage", "Generates a code coverage report for tests")
}
env.hardhatArguments = Object.assign(env.hardhatArguments, flags)
// Error if --network flag is set
if (env.hardhatArguments.network){
throw new Error(ui.generate('network-fail'));
}
// ===========================
// Generate abi diff component
// (This flag only useful within codecheck context)
@ -201,7 +205,6 @@ task("coverage", "Generates a code coverage report for tests")
// ==============
let network = await nomiclabsUtils.setupHardhatNetwork(env, api, ui);
if (network.isHardhatEVM){
accounts = await utils.getAccountsHardhat(network.provider);
nodeInfo = await utils.getNodeInfoHardhat(network.provider);
@ -217,20 +220,6 @@ task("coverage", "Generates a code coverage report for tests")
nodeInfo.split('/')[1],
env.network.name,
]);
} else {
client = api.client || require('ganache-cli');
address = await api.ganache(client);
const accountsRequest = await utils.getAccountsGanache(api.server.provider);
const nodeInfoRequest = await utils.getNodeInfoGanache(api.server.provider);
ui.report('ganache-network', [
nodeInfoRequest.result.split('/')[1],
env.network.name,
api.port
]);
accounts = accountsRequest.result;
}
// Set default account (if not already configured)
nomiclabsUtils.setNetworkFrom(network.config, accounts);

@ -45,9 +45,6 @@ class PluginUI extends UI {
'instr-skipped': `${ds} ${c.grey(args[0])}`,
'versions': `${ct} ${c.bold('ganache-core')}: ${args[0]}\n` +
`${ct} ${c.bold('solidity-coverage')}: v${args[1]}`,
'hardhat-versions': `\n${c.bold('Version')}` +
`\n${c.bold('=======')}\n` +
`${ct} ${c.bold('solidity-coverage')}: v${args[0]}`,
@ -57,17 +54,6 @@ class PluginUI extends UI {
`${ct} ${c.bold('HardhatEVM')}: v${args[0]}\n` +
`${ct} ${c.bold('network')}: ${args[1]}\n`,
'ganache-network': `\n${c.bold('Network Info')}` +
`\n${c.bold('============')}\n` +
`${ct} ${c.bold('port')}: ${args[1]}\n` +
`${ct} ${c.bold('network')}: ${args[0]}\n`,
'port-clash': `${w} ${c.red("The 'port' values in your config's network url ")}` +
`${c.red("and .solcover.js are different. Using network's: ")} ${c.bold(args[0])}.\n`,
'port-clash-hardhat': `${w} ${c.red("The 'port' values in your Hardhat network's url ")}` +
`${c.red("and .solcover.js are different. Using Hardhat's: ")} ${c.bold(args[0])}.\n`,
}
this._write(kinds[kind]);
@ -84,8 +70,8 @@ class PluginUI extends UI {
const x = ":x:";
const kinds = {
'network-fail': `${c.red('--network argument: ')}${args[0]}` +
`${c.red(' is not a defined network in hardhat.config.js.')}`,
'network-fail': `${c.red('--network cli flag is not supported for the coverage task. ')}` +
`${c.red('Beginning with v0.8.7, coverage must use the default "hardhat" network.')}`,
'sources-fail': `${c.red('Cannot locate expected contract sources folder: ')} ${args[0]}`,

@ -64,62 +64,34 @@ async function setupHardhatNetwork(env, api, ui){
const newCreateProviderSignature = semver.satisfies(hardhatPackage.version, "^2.15.0");
let provider, networkName, networkConfig;
let isHardhatEVM = false;
networkName = env.hardhatArguments.network || HARDHAT_NETWORK_NAME;
// HardhatEVM
if (networkName === HARDHAT_NETWORK_NAME){
isHardhatEVM = true;
networkConfig = env.network.config;
configureHardhatEVMGas(networkConfig, api);
if (newCreateProviderSignature) {
provider = await createProvider(
env.config,
networkName,
HARDHAT_NETWORK_NAME,
env.artifacts,
)
} else {
provider = createProvider(
networkName,
HARDHAT_NETWORK_NAME,
networkConfig,
env.config.paths,
env.artifacts,
)
}
// HttpProvider
} else {
if (!(env.config.networks && env.config.networks[networkName])){
throw new Error(ui.generate('network-fail', [networkName]))
}
networkConfig = env.config.networks[networkName]
configureNetworkGas(networkConfig, api);
configureHttpProvider(networkConfig, api, ui)
if (newCreateProviderSignature) {
provider = await createProvider(env.config, networkName);
} else {
provider = createProvider(networkName, networkConfig);
}
}
return configureNetworkEnv(
env,
networkName,
HARDHAT_NETWORK_NAME,
networkConfig,
provider,
isHardhatEVM
provider
)
}
function configureNetworkGas(networkConfig, api){
networkConfig.gas = api.gasLimit;
networkConfig.gasPrice = api.gasPrice;
}
function configureHardhatEVMGas(networkConfig, api){
networkConfig.allowUnlimitedContractSize = true;
networkConfig.blockGasLimit = api.gasLimitNumber;
@ -128,7 +100,7 @@ function configureHardhatEVMGas(networkConfig, api){
networkConfig.initialBaseFeePerGas = 0;
}
function configureNetworkEnv(env, networkName, networkConfig, provider, isHardhatEVM){
function configureNetworkEnv(env, networkName, networkConfig, provider){
env.config.networks[networkName] = networkConfig;
env.config.defaultNetwork = networkName;
@ -136,7 +108,7 @@ function configureNetworkEnv(env, networkName, networkConfig, provider, isHardha
name: networkName,
config: networkConfig,
provider: provider,
isHardhatEVM: isHardhatEVM
isHardhatEVM: true
});
env.ethereum = provider;
@ -145,24 +117,6 @@ function configureNetworkEnv(env, networkName, networkConfig, provider, isHardha
return env.network;
}
/**
* Extracts port from url / sets network.url
* @param {Object} networkConfig
* @param {SolidityCoverage} api
*/
function configureHttpProvider(networkConfig, api, ui){
const configPort = networkConfig.url.split(':')[2];
// Warn: port conflicts
if (api.port !== api.defaultPort && api.port !== configPort){
ui.report('port-clash', [ configPort ])
}
// Prefer network port
api.port = parseInt(configPort);
networkConfig.url = `http://${api.host}:${api.port}`;
}
/**
* Configures mocha to generate a json object which maps which tests
* hit which lines of code.

@ -262,35 +262,6 @@ async function getNodeInfoHardhat(provider){
return provider.send("web3_clientVersion", [])
}
async function getAccountsGanache(provider){
const payload = {
jsonrpc: "2.0",
method: "eth_accounts",
params: [],
id: 1
};
return ganacheRequest(provider, payload)
}
async function getNodeInfoGanache(provider){
const payload = {
jsonrpc: "2.0",
method: "web3_clientVersion",
params: [],
id: 1
};
return ganacheRequest(provider, payload)
}
async function ganacheRequest(provider, payload){
return new Promise((resolve, reject) => {
provider.sendAsync(payload, function(err, res){
if (err) return reject(err)
resolve(res);
})
});
}
// ==========================
// Finishing / Cleanup
// ==========================
@ -330,6 +301,4 @@ module.exports = {
setupTempFolders,
getAccountsHardhat,
getNodeInfoHardhat,
getAccountsGanache,
getNodeInfoGanache
}

@ -3,7 +3,6 @@ const fs = require('fs');
const path = require('path')
const pify = require('pify')
const shell = require('shelljs');
const ganache = require('ganache-cli')
const verify = require('./../util/verifiers')
const mock = require('./../util/integration');
@ -64,37 +63,11 @@ describe('Hardhat Plugin: error cases', function() {
}
});
it('tries to launch with a port already in use', async function(){
it('tries to launch with the network flag', async function(){
const taskArgs = {
network: "development"
}
const server = ganache.server();
mock.install('Simple', 'simple.js', solcoverConfig);
mock.hardhatSetupEnv(this);
await pify(server.listen)(8545);
try {
await this.env.run("coverage", taskArgs);
assert.fail();
} catch(err){
assert(
err.message.includes('already in use') &&
err.message.includes('lsof'),
`Should error on port-in-use with advice: ${err.message}`
)
}
await pify(server.close)();
});
it('tries to launch with a non-existent network', async function(){
const taskArgs = {
network: "does-not-exist"
}
mock.install('Simple', 'simple.js', solcoverConfig);
mock.hardhatSetupEnv(this);
@ -103,9 +76,8 @@ describe('Hardhat Plugin: error cases', function() {
assert.fail();
} catch(err){
assert(
err.message.includes('is not a defined network in hardhat.config.js') &&
err.message.includes('does-not-exist'),
`Should error missing network error: ${err.message}`
err.message.includes('--network cli flag is not supported') &&
`Should error network flag disallowed: ${err.message}`
)
}
});

@ -51,39 +51,6 @@ describe('Hardhat Plugin: command line options', function() {
verify.lineCoverage(expected);
});
it('--network (declared port mismatches)', async function(){
solcoverConfig.port = 8222;
mock.install('Simple', 'simple.js', solcoverConfig);
mock.hardhatSetupEnv(this);
this.env.hardhatArguments.network = "development";
await this.env.run("coverage");
assert(
mock.loggerOutput.val.includes("The 'port' values"),
`Should notify about mismatched port values: ${mock.loggerOutput.val}`
);
assert(
mock.loggerOutput.val.includes("8545"),
`Should have used default coverage port 8545: ${mock.loggerOutput.val}`
);
assert(
mock.loggerOutput.val.includes("development"),
`Should have used specified network name: ${mock.loggerOutput.val}`
);
const expected = [{
file: mock.pathToContract(hardhatConfig, 'Simple.sol'),
pct: 100
}];
verify.lineCoverage(expected);
});
it('--testfiles test/<fileName>', async function() {
const taskArgs = {
testfiles: path.join(

@ -53,25 +53,6 @@ describe('Hardhat Plugin: standard use cases', function() {
);
});
it('default network ("hardhat")', async function(){
mock.install('Simple', 'simple.js', solcoverConfig);
mock.hardhatSetupEnv(this);
this.env.hardhatArguments.network = "hardhat"
await this.env.run("coverage");
assert(
mock.loggerOutput.val.includes("HardhatEVM"),
`Should have displayed HardhatEVM version: ${mock.loggerOutput.val}`
);
assert(
mock.loggerOutput.val.includes("hardhat"),
`Should have used 'hardhat' network name: ${mock.loggerOutput.val}`
);
});
it('uses inheritance', async function() {
mock.installDouble(
['Proxy', 'Owned'],
@ -193,20 +174,6 @@ describe('Hardhat Plugin: standard use cases', function() {
verify.lineCoverage(expected);
});
// hardhat-truffle5 test asserts balance is 777 ether
it('config: providerOptions', async function() {
const taskArgs = {
network: 'development'
};
solcoverConfig.providerOptions = { default_balance_ether: 777 }
mock.install('Simple', 'testrpc-options.js', solcoverConfig);
mock.hardhatSetupEnv(this);
await this.env.run("coverage", taskArgs);
});
it('config: skipped file', async function() {
solcoverConfig.skipFiles = ['Migrations.sol', 'Owned.sol'];
@ -328,61 +295,6 @@ describe('Hardhat Plugin: standard use cases', function() {
await this.env.run("coverage");
});
it('uses account[0] as default "from" (ganache)', async function(){
const mnemonic = "purity blame spice arm main narrow olive roof science verb parrot flash";
const account0 = "0x42ecc9ab31d7c0240532992682ee3533421dd7f5"
const taskArgs = {
network: "development"
}
solcoverConfig.providerOptions = {
mnemonic: mnemonic
};
mock.install('Simple', 'account-zero.js', solcoverConfig);
mock.hardhatSetupEnv(this);
await this.env.run("coverage", taskArgs);
const expected = [
{
file: mock.pathToContract(hardhatConfig, 'Simple.sol'),
pct: 50
}
];
verify.lineCoverage(expected);
})
it('inherits network defined "from" (ganache)', async function(){
const mnemonic = "purity blame spice arm main narrow olive roof science verb parrot flash";
const account1 = "0xe7a46b209a65baadc11bf973c0f4d5f19465ae83"
const taskArgs = {
network: "development"
}
solcoverConfig.providerOptions = {
mnemonic: mnemonic
};
const hardhatConfig = mock.getDefaultHardhatConfig()
hardhatConfig.networks.development.from = account1;
mock.install('Simple', 'account-one.js', solcoverConfig, hardhatConfig);
mock.hardhatSetupEnv(this);
await this.env.run("coverage", taskArgs);
const expected = [
{
file: mock.pathToContract(hardhatConfig, 'Simple.sol'),
pct: 50
}
];
verify.lineCoverage(expected);
})
it('inherits network defined "from" (hardhat)', async function(){
const mnemonic = "purity blame spice arm main narrow olive roof science verb parrot flash";
const account1 = "0xe7a46b209a65baadc11bf973c0f4d5f19465ae83"

@ -1,5 +0,0 @@
module.exports = {
client: require('ganache-cli'),
silent: process.env.SILENT ? true : false,
istanbulReporter: ['json-summary', 'text'],
}

@ -1,17 +0,0 @@
pragma solidity >=0.8.0 <0.9.0;
contract ContractA {
uint x;
constructor() public {
}
function sendFn() public {
x = 5;
}
function callFn() public pure returns (uint){
uint y = 5;
return y;
}
}

@ -1,17 +0,0 @@
pragma solidity >=0.8.0 <0.9.0;
contract ContractB {
uint x;
constructor() public {
}
function sendFn() public {
x = 5;
}
function callFn() public pure returns (uint){
uint y = 5;
return y;
}
}

@ -1,17 +0,0 @@
pragma solidity >=0.8.0 <0.9.0;
contract ContractC {
uint x;
constructor() public {
}
function sendFn() public {
x = 5;
}
function callFn() public pure returns (uint){
uint y = 5;
return y;
}
}

@ -1,23 +0,0 @@
pragma solidity >=0.4.21 <0.6.0;
contract Migrations {
address public owner;
uint public last_completed_migration;
constructor() public {
owner = msg.sender;
}
modifier restricted() {
if (msg.sender == owner) _;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}

@ -1,14 +0,0 @@
require("@nomiclabs/hardhat-truffle5");
require(__dirname + "/../plugins/nomiclabs.plugin");
module.exports = {
solidity: {
version: "0.8.17"
},
networks: {
coverage: {
url: "http://127.0.0.1:8555"
}
}.
logger: process.env.SILENT ? { log: () => {} } : console,
};

@ -1,15 +0,0 @@
const ContractA = artifacts.require("ContractA");
contract("contracta", function(accounts) {
let instance;
before(async () => instance = await ContractA.new())
it('sends [ @skipForCoverage ]', async function(){
await instance.sendFn();
});
it('calls [ @skipForCoverage ]', async function(){
await instance.callFn();
})
});

@ -1,15 +0,0 @@
const ContractB = artifacts.require("ContractB");
contract("contractB [ @skipForCoverage ]", function(accounts) {
let instance;
before(async () => instance = await ContractB.new())
it('sends', async function(){
await instance.sendFn();
});
it('calls', async function(){
await instance.callFn();
})
});

@ -1,20 +0,0 @@
const ContractC = artifacts.require("ContractC");
contract("contractc", function(accounts) {
let instance;
before(async () => instance = await ContractC.new())
it('sends', async function(){
await instance.sendFn();
});
it('calls', async function(){
await instance.callFn();
})
it('sends', async function(){
await instance.sendFn();
});
});

@ -1,6 +1,4 @@
const assert = require('assert');
const detect = require('detect-port');
const Ganache = require('ganache-cli');
const util = require('./../util/util.js');
const API = require('./../../api.js');
@ -58,36 +56,6 @@ describe('api', () => {
assert(cloneC[hash].hits === 5);
});
it('ganache: autoLaunchServer === false', async function(){
const api = new API(opts);
const port = api.port;
const server = await api.ganache(Ganache, false);
assert(typeof port === 'number')
assert(typeof server === 'object');
assert(typeof server.listen === 'function');
const freePort = await detect(port);
assert(freePort === port);
});
it('config: autoLaunchServer: false', async function(){
opts.autoLaunchServer = false;
const api = new API(opts);
const port = api.port;
const server = await api.ganache(Ganache);
assert(typeof port === 'number')
assert(typeof server === 'object');
assert(typeof server.listen === 'function');
const freePort = await detect(port);
assert(freePort === port);
})
it('utils', async function(){
assert(utils.assembleFiles !== undefined)
assert(utils.checkContext !== undefined)

@ -21,7 +21,6 @@ describe('config validation', () => {
it('validates the "string" options', function(){
const options = [
"cwd",
"host",
"istanbulFolder",
"abiOutputPath",
"matrixOutputPath",
@ -47,7 +46,6 @@ describe('config validation', () => {
it('validates the "boolean" options', function(){
const options = [
"silent",
"autoLaunchServer",
"measureStatementCoverage",
"measureFunctionCoverage",
"measureModifierCoverage",

@ -1508,10 +1508,6 @@ accepts@~1.3.8:
mime-types "~2.1.34"
negotiator "0.6.3"
address@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6"
adm-zip@^0.4.16:
version "0.4.16"
resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365"
@ -3053,7 +3049,7 @@ cross-spawn@^4:
lru-cache "^4.0.1"
which "^1.2.9"
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
dependencies:
@ -3114,7 +3110,7 @@ death@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/death/-/death-1.1.0.tgz#01aa9c401edd92750514470b8266390c66c67318"
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9:
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
@ -3269,13 +3265,6 @@ detect-indent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
detect-port@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.3.0.tgz#d9c40e9accadd4df5cac6a782aefd014d573d1f1"
dependencies:
address "^1.0.1"
debug "^2.6.0"
diff@3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
@ -4045,18 +4034,6 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
md5.js "^1.3.4"
safe-buffer "^5.1.1"
execa@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
dependencies:
cross-spawn "^6.0.0"
get-stream "^4.0.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
expand-brackets@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
@ -4427,14 +4404,6 @@ functions-have-names@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
ganache-cli@6.12.2:
version "6.12.2"
resolved "https://registry.yarnpkg.com/ganache-cli/-/ganache-cli-6.12.2.tgz#c0920f7db0d4ac062ffe2375cb004089806f627a"
dependencies:
ethereumjs-util "6.2.1"
source-map-support "0.5.12"
yargs "13.2.4"
ganache-core@^2.13.2:
version "2.13.2"
resolved "https://registry.yarnpkg.com/ganache-core/-/ganache-core-2.13.2.tgz#27e6fc5417c10e6e76e2e646671869d7665814a3"
@ -4499,7 +4468,7 @@ get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
get-stream@^4.0.0, get-stream@^4.1.0:
get-stream@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
dependencies:
@ -5038,10 +5007,6 @@ invert-kv@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
invert-kv@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
io-ts@1.10.4:
version "1.10.4"
resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-1.10.4.tgz#cd5401b138de88e4f920adbcb7026e2d1967e6e2"
@ -5267,7 +5232,7 @@ is-shared-array-buffer@^1.0.2:
dependencies:
call-bind "^1.0.2"
is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
is-stream@^1.0.0, is-stream@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@ -5609,12 +5574,6 @@ lcid@^1.0.0:
dependencies:
invert-kv "^1.0.0"
lcid@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
dependencies:
invert-kv "^2.0.0"
level-codec@^9.0.0:
version "9.0.2"
resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-9.0.2.tgz#fd60df8c64786a80d44e63423096ffead63d8cbc"
@ -5924,12 +5883,6 @@ make-dir@^2.0.0, make-dir@^2.1.0:
pify "^4.0.1"
semver "^5.6.0"
map-age-cleaner@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
dependencies:
p-defer "^1.0.0"
map-cache@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
@ -5960,14 +5913,6 @@ media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
mem@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178"
dependencies:
map-age-cleaner "^0.1.1"
mimic-fn "^2.0.0"
p-is-promise "^2.0.0"
memdown@^1.0.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/memdown/-/memdown-1.4.1.tgz#b4e4e192174664ffbae41361aa500f3119efe215"
@ -6091,10 +6036,6 @@ mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
mimic-fn@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
mimic-response@^1.0.0, mimic-response@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
@ -6433,12 +6374,6 @@ normalize-url@^4.1.0:
version "4.5.1"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
dependencies:
path-key "^2.0.0"
nth-check@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2"
@ -6615,14 +6550,6 @@ os-locale@^1.4.0:
dependencies:
lcid "^1.0.0"
os-locale@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
dependencies:
execa "^1.0.0"
lcid "^2.0.0"
mem "^4.0.0"
os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@ -6635,18 +6562,10 @@ p-cancelable@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
p-defer@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
p-is-promise@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e"
p-limit@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
@ -6822,7 +6741,7 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
path-key@^2.0.0, path-key@^2.0.1:
path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
@ -7965,10 +7884,6 @@ strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
strip-eof@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
strip-hex-prefix@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz#0c5f155fef1151373377de9dbb588da05500e36f"
@ -9456,7 +9371,7 @@ yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
yargs-parser@13.1.2, yargs-parser@^13.0.0, yargs-parser@^13.1.0, yargs-parser@^13.1.2:
yargs-parser@13.1.2, yargs-parser@^13.0.0, yargs-parser@^13.1.2:
version "13.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
dependencies:
@ -9495,22 +9410,6 @@ yargs-unparser@2.0.0:
flat "^5.0.2"
is-plain-obj "^2.1.0"
yargs@13.2.4:
version "13.2.4"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83"
dependencies:
cliui "^5.0.0"
find-up "^3.0.0"
get-caller-file "^2.0.1"
os-locale "^3.1.0"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^3.0.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^13.1.0"
yargs@13.3.2, yargs@^13.2.2, yargs@^13.3.0:
version "13.3.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"

Loading…
Cancel
Save