Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

Commit 3123c7b

Browse files
cds-amaldavidmurdochMicaiahReid
authored
Implement console.log in Truffle (#5687)
* Enable Ganache console log - Introduce a config namespace for `solidityLog` (see below) - Make managed Ganache instances aware of truffle-config because `connectOrStart` (ganache) needs to know relevant solidityLog settings to hook up Ganache events. - Use asynchronous swawn instead of synchronous option in core(console). It is necessary to interleave ganache's console stdout events (child process), with mocha's test runner (parent process). - Modify the GanacheMixin in `chain.js` to register for and, forward Ganache's console log events using the `truffle.solidity.log` channel/topic. - Use chalk decorated `console.log` to display solidity console log which will allow users to redirect `truffle test` output to a file. The debug module uses `stderr` insted of stdout which would make redirect to file awkward and complicated. - Update truffle resolver to handle `@truffle/Console` or `@truffle/console` Truffle will include @ganache/console.log/console.sol in its asset and allow smart contract writers users to integrate console.log by importing "truffle/Console.sol". This dependency is managed through npm and is currently pinned. The user does not have to import any console.log packages. yay! - Add @truffle/[Cc]onsole.log as a resolver dependency. This dependency is pinned for the time being - Make `includeTruffleSources` the default resolver option. Settings ======== The settings are namespaced in solidityLog ``` solidityLog: { displayPrefix: string; preventConsoleLogMigration: boolean; } ``` - solidityLog.displayPrefix - string [ default: "" ]. it is the display prefix for all detected console-log messages. NOTE: there is some defensive guards that resolve, null, undefined to behave exactly like the default setting () - solidityLog.preventConsoleLogMigration - boolean [ default: false]. If set to true, `truffle migrate` will not allow deployment on Mainnet. File changes ============ packages/config/src/configDefaults.ts packages/config/test/unit.test.ts - add defaults and tests for solidityLog.{displayPrefix,preventConsoleLogMigration} packages/core/lib/commands/develop/run.js - pass configOptions to connectOrStart packages/core/lib/commands/migrate/runMigrations.js - add migration guards to log when a deployment set has consoleLog assets and additionally prevent deployment based on settings. packages/core/lib/commands/test/run.js - hook up consoleLog for test command packages/core/lib/console.js - use spawn instead of spawnSync so that child process commands can be printed in-order instead of buffering and printing when process ends. This allows consoleLog events from the child process to be interleaved correctly with test outline from parent process. - modify provision method to eliminate need for filter loop packages/core/package.json - add JSONStream dependency packages/environment/chain.js packages/environment/develop.js packages/environment/package.json packages/environment/test/connectOrStartTest.js - hook into the "ganache:vm:tx:console.log" provider event and forward it across IPC infra as SolidityConsoleLogs - hook up client side IPC to listen for SolidityConsoleLogs - add @truffle/config and chalk dependencies - update tests to handle updated interfaces packages/resolver/.eslintrc.json - add eslintrc settings for truffle packages packages/core/lib/debug/compiler.js packages/test/src/Test.ts packages/test/src/TestRunner.ts - allow resolver to includeTruffleSources by default packages/resolver/lib/sources/truffle/index.ts packages/resolver/lib/resolver.ts packages/resolver/test/truffle.js packages/resolver/package.json - resolve "truffle/[cC]onsole.sol" - add tests for console.sol resolutions - add pinned @ganache/console.log/console.sol dependency - use for-of instead of forEach to utilize short circuit breakout. for-each will visit every item, even after a match has been found because there's no way to [1] break out of a for-each loop. > >There is no way to stop or break a forEach() loop other than by >throwing an exception. If you need such behavior, the forEach() method >is the wrong tool. > >Early termination may be accomplished with looping statements like for, >for...of, and for...in. Array methods like every(), some(), find(), and >findIndex() also stops iteration immediately when further iteration is >not necessary. packages/test/src/SolidityTest.ts - include `console.sol` in set of truffle assets to deploy for testing packages/truffle/package.json - add @ganache/console.log dependency packages/truffle/test/scenarios/solidity_console/Printf.sol packages/truffle/test/scenarios/solidity_console/printf.js packages/truffle/test/sources/init/config-disable-migrate-false.js packages/truffle/test/sources/init/config-disable-migrate-true.js packages/truffle/test/sources/init/truffle-config.js - integration tests for consoleLog packages/truffle/test/scenarios/sandbox.js - return tempDirPath when creating a sandbox. Printf tests depend on that. packages/truffle/webpack.config.js - bundle @ganache/console.log/console.sol Links ===== 1: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#:~:text=There%20is%20no%20way%20to,and%20for...in%20. Co-authored-by: David Murdoch <[email protected]> Co-authored-by: Micaiah Reid <[email protected]>
1 parent bcbc026 commit 3123c7b

File tree

29 files changed

+741
-164
lines changed

29 files changed

+741
-164
lines changed

packages/config/src/configDefaults.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export const getInitialConfig = ({
2828
}
2929
},
3030
verboseRpc: false,
31+
solidityLog: {
32+
displayPrefix: "",
33+
preventConsoleLogMigration: false
34+
},
3135
gas: null,
3236
gasPrice: null,
3337
maxFeePerGas: null,
@@ -93,6 +97,7 @@ export const configProps = ({
9397
network() {},
9498
networks() {},
9599
verboseRpc() {},
100+
solidityLog() {},
96101
build() {},
97102
resolver() {},
98103
artifactor() {},

packages/config/test/unit.test.ts

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,65 @@ import TruffleConfig from "../dist";
33
import { describe, it } from "mocha";
44

55
describe("TruffleConfig unit tests", async () => {
6-
describe("with", async () => {
7-
let truffleConfig: TruffleConfig;
6+
let truffleConfig: TruffleConfig;
87

9-
beforeEach(() => {
8+
describe("Defaults", async () => {
9+
before(() => {
1010
truffleConfig = TruffleConfig.default();
1111
});
1212

13-
it("a simple object", async () => {
14-
const expectedRandom = 42;
15-
const expectedFoo = "bar";
16-
const obj = {
17-
random: expectedRandom,
18-
foo: expectedFoo
13+
it("solidityLog", () => {
14+
const expectedSolidityLog = {
15+
displayPrefix: "",
16+
preventConsoleLogMigration: false
1917
};
20-
const newConfig = truffleConfig.with(obj);
21-
assert.equal(expectedRandom, newConfig.random);
22-
assert.equal(expectedFoo, newConfig.foo);
18+
assert.deepStrictEqual(truffleConfig.solidityLog, expectedSolidityLog);
2319
});
20+
}),
21+
describe("with", async () => {
22+
beforeEach(() => {
23+
truffleConfig = TruffleConfig.default();
24+
});
2425

25-
it("overwrites a known property", () => {
26-
const expectedProvider = { a: "propertyA", b: "propertyB" };
27-
const newConfig = truffleConfig.with({ provider: expectedProvider });
28-
assert.deepEqual(expectedProvider, newConfig.provider);
29-
});
26+
it("a simple object", async () => {
27+
const expectedRandom = 42;
28+
const expectedFoo = "bar";
29+
const obj = {
30+
random: expectedRandom,
31+
foo: expectedFoo
32+
};
33+
const newConfig = truffleConfig.with(obj);
34+
assert.strictEqual(expectedRandom, newConfig.random);
35+
assert.strictEqual(expectedFoo, newConfig.foo);
36+
});
3037

31-
it("ignores properties that throw", () => {
32-
const expectedSurvivor = "BatMan";
33-
const minefield = { who: expectedSurvivor };
34-
35-
const hits = ["boom", "pow", "crash", "zonk"];
36-
hits.forEach(hit => {
37-
Object.defineProperty(minefield, hit, {
38-
get() {
39-
throw new Error("BOOM!");
40-
},
41-
enumerable: true //must be enumerable
42-
});
38+
it("overwrites a known property", () => {
39+
const expectedProvider = { a: "propertyA", b: "propertyB" };
40+
const newConfig = truffleConfig.with({ provider: expectedProvider });
41+
assert.deepStrictEqual(expectedProvider, newConfig.provider);
4342
});
4443

45-
const newConfig = truffleConfig.with(minefield);
44+
it("ignores properties that throw", () => {
45+
const expectedSurvivor = "BatMan";
46+
const minefield = { who: expectedSurvivor };
4647

47-
//one survivor
48-
assert.equal(expectedSurvivor, newConfig.who);
48+
const hits = ["boom", "pow", "crash", "zonk"];
49+
hits.forEach(hit => {
50+
Object.defineProperty(minefield, hit, {
51+
get() {
52+
throw new Error("BOOM!");
53+
},
54+
enumerable: true //must be enumerable
55+
});
56+
});
57+
58+
const newConfig = truffleConfig.with(minefield);
4959

50-
//these jokers shouldn't be included
51-
hits.forEach(hit => assert.equal(undefined, newConfig[hit]));
60+
//one survivor
61+
assert.strictEqual(expectedSurvivor, newConfig.who);
62+
63+
//these jokers shouldn't be included
64+
hits.forEach(hit => assert.strictEqual(undefined, newConfig[hit]));
65+
});
5266
});
53-
});
5467
});

packages/core/lib/commands/develop/run.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,23 @@ module.exports = async options => {
4545
"This mnemonic was created for you by Truffle. It is not secure.\n" +
4646
"Ensure you do not use it on production blockchains, or else you risk losing funds.";
4747

48-
const ipcOptions = { log: options.log };
48+
const ipcOptions = {};
49+
50+
if (options.log) {
51+
ipcOptions.log = options.log;
52+
}
53+
4954
const ganacheOptions = configureManagedGanache(
5055
config,
5156
customConfig,
5257
mnemonic
5358
);
5459

55-
const { started } = await Develop.connectOrStart(ipcOptions, ganacheOptions);
60+
const { started } = await Develop.connectOrStart(
61+
ipcOptions,
62+
ganacheOptions,
63+
config
64+
);
5665
const url = `http://${ganacheOptions.host}:${ganacheOptions.port}/`;
5766

5867
if (started) {

packages/core/lib/commands/migrate/runMigrations.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,110 @@
1+
const { createReadStream } = require("fs");
2+
const { readdir } = require("fs/promises");
3+
const path = require("path");
4+
const JSONStream = require("JSONStream");
15
const Migrate = require("@truffle/migrate").default;
6+
const TruffleError = require("@truffle/error");
7+
const os = require("os");
8+
const debug = require("debug")("migrate:run");
9+
10+
// Search for Push1 (60) to Push(32) 7F + console.log
11+
// 60 ---- 7F C O N S O L E . L O G
12+
const consoleLogRex = /[67][0-9a-f]636F6e736F6c652e6c6f67/i;
13+
14+
async function usesConsoleLog(artifactJson) {
15+
const debugLog = debug.extend("test");
16+
debugLog("Artifact: %o", artifactJson);
17+
18+
//create a parser to get the value of jsonpath .deployedBytecode
19+
const parser = JSONStream.parse(["deployedBytecode"]);
20+
const stream = createReadStream(artifactJson).pipe(parser);
21+
22+
return new Promise((resolve, reject) => {
23+
stream.on("data", data => {
24+
//JSONParse will emit the entire string/value
25+
//so initiate stream cleanup here
26+
stream.destroy();
27+
const usesConsoleLog = consoleLogRex.test(data);
28+
debugLog("usesConsoleLog:", usesConsoleLog);
29+
resolve(usesConsoleLog);
30+
});
31+
32+
stream.on("error", err => {
33+
stream.destroy();
34+
debugLog("onError: %o", err);
35+
reject(err);
36+
});
37+
});
38+
}
39+
40+
async function findArtifactsThatUseConsoleLog(buildDir) {
41+
const debugLog = debug.extend("dirty-files");
42+
const filenames = await readdir(buildDir);
43+
44+
const artifacts = [];
45+
await Promise.allSettled(
46+
filenames.map(async filename => {
47+
if (filename.endsWith(".json")) {
48+
try {
49+
const itLogs = await usesConsoleLog(path.join(buildDir, filename));
50+
if (itLogs) {
51+
artifacts.push(filename);
52+
}
53+
} catch (e) {
54+
debugLog("Promise failure: %o", e.message);
55+
}
56+
}
57+
})
58+
);
59+
return artifacts;
60+
}
261

362
module.exports = async function (config) {
63+
const debugLog = debug.extend("guard");
64+
// only check if deploying on MAINNET
65+
// NOTE: this includes Ethereum Classic as well as Ethereum as they're only
66+
// distinguishable by checking their chainIds, 2 and 1 respectively.
67+
if (config.network_id === 1) {
68+
debugLog("solidityLog guard for mainnet");
69+
try {
70+
const buildDir = config.contracts_build_directory;
71+
const loggingArtifacts = await findArtifactsThatUseConsoleLog(buildDir);
72+
73+
debugLog(`${loggingArtifacts.length} consoleLog artifacts detected`);
74+
debugLog(
75+
"config.solidityLog.preventConsoleLogMigration: " +
76+
config.solidityLog.preventConsoleLogMigration
77+
);
78+
79+
if (loggingArtifacts.length) {
80+
console.warn(
81+
`${os.EOL}Solidity console.log detected in the following assets:`
82+
);
83+
console.warn(loggingArtifacts.join(", "));
84+
console.warn();
85+
86+
if (config.solidityLog.preventConsoleLogMigration) {
87+
throw new TruffleError(
88+
"You are trying to deploy contracts that use console.log." +
89+
os.EOL +
90+
"Please fix, or disable this check by setting solidityLog.preventConsoleLogMigration to false" +
91+
os.EOL
92+
);
93+
}
94+
}
95+
} catch (err) {
96+
if (err instanceof TruffleError) throw err;
97+
98+
debugLog("Unexpected error %o:", err);
99+
// Something went wrong while inspecting for console log.
100+
// Log warning and skip the remaining logic in this branch
101+
console.warn();
102+
console.warn(
103+
"Failed to detect Solidity console.log usage:" + os.EOL + err
104+
);
105+
}
106+
}
107+
4108
if (config.f) {
5109
return await Migrate.runFrom(config.f, config);
6110
} else {

packages/core/lib/commands/test/run.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,21 @@ module.exports = async function (options) {
3838
}
3939

4040
// Start managed ganache network
41-
async function startGanacheAndRunTests(ipcOptions, ganacheOptions, config) {
41+
async function startGanacheAndRunTests(
42+
ipcOptions,
43+
ganacheOptions,
44+
truffleConfig
45+
) {
4246
const { disconnect } = await Develop.connectOrStart(
4347
ipcOptions,
44-
ganacheOptions
48+
ganacheOptions,
49+
truffleConfig
4550
);
4651
const ipcDisconnect = disconnect;
47-
await Environment.develop(config, ganacheOptions);
48-
const { temporaryDirectory } = await copyArtifactsToTempDir(config);
52+
await Environment.develop(truffleConfig, ganacheOptions);
53+
const { temporaryDirectory } = await copyArtifactsToTempDir(truffleConfig);
4954
const numberOfFailures = await prepareConfigAndRunTests({
50-
config,
55+
config: truffleConfig,
5156
files,
5257
temporaryDirectory
5358
});
@@ -103,6 +108,7 @@ module.exports = async function (options) {
103108
);
104109

105110
const ipcOptions = { network: "test" };
111+
106112
numberOfFailures = await startGanacheAndRunTests(
107113
ipcOptions,
108114
ganacheOptions,

0 commit comments

Comments
 (0)