diff --git a/bin/waffle b/bin/waffle index 4ac89c3d6..0782a3721 100755 --- a/bin/waffle +++ b/bin/waffle @@ -1,5 +1,10 @@ #!/usr/bin/env node 'use strict'; -const Waffle = require('../dist/compiler.js'); -Waffle.compile(process.argv[2]); +const Waffle = require('../dist/compiler/compiler.js'); +Waffle + .compileProject(process.argv[2]) + .catch(e => { + console.error(e); + process.exit(1); + }); diff --git a/docs/release-notes/2.0.3.md b/docs/release-notes/2.0.3.md new file mode 100644 index 000000000..1d298e37e --- /dev/null +++ b/docs/release-notes/2.0.3.md @@ -0,0 +1,10 @@ +# Summary +This release introduces TypeScript declaration files for custom matchers and +a rewrite of the compiler. + +## Type declarations for custom matchers +You can now enjoy full type safety when writing your tests with waffle. + +## Compiler rewrite +While no external facing api is changing the internals of the compiler have been +rewritten to enable further developments. diff --git a/lib/compiler.ts b/lib/compiler.ts deleted file mode 100644 index e60f976b0..000000000 --- a/lib/compiler.ts +++ /dev/null @@ -1,106 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import defaultConfig, {Config} from './config/config'; -import {isDirectory, readFileContent, isWarningMessage} from './utils'; -import {createWrapper, Wrapper} from './wrappers/createWrapper'; - -interface CompilerOptions { - wrapper?: Wrapper; - overrideProcess?: NodeJS.Process; - overrideConsole?: Console; -} - -export default class Compiler { - private config: Config; - private process: NodeJS.Process; - private console: Console; - private wrapper: Wrapper; - - constructor(config: Partial = {}, options: CompilerOptions = {}) { - this.config = {...defaultConfig, ...config}; - this.process = options.overrideProcess || process; - this.console = options.overrideConsole || console; - this.wrapper = options.wrapper || createWrapper(this.config); - } - - public async findInputFiles(sourcesPath: string) { - const dirs = [sourcesPath]; - const inputFiles: string[] = []; - while (dirs.length) { - const dir = dirs.pop(); - const files = fs.readdirSync(dir); - for (const file of files) { - const filePath = path.join(dir, file); - if (isDirectory(filePath)) { - dirs.push(filePath); - } else if (file.endsWith('.sol')) { - inputFiles.push(filePath); - } - } - } - return inputFiles; - } - - public findImports(file: string) { - const libPath = path.join(this.config.npmPath, file); - if (fs.existsSync(file)) { - const contents = readFileContent(file); - return {contents}; - } else if (fs.existsSync(libPath)) { - const contents = readFileContent(libPath); - return {contents}; - } - return {error: `File not found: ${file}`}; - } - - public async doCompile() { - const sourcesFiles = await this.findInputFiles(this.config.sourcesPath); - return this.wrapper.compile(sourcesFiles, this.findImports.bind(this)); - } - - private anyNonWarningErrors(errors?: any[]) { - if (!errors) { - return false; - } - for (const error of errors) { - if (!isWarningMessage(error)) { - return true; - } - } - return false; - } - - private toFormattedMessage(error: any) { - return typeof error === 'string' ? error : error.formattedMessage; - } - - public async compile() { - const output = await this.doCompile(); - if (output.errors) { - const errors = output.errors.map((error: any) => this.toFormattedMessage(error)).join('\n'); - this.console.error(errors); - } - if (this.anyNonWarningErrors(output.errors)) { - this.process.exit(1); - } else { - await this.wrapper.saveOutput(output, this.config.targetPath); - } - } -} - -export function loadConfig(configPath: string) { - if (configPath) { - return require(path.join(process.cwd(), configPath)); - } - return {}; -} - -export async function compile(configPath: string, options = {}) { - try { - const config = loadConfig(configPath); - const compiler = new Compiler(config, options); - await compiler.compile(); - } catch (err) { - console.error(err); - } -} diff --git a/lib/compiler/buildUitls.ts b/lib/compiler/buildUitls.ts new file mode 100644 index 000000000..6792387c8 --- /dev/null +++ b/lib/compiler/buildUitls.ts @@ -0,0 +1,18 @@ +export function buildInputObject(sources: any, remappings?: any) { + return { + language: 'Solidity', + sources, + settings: { + remappings, + outputSelection: {'*': {'*': ['abi', 'evm.bytecode', 'evm.deployedBytecode']}} + } + }; +} + +export function buildSources(inputs: string[], transform: (input: string) => string) { + const sources: Record = {}; + for (const input of inputs) { + sources[input.replace(/\\/g, '/')] = {urls: [transform(input)]}; + } + return sources; +} diff --git a/lib/compiler/compileDocker.ts b/lib/compiler/compileDocker.ts new file mode 100644 index 000000000..4af378f8a --- /dev/null +++ b/lib/compiler/compileDocker.ts @@ -0,0 +1,47 @@ +import {join} from 'path'; +import ImportMappingBuilder from './importMappingBuilder'; +import {Config} from '../config/config'; +import {execSync} from 'child_process'; +import {buildInputObject, buildSources} from './buildUitls'; + +const CONTAINER_PATH = '/home/project'; +const NPM_PATH = '/home/npm'; + +export function compileDocker(config: Config) { + return async function compile(sources: string[]) { + const command = createBuildCommand(config); + const input = JSON.stringify(buildInputJson(sources, config), null, 2); + return JSON.parse(execSync(command, {input}).toString()); + }; +} + +export function createBuildCommand(config: Config) { + const configTag = config['docker-tag']; + const tag = configTag ? `:${configTag}` : ':stable'; + const allowedPaths = `"${CONTAINER_PATH},${NPM_PATH}"`; + return `docker run ${getVolumes(config)} -i -a stdin -a stdout ` + + `ethereum/solc${tag} solc --standard-json --allow-paths ${allowedPaths}`; +} + +export function getVolumes(config: Config) { + const hostPath = process.cwd(); + const hostNpmPath = join(hostPath, config.npmPath); + return `-v ${hostPath}:${CONTAINER_PATH} -v ${hostNpmPath}:${NPM_PATH}`; +} + +export function buildInputJson(sources: string[], config: Config) { + return buildInputObject( + buildSources(sources, (input) => join(CONTAINER_PATH, input)), + getMappings(sources, config) + ); +} + +function getMappings(sources: string[], config: Config) { + const mappingBuilder = new ImportMappingBuilder( + config.sourcesPath, + config.npmPath, + CONTAINER_PATH, + NPM_PATH + ); + return mappingBuilder.getMappings(sources); +} diff --git a/lib/compiler/compileNative.ts b/lib/compiler/compileNative.ts new file mode 100644 index 000000000..feec1d2fa --- /dev/null +++ b/lib/compiler/compileNative.ts @@ -0,0 +1,33 @@ +import {join, resolve} from 'path'; +import {execSync} from 'child_process'; +import {Config} from '../config/config'; +import ImportMappingBuilder from './importMappingBuilder'; +import {buildInputObject, buildSources} from './buildUitls'; + +export function compileNative(config: Config) { + return async function compile(sources: string[]) { + const command = createBuildCommand(config); + const input = JSON.stringify(buildInputJson(sources, config), null, 2); + return JSON.parse(execSync(command, {input}).toString()); + }; +} + +export function createBuildCommand(config: Config) { + const command = 'solc'; + const params = '--standard-json'; + const customAllowedPaths = (config.allowedPaths || []).map((path: string) => resolve(path)); + const allowedPaths = [resolve(config.sourcesPath), resolve(config.npmPath), ...customAllowedPaths]; + return `${command} ${params} --allow-paths ${allowedPaths.join(',')}`; +} + +function buildInputJson(sources: string[], config: Config) { + return buildInputObject( + buildSources(sources, (input) => join(process.cwd(), input)), + getMappings(sources, config) + ); +} + +function getMappings(sources: string[], config: Config) { + const mappingBuilder = new ImportMappingBuilder(config.sourcesPath, config.npmPath); + return mappingBuilder.getMappings(sources); +} diff --git a/lib/compiler/compileSolcjs.ts b/lib/compiler/compileSolcjs.ts new file mode 100644 index 000000000..5ef849b25 --- /dev/null +++ b/lib/compiler/compileSolcjs.ts @@ -0,0 +1,37 @@ +import solc from 'solc'; +import {promisify} from 'util'; +import {readFileContent} from '../utils'; +import {Config} from '../config/config'; +import {buildInputObject} from './buildUitls'; + +const loadRemoteVersion = promisify(solc.loadRemoteVersion); + +async function loadCompiler(config: Config) { + if (config.solcVersion) { + return loadRemoteVersion(config.solcVersion); + } else { + return solc; + } +} + +export function compileSolcjs(config: Config) { + return async function compile(sources: string[], findImports: (file: string) => any) { + const solc = await loadCompiler(config); + const inputs = findInputs(sources); + const input = buildInputObject(convertInputs(inputs)); + const output = solc.compile(JSON.stringify(input), findImports); + return JSON.parse(output); + }; +} + +function convertInputs(inputs: Record) { + const converted: Record = {}; + Object.keys(inputs).map((key) => converted[key.replace(/\\/g, '/')] = {content: inputs[key]}); + return converted; +} + +export function findInputs(files: string[]) { + return Object.assign({}, ...files.map((file) => ({ + [file]: readFileContent(file) + }))); +} diff --git a/lib/compiler/compiler.ts b/lib/compiler/compiler.ts new file mode 100644 index 000000000..53e42d41f --- /dev/null +++ b/lib/compiler/compiler.ts @@ -0,0 +1,46 @@ +import {Config} from '../config/config'; +import {isWarningMessage} from '../utils'; +import {getCompileFunction} from './getCompileFunction'; +import {findInputs} from './findInputs'; +import {findImports} from './findImports'; +import {loadConfig} from '../config/loadConfig'; +import {saveOutput} from './saveOutput'; + +export async function compileProject(configPath: string) { + await compileAndSave(loadConfig(configPath)); +} + +export async function compileAndSave(config: Config) { + const output = await compile(config); + await processOutput(output, config); +} + +export async function compile(config: Config) { + return getCompileFunction(config)( + findInputs(config.sourcesPath), + findImports(config.npmPath) + ); +} + +async function processOutput(output: any, config: Config) { + if (output.errors) { + console.error(formatErrors(output.errors)); + } + if (anyNonWarningErrors(output.errors)) { + throw new Error('Compilation failed'); + } else { + await saveOutput(output, config); + } +} + +function anyNonWarningErrors(errors?: any[]) { + return errors && !errors.every(isWarningMessage); +} + +function formatErrors(errors: any[]) { + return errors.map(toFormattedMessage).join('\n'); +} + +function toFormattedMessage(error: any) { + return typeof error === 'string' ? error : error.formattedMessage; +} diff --git a/lib/compiler/findImports.ts b/lib/compiler/findImports.ts new file mode 100644 index 000000000..ba925dd56 --- /dev/null +++ b/lib/compiler/findImports.ts @@ -0,0 +1,20 @@ +import fs from 'fs'; +import path from 'path'; +import {readFileContent} from '../utils'; + +export function findImports(libraryPath: string) { + return (file: string) => { + try { + const libFile = path.join(libraryPath, file); + if (fs.existsSync(file)) { + return { contents: readFileContent(file) }; + } else if (fs.existsSync(libFile)) { + return { contents: readFileContent(libFile) }; + } else { + throw new Error(`File not found: ${file}`); + } + } catch (e) { + return { error: e.message }; + } + }; +} diff --git a/lib/compiler/findInputs.ts b/lib/compiler/findInputs.ts new file mode 100644 index 000000000..a1857eebe --- /dev/null +++ b/lib/compiler/findInputs.ts @@ -0,0 +1,24 @@ +import fs from 'fs'; +import path from 'path'; + +export function findInputs(sourcePath: string) { + const stack = [sourcePath]; + const inputFiles: string[] = []; + while (stack.length > 0) { + const dir = stack.pop(); + const files = fs.readdirSync(dir); + for (const file of files) { + const filePath = path.join(dir, file); + if (isDirectory(filePath)) { + stack.push(filePath); + } else if (file.endsWith('.sol')) { + inputFiles.push(filePath); + } + } + } + return inputFiles; +} + +const isDirectory = (filePath: string) => + fs.existsSync(filePath) && + fs.statSync(filePath).isDirectory(); diff --git a/lib/compiler/getCompileFunction.ts b/lib/compiler/getCompileFunction.ts new file mode 100644 index 000000000..736822e34 --- /dev/null +++ b/lib/compiler/getCompileFunction.ts @@ -0,0 +1,20 @@ +import { Config } from '../config/config'; +import {compileSolcjs} from './compileSolcjs'; +import {compileNative} from './compileNative'; +import {compileDocker} from './compileDocker'; + +export type CompileFunction = ( + sources: string[], + findImports: (file: string) => any +) => any; + +export function getCompileFunction(config: Config): CompileFunction { + if (config.compiler === 'native') { + return compileNative(config); + } else if (config.compiler === 'dockerized-solc') { + return compileDocker(config); + } else if (config.compiler === 'solcjs' || !config.compiler) { + return compileSolcjs(config); + } + throw new Error(`Unknown compiler ${config.compiler}`); +} diff --git a/lib/wrappers/importMappingBuilder.ts b/lib/compiler/importMappingBuilder.ts similarity index 95% rename from lib/wrappers/importMappingBuilder.ts rename to lib/compiler/importMappingBuilder.ts index 7f4f4900f..9043f03cd 100644 --- a/lib/wrappers/importMappingBuilder.ts +++ b/lib/compiler/importMappingBuilder.ts @@ -63,10 +63,11 @@ class ImportMappingBuilder { } public getMappings(sources: string[]) { - return falttenObjectArray( + const mappings = falttenObjectArray( sources.map((path) => this.getMappingForUnit(readFileContent(path), path) )); + return Object.entries(mappings).map(([key, value]) => `${key}=${value}`); } } diff --git a/lib/compiler/saveOutput.ts b/lib/compiler/saveOutput.ts new file mode 100644 index 000000000..b2ae65b63 --- /dev/null +++ b/lib/compiler/saveOutput.ts @@ -0,0 +1,42 @@ +import {join, dirname} from 'path'; +import fs from 'fs'; +import {Config} from '../config/config'; + +export interface BytecodeJson { + linkReferences: object; + object: string; + opcodes: string; + sourceMap: string; +} + +export interface EvmJson { + bytecode: BytecodeJson; +} + +export interface ContractJson { + interface: object[]; + abi: object[]; + bytecode: string; + evm: EvmJson; +} + +export async function saveOutput(output: any, config: Config, filesystem = fs) { + for (const [, file] of Object.entries(output.contracts)) { + for (const [contractName, contractJson] of Object.entries(file)) { + const filePath = join(config.targetPath, `${contractName}.json`); + const dirPath = dirname(filePath); + if (!filesystem.existsSync(dirPath)) { + filesystem.mkdirSync(dirPath); + } + filesystem.writeFileSync(filePath, getContent(contractJson, config)); + } + } +} + +function getContent(contractJson: ContractJson, config: Config) { + if (config.legacyOutput || !contractJson.interface) { + contractJson.interface = contractJson.abi; + contractJson.bytecode = contractJson.evm.bytecode.object; + } + return JSON.stringify(contractJson, null, 2); +} diff --git a/lib/config/loadConfig.ts b/lib/config/loadConfig.ts new file mode 100644 index 000000000..55c82d6b2 --- /dev/null +++ b/lib/config/loadConfig.ts @@ -0,0 +1,16 @@ +import path from 'path'; +import defaultConfig, { Config } from './config'; + +function readConfigFile(configPath: string) { + if (configPath) { + return require(path.join(process.cwd(), configPath)); + } + return {}; +} + +export function loadConfig(configPath: string): Config { + return { + ...defaultConfig, + ...readConfigFile(configPath) + }; +} diff --git a/lib/utils.ts b/lib/utils.ts index 98bd21025..cf573fe3e 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -29,9 +29,6 @@ export async function getBalanceChanges(transactionCallback: () => any, wallets: return balancesAfter.map((balance, ind) => balance.sub(balancesBefore[ind])); } -export const isDirectory = (filePath: string) => - fs.existsSync(filePath) && fs.statSync(filePath).isDirectory(); - export const isFile = (filePath: string) => fs.existsSync(filePath) && fs.lstatSync(filePath).isFile(); diff --git a/lib/wrappers/baseWrapper.ts b/lib/wrappers/baseWrapper.ts deleted file mode 100644 index 19a7b939f..000000000 --- a/lib/wrappers/baseWrapper.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {execSync} from 'child_process'; -import fs from 'fs'; -import {join, dirname} from 'path'; -import {Config} from '../config/config'; -import ImportMappingBuilder from './importMappingBuilder'; - -export interface BytecodeJson { - linkReferences: object; - object: string; - opcodes: string; - sourceMap: string; -} - -export interface EvmJson { - bytecode: BytecodeJson; -} - -export interface ContractJson { - interface: object[]; - abi: object[]; - bytecode: string; - evm: EvmJson; -} - -export default class BaseWrapper { - protected config: Config; - protected mappingBuilder: ImportMappingBuilder; - - constructor(config: Config) { - this.config = config; - } - - protected getContent(contractJson: ContractJson) { - if (this.config.legacyOutput) { - contractJson.interface = contractJson.abi; - contractJson.bytecode = contractJson.evm.bytecode.object; - } - return JSON.stringify(contractJson, null, 2); - } - - protected getAbsolutePath(input: string) { - return ''; - } - - protected buildSources(inputs: string[]) { - const sources: Record = {}; - for (const input of inputs) { - sources[input.replace(/\\/g, '/')] = {urls: [this.getAbsolutePath(input)]}; - } - return sources; - } - - public async saveOutput(output: any, targetPath: string, filesystem = fs) { - for (const [, file] of Object.entries(output.contracts)) { - for (const [contractName, contractJson] of Object.entries(file)) { - const filePath = join(targetPath, `${contractName}.json`); - const dirPath = dirname(filePath); - if (!filesystem.existsSync(dirPath)) { - filesystem.mkdirSync(dirPath); - } - filesystem.writeFileSync(filePath, this.getContent(contractJson)); - } - } - } - - public buildInputJson(sources: string[]) { - return { - language: 'Solidity', - sources: this.buildSources(sources), - settings: { - remappings: this.getMappings(sources), - outputSelection: {'*': {'*': ['abi', 'evm.bytecode', 'evm.deployedBytecode']}} - } - }; - } - - protected getMappings(sources: string[]) { - const mappings = this.mappingBuilder.getMappings(sources); - return Object.entries(mappings).map(([key, value]) => `${key}=${value}`); - } - - protected buildCommand(sources: string[]) { - return ''; - } - - public compile(sources: string[], findImports?: any) { - const command = this.buildCommand(sources); - const input = JSON.stringify(this.buildInputJson(sources), null, 2); - return JSON.parse(execSync(command, {input}).toString()); - } -} diff --git a/lib/wrappers/createWrapper.ts b/lib/wrappers/createWrapper.ts deleted file mode 100644 index 347eedc4e..000000000 --- a/lib/wrappers/createWrapper.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Config } from '../config/config'; -import SolcjsWrapper from './solcjsWrapper'; -import NativeWrapper from './nativeWrapper'; -import DockerWrapper from './dockerWrapper'; - -export interface Wrapper { - compile(sourceFiles: string[], findImports: () => any): Promise; - saveOutput(output: any, targetPath: string): Promise; -} - -export function createWrapper(config: Config): Wrapper { - if (config.compiler === 'native') { - return new NativeWrapper(config); - } else if (config.compiler === 'dockerized-solc') { - return new DockerWrapper(config); - } else if (config.compiler === 'solcjs' || !config.compiler) { - return new SolcjsWrapper(config); - } - throw new Error(`Unknown compiler ${config.compiler}`); -} diff --git a/lib/wrappers/dockerWrapper.ts b/lib/wrappers/dockerWrapper.ts deleted file mode 100644 index 653479259..000000000 --- a/lib/wrappers/dockerWrapper.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {join} from 'path'; -import ImportMappingBuilder from './importMappingBuilder'; -import BaseWrapper from './baseWrapper'; -import { Config } from '../config/config'; - -export default class DockerWrapper extends BaseWrapper { - private containerPath: string; - private containerNpmPath: string; - constructor(config: Config) { - super(config); - this.containerPath = '/home/project'; - this.containerNpmPath = '/home/npm'; - const {sourcesPath, npmPath} = this.config; - this.mappingBuilder = new ImportMappingBuilder(sourcesPath, npmPath, this.containerPath, this.containerNpmPath); - } - - protected getAbsolutePath(relativePath: string) { - return join(this.containerPath, relativePath); - } - - public getVolumes() { - const hostPath = process.cwd(); - const hostNpmPath = join(hostPath, this.config.npmPath); - return `-v ${hostPath}:${this.containerPath} -v ${hostNpmPath}:${this.containerNpmPath}`; - } - - public buildCommand() { - const configTag = this.config['docker-tag']; - const tag = configTag ? `:${configTag}` : ':stable'; - const allowedPaths = `"${this.containerPath},${this.containerNpmPath}"`; - return `docker run ${this.getVolumes()} -i -a stdin -a stdout ` + - `ethereum/solc${tag} solc --standard-json --allow-paths ${allowedPaths}`; - } -} diff --git a/lib/wrappers/nativeWrapper.ts b/lib/wrappers/nativeWrapper.ts deleted file mode 100644 index a2c7517c2..000000000 --- a/lib/wrappers/nativeWrapper.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {join, resolve} from 'path'; -import ImportMappingBuilder from './importMappingBuilder'; -import BaseWrapper from './baseWrapper'; -import { Config } from '../config/config'; - -export default class NativeWrapper extends BaseWrapper { - constructor(config: Config) { - super(config); - const {sourcesPath, npmPath} = this.config; - this.mappingBuilder = new ImportMappingBuilder(sourcesPath, npmPath); - } - - public getAbsolutePath(relativePath: string) { - return join(process.cwd(), relativePath); - } - - public buildCommand() { - const command = 'solc'; - const params = '--standard-json'; - const customAllowedPaths = (this.config.allowedPaths || []).map( (path) => resolve(path)); - const allowedPaths = [resolve(this.config.sourcesPath), resolve(this.config.npmPath), ...customAllowedPaths]; - return `${command} ${params} --allow-paths ${allowedPaths.join(',')}`; - } -} diff --git a/lib/wrappers/solcjsWrapper.ts b/lib/wrappers/solcjsWrapper.ts deleted file mode 100644 index 82a072655..000000000 --- a/lib/wrappers/solcjsWrapper.ts +++ /dev/null @@ -1,76 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import solc, { SolcCompiler } from 'solc'; -import {promisify} from 'util'; -import {readFileContent} from '../utils'; -import BaseWrapper, {ContractJson} from './baseWrapper'; - -const loadRemoteVersion = promisify(solc.loadRemoteVersion); - -class SolcjsWrapper extends BaseWrapper { - protected solc: SolcCompiler; - - public async findInputs(files: string[]) { - return Object.assign({}, ...files.map((file) => ({[file]: readFileContent(file)}))); - } - - protected convertSources(sources: Record) { - const convertedSources: Record = {}; - Object.keys(sources).map((key) => convertedSources[key.replace(/\\/g, '/')] = {content: sources[key]}); - return convertedSources; - } - - protected async loadCompiler() { - if (this.solc) { - return; - } else if (this.config.solcVersion) { - this.solc = await loadRemoteVersion(this.config.solcVersion); - } else { - this.solc = solc; - } - } - - public async compile(sourcesFiles: string[], findImports: (...args: any) => any) { - await this.loadCompiler(); - const sources = await this.findInputs(sourcesFiles); - const input = { - language: 'Solidity', - sources: this.convertSources(sources), - settings: { - outputSelection: { - '*': { - '*': ['abi', 'evm.bytecode', 'evm.deployedBytecode'] - } - } - } - }; - const output = this.solc.compile(JSON.stringify(input), findImports); - return JSON.parse(output); - } - - protected getContent(contractJson: ContractJson) { - contractJson.interface = contractJson.abi; - contractJson.bytecode = contractJson.evm.bytecode.object; - return JSON.stringify(contractJson, null, 2); - } - - public async saveOutput(output: any, targetPath: string) { - for (const key of Object.keys(output.contracts)) { - for (const contract of Object.keys(output.contracts[key])) { - const filePath = path.join(targetPath, `${contract}.json`); - const dirPath = path.dirname(filePath); - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath); - } - const content = this.getContent(output.contracts[key][contract]); - try { - fs.writeFileSync(filePath, content); - } catch (err) { - console.error(err); - } - } - } - } -} - -export default SolcjsWrapper; diff --git a/package.json b/package.json index e5938e1df..9b704327d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ethereum-waffle", "description": "Sweeter, faster and simpler than truffle.", - "version": "2.0.2", + "version": "2.0.3", "author": "Marek Kirejczyk (http://ethworks.io)", "repository": "git@github.com:EthWorks/Waffle.git", "private": false, @@ -40,7 +40,7 @@ "chai": "^4.1.2", "chai-as-promised": "^7.1.1", "chai-string": "~1.4.0", - "fs-extra": "^7.0.0", + "fs-extra": "^7.0.1", "mocha": "^5.1.1", "openzeppelin-solidity": "^2.1.1", "rimraf": "^2.6.3", diff --git a/script/buildTestContracts.ts b/script/buildTestContracts.ts index 83b6a5247..ad25b1308 100644 --- a/script/buildTestContracts.ts +++ b/script/buildTestContracts.ts @@ -1,21 +1,20 @@ -import Compiler from '../lib/compiler'; +import {compileAndSave} from '../lib/compiler/compiler'; +import defaultConfig from '../lib/config/config'; const buildExampleContracts = async () => { console.log('Building example contracts...'); const sourcesPath = './test/projects/example'; const targetPath = './test/example/build'; - const config = {sourcesPath, targetPath}; - const compiler = new Compiler(config); - await compiler.compile(); + const config = {...defaultConfig, sourcesPath, targetPath}; + await compileAndSave(config); }; const buildMatchers = async () => { console.log('Building matchers contracts...'); const sourcesPath = './test/matchers/contracts'; const targetPath = './test/matchers/build'; - const config = {sourcesPath, targetPath}; - const compiler = new Compiler(config); - await compiler.compile(); + const config = {...defaultConfig, sourcesPath, targetPath}; + await compileAndSave(config); }; const buildAll = async () => { diff --git a/test/compiler/compiler.ts b/test/compiler/compiler.ts index d4098d58c..8e2f6a593 100644 --- a/test/compiler/compiler.ts +++ b/test/compiler/compiler.ts @@ -2,83 +2,42 @@ import chai, {expect} from 'chai'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import chaiString from 'chai-string'; -import Compiler from '../../lib/compiler'; -import {readFileContent} from '../../lib/utils'; +import {compile, compileAndSave} from '../../lib/compiler/compiler'; +import defaultConfig from '../../lib/config/config'; const sourcesPath = './test/projects/example'; const targetPath = './test/compiler/build'; -const config = {sourcesPath, targetPath}; -const expectedInputs = [ - 'test/projects/example/BasicToken.sol', - 'test/projects/example/ERC20Basic.sol', - 'test/projects/example/LibraryConsumer.sol', - 'test/projects/example/MyLibrary.sol', - 'test/projects/example/SafeMath.sol', - 'test/projects/example/mock/BasicTokenMock.sol' -]; +const config = {...defaultConfig, sourcesPath, targetPath}; chai.use(chaiString); chai.use(sinonChai); describe('INTEGRATION: Compiler', () => { - let compiler: Compiler; - - beforeEach(async () => { - compiler = new Compiler(config); - }); - - it('findInputsFiles', async () => { - const actualInputs = await compiler.findInputFiles(sourcesPath); - expect(actualInputs).to.deep.eq(expectedInputs); - }); - - it('findImports in source path', async () => { - const expected = await readFileContent('test/projects/example/BasicToken.sol'); - const actual = compiler.findImports('test/projects/example/BasicToken.sol'); - expect(expected).to.eq(actual.contents); - }); - - it('findImports in imports path', async () => { - const expected = await readFileContent('node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol'); - const actual = compiler.findImports('openzeppelin-solidity/contracts/math/SafeMath.sol'); - expect(expected).to.eq(actual.contents); - }); - - it('findImports file not found', async () => { - const expected = {error: `File not found: random/nonexisting.sol`}; - const actual = compiler.findImports('random/nonexisting.sol'); - expect(expected).to.deep.eq(actual); - }); - - describe('doCompile', () => { - let output: any; - beforeEach(async () => { - output = await compiler.doCompile(); - }); - - it('just compile', async () => { - const basicTokenOutput = output.contracts['test/projects/example/BasicToken.sol'].BasicToken; - expect(output.errors).to.equal(undefined); - expect(basicTokenOutput.evm.bytecode.object).to.startsWith('6080604052'); - expect(JSON.stringify(basicTokenOutput.abi)).to.startsWith('[{"constant":true,'); - }); + it('compile just compiles', async () => { + const output = await compile(config); + const basicTokenOutput = output.contracts['test/projects/example/BasicToken.sol'].BasicToken; + expect(output.errors).to.equal(undefined); + expect(basicTokenOutput.evm.bytecode.object).to.startsWith('6080604052'); + expect(JSON.stringify(basicTokenOutput.abi)).to.startsWith('[{"constant":true,'); }); - describe('compile: invalid input', () => { + describe('compileAndSave: invalid input', () => { const sourcesPath = './test/projects/invalidContracts'; // tslint:disable-line const targetPath = './test/projects/build'; // tslint:disable-line - const config = {sourcesPath, targetPath}; // tslint:disable-line + const config = {...defaultConfig, sourcesPath, targetPath}; // tslint:disable-line const expectedOutput = 'test/projects/invalidContracts/invalid.sol:6:14: ' + 'DeclarationError: Identifier not found or not unique.\n' + ' function f(wrongType arg) public {\n ^-------^\n'; it('shows error message', async () => { - const overrideConsole = {error: sinon.spy()} as any; - const overrideProcess = {exit: sinon.spy()} as any; - compiler = new Compiler(config, {overrideConsole, overrideProcess}); - await compiler.compile(); - expect(overrideConsole.error).to.be.calledWith(expectedOutput); - expect(overrideProcess.exit).to.be.calledWith(1); + const consoleError = console.error; + console.error = sinon.spy(); + + const promise = compileAndSave(config); + await expect(promise).to.be.rejectedWith(Error, 'Compilation failed'); + expect(console.error).to.be.calledWith(expectedOutput); + + console.error = consoleError; }); }); }); diff --git a/test/compiler/e2e.ts b/test/compiler/e2e.ts index c78ffea6e..662bbdd4a 100644 --- a/test/compiler/e2e.ts +++ b/test/compiler/e2e.ts @@ -1,9 +1,9 @@ import fs from 'fs'; import fsx from 'fs-extra'; -import path from 'path'; import {join, resolve} from 'path'; import {expect} from 'chai'; -import {compile, loadConfig} from '../../lib/compiler'; +import {compileProject} from '../../lib/compiler/compiler'; +import {loadConfig} from '../../lib/config/loadConfig'; import {readFileContent, isFile, deepCopy} from '../../lib/utils'; import {deployContract, link, getWallets, createMockProvider} from '../../lib/waffle'; @@ -33,7 +33,7 @@ describe('E2E: Compiler integration', () => { }); it('compile and produce artefacts', async () => { - await compile('config_docker.json'); + await compileProject('config_docker.json'); for (const artefact of artefacts) { const filePath = join('../build', artefact); expect(isFile(filePath), `Expected compilation artefact "${filePath}" to exist.`).to.equal(true); @@ -46,14 +46,14 @@ describe('E2E: Compiler integration', () => { }); for (const configurationPath of configurations) { - const configuration = loadConfig(configurationPath); + const configuration = loadConfig(configurationPath) as any; const {name, targetPath, legacyOutput} = configuration; /* eslint-disable no-loop-func */ describe(name, () => { before(async () => { fsx.removeSync(targetPath); - await compile(configurationPath); + await compileProject(configurationPath); }); it('produce output files', async () => { diff --git a/test/compiler/e2e_errors.ts b/test/compiler/e2e_errors.ts index c69654116..76b56fbc4 100644 --- a/test/compiler/e2e_errors.ts +++ b/test/compiler/e2e_errors.ts @@ -1,6 +1,6 @@ import chai, {expect} from 'chai'; import {readFileContent} from '../../lib/utils'; -import {compile} from '../../lib/compiler'; +import {compileProject} from '../../lib/compiler/compiler'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; @@ -16,14 +16,17 @@ describe('E2E: Compiler integration - error messages', () => { for (const configurationPath of configurations) { const configuration = JSON.parse(readFileContent(configurationPath)); - /* eslint-disable no-loop-func */ it(configuration.name, async () => { - const overrideConsole = {error: sinon.spy()}; - const overrideProcess = {exit: sinon.spy()}; - await compile(configurationPath, {overrideConsole, overrideProcess}); + const consoleError = console.error; + console.error = sinon.spy(); + const expectedPattern = /invalid\.sol.*Identifier not found or not.*/; - expect(overrideConsole.error).to.be.calledWith(sinon.match(expectedPattern)); - expect(overrideProcess.exit).to.be.calledWith(1); + + const promise = compileProject(configurationPath); + await expect(promise).to.be.rejectedWith(Error, 'Compilation failed'); + expect(console.error).to.be.calledWith(sinon.match(expectedPattern)); + + console.error = consoleError; }); } }); diff --git a/test/compiler/findImports.ts b/test/compiler/findImports.ts new file mode 100644 index 000000000..ba1aa03d7 --- /dev/null +++ b/test/compiler/findImports.ts @@ -0,0 +1,31 @@ +import fs from 'fs-extra'; +import {expect} from 'chai'; +import {findImports} from '../../lib/compiler/findImports'; +import {readFileContent} from '../../lib/utils'; + +describe('INTEGRATION: findImports', () => { + before(async () => { + await fs.mkdirp('/tmp/waffle/b'); + await fs.writeFile('/tmp/waffle/b/b.sol', 'contents of b.sol'); + }); + + after(async () => { + await fs.remove('/tmp/waffle'); + }); + + it('finds imports in source path', async () => { + const contents = await readFileContent('test/projects/example/BasicToken.sol'); + const result = findImports('/')('test/projects/example/BasicToken.sol'); + expect(result).to.deep.equal({contents}); + }); + + it('finds imports in library path', () => { + const result = findImports('/tmp/waffle')('b/b.sol'); + expect(result).to.deep.equal({contents: 'contents of b.sol'}); + }); + + it('findImports file not found', async () => { + const result = findImports('/tmp/waffle')('random/nonexisting.sol'); + expect(result).to.deep.equal({error: `File not found: random/nonexisting.sol`}); + }); +}); diff --git a/test/compiler/findInputs.ts b/test/compiler/findInputs.ts new file mode 100644 index 000000000..df470f3b2 --- /dev/null +++ b/test/compiler/findInputs.ts @@ -0,0 +1,40 @@ +import fs from 'fs-extra'; +import {expect} from 'chai'; +import { findInputs } from '../../lib/compiler/findInputs'; + +describe('INTEGRATION: findInputs', () => { + before(async () => { + await fs.mkdirp('/tmp/waffle/a'); + await fs.mkdirp('/tmp/waffle/b'); + await fs.mkdirp('/tmp/waffle/c/c'); + + await fs.writeFile('/tmp/waffle/b/b.sol', ''); + await fs.writeFile('/tmp/waffle/b/xxx.xxx', ''); + await fs.writeFile('/tmp/waffle/c/c1.sol', ''); + await fs.writeFile('/tmp/waffle/c/c/c2.sol', ''); + }); + + after(async () => { + await fs.remove('/tmp/waffle'); + }); + + it('returns empty array for an empty forlder', () => { + const result = findInputs('/tmp/waffle/a'); + expect(result).to.deep.equal([]); + }); + + it('only returns .sol files', () => { + const result = findInputs('/tmp/waffle/b'); + expect(result).to.deep.equal([ + '/tmp/waffle/b/b.sol' + ]); + }); + + it('returns files found in a folder recursively', () => { + const result = findInputs('/tmp/waffle/c'); + expect(result).to.deep.equal([ + '/tmp/waffle/c/c1.sol', + '/tmp/waffle/c/c/c2.sol' + ]); + }); +}); diff --git a/test/compiler/wrappers/dockerWrapper.ts b/test/compiler/wrappers/compileDocker.ts similarity index 81% rename from test/compiler/wrappers/dockerWrapper.ts rename to test/compiler/wrappers/compileDocker.ts index b0aadc641..5ed1377d1 100644 --- a/test/compiler/wrappers/dockerWrapper.ts +++ b/test/compiler/wrappers/compileDocker.ts @@ -1,5 +1,9 @@ import chai, {expect} from 'chai'; -import DockerWrapper from '../../../lib/wrappers/dockerWrapper'; +import { + getVolumes, + buildInputJson, + createBuildCommand +} from '../../../lib/compiler/compileDocker'; import sinonChai from 'sinon-chai'; import {join} from 'path'; @@ -13,24 +17,18 @@ const npmPath = './test/projects/custom/custom_node_modules'; const config = {sourcesPath, npmPath}; describe('UNIT: DockerWrapper', () => { - let dockerWrapper: DockerWrapper; - - before(() => { - dockerWrapper = new DockerWrapper(config); - }); - describe('getVolumes', () => { it('simple config', () => { const hostProjectPath = process.cwd(); const hostNpmPath = join(hostProjectPath, npmPath); const expectedVolumes = `-v ${hostProjectPath}:/home/project -v ${hostNpmPath}:/home/npm`; - expect(dockerWrapper.getVolumes()).to.eq(expectedVolumes); + expect(getVolumes(config)).to.eq(expectedVolumes); }); }); describe('buildInputJson', () => { it('empty sources', () => { - expect(dockerWrapper.buildInputJson([])).to.deep.eq({ + expect(buildInputJson([], config)).to.deep.eq({ language: 'Solidity', sources: {}, settings: { @@ -42,7 +40,7 @@ describe('UNIT: DockerWrapper', () => { it('example sources', () => { const prefix = '/home/project/'; - expect(dockerWrapper.buildInputJson(inputs)).to.deep.eq({ + expect(buildInputJson(inputs, config)).to.deep.eq({ language: 'Solidity', sources: { 'test/projects/custom/custom_contracts/Custom.sol': {urls: [`${prefix}${inputs[0]}`]}, @@ -59,7 +57,7 @@ describe('UNIT: DockerWrapper', () => { describe('buildCommand', () => { it('no version', () => { - const command = dockerWrapper.buildCommand(); + const command = createBuildCommand(config); expect(command).to.startWith('docker run -v '); expect(command).to.endWith( ':/home/npm -i -a stdin -a stdout ethereum/solc:stable solc ' + @@ -68,8 +66,7 @@ describe('UNIT: DockerWrapper', () => { }); it('specific version', () => { - const wrapper = new DockerWrapper({...config, 'docker-tag': '0.4.24'}); - const command = wrapper.buildCommand(); + const command = createBuildCommand({...config, 'docker-tag': '0.4.24'}); expect(command).to.startWith('docker run -v '); expect(command).to.endWith( ':/home/npm -i -a stdin -a stdout ethereum/solc:0.4.24 solc ' + diff --git a/test/compiler/wrappers/nativeWrapper.ts b/test/compiler/wrappers/compileNative.ts similarity index 75% rename from test/compiler/wrappers/nativeWrapper.ts rename to test/compiler/wrappers/compileNative.ts index 248863818..50c19d8aa 100644 --- a/test/compiler/wrappers/nativeWrapper.ts +++ b/test/compiler/wrappers/compileNative.ts @@ -1,6 +1,6 @@ import chai, {expect} from 'chai'; import chaiString from 'chai-string'; -import NativeWrapper from '../../../lib/wrappers/nativeWrapper'; +import {createBuildCommand} from '../../../lib/compiler/compileNative'; chai.use(chaiString); @@ -8,11 +8,9 @@ const sourcesPath = './test/projects/custom/custom_contracts'; const npmPath = './test/projects/custom/custom_node_modules'; const config = {sourcesPath, npmPath}; -describe('UNIT: NativeWrapper', () => { - +describe('UNIT: compileNative', () => { it('buildCommand', async () => { - const wrapper = new NativeWrapper(config); - const actualCommand = wrapper.buildCommand(); + const actualCommand = createBuildCommand(config); const expectedCommand = 'solc --standard-json --allow-paths ' + '.*test/projects/custom/custom_contracts,.*/test/projects/custom/custom_node_modules'; expect(actualCommand).to.match(new RegExp(expectedCommand)); @@ -20,8 +18,7 @@ describe('UNIT: NativeWrapper', () => { it('buildCommand with custom allow_paths', async () => { const configWithAllowedPaths = {...config, allowedPaths: ['some/random/path', './yet/another/path']}; - const wrapper = new NativeWrapper(configWithAllowedPaths); - const actualCommand = wrapper.buildCommand(); + const actualCommand = createBuildCommand(configWithAllowedPaths); const expectedCommand = 'solc --standard-json --allow-paths ' + '.*test/projects/custom/custom_contracts,.*/test/projects/custom/custom_node_modules' + ',.*/some/random/path.*/yet/another/path'; diff --git a/test/compiler/wrappers/solcjswrapper.ts b/test/compiler/wrappers/compileSolcjs.ts similarity index 71% rename from test/compiler/wrappers/solcjswrapper.ts rename to test/compiler/wrappers/compileSolcjs.ts index ab1df2f84..6d6528b18 100644 --- a/test/compiler/wrappers/solcjswrapper.ts +++ b/test/compiler/wrappers/compileSolcjs.ts @@ -1,6 +1,6 @@ import chai, {expect} from 'chai'; import chaiString from 'chai-string'; -import SolcJsWrapper from '../../../lib/wrappers/solcjsWrapper'; +import {findInputs} from '../../../lib/compiler/compileSolcjs'; import {readFileContent} from '../../../lib/utils'; chai.use(chaiString); @@ -11,15 +11,9 @@ const expectedInputs = [ 'test/projects/example/mock/BasicTokenMock.sol' ]; -describe('UNIT: SolcJsWrapper', () => { - let wrapper: SolcJsWrapper; - - before(() => { - wrapper = new SolcJsWrapper(null as any); - }); - +describe('UNIT: findInputs', () => { it('findInputs', async () => { - const actualInputs = await wrapper.findInputs(expectedInputs); + const actualInputs = findInputs(expectedInputs); expect(Object.keys(actualInputs)).to.deep.eq(expectedInputs); const basicTokenContractActual = actualInputs['test/projects/example/BasicToken.sol']; const basicTokenContractExpected = await readFileContent('test/projects/example/BasicToken.sol'); diff --git a/test/compiler/wrappers/importMappingBuilder.ts b/test/compiler/wrappers/importMappingBuilder.ts index 113b2fec1..902b6b9ee 100644 --- a/test/compiler/wrappers/importMappingBuilder.ts +++ b/test/compiler/wrappers/importMappingBuilder.ts @@ -4,7 +4,7 @@ import ImportsMappingBuilder, { IMPORT_TYPE_A, IMPORT_TYPE_B, IMPORT_TYPE_C -} from '../../../lib/wrappers/importMappingBuilder'; +} from '../../../lib/compiler/importMappingBuilder'; chai.use(chaiString); @@ -28,7 +28,7 @@ describe('UNIT: ImportsMappingBuilder', () => { describe('convertPath', () => { let builder: ImportsMappingBuilder; - let expectedPath; + let expectedPath: string; let expectedMapping: any; before(() => { @@ -92,7 +92,9 @@ describe('UNIT: ImportsMappingBuilder', () => { it('generates mapping for sources', async () => { const sources = ['./test/projects/custom/custom_contracts/Custom.sol']; - expect(builder.getMappings(sources)).to.deep.eq(expectedMapping); + expect(builder.getMappings(sources)).to.deep.eq([ + `openzeppelin-solidity=${expectedPath}` + ]); }); }); }); diff --git a/test/compiler/wrappers/baseWrapper.ts b/test/compiler/wrappers/saveOutput.ts similarity index 68% rename from test/compiler/wrappers/baseWrapper.ts rename to test/compiler/wrappers/saveOutput.ts index ce1112c77..250fbc4b9 100644 --- a/test/compiler/wrappers/baseWrapper.ts +++ b/test/compiler/wrappers/saveOutput.ts @@ -1,30 +1,25 @@ import chai, {expect} from 'chai'; -import BaseWrapper from '../../../lib/wrappers/baseWrapper'; import {readFileContent} from '../../../lib/utils'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; +import {saveOutput} from '../../../lib/compiler/saveOutput'; chai.use(sinonChai); const sourcesPath = './test/projects/custom/custom_contracts'; const npmPath = './test/projects/custom/custom_node_modules'; -const config = {sourcesPath, npmPath}; +const targetPath = './buildtmp'; +const config = {sourcesPath, npmPath, targetPath}; -describe('UNIT: BaseWrapper', () => { - let wrapper: BaseWrapper; - - before(() => { - wrapper = new BaseWrapper(config); - }); - - it('saveOutput', () => { +describe('UNIT: saveOutput', () => { + it('calls the required fs methods', () => { const fs = { writeFileSync: sinon.spy(), existsSync: sinon.spy(), mkdirSync: sinon.spy() }; const output = JSON.parse(readFileContent('./test/compiler/wrappers/compilerOutput.json')); - wrapper.saveOutput(output, './buildtmp', fs as any); + saveOutput(output, config, fs as any); const expectedContent = JSON.stringify(output.contracts['One.sol'].One, null, 2); expect(fs.writeFileSync).to.be.calledWith('buildtmp/One.json', expectedContent); }); diff --git a/yarn.lock b/yarn.lock index e8a3288c4..eb4a52b6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2379,9 +2379,10 @@ fs-extra@^2.0.0, fs-extra@^2.1.2: graceful-fs "^4.1.2" jsonfile "^2.1.0" -fs-extra@^7.0.0: +fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== dependencies: graceful-fs "^4.1.2" jsonfile "^4.0.0"