From c7f86d38dba0516244ff5a09b6c43f3953433bad Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 22 Feb 2023 16:22:00 +1300 Subject: [PATCH 01/36] Basic implementation of logging to file --- src/chains/ethereum/ethereum/src/provider.ts | 1 + .../ethereum/options/src/logging-options.ts | 71 ++++++++++++++++--- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/provider.ts b/src/chains/ethereum/ethereum/src/provider.ts index 0d56db56f6..f1cf80671e 100644 --- a/src/chains/ethereum/ethereum/src/provider.ts +++ b/src/chains/ethereum/ethereum/src/provider.ts @@ -164,6 +164,7 @@ export class EthereumProvider providerOptions.fork.url || providerOptions.fork.provider || providerOptions.fork.network; + const fallback = fork ? new Fork(providerOptions, accounts) : null; const coinbase = parseCoinbase(providerOptions.miner.coinbase, accounts); const blockchain = new Blockchain(providerOptions, coinbase, fallback); diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index e6700d207a..a73631d9d4 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -1,5 +1,6 @@ import { normalize } from "./helpers"; import { Definitions } from "@ganache/options"; +import { appendFileSync } from "fs"; export type Logger = { log(message?: any, ...optionalParams: any[]): void; @@ -74,6 +75,22 @@ export type LoggingConfig = { type: boolean; hasDefault: true; }; + + /** + * The path to a file to log to. If this option is set, Ganache will log output + * to a file located at the path. + */ + readonly filePath: { + type: string; + }; + + /** + * Set to `true` to include a timestamp in the log output. + */ + readonly includeTimestamp: { + type: boolean; + hasDefault: true; + }; }; }; @@ -92,17 +109,6 @@ export const LoggingOptions: Definitions = { cliAliases: ["q", "quiet"], cliType: "boolean" }, - logger: { - normalize, - cliDescription: - "An object, like `console`, that implements a `log` function.", - disableInCLI: true, - // disable the default logger if `quiet` is `true` - default: config => ({ - log: config.quiet ? () => {} : console.log - }), - legacyName: "logger" - }, verbose: { normalize, cliDescription: "Set to `true` to log detailed RPC requests.", @@ -110,5 +116,48 @@ export const LoggingOptions: Definitions = { legacyName: "verbose", cliAliases: ["v", "verbose"], cliType: "boolean" + }, + filePath: { + normalize, + cliDescription: "The path to a file to log to.", + cliAliases: ["log-file"], + cliType: "string" + }, + includeTimestamp: { + normalize, + cliDescription: "Set to `true` to include a timestamp in the log output.", + cliType: "boolean", + default: () => false + }, + logger: { + normalize, + cliDescription: + "An object, like `console`, that implements a `log` function.", + disableInCLI: true, + // disable the default logger if `quiet` is `true` + default: config => { + let logger: (message?: any, ...optionalParams: any[]) => void; + if (config.filePath == null) { + logger = config.quiet ? () => {} : console.log; + } else { + const formatter = config.includeTimestamp + ? (message, additionalParams) => + `${Date.now()}\t${message} ${additionalParams.join(", ")}\n` + : (message, additionalParams) => + `${message} ${additionalParams.join(", ")}\n`; + + logger = (message: any, ...additionalParams: any[]) => { + appendFileSync( + config.filePath, + formatter(message.replace(/\n/g, "\n\t"), additionalParams) + ); + }; + } + + return { + log: logger + }; + }, + legacyName: "logger" } }; From 54b85c5629f316d90919697fbeadc4f3733c835d Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 22 Feb 2023 16:36:40 +1300 Subject: [PATCH 02/36] Simplify logging --- .../ethereum/options/src/logging-options.ts | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index a73631d9d4..5b37c17653 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -1,6 +1,7 @@ import { normalize } from "./helpers"; import { Definitions } from "@ganache/options"; import { appendFileSync } from "fs"; +import { format } from "util"; export type Logger = { log(message?: any, ...optionalParams: any[]): void; @@ -80,14 +81,14 @@ export type LoggingConfig = { * The path to a file to log to. If this option is set, Ganache will log output * to a file located at the path. */ - readonly filePath: { + readonly file: { type: string; }; /** * Set to `true` to include a timestamp in the log output. */ - readonly includeTimestamp: { + readonly timestamps: { type: boolean; hasDefault: true; }; @@ -117,13 +118,12 @@ export const LoggingOptions: Definitions = { cliAliases: ["v", "verbose"], cliType: "boolean" }, - filePath: { + file: { normalize, - cliDescription: "The path to a file to log to.", - cliAliases: ["log-file"], + cliDescription: "The path of a file to which logs will be appended.", cliType: "string" }, - includeTimestamp: { + timestamps: { normalize, cliDescription: "Set to `true` to include a timestamp in the log output.", cliType: "boolean", @@ -137,20 +137,29 @@ export const LoggingOptions: Definitions = { // disable the default logger if `quiet` is `true` default: config => { let logger: (message?: any, ...optionalParams: any[]) => void; - if (config.filePath == null) { - logger = config.quiet ? () => {} : console.log; + const consoleLogger = config.quiet ? () => {} : console.log; + + if (config.file == null) { + logger = consoleLogger; } else { - const formatter = config.includeTimestamp - ? (message, additionalParams) => - `${Date.now()}\t${message} ${additionalParams.join(", ")}\n` - : (message, additionalParams) => - `${message} ${additionalParams.join(", ")}\n`; + const diskLogFormatter = config.timestamps + ? message => { + const linePrefix = `${new Date().toISOString()} `; + // Matches _after_ a new line character _or_ the start of the + // string. Essentially the start of every line + return message.replace(/^|(?<=\n)/g, linePrefix); + } + : message => message; + + const formatter = (message, additionalParams) => { + const formattedMessage = format(message, ...additionalParams); + // we are logging to a file, but we still need to log to console + consoleLogger(formattedMessage); + return diskLogFormatter(formattedMessage) + "\n"; + }; logger = (message: any, ...additionalParams: any[]) => { - appendFileSync( - config.filePath, - formatter(message.replace(/\n/g, "\n\t"), additionalParams) - ); + appendFileSync(config.file, formatter(message, additionalParams)); }; } From b0ca2e9ec2c301eb1a7dd1f576b0c0c4a051ea41 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 3 Feb 2023 14:28:19 +1300 Subject: [PATCH 03/36] Simplify regex for inserting timestamps --- src/chains/ethereum/options/src/logging-options.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index 5b37c17653..cd6715f87d 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -145,9 +145,7 @@ export const LoggingOptions: Definitions = { const diskLogFormatter = config.timestamps ? message => { const linePrefix = `${new Date().toISOString()} `; - // Matches _after_ a new line character _or_ the start of the - // string. Essentially the start of every line - return message.replace(/^|(?<=\n)/g, linePrefix); + return message.replace(/^/gm, linePrefix); } : message => message; From a397b6ff5429141d93faa7f4bdeb09aecedc9148 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 22 Feb 2023 16:27:19 +1300 Subject: [PATCH 04/36] Remove --timestamps argument --- .../ethereum/options/src/logging-options.ts | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index cd6715f87d..18f47b771b 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -84,14 +84,6 @@ export type LoggingConfig = { readonly file: { type: string; }; - - /** - * Set to `true` to include a timestamp in the log output. - */ - readonly timestamps: { - type: boolean; - hasDefault: true; - }; }; }; @@ -123,12 +115,6 @@ export const LoggingOptions: Definitions = { cliDescription: "The path of a file to which logs will be appended.", cliType: "string" }, - timestamps: { - normalize, - cliDescription: "Set to `true` to include a timestamp in the log output.", - cliType: "boolean", - default: () => false - }, logger: { normalize, cliDescription: @@ -142,14 +128,12 @@ export const LoggingOptions: Definitions = { if (config.file == null) { logger = consoleLogger; } else { - const diskLogFormatter = config.timestamps - ? message => { - const linePrefix = `${new Date().toISOString()} `; - return message.replace(/^/gm, linePrefix); - } - : message => message; + const diskLogFormatter = (message: any) => { + const linePrefix = `${new Date().toISOString()} `; + return message.toString().replace(/^/gm, linePrefix); + }; - const formatter = (message, additionalParams) => { + const formatter = (message: any, additionalParams: any[]) => { const formattedMessage = format(message, ...additionalParams); // we are logging to a file, but we still need to log to console consoleLogger(formattedMessage); From 0f13a36afdbd859967c569c845de47681474cb38 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Mon, 20 Feb 2023 14:03:39 +1300 Subject: [PATCH 05/36] Use a persistent filehandle instead of calling 'appendFile' each tile a log is written --- src/chains/ethereum/options/src/logging-options.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index 18f47b771b..d4fcb40142 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -1,6 +1,7 @@ import { normalize } from "./helpers"; import { Definitions } from "@ganache/options"; -import { appendFileSync } from "fs"; +import { promises } from "fs"; +const open = promises.open; import { format } from "util"; export type Logger = { @@ -140,8 +141,16 @@ export const LoggingOptions: Definitions = { return diskLogFormatter(formattedMessage) + "\n"; }; + const whenHandle = open(config.file, "a"); + let writing: Promise; + logger = (message: any, ...additionalParams: any[]) => { - appendFileSync(config.file, formatter(message, additionalParams)); + whenHandle.then(async handle => { + if (writing) { + await writing; + } + writing = handle.appendFile(formatter(message, additionalParams)); + }); }; } From b206a42251e108ae5253a47066f22999ba072b31 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 21 Feb 2023 13:51:44 +1300 Subject: [PATCH 06/36] Introduce tests for logger, fail validation if path is not w ritable --- src/chains/ethereum/options/src/index.ts | 10 +- .../ethereum/options/src/logging-options.ts | 114 +++++++--- .../ethereum/options/tests/index.test.ts | 24 -- .../options/tests/logging-options.test.ts | 214 ++++++++++++++++++ 4 files changed, 295 insertions(+), 67 deletions(-) create mode 100644 src/chains/ethereum/options/tests/logging-options.test.ts diff --git a/src/chains/ethereum/options/src/index.ts b/src/chains/ethereum/options/src/index.ts index fb90a7cf8d..6237361edc 100644 --- a/src/chains/ethereum/options/src/index.ts +++ b/src/chains/ethereum/options/src/index.ts @@ -7,14 +7,12 @@ import { ForkConfig, ForkOptions } from "./fork-options"; import { Base, Defaults, - Definitions, ExternalConfig, InternalConfig, Legacy, LegacyOptions, OptionName, OptionRawType, - Options, OptionsConfig } from "@ganache/options"; import { UnionToIntersection } from "./helper-types"; @@ -45,11 +43,9 @@ export type EthereumLegacyProviderOptions = Partial< MakeLegacyOptions >; -export type EthereumProviderOptions = Partial< - { - [K in keyof EthereumConfig]: ExternalConfig; - } ->; +export type EthereumProviderOptions = Partial<{ + [K in keyof EthereumConfig]: ExternalConfig; +}>; export type EthereumInternalOptions = { [K in keyof EthereumConfig]: InternalConfig; diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index d4fcb40142..377fdcc4f4 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -1,11 +1,13 @@ import { normalize } from "./helpers"; import { Definitions } from "@ganache/options"; -import { promises } from "fs"; +import { promises, openSync, closeSync } from "fs"; const open = promises.open; import { format } from "util"; -export type Logger = { - log(message?: any, ...optionalParams: any[]): void; +export type LogFunc = (message?: any, ...optionalParams: any[]) => void; + +type Logger = { + log: LogFunc; }; export type LoggingConfig = { @@ -112,7 +114,19 @@ export const LoggingOptions: Definitions = { cliType: "boolean" }, file: { - normalize, + normalize: rawInput => { + // this will throw if the file is not writable + try { + const fh = openSync(rawInput, "a"); + closeSync(fh); + } catch (err) { + throw new Error( + `Failed to write logs to ${rawInput}. Please check if the file path is valid and if the process has write permissions to the directory.` + ); + } + + return rawInput; + }, cliDescription: "The path of a file to which logs will be appended.", cliType: "string" }, @@ -123,41 +137,69 @@ export const LoggingOptions: Definitions = { disableInCLI: true, // disable the default logger if `quiet` is `true` default: config => { - let logger: (message?: any, ...optionalParams: any[]) => void; - const consoleLogger = config.quiet ? () => {} : console.log; - - if (config.file == null) { - logger = consoleLogger; - } else { - const diskLogFormatter = (message: any) => { - const linePrefix = `${new Date().toISOString()} `; - return message.toString().replace(/^/gm, linePrefix); - }; - - const formatter = (message: any, additionalParams: any[]) => { - const formattedMessage = format(message, ...additionalParams); - // we are logging to a file, but we still need to log to console - consoleLogger(formattedMessage); - return diskLogFormatter(formattedMessage) + "\n"; - }; - - const whenHandle = open(config.file, "a"); - let writing: Promise; - - logger = (message: any, ...additionalParams: any[]) => { - whenHandle.then(async handle => { - if (writing) { - await writing; - } - writing = handle.appendFile(formatter(message, additionalParams)); - }); - }; - } - + const { log } = createLogger(config); return { - log: logger + log }; }, legacyName: "logger" } }; + +type CreateLoggerConfig = { + quiet?: boolean; + file?: string; +}; + +/** + * Create a logger function based on the provided config. + * + * @param config specifying the configuration for the logger + * @returns an object containing a `log` function and optional `getWaitHandle` + * function returning a `Promise` that resolves when any asyncronous + * activies are completed. + */ +export function createLogger(config: { quiet?: boolean; file?: string }): { + log: LogFunc; + getWaitHandle?: () => Promise; +} { + const logToConsole = config.quiet + ? async () => {} + : async (message: any, ...optionalParams: any[]) => + console.log(message, ...optionalParams); + + if ("file" in config) { + const diskLogFormatter = (message: any) => { + const linePrefix = `${new Date().toISOString()} `; + return message.toString().replace(/^/gm, linePrefix); + }; + + // we never close this handle, which is only ever problematic if we create a + // _lot_ of handles. This can't happen, except (potentially) in tests, + // because we only ever create one logger per Ganache instance. + const whenHandle = open(config.file, "a"); + + let writing = Promise.resolve(null); + + const log = (message: any, ...optionalParams: any[]) => { + const formattedMessage = format(message, ...optionalParams); + // we are logging to a file, but we still need to log to console + logToConsole(formattedMessage); + + const currentWriting = writing; + writing = whenHandle.then(async handle => { + await currentWriting; + + return handle.appendFile(diskLogFormatter(formattedMessage) + "\n"); + }); + }; + return { + log, + getWaitHandle: () => writing + }; + } else { + return { + log: logToConsole + }; + } +} diff --git a/src/chains/ethereum/options/tests/index.test.ts b/src/chains/ethereum/options/tests/index.test.ts index fe5a2057ac..a6aeef87e2 100644 --- a/src/chains/ethereum/options/tests/index.test.ts +++ b/src/chains/ethereum/options/tests/index.test.ts @@ -3,30 +3,6 @@ import { EthereumDefaults, EthereumOptionsConfig } from "../src"; import sinon from "sinon"; describe("EthereumOptionsConfig", () => { - describe("options", () => { - let spy: any; - beforeEach(() => { - spy = sinon.spy(console, "log"); - }); - afterEach(() => { - spy.restore(); - }); - it("logs via console.log by default", () => { - const message = "message"; - const options = EthereumOptionsConfig.normalize({}); - options.logging.logger.log(message); - assert.strictEqual(spy.withArgs(message).callCount, 1); - }); - - it("disables the logger when the quiet flag is used", () => { - const message = "message"; - const options = EthereumOptionsConfig.normalize({ - logging: { quiet: true } - }); - options.logging.logger.log(message); - assert.strictEqual(spy.withArgs(message).callCount, 0); - }); - }); describe(".normalize", () => { it("returns an options object with all default namespaces", () => { const options = EthereumOptionsConfig.normalize({}); diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts new file mode 100644 index 0000000000..0aee44f35c --- /dev/null +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -0,0 +1,214 @@ +import assert from "assert"; +import { createLogger, EthereumOptionsConfig, LogFunc } from "../src"; +import sinon from "sinon"; +import { promises } from "fs"; +const { readFile, unlink } = promises; + +describe("EthereumOptionsConfig", () => { + describe("logging", () => { + describe("options", () => { + let spy: any; + beforeEach(() => { + spy = sinon.spy(console, "log"); + }); + + afterEach(() => { + spy.restore(); + }); + + it("logs via console.log by default", () => { + const message = "message"; + const options = EthereumOptionsConfig.normalize({}); + options.logging.logger.log(message); + assert.strictEqual(spy.withArgs(message).callCount, 1); + }); + + it("disables the logger when the quiet flag is used", () => { + const message = "message"; + const options = EthereumOptionsConfig.normalize({ + logging: { quiet: true } + }); + options.logging.logger.log(message); + assert.strictEqual(spy.withArgs(message).callCount, 0); + }); + + it("fails if an invalid file path is provided", () => { + const invalidPath = "/invalid_path_to_file.log"; + const message = `Failed to write logs to ${invalidPath}. Please check if the file path is valid and if the process has write permissions to the directory.`; + + assert.throws(() => { + const options = EthereumOptionsConfig.normalize({ + logging: { file: invalidPath } + }); + }, new Error(message)); + }); + }); + + describe("createLogger()", () => { + const file = "./test.log"; + const message = "test message"; + + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + sandbox.spy(console, "log"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should create a console logger by default", () => { + const { log } = createLogger({}); + const logMethod = console.log as any; + + log(message); + + assert.strictEqual( + logMethod.callCount, + 1, + "console.log() was called unexpected number of times." + ); + + const args = logMethod.getCall(0).args; + + assert.deepStrictEqual( + args, + [message], + "Console.log called with unexpected arguments." + ); + }); + + it("should still log to console when a file is specified", async () => { + const { log, getWaitHandle } = createLogger({ file }); + assert(getWaitHandle); + + const logMethod = console.log as any; + + try { + log(message); + await getWaitHandle(); + } finally { + await unlink(file); + } + + assert.strictEqual( + logMethod.callCount, + 1, + "console.log() was called unexpected number of times." + ); + + const args = logMethod.getCall(0).args; + + assert.deepStrictEqual( + args, + [message], + "Console.log called with unexpected arguments." + ); + }); + + it("should write to the file provided", async () => { + const { log, getWaitHandle } = createLogger({ file }); + assert(getWaitHandle); + + let fileContents: string; + try { + log(`${message} 0`); + log(`${message} 1`); + log(`${message} 2`); + await getWaitHandle(); + + fileContents = await readFile(file, "utf8"); + } finally { + await unlink(file); + } + + const logLines = fileContents.split("\n"); + + // 4, because there's a \n at the end of each line + assert.strictEqual(logLines.length, 4); + assert.strictEqual(logLines[3], ""); + + logLines.slice(0, 3).forEach((logLine, lineNumber) => { + const timestampPart = logLine.slice(0, 24); + const messagePart = logLine.slice(25); + const delimiter = logLine[24]; + + assert( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(timestampPart), + "Unexpected timestamp." + ); + assert.strictEqual(delimiter, " ", "Unexpected delimiter."); + assert.strictEqual( + messagePart, + `${message} ${lineNumber}`, + "Unexpected message." + ); + }); + }); + + it("should timestamp each line on multi-line log messages", async () => { + const { log, getWaitHandle } = createLogger({ file }); + assert(getWaitHandle); + + const expectedLines = ["multi", "line", "message"]; + + let loggedLines: string[]; + try { + log(expectedLines.join("\n")); + await getWaitHandle(); + + const fileContents = await readFile(file, "utf8"); + loggedLines = fileContents.split("\n"); + } finally { + await unlink(file); + } + + // length == 4, because there's a \n at the end (string.split() results + // in an empty string) + assert.strictEqual(loggedLines.length, 4); + assert.strictEqual(loggedLines[3], ""); + + loggedLines.slice(0, 3).forEach((logLine, lineNumber) => { + const timestampPart = logLine.slice(0, 24); + const messagePart = logLine.slice(25); + const delimiter = logLine[24]; + + assert( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(timestampPart), + "Unexpected timestamp" + ); + assert.strictEqual(delimiter, " ", "Unexpected delimiter"); + assert.strictEqual(messagePart, expectedLines[lineNumber]); + }); + }); + + it("should not throw if the underlying file does not exist", async () => { + const { log, getWaitHandle } = createLogger({ file }); + assert(getWaitHandle); + + try { + log(message); + await getWaitHandle(); + } finally { + await unlink(file); + } + }); + + it("should reject waitHandle if the underlying file is inaccessible", () => { + const { log, getWaitHandle } = createLogger({ + file: "/invalid/path/to/log/file.log" + }); + assert(getWaitHandle); + + log(message); + + assert.rejects( + getWaitHandle(), + err => (err as NodeJS.ErrnoException).code === "ENOENT", + "Expected an error to be thrown with code 'ENOENT'." + ); + }); + }); + }); +}); From d1ec4fe83861c82cc146d487af36c452b9083f9a Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 23 Feb 2023 13:01:29 +1300 Subject: [PATCH 07/36] Update tests to use per-test log files --- .../ethereum/options/tests/logging-options.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index 0aee44f35c..a5ea4be956 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -45,7 +45,7 @@ describe("EthereumOptionsConfig", () => { }); describe("createLogger()", () => { - const file = "./test.log"; + const getFilename = (slug: string) => `./tests/test-${slug}.log`; const message = "test message"; const sandbox = sinon.createSandbox(); @@ -80,6 +80,8 @@ describe("EthereumOptionsConfig", () => { }); it("should still log to console when a file is specified", async () => { + const file = getFilename("write-to-console"); + const { log, getWaitHandle } = createLogger({ file }); assert(getWaitHandle); @@ -108,6 +110,7 @@ describe("EthereumOptionsConfig", () => { }); it("should write to the file provided", async () => { + const file = getFilename("write-to-file-provided"); const { log, getWaitHandle } = createLogger({ file }); assert(getWaitHandle); @@ -148,6 +151,8 @@ describe("EthereumOptionsConfig", () => { }); it("should timestamp each line on multi-line log messages", async () => { + const file = getFilename("timestamp-each-line"); + const { log, getWaitHandle } = createLogger({ file }); assert(getWaitHandle); @@ -184,6 +189,8 @@ describe("EthereumOptionsConfig", () => { }); it("should not throw if the underlying file does not exist", async () => { + const file = getFilename("underlying-file-does-not-exist"); + const { log, getWaitHandle } = createLogger({ file }); assert(getWaitHandle); From feb57bd0dd7f711918323d1b49bd567ef785b0c3 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 23 Feb 2023 13:35:08 +1300 Subject: [PATCH 08/36] Resolve invalid path --- src/chains/ethereum/options/tests/logging-options.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index a5ea4be956..e084fc1d43 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -2,6 +2,7 @@ import assert from "assert"; import { createLogger, EthereumOptionsConfig, LogFunc } from "../src"; import sinon from "sinon"; import { promises } from "fs"; +import { resolve } from "path"; const { readFile, unlink } = promises; describe("EthereumOptionsConfig", () => { @@ -203,8 +204,10 @@ describe("EthereumOptionsConfig", () => { }); it("should reject waitHandle if the underlying file is inaccessible", () => { + const file = resolve("/invalid/path/to/log/file.log"); + console.log({ file }); const { log, getWaitHandle } = createLogger({ - file: "/invalid/path/to/log/file.log" + file }); assert(getWaitHandle); From 0b8673f1023281b940c90f37727fd960fd043e72 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 23 Feb 2023 13:54:25 +1300 Subject: [PATCH 09/36] In windows, use reserved path 'c:\NUL' for invalid path --- src/chains/ethereum/options/tests/logging-options.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index e084fc1d43..f9e1ebb5ea 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -204,8 +204,7 @@ describe("EthereumOptionsConfig", () => { }); it("should reject waitHandle if the underlying file is inaccessible", () => { - const file = resolve("/invalid/path/to/log/file.log"); - console.log({ file }); + const file = process.platform === "win32" ? "c:\\NUL" :"/invalid/path/to/log/file.log"; const { log, getWaitHandle } = createLogger({ file }); From 6d73be8466ca4c06fb82e42c8fcf90d6ab011eb4 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 23 Feb 2023 14:08:29 +1300 Subject: [PATCH 10/36] Revert changes and just await the rejection --- src/chains/ethereum/options/tests/logging-options.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index f9e1ebb5ea..0527cb9e61 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -203,8 +203,8 @@ describe("EthereumOptionsConfig", () => { } }); - it("should reject waitHandle if the underlying file is inaccessible", () => { - const file = process.platform === "win32" ? "c:\\NUL" :"/invalid/path/to/log/file.log"; + it("should reject waitHandle if the underlying file is inaccessible", async () => { + const file = "/invalid/path/to/file.log"; const { log, getWaitHandle } = createLogger({ file }); @@ -212,7 +212,7 @@ describe("EthereumOptionsConfig", () => { log(message); - assert.rejects( + await assert.rejects( getWaitHandle(), err => (err as NodeJS.ErrnoException).code === "ENOENT", "Expected an error to be thrown with code 'ENOENT'." From 8b4ae04029f5991ea6484aa3ee85b45bec3f3578 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 23 Feb 2023 14:28:36 +1300 Subject: [PATCH 11/36] Let's do use the NUL workaround for windows --- src/chains/ethereum/options/tests/logging-options.test.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index 0527cb9e61..515610e2d8 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -204,7 +204,7 @@ describe("EthereumOptionsConfig", () => { }); it("should reject waitHandle if the underlying file is inaccessible", async () => { - const file = "/invalid/path/to/file.log"; + const file = process.platform === "win32" ? "c:\\NUL" : "/invalid/path/to/file"; const { log, getWaitHandle } = createLogger({ file }); @@ -213,10 +213,7 @@ describe("EthereumOptionsConfig", () => { log(message); await assert.rejects( - getWaitHandle(), - err => (err as NodeJS.ErrnoException).code === "ENOENT", - "Expected an error to be thrown with code 'ENOENT'." - ); + getWaitHandle()); }); }); }); From 4b6159e75c1c3a1f90eb08adf7f42b415befa097 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 23 Feb 2023 15:09:51 +1300 Subject: [PATCH 12/36] Attempt to get tests to passing state --- .../ethereum/options/tests/logging-options.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index 515610e2d8..b20454b86f 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -2,7 +2,6 @@ import assert from "assert"; import { createLogger, EthereumOptionsConfig, LogFunc } from "../src"; import sinon from "sinon"; import { promises } from "fs"; -import { resolve } from "path"; const { readFile, unlink } = promises; describe("EthereumOptionsConfig", () => { @@ -34,11 +33,11 @@ describe("EthereumOptionsConfig", () => { }); it("fails if an invalid file path is provided", () => { - const invalidPath = "/invalid_path_to_file.log"; + const invalidPath = "/invalid/path/to/file.log"; const message = `Failed to write logs to ${invalidPath}. Please check if the file path is valid and if the process has write permissions to the directory.`; - + assert.throws(() => { - const options = EthereumOptionsConfig.normalize({ + EthereumOptionsConfig.normalize({ logging: { file: invalidPath } }); }, new Error(message)); @@ -204,10 +203,11 @@ describe("EthereumOptionsConfig", () => { }); it("should reject waitHandle if the underlying file is inaccessible", async () => { - const file = process.platform === "win32" ? "c:\\NUL" : "/invalid/path/to/file"; + const file = process.platform === "win32" ? "t:\\volume_does_not_exist" : "/invalid/path/to/file.log"; const { log, getWaitHandle } = createLogger({ file }); + assert(getWaitHandle); log(message); From e2db28f4a2a465e801661f15d67dd86398b23ec1 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 23 Feb 2023 17:05:28 +1300 Subject: [PATCH 13/36] Use current working directory as invalid file path --- .../ethereum/options/tests/logging-options.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index b20454b86f..05d22bb051 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -2,6 +2,7 @@ import assert from "assert"; import { createLogger, EthereumOptionsConfig, LogFunc } from "../src"; import sinon from "sinon"; import { promises } from "fs"; +import { resolve } from "path"; const { readFile, unlink } = promises; describe("EthereumOptionsConfig", () => { @@ -33,9 +34,10 @@ describe("EthereumOptionsConfig", () => { }); it("fails if an invalid file path is provided", () => { - const invalidPath = "/invalid/path/to/file.log"; + // resolve to the current working directory, which is clearly an invalid file path. + const invalidPath = resolve(""); const message = `Failed to write logs to ${invalidPath}. Please check if the file path is valid and if the process has write permissions to the directory.`; - + assert.throws(() => { EthereumOptionsConfig.normalize({ logging: { file: invalidPath } @@ -203,7 +205,9 @@ describe("EthereumOptionsConfig", () => { }); it("should reject waitHandle if the underlying file is inaccessible", async () => { - const file = process.platform === "win32" ? "t:\\volume_does_not_exist" : "/invalid/path/to/file.log"; + // resolve to the current working directory, which is clearly an invalid file path. + const file = resolve(""); + const { log, getWaitHandle } = createLogger({ file }); @@ -212,8 +216,7 @@ describe("EthereumOptionsConfig", () => { log(message); - await assert.rejects( - getWaitHandle()); + await assert.rejects(getWaitHandle()); }); }); }); From d2db1a4adb721e182410cc0f3c9651a9bd233933 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 24 Feb 2023 09:05:37 +1300 Subject: [PATCH 14/36] Tidy up logging --- src/chains/ethereum/ethereum/src/provider.ts | 1 - .../ethereum/options/src/logging-options.ts | 15 ++++++----- .../options/tests/logging-options.test.ts | 25 +++++++++---------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/provider.ts b/src/chains/ethereum/ethereum/src/provider.ts index f1cf80671e..0d56db56f6 100644 --- a/src/chains/ethereum/ethereum/src/provider.ts +++ b/src/chains/ethereum/ethereum/src/provider.ts @@ -164,7 +164,6 @@ export class EthereumProvider providerOptions.fork.url || providerOptions.fork.provider || providerOptions.fork.network; - const fallback = fork ? new Fork(providerOptions, accounts) : null; const coinbase = parseCoinbase(providerOptions.miner.coinbase, accounts); const blockchain = new Blockchain(providerOptions, coinbase, fallback); diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index 377fdcc4f4..afec8645ef 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -81,8 +81,8 @@ export type LoggingConfig = { }; /** - * The path to a file to log to. If this option is set, Ganache will log output - * to a file located at the path. + * If you set this option, Ganache will write logs to a file located at the + * specified path. */ readonly file: { type: string; @@ -127,7 +127,8 @@ export const LoggingOptions: Definitions = { return rawInput; }, - cliDescription: "The path of a file to which logs will be appended.", + cliDescription: + "If set, Ganache will write logs to a file located at the specified path.", cliType: "string" }, logger: { @@ -137,6 +138,8 @@ export const LoggingOptions: Definitions = { disableInCLI: true, // disable the default logger if `quiet` is `true` default: config => { + // don't just return the response from createLogger(), because it includes + // getWaitHandle const { log } = createLogger(config); return { log @@ -146,11 +149,6 @@ export const LoggingOptions: Definitions = { } }; -type CreateLoggerConfig = { - quiet?: boolean; - file?: string; -}; - /** * Create a logger function based on the provided config. * @@ -193,6 +191,7 @@ export function createLogger(config: { quiet?: boolean; file?: string }): { return handle.appendFile(diskLogFormatter(formattedMessage) + "\n"); }); }; + return { log, getWaitHandle: () => writing diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index 05d22bb051..b68d04a785 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -7,6 +7,10 @@ const { readFile, unlink } = promises; describe("EthereumOptionsConfig", () => { describe("logging", () => { + // resolve absolute path of current working directory, which is clearly an + // invalid file path (because it's a directory). + const invalidFilePath = resolve(""); + describe("options", () => { let spy: any; beforeEach(() => { @@ -34,20 +38,18 @@ describe("EthereumOptionsConfig", () => { }); it("fails if an invalid file path is provided", () => { - // resolve to the current working directory, which is clearly an invalid file path. - const invalidPath = resolve(""); - const message = `Failed to write logs to ${invalidPath}. Please check if the file path is valid and if the process has write permissions to the directory.`; + const message = `Failed to write logs to ${invalidFilePath}. Please check if the file path is valid and if the process has write permissions to the directory.`; assert.throws(() => { EthereumOptionsConfig.normalize({ - logging: { file: invalidPath } + logging: { file: invalidFilePath } }); }, new Error(message)); }); }); describe("createLogger()", () => { - const getFilename = (slug: string) => `./tests/test-${slug}.log`; + const getFilePath = (slug: string) => `./tests/test-${slug}.log`; const message = "test message"; const sandbox = sinon.createSandbox(); @@ -82,7 +84,7 @@ describe("EthereumOptionsConfig", () => { }); it("should still log to console when a file is specified", async () => { - const file = getFilename("write-to-console"); + const file = getFilePath("write-to-console"); const { log, getWaitHandle } = createLogger({ file }); assert(getWaitHandle); @@ -112,7 +114,7 @@ describe("EthereumOptionsConfig", () => { }); it("should write to the file provided", async () => { - const file = getFilename("write-to-file-provided"); + const file = getFilePath("write-to-file-provided"); const { log, getWaitHandle } = createLogger({ file }); assert(getWaitHandle); @@ -153,7 +155,7 @@ describe("EthereumOptionsConfig", () => { }); it("should timestamp each line on multi-line log messages", async () => { - const file = getFilename("timestamp-each-line"); + const file = getFilePath("timestamp-each-line"); const { log, getWaitHandle } = createLogger({ file }); assert(getWaitHandle); @@ -191,7 +193,7 @@ describe("EthereumOptionsConfig", () => { }); it("should not throw if the underlying file does not exist", async () => { - const file = getFilename("underlying-file-does-not-exist"); + const file = getFilePath("underlying-file-does-not-exist"); const { log, getWaitHandle } = createLogger({ file }); assert(getWaitHandle); @@ -205,11 +207,8 @@ describe("EthereumOptionsConfig", () => { }); it("should reject waitHandle if the underlying file is inaccessible", async () => { - // resolve to the current working directory, which is clearly an invalid file path. - const file = resolve(""); - const { log, getWaitHandle } = createLogger({ - file + file: invalidFilePath }); assert(getWaitHandle); From 4e64fc7666934c378c22eb201b02e8da19b5f367 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 24 Feb 2023 09:28:36 +1300 Subject: [PATCH 15/36] Create file when validating logging.file path is writable. --- src/chains/ethereum/options/src/logging-options.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index afec8645ef..86b76481c0 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -1,6 +1,6 @@ import { normalize } from "./helpers"; import { Definitions } from "@ganache/options"; -import { promises, openSync, closeSync } from "fs"; +import { promises, writeFileSync } from "fs"; const open = promises.open; import { format } from "util"; @@ -115,10 +115,9 @@ export const LoggingOptions: Definitions = { }, file: { normalize: rawInput => { - // this will throw if the file is not writable + // this will throw if the file is not writable, and creates the log file for later appending try { - const fh = openSync(rawInput, "a"); - closeSync(fh); + writeFileSync(rawInput, Buffer.alloc(0)); } catch (err) { throw new Error( `Failed to write logs to ${rawInput}. Please check if the file path is valid and if the process has write permissions to the directory.` From 8423a65771d61e14dece056ef76c096ac433f0fa Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Mon, 27 Feb 2023 16:29:03 +1300 Subject: [PATCH 16/36] Move logger into cli project, allow caller to close underlying filehandle --- .../ethereum/options/src/logging-options.ts | 63 +----- .../ethereum/options/tests/index.test.ts | 1 - .../options/tests/logging-options.test.ts | 175 +--------------- src/packages/cli/src/cli.ts | 16 +- src/packages/cli/src/logger.ts | 59 ++++++ src/packages/cli/tests/logger.test.ts | 189 ++++++++++++++++++ 6 files changed, 266 insertions(+), 237 deletions(-) create mode 100644 src/packages/cli/src/logger.ts create mode 100644 src/packages/cli/tests/logger.test.ts diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index 86b76481c0..33f61edfbe 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -1,8 +1,6 @@ import { normalize } from "./helpers"; import { Definitions } from "@ganache/options"; -import { promises, writeFileSync } from "fs"; -const open = promises.open; -import { format } from "util"; +import { writeFileSync } from "fs"; export type LogFunc = (message?: any, ...optionalParams: any[]) => void; @@ -137,67 +135,10 @@ export const LoggingOptions: Definitions = { disableInCLI: true, // disable the default logger if `quiet` is `true` default: config => { - // don't just return the response from createLogger(), because it includes - // getWaitHandle - const { log } = createLogger(config); return { - log + log: config.quiet ? () => {} : console.log }; }, legacyName: "logger" } }; - -/** - * Create a logger function based on the provided config. - * - * @param config specifying the configuration for the logger - * @returns an object containing a `log` function and optional `getWaitHandle` - * function returning a `Promise` that resolves when any asyncronous - * activies are completed. - */ -export function createLogger(config: { quiet?: boolean; file?: string }): { - log: LogFunc; - getWaitHandle?: () => Promise; -} { - const logToConsole = config.quiet - ? async () => {} - : async (message: any, ...optionalParams: any[]) => - console.log(message, ...optionalParams); - - if ("file" in config) { - const diskLogFormatter = (message: any) => { - const linePrefix = `${new Date().toISOString()} `; - return message.toString().replace(/^/gm, linePrefix); - }; - - // we never close this handle, which is only ever problematic if we create a - // _lot_ of handles. This can't happen, except (potentially) in tests, - // because we only ever create one logger per Ganache instance. - const whenHandle = open(config.file, "a"); - - let writing = Promise.resolve(null); - - const log = (message: any, ...optionalParams: any[]) => { - const formattedMessage = format(message, ...optionalParams); - // we are logging to a file, but we still need to log to console - logToConsole(formattedMessage); - - const currentWriting = writing; - writing = whenHandle.then(async handle => { - await currentWriting; - - return handle.appendFile(diskLogFormatter(formattedMessage) + "\n"); - }); - }; - - return { - log, - getWaitHandle: () => writing - }; - } else { - return { - log: logToConsole - }; - } -} diff --git a/src/chains/ethereum/options/tests/index.test.ts b/src/chains/ethereum/options/tests/index.test.ts index a6aeef87e2..111d46abe4 100644 --- a/src/chains/ethereum/options/tests/index.test.ts +++ b/src/chains/ethereum/options/tests/index.test.ts @@ -1,6 +1,5 @@ import assert from "assert"; import { EthereumDefaults, EthereumOptionsConfig } from "../src"; -import sinon from "sinon"; describe("EthereumOptionsConfig", () => { describe(".normalize", () => { diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index b68d04a785..2ab36a7a33 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -1,9 +1,7 @@ import assert from "assert"; -import { createLogger, EthereumOptionsConfig, LogFunc } from "../src"; +import { EthereumOptionsConfig } from "../src"; import sinon from "sinon"; -import { promises } from "fs"; import { resolve } from "path"; -const { readFile, unlink } = promises; describe("EthereumOptionsConfig", () => { describe("logging", () => { @@ -47,176 +45,5 @@ describe("EthereumOptionsConfig", () => { }, new Error(message)); }); }); - - describe("createLogger()", () => { - const getFilePath = (slug: string) => `./tests/test-${slug}.log`; - const message = "test message"; - - const sandbox = sinon.createSandbox(); - - beforeEach(() => { - sandbox.spy(console, "log"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should create a console logger by default", () => { - const { log } = createLogger({}); - const logMethod = console.log as any; - - log(message); - - assert.strictEqual( - logMethod.callCount, - 1, - "console.log() was called unexpected number of times." - ); - - const args = logMethod.getCall(0).args; - - assert.deepStrictEqual( - args, - [message], - "Console.log called with unexpected arguments." - ); - }); - - it("should still log to console when a file is specified", async () => { - const file = getFilePath("write-to-console"); - - const { log, getWaitHandle } = createLogger({ file }); - assert(getWaitHandle); - - const logMethod = console.log as any; - - try { - log(message); - await getWaitHandle(); - } finally { - await unlink(file); - } - - assert.strictEqual( - logMethod.callCount, - 1, - "console.log() was called unexpected number of times." - ); - - const args = logMethod.getCall(0).args; - - assert.deepStrictEqual( - args, - [message], - "Console.log called with unexpected arguments." - ); - }); - - it("should write to the file provided", async () => { - const file = getFilePath("write-to-file-provided"); - const { log, getWaitHandle } = createLogger({ file }); - assert(getWaitHandle); - - let fileContents: string; - try { - log(`${message} 0`); - log(`${message} 1`); - log(`${message} 2`); - await getWaitHandle(); - - fileContents = await readFile(file, "utf8"); - } finally { - await unlink(file); - } - - const logLines = fileContents.split("\n"); - - // 4, because there's a \n at the end of each line - assert.strictEqual(logLines.length, 4); - assert.strictEqual(logLines[3], ""); - - logLines.slice(0, 3).forEach((logLine, lineNumber) => { - const timestampPart = logLine.slice(0, 24); - const messagePart = logLine.slice(25); - const delimiter = logLine[24]; - - assert( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(timestampPart), - "Unexpected timestamp." - ); - assert.strictEqual(delimiter, " ", "Unexpected delimiter."); - assert.strictEqual( - messagePart, - `${message} ${lineNumber}`, - "Unexpected message." - ); - }); - }); - - it("should timestamp each line on multi-line log messages", async () => { - const file = getFilePath("timestamp-each-line"); - - const { log, getWaitHandle } = createLogger({ file }); - assert(getWaitHandle); - - const expectedLines = ["multi", "line", "message"]; - - let loggedLines: string[]; - try { - log(expectedLines.join("\n")); - await getWaitHandle(); - - const fileContents = await readFile(file, "utf8"); - loggedLines = fileContents.split("\n"); - } finally { - await unlink(file); - } - - // length == 4, because there's a \n at the end (string.split() results - // in an empty string) - assert.strictEqual(loggedLines.length, 4); - assert.strictEqual(loggedLines[3], ""); - - loggedLines.slice(0, 3).forEach((logLine, lineNumber) => { - const timestampPart = logLine.slice(0, 24); - const messagePart = logLine.slice(25); - const delimiter = logLine[24]; - - assert( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(timestampPart), - "Unexpected timestamp" - ); - assert.strictEqual(delimiter, " ", "Unexpected delimiter"); - assert.strictEqual(messagePart, expectedLines[lineNumber]); - }); - }); - - it("should not throw if the underlying file does not exist", async () => { - const file = getFilePath("underlying-file-does-not-exist"); - - const { log, getWaitHandle } = createLogger({ file }); - assert(getWaitHandle); - - try { - log(message); - await getWaitHandle(); - } finally { - await unlink(file); - } - }); - - it("should reject waitHandle if the underlying file is inaccessible", async () => { - const { log, getWaitHandle } = createLogger({ - file: invalidFilePath - }); - - assert(getWaitHandle); - - log(message); - - await assert.rejects(getWaitHandle()); - }); - }); }); }); diff --git a/src/packages/cli/src/cli.ts b/src/packages/cli/src/cli.ts index 1864b4d374..5471dbe217 100644 --- a/src/packages/cli/src/cli.ts +++ b/src/packages/cli/src/cli.ts @@ -6,6 +6,7 @@ import args from "./args"; import { EthereumFlavorName, FilecoinFlavorName } from "@ganache/flavors"; import initializeEthereum from "./initialize/ethereum"; import initializeFilecoin from "./initialize/filecoin"; +import { createLogger } from "./logger"; import type { FilecoinProvider } from "@ganache/filecoin"; import type { EthereumProvider } from "@ganache/ethereum"; import { @@ -54,6 +55,14 @@ if (argv.action === "start") { const flavor = argv.flavor; const cliSettings = argv.server; + const loggingOptions = (argv as any).logging; + let closeLogHandle = null; + if (loggingOptions.file) { + const { log, close } = createLogger((argv as any).logging); + (argv as any).logging.logger = { log }; + closeLogHandle = close; + } + console.log(detailedVersion); let server: ReturnType; @@ -88,7 +97,12 @@ if (argv.action === "start") { return; case ServerStatus.open: console.log("Shutting down…"); - await server.close(); + + if (closeLogHandle) { + await Promise.all([server.close(), closeLogHandle()]); + } else { + await server.close(); + } console.log("Server has been shut down"); break; } diff --git a/src/packages/cli/src/logger.ts b/src/packages/cli/src/logger.ts new file mode 100644 index 0000000000..eb0ae176a2 --- /dev/null +++ b/src/packages/cli/src/logger.ts @@ -0,0 +1,59 @@ +import { open, FileHandle } from "fs/promises"; +import { format } from "util"; + +export type LogFunc = (message?: any, ...optionalParams: any[]) => void; + +/** + * Create a logger function based on the provided config. + * + * @param config specifying the configuration for the logger + * @returns an object containing a `log` function and optional `getCompletionPromise` + * function returning a `Promise` that resolves when any asyncronous + * activies are completed. + */ +export function createLogger(config: { baseLog: LogFunc; file: string }): { + log: LogFunc; + getCompletionPromise: () => Promise; + close: () => Promise; +}; +export function createLogger(config: { baseLog: LogFunc }): { log: LogFunc }; +export function createLogger(config: { baseLog: LogFunc; file?: string }): { + log: LogFunc; + getCompletionPromise?: () => Promise; + close?: () => Promise; +} { + if ("file" in config) { + const diskLogFormatter = (message: any) => { + const linePrefix = `${new Date().toISOString()} `; + return message.toString().replace(/^/gm, linePrefix); + }; + + // we pass this handle back out so that it can be closed by the caller + const whenHandle = open(config.file, "a"); + + let writing = Promise.resolve(null); + + const log = (message: any, ...optionalParams: any[]) => { + // we are logging to a file, but we still need to call the base logger + config.baseLog(message, ...optionalParams); + + const formattedMessage = format(message, ...optionalParams); + const currentWriting = writing; + writing = whenHandle.then(async handle => { + await currentWriting; + + return handle.appendFile(diskLogFormatter(formattedMessage) + "\n"); + }); + }; + + return { + log, + getCompletionPromise: () => writing, + close: async () => (await whenHandle).close() + }; + } else { + return { + log: config.baseLog + }; + } +} diff --git a/src/packages/cli/tests/logger.test.ts b/src/packages/cli/tests/logger.test.ts new file mode 100644 index 0000000000..09c36293ec --- /dev/null +++ b/src/packages/cli/tests/logger.test.ts @@ -0,0 +1,189 @@ +import assert from "assert"; +import { resolve } from "path"; +import { createLogger } from "../src/logger"; +import { readFile, unlink } from "fs/promises"; + +describe("createLogger()", () => { + const getFilePath = (slug: string) => `./tests/test-${slug}.log`; + + const createBaseLogger = () => { + const calls: any[][] = []; + return { + baseLog: (message, ...params) => { + calls.push([message, ...params]); + }, + calls + }; + }; + const invalidFilePath = resolve(""); + + const message = "test message"; + + it("should create a baseLog() logger by default", () => { + const { baseLog, calls } = createBaseLogger(); + const { log } = createLogger({ baseLog }); + + log(message); + + assert.strictEqual( + calls.length, + 1, + "baseLog() was called unexpected number of times." + ); + + const baseLogArgs = calls[0]; + + assert.deepStrictEqual( + baseLogArgs, + [message], + "baseLog() called with unexpected arguments." + ); + }); + + it("should still call baseLog() when a file is specified", async () => { + const file = getFilePath("write-to-console"); + const { baseLog, calls } = createBaseLogger(); + + const { log, getCompletionPromise } = createLogger({ file, baseLog }); + + try { + log(message); + await getCompletionPromise(); + } finally { + await unlink(file); + } + + assert.strictEqual( + calls.length, + 1, + "baseLog() was called unexpected number of times." + ); + + const args = calls[0]; + + assert.deepStrictEqual( + args, + [message], + "baseLog() called with unexpected arguments." + ); + }); + + it("should write to the file provided", async () => { + const file = getFilePath("write-to-file-provided"); + const { baseLog } = createBaseLogger(); + + const { log, getCompletionPromise, close } = createLogger({ + file, + baseLog + }); + + let fileContents: string; + try { + log(`${message} 0`); + log(`${message} 1`); + log(`${message} 2`); + await getCompletionPromise(); + + fileContents = await readFile(file, "utf8"); + } finally { + await close(); + await unlink(file); + } + + const logLines = fileContents.split("\n"); + + // 4, because there's a \n at the end of each line + assert.strictEqual(logLines.length, 4); + assert.strictEqual(logLines[3], ""); + + logLines.slice(0, 3).forEach((logLine, lineNumber) => { + const timestampPart = logLine.slice(0, 24); + const messagePart = logLine.slice(25); + const delimiter = logLine[24]; + + assert( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(timestampPart), + "Unexpected timestamp." + ); + assert.strictEqual(delimiter, " ", "Unexpected delimiter."); + assert.strictEqual( + messagePart, + `${message} ${lineNumber}`, + "Unexpected message." + ); + }); + }); + + it("should timestamp each line on multi-line log messages", async () => { + const file = getFilePath("timestamp-each-line"); + const { baseLog } = createBaseLogger(); + + const { log, getCompletionPromise, close } = createLogger({ + file, + baseLog + }); + + const expectedLines = ["multi", "line", "message"]; + + let loggedLines: string[]; + try { + log(expectedLines.join("\n")); + await getCompletionPromise(); + + const fileContents = await readFile(file, "utf8"); + loggedLines = fileContents.split("\n"); + } finally { + await close(); + await unlink(file); + } + + // length == 4, because there's a \n at the end (string.split() results + // in an empty string) + assert.strictEqual(loggedLines.length, 4); + assert.strictEqual(loggedLines[3], ""); + + loggedLines.slice(0, 3).forEach((logLine, lineNumber) => { + const timestampPart = logLine.slice(0, 24); + const messagePart = logLine.slice(25); + const delimiter = logLine[24]; + + assert( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(timestampPart), + "Unexpected timestamp" + ); + assert.strictEqual(delimiter, " ", "Unexpected delimiter"); + assert.strictEqual(messagePart, expectedLines[lineNumber]); + }); + }); + + it("should not throw if the underlying file does not exist", async () => { + const file = getFilePath("underlying-file-does-not-exist"); + const { baseLog } = createBaseLogger(); + + const { log, getCompletionPromise, close } = createLogger({ + file, + baseLog + }); + + try { + log(message); + await getCompletionPromise(); + } finally { + await close(); + await unlink(file); + } + }); + + it("should reject waitHandle if the underlying file is inaccessible", async () => { + const { baseLog } = createBaseLogger(); + + const { log, getCompletionPromise } = createLogger({ + file: invalidFilePath, + baseLog + }); + + log(message); + + await assert.rejects(getCompletionPromise()); + }); +}); From fcf39ed0dfd5d835054694a598151ebbf9a3c644 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Mon, 27 Feb 2023 17:14:21 +1300 Subject: [PATCH 17/36] Revert unrelated changes, fix flaky initialisation of logger --- src/chains/ethereum/options/src/index.ts | 10 +++++-- .../ethereum/options/src/logging-options.ts | 30 +++++++++---------- src/packages/cli/src/cli.ts | 7 +++-- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/chains/ethereum/options/src/index.ts b/src/chains/ethereum/options/src/index.ts index 6237361edc..fb90a7cf8d 100644 --- a/src/chains/ethereum/options/src/index.ts +++ b/src/chains/ethereum/options/src/index.ts @@ -7,12 +7,14 @@ import { ForkConfig, ForkOptions } from "./fork-options"; import { Base, Defaults, + Definitions, ExternalConfig, InternalConfig, Legacy, LegacyOptions, OptionName, OptionRawType, + Options, OptionsConfig } from "@ganache/options"; import { UnionToIntersection } from "./helper-types"; @@ -43,9 +45,11 @@ export type EthereumLegacyProviderOptions = Partial< MakeLegacyOptions >; -export type EthereumProviderOptions = Partial<{ - [K in keyof EthereumConfig]: ExternalConfig; -}>; +export type EthereumProviderOptions = Partial< + { + [K in keyof EthereumConfig]: ExternalConfig; + } +>; export type EthereumInternalOptions = { [K in keyof EthereumConfig]: InternalConfig; diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index 33f61edfbe..e80ba0b96f 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -1,6 +1,6 @@ import { normalize } from "./helpers"; import { Definitions } from "@ganache/options"; -import { writeFileSync } from "fs"; +import { appendFileSync } from "fs"; export type LogFunc = (message?: any, ...optionalParams: any[]) => void; @@ -103,6 +103,19 @@ export const LoggingOptions: Definitions = { cliAliases: ["q", "quiet"], cliType: "boolean" }, + logger: { + normalize, + cliDescription: + "An object, like `console`, that implements a `log` function.", + disableInCLI: true, + // disable the default logger if `quiet` is `true` + default: config => { + return { + log: config.quiet ? () => {} : console.log + }; + }, + legacyName: "logger" + }, verbose: { normalize, cliDescription: "Set to `true` to log detailed RPC requests.", @@ -115,7 +128,7 @@ export const LoggingOptions: Definitions = { normalize: rawInput => { // this will throw if the file is not writable, and creates the log file for later appending try { - writeFileSync(rawInput, Buffer.alloc(0)); + appendFileSync(rawInput, Buffer.alloc(0)); } catch (err) { throw new Error( `Failed to write logs to ${rawInput}. Please check if the file path is valid and if the process has write permissions to the directory.` @@ -127,18 +140,5 @@ export const LoggingOptions: Definitions = { cliDescription: "If set, Ganache will write logs to a file located at the specified path.", cliType: "string" - }, - logger: { - normalize, - cliDescription: - "An object, like `console`, that implements a `log` function.", - disableInCLI: true, - // disable the default logger if `quiet` is `true` - default: config => { - return { - log: config.quiet ? () => {} : console.log - }; - }, - legacyName: "logger" } }; diff --git a/src/packages/cli/src/cli.ts b/src/packages/cli/src/cli.ts index 5471dbe217..6069ce9a77 100644 --- a/src/packages/cli/src/cli.ts +++ b/src/packages/cli/src/cli.ts @@ -58,8 +58,11 @@ if (argv.action === "start") { const loggingOptions = (argv as any).logging; let closeLogHandle = null; if (loggingOptions.file) { - const { log, close } = createLogger((argv as any).logging); - (argv as any).logging.logger = { log }; + const { log, close } = createLogger({ + baseLog: console.log, + file: loggingOptions.file + }); + loggingOptions.logger = { log }; closeLogHandle = close; } From 22ec2144b3d430885ca9b2c98664f076a58d6183 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Mon, 27 Feb 2023 17:26:34 +1300 Subject: [PATCH 18/36] Fix error when no logging config exists --- src/packages/cli/src/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/cli/src/cli.ts b/src/packages/cli/src/cli.ts index 6069ce9a77..22e42b6c04 100644 --- a/src/packages/cli/src/cli.ts +++ b/src/packages/cli/src/cli.ts @@ -57,7 +57,7 @@ if (argv.action === "start") { const loggingOptions = (argv as any).logging; let closeLogHandle = null; - if (loggingOptions.file) { + if (loggingOptions?.file) { const { log, close } = createLogger({ baseLog: console.log, file: loggingOptions.file From 37e1616419fbb9eb1584bdf5a095a9243a2de48a Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 28 Feb 2023 12:31:08 +1300 Subject: [PATCH 19/36] Switch to use 'fs.openSync()' instead of 'fs.writeSync()' because windows won't if the content is empty :( --- src/chains/ethereum/options/src/logging-options.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index e80ba0b96f..df0f7009ca 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -1,6 +1,6 @@ import { normalize } from "./helpers"; import { Definitions } from "@ganache/options"; -import { appendFileSync } from "fs"; +import { openSync, closeSync } from "fs"; export type LogFunc = (message?: any, ...optionalParams: any[]) => void; @@ -128,7 +128,8 @@ export const LoggingOptions: Definitions = { normalize: rawInput => { // this will throw if the file is not writable, and creates the log file for later appending try { - appendFileSync(rawInput, Buffer.alloc(0)); + const descriptor = openSync(rawInput, "w"); + closeSync(descriptor); } catch (err) { throw new Error( `Failed to write logs to ${rawInput}. Please check if the file path is valid and if the process has write permissions to the directory.` From 44a7107816804612ad32b695465de1ca404d2d32 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 3 Mar 2023 18:23:36 +1300 Subject: [PATCH 20/36] Work in progress. Move logger into @ganache/utils, resolve filehandle within logging-options. --- .../ethereum/ethereum/src/blockchain.ts | 7 +- src/chains/ethereum/ethereum/src/provider.ts | 7 + src/chains/ethereum/options/src/index.ts | 10 +- .../ethereum/options/src/logging-options.ts | 60 +++--- .../options/tests/logging-options.test.ts | 120 +++++++++-- src/packages/cli/src/cli.ts | 17 +- src/packages/cli/src/logger.ts | 59 ------ src/packages/cli/tests/logger.test.ts | 189 ----------------- src/packages/ganache/npm-shrinkwrap.json | 2 +- src/packages/utils/index.ts | 1 + src/packages/utils/src/things/logger.ts | 73 +++++++ src/packages/utils/tests/logger.test.ts | 193 ++++++++++++++++++ 12 files changed, 409 insertions(+), 329 deletions(-) delete mode 100644 src/packages/cli/src/logger.ts delete mode 100644 src/packages/cli/tests/logger.test.ts create mode 100644 src/packages/utils/src/things/logger.ts create mode 100644 src/packages/utils/tests/logger.test.ts diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index 191fd2c0e3..c4b8dc326f 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -37,7 +37,8 @@ import { BUFFER_32_ZERO, BUFFER_256_ZERO, KNOWN_CHAINIDS, - keccak + keccak, + Logger } from "@ganache/utils"; import AccountManager from "./data-managers/account-manager"; import BlockManager from "./data-managers/block-manager"; @@ -105,10 +106,6 @@ type BlockchainTypedEvents = { stop: undefined; }; -interface Logger { - log(message?: any, ...optionalParams: any[]): void; -} - export type BlockchainOptions = { db?: string | object; db_path?: string; diff --git a/src/chains/ethereum/ethereum/src/provider.ts b/src/chains/ethereum/ethereum/src/provider.ts index 0d56db56f6..f3094f2a29 100644 --- a/src/chains/ethereum/ethereum/src/provider.ts +++ b/src/chains/ethereum/ethereum/src/provider.ts @@ -432,6 +432,13 @@ export class EthereumProvider this.#executor.stop(); await this.#blockchain.stop(); + // only call close on the logger if it's an instance of AsyncronousLogger + if ("getCompletionHandle" in this.#options.logging.logger) { + //todo: maybe need to stop the logger from accepting new logs. This should work as is, because it's only await _current_ logs to complete. + await this.#options.logging.logger.getCompletionHandle(); + await this.#options.logging.logger.close(); + } + this.#executor.end(); this.emit("disconnect"); }; diff --git a/src/chains/ethereum/options/src/index.ts b/src/chains/ethereum/options/src/index.ts index fb90a7cf8d..6237361edc 100644 --- a/src/chains/ethereum/options/src/index.ts +++ b/src/chains/ethereum/options/src/index.ts @@ -7,14 +7,12 @@ import { ForkConfig, ForkOptions } from "./fork-options"; import { Base, Defaults, - Definitions, ExternalConfig, InternalConfig, Legacy, LegacyOptions, OptionName, OptionRawType, - Options, OptionsConfig } from "@ganache/options"; import { UnionToIntersection } from "./helper-types"; @@ -45,11 +43,9 @@ export type EthereumLegacyProviderOptions = Partial< MakeLegacyOptions >; -export type EthereumProviderOptions = Partial< - { - [K in keyof EthereumConfig]: ExternalConfig; - } ->; +export type EthereumProviderOptions = Partial<{ + [K in keyof EthereumConfig]: ExternalConfig; +}>; export type EthereumInternalOptions = { [K in keyof EthereumConfig]: InternalConfig; diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index df0f7009ca..c9534eaff1 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -1,12 +1,7 @@ import { normalize } from "./helpers"; import { Definitions } from "@ganache/options"; -import { openSync, closeSync } from "fs"; - -export type LogFunc = (message?: any, ...optionalParams: any[]) => void; - -type Logger = { - log: LogFunc; -}; +import { openSync, PathLike } from "fs"; +import { Logger, createLogger } from "@ganache/utils"; export type LoggingConfig = { options: { @@ -80,10 +75,10 @@ export type LoggingConfig = { /** * If you set this option, Ganache will write logs to a file located at the - * specified path. + * specified path. You can provide a path, or numerical file descriptor. */ readonly file: { - type: string; + type: number | PathLike; }; }; }; @@ -103,19 +98,6 @@ export const LoggingOptions: Definitions = { cliAliases: ["q", "quiet"], cliType: "boolean" }, - logger: { - normalize, - cliDescription: - "An object, like `console`, that implements a `log` function.", - disableInCLI: true, - // disable the default logger if `quiet` is `true` - default: config => { - return { - log: config.quiet ? () => {} : console.log - }; - }, - legacyName: "logger" - }, verbose: { normalize, cliDescription: "Set to `true` to log detailed RPC requests.", @@ -125,21 +107,33 @@ export const LoggingOptions: Definitions = { cliType: "boolean" }, file: { - normalize: rawInput => { - // this will throw if the file is not writable, and creates the log file for later appending - try { - const descriptor = openSync(rawInput, "w"); - closeSync(descriptor); - } catch (err) { - throw new Error( - `Failed to write logs to ${rawInput}. Please check if the file path is valid and if the process has write permissions to the directory.` - ); + normalize: (raw: number | PathLike) => { + let descriptor: number; + if (typeof raw === "number") { + descriptor = raw as number; + } else { + try { + descriptor = openSync(raw as PathLike, "a"); + } catch (err) { + throw new Error( + `Failed to open log file ${raw}. Please check if the file path is valid and if the process has write permissions to the directory.` + ); + } } - - return rawInput; + return descriptor; }, + cliDescription: "If set, Ganache will write logs to a file located at the specified path.", cliType: "string" + }, + logger: { + normalize, + cliDescription: + "An object, like `console`, that implements a `log` function.", + disableInCLI: true, + // disable the default logger if `quiet` is `true` + default: raw => createLogger(raw), + legacyName: "logger" } }; diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index 2ab36a7a33..34b43a21a0 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -2,12 +2,17 @@ import assert from "assert"; import { EthereumOptionsConfig } from "../src"; import sinon from "sinon"; import { resolve } from "path"; +import { promises } from "fs"; +const unlink = promises.unlink; +import { closeSync, openSync } from "fs"; +import { URL } from "url"; describe("EthereumOptionsConfig", () => { describe("logging", () => { // resolve absolute path of current working directory, which is clearly an // invalid file path (because it's a directory). const invalidFilePath = resolve(""); + const validFilePath = resolve("./tests/test-file.log"); describe("options", () => { let spy: any; @@ -19,30 +24,107 @@ describe("EthereumOptionsConfig", () => { spy.restore(); }); - it("logs via console.log by default", () => { - const message = "message"; - const options = EthereumOptionsConfig.normalize({}); - options.logging.logger.log(message); - assert.strictEqual(spy.withArgs(message).callCount, 1); - }); + describe("logger", () => { + it("uses console.log by default", () => { + const message = "message"; + const options = EthereumOptionsConfig.normalize({}); + options.logging.logger.log(message); + assert.strictEqual(spy.withArgs(message).callCount, 1); + }); - it("disables the logger when the quiet flag is used", () => { - const message = "message"; - const options = EthereumOptionsConfig.normalize({ - logging: { quiet: true } + it("disables the logger when the quiet flag is used", () => { + const message = "message"; + const options = EthereumOptionsConfig.normalize({ + logging: { quiet: true } + }); + options.logging.logger.log(message); + assert.strictEqual(spy.withArgs(message).callCount, 0); + }); + + it("resolves a file path to descriptor", async () => { + const options = EthereumOptionsConfig.normalize({ + logging: { file: validFilePath } + }); + try { + assert(typeof options.logging.file === "number"); + assert.doesNotThrow(() => + closeSync(options.logging.file as number) + ); + } finally { + await unlink(validFilePath); + } + }); + + it("resolves a file path as Buffer to descriptor", async () => { + const options = EthereumOptionsConfig.normalize({ + logging: { file: Buffer.from(validFilePath, "utf8") } + }); + try { + assert(typeof options.logging.file === "number"); + assert.doesNotThrow(() => + closeSync(options.logging.file as number) + ); + } finally { + await unlink(validFilePath); + } }); - options.logging.logger.log(message); - assert.strictEqual(spy.withArgs(message).callCount, 0); - }); - it("fails if an invalid file path is provided", () => { - const message = `Failed to write logs to ${invalidFilePath}. Please check if the file path is valid and if the process has write permissions to the directory.`; + it("resolves a file URL as Buffer to descriptor", async () => { + const options = EthereumOptionsConfig.normalize({ + logging: { file: new URL(`file://${validFilePath}`) } + }); + try { + assert(typeof options.logging.file === "number"); + assert.doesNotThrow(() => + closeSync(options.logging.file as number) + ); + } finally { + await unlink(validFilePath); + } + }); + + it("uses an existing file handle if passed in", async () => { + const fd = openSync(validFilePath, "a"); - assert.throws(() => { - EthereumOptionsConfig.normalize({ - logging: { file: invalidFilePath } + const options = EthereumOptionsConfig.normalize({ + logging: { file: fd } }); - }, new Error(message)); + + try { + assert.strictEqual(options.logging.file, fd); + assert(typeof options.logging.file === "number"); + assert.doesNotThrow(() => + closeSync(options.logging.file as number) + ); + } finally { + await unlink(validFilePath); + } + }); + + it("fails if an invalid file path is provided", () => { + const message = `Failed to open log file ${invalidFilePath}. Please check if the file path is valid and if the process has write permissions to the directory.`; + + assert.throws(() => { + EthereumOptionsConfig.normalize({ + logging: { file: invalidFilePath } + }); + }, new Error(message)); + }); + + it("fails if both `logger` and `file` is provided", async () => { + try { + assert.throws(() => + EthereumOptionsConfig.normalize({ + logging: { + logger: { log: (message, ...params) => {} }, + file: validFilePath + } + }) + ); + } finally { + await unlink(validFilePath); + } + }); }); }); }); diff --git a/src/packages/cli/src/cli.ts b/src/packages/cli/src/cli.ts index 22e42b6c04..d887eee852 100644 --- a/src/packages/cli/src/cli.ts +++ b/src/packages/cli/src/cli.ts @@ -55,17 +55,6 @@ if (argv.action === "start") { const flavor = argv.flavor; const cliSettings = argv.server; - const loggingOptions = (argv as any).logging; - let closeLogHandle = null; - if (loggingOptions?.file) { - const { log, close } = createLogger({ - baseLog: console.log, - file: loggingOptions.file - }); - loggingOptions.logger = { log }; - closeLogHandle = close; - } - console.log(detailedVersion); let server: ReturnType; @@ -100,12 +89,8 @@ if (argv.action === "start") { return; case ServerStatus.open: console.log("Shutting down…"); + await server.close(); - if (closeLogHandle) { - await Promise.all([server.close(), closeLogHandle()]); - } else { - await server.close(); - } console.log("Server has been shut down"); break; } diff --git a/src/packages/cli/src/logger.ts b/src/packages/cli/src/logger.ts deleted file mode 100644 index eb0ae176a2..0000000000 --- a/src/packages/cli/src/logger.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { open, FileHandle } from "fs/promises"; -import { format } from "util"; - -export type LogFunc = (message?: any, ...optionalParams: any[]) => void; - -/** - * Create a logger function based on the provided config. - * - * @param config specifying the configuration for the logger - * @returns an object containing a `log` function and optional `getCompletionPromise` - * function returning a `Promise` that resolves when any asyncronous - * activies are completed. - */ -export function createLogger(config: { baseLog: LogFunc; file: string }): { - log: LogFunc; - getCompletionPromise: () => Promise; - close: () => Promise; -}; -export function createLogger(config: { baseLog: LogFunc }): { log: LogFunc }; -export function createLogger(config: { baseLog: LogFunc; file?: string }): { - log: LogFunc; - getCompletionPromise?: () => Promise; - close?: () => Promise; -} { - if ("file" in config) { - const diskLogFormatter = (message: any) => { - const linePrefix = `${new Date().toISOString()} `; - return message.toString().replace(/^/gm, linePrefix); - }; - - // we pass this handle back out so that it can be closed by the caller - const whenHandle = open(config.file, "a"); - - let writing = Promise.resolve(null); - - const log = (message: any, ...optionalParams: any[]) => { - // we are logging to a file, but we still need to call the base logger - config.baseLog(message, ...optionalParams); - - const formattedMessage = format(message, ...optionalParams); - const currentWriting = writing; - writing = whenHandle.then(async handle => { - await currentWriting; - - return handle.appendFile(diskLogFormatter(formattedMessage) + "\n"); - }); - }; - - return { - log, - getCompletionPromise: () => writing, - close: async () => (await whenHandle).close() - }; - } else { - return { - log: config.baseLog - }; - } -} diff --git a/src/packages/cli/tests/logger.test.ts b/src/packages/cli/tests/logger.test.ts deleted file mode 100644 index 09c36293ec..0000000000 --- a/src/packages/cli/tests/logger.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import assert from "assert"; -import { resolve } from "path"; -import { createLogger } from "../src/logger"; -import { readFile, unlink } from "fs/promises"; - -describe("createLogger()", () => { - const getFilePath = (slug: string) => `./tests/test-${slug}.log`; - - const createBaseLogger = () => { - const calls: any[][] = []; - return { - baseLog: (message, ...params) => { - calls.push([message, ...params]); - }, - calls - }; - }; - const invalidFilePath = resolve(""); - - const message = "test message"; - - it("should create a baseLog() logger by default", () => { - const { baseLog, calls } = createBaseLogger(); - const { log } = createLogger({ baseLog }); - - log(message); - - assert.strictEqual( - calls.length, - 1, - "baseLog() was called unexpected number of times." - ); - - const baseLogArgs = calls[0]; - - assert.deepStrictEqual( - baseLogArgs, - [message], - "baseLog() called with unexpected arguments." - ); - }); - - it("should still call baseLog() when a file is specified", async () => { - const file = getFilePath("write-to-console"); - const { baseLog, calls } = createBaseLogger(); - - const { log, getCompletionPromise } = createLogger({ file, baseLog }); - - try { - log(message); - await getCompletionPromise(); - } finally { - await unlink(file); - } - - assert.strictEqual( - calls.length, - 1, - "baseLog() was called unexpected number of times." - ); - - const args = calls[0]; - - assert.deepStrictEqual( - args, - [message], - "baseLog() called with unexpected arguments." - ); - }); - - it("should write to the file provided", async () => { - const file = getFilePath("write-to-file-provided"); - const { baseLog } = createBaseLogger(); - - const { log, getCompletionPromise, close } = createLogger({ - file, - baseLog - }); - - let fileContents: string; - try { - log(`${message} 0`); - log(`${message} 1`); - log(`${message} 2`); - await getCompletionPromise(); - - fileContents = await readFile(file, "utf8"); - } finally { - await close(); - await unlink(file); - } - - const logLines = fileContents.split("\n"); - - // 4, because there's a \n at the end of each line - assert.strictEqual(logLines.length, 4); - assert.strictEqual(logLines[3], ""); - - logLines.slice(0, 3).forEach((logLine, lineNumber) => { - const timestampPart = logLine.slice(0, 24); - const messagePart = logLine.slice(25); - const delimiter = logLine[24]; - - assert( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(timestampPart), - "Unexpected timestamp." - ); - assert.strictEqual(delimiter, " ", "Unexpected delimiter."); - assert.strictEqual( - messagePart, - `${message} ${lineNumber}`, - "Unexpected message." - ); - }); - }); - - it("should timestamp each line on multi-line log messages", async () => { - const file = getFilePath("timestamp-each-line"); - const { baseLog } = createBaseLogger(); - - const { log, getCompletionPromise, close } = createLogger({ - file, - baseLog - }); - - const expectedLines = ["multi", "line", "message"]; - - let loggedLines: string[]; - try { - log(expectedLines.join("\n")); - await getCompletionPromise(); - - const fileContents = await readFile(file, "utf8"); - loggedLines = fileContents.split("\n"); - } finally { - await close(); - await unlink(file); - } - - // length == 4, because there's a \n at the end (string.split() results - // in an empty string) - assert.strictEqual(loggedLines.length, 4); - assert.strictEqual(loggedLines[3], ""); - - loggedLines.slice(0, 3).forEach((logLine, lineNumber) => { - const timestampPart = logLine.slice(0, 24); - const messagePart = logLine.slice(25); - const delimiter = logLine[24]; - - assert( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(timestampPart), - "Unexpected timestamp" - ); - assert.strictEqual(delimiter, " ", "Unexpected delimiter"); - assert.strictEqual(messagePart, expectedLines[lineNumber]); - }); - }); - - it("should not throw if the underlying file does not exist", async () => { - const file = getFilePath("underlying-file-does-not-exist"); - const { baseLog } = createBaseLogger(); - - const { log, getCompletionPromise, close } = createLogger({ - file, - baseLog - }); - - try { - log(message); - await getCompletionPromise(); - } finally { - await close(); - await unlink(file); - } - }); - - it("should reject waitHandle if the underlying file is inaccessible", async () => { - const { baseLog } = createBaseLogger(); - - const { log, getCompletionPromise } = createLogger({ - file: invalidFilePath, - baseLog - }); - - log(message); - - await assert.rejects(getCompletionPromise()); - }); -}); diff --git a/src/packages/ganache/npm-shrinkwrap.json b/src/packages/ganache/npm-shrinkwrap.json index d1c3d8ea90..16a9092af3 100644 --- a/src/packages/ganache/npm-shrinkwrap.json +++ b/src/packages/ganache/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "ganache", - "version": "7.5.0", + "version": "7.7.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/packages/utils/index.ts b/src/packages/utils/index.ts index 9763262908..4e3eebe2dd 100644 --- a/src/packages/utils/index.ts +++ b/src/packages/utils/index.ts @@ -14,3 +14,4 @@ export { JsonRpcErrorCode } from "./src/things/jsonrpc"; export { default as PromiEvent } from "./src/things/promievent"; +export * from "./src/things/logger"; diff --git a/src/packages/utils/src/things/logger.ts b/src/packages/utils/src/things/logger.ts new file mode 100644 index 0000000000..f56f0ac821 --- /dev/null +++ b/src/packages/utils/src/things/logger.ts @@ -0,0 +1,73 @@ +import { appendFile, close, PathLike } from "fs"; +import { promisify, format } from "util"; +const appendFilePromise = promisify(appendFile); +const closePromise = promisify(close); +export type LogFunc = (message?: any, ...optionalParams: any[]) => void; + +type SyncronousLogger = { + log: LogFunc; +}; +type AsyncronousLogger = SyncronousLogger & { + close: () => Promise; + getCompletionHandle: () => Promise; +}; + +export type Logger = SyncronousLogger | AsyncronousLogger; + +export function createLogger(config: { + file: number; + quiet?: boolean; + verbose?: boolean; + baseLog?: LogFunc; +}): AsyncronousLogger; +export function createLogger(config: { + quiet?: boolean; + verbose?: boolean; + baseLog?: LogFunc; +}): SyncronousLogger; +export function createLogger(config: { + quiet?: boolean; + file?: number; + verbose?: boolean; + baseLog?: LogFunc; +}): Logger { + const baseLog = config.quiet ? () => {} : config.baseLog || console.log; + if (config.file === undefined) { + return { + log: baseLog + }; + } else { + if (typeof config.file !== "number") { + throw new Error( + `We didn't normalize the config.file to a descriptor correctly. Got ${config.file}.` + ); + } + const descriptor = config.file as number; + + const diskLogFormatter = (message: any) => { + const linePrefix = `${new Date().toISOString()} `; + return message.toString().replace(/^/gm, linePrefix); + }; + + let writing = Promise.resolve(null); + + const log = (message: any, ...optionalParams: any[]) => { + // we are logging to a file, but we still need to writing to console + baseLog(message, ...optionalParams); + + const formattedMessage: string = format(message, ...optionalParams); + + writing = writing.then(() => { + return appendFilePromise( + descriptor, + diskLogFormatter(formattedMessage) + "\n" + ); + }); + }; + return { + log, + close: () => closePromise(descriptor), + getCompletionHandle: () => writing + }; + } +} diff --git a/src/packages/utils/tests/logger.test.ts b/src/packages/utils/tests/logger.test.ts new file mode 100644 index 0000000000..64b0447602 --- /dev/null +++ b/src/packages/utils/tests/logger.test.ts @@ -0,0 +1,193 @@ +import assert from "assert"; +import { createLogger } from "../src/things/logger"; +import { openSync, promises } from "fs"; +const { readFile, unlink } = promises; + +describe("createLogger()", () => { + const getFileDescriptor = (slug: string) => { + const path = `./tests/test-${slug}.log`; + return { + path, + descriptor: openSync(path, "a") + }; + }; + + const createBaseLogger = () => { + const calls: any[][] = []; + return { + baseLog: (message, ...params) => { + calls.push([message, ...params]); + }, + calls + }; + }; + + const message = "test message"; + + describe("createLogger()", () => { + it("should create a baseLog() logger by default", () => { + const { baseLog, calls } = createBaseLogger(); + const { log } = createLogger({ baseLog }); + + log(message); + + assert.strictEqual( + calls.length, + 1, + "baseLog() was called unexpected number of times." + ); + + const baseLogArgs = calls[0]; + + assert.deepStrictEqual( + baseLogArgs, + [message], + "baseLog() called with unexpected arguments." + ); + }); + + it("should still call baseLog() when a file is specified", async () => { + const { descriptor, path } = getFileDescriptor("write-to-console"); + const { baseLog, calls } = createBaseLogger(); + + const { log, getCompletionHandle } = createLogger({ + file: descriptor, + baseLog + }); + + try { + log(message); + await getCompletionHandle(); + } finally { + await unlink(path); + } + + assert.strictEqual( + calls.length, + 1, + "baseLog() was called unexpected number of times." + ); + + const args = calls[0]; + + assert.deepStrictEqual( + args, + [message], + "baseLog() called with unexpected arguments." + ); + }); + + it("should write to the file provided", async () => { + const { descriptor, path } = getFileDescriptor("write-to-file-provided"); + const { baseLog } = createBaseLogger(); + + const { log, getCompletionHandle, close } = createLogger({ + file: descriptor, + baseLog + }); + + let fileContents: string; + try { + log(`${message} 0`); + log(`${message} 1`); + log(`${message} 2`); + await getCompletionHandle(); + + fileContents = await readFile(path, "utf8"); + } finally { + await close(); + await unlink(path); + } + + const logLines = fileContents.split("\n"); + + // 4, because there's a \n at the end of each line + assert.strictEqual(logLines.length, 4); + assert.strictEqual(logLines[3], ""); + + logLines.slice(0, 3).forEach((logLine, lineNumber) => { + const timestampPart = logLine.slice(0, 24); + const messagePart = logLine.slice(25); + const delimiter = logLine[24]; + + assert( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(timestampPart), + "Unexpected timestamp." + ); + assert.strictEqual(delimiter, " ", "Unexpected delimiter."); + assert.strictEqual( + messagePart, + `${message} ${lineNumber}`, + "Unexpected message." + ); + }); + }); + + it("should timestamp each line on multi-line log messages", async () => { + const { descriptor, path } = getFileDescriptor("timestamp-each-line"); + const { baseLog } = createBaseLogger(); + + const { log, getCompletionHandle, close } = createLogger({ + file: descriptor, + baseLog + }); + + const expectedLines = ["multi", "line", "message"]; + + let loggedLines: string[]; + try { + log(expectedLines.join("\n")); + await getCompletionHandle(); + + const fileContents = await readFile(path, "utf8"); + loggedLines = fileContents.split("\n"); + } finally { + await close(); + await unlink(path); + } + + // length == 4, because there's a \n at the end (string.split() results + // in an empty string) + assert.strictEqual(loggedLines.length, 4); + assert.strictEqual(loggedLines[3], ""); + + loggedLines.slice(0, 3).forEach((logLine, lineNumber) => { + const timestampPart = logLine.slice(0, 24); + const messagePart = logLine.slice(25); + const delimiter = logLine[24]; + + assert( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(timestampPart), + "Unexpected timestamp" + ); + assert.strictEqual(delimiter, " ", "Unexpected delimiter"); + assert.strictEqual(messagePart, expectedLines[lineNumber]); + }); + }); + + it("should not throw if the underlying file does not exist", async () => { + const { descriptor, path } = getFileDescriptor( + "underlying-file-does-not-exist" + ); + const { baseLog } = createBaseLogger(); + + const { log, getCompletionHandle, close } = createLogger({ + file: descriptor, + baseLog + }); + + try { + log(message); + await getCompletionHandle(); + } finally { + await close(); + await unlink(path); + } + }); + }); + describe("close()", () => { + it("needs tests!", () => { + throw new Error("needs tests!"); + }); + }); +}); From b02821d21a7f6ad3673a2e7aeae21d8164f6a8ca Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Mon, 6 Mar 2023 11:28:26 +1300 Subject: [PATCH 21/36] Improve tests, implementation, support for filecoin --- src/chains/ethereum/ethereum/src/provider.ts | 7 +- .../ethereum/options/src/logging-options.ts | 13 +- .../options/tests/logging-options.test.ts | 40 +++--- src/chains/filecoin/options/package.json | 3 +- .../filecoin/options/src/logging-options.ts | 41 +++++- src/chains/filecoin/options/tsconfig.json | 4 + src/packages/cli/src/cli.ts | 1 - src/packages/utils/src/things/logger.ts | 13 +- src/packages/utils/tests/logger.test.ts | 130 +++++++++++++----- 9 files changed, 173 insertions(+), 79 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/provider.ts b/src/chains/ethereum/ethereum/src/provider.ts index f3094f2a29..067bfe8707 100644 --- a/src/chains/ethereum/ethereum/src/provider.ts +++ b/src/chains/ethereum/ethereum/src/provider.ts @@ -35,7 +35,7 @@ import { MessageEvent, VmConsoleLogEvent } from "./provider-events"; - +import { closeSync } from "fs"; declare type RequestMethods = KnownKeys; function parseCoinbase( @@ -434,9 +434,10 @@ export class EthereumProvider // only call close on the logger if it's an instance of AsyncronousLogger if ("getCompletionHandle" in this.#options.logging.logger) { - //todo: maybe need to stop the logger from accepting new logs. This should work as is, because it's only await _current_ logs to complete. + // todo: maybe need to stop the logger from accepting new logs. This should work as is, because we wait for + // any logs created before we call getCompletionHandle(). await this.#options.logging.logger.getCompletionHandle(); - await this.#options.logging.logger.close(); + closeSync(+this.#options.logging.file); } this.#executor.end(); diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index c9534eaff1..f65d1a9683 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -81,6 +81,7 @@ export type LoggingConfig = { type: number | PathLike; }; }; + exclusiveGroups: [["logger", "file"]]; }; export const LoggingOptions: Definitions = { @@ -107,7 +108,9 @@ export const LoggingOptions: Definitions = { cliType: "boolean" }, file: { - normalize: (raw: number | PathLike) => { + // always normalizes to a file descriptor + // todo: it would be nice if the accessor for file was a number type + normalize: (raw: number | PathLike): number => { let descriptor: number; if (typeof raw === "number") { descriptor = raw as number; @@ -125,7 +128,8 @@ export const LoggingOptions: Definitions = { cliDescription: "If set, Ganache will write logs to a file located at the specified path.", - cliType: "string" + cliType: "string", + conflicts: ["logger"] }, logger: { normalize, @@ -133,7 +137,8 @@ export const LoggingOptions: Definitions = { "An object, like `console`, that implements a `log` function.", disableInCLI: true, // disable the default logger if `quiet` is `true` - default: raw => createLogger(raw), - legacyName: "logger" + default: raw => createLogger({ ...raw, baseLog: console.log }), + legacyName: "logger", + conflicts: ["file"] } }; diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index 34b43a21a0..7fc645820a 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -40,7 +40,9 @@ describe("EthereumOptionsConfig", () => { options.logging.logger.log(message); assert.strictEqual(spy.withArgs(message).callCount, 0); }); + }); + describe("file", () => { it("resolves a file path to descriptor", async () => { const options = EthereumOptionsConfig.normalize({ logging: { file: validFilePath } @@ -83,7 +85,7 @@ describe("EthereumOptionsConfig", () => { } }); - it("uses an existing file handle if passed in", async () => { + it("uses an existing descriptor if passed in", async () => { const fd = openSync(validFilePath, "a"); const options = EthereumOptionsConfig.normalize({ @@ -104,26 +106,28 @@ describe("EthereumOptionsConfig", () => { it("fails if an invalid file path is provided", () => { const message = `Failed to open log file ${invalidFilePath}. Please check if the file path is valid and if the process has write permissions to the directory.`; - assert.throws(() => { - EthereumOptionsConfig.normalize({ - logging: { file: invalidFilePath } - }); - }, new Error(message)); + assert.throws( + () => { + EthereumOptionsConfig.normalize({ + logging: { file: invalidFilePath } + }); + }, + { message } + ); }); it("fails if both `logger` and `file` is provided", async () => { - try { - assert.throws(() => - EthereumOptionsConfig.normalize({ - logging: { - logger: { log: (message, ...params) => {} }, - file: validFilePath - } - }) - ); - } finally { - await unlink(validFilePath); - } + // this needs to be of type any, because it's an invalid config (both logger and file specified) + const config = { + logging: { + logger: { log: (message, ...params) => {} }, + file: 1 + } + } as any; + + assert.throws(() => EthereumOptionsConfig.normalize(config), { + message: `Values for both "logging.logger" and "logging.file" cannot be specified; they are mutually exclusive.` + }); }); }); }); diff --git a/src/chains/filecoin/options/package.json b/src/chains/filecoin/options/package.json index 7dbe0abf66..74fe17f53b 100644 --- a/src/chains/filecoin/options/package.json +++ b/src/chains/filecoin/options/package.json @@ -66,6 +66,7 @@ "webpack-cli": "4.9.1" }, "dependencies": { - "keccak": "3.0.2" + "keccak": "3.0.2", + "@ganache/utils": "0.7.0" } } diff --git a/src/chains/filecoin/options/src/logging-options.ts b/src/chains/filecoin/options/src/logging-options.ts index e85ac3445c..436af5c6e0 100644 --- a/src/chains/filecoin/options/src/logging-options.ts +++ b/src/chains/filecoin/options/src/logging-options.ts @@ -1,5 +1,7 @@ import { normalize } from "./helpers"; import { Definitions } from "@ganache/options"; +import { openSync, PathLike } from "fs"; +import { Logger, createLogger } from "@ganache/utils"; export type LoggingConfig = { options: { @@ -18,22 +20,49 @@ export type LoggingConfig = { * ``` */ readonly logger: { - type: { - log(message?: any, ...optionalParams: any[]): void; - }; + type: Logger; hasDefault: true; }; + + /** + * If you set this option, Ganache will write logs to a file located at the + * specified path. You can provide a path, or numerical file descriptor. + */ + readonly file: { + type: number | PathLike; + }; }; }; -const logger = { log: console.log }; - export const LoggingOptions: Definitions = { + file: { + normalize: (raw: number | PathLike) => { + let descriptor: number; + if (typeof raw === "number") { + descriptor = raw as number; + } else { + try { + descriptor = openSync(raw as PathLike, "a"); + } catch (err) { + throw new Error( + `Failed to open log file ${raw}. Please check if the file path is valid and if the process has write permissions to the directory.` + ); + } + } + return descriptor; + }, + + cliDescription: + "If set, Ganache will write logs to a file located at the specified path.", + cliType: "string" + }, logger: { normalize, cliDescription: "An object, like `console`, that implements a `log` function.", disableInCLI: true, - default: () => logger + //todo: why is this type conversion required here but not in Ethereum options? + default: raw => + createLogger({ ...raw, file: +raw.file, baseLog: console.log }) } }; diff --git a/src/chains/filecoin/options/tsconfig.json b/src/chains/filecoin/options/tsconfig.json index 5dfe28e945..4c788f00e3 100644 --- a/src/chains/filecoin/options/tsconfig.json +++ b/src/chains/filecoin/options/tsconfig.json @@ -15,6 +15,10 @@ { "name": "@ganache/options", "path": "../../../packages/options" + }, + { + "name": "@ganache/utils", + "path": "../../../packages/utils" } ] } \ No newline at end of file diff --git a/src/packages/cli/src/cli.ts b/src/packages/cli/src/cli.ts index d887eee852..710d5004b5 100644 --- a/src/packages/cli/src/cli.ts +++ b/src/packages/cli/src/cli.ts @@ -6,7 +6,6 @@ import args from "./args"; import { EthereumFlavorName, FilecoinFlavorName } from "@ganache/flavors"; import initializeEthereum from "./initialize/ethereum"; import initializeFilecoin from "./initialize/filecoin"; -import { createLogger } from "./logger"; import type { FilecoinProvider } from "@ganache/filecoin"; import type { EthereumProvider } from "@ganache/ethereum"; import { diff --git a/src/packages/utils/src/things/logger.ts b/src/packages/utils/src/things/logger.ts index f56f0ac821..a26f7765a9 100644 --- a/src/packages/utils/src/things/logger.ts +++ b/src/packages/utils/src/things/logger.ts @@ -1,14 +1,12 @@ -import { appendFile, close, PathLike } from "fs"; +import { appendFile } from "fs"; import { promisify, format } from "util"; const appendFilePromise = promisify(appendFile); -const closePromise = promisify(close); export type LogFunc = (message?: any, ...optionalParams: any[]) => void; type SyncronousLogger = { log: LogFunc; }; type AsyncronousLogger = SyncronousLogger & { - close: () => Promise; getCompletionHandle: () => Promise; }; @@ -18,20 +16,20 @@ export function createLogger(config: { file: number; quiet?: boolean; verbose?: boolean; - baseLog?: LogFunc; + baseLog: LogFunc; }): AsyncronousLogger; export function createLogger(config: { quiet?: boolean; verbose?: boolean; - baseLog?: LogFunc; + baseLog: LogFunc; }): SyncronousLogger; export function createLogger(config: { quiet?: boolean; file?: number; verbose?: boolean; - baseLog?: LogFunc; + baseLog: LogFunc; }): Logger { - const baseLog = config.quiet ? () => {} : config.baseLog || console.log; + const baseLog = config.quiet ? () => {} : config.baseLog; if (config.file === undefined) { return { log: baseLog @@ -66,7 +64,6 @@ export function createLogger(config: { }; return { log, - close: () => closePromise(descriptor), getCompletionHandle: () => writing }; } diff --git a/src/packages/utils/tests/logger.test.ts b/src/packages/utils/tests/logger.test.ts index 64b0447602..8c4e746ebc 100644 --- a/src/packages/utils/tests/logger.test.ts +++ b/src/packages/utils/tests/logger.test.ts @@ -1,16 +1,18 @@ import assert from "assert"; import { createLogger } from "../src/things/logger"; -import { openSync, promises } from "fs"; +import { openSync, promises, closeSync } from "fs"; const { readFile, unlink } = promises; -describe("createLogger()", () => { - const getFileDescriptor = (slug: string) => { - const path = `./tests/test-${slug}.log`; - return { - path, - descriptor: openSync(path, "a") - }; +const getFileDescriptor = (slug: string) => { + const path = `./tests/test-${slug}.log`; + return { + path, + descriptor: openSync(path, "a") }; +}; + +describe("createLogger()", () => { + const timestampRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/; const createBaseLogger = () => { const calls: any[][] = []; @@ -25,7 +27,7 @@ describe("createLogger()", () => { const message = "test message"; describe("createLogger()", () => { - it("should create a baseLog() logger by default", () => { + it("should create a baseLog() logger", () => { const { baseLog, calls } = createBaseLogger(); const { log } = createLogger({ baseLog }); @@ -59,6 +61,7 @@ describe("createLogger()", () => { log(message); await getCompletionHandle(); } finally { + closeSync(descriptor); await unlink(path); } @@ -81,7 +84,7 @@ describe("createLogger()", () => { const { descriptor, path } = getFileDescriptor("write-to-file-provided"); const { baseLog } = createBaseLogger(); - const { log, getCompletionHandle, close } = createLogger({ + const { log, getCompletionHandle } = createLogger({ file: descriptor, baseLog }); @@ -95,7 +98,7 @@ describe("createLogger()", () => { fileContents = await readFile(path, "utf8"); } finally { - await close(); + closeSync(descriptor); await unlink(path); } @@ -110,10 +113,74 @@ describe("createLogger()", () => { const messagePart = logLine.slice(25); const delimiter = logLine[24]; - assert( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(timestampPart), - "Unexpected timestamp." + assert(timestampRegex.test(timestampPart), "Unexpected timestamp."); + assert.strictEqual(delimiter, " ", "Unexpected delimiter."); + assert.strictEqual( + messagePart, + `${message} ${lineNumber}`, + "Unexpected message." ); + }); + }); + + it("should not call baseLog() when `quiet`", async () => { + const { baseLog, calls } = createBaseLogger(); + + const { log } = createLogger({ + baseLog, + quiet: true + }); + + log(message); + + assert.strictEqual( + calls.length, + 0, + "Expected baselogger to not have been called when `quiet` is specified" + ); + }); + + it("should write to the file, but not call baseLog() when `quiet`", async () => { + const { descriptor, path } = getFileDescriptor("quiet-logger"); + const { baseLog, calls } = createBaseLogger(); + + const { log, getCompletionHandle } = createLogger({ + file: descriptor, + baseLog, + quiet: true + }); + + let fileContents: string; + try { + log(`${message} 0`); + log(`${message} 1`); + log(`${message} 2`); + await getCompletionHandle(); + + fileContents = await readFile(path, "utf8"); + } finally { + closeSync(descriptor); + await unlink(path); + } + + assert.strictEqual( + calls.length, + 0, + "Expected baselogger to not have been called when `quiet` is specified" + ); + + const logLines = fileContents.split("\n"); + + // 4, because there's a \n at the end of each line + assert.strictEqual(logLines.length, 4); + assert.strictEqual(logLines[3], ""); + + logLines.slice(0, 3).forEach((logLine, lineNumber) => { + const timestampPart = logLine.slice(0, 24); + const messagePart = logLine.slice(25); + const delimiter = logLine[24]; + + assert(timestampRegex.test(timestampPart), "Unexpected timestamp."); assert.strictEqual(delimiter, " ", "Unexpected delimiter."); assert.strictEqual( messagePart, @@ -127,7 +194,7 @@ describe("createLogger()", () => { const { descriptor, path } = getFileDescriptor("timestamp-each-line"); const { baseLog } = createBaseLogger(); - const { log, getCompletionHandle, close } = createLogger({ + const { log, getCompletionHandle } = createLogger({ file: descriptor, baseLog }); @@ -142,12 +209,12 @@ describe("createLogger()", () => { const fileContents = await readFile(path, "utf8"); loggedLines = fileContents.split("\n"); } finally { - await close(); + closeSync(descriptor); await unlink(path); } // length == 4, because there's a \n at the end (string.split() results - // in an empty string) + // in each log line, follwed by an empty string) assert.strictEqual(loggedLines.length, 4); assert.strictEqual(loggedLines[3], ""); @@ -156,38 +223,25 @@ describe("createLogger()", () => { const messagePart = logLine.slice(25); const delimiter = logLine[24]; - assert( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(timestampPart), - "Unexpected timestamp" - ); + assert(timestampRegex.test(timestampPart), "Unexpected timestamp"); assert.strictEqual(delimiter, " ", "Unexpected delimiter"); assert.strictEqual(messagePart, expectedLines[lineNumber]); }); }); - it("should not throw if the underlying file does not exist", async () => { - const { descriptor, path } = getFileDescriptor( - "underlying-file-does-not-exist" - ); + it("should throw if the file descriptor is invalid", async () => { + // unlikely that this will be a valid file descriptor + const descriptor = 1234567890; const { baseLog } = createBaseLogger(); - const { log, getCompletionHandle, close } = createLogger({ + const { log, getCompletionHandle } = createLogger({ file: descriptor, baseLog }); - try { - log(message); - await getCompletionHandle(); - } finally { - await close(); - await unlink(path); - } - }); - }); - describe("close()", () => { - it("needs tests!", () => { - throw new Error("needs tests!"); + log("descriptor is invalid"); + + await assert.rejects(getCompletionHandle(), { code: "EBADF" }); }); }); }); From 4e7ddd640e47e60af311aade398778f8793d2ddb Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Mon, 6 Mar 2023 15:37:07 +1300 Subject: [PATCH 22/36] =?UTF-8?q?Finish=20tidying=20up=20implementation.?= =?UTF-8?q?=20Broke=20the=20console.log=20tests=20though=20=C2=AF\=5F(?= =?UTF-8?q?=E3=83=84)=5F/=C2=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ethereum/console.log/tests/index.test.ts | 2 +- .../ethereum/options/src/logging-options.ts | 16 +- .../options/tests/logging-options.test.ts | 47 ++++-- src/chains/filecoin/options/package-lock.json | 141 ++++++++++++++++++ src/chains/filecoin/options/package.json | 2 + .../filecoin/options/src/logging-options.ts | 14 +- .../options/tests/logging-options.test.ts | 136 +++++++++++++++++ src/packages/utils/src/things/logger.ts | 17 +-- src/packages/utils/tests/logger.test.ts | 69 +-------- 9 files changed, 339 insertions(+), 105 deletions(-) create mode 100644 src/chains/filecoin/options/tests/logging-options.test.ts diff --git a/src/chains/ethereum/console.log/tests/index.test.ts b/src/chains/ethereum/console.log/tests/index.test.ts index 06af7796a4..7a9ad75af8 100644 --- a/src/chains/ethereum/console.log/tests/index.test.ts +++ b/src/chains/ethereum/console.log/tests/index.test.ts @@ -21,7 +21,7 @@ import { CONTRACT_NAME } from "./helpers"; -describe("@ganache/console.log", () => { +describe.skip("@ganache/console.log", () => { const logger = { log: () => {} }; diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index f65d1a9683..eae55127d5 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -81,7 +81,6 @@ export type LoggingConfig = { type: number | PathLike; }; }; - exclusiveGroups: [["logger", "file"]]; }; export const LoggingOptions: Definitions = { @@ -128,17 +127,18 @@ export const LoggingOptions: Definitions = { cliDescription: "If set, Ganache will write logs to a file located at the specified path.", - cliType: "string", - conflicts: ["logger"] + cliType: "string" }, logger: { - normalize, + normalize: raw => + createLogger({ + ...raw, + baseLog: (message: any, ...params: any[]) => raw.log(message, params) + }), cliDescription: "An object, like `console`, that implements a `log` function.", disableInCLI: true, - // disable the default logger if `quiet` is `true` - default: raw => createLogger({ ...raw, baseLog: console.log }), - legacyName: "logger", - conflicts: ["file"] + default: config => (config.quiet ? { log: () => {} } : console), + legacyName: "logger" } }; diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index 7fc645820a..7b63f783af 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -40,6 +40,23 @@ describe("EthereumOptionsConfig", () => { options.logging.logger.log(message); assert.strictEqual(spy.withArgs(message).callCount, 0); }); + + it("calls the provided logger, even when quiet flag is used", () => { + let callCount = 0; + + const options = EthereumOptionsConfig.normalize({ + logging: { + quiet: true, + logger: { + log: (message: any, ...params: any[]) => callCount++ + } + } + }); + + options.logging.logger.log("message"); + + assert.strictEqual(callCount, 1); + }); }); describe("file", () => { @@ -116,18 +133,28 @@ describe("EthereumOptionsConfig", () => { ); }); - it("fails if both `logger` and `file` is provided", async () => { - // this needs to be of type any, because it's an invalid config (both logger and file specified) - const config = { - logging: { - logger: { log: (message, ...params) => {} }, - file: 1 + it("uses the provided logger when both `logger` and `file` are provided", async () => { + const calls: any[] = []; + const logger = { + log: (message: any, ...params: any[]) => { + calls.push([message, ...params]); } - } as any; + }; + const descriptor = openSync(validFilePath, "a"); - assert.throws(() => EthereumOptionsConfig.normalize(config), { - message: `Values for both "logging.logger" and "logging.file" cannot be specified; they are mutually exclusive.` - }); + try { + const options = EthereumOptionsConfig.normalize({ + logging: { + logger, + file: descriptor + } + }); + + options.logging.logger.log("message", "param1", "param2"); + assert.deepStrictEqual(calls, [["message", ["param1", "param2"]]]); + } finally { + await unlink(validFilePath); + } }); }); }); diff --git a/src/chains/filecoin/options/package-lock.json b/src/chains/filecoin/options/package-lock.json index c111c2873d..07dc0c5294 100644 --- a/src/chains/filecoin/options/package-lock.json +++ b/src/chains/filecoin/options/package-lock.json @@ -41,6 +41,41 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.3.tgz", + "integrity": "sha512-nhOb2dWPeb1sd3IQXL/dVPnKHDOAFfvichtBf4xV00/rU1QbPCQqKMbvIheIjqwVjh7qIgf2AHTHi391yMOMpQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -115,6 +150,21 @@ "integrity": "sha512-giB9gzDeiCeloIXDgzFBCgjj1k4WxcDrZtGl6h1IqmUPlxF+Nx8Ve+96QCyDZ/HseB/uvDsKbpib9hU5cU53pw==", "dev": true }, + "@types/sinon": { + "version": "10.0.13", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.13.tgz", + "integrity": "sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", + "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", + "dev": true + }, "@types/terser-webpack-plugin": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@types/terser-webpack-plugin/-/terser-webpack-plugin-5.0.2.tgz", @@ -969,6 +1019,12 @@ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1022,6 +1078,12 @@ "minimist": "^1.2.5" } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "keccak": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", @@ -1064,6 +1126,12 @@ "p-locate": "^5.0.0" } }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -1199,6 +1267,39 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", + "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0" + } + } + } + }, "node-addon-api": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", @@ -1306,6 +1407,15 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -1510,6 +1620,31 @@ "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", "dev": true }, + "sinon": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz", + "integrity": "sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^8.1.0", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", @@ -1700,6 +1835,12 @@ } } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "typescript": { "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", diff --git a/src/chains/filecoin/options/package.json b/src/chains/filecoin/options/package.json index 74fe17f53b..1ad8241f40 100644 --- a/src/chains/filecoin/options/package.json +++ b/src/chains/filecoin/options/package.json @@ -53,11 +53,13 @@ "@ganache/options": "0.7.1", "@types/mocha": "9.0.0", "@types/seedrandom": "3.0.1", + "@types/sinon": "^10.0.13", "@types/terser-webpack-plugin": "5.0.2", "cross-env": "7.0.3", "mocha": "9.1.3", "node-loader": "1.0.2", "seedrandom": "3.0.5", + "sinon": "12.0.1", "terser-webpack-plugin": "5.2.5", "ts-loader": "9.3.1", "ts-node": "10.9.1", diff --git a/src/chains/filecoin/options/src/logging-options.ts b/src/chains/filecoin/options/src/logging-options.ts index 436af5c6e0..883c6cb0c9 100644 --- a/src/chains/filecoin/options/src/logging-options.ts +++ b/src/chains/filecoin/options/src/logging-options.ts @@ -36,7 +36,9 @@ export type LoggingConfig = { export const LoggingOptions: Definitions = { file: { - normalize: (raw: number | PathLike) => { + // always normalizes to a file descriptor + // todo: it would be nice if the accessor for file was a number type + normalize: (raw: number | PathLike): number => { let descriptor: number; if (typeof raw === "number") { descriptor = raw as number; @@ -57,12 +59,14 @@ export const LoggingOptions: Definitions = { cliType: "string" }, logger: { - normalize, + normalize: raw => + createLogger({ + ...raw, + baseLog: (message: any, ...params: any[]) => raw.log(message, params) + }), cliDescription: "An object, like `console`, that implements a `log` function.", disableInCLI: true, - //todo: why is this type conversion required here but not in Ethereum options? - default: raw => - createLogger({ ...raw, file: +raw.file, baseLog: console.log }) + default: config => console } }; diff --git a/src/chains/filecoin/options/tests/logging-options.test.ts b/src/chains/filecoin/options/tests/logging-options.test.ts new file mode 100644 index 0000000000..dead6e4369 --- /dev/null +++ b/src/chains/filecoin/options/tests/logging-options.test.ts @@ -0,0 +1,136 @@ +import assert from "assert"; +import { FilecoinOptionsConfig } from "../src"; +import sinon from "sinon"; +import { resolve } from "path"; +import { promises } from "fs"; +const unlink = promises.unlink; +import { closeSync, openSync } from "fs"; +import { URL } from "url"; + +describe("FilecoinOptionsConfig", () => { + describe("logging", () => { + // resolve absolute path of current working directory, which is clearly an + // invalid file path (because it's a directory). + const invalidFilePath = resolve(""); + const validFilePath = resolve("./tests/test-file.log"); + + describe("options", () => { + let spy: any; + beforeEach(() => { + spy = sinon.spy(console, "log"); + }); + + afterEach(() => { + spy.restore(); + }); + + describe("logger", () => { + it("uses console.log by default", () => { + const message = "message"; + const options = FilecoinOptionsConfig.normalize({}); + options.logging.logger.log(message); + assert.strictEqual(spy.withArgs(message).callCount, 1); + }); + }); + + describe("file", () => { + it("resolves a file path to descriptor", async () => { + const options = FilecoinOptionsConfig.normalize({ + logging: { file: validFilePath } + }); + try { + assert(typeof options.logging.file === "number"); + assert.doesNotThrow(() => + closeSync(options.logging.file as number) + ); + } finally { + await unlink(validFilePath); + } + }); + + it("resolves a file path as Buffer to descriptor", async () => { + const options = FilecoinOptionsConfig.normalize({ + logging: { file: Buffer.from(validFilePath, "utf8") } + }); + try { + assert(typeof options.logging.file === "number"); + assert.doesNotThrow(() => + closeSync(options.logging.file as number) + ); + } finally { + await unlink(validFilePath); + } + }); + + it("resolves a file URL as Buffer to descriptor", async () => { + const options = FilecoinOptionsConfig.normalize({ + logging: { file: new URL(`file://${validFilePath}`) } + }); + try { + assert(typeof options.logging.file === "number"); + assert.doesNotThrow(() => + closeSync(options.logging.file as number) + ); + } finally { + await unlink(validFilePath); + } + }); + + it("uses an existing descriptor if passed in", async () => { + const fd = openSync(validFilePath, "a"); + + const options = FilecoinOptionsConfig.normalize({ + logging: { file: fd } + }); + + try { + assert.strictEqual(options.logging.file, fd); + assert(typeof options.logging.file === "number"); + assert.doesNotThrow(() => + closeSync(options.logging.file as number) + ); + } finally { + await unlink(validFilePath); + } + }); + + it("fails if an invalid file path is provided", () => { + const message = `Failed to open log file ${invalidFilePath}. Please check if the file path is valid and if the process has write permissions to the directory.`; + + assert.throws( + () => { + FilecoinOptionsConfig.normalize({ + logging: { file: invalidFilePath } + }); + }, + { message } + ); + }); + + it("uses the provided logger when both `logger` and `file` are provided", async () => { + const calls: any[] = []; + const logger = { + log: (message: any, ...params: any[]) => { + calls.push([message, ...params]); + } + }; + const descriptor = openSync(validFilePath, "a"); + + try { + const options = FilecoinOptionsConfig.normalize({ + logging: { + logger, + file: descriptor + } + }); + + options.logging.logger.log("message", "param1", "param2"); + assert.deepStrictEqual(calls, [["message", ["param1", "param2"]]]); + } finally { + await unlink(validFilePath); + } + }); + }); + }); + }); +}); diff --git a/src/packages/utils/src/things/logger.ts b/src/packages/utils/src/things/logger.ts index a26f7765a9..0035b395d8 100644 --- a/src/packages/utils/src/things/logger.ts +++ b/src/packages/utils/src/things/logger.ts @@ -14,30 +14,21 @@ export type Logger = SyncronousLogger | AsyncronousLogger; export function createLogger(config: { file: number; - quiet?: boolean; - verbose?: boolean; baseLog: LogFunc; }): AsyncronousLogger; +export function createLogger(config: { baseLog: LogFunc }): SyncronousLogger; export function createLogger(config: { - quiet?: boolean; - verbose?: boolean; - baseLog: LogFunc; -}): SyncronousLogger; -export function createLogger(config: { - quiet?: boolean; file?: number; - verbose?: boolean; baseLog: LogFunc; }): Logger { - const baseLog = config.quiet ? () => {} : config.baseLog; if (config.file === undefined) { return { - log: baseLog + log: config.baseLog }; } else { if (typeof config.file !== "number") { throw new Error( - `We didn't normalize the config.file to a descriptor correctly. Got ${config.file}.` + "`config.file` was not correctly noramlized to a file descriptor. This should not happen." ); } const descriptor = config.file as number; @@ -51,7 +42,7 @@ export function createLogger(config: { const log = (message: any, ...optionalParams: any[]) => { // we are logging to a file, but we still need to writing to console - baseLog(message, ...optionalParams); + config.baseLog(message, ...optionalParams); const formattedMessage: string = format(message, ...optionalParams); diff --git a/src/packages/utils/tests/logger.test.ts b/src/packages/utils/tests/logger.test.ts index 8c4e746ebc..5f025dfc45 100644 --- a/src/packages/utils/tests/logger.test.ts +++ b/src/packages/utils/tests/logger.test.ts @@ -26,7 +26,7 @@ describe("createLogger()", () => { const message = "test message"; - describe("createLogger()", () => { + describe("log()", () => { it("should create a baseLog() logger", () => { const { baseLog, calls } = createBaseLogger(); const { log } = createLogger({ baseLog }); @@ -123,73 +123,6 @@ describe("createLogger()", () => { }); }); - it("should not call baseLog() when `quiet`", async () => { - const { baseLog, calls } = createBaseLogger(); - - const { log } = createLogger({ - baseLog, - quiet: true - }); - - log(message); - - assert.strictEqual( - calls.length, - 0, - "Expected baselogger to not have been called when `quiet` is specified" - ); - }); - - it("should write to the file, but not call baseLog() when `quiet`", async () => { - const { descriptor, path } = getFileDescriptor("quiet-logger"); - const { baseLog, calls } = createBaseLogger(); - - const { log, getCompletionHandle } = createLogger({ - file: descriptor, - baseLog, - quiet: true - }); - - let fileContents: string; - try { - log(`${message} 0`); - log(`${message} 1`); - log(`${message} 2`); - await getCompletionHandle(); - - fileContents = await readFile(path, "utf8"); - } finally { - closeSync(descriptor); - await unlink(path); - } - - assert.strictEqual( - calls.length, - 0, - "Expected baselogger to not have been called when `quiet` is specified" - ); - - const logLines = fileContents.split("\n"); - - // 4, because there's a \n at the end of each line - assert.strictEqual(logLines.length, 4); - assert.strictEqual(logLines[3], ""); - - logLines.slice(0, 3).forEach((logLine, lineNumber) => { - const timestampPart = logLine.slice(0, 24); - const messagePart = logLine.slice(25); - const delimiter = logLine[24]; - - assert(timestampRegex.test(timestampPart), "Unexpected timestamp."); - assert.strictEqual(delimiter, " ", "Unexpected delimiter."); - assert.strictEqual( - messagePart, - `${message} ${lineNumber}`, - "Unexpected message." - ); - }); - }); - it("should timestamp each line on multi-line log messages", async () => { const { descriptor, path } = getFileDescriptor("timestamp-each-line"); const { baseLog } = createBaseLogger(); From f9aac474399d37b559a059c03fc9c9fbf6016662 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 8 Mar 2023 12:43:08 +1300 Subject: [PATCH 23/36] Support setting both file and logger, refactor tangentially related test --- .../ethereum/options/src/logging-options.ts | 25 ++++++----- .../ethereum/options/src/miner-options.ts | 10 ++--- .../options/tests/logging-options.test.ts | 34 ++++++++++---- .../filecoin/options/src/logging-options.ts | 14 +++--- .../options/tests/logging-options.test.ts | 2 +- src/packages/core/tests/connector.test.ts | 40 +++++++++-------- src/packages/options/src/create.ts | 14 +++--- src/packages/options/src/definition.ts | 10 +++-- src/packages/utils/src/things/logger.ts | 12 +++-- src/packages/utils/tests/logger.test.ts | 44 ++++++++++--------- 10 files changed, 117 insertions(+), 88 deletions(-) diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index eae55127d5..194ddf4bae 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -78,7 +78,8 @@ export type LoggingConfig = { * specified path. You can provide a path, or numerical file descriptor. */ readonly file: { - type: number | PathLike; + type: number; + rawType: number | PathLike; }; }; }; @@ -107,8 +108,6 @@ export const LoggingOptions: Definitions = { cliType: "boolean" }, file: { - // always normalizes to a file descriptor - // todo: it would be nice if the accessor for file was a number type normalize: (raw: number | PathLike): number => { let descriptor: number; if (typeof raw === "number") { @@ -124,21 +123,27 @@ export const LoggingOptions: Definitions = { } return descriptor; }, - cliDescription: "If set, Ganache will write logs to a file located at the specified path.", cliType: "string" }, logger: { - normalize: raw => - createLogger({ - ...raw, - baseLog: (message: any, ...params: any[]) => raw.log(message, params) - }), + normalize: (logger: Logger, config: Readonly<{ file: number }>) => { + return createLogger({ + file: config.file, + baseLogger: logger + }); + }, cliDescription: "An object, like `console`, that implements a `log` function.", disableInCLI: true, - default: config => (config.quiet ? { log: () => {} } : console), + default: config => { + const baseLogger = config.quiet ? { log: () => {} } : console; + return createLogger({ + file: config.file, + baseLogger + }); + }, legacyName: "logger" } }; diff --git a/src/chains/ethereum/options/src/miner-options.ts b/src/chains/ethereum/options/src/miner-options.ts index f6246929f3..559f5bb779 100644 --- a/src/chains/ethereum/options/src/miner-options.ts +++ b/src/chains/ethereum/options/src/miner-options.ts @@ -220,7 +220,7 @@ const toNumberOrString = (str: string) => { return parseInt(str); } }; - +const normalizeQuantity = value => Quantity.from(value); export const MinerOptions: Definitions = { blockTime: { normalize: rawInput => { @@ -246,7 +246,7 @@ export const MinerOptions: Definitions = { cliType: "string" }, defaultGasPrice: { - normalize: Quantity.from, + normalize: normalizeQuantity, cliDescription: "Sets the default gas price in WEI for transactions if not otherwise specified.", default: () => Quantity.from(2_000_000_000), @@ -256,7 +256,7 @@ export const MinerOptions: Definitions = { cliCoerce: toBigIntOrString }, blockGasLimit: { - normalize: Quantity.from, + normalize: normalizeQuantity, cliDescription: "Sets the block gas limit in WEI.", default: () => Quantity.from(30_000_000), legacyName: "gasLimit", @@ -274,7 +274,7 @@ export const MinerOptions: Definitions = { cliCoerce: estimateOrToBigIntOrString }, difficulty: { - normalize: Quantity.from, + normalize: normalizeQuantity, cliDescription: "Sets the block difficulty. Value is always 0 after the merge hardfork.", default: () => Quantity.One, @@ -282,7 +282,7 @@ export const MinerOptions: Definitions = { cliCoerce: toBigIntOrString }, callGasLimit: { - normalize: Quantity.from, + normalize: normalizeQuantity, cliDescription: "Sets the transaction gas limit in WEI for `eth_call` and `eth_estimateGas` calls.", default: () => Quantity.from(50_000_000), diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index 7b63f783af..e3e1dc953a 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -3,7 +3,7 @@ import { EthereumOptionsConfig } from "../src"; import sinon from "sinon"; import { resolve } from "path"; import { promises } from "fs"; -const unlink = promises.unlink; +const { unlink, readFile } = promises; import { closeSync, openSync } from "fs"; import { URL } from "url"; @@ -41,21 +41,21 @@ describe("EthereumOptionsConfig", () => { assert.strictEqual(spy.withArgs(message).callCount, 0); }); - it("calls the provided logger, even when quiet flag is used", () => { - let callCount = 0; - + it("calls the provided logger when quiet flag is used", () => { + const logLines: string[][] = []; const options = EthereumOptionsConfig.normalize({ logging: { quiet: true, logger: { - log: (message: any, ...params: any[]) => callCount++ + log: (message: any, ...params: any[]) => + logLines.push([message, ...params]) } } }); - options.logging.logger.log("message"); + options.logging.logger.log("message", "param1", "param2"); - assert.strictEqual(callCount, 1); + assert.deepStrictEqual(logLines, [["message", "param1", "param2"]]); }); }); @@ -133,7 +133,7 @@ describe("EthereumOptionsConfig", () => { ); }); - it("uses the provided logger when both `logger` and `file` are provided", async () => { + it("uses the provided logger, and file when both `logger` and `file` are provided", async () => { const calls: any[] = []; const logger = { log: (message: any, ...params: any[]) => { @@ -151,7 +151,23 @@ describe("EthereumOptionsConfig", () => { }); options.logging.logger.log("message", "param1", "param2"); - assert.deepStrictEqual(calls, [["message", ["param1", "param2"]]]); + assert.deepStrictEqual(calls, [["message", "param1", "param2"]]); + + const fromFile = await readFile(validFilePath, "utf8"); + assert(fromFile !== "", "Nothing written to the log file"); + + const timestampPart = fromFile.substring(0, 24); + + const timestampRegex = + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/; + assert( + timestampPart.match(timestampRegex), + `Unexpected timestamp from file ${timestampPart}` + ); + + const messagePart = fromFile.substring(25); + + assert.strictEqual(messagePart, "message param1 param2\n"); } finally { await unlink(validFilePath); } diff --git a/src/chains/filecoin/options/src/logging-options.ts b/src/chains/filecoin/options/src/logging-options.ts index 883c6cb0c9..20624bc3ca 100644 --- a/src/chains/filecoin/options/src/logging-options.ts +++ b/src/chains/filecoin/options/src/logging-options.ts @@ -29,15 +29,14 @@ export type LoggingConfig = { * specified path. You can provide a path, or numerical file descriptor. */ readonly file: { - type: number | PathLike; + type: number; + rawType: number | PathLike; }; }; }; export const LoggingOptions: Definitions = { file: { - // always normalizes to a file descriptor - // todo: it would be nice if the accessor for file was a number type normalize: (raw: number | PathLike): number => { let descriptor: number; if (typeof raw === "number") { @@ -62,11 +61,16 @@ export const LoggingOptions: Definitions = { normalize: raw => createLogger({ ...raw, - baseLog: (message: any, ...params: any[]) => raw.log(message, params) + baseLogger: raw }), cliDescription: "An object, like `console`, that implements a `log` function.", disableInCLI: true, - default: config => console + default: config => { + return createLogger({ + ...config, + baseLogger: console + }); + } } }; diff --git a/src/chains/filecoin/options/tests/logging-options.test.ts b/src/chains/filecoin/options/tests/logging-options.test.ts index dead6e4369..17e15d703b 100644 --- a/src/chains/filecoin/options/tests/logging-options.test.ts +++ b/src/chains/filecoin/options/tests/logging-options.test.ts @@ -125,7 +125,7 @@ describe("FilecoinOptionsConfig", () => { }); options.logging.logger.log("message", "param1", "param2"); - assert.deepStrictEqual(calls, [["message", ["param1", "param2"]]]); + assert.deepStrictEqual(calls, [["message", "param1", "param2"]]); } finally { await unlink(validFilePath); } diff --git a/src/packages/core/tests/connector.test.ts b/src/packages/core/tests/connector.test.ts index 0969407b6a..d6ebf5eef3 100644 --- a/src/packages/core/tests/connector.test.ts +++ b/src/packages/core/tests/connector.test.ts @@ -9,29 +9,33 @@ describe("connector", () => { }); it("it logs when `options.verbose` is `true`", async () => { - const logger = { log: (_msg: string) => {} }; + const logLines: string[] = []; + const logger = { + log: (_msg: string) => { + logLines.push(_msg); + } + }; const p = Ganache.provider({ logging: { logger, verbose: true } }); - logger.log = msg => { - assert.strictEqual( - msg, - " > net_version: undefined", - "doesn't work when no params" - ); - }; await p.send("net_version"); + assert.deepStrictEqual( + logLines, + [" > net_version: undefined"], + "doesn't work when no params" + ); - return new Promise(async resolve => { - logger.log = msg => { - const expected = - " > web3_sha3: [\n" + ' > "Tim is a swell guy."\n' + " > ]"; - assert.strictEqual(msg, expected, "doesn't work with params"); - resolve(); - }; - await p.send("web3_sha3", ["Tim is a swell guy."]); - }); + // clear the logLines + logLines.splice(0, logLines.length); + + const message = `0x${Buffer.from("Tim is a swell guy.").toString("hex")}`; + await p.send("web3_sha3", [message]); + assert.deepStrictEqual( + logLines, + [` > web3_sha3: [\n > \"${message}\"\n > ]`], + "doesn't work with params" + ); }); it("it processes requests asynchronously when `asyncRequestProcessing` is default (true)", async () => { @@ -108,7 +112,7 @@ describe("connector", () => { const illegalMethodTypes = [ 123, // just cast as string to make TS let me test weird stuff... - (Buffer.from([1]) as unknown) as string, + Buffer.from([1]) as unknown as string, null, void 0, {}, diff --git a/src/packages/options/src/create.ts b/src/packages/options/src/create.ts index c52b46b1bc..a9e024730d 100644 --- a/src/packages/options/src/create.ts +++ b/src/packages/options/src/create.ts @@ -6,11 +6,9 @@ import { hasOwn } from "@ganache/utils"; export type NamespacedOptions = { [key: string]: Base.Config }; -export type ProviderOptions = Partial< - { - [K in keyof O]: ExternalConfig; - } ->; +export type ProviderOptions = Partial<{ + [K in keyof O]: ExternalConfig; +}>; export type InternalOptions = { [K in keyof O]: InternalConfig; @@ -53,7 +51,7 @@ function fill(defaults: any, options: any, target: any, namespace: any) { const propDefinition = def[key]; let value = namespaceOptions[key]; if (value !== undefined) { - const normalized = propDefinition.normalize(namespaceOptions[key]); + const normalized = propDefinition.normalize(value, namespaceOptions); if (normalized !== undefined) { checkForConflicts( key, @@ -68,7 +66,7 @@ function fill(defaults: any, options: any, target: any, namespace: any) { const legacyName = propDefinition.legacyName || key; value = options[legacyName]; if (value !== undefined) { - const normalized = propDefinition.normalize(value); + const normalized = propDefinition.normalize(value, options); if (normalized !== undefined) { checkForConflicts( key, @@ -92,7 +90,7 @@ function fill(defaults: any, options: any, target: any, namespace: any) { const legacyName = propDefinition.legacyName || key; const value = options[legacyName]; if (value !== undefined) { - const normalized = propDefinition.normalize(value); + const normalized = propDefinition.normalize(value, options); if (normalized !== undefined) { checkForConflicts( key, diff --git a/src/packages/options/src/definition.ts b/src/packages/options/src/definition.ts index 6f22099219..33cbad0a42 100644 --- a/src/packages/options/src/definition.ts +++ b/src/packages/options/src/definition.ts @@ -19,15 +19,17 @@ import { UnionToIntersection } from "./types"; type Normalize< C extends Base.Config, N extends OptionName = OptionName -> = (rawInput: OptionRawType) => OptionType; +> = ( + rawInput: OptionRawType, + config?: Readonly> +) => OptionType; export type ExternalConfig = Partial< ExclusiveGroupUnionAndUnconstrainedPlus >; -export type InternalConfig< - C extends Base.Config -> = ExclusiveGroupUnionAndUnconstrainedPlus; +export type InternalConfig = + ExclusiveGroupUnionAndUnconstrainedPlus; export type Definitions = { [N in OptionName]: { diff --git a/src/packages/utils/src/things/logger.ts b/src/packages/utils/src/things/logger.ts index 0035b395d8..dcaeac4e91 100644 --- a/src/packages/utils/src/things/logger.ts +++ b/src/packages/utils/src/things/logger.ts @@ -14,17 +14,15 @@ export type Logger = SyncronousLogger | AsyncronousLogger; export function createLogger(config: { file: number; - baseLog: LogFunc; + baseLogger: Logger; }): AsyncronousLogger; -export function createLogger(config: { baseLog: LogFunc }): SyncronousLogger; +export function createLogger(config: { baseLogger: Logger }): SyncronousLogger; export function createLogger(config: { file?: number; - baseLog: LogFunc; + baseLogger: Logger; }): Logger { if (config.file === undefined) { - return { - log: config.baseLog - }; + return config.baseLogger; } else { if (typeof config.file !== "number") { throw new Error( @@ -42,7 +40,7 @@ export function createLogger(config: { const log = (message: any, ...optionalParams: any[]) => { // we are logging to a file, but we still need to writing to console - config.baseLog(message, ...optionalParams); + config.baseLogger.log(message, ...optionalParams); const formattedMessage: string = format(message, ...optionalParams); diff --git a/src/packages/utils/tests/logger.test.ts b/src/packages/utils/tests/logger.test.ts index 5f025dfc45..4c33e08778 100644 --- a/src/packages/utils/tests/logger.test.ts +++ b/src/packages/utils/tests/logger.test.ts @@ -14,11 +14,13 @@ const getFileDescriptor = (slug: string) => { describe("createLogger()", () => { const timestampRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/; - const createBaseLogger = () => { + const createbaseLoggerger = () => { const calls: any[][] = []; return { - baseLog: (message, ...params) => { - calls.push([message, ...params]); + baseLogger: { + log: (message, ...params) => { + calls.push([message, ...params]); + } }, calls }; @@ -27,34 +29,34 @@ describe("createLogger()", () => { const message = "test message"; describe("log()", () => { - it("should create a baseLog() logger", () => { - const { baseLog, calls } = createBaseLogger(); - const { log } = createLogger({ baseLog }); + it("should create a baseLogger() logger", () => { + const { baseLogger, calls } = createbaseLoggerger(); + const { log } = createLogger({ baseLogger }); log(message); assert.strictEqual( calls.length, 1, - "baseLog() was called unexpected number of times." + "baseLogger() was called unexpected number of times." ); - const baseLogArgs = calls[0]; + const baseLoggerArgs = calls[0]; assert.deepStrictEqual( - baseLogArgs, + baseLoggerArgs, [message], - "baseLog() called with unexpected arguments." + "baseLogger() called with unexpected arguments." ); }); - it("should still call baseLog() when a file is specified", async () => { + it("should still call baseLogger() when a file is specified", async () => { const { descriptor, path } = getFileDescriptor("write-to-console"); - const { baseLog, calls } = createBaseLogger(); + const { baseLogger, calls } = createbaseLoggerger(); const { log, getCompletionHandle } = createLogger({ file: descriptor, - baseLog + baseLogger }); try { @@ -68,7 +70,7 @@ describe("createLogger()", () => { assert.strictEqual( calls.length, 1, - "baseLog() was called unexpected number of times." + "baseLogger() was called unexpected number of times." ); const args = calls[0]; @@ -76,17 +78,17 @@ describe("createLogger()", () => { assert.deepStrictEqual( args, [message], - "baseLog() called with unexpected arguments." + "baseLogger() called with unexpected arguments." ); }); it("should write to the file provided", async () => { const { descriptor, path } = getFileDescriptor("write-to-file-provided"); - const { baseLog } = createBaseLogger(); + const { baseLogger } = createbaseLoggerger(); const { log, getCompletionHandle } = createLogger({ file: descriptor, - baseLog + baseLogger }); let fileContents: string; @@ -125,11 +127,11 @@ describe("createLogger()", () => { it("should timestamp each line on multi-line log messages", async () => { const { descriptor, path } = getFileDescriptor("timestamp-each-line"); - const { baseLog } = createBaseLogger(); + const { baseLogger } = createbaseLoggerger(); const { log, getCompletionHandle } = createLogger({ file: descriptor, - baseLog + baseLogger }); const expectedLines = ["multi", "line", "message"]; @@ -165,11 +167,11 @@ describe("createLogger()", () => { it("should throw if the file descriptor is invalid", async () => { // unlikely that this will be a valid file descriptor const descriptor = 1234567890; - const { baseLog } = createBaseLogger(); + const { baseLogger } = createbaseLoggerger(); const { log, getCompletionHandle } = createLogger({ file: descriptor, - baseLog + baseLogger }); log("descriptor is invalid"); From 6cc6fb25e9c184edbca32179320a2adb90f7b1fc Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 8 Mar 2023 15:40:15 +1300 Subject: [PATCH 24/36] tidy up implementation, fix tests, don't accept filehandle as logging.file because then it's impossible for us to now whether we should close it --- .../ethereum/console.log/tests/index.test.ts | 2 +- src/chains/ethereum/ethereum/src/provider.ts | 6 ++-- .../ethereum/options/src/logging-options.ts | 23 ++++++------- .../options/tests/logging-options.test.ts | 33 +++---------------- .../filecoin/options/src/logging-options.ts | 9 +++-- .../options/tests/logging-options.test.ts | 33 +++---------------- src/packages/options/src/create.ts | 6 ++-- src/packages/utils/src/things/logger.ts | 6 ++-- 8 files changed, 32 insertions(+), 86 deletions(-) diff --git a/src/chains/ethereum/console.log/tests/index.test.ts b/src/chains/ethereum/console.log/tests/index.test.ts index 7a9ad75af8..06af7796a4 100644 --- a/src/chains/ethereum/console.log/tests/index.test.ts +++ b/src/chains/ethereum/console.log/tests/index.test.ts @@ -21,7 +21,7 @@ import { CONTRACT_NAME } from "./helpers"; -describe.skip("@ganache/console.log", () => { +describe("@ganache/console.log", () => { const logger = { log: () => {} }; diff --git a/src/chains/ethereum/ethereum/src/provider.ts b/src/chains/ethereum/ethereum/src/provider.ts index 067bfe8707..af2e79051b 100644 --- a/src/chains/ethereum/ethereum/src/provider.ts +++ b/src/chains/ethereum/ethereum/src/provider.ts @@ -432,12 +432,10 @@ export class EthereumProvider this.#executor.stop(); await this.#blockchain.stop(); - // only call close on the logger if it's an instance of AsyncronousLogger + // only need to do this if it's an `AsyncronousLogger` if ("getCompletionHandle" in this.#options.logging.logger) { - // todo: maybe need to stop the logger from accepting new logs. This should work as is, because we wait for - // any logs created before we call getCompletionHandle(). await this.#options.logging.logger.getCompletionHandle(); - closeSync(+this.#options.logging.file); + closeSync(this.#options.logging.file); } this.#executor.end(); diff --git a/src/chains/ethereum/options/src/logging-options.ts b/src/chains/ethereum/options/src/logging-options.ts index 194ddf4bae..3a092502a2 100644 --- a/src/chains/ethereum/options/src/logging-options.ts +++ b/src/chains/ethereum/options/src/logging-options.ts @@ -75,11 +75,11 @@ export type LoggingConfig = { /** * If you set this option, Ganache will write logs to a file located at the - * specified path. You can provide a path, or numerical file descriptor. + * specified path. */ readonly file: { type: number; - rawType: number | PathLike; + rawType: PathLike; }; }; }; @@ -108,18 +108,15 @@ export const LoggingOptions: Definitions = { cliType: "boolean" }, file: { - normalize: (raw: number | PathLike): number => { + normalize: (raw: PathLike): number => { let descriptor: number; - if (typeof raw === "number") { - descriptor = raw as number; - } else { - try { - descriptor = openSync(raw as PathLike, "a"); - } catch (err) { - throw new Error( - `Failed to open log file ${raw}. Please check if the file path is valid and if the process has write permissions to the directory.` - ); - } + + try { + descriptor = openSync(raw, "a"); + } catch (err) { + throw new Error( + `Failed to open log file ${raw}. Please check if the file path is valid and if the process has write permissions to the directory.` + ); } return descriptor; }, diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index e3e1dc953a..d6a1779dd8 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -66,9 +66,7 @@ describe("EthereumOptionsConfig", () => { }); try { assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => - closeSync(options.logging.file as number) - ); + assert.doesNotThrow(() => closeSync(options.logging.file)); } finally { await unlink(validFilePath); } @@ -80,9 +78,7 @@ describe("EthereumOptionsConfig", () => { }); try { assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => - closeSync(options.logging.file as number) - ); + assert.doesNotThrow(() => closeSync(options.logging.file)); } finally { await unlink(validFilePath); } @@ -94,27 +90,7 @@ describe("EthereumOptionsConfig", () => { }); try { assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => - closeSync(options.logging.file as number) - ); - } finally { - await unlink(validFilePath); - } - }); - - it("uses an existing descriptor if passed in", async () => { - const fd = openSync(validFilePath, "a"); - - const options = EthereumOptionsConfig.normalize({ - logging: { file: fd } - }); - - try { - assert.strictEqual(options.logging.file, fd); - assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => - closeSync(options.logging.file as number) - ); + assert.doesNotThrow(() => closeSync(options.logging.file)); } finally { await unlink(validFilePath); } @@ -140,13 +116,12 @@ describe("EthereumOptionsConfig", () => { calls.push([message, ...params]); } }; - const descriptor = openSync(validFilePath, "a"); try { const options = EthereumOptionsConfig.normalize({ logging: { logger, - file: descriptor + file: validFilePath } }); diff --git a/src/chains/filecoin/options/src/logging-options.ts b/src/chains/filecoin/options/src/logging-options.ts index 20624bc3ca..043f1f9315 100644 --- a/src/chains/filecoin/options/src/logging-options.ts +++ b/src/chains/filecoin/options/src/logging-options.ts @@ -1,4 +1,3 @@ -import { normalize } from "./helpers"; import { Definitions } from "@ganache/options"; import { openSync, PathLike } from "fs"; import { Logger, createLogger } from "@ganache/utils"; @@ -26,24 +25,24 @@ export type LoggingConfig = { /** * If you set this option, Ganache will write logs to a file located at the - * specified path. You can provide a path, or numerical file descriptor. + * specified path. */ readonly file: { type: number; - rawType: number | PathLike; + rawType: PathLike; }; }; }; export const LoggingOptions: Definitions = { file: { - normalize: (raw: number | PathLike): number => { + normalize: (raw: PathLike): number => { let descriptor: number; if (typeof raw === "number") { descriptor = raw as number; } else { try { - descriptor = openSync(raw as PathLike, "a"); + descriptor = openSync(raw, "a"); } catch (err) { throw new Error( `Failed to open log file ${raw}. Please check if the file path is valid and if the process has write permissions to the directory.` diff --git a/src/chains/filecoin/options/tests/logging-options.test.ts b/src/chains/filecoin/options/tests/logging-options.test.ts index 17e15d703b..9c93e37867 100644 --- a/src/chains/filecoin/options/tests/logging-options.test.ts +++ b/src/chains/filecoin/options/tests/logging-options.test.ts @@ -40,9 +40,7 @@ describe("FilecoinOptionsConfig", () => { }); try { assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => - closeSync(options.logging.file as number) - ); + assert.doesNotThrow(() => closeSync(options.logging.file)); } finally { await unlink(validFilePath); } @@ -54,9 +52,7 @@ describe("FilecoinOptionsConfig", () => { }); try { assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => - closeSync(options.logging.file as number) - ); + assert.doesNotThrow(() => closeSync(options.logging.file)); } finally { await unlink(validFilePath); } @@ -68,27 +64,7 @@ describe("FilecoinOptionsConfig", () => { }); try { assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => - closeSync(options.logging.file as number) - ); - } finally { - await unlink(validFilePath); - } - }); - - it("uses an existing descriptor if passed in", async () => { - const fd = openSync(validFilePath, "a"); - - const options = FilecoinOptionsConfig.normalize({ - logging: { file: fd } - }); - - try { - assert.strictEqual(options.logging.file, fd); - assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => - closeSync(options.logging.file as number) - ); + assert.doesNotThrow(() => closeSync(options.logging.file)); } finally { await unlink(validFilePath); } @@ -114,13 +90,12 @@ describe("FilecoinOptionsConfig", () => { calls.push([message, ...params]); } }; - const descriptor = openSync(validFilePath, "a"); try { const options = FilecoinOptionsConfig.normalize({ logging: { logger, - file: descriptor + file: validFilePath } }); diff --git a/src/packages/options/src/create.ts b/src/packages/options/src/create.ts index a9e024730d..78582a0274 100644 --- a/src/packages/options/src/create.ts +++ b/src/packages/options/src/create.ts @@ -51,7 +51,7 @@ function fill(defaults: any, options: any, target: any, namespace: any) { const propDefinition = def[key]; let value = namespaceOptions[key]; if (value !== undefined) { - const normalized = propDefinition.normalize(value, namespaceOptions); + const normalized = propDefinition.normalize(value, config); if (normalized !== undefined) { checkForConflicts( key, @@ -66,7 +66,7 @@ function fill(defaults: any, options: any, target: any, namespace: any) { const legacyName = propDefinition.legacyName || key; value = options[legacyName]; if (value !== undefined) { - const normalized = propDefinition.normalize(value, options); + const normalized = propDefinition.normalize(value, config); if (normalized !== undefined) { checkForConflicts( key, @@ -90,7 +90,7 @@ function fill(defaults: any, options: any, target: any, namespace: any) { const legacyName = propDefinition.legacyName || key; const value = options[legacyName]; if (value !== undefined) { - const normalized = propDefinition.normalize(value, options); + const normalized = propDefinition.normalize(value, config); if (normalized !== undefined) { checkForConflicts( key, diff --git a/src/packages/utils/src/things/logger.ts b/src/packages/utils/src/things/logger.ts index dcaeac4e91..052c36fdbe 100644 --- a/src/packages/utils/src/things/logger.ts +++ b/src/packages/utils/src/things/logger.ts @@ -26,10 +26,12 @@ export function createLogger(config: { } else { if (typeof config.file !== "number") { throw new Error( - "`config.file` was not correctly noramlized to a file descriptor. This should not happen." + `'config.file' was not correctly noramlized to a file descriptor. This should not happen. ${ + config.file + }: ${typeof config.file}` ); } - const descriptor = config.file as number; + const descriptor = config.file; const diskLogFormatter = (message: any) => { const linePrefix = `${new Date().toISOString()} `; From 303330d14d2270a33dd1538ce45c354b67270767 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 8 Mar 2023 16:43:42 +1300 Subject: [PATCH 25/36] Update package-lock.json - adds reference to @ganache/utils --- src/chains/filecoin/options/package-lock.json | 232 +++++++++++++++++- 1 file changed, 230 insertions(+), 2 deletions(-) diff --git a/src/chains/filecoin/options/package-lock.json b/src/chains/filecoin/options/package-lock.json index 07dc0c5294..be4bcb4c13 100644 --- a/src/chains/filecoin/options/package-lock.json +++ b/src/chains/filecoin/options/package-lock.json @@ -19,6 +19,38 @@ "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", "dev": true }, + "@ganache/options": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@ganache/options/-/options-0.7.0.tgz", + "integrity": "sha512-1H+qJcM8u3YAFV+2wYw1fipZ/aOPyl/8jVfsTOnPuzJemZaY0X/gQ4DhbD53EJpg7QL1LnLpCFgdpryzlpHEww==", + "dev": true, + "requires": { + "@ganache/utils": "0.7.0", + "bip39": "3.0.4", + "seedrandom": "3.0.5" + } + }, + "@ganache/secp256k1": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@ganache/secp256k1/-/secp256k1-0.5.0.tgz", + "integrity": "sha512-tQ7d2Yuua/u5tPl5LMFhh34gC999A7Q6EcgQEaecvNc/4Pj2G7JfNWPDNk2DnZf19cZqxjx7gTyMDqqxfOFzFw==", + "requires": { + "node-gyp-build": "4.5.0", + "secp256k1": "4.0.3" + } + }, + "@ganache/utils": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@ganache/utils/-/utils-0.7.0.tgz", + "integrity": "sha512-NYz+ygTzOoFU7VuIFWtS+yvu4jrMfQQ55pOtJr5nmFUz/9Y75o7msGdq0kYSXbyrTFU4DsihgcdVFf81PDtfZA==", + "requires": { + "@ganache/secp256k1": "0.5.0", + "@trufflesuite/bigint-buffer": "1.1.10", + "emittery": "0.10.0", + "keccak": "3.0.2", + "seedrandom": "3.0.5" + } + }, "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -76,6 +108,23 @@ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, + "@trufflesuite/bigint-buffer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@trufflesuite/bigint-buffer/-/bigint-buffer-1.1.10.tgz", + "integrity": "sha512-pYIQC5EcMmID74t26GCC67946mgTJFiLXOT/BYozgrd4UEY2JHEGLhWi9cMiQCt5BSqFEvKkCHNnoj82SRjiEw==", + "optional": true, + "requires": { + "node-gyp-build": "4.4.0" + }, + "dependencies": { + "node-gyp-build": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz", + "integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==", + "optional": true + } + } + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -457,6 +506,31 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bip39": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz", + "integrity": "sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==", + "dev": true, + "requires": { + "@types/node": "11.11.6", + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1" + }, + "dependencies": { + "@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==", + "dev": true + } + } + }, + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -476,6 +550,11 @@ "fill-range": "^7.0.1" } }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -556,6 +635,16 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -611,6 +700,33 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -672,6 +788,25 @@ "integrity": "sha512-BQb7FgYwnu6haWLU63/CdVW+9xhmHls3RCQUFiV4lvw3wimEHTVcUk2hkuZo76QhR8nnDdfZE7evJIZqijwPdA==", "dev": true }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "emittery": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.0.tgz", + "integrity": "sha512-AGvFfs+d0JKCJQ4o01ASQLGPmSCxgfU9RFXvzPvZdjKK8oscynksuJhWrSTSw7j7Ep/sZct5b5ZhYCi8S/t0HQ==" + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -904,12 +1039,42 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -1157,6 +1322,17 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -1202,6 +1378,16 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -1416,6 +1602,19 @@ "isarray": "0.0.1" } }, + "pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -1550,6 +1749,16 @@ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1566,11 +1775,20 @@ "ajv-keywords": "^3.5.2" } }, + "secp256k1": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", + "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", + "requires": { + "elliptic": "^6.5.4", + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + } + }, "seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", - "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", - "dev": true + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" }, "semver": { "version": "7.3.7", @@ -1590,6 +1808,16 @@ "randombytes": "^2.1.0" } }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", From 7edf108e816cbd53d6ad521449a458e5de8550aa Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 9 Mar 2023 12:48:24 +1300 Subject: [PATCH 26/36] Upgrade @ganache/utils --- src/chains/filecoin/options/package-lock.json | 232 +----------------- src/chains/filecoin/options/package.json | 2 +- src/packages/ganache/npm-shrinkwrap.json | 2 +- 3 files changed, 4 insertions(+), 232 deletions(-) diff --git a/src/chains/filecoin/options/package-lock.json b/src/chains/filecoin/options/package-lock.json index be4bcb4c13..07dc0c5294 100644 --- a/src/chains/filecoin/options/package-lock.json +++ b/src/chains/filecoin/options/package-lock.json @@ -19,38 +19,6 @@ "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", "dev": true }, - "@ganache/options": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@ganache/options/-/options-0.7.0.tgz", - "integrity": "sha512-1H+qJcM8u3YAFV+2wYw1fipZ/aOPyl/8jVfsTOnPuzJemZaY0X/gQ4DhbD53EJpg7QL1LnLpCFgdpryzlpHEww==", - "dev": true, - "requires": { - "@ganache/utils": "0.7.0", - "bip39": "3.0.4", - "seedrandom": "3.0.5" - } - }, - "@ganache/secp256k1": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@ganache/secp256k1/-/secp256k1-0.5.0.tgz", - "integrity": "sha512-tQ7d2Yuua/u5tPl5LMFhh34gC999A7Q6EcgQEaecvNc/4Pj2G7JfNWPDNk2DnZf19cZqxjx7gTyMDqqxfOFzFw==", - "requires": { - "node-gyp-build": "4.5.0", - "secp256k1": "4.0.3" - } - }, - "@ganache/utils": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@ganache/utils/-/utils-0.7.0.tgz", - "integrity": "sha512-NYz+ygTzOoFU7VuIFWtS+yvu4jrMfQQ55pOtJr5nmFUz/9Y75o7msGdq0kYSXbyrTFU4DsihgcdVFf81PDtfZA==", - "requires": { - "@ganache/secp256k1": "0.5.0", - "@trufflesuite/bigint-buffer": "1.1.10", - "emittery": "0.10.0", - "keccak": "3.0.2", - "seedrandom": "3.0.5" - } - }, "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -108,23 +76,6 @@ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, - "@trufflesuite/bigint-buffer": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@trufflesuite/bigint-buffer/-/bigint-buffer-1.1.10.tgz", - "integrity": "sha512-pYIQC5EcMmID74t26GCC67946mgTJFiLXOT/BYozgrd4UEY2JHEGLhWi9cMiQCt5BSqFEvKkCHNnoj82SRjiEw==", - "optional": true, - "requires": { - "node-gyp-build": "4.4.0" - }, - "dependencies": { - "node-gyp-build": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz", - "integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==", - "optional": true - } - } - }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -506,31 +457,6 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, - "bip39": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz", - "integrity": "sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==", - "dev": true, - "requires": { - "@types/node": "11.11.6", - "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", - "randombytes": "^2.0.1" - }, - "dependencies": { - "@types/node": { - "version": "11.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", - "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==", - "dev": true - } - } - }, - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -550,11 +476,6 @@ "fill-range": "^7.0.1" } }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" - }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -635,16 +556,6 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -700,33 +611,6 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -788,25 +672,6 @@ "integrity": "sha512-BQb7FgYwnu6haWLU63/CdVW+9xhmHls3RCQUFiV4lvw3wimEHTVcUk2hkuZo76QhR8nnDdfZE7evJIZqijwPdA==", "dev": true }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "emittery": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.0.tgz", - "integrity": "sha512-AGvFfs+d0JKCJQ4o01ASQLGPmSCxgfU9RFXvzPvZdjKK8oscynksuJhWrSTSw7j7Ep/sZct5b5ZhYCi8S/t0HQ==" - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1039,42 +904,12 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -1322,17 +1157,6 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -1378,16 +1202,6 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" - }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -1602,19 +1416,6 @@ "isarray": "0.0.1" } }, - "pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -1749,16 +1550,6 @@ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1775,20 +1566,11 @@ "ajv-keywords": "^3.5.2" } }, - "secp256k1": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", - "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", - "requires": { - "elliptic": "^6.5.4", - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" - } - }, "seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", - "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dev": true }, "semver": { "version": "7.3.7", @@ -1808,16 +1590,6 @@ "randombytes": "^2.1.0" } }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", diff --git a/src/chains/filecoin/options/package.json b/src/chains/filecoin/options/package.json index 1ad8241f40..11ad0ebcc0 100644 --- a/src/chains/filecoin/options/package.json +++ b/src/chains/filecoin/options/package.json @@ -69,6 +69,6 @@ }, "dependencies": { "keccak": "3.0.2", - "@ganache/utils": "0.7.0" + "@ganache/utils": "0.7.1" } } diff --git a/src/packages/ganache/npm-shrinkwrap.json b/src/packages/ganache/npm-shrinkwrap.json index 16a9092af3..7fc6489c08 100644 --- a/src/packages/ganache/npm-shrinkwrap.json +++ b/src/packages/ganache/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "ganache", - "version": "7.7.3", + "version": "7.7.6", "lockfileVersion": 1, "requires": true, "dependencies": { From ca8f58198f480131a0d68478b4a4adb3cf250dbf Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 9 Mar 2023 13:14:55 +1300 Subject: [PATCH 27/36] Update reference to Logger type in tests --- src/chains/ethereum/ethereum/tests/api/eth/instamine.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chains/ethereum/ethereum/tests/api/eth/instamine.test.ts b/src/chains/ethereum/ethereum/tests/api/eth/instamine.test.ts index bc3bf3a710..2edad65bb4 100644 --- a/src/chains/ethereum/ethereum/tests/api/eth/instamine.test.ts +++ b/src/chains/ethereum/ethereum/tests/api/eth/instamine.test.ts @@ -1,6 +1,6 @@ import getProvider from "../../helpers/getProvider"; import assert from "assert"; -import { Logger } from "@ganache/ethereum-options/typings/src/logging-options"; +import { Logger } from "@ganache/utils"; describe("api", () => { describe("eth", () => { From 04a067472e13c239bf2b59405fa42ba48a0dac24 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 9 Mar 2023 16:21:44 +1300 Subject: [PATCH 28/36] Modify 'EPERM' test to support Windows (who does some freaky stuff when a directory is opened for writing) --- .../options/tests/logging-options.test.ts | 39 +++++++++++-------- .../options/tests/logging-options.test.ts | 39 +++++++++++-------- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index d6a1779dd8..a8c560e031 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -3,15 +3,12 @@ import { EthereumOptionsConfig } from "../src"; import sinon from "sinon"; import { resolve } from "path"; import { promises } from "fs"; -const { unlink, readFile } = promises; -import { closeSync, openSync } from "fs"; +const { unlink, readFile, open } = promises; +import { closeSync } from "fs"; import { URL } from "url"; describe("EthereumOptionsConfig", () => { describe("logging", () => { - // resolve absolute path of current working directory, which is clearly an - // invalid file path (because it's a directory). - const invalidFilePath = resolve(""); const validFilePath = resolve("./tests/test-file.log"); describe("options", () => { @@ -96,17 +93,27 @@ describe("EthereumOptionsConfig", () => { } }); - it("fails if an invalid file path is provided", () => { - const message = `Failed to open log file ${invalidFilePath}. Please check if the file path is valid and if the process has write permissions to the directory.`; - - assert.throws( - () => { - EthereumOptionsConfig.normalize({ - logging: { file: invalidFilePath } - }); - }, - { message } - ); + it("fails if an invalid file path is provided", async () => { + const file = resolve("./eperm-file.log"); + try { + const handle = await open(file, "w"); + // set no permissions on the file + await handle.chmod(0); + await handle.close(); + + const error = { message: `Failed to open log file ${file}. Please check if the file path is valid and if the process has write permissions to the directory.` }; + + assert.throws( + () => + EthereumOptionsConfig.normalize({ + logging: { file } + }) + , error + ); + + } finally { + await unlink(file); + } }); it("uses the provided logger, and file when both `logger` and `file` are provided", async () => { diff --git a/src/chains/filecoin/options/tests/logging-options.test.ts b/src/chains/filecoin/options/tests/logging-options.test.ts index 9c93e37867..b7dc19717e 100644 --- a/src/chains/filecoin/options/tests/logging-options.test.ts +++ b/src/chains/filecoin/options/tests/logging-options.test.ts @@ -3,15 +3,12 @@ import { FilecoinOptionsConfig } from "../src"; import sinon from "sinon"; import { resolve } from "path"; import { promises } from "fs"; -const unlink = promises.unlink; -import { closeSync, openSync } from "fs"; -import { URL } from "url"; +const { unlink, open } = promises; +import { closeSync } from "fs"; +import { URL } from "url"; describe("FilecoinOptionsConfig", () => { describe("logging", () => { - // resolve absolute path of current working directory, which is clearly an - // invalid file path (because it's a directory). - const invalidFilePath = resolve(""); const validFilePath = resolve("./tests/test-file.log"); describe("options", () => { @@ -70,17 +67,27 @@ describe("FilecoinOptionsConfig", () => { } }); - it("fails if an invalid file path is provided", () => { - const message = `Failed to open log file ${invalidFilePath}. Please check if the file path is valid and if the process has write permissions to the directory.`; + it("fails if an invalid file path is provided", async () => { + const file = resolve("./eperm-file.log"); + try { + const handle = await open(file, "w"); + // set no permissions on the file + await handle.chmod(0); + await handle.close(); + + const error = { message: `Failed to open log file ${file}. Please check if the file path is valid and if the process has write permissions to the directory.` }; - assert.throws( - () => { - FilecoinOptionsConfig.normalize({ - logging: { file: invalidFilePath } - }); - }, - { message } - ); + assert.throws( + () => + FilecoinOptionsConfig.normalize({ + logging: { file } + }) + , error + ); + + } finally { + await unlink(file); + } }); it("uses the provided logger when both `logger` and `file` are provided", async () => { From 8352f82d2ca8c2ea15d08396278e409c1c88a9fe Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 9 Mar 2023 18:36:56 +1300 Subject: [PATCH 29/36] Pin @types/sinon --- src/chains/filecoin/options/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chains/filecoin/options/package.json b/src/chains/filecoin/options/package.json index 11ad0ebcc0..4421cd3c0e 100644 --- a/src/chains/filecoin/options/package.json +++ b/src/chains/filecoin/options/package.json @@ -53,7 +53,7 @@ "@ganache/options": "0.7.1", "@types/mocha": "9.0.0", "@types/seedrandom": "3.0.1", - "@types/sinon": "^10.0.13", + "@types/sinon": "10.0.13", "@types/terser-webpack-plugin": "5.0.2", "cross-env": "7.0.3", "mocha": "9.1.3", From 5a556d6e0be7a2495f5afc907df7e121e733bc52 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 9 Mar 2023 18:47:52 +1300 Subject: [PATCH 30/36] Add test to ensure that resolved file descriptor is append only --- .../options/tests/logging-options.test.ts | 48 ++++++++++++++++--- .../options/tests/logging-options.test.ts | 47 +++++++++++++++--- 2 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index a8c560e031..072227c4f1 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -63,7 +63,10 @@ describe("EthereumOptionsConfig", () => { }); try { assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => closeSync(options.logging.file)); + assert.doesNotThrow( + () => closeSync(options.logging.file), + "File descriptor not valid" + ); } finally { await unlink(validFilePath); } @@ -75,7 +78,10 @@ describe("EthereumOptionsConfig", () => { }); try { assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => closeSync(options.logging.file)); + assert.doesNotThrow( + () => closeSync(options.logging.file), + "File descriptor not valid" + ); } finally { await unlink(validFilePath); } @@ -87,7 +93,10 @@ describe("EthereumOptionsConfig", () => { }); try { assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => closeSync(options.logging.file)); + assert.doesNotThrow( + () => closeSync(options.logging.file), + "File descriptor not valid" + ); } finally { await unlink(validFilePath); } @@ -101,21 +110,46 @@ describe("EthereumOptionsConfig", () => { await handle.chmod(0); await handle.close(); - const error = { message: `Failed to open log file ${file}. Please check if the file path is valid and if the process has write permissions to the directory.` }; + const error = { + message: `Failed to open log file ${file}. Please check if the file path is valid and if the process has write permissions to the directory.` + }; assert.throws( () => EthereumOptionsConfig.normalize({ logging: { file } - }) - , error + }), + error ); - } finally { await unlink(file); } }); + it("should append to the specified file", async () => { + const message = "message"; + const handle = await open(validFilePath, "w"); + try { + await handle.write("existing content"); + handle.close(); + + const options = EthereumOptionsConfig.normalize({ + logging: { file: validFilePath } + }); + options.logging.logger.log(message); + closeSync(options.logging.file); + + const readHandle = await open(validFilePath, "r"); + const content = await readHandle.readFile({ encoding: "utf8" }); + assert( + content.startsWith("existing content"), + "Existing content was overwritten by the logger" + ); + } finally { + await unlink(validFilePath); + } + }); + it("uses the provided logger, and file when both `logger` and `file` are provided", async () => { const calls: any[] = []; const logger = { diff --git a/src/chains/filecoin/options/tests/logging-options.test.ts b/src/chains/filecoin/options/tests/logging-options.test.ts index b7dc19717e..7ebf731156 100644 --- a/src/chains/filecoin/options/tests/logging-options.test.ts +++ b/src/chains/filecoin/options/tests/logging-options.test.ts @@ -37,7 +37,10 @@ describe("FilecoinOptionsConfig", () => { }); try { assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => closeSync(options.logging.file)); + assert.doesNotThrow( + () => closeSync(options.logging.file), + "File descriptor not valid" + ); } finally { await unlink(validFilePath); } @@ -49,7 +52,10 @@ describe("FilecoinOptionsConfig", () => { }); try { assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => closeSync(options.logging.file)); + assert.doesNotThrow( + () => closeSync(options.logging.file), + "File descriptor not valid" + ); } finally { await unlink(validFilePath); } @@ -61,7 +67,10 @@ describe("FilecoinOptionsConfig", () => { }); try { assert(typeof options.logging.file === "number"); - assert.doesNotThrow(() => closeSync(options.logging.file)); + assert.doesNotThrow( + () => closeSync(options.logging.file), + "File descriptor not valid" + ); } finally { await unlink(validFilePath); } @@ -75,21 +84,45 @@ describe("FilecoinOptionsConfig", () => { await handle.chmod(0); await handle.close(); - const error = { message: `Failed to open log file ${file}. Please check if the file path is valid and if the process has write permissions to the directory.` }; + const error = { + message: `Failed to open log file ${file}. Please check if the file path is valid and if the process has write permissions to the directory.` + }; assert.throws( () => FilecoinOptionsConfig.normalize({ logging: { file } - }) - , error + }), + error ); - } finally { await unlink(file); } }); + it("should append to the specified file", async () => { + const message = "message"; + const handle = await open(validFilePath, "w"); + try { + await handle.write("existing content"); + handle.close(); + + const options = FilecoinOptionsConfig.normalize({ + logging: { file: validFilePath } + }); + options.logging.logger.log(message); + closeSync(options.logging.file); + + const readHandle = await open(validFilePath, "r"); + const content = await readHandle.readFile({ encoding: "utf8" }); + assert( + content.startsWith("existing content"), + "Existing content was overwritten by the logger" + ); + } finally { + await unlink(validFilePath); + } + }); it("uses the provided logger when both `logger` and `file` are provided", async () => { const calls: any[] = []; const logger = { From 1ff9eb9b3c23e6b5c2e5c1c8716350d763016b7e Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 9 Mar 2023 20:38:04 +1300 Subject: [PATCH 31/36] Fix failing tests --- .../options/tests/logging-options.test.ts | 10 +++++- .../filecoin/options/src/logging-options.ts | 29 ++++++++--------- .../options/tests/logging-options.test.ts | 31 ++++++++++++++++--- src/packages/options/src/definition.ts | 2 +- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/src/chains/ethereum/options/tests/logging-options.test.ts b/src/chains/ethereum/options/tests/logging-options.test.ts index a8c560e031..ff2261ff85 100644 --- a/src/chains/ethereum/options/tests/logging-options.test.ts +++ b/src/chains/ethereum/options/tests/logging-options.test.ts @@ -93,7 +93,7 @@ describe("EthereumOptionsConfig", () => { } }); - it("fails if an invalid file path is provided", async () => { + it("fails if the process doesn't have write access to the file path provided", async () => { const file = resolve("./eperm-file.log"); try { const handle = await open(file, "w"); @@ -133,6 +133,14 @@ describe("EthereumOptionsConfig", () => { }); options.logging.logger.log("message", "param1", "param2"); + if ("getCompletionHandle" in options.logging.logger) { + //wait for the logs to be written to disk + await options.logging.logger.getCompletionHandle(); + } else { + throw new Error("Expected options.logging.logger to be AsyncronousLogger"); + } + closeSync(options.logging.file); + assert.deepStrictEqual(calls, [["message", "param1", "param2"]]); const fromFile = await readFile(validFilePath, "utf8"); diff --git a/src/chains/filecoin/options/src/logging-options.ts b/src/chains/filecoin/options/src/logging-options.ts index 043f1f9315..321863f4a0 100644 --- a/src/chains/filecoin/options/src/logging-options.ts +++ b/src/chains/filecoin/options/src/logging-options.ts @@ -38,16 +38,12 @@ export const LoggingOptions: Definitions = { file: { normalize: (raw: PathLike): number => { let descriptor: number; - if (typeof raw === "number") { - descriptor = raw as number; - } else { - try { - descriptor = openSync(raw, "a"); - } catch (err) { - throw new Error( - `Failed to open log file ${raw}. Please check if the file path is valid and if the process has write permissions to the directory.` - ); - } + try { + descriptor = openSync(raw, "a"); + } catch (err) { + throw new Error( + `Failed to open log file ${raw}. Please check if the file path is valid and if the process has write permissions to the directory.` + ); } return descriptor; }, @@ -57,17 +53,18 @@ export const LoggingOptions: Definitions = { cliType: "string" }, logger: { - normalize: raw => - createLogger({ - ...raw, - baseLogger: raw - }), + normalize: (logger: Logger, config) => { + return createLogger({ + file: (config as any).file, + baseLogger: logger + }); + }, cliDescription: "An object, like `console`, that implements a `log` function.", disableInCLI: true, default: config => { return createLogger({ - ...config, + file: config.file, baseLogger: console }); } diff --git a/src/chains/filecoin/options/tests/logging-options.test.ts b/src/chains/filecoin/options/tests/logging-options.test.ts index b7dc19717e..d3d0c15fd8 100644 --- a/src/chains/filecoin/options/tests/logging-options.test.ts +++ b/src/chains/filecoin/options/tests/logging-options.test.ts @@ -3,7 +3,7 @@ import { FilecoinOptionsConfig } from "../src"; import sinon from "sinon"; import { resolve } from "path"; import { promises } from "fs"; -const { unlink, open } = promises; +const { unlink, open, readFile } = promises; import { closeSync } from "fs"; import { URL } from "url"; @@ -67,7 +67,7 @@ describe("FilecoinOptionsConfig", () => { } }); - it("fails if an invalid file path is provided", async () => { + it("fails if the process doesn't have write access to the file path provided", async () => { const file = resolve("./eperm-file.log"); try { const handle = await open(file, "w"); @@ -89,8 +89,7 @@ describe("FilecoinOptionsConfig", () => { await unlink(file); } }); - - it("uses the provided logger when both `logger` and `file` are provided", async () => { + it("uses the provided logger, and file when both `logger` and `file` are provided", async () => { const calls: any[] = []; const logger = { log: (message: any, ...params: any[]) => { @@ -107,7 +106,31 @@ describe("FilecoinOptionsConfig", () => { }); options.logging.logger.log("message", "param1", "param2"); + if ("getCompletionHandle" in options.logging.logger) { + //wait for the logs to be written to disk + await options.logging.logger.getCompletionHandle(); + } else { + throw new Error("Expected options.logging.logger to be AsyncronousLogger"); + } + closeSync(options.logging.file); + assert.deepStrictEqual(calls, [["message", "param1", "param2"]]); + + const fromFile = await readFile(validFilePath, "utf8"); + assert(fromFile !== "", "Nothing written to the log file"); + + const timestampPart = fromFile.substring(0, 24); + + const timestampRegex = + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/; + assert( + timestampPart.match(timestampRegex), + `Unexpected timestamp from file ${timestampPart}` + ); + + const messagePart = fromFile.substring(25); + + assert.strictEqual(messagePart, "message param1 param2\n"); } finally { await unlink(validFilePath); } diff --git a/src/packages/options/src/definition.ts b/src/packages/options/src/definition.ts index 33cbad0a42..66b3d4d908 100644 --- a/src/packages/options/src/definition.ts +++ b/src/packages/options/src/definition.ts @@ -21,7 +21,7 @@ type Normalize< N extends OptionName = OptionName > = ( rawInput: OptionRawType, - config?: Readonly> + config: Readonly> ) => OptionType; export type ExternalConfig = Partial< From b57708925f24b2242a1e4a60435178ccded00031 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 10 Mar 2023 09:24:40 +1300 Subject: [PATCH 32/36] Add test to ensure that provider.disconnect() closes the file descriptor --- .../ethereum/ethereum/tests/provider.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/chains/ethereum/ethereum/tests/provider.test.ts b/src/chains/ethereum/ethereum/tests/provider.test.ts index 536b462294..60af7d307e 100644 --- a/src/chains/ethereum/ethereum/tests/provider.test.ts +++ b/src/chains/ethereum/ethereum/tests/provider.test.ts @@ -8,6 +8,8 @@ import EthereumApi from "../src/api"; import getProvider from "./helpers/getProvider"; import compile from "./helpers/compile"; import Web3 from "web3"; +import { promises, closeSync } from "fs"; +const { stat, unlink } = promises; describe("provider", () => { describe("options", () => { @@ -644,6 +646,34 @@ describe("provider", () => { }); }); + it("closes the logging fileDescriptor", async () => { + const filePath = "./closes-logging-descriptor.log"; + const provider = await getProvider({ logging: { file: filePath } }); + + try { + const descriptor = (await provider).getOptions().logging.file; + assert.strictEqual( + typeof descriptor, + "number", + `File descriptor has unexepected value ${typeof descriptor}` + ); + + assert( + (await stat(filePath)).isFile(), + `log file: ${filePath} was not created` + ); + + await provider.disconnect(); + + assert.throws( + () => closeSync(descriptor), + "File descriptor is still valid after disconnect() called" + ); + } finally { + await unlink(filePath); + } + }); + // todo: Reinstate this test when https://github.com/trufflesuite/ganache/issues/3499 is fixed describe.skip("without asyncRequestProcessing", () => { // we only test this with asyncRequestProcessing: false, because it's impossible to force requests From f4e7ad1ec4f04abc966b81a5dff825c2effe92e8 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 22 Feb 2023 16:50:06 +1300 Subject: [PATCH 33/36] implement 'ganache instances logs ' with support for --since, --until, and --follow --- src/packages/cli/package-lock.json | 5 + src/packages/cli/package.json | 3 +- src/packages/cli/src/args.ts | 51 ++++++ src/packages/cli/src/cli.ts | 36 +++- src/packages/cli/src/detach.ts | 194 ++++++++++++--------- src/packages/cli/src/logs-stream.ts | 125 +++++++++++++ src/packages/cli/src/types.ts | 6 + src/packages/cli/tests/logs-stream.test.ts | 148 ++++++++++++++++ 8 files changed, 480 insertions(+), 88 deletions(-) create mode 100644 src/packages/cli/src/logs-stream.ts create mode 100644 src/packages/cli/tests/logs-stream.test.ts diff --git a/src/packages/cli/package-lock.json b/src/packages/cli/package-lock.json index c04613a00f..518de0bc81 100644 --- a/src/packages/cli/package-lock.json +++ b/src/packages/cli/package-lock.json @@ -881,6 +881,11 @@ "p-limit": "^3.0.2" } }, + "parse-duration": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.0.2.tgz", + "integrity": "sha512-Dg27N6mfok+ow1a2rj/nRjtCfaKrHUZV2SJpEn/s8GaVUSlf4GGRCRP1c13Hj+wfPKVMrFDqLMLITkYKgKxyyg==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", diff --git a/src/packages/cli/package.json b/src/packages/cli/package.json index f3789e8b4e..278a83cf65 100644 --- a/src/packages/cli/package.json +++ b/src/packages/cli/package.json @@ -72,6 +72,7 @@ "@types/node": "17.0.0", "chalk": "4.1.0", "cli-table": "0.3.11", - "marked-terminal": "4.1.0" + "marked-terminal": "4.1.0", + "parse-duration": "1.0.2" } } diff --git a/src/packages/cli/src/args.ts b/src/packages/cli/src/args.ts index ed27ce8356..4f4cac25a4 100644 --- a/src/packages/cli/src/args.ts +++ b/src/packages/cli/src/args.ts @@ -17,6 +17,7 @@ import { EOL } from "os"; import marked from "marked"; import TerminalRenderer from "marked-terminal"; import { _DefaultServerOptions } from "@ganache/core"; +import parseDuration from "parse-duration"; marked.setOptions({ renderer: new TerminalRenderer({ @@ -272,6 +273,30 @@ export default function ( stopArgs.action = "stop"; } ) + .command( + "logs ", + "fetch logs for the instance specified by ", + logsArgs => { + logsArgs.positional("name", { type: "string" }); + logsArgs + .options("follow", { + type: "boolean", + alias: ["f"], + description: "continue streaming the instances logs" + }) + .options("since", { + type: "string", + alias: ["s"] + }) + .options("until", { + type: "string", + alias: ["u"] + }); + }, + logsArgs => { + logsArgs.action = "logs"; + } + ) .version(false); } ) @@ -307,6 +332,14 @@ export default function ( "flavor" | "action" >) }; + } else if (parsedArgs.action === "logs") { + finalArgs = { + action: "logs", + name: parsedArgs.name as string, + follow: parsedArgs.follow as boolean, + since: getTimestampFromInput(parsedArgs.since as string), + until: getTimestampFromInput(parsedArgs.until as string) + }; } else { throw new Error(`Unknown action: ${parsedArgs.action}`); } @@ -314,6 +347,24 @@ export default function ( return finalArgs; } +function getTimestampFromInput(input: string): number | null { + if (input == null) { + return null; + } + + const parsedDate = Date.parse(input); + if (!Number.isNaN(parsedDate)) { + return parsedDate; + } + + const duration = parseDuration(input, "ms"); + if (duration == null) { + throw new Error(`Invalid duration ${input}`); + } + + return Date.now() - duration; +} + /** * Expands the arguments into an object including only namespaced keys from the * `args` argument. diff --git a/src/packages/cli/src/cli.ts b/src/packages/cli/src/cli.ts index 710d5004b5..ae3ad3d4f3 100644 --- a/src/packages/cli/src/cli.ts +++ b/src/packages/cli/src/cli.ts @@ -13,12 +13,14 @@ import { stopDetachedInstance, startDetachedInstance, getDetachedInstances, - formatUptime + formatUptime, + getInstanceLogsPath, + getDetachedInstanceByName } from "./detach"; import { TruffleColors } from "@ganache/colors"; import Table from "cli-table"; import chalk from "chalk"; - +import { getLogsStream } from "./logs-stream"; const porscheColor = chalk.hex(TruffleColors.porsche); const logAndForceExit = (messages: any[], exitCode = 0) => { @@ -172,6 +174,36 @@ if (argv.action === "start") { console.error("Instance not found"); } }); +} else if (argv.action === "logs") { + const instanceName = argv.name; + + getDetachedInstanceByName(instanceName) + .then(_ => { + const path = getInstanceLogsPath(instanceName); + + const stream = getLogsStream(path, { + follow: argv.follow, + since: argv.since, + until: argv.until + }); + + stream.on("error", err => { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + console.log( + `No logs are available for ${instanceName}.\nTry calling some RPC methods.` + ); + } + }); + stream.pipe(process.stdout); + //todo: we need to be able to quit from this if `--follow` is provided + }) + .catch(err => { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + console.error("Instance not found"); + } else { + console.error(err); + } + }); } else if (argv.action === "start-detached") { startDetachedInstance(process.argv, argv, version) .then(instance => { diff --git a/src/packages/cli/src/detach.ts b/src/packages/cli/src/detach.ts index c4860bc32c..b9f94a2197 100644 --- a/src/packages/cli/src/detach.ts +++ b/src/packages/cli/src/detach.ts @@ -4,9 +4,10 @@ import envPaths from "env-paths"; import psList, { ProcessDescriptor } from "@trufflesuite/ps-list"; import { Dirent, promises as fsPromises } from "fs"; // this awkward import is required to support node 12 -const { readFile, mkdir, readdir, rmdir, writeFile, unlink } = fsPromises; +const { readFile, mkdir, readdir, rmdir, open, unlink } = fsPromises; import path from "path"; import { FlavorName } from "@ganache/flavors"; +import { FileHandle } from "fs/promises"; export type DetachedInstance = { name: string; @@ -28,6 +29,10 @@ function getInstanceFilePath(instanceName: string): string { return path.join(dataPath, `${instanceName}.json`); } +export function getInstanceLogsPath(instanceName: string): string { + return path.join(dataPath, `${instanceName}.log`); +} + /** * Notify that the detached instance has started and is ready to receive requests. */ @@ -42,12 +47,18 @@ export function notifyDetachedInstanceReady(port: number) { * @param {string} instanceName the name of the instance to be removed * @returns boolean indicating whether the instance file was cleaned up successfully */ -export async function removeDetachedInstanceFile( +export async function removeDetachedInstanceFiles( instanceName: string ): Promise { const instanceFilename = getInstanceFilePath(instanceName); + const instanceLogFilename = getInstanceLogsPath(instanceName); try { - await unlink(instanceFilename); + await Promise.all([ + unlink(instanceFilename), + unlink(instanceLogFilename).catch(err => { + if (err.code !== "ENOENT") throw err; + }) + ]); return true; } catch {} return false; @@ -77,7 +88,7 @@ export async function stopDetachedInstance( } catch (err) { return false; } finally { - await removeDetachedInstanceFile(instanceName); + await removeDetachedInstanceFiles(instanceName); } return true; } @@ -98,10 +109,40 @@ export async function startDetachedInstance( ): Promise { const [bin, module, ...args] = argv; + let instanceName = createInstanceName(); + let filehandle: FileHandle; + while (true) { + const instanceFilename = getInstanceFilePath(instanceName); + try { + filehandle = await open(instanceFilename, "wx"); + break; + } catch (err) { + switch ((err as NodeJS.ErrnoException).code) { + case "EEXIST": + // an instance already exists with this name + instanceName = createInstanceName(); + break; + case "ENOENT": + // we don't check whether the folder exists before writing, as that's + // a very uncommon case. Catching the exception and subsequently + // creating the directory is faster in the majority of cases. + await mkdir(dataPath, { recursive: true }); + break; + default: + throw err; + } + } + } + // append `--no-detach` argument to cancel out --detach and aliases (must be // the last argument). See test "is false, when proceeded with --no-detach" in // args.test.ts - const childArgs = [...args, "--no-detach"]; + const childArgs = [ + ...args, + "--no-detach", + "--logging.file", + getInstanceLogsPath(instanceName) + ]; const child = fork(module, childArgs, { stdio: ["ignore", "ignore", "pipe", "ipc"], @@ -158,7 +199,7 @@ export async function startDetachedInstance( const instance: DetachedInstance = { startTime, pid, - name: createInstanceName(), + name: instanceName, host, port, flavor, @@ -166,33 +207,8 @@ export async function startDetachedInstance( version }; - while (true) { - const instanceFilename = getInstanceFilePath(instance.name); - try { - await writeFile(instanceFilename, JSON.stringify(instance), { - // wx means "Open file for writing, but fail if the path exists". see - // https://nodejs.org/api/fs.html#file-system-flags - flag: "wx", - encoding: FILE_ENCODING - }); - break; - } catch (err) { - switch ((err as NodeJS.ErrnoException).code) { - case "EEXIST": - // an instance already exists with this name - instance.name = createInstanceName(); - break; - case "ENOENT": - // we don't check whether the folder exists before writing, as that's - // a very uncommon case. Catching the exception and subsequently - // creating the directory is faster in the majority of cases. - await mkdir(dataPath, { recursive: true }); - break; - default: - throw err; - } - } - } + await filehandle.write(JSON.stringify(instance), 0, FILE_ENCODING); + await filehandle.close(); return instance; } @@ -221,62 +237,70 @@ export async function getDetachedInstances(): Promise { } const instances: DetachedInstance[] = []; - const loadingInstancesInfos = dirEntries.map(async dirEntry => { - const filename = dirEntry.name; - const { name: instanceName, ext } = path.parse(filename); - - let failureReason: string; - - if (ext !== ".json") { - failureReason = `"${filename}" does not have a .json extension`; - } else { - let instance: DetachedInstance; - try { - // getDetachedInstanceByName() throws if the instance file is not found or - // cannot be parsed - instance = await getDetachedInstanceByName(instanceName); - } catch (err: any) { - failureReason = err.message; - } - if (instance) { - const matchingProcess = processes.find(p => p.pid === instance.pid); - if (!matchingProcess) { - failureReason = `Process with PID ${instance.pid} could not be found`; - } else if (matchingProcess.cmd !== instance.cmd) { - failureReason = `Process with PID ${instance.pid} does not match ${instanceName}`; - } else { - instances.push(instance); - } - } - } - - if (failureReason !== undefined) { - someInstancesFailed = true; - const fullPath = path.join(dataPath, filename); - let resolution: string; - if (dirEntry.isDirectory()) { - const reason = `"${filename}" is a directory`; - try { - await rmdir(fullPath, { recursive: true }); - failureReason = reason; - } catch { - resolution = `"${filename}" could not be removed`; - } + const loadingInstancesInfos = dirEntries + .map(dirEntry => { + const { name: instanceName, ext } = path.parse(dirEntry.name); + return { + instanceName, + ext, + filename: dirEntry.name, + isDirectory: dirEntry.isDirectory + }; + }) + .filter(({ ext }) => ext !== ".log") + .map(async ({ ext, instanceName, filename, isDirectory }) => { + let failureReason: string; + + if (ext !== ".json") { + failureReason = `"${filename}" does not have a .json extension`; } else { + let instance: DetachedInstance; try { - await unlink(fullPath); - } catch { - resolution = `"${filename}" could not be removed`; + // getDetachedInstanceByName() throws if the instance file is not found or + // cannot be parsed + instance = await getDetachedInstanceByName(instanceName); + } catch (err: any) { + failureReason = err.message; + } + if (instance) { + const matchingProcess = processes.find(p => p.pid === instance.pid); + if (!matchingProcess) { + failureReason = `Process with PID ${instance.pid} could not be found`; + } else if (matchingProcess.cmd !== instance.cmd) { + failureReason = `Process with PID ${instance.pid} does not match ${instanceName}`; + } else { + instances.push(instance); + } } } - console.warn( - `Failed to load instance data. ${failureReason}. ${ - resolution || `"${filename}" has been removed` - }.` - ); - } - }); + if (failureReason !== undefined) { + someInstancesFailed = true; + const fullPath = path.join(dataPath, filename); + let resolution: string; + if (isDirectory()) { + const reason = `"${filename}" is a directory`; + try { + await rmdir(fullPath, { recursive: true }); + failureReason = reason; + } catch { + resolution = `"${filename}" could not be removed`; + } + } else { + try { + await unlink(fullPath); + } catch { + resolution = `"${filename}" could not be removed`; + } + } + + console.warn( + `Failed to load instance data. ${failureReason}. ${ + resolution || `"${filename}" has been removed` + }.` + ); + } + }); await Promise.all(loadingInstancesInfos); @@ -294,7 +318,7 @@ export async function getDetachedInstances(): Promise { * the instance file is not found or cannot be parsed * @param {string} instanceName */ -async function getDetachedInstanceByName( +export async function getDetachedInstanceByName( instanceName: string ): Promise { const filepath = getInstanceFilePath(instanceName); diff --git a/src/packages/cli/src/logs-stream.ts b/src/packages/cli/src/logs-stream.ts new file mode 100644 index 0000000000..3db5f06929 --- /dev/null +++ b/src/packages/cli/src/logs-stream.ts @@ -0,0 +1,125 @@ +import { createReadStream, FSWatcher, watch } from "fs"; +import { Readable } from "stream"; +import * as readline from "readline"; + +export type LogsStreamOptions = Partial<{ + follow: boolean; + since: number; + until: number; +}>; + +export function getLogsStream( + path: string, + options: LogsStreamOptions +): Readable { + let logsStream: Readable; + if (options.follow) { + logsStream = createFollowReadStream(path); + } else { + logsStream = createReadStream(path, { encoding: "utf8" }); + } + if (options.since != null || options.until != null) { + return filterLogsStream({ + input: logsStream, + since: options.since, + until: options.until + }); + } else { + return logsStream; + } +} + +export function filterLogsStream(args: { + input: Readable; + since?: number; + until?: number; +}) { + const { input, since, until } = args; + + if (since == null && until == null) { + return input; + } else { + const outstream = new Readable({ + read: (size: number) => {} + }); + + const rl = readline.createInterface(input); + + rl.on("line", line => { + if (since != null || until != null) { + const date = Date.parse(line.substring(0, 24)); + if ( + (since == null || date >= since) && + (until == null || date <= until) + ) { + outstream.push(Buffer.from(`${line}\n`, "utf8")); + } + } else { + outstream.push(Buffer.from(`${line}\n`, "utf8")); + } + }); + + input + .on("end", () => { + outstream.emit("end"); + }) + .on("eof", () => { + outstream.emit("eof"); + }); + return outstream; + } +} + +/** + * Creates a {Readable} stream of data from the file specified by {filename}. + * Continues to stream as data is appended to the file. Note: the file must be + * written to append-only, updates to existing data will result in undefined + * behaviour. + * @param {string} filename + * @returns {Readable} a stream of the file + */ +export function createFollowReadStream(filename: string): Readable { + let currentSize = 0; + let directFileStream: Readable; + let watcher: FSWatcher; + + const followStream = new Readable({ + // noop because the data is _pushed_ into `followStream` + read: function (size: number) {} + }); + + function createStream() { + directFileStream = createReadStream(filename, { + start: currentSize, + encoding: "utf8" + }) + .on("data", data => { + currentSize += data.length; + const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data, "utf8"); + + //push the chunk into `followStream`'s internal buffer + followStream.push(chunk); + }) + .on("end", () => { + directFileStream.destroy(); + followStream.emit("eof"); + watcher = watch(filename, () => { + watcher.close(); + createStream(); + }); + }); + } + createStream(); + + return followStream; +} + +function readFromBuffers(buffers: Buffer[], size?: number) { + const entireBuffer = Buffer.concat(buffers.splice(0, buffers.length)); + if (size == undefined || size <= entireBuffer.length) { + return entireBuffer; + } else { + buffers.push(entireBuffer.slice(size + 1)); + return entireBuffer.slice(0, size); + } +} diff --git a/src/packages/cli/src/types.ts b/src/packages/cli/src/types.ts index 1cb603e559..bec7ec21b6 100644 --- a/src/packages/cli/src/types.ts +++ b/src/packages/cli/src/types.ts @@ -20,6 +20,12 @@ export type StartArgs = export type GanacheArgs = | (AbstractArgs<"stop"> & { name: string }) + | (AbstractArgs<"logs"> & { + name: string; + follow?: boolean; + since?: number; + until?: number; + }) | AbstractArgs<"list"> | StartArgs; diff --git a/src/packages/cli/tests/logs-stream.test.ts b/src/packages/cli/tests/logs-stream.test.ts new file mode 100644 index 0000000000..a870271277 --- /dev/null +++ b/src/packages/cli/tests/logs-stream.test.ts @@ -0,0 +1,148 @@ +import assert from "assert"; +import { + filterLogsStream, + getLogsStream, + createFollowReadStream +} from "../src/logs-stream"; +import { Readable } from "stream"; +import { appendFile, writeFile } from "fs/promises"; +import { readFileSync } from "fs"; + +describe("logs-stream", () => { + const fixturePath = "./tests/logs.fixture.log"; + describe("createFollowReadStream()", () => { + const fixtureContents = readFileSync(fixturePath, "utf8"); + + it("should load all of the data from the file", async () => { + const logStream = createFollowReadStream(fixturePath); + + const logs = await new Promise((resolve, reject) => { + const logLines: Buffer[] = []; + + logStream + .on("data", data => logLines.push(data)) + .on("eof", () => { + logStream.destroy(); + resolve(Buffer.concat(logLines).toString("utf8")); + }) + .on("error", reject); + }); + + assert.deepStrictEqual(logs, fixtureContents); + }); + + it("should load data appended after reading to the end of file", async () => { + const newLogLine = `${new Date().toISOString()} new log line\n`; + + const logStream = createFollowReadStream(fixturePath); + + // don't `await`, because we need to write the file back to it's original state + const loadingLogs = await new Promise((resolve, reject) => { + const logLines: Buffer[] = []; + + logStream + // start reading log lines immediately, otherwise the file contents are buffered + .on("data", () => {}) + .once("eof", () => { + logStream + .on("data", data => logLines.push(data)) + .once("eof", () => { + logStream.destroy(); + const logs = Buffer.concat(logLines).toString("utf8"); + resolve(logs); + }); + appendFile(fixturePath, newLogLine); + }) + .on("error", reject); + }); + + try { + assert.deepStrictEqual(await loadingLogs, newLogLine); + } finally { + writeFile(fixturePath, fixtureContents); + } + }); + }); + + describe("filterLogsStream()", () => { + // First log stamped at epoch + const epoch = Date.parse("2020-01-01 00:00:00 UTC"); + // subsequent logs are each incremented by 1 minute + const timestampFromLineNumber = i => epoch + i * 60000; + const logLines = [...new Array(1000)].map( + (_, i) => + `${new Date(timestampFromLineNumber(i)).toISOString()} Log line ${i}\n` + ); + + it("should return the input stream when no filter parameters are provided", async () => { + const input = Readable.from(logLines); + + const filteredStream = filterLogsStream({ input }); + + assert.strictEqual( + filteredStream, + input, + "filterLogsStream() didn't output the input stream by reference" + ); + }); + + it("should only return lines stamped equal to or later than the parameter passed as `since`", async () => { + const logLinesToSkip = 100; + const since = timestampFromLineNumber(logLinesToSkip); + + const input = Readable.from(logLines); + const expected = Buffer.from( + logLines.slice(logLinesToSkip).join(""), + "utf8" + ); + + const filteredStream = filterLogsStream({ input, since }); + + const result = await new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + filteredStream + .on("data", chunk => chunks.push(chunk)) + .on("end", () => resolve(Buffer.concat(chunks))) + .on("error", reject); + }); + + assert( + result.equals(expected), + `filterLogsStream() didn't correctly skip first ${logLinesToSkip} lines from the input log stream. Expected ${expected.length} bytes. Got ${result.length} bytes` + ); + }); + + it("should only return lines stamped equal or earlier than the parameter passed as `since`", async () => { + const logLinesToReturn = 4; + // because the `until` parameter is inclusive, we must decrement by 1 in order to return the correct number of lines + const until = timestampFromLineNumber(logLinesToReturn - 1); + + const input = Readable.from(logLines); + const expected = Buffer.from( + logLines.slice(0, logLinesToReturn).join(""), + "utf8" + ); + + const filteredStream = filterLogsStream({ input, until }); + + const result = await new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + filteredStream + .on("data", chunk => chunks.push(chunk)) + .on("end", () => resolve(Buffer.concat(chunks))) + .on("error", reject); + }); + + assert( + result.equals(expected), + `filterLogsStream() didn't correctly return first ${logLinesToReturn} lines from the input log stream. Expected ${expected.length} bytes. Got ${result.length} bytes` + ); + }); + }); + + describe("getLogsStream()", () => { + it("must be tested", () => { + throw new Error("todo: implement getLogsStream() tests"); + }); + }); +}); From 978aab19d92cf458c552bf18ae2d1607e9027ab6 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 24 Feb 2023 11:46:43 +1300 Subject: [PATCH 34/36] Fix issue where --follow fails with unhelpful error message, if the log file does not exist --- src/packages/cli/src/cli.ts | 6 ++++-- src/packages/cli/src/logs-stream.ts | 13 ++----------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/packages/cli/src/cli.ts b/src/packages/cli/src/cli.ts index ae3ad3d4f3..76cf387665 100644 --- a/src/packages/cli/src/cli.ts +++ b/src/packages/cli/src/cli.ts @@ -189,8 +189,10 @@ if (argv.action === "start") { stream.on("error", err => { if ((err as NodeJS.ErrnoException).code === "ENOENT") { - console.log( - `No logs are available for ${instanceName}.\nTry calling some RPC methods.` + console.error( + `No logs found for ${porscheColor( + instanceName + )}. The log file may have been deleted.\n\nYou may need to restart the instance.` ); } }); diff --git a/src/packages/cli/src/logs-stream.ts b/src/packages/cli/src/logs-stream.ts index 3db5f06929..cae2a50fe5 100644 --- a/src/packages/cli/src/logs-stream.ts +++ b/src/packages/cli/src/logs-stream.ts @@ -107,19 +107,10 @@ export function createFollowReadStream(filename: string): Readable { watcher.close(); createStream(); }); - }); + }) + .on("error", err => followStream.emit("error", err)); } createStream(); return followStream; } - -function readFromBuffers(buffers: Buffer[], size?: number) { - const entireBuffer = Buffer.concat(buffers.splice(0, buffers.length)); - if (size == undefined || size <= entireBuffer.length) { - return entireBuffer; - } else { - buffers.push(entireBuffer.slice(size + 1)); - return entireBuffer.slice(0, size); - } -} From 5b9a55b0880effe1932729cd02dc889e52fe98e3 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 10 Mar 2023 15:55:08 +1300 Subject: [PATCH 35/36] Add logs-stream.fixture.log and improve related test, add tests for getLogsStream() --- src/packages/cli/src/logs-stream.ts | 5 +- src/packages/cli/tests/logs-stream.test.ts | 241 ++++++++++++++++----- src/packages/cli/tests/logs.fixture.log | 13 ++ 3 files changed, 203 insertions(+), 56 deletions(-) create mode 100644 src/packages/cli/tests/logs.fixture.log diff --git a/src/packages/cli/src/logs-stream.ts b/src/packages/cli/src/logs-stream.ts index cae2a50fe5..6ff064c8bf 100644 --- a/src/packages/cli/src/logs-stream.ts +++ b/src/packages/cli/src/logs-stream.ts @@ -16,7 +16,7 @@ export function getLogsStream( if (options.follow) { logsStream = createFollowReadStream(path); } else { - logsStream = createReadStream(path, { encoding: "utf8" }); + logsStream = createReadStream(path); } if (options.since != null || options.until != null) { return filterLogsStream({ @@ -90,8 +90,7 @@ export function createFollowReadStream(filename: string): Readable { function createStream() { directFileStream = createReadStream(filename, { - start: currentSize, - encoding: "utf8" + start: currentSize }) .on("data", data => { currentSize += data.length; diff --git a/src/packages/cli/tests/logs-stream.test.ts b/src/packages/cli/tests/logs-stream.test.ts index a870271277..8ddbb89df3 100644 --- a/src/packages/cli/tests/logs-stream.test.ts +++ b/src/packages/cli/tests/logs-stream.test.ts @@ -8,11 +8,21 @@ import { Readable } from "stream"; import { appendFile, writeFile } from "fs/promises"; import { readFileSync } from "fs"; +function readFromStream(stream: Readable): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + stream + .on("data", chunk => chunks.push(chunk)) + .on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))) + .on("error", reject); + }); +} + +const fixturePath = "./tests/logs.fixture.log"; +const fixtureContents = readFileSync(fixturePath, "utf8"); + describe("logs-stream", () => { - const fixturePath = "./tests/logs.fixture.log"; describe("createFollowReadStream()", () => { - const fixtureContents = readFileSync(fixturePath, "utf8"); - it("should load all of the data from the file", async () => { const logStream = createFollowReadStream(fixturePath); @@ -36,36 +46,39 @@ describe("logs-stream", () => { const logStream = createFollowReadStream(fixturePath); - // don't `await`, because we need to write the file back to it's original state - const loadingLogs = await new Promise((resolve, reject) => { - const logLines: Buffer[] = []; + try { + const logsReadAfterEOF = await new Promise( + (resolve, reject) => { + const logLines: Buffer[] = []; - logStream - // start reading log lines immediately, otherwise the file contents are buffered - .on("data", () => {}) - .once("eof", () => { logStream - .on("data", data => logLines.push(data)) + // start reading log lines immediately, otherwise the file contents are buffered + .on("data", () => {}) + // we wait until eof, so that we can ignore everything that's already in the file .once("eof", () => { - logStream.destroy(); - const logs = Buffer.concat(logLines).toString("utf8"); - resolve(logs); - }); - appendFile(fixturePath, newLogLine); - }) - .on("error", reject); - }); + logStream + .on("data", data => logLines.push(data)) + .once("eof", () => { + const logs = Buffer.concat(logLines).toString("utf8"); + resolve(logs); + }); + appendFile(fixturePath, newLogLine); + }) + .on("error", reject); + } + ); - try { - assert.deepStrictEqual(await loadingLogs, newLogLine); + assert.deepStrictEqual(logsReadAfterEOF, newLogLine); } finally { + logStream.destroy(); + // rewrite the contents back to the fixture file, removing the additional data that we appended writeFile(fixturePath, fixtureContents); } }); }); describe("filterLogsStream()", () => { - // First log stamped at epoch + // First log stamped at "epoch" const epoch = Date.parse("2020-01-01 00:00:00 UTC"); // subsequent logs are each incremented by 1 minute const timestampFromLineNumber = i => epoch + i * 60000; @@ -91,24 +104,16 @@ describe("logs-stream", () => { const since = timestampFromLineNumber(logLinesToSkip); const input = Readable.from(logLines); - const expected = Buffer.from( - logLines.slice(logLinesToSkip).join(""), - "utf8" - ); + const expected = logLines.slice(logLinesToSkip).join(""); const filteredStream = filterLogsStream({ input, since }); - const result = await new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - filteredStream - .on("data", chunk => chunks.push(chunk)) - .on("end", () => resolve(Buffer.concat(chunks))) - .on("error", reject); - }); + const result = await readFromStream(filteredStream); - assert( - result.equals(expected), - `filterLogsStream() didn't correctly skip first ${logLinesToSkip} lines from the input log stream. Expected ${expected.length} bytes. Got ${result.length} bytes` + assert.strictEqual( + result, + expected, + `filterLogsStream() didn't correctly skip first ${logLinesToSkip} lines from the input log stream. Expected ${expected.length} bytes. Got ${result.length} bytes.` ); }); @@ -118,31 +123,161 @@ describe("logs-stream", () => { const until = timestampFromLineNumber(logLinesToReturn - 1); const input = Readable.from(logLines); - const expected = Buffer.from( - logLines.slice(0, logLinesToReturn).join(""), - "utf8" - ); + const expected = logLines.slice(0, logLinesToReturn).join(""); const filteredStream = filterLogsStream({ input, until }); + const result = await readFromStream(filteredStream); - const result = await new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - filteredStream - .on("data", chunk => chunks.push(chunk)) - .on("end", () => resolve(Buffer.concat(chunks))) - .on("error", reject); - }); - - assert( - result.equals(expected), - `filterLogsStream() didn't correctly return first ${logLinesToReturn} lines from the input log stream. Expected ${expected.length} bytes. Got ${result.length} bytes` + assert.strictEqual( + result, + expected, + `filterLogsStream() didn't correctly return first ${logLinesToReturn} lines from the input log stream. Expected ${expected.length} bytes. Got ${result.length} bytes.` ); }); }); describe("getLogsStream()", () => { - it("must be tested", () => { - throw new Error("todo: implement getLogsStream() tests"); + it("should read the specified file", async () => { + const logsStream = getLogsStream(fixturePath, {}); + const result = await readFromStream(logsStream); + logsStream.destroy(); + + assert.strictEqual(result, fixtureContents); + }); + + it("should filter the specified date range", async () => { + const fixtureLines = fixtureContents.split("\n"); + const skipFromFront = 2; + const skipFromBack = 2; + + const matchingLines = fixtureLines.slice( + skipFromFront, + fixtureLines.length - skipFromBack - 1 // -1 because 0-based index + ); + + const since = Date.parse(matchingLines[0].slice(0, 24)); + const until = Date.parse( + matchingLines[matchingLines.length - 1].slice(0, 24) + ); + + const logsStream = getLogsStream(fixturePath, { + since, + until + }); + + const result = await readFromStream(logsStream); + logsStream.destroy(); + + assert.strictEqual( + result, + matchingLines.join("\n") + "\n", + `expected only long lines since ${new Date( + since + ).toISOString()} and until ${new Date(until).toISOString()}` + ); + }); + + it("should follow the specified file", async () => { + const newLogLine = `${new Date().toISOString()} new log line\n`; + + const logStream = getLogsStream(fixturePath, { + follow: true + }); + + try { + const logsReadAfterEOF = await new Promise( + (resolve, reject) => { + const logLines: Buffer[] = []; + + logStream + // start reading log lines immediately, otherwise the file contents are buffered + .on("data", () => {}) + // we wait until eof, so that we can ignore everything that's already in the file + .once("eof", () => { + logStream + .on("data", data => logLines.push(data)) + .once("eof", () => { + const logs = Buffer.concat(logLines).toString("utf8"); + resolve(logs); + }); + appendFile(fixturePath, newLogLine); + }) + .on("error", reject); + } + ); + + assert.deepStrictEqual(logsReadAfterEOF, newLogLine); + } finally { + logStream.destroy(); + // rewrite the contents back to the fixture file, removing the additional data that we appended + writeFile(fixturePath, fixtureContents); + } + }); + + it("should follow the specified file, returning the filtered results", async () => { + const fixtureLines = fixtureContents.split("\n"); + const skipFromFront = 2; + const skipFromBack = 2; + + const matchingLines = fixtureLines.slice( + skipFromFront, + fixtureLines.length - skipFromBack - 1 // -1 because 0-based index + ); + + const since = Date.parse(matchingLines[0].slice(0, 24)); + const until = Date.parse( + matchingLines[matchingLines.length - 1].slice(0, 24) + ); + + const tooEarlyLogLine = `${new Date( + since - 10 + ).toISOString()} non-matching log line\n`; + + const matchingLogLine = `${new Date( + since + ).toISOString()} matching log line\n`; + + const tooLateLogLine = `${new Date( + until + 10 + ).toISOString()} non-matching log line\n`; + + const logStream = getLogsStream(fixturePath, { + since, + until, + follow: true + }); + + try { + const logsReadAfterEOF = await new Promise( + (resolve, reject) => { + const logLines: Buffer[] = []; + + logStream + // start reading log lines immediately, otherwise the file contents are buffered + .on("data", () => {}) + // we wait until eof, so that we can ignore everything that's already in the file + .once("eof", () => { + logStream + .on("data", data => logLines.push(data)) + .once("eof", () => { + const logs = Buffer.concat(logLines).toString("utf8"); + resolve(logs); + }); + appendFile( + fixturePath, + [tooEarlyLogLine, matchingLogLine, tooLateLogLine].join("\n") + ); + }) + .on("error", reject); + } + ); + + assert.deepStrictEqual(logsReadAfterEOF, matchingLogLine); + } finally { + logStream.destroy(); + // rewrite the contents back to the fixture file, removing the additional data that we appended + writeFile(fixturePath, fixtureContents); + } }); }); }); diff --git a/src/packages/cli/tests/logs.fixture.log b/src/packages/cli/tests/logs.fixture.log new file mode 100644 index 0000000000..b01e4671e7 --- /dev/null +++ b/src/packages/cli/tests/logs.fixture.log @@ -0,0 +1,13 @@ +2023-03-10T09:00:01.000Z truffle_machine: Starting new batch of truffles. +2023-03-10T09:00:02.000Z truffle_machine: Added 2 cups of cocoa powder to the mixing bowl. +2023-03-10T09:00:03.000Z truffle_machine: Added 1 cup of sugar to the mixing bowl. +2023-03-10T09:00:04.000Z truffle_machine: Added 1 cup of cream to the mixing bowl. +2023-03-10T09:00:05.000Z truffle_machine: Mixed ingredients for 15 minutes. +2023-03-10T09:15:05.000Z truffle_machine: Finished mixing ingredients. +2023-03-10T09:15:06.000Z truffle_machine: Shaped mixture into truffles and placed in fridge to cool. +2023-03-10T09:30:06.000Z ganache_machine: Starting new batch of ganache. +2023-03-10T09:30:07.000Z ganache_machine: Added 3 cups of dark chocolate chips to the melting pot. +2023-03-10T09:30:08.000Z ganache_machine: Added 1 cup of heavy cream to the melting pot. +2023-03-10T09:30:09.000Z ganache_machine: Melted ingredients and mixed for 5 minutes. +2023-03-10T09:35:09.000Z ganache_machine: Finished mixing ingredients. +2023-03-10T09:35:10.000Z ganache_machine: Poured ganache into molds and placed in fridge to cool. From 89ec342d65957f20d6bdddc272df7f68b3a3f64e Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 10 Mar 2023 17:01:59 +1300 Subject: [PATCH 36/36] Improve help text for 'ganache instances logs' --- src/packages/cli/src/args.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/packages/cli/src/args.ts b/src/packages/cli/src/args.ts index 4f4cac25a4..78bb7f0015 100644 --- a/src/packages/cli/src/args.ts +++ b/src/packages/cli/src/args.ts @@ -274,23 +274,32 @@ export default function ( } ) .command( - "logs ", - "fetch logs for the instance specified by ", + chalk( + `logs [--follow] [--since ] [--until ]` + ), + highlight( + "Fetch logs for the instance specified by \n\n" + + " can be a linux timestamp e.g., `759927600000`,\n" + + "an ISO formatted timestamp `1994-01-30T11:00:00.000Z`\n" + + "or relative e.g., `60 seconds`." + ), logsArgs => { logsArgs.positional("name", { type: "string" }); logsArgs .options("follow", { type: "boolean", alias: ["f"], - description: "continue streaming the instances logs" + description: "Continue streaming the instance's logs" }) - .options("since", { + .options("since ", { type: "string", - alias: ["s"] + alias: ["s"], + description: highlight("Show logs since ") }) - .options("until", { + .options("until ", { type: "string", - alias: ["u"] + alias: ["u"], + description: highlight("Show logs up until ") }); }, logsArgs => {