diff --git a/README.md b/README.md index 2fceb17e..d502fb36 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,16 @@ module.exports = { [* Advanced use][14] +## API + +Solidity-coverage's core methods and many utilities are available as an API. + +```javascript +const CoverageAPI = require('solidity-coverage/api'); +``` + +[Documentation available here][28]. + ## FAQ Common problems & questions: @@ -171,3 +181,4 @@ $ yarn [25]: https://github.com/sc-forks/solidity-coverage/issues/417 [26]: https://buidler.dev/ [27]: https://www.trufflesuite.com/docs +[28]: https://github.com/sc-forks/solidity-coverage/blob/beta/docs/api.md diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 00000000..8c8d7c63 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,368 @@ +# Solidity-Coverage API Documentation + +`solidity-coverage`'s API provides test coverage measurement for the Solidity language. +The repository contains two complete coverage tool/plugin implementations (for Buidler and Truffle) +which can be used as sources if you're building something similar. + +`solidity-coverage`'s core algorithm resembles the one used by [Istanbul][3] for javascript programs. +It tracks line and branch locations by 'instrumenting' solidity contracts with special solidity +statements and detecting their execution in a coverage-enabled EVM. As such, its API spans the +full set of tasks typically required to run a solidity test suite. + ++ compile ++ ethereum client launch ++ test ++ report outcome and exit + +[3]: https://github.com/gotwarlost/istanbul + +The API's corresponding methods are: + ++ `instrument`: Rewrites contracts for instrumented compilation. Generates an instrumentation data map. ++ `ganache`: 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. ++ `report`: Generates a coverage report from the data collected by the VM after tests complete. Converts + the instrumentation data map into an object IstanbulJS can process. ++ `finish`: Shuts client down + +The library also includes some file system [utilities](#Utils) which are helpful for managing the +disposable set of contracts/artifacts which coverage must use in lieu of the 'real' contracts/artifacts. + +# Table of Contents + +- [API Methods](#api) + * [constructor](#constructor) + * [instrument](#instrument) + * [ganache](#ganache) + * [report](#report) + * [finish](#finish) + * [getInstrumentationData](#getinstrumentationdata) + * [setInstrumentationData](#setinstrumentationdata) +- [Utils Methods](#utils) + * [loadSolcoverJS](#loadsolcoverjs) + * [assembleFiles](#assemblefiles) + * [getTempLocations](#gettemplocations) + * [setupTempFolders](#setuptempfolders) + * [save](#save) + * [finish](#finish-1) + +# API + +**Example** +```javascript +const CoverageAPI = require("solidity-coverage/api"); +const api = new CoverageAPI(options); +``` + +## constructor + +Creates a coverage API instance. Configurable. + +**Parameters** + +- `options` **Object** : API options + +| Option | Type | Default | Description | +| ------ | ---- | ------- | ----------- | +| 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/ + +-------------- + +## instrument + +Instruments a set of sources to prepare them for compilation. + +:warning: **Important:** Instrumented sources must be compiled with **solc optimization OFF** :warning: + +**Parameters** + +- `contracts` **Object[]**: Array of solidity sources and their paths + +Returns **Object[]** in the same format as the `contracts` param, but with sources instrumented. + +**Example** +```javascript +const contracts = [{ + source: "contract Simple { uint x = 5; }", + canonicalPath: "/Users/user/project/contracts/Simple.sol", + relativePath: "Simple.sol" // Optional, used for pretty printing. +},...] + +const instrumented = api.instrument(contracts) +``` + +-------------- + +## ganache + +Enables coverage data collection on an in-process ganache server. By default, will return +a url after server has begun listening on the port specified in the [config](#constructor) +(or 8555 by default). When `autoLaunchServer` is false, method returns`ganache.server` +so the consumer can control the 'server.listen' invocation themselves. + +**Parameters** + +- `client` **Object**: (*Optional*) ganache module +- `autoLaunchServer` **Boolean**: (*Optional*) + +Returns **Promise** Address of server to connect to, or initialized, unlaunched server + +**Example** +```javascript +const client = require('ganache-cli'); + +const api = new CoverageAPI( { client: client } ); +const address = await api.ganache(); + +> http://127.0.0.1:8555 + +// Alternatively... + +const server = await api.ganache(client, false); +await pify(server.listen()(8545)); +``` + +-------------- + +## report + +Generates coverage report using IstanbulJS + +**Parameters** + +- `istanbulFolder` **String**: (*Optional*) path to folder IstanbulJS will deposit coverage reports in. + +Returns **Promise** + +**Example** +```javascript +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 +coverage collection to multiple processes. + +Returns **Object** instrumentation data; + + +**Example** +```javascript +const contracts = api.instrument(contracts); +const data = api.getInstrumentationData(); +save(data); +``` + +------------- + +## setInstrumentationData + +Sets the hit map object generated during instrumentation. Useful if you'd like +to collect or convert data to coverage for an instrumentation which was generated +in a different process. + +**Example** +```javascript +const data = load(data); +api.setIntrumentationData(data); + +// Client will collect data for the loaded map +const address = await api.ganache(client); + +// Or to `report` instrumentation data which was collected in a different process. +const data = load(data); +api.setInstrumentationData(data); + +api.report(); +``` + +---------------------------------------------------------------------------------------------------- + +# Utils + +```javascript +const utils = require('solidity-coverage/utils'); +``` + +Many of the utils methods take a `config` object param which +defines the absolute paths to your project root and contracts directory. + +**Example** +```javascript +const config = { + workingDir: process.cwd(), + contractsDir: path.join(process.cwd(), 'contracts'), +} +``` +------------- + +## loadSolcoverJS + +Loads `.solcoverjs`. Users may specify options described in the README in `.solcover.js` config +file which your application needs to consume. + +**Parameters** + +- `config` **Object**: [See *config* above](#Utils) + +Returns **Object** Normalized coverage config + + +**Example** +```javascript +const solcoverJS = utils.loadSolcoverJS(config); +const api = new CoverageAPI(solcoverJS); +``` + +------------- + +## assembleFiles + +Loads contracts from the filesystem in a format that can be passed directly to the +[api.instrument](#instrument) method. Filters by an optional `skipFiles` parameter. + +**Parameters** + +- `config` **Object**: [See *config* above](#Utils) +- `skipFiles` **String[]**: (*Optional*) Array of files or folders to skip + [See API *constructor*](#constructor) + +Returns **Object** with `targets` and `skipped` keys. These are Object arrays of contract sources +and paths. + +**Example** +```javascript +const { + targets, + skipped +} = utils.assembleFiles(config, ['Migrations.sol']) + +const instrumented = api.instrument(targets); +``` + +-------------- + +## getTempLocations + +Returns a pair of canonically named temporary directory paths for contracts +and artifacts. Instrumented assets can be compiled from and written to these so the unit tests can +use them as sources. + +**Parameters** + +- `config` **Object**: [See *config* above](#Utils) + +Returns **Object** with two absolute paths to disposable folders, `tempContractsDir`, `tempArtifactsDir`. +These directories are named `.coverage_contracts` and `.coverage_artifacts`. + +**Example** +```javascript +const { + tempContractsDir, + tempArtifactsDir +} = utils.getTempLocations(config) + +utils.setupTempFolders(config, tempContractsDir, tempArtifactsDir) + +// Later, you can call `utils.finish` to delete these... +utils.finish(config, api) +``` + +---------- + +## setupTempFolders + +Creates temporary directories to store instrumented contracts and their compilation artifacts in. + +**Parameters** + +- `config` **Object**: [See *config* above](#Utils) +- `tempContractsDir` **String**: absolute path to temporary contracts directory +- `tempArtifactsDir` **String**: absolute path to temporary artifacts directory + +**Example** +```javascript +const { + tempContractsDir, + tempArtifactsDir +} = utils.getTempLocations(config) + +utils.setupTempFolders(config, tempContractsDir, tempArtifactsDir); +``` +------------- + +## save + +Writes an array of instrumented sources in the object format returned by +[api.instrument](#instrument) to a temporary directory. + +**Parameters** + +- `contracts` **Object[]**: array of contracts & paths generated by [api.instrument](#instrument) +- `originalDir` **String**: absolute path to original contracts directory +- `tempDir` **String**: absolute path to temp contracts directory (the destination of the save) + +**Example** +```javascript +const { + tempContractsDir, + tempArtifactsDir +} = utils.getTempLocations(config) + +utils.setupTempFolders(config, tempContractsDir, tempArtifactsDir); + +const instrumented = api.instrument(targets); + +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. + +**Parameters** + +- `config` **Object**: [See *config* above](#Utils) +- `api` **Object**: (*Optional*) coverage api instance whose own `finish` method will be called + +Returns **Promise** + +**Example** +```javascript +await utils.finish(); +``` + + + + diff --git a/lib/api.js b/lib/api.js index 480d33f4..3b5d1cf9 100644 --- a/lib/api.js +++ b/lib/api.js @@ -12,8 +12,6 @@ const Instrumenter = require('./instrumenter'); const Coverage = require('./coverage'); const DataCollector = require('./collector'); const AppUI = require('./ui').AppUI; -const utils = require('./../plugins/resources/plugin.utils'); - /** * Coverage Runner @@ -61,7 +59,6 @@ class API { this.setLoggingLevel(config.silent); this.ui = new AppUI(this.log); - this.utils = utils; } /** @@ -138,7 +135,7 @@ class API { * the consumer can control the 'server.listen' invocation themselves. * @param {Object} client ganache client * @param {Boolean} autoLaunchServer boolean - * @return {String | Server} address of server to connect to, or initialized, unlaunched server. + * @return { (String | Server) } address of server to connect to, or initialized, unlaunched server. */ async ganache(client, autoLaunchServer){ // Check for port-in-use diff --git a/plugins/resources/plugin.utils.js b/plugins/resources/plugin.utils.js index 20c3034a..72fa5a31 100644 --- a/plugins/resources/plugin.utils.js +++ b/plugins/resources/plugin.utils.js @@ -61,8 +61,8 @@ function setupTempFolders(config, tempContractsDir, tempArtifactsDir){ /** * Save a set of instrumented files to a temporary directory. * @param {Object[]} targets array of targets generated by `assembleTargets` - * @param {[type]} originalDir absolute path to parent directory of original source - * @param {[type]} tempDir absolute path to temp parent directory + * @param {[type]} originalDir absolute path to original contracts directory + * @param {[type]} tempDir absolute path to temp contracts directory */ function save(targets, originalDir, tempDir){ let _path; @@ -189,10 +189,11 @@ function assembleSkipped(config, targets, skipFiles=[]){ return skipFiles; } -function loadSolcoverJS(config){ +function loadSolcoverJS(config={}){ let solcoverjs; let coverageConfig; - let ui = new PluginUI(config.logger.log); + let log = config.logger ? config.logger.log : console.log; + let ui = new PluginUI(log); // Handle --solcoverjs flag (config.solcoverjs) @@ -215,7 +216,7 @@ function loadSolcoverJS(config){ } // Truffle writes to coverage config - coverageConfig.log = config.logger.log; + coverageConfig.log = log; coverageConfig.cwd = config.workingDir; coverageConfig.originalContractsDir = config.contractsDir; diff --git a/test/units/api.js b/test/units/api.js index 39f9e314..4290e7a0 100644 --- a/test/units/api.js +++ b/test/units/api.js @@ -1,9 +1,11 @@ const assert = require('assert'); -const util = require('./../util/util.js'); -const API = require('./../../api.js'); const detect = require('detect-port'); const Ganache = require('ganache-cli'); +const util = require('./../util/util.js'); +const API = require('./../../api.js'); +const utils = require('./../../utils.js'); + describe('api', () => { let opts; @@ -86,15 +88,14 @@ describe('api', () => { assert(freePort === port); }) - it('api.utils', async function(){ - const api = new API(opts); - assert(api.utils.assembleFiles !== undefined) - assert(api.utils.checkContext !== undefined) - assert(api.utils.finish !== undefined) - assert(api.utils.getTempLocations !== undefined) - assert(api.utils.setupTempFolders !== undefined) - assert(api.utils.loadSource !== undefined) - assert(api.utils.loadSolcoverJS !== undefined) - assert(api.utils.save !== undefined) + it('utils', async function(){ + assert(utils.assembleFiles !== undefined) + assert(utils.checkContext !== undefined) + assert(utils.finish !== undefined) + assert(utils.getTempLocations !== undefined) + assert(utils.setupTempFolders !== undefined) + assert(utils.loadSource !== undefined) + assert(utils.loadSolcoverJS !== undefined) + assert(utils.save !== undefined) }); }) diff --git a/utils.js b/utils.js new file mode 100644 index 00000000..6520168e --- /dev/null +++ b/utils.js @@ -0,0 +1,4 @@ +// For require('solidity-coverage/utils'); +const utils = require('./plugins/resources/plugin.utils'); + +module.exports = utils;