diff --git a/lib/app.js b/lib/app.js index 99635de..5f316f5 100644 --- a/lib/app.js +++ b/lib/app.js @@ -6,6 +6,7 @@ const istanbul = require('istanbul'); const util = require('util'); const assert = require('assert'); +const ConfigValidator = require('./validator'); const Instrumenter = require('./instrumenter'); const Coverage = require('./coverage'); const DataCollector = require('./collector'); @@ -20,8 +21,12 @@ class App { constructor(config) { this.coverage = new Coverage(); this.instrumenter = new Instrumenter(); + this.validator = new ConfigValidator() this.config = config || {}; + // Validate + this.validator.validate(this.config); + // Options this.testsErrored = false; this.instrumentToFile = (config.instrumentToFile === false) ? false : true; diff --git a/lib/ui.js b/lib/ui.js index c21fc70..cf6e2ee 100644 --- a/lib/ui.js +++ b/lib/ui.js @@ -91,6 +91,9 @@ class AppUI extends UI { const c = this.chalk; const kinds = { + 'config-fail':`${c.red('A config option (.solcover.js) is incorrectly formatted: ')}` + + `${c.red(args[0])}.`, + 'instr-fail': `${c.red('Could not instrument:')} ${args[0]}. ` + `${c.red('(Please verify solc can compile this file without errors.) ')}`, diff --git a/lib/validator.js b/lib/validator.js new file mode 100644 index 0000000..b5d8441 --- /dev/null +++ b/lib/validator.js @@ -0,0 +1,72 @@ +const Validator = require('jsonschema').Validator; +const AppUI = require('./ui').AppUI; +const util = require('util') + + +function isFunction(input){ + +} + +Validator.prototype.customFormats.isFunction = function(input) { + return typeof input === "function" +}; + +const configSchema = { + id: "/solcoverjs", + type: "object", + properties: { + + client: {type: "object"}, + cwd: {type: "string"}, + host: {type: "string"}, + + + originalContractsDir: {type: "string"}, + port: {type: "number"}, + providerOptions: {type: "object"}, + silent: {type: "boolean"}, + + // Hooks: + onServerReady: {type: "function", format: "isFunction"}, + onTestComplete: {type: "function", format: "isFunction"}, + onIstanbulComplete: {type: "function", format: "isFunction"}, + + // Arrays + skipFiles: { + type: "array", + items: {type: "string"} + }, + + istanbulReporter: { + type: "array", + items: {type: "string"} + }, + }, +}; + +class ConfigValidator { + constructor(){ + this.validator = new Validator(); + this.validator.addSchema(configSchema); + this.ui = new AppUI(); + } + + validate(config){ + let result = this.validator.validate(config, configSchema); + + if (result.errors.length){ + let msg; + const option = `"${result.errors[0].property.replace('instance.', '')}"`; + + (result.errors[0].argument === 'isFunction') + ? msg = `${option} is not a function` + : msg = `${option} ${result.errors[0].message}`; + + throw new Error(this.ui.generate('config-fail', [msg])); + } + + return true; + } +} + +module.exports = ConfigValidator; \ No newline at end of file diff --git a/package.json b/package.json index b7201e2..9f0b9aa 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "global-modules": "^2.0.0", "globby": "^10.0.1", "istanbul": "^0.4.5", + "jsonschema": "^1.2.4", "node-dir": "^0.1.17", "node-emoji": "^1.10.0", "pify": "^4.0.1", diff --git a/test/units/truffle/errors.js b/test/units/truffle/errors.js index 8825091..e1865c3 100644 --- a/test/units/truffle/errors.js +++ b/test/units/truffle/errors.js @@ -47,7 +47,7 @@ describe('Truffle Plugin: error cases', function() { verify.coverageNotGenerated(truffleConfig); }); - it('project .solcover.js has syntax error', async function(){ + it('.solcover.js has syntax error', async function(){ verify.cleanInitialState(); mock.installFullProject('bad-solcoverjs'); @@ -64,7 +64,22 @@ describe('Truffle Plugin: error cases', function() { verify.coverageNotGenerated(truffleConfig); }) + it('.solcover.js has incorrectly formatted option', async function(){ + verify.cleanInitialState(); + solcoverConfig.port = "Antwerpen"; + + mock.install('Simple', 'simple.js', solcoverConfig); + try { + await plugin(truffleConfig); + assert.fail() + } catch (err) { + assert( + err.message.includes('config option'), + `Should error on incorrect config options: ${err.message}` + ); + } + }); it('lib module load failure', async function(){ verify.cleanInitialState(); diff --git a/test/units/validator.js b/test/units/validator.js new file mode 100644 index 0000000..14335fc --- /dev/null +++ b/test/units/validator.js @@ -0,0 +1,137 @@ +const assert = require('assert'); +const util = require('util'); +const ConfigValidator = require('./../../lib/validator'); + +describe('config validation', () => { + let validator; + let solcoverjs; + + before(() => validator = new ConfigValidator()); + beforeEach(() => solcoverjs = {}); + + it('validates an empty config', function() { + assert(validator.validate(solcoverjs), '{} should be valid'); + }) + + it('validates config with unknown options', function(){ + solcoverjs.unknown_option = 'hello'; + assert(validator.validate(solcoverjs), '.cwd string should be valid') + }) + + it('validates the "string" options', function(){ + const options = [ + "cwd", + "host", + "originalContractsDir", + ] + + options.forEach(name => { + // Pass + solcoverjs = {}; + solcoverjs[name] = "a_string"; + assert(validator.validate(solcoverjs), `${name} string should be valid`) + + // Fail + solcoverjs[name] = 0; + try { + validator.validate(solcoverjs); + assert.fail() + } catch (err){ + assert(err.message.includes(`"${name}" is not of a type(s) string`), err.message); + } + }); + }); + + it('validates the "object" options', function(){ + const options = [ + "client", + "providerOptions", + ] + + options.forEach(name => { + // Pass + solcoverjs = {}; + solcoverjs[name] = {a_property: 'a'}; + assert(validator.validate(solcoverjs), `${name} object should be valid`) + + // Fail + solcoverjs[name] = 0; + try { + validator.validate(solcoverjs); + assert.fail() + } catch (err){ + assert(err.message.includes(`"${name}" is not of a type(s) object`), err.message); + } + }); + }); + + it('validates the "number" options', function(){ + const options = [ + "port", + ] + + options.forEach(name => { + // Pass + solcoverjs = {}; + solcoverjs[name] = 0; + assert(validator.validate(solcoverjs), `${name} number should be valid`) + + // Fail + solcoverjs[name] = "a_string"; + try { + validator.validate(solcoverjs); + assert.fail() + } catch (err){ + assert(err.message.includes(`"${name}" is not of a type(s) number`), err.message); + } + }); + }); + + it('validates string array options', function(){ + const options = [ + "skipFiles", + "istanbulReporter", + ] + + options.forEach(name => { + // Pass + solcoverjs = {}; + solcoverjs[name] = ['a_string']; + assert(validator.validate(solcoverjs), `${name} string array should be valid`) + + // Fail + solcoverjs[name] = "a_string"; + try { + validator.validate(solcoverjs); + assert.fail() + } catch (err){ + assert(err.message.includes(`"${name}" is not of a type(s) array`), err.message); + } + }); + }); + + it('validates function options', function(){ + + const options = [ + "onServerReady", + "onTestComplete", + "onIstanbulComplete", + ] + + options.forEach(name => { + // Pass + solcoverjs = {}; + solcoverjs[name] = async (a,b) => {}; + assert(validator.validate(solcoverjs), `${name} string array should be valid`) + + // Fail + solcoverjs[name] = "a_string"; + try { + validator.validate(solcoverjs); + assert.fail() + } catch (err){ + assert(err.message.includes(`"${name}" is not a function`), err.message); + } + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index aa8893d..459051a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4138,6 +4138,11 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= +jsonschema@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.4.tgz#a46bac5d3506a254465bc548876e267c6d0d6464" + integrity sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw== + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"