From 814733b6458284339d468ab3755de8a92da765fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BE=C3=B3r=20Magn=C3=BAsson?= Date: Thu, 26 Apr 2018 15:01:09 +0200 Subject: [PATCH] feat: add filter and find methods to logger --- src/logger/index.ts | 15 ++- src/logger/types.ts | 1 + src/logger/util.ts | 53 +++++----- src/logger/writers.ts | 4 +- test/src/logger.ts | 222 ++++++++++++++++++++++++------------------ 5 files changed, 173 insertions(+), 122 deletions(-) diff --git a/src/logger/index.ts b/src/logger/index.ts index e4169c76f5..81910c223d 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -12,7 +12,8 @@ import chalk from "chalk" import { combine } from "./renderers" import { duration, - getChildNodes, + findLogEntry, + getChildEntries, mergeLogOpts, } from "./util" import { @@ -122,6 +123,14 @@ export abstract class LogNode { return this.addNode(LogLevel.error, { ...makeLogOpts(entryVal), entryStyle: EntryStyle.error }) } + public findById(id: string): LogEntry | void { + return findLogEntry(this, entry => entry.opts.id === id) + } + + public filterBySection(section: string): LogEntry[] { + return getChildEntries(this).filter(entry => entry.opts.section === section) + } + } export class LogEntry extends LogNode { @@ -173,7 +182,7 @@ export class LogEntry extends LogNode { // Update node and child nodes private deepSetState(opts: LogEntryOpts, status: EntryStatus): void { this.setOwnState(opts, status) - getChildNodes(this).forEach(entry => { + getChildEntries(this).forEach(entry => { if (entry.status === EntryStatus.ACTIVE) { entry.setOwnState({}, EntryStatus.DONE) } @@ -248,7 +257,7 @@ export class RootLogNode extends LogNode { } public getLogEntries(): LogEntry[] { - return getChildNodes(this).filter(entry => !entry.notOriginatedFromLogger()) + return getChildEntries(this).filter(entry => !entry.notOriginatedFromLogger()) } public header( diff --git a/src/logger/types.ts b/src/logger/types.ts index 0c21349f32..4a79af45d6 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -66,4 +66,5 @@ export interface LogEntryOpts { notOriginatedFromLogger?: boolean showDuration?: boolean error?: GardenError | Error + id?: string } diff --git a/src/logger/util.ts b/src/logger/util.ts index 2f75db5dc9..fe8618cf77 100644 --- a/src/logger/util.ts +++ b/src/logger/util.ts @@ -6,9 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { flatten } from "lodash" - import { LogEntryOpts } from "./types" +import { LogEntry, LogNode } from "." export interface Node { children: any[] @@ -16,33 +15,43 @@ export interface Node { export type LogOptsResolvers = { [K in keyof LogEntryOpts]?: Function } -// TODO Tail call optimization? -export function getNodeListFromTree(node: T): T[] { - let arr: T[] = [] - arr.push(node) - if (node.children.length === 0) { - return arr +export type ProcessNode = (node: T) => boolean + +// Assumes root node can be of different type than child nodes +function traverseChildren(node: T | U, cb: ProcessNode) { + const children = node.children + for (let idx = 0; idx < children.length; idx++) { + const proceed = cb(children[idx]) + if (!proceed) { + return + } + traverseChildren(children[idx], cb) } - return arr.concat(flatten(node.children.map(child => getNodeListFromTree(child)))) } -export function getChildNodes(node: T): T[] { - return getNodeListFromTree(node).slice(1) +export function getChildNodes(node: T | U): U[] { + let array: U[] = [] + traverseChildren(node, child => { + array.push(child) + return true + }) + return array } -export function traverseTree(root: T, visitNode: Function): void { - let stack: any[] = [] - stack.push(root) +export function getChildEntries(node: LogNode): LogEntry[] { + return getChildNodes(node) +} - while (stack.length !== 0) { - const node = stack.pop() - visitNode(node) - if (node.children.length !== 0) { - for (let i = node.children.length - 1; i >= 0; i--) { - stack.push(node.children[i]) - } +export function findLogEntry(node: LogNode, predicate: ProcessNode): LogEntry | void { + let found + traverseChildren(node, entry => { + if (predicate(entry)) { + found = entry + return false } - } + return true + }) + return found } function mergeWithResolvers(objA: any, objB: any, resolvers: any = {}) { diff --git a/src/logger/writers.ts b/src/logger/writers.ts index 130e6eb021..bbcde95e11 100644 --- a/src/logger/writers.ts +++ b/src/logger/writers.ts @@ -14,7 +14,7 @@ import * as winston from "winston" import chalk from "chalk" const stripAnsi = require("strip-ansi") -import { getChildNodes, interceptStream } from "./util" +import { getChildEntries, interceptStream } from "./util" import { EntryStatus, LogLevel, @@ -253,7 +253,7 @@ export class FancyConsoleWriter extends Writer { public render(rootLogNode: RootLogNode): string[] | null { let hasActiveEntries = false const level = this.level || rootLogNode.level - const entries = getChildNodes(rootLogNode) + const entries = getChildEntries(rootLogNode) /** * This is a bit ugly for performance sake. diff --git a/test/src/logger.ts b/test/src/logger.ts index bb1aa46f3b..eca8672705 100644 --- a/test/src/logger.ts +++ b/test/src/logger.ts @@ -1,24 +1,50 @@ import { expect } from "chai" import { LogLevel, EntryStatus, LogSymbolType, LoggerType } from "../../src/logger/types" -import { BasicConsoleWriter, FancyConsoleWriter } from "../../src/logger/writers" +import { BasicConsoleWriter, FancyConsoleWriter } from "../../src/logger/writers" import { RootLogNode } from "../../src/logger" import { getChildNodes } from "../../src/logger/util" -const logger = new RootLogNode({ level: LogLevel.silent }) +describe("LogNode", () => { + describe("findById", () => { + it("should return the first log entry with a matching id and undefined otherwise", () => { + const logger = new RootLogNode({ level: LogLevel.info }) + logger.info({msg: "0"}) + logger.info({msg: "a1", id: "a"}) + logger.info({msg: "a2", id: "a"}) + expect(logger.findById("a")["opts"]["msg"]).to.eql("a1") + expect(logger.findById("z")).to.be.undefined + }) + }) + + describe("filterBySection", () => { + it("should return an array of all entries with the matching section name", () => { + const logger = new RootLogNode({ level: LogLevel.info }) + logger.info({section: "s0"}) + logger.info({section: "s1", id: "a"}) + logger.info({section: "s2"}) + logger.info({section: "s1", id: "b"}) + const s1 = logger.filterBySection("s1") + const sEmpty = logger.filterBySection("s99") + expect(s1.map(entry => entry.opts.id)).to.eql(["a", "b"]) + expect(sEmpty).to.eql([]) + }) + }) -logger.error("error") -logger.warn("warn") -logger.info("info") -logger.verbose("verbose") -logger.debug("debug") -logger.silly("silly") +}) describe("RootLogNode", () => { + const logger = new RootLogNode({ level: LogLevel.info }) + + logger.error("error") + logger.warn("warn") + logger.info("info") + logger.verbose("verbose") + logger.debug("debug") + logger.silly("silly") describe("getLogEntries", () => { it("should return an ordered list of log entries", () => { - const entries = logger.getLogEntries() const levels = entries.map(e => e.level) @@ -49,105 +75,111 @@ describe("RootLogNode", () => { }) }) - describe("BasicConsoleWriter.render", () => { - it("should return a string if log level is geq than entry level", () => { - const writer = new BasicConsoleWriter({ level: LogLevel.silent }) - const logger2 = new RootLogNode({ level: LogLevel.silent }) - const entry = logger2.info("") - const out1 = writer.render(entry, logger2) - writer.level = LogLevel.verbose - const out2 = writer.render(entry, logger2) - - expect(out1).to.be.a("null") - expect(out2).to.be.a("string") - }) +}) + +describe("BasicConsoleWriter.render", () => { + it("should return a string if log level is geq than entry level", () => { + const writer = new BasicConsoleWriter({ level: LogLevel.silent }) + const logger = new RootLogNode({ level: LogLevel.silent }) + const entry = logger.info("") + const out1 = writer.render(entry, logger) + writer.level = LogLevel.verbose + const out2 = writer.render(entry, logger) + + expect(out1).to.be.a("null") + expect(out2).to.be.a("string") }) +}) - describe("FancyConsoleWriter.render", () => { - it("should return an array of strings if log level is geq than respective entry level", () => { - const writer = new FancyConsoleWriter({ level: LogLevel.silent }) - const logger3 = new RootLogNode({level: LogLevel.silent}) - const entry = logger3.info("") - const out1 = writer.render(logger3) - writer.level = LogLevel.verbose - const out2 = writer.render(logger3) +describe("FancyConsoleWriter.render", () => { + it("should return an array of strings if log level is geq than respective entry level", () => { + const writer = new FancyConsoleWriter({ level: LogLevel.silent }) + const logger = new RootLogNode({ level: LogLevel.silent }) + const entry = logger.info("") + const out1 = writer.render(logger) + writer.level = LogLevel.verbose + const out2 = writer.render(logger) - writer.stop() + writer.stop() - expect(out1).to.be.a("null") - expect(out2).to.be.an("array").of.length(1) - }) + expect(out1).to.be.a("null") + expect(out2).to.be.an("array").of.length(1) }) +}) - describe("LogEntry", () => { - const entry = logger.children[0] - describe("setState", () => { - it("should update entry state and optionally append new msg to previous msg", () => { - entry.setState("new") - expect(entry["opts"]["msg"]).to.equal("new") - entry.setState({ msg: "new2", append: true }) - expect(entry["opts"]["msg"]).to.eql(["new", "new2"]) - }) +describe("LogEntry", () => { + const logger = new RootLogNode({ level: LogLevel.info }) + const entry = logger.info("") + describe("setState", () => { + it("should update entry state and optionally append new msg to previous msg", () => { + entry.setState("new") + expect(entry["opts"]["msg"]).to.equal("new") + entry.setState({ msg: "new2", append: true }) + expect(entry["opts"]["msg"]).to.eql(["new", "new2"]) }) - describe("setDone", () => { - it("should update entry state and set status to done", () => { - entry.setDone() - expect(entry["status"]).to.equal(EntryStatus.DONE) - }) + }) + describe("setDone", () => { + it("should update entry state and set status to done", () => { + entry.setDone() + expect(entry["status"]).to.equal(EntryStatus.DONE) }) - describe("setSuccess", () => { - it("should update entry state and set status and symbol to success", () => { - entry.setSuccess() - expect(entry["status"]).to.equal(EntryStatus.SUCCESS) - expect(entry["opts"]["symbol"]).to.equal(LogSymbolType.success) - }) + }) + describe("setSuccess", () => { + it("should update entry state and set status and symbol to success", () => { + entry.setSuccess() + expect(entry["status"]).to.equal(EntryStatus.SUCCESS) + expect(entry["opts"]["symbol"]).to.equal(LogSymbolType.success) }) - describe("setError", () => { - it("should update entry state and set status and symbol to error", () => { - entry.setError() - expect(entry["status"]).to.equal(EntryStatus.ERROR) - expect(entry["opts"]["symbol"]).to.equal(LogSymbolType.error) - }) + }) + describe("setError", () => { + it("should update entry state and set status and symbol to error", () => { + entry.setError() + expect(entry["status"]).to.equal(EntryStatus.ERROR) + expect(entry["opts"]["symbol"]).to.equal(LogSymbolType.error) }) - describe("setWarn", () => { - it("should update entry state and set status and symbol to warn", () => { - entry.setWarn() - expect(entry["status"]).to.equal(EntryStatus.WARN) - expect(entry["opts"]["symbol"]).to.equal(LogSymbolType.warn) - }) + }) + describe("setWarn", () => { + it("should update entry state and set status and symbol to warn", () => { + entry.setWarn() + expect(entry["status"]).to.equal(EntryStatus.WARN) + expect(entry["opts"]["symbol"]).to.equal(LogSymbolType.warn) }) }) +}) - describe("util", () => { - describe("getChildNodes", () => { - it("should convert an n-ary tree into an ordered list of nodes", () => { - const graph = { - children: [ - { - children: [ - { - children: [ - { children: [], id: 3 }, - ], - id: 2, - }, - { children: [], id: 4 }, - { children: [], id: 5 }, - ], - id: 1, - }, - { - children: [ - - ], - id: 6, - }, - ], - id: "root", - } - const nodeList = getChildNodes(graph) - expect(nodeList.map(n => n.id)).to.eql([1, 2, 3, 4, 5, 6]) - }) +describe("util", () => { + describe("getChildNodes", () => { + it("should convert an n-ary tree into an ordered list of child nodes (skipping the root)", () => { + interface TestNode { + children: any[] + id: number + } + const graph = { + children: [ + { + children: [ + { + children: [ + { children: [], id: 3 }, + ], + id: 2, + }, + { children: [], id: 4 }, + { children: [], id: 5 }, + ], + id: 1, + }, + { + children: [ + + ], + id: 6, + }, + ], + id: 0, + } + const nodeList = getChildNodes(graph) + expect(nodeList.map(n => n.id)).to.eql([1, 2, 3, 4, 5, 6]) }) }) })