diff --git a/garden-service/src/cli/cli.ts b/garden-service/src/cli/cli.ts index 1e2ecf242c..3b2774e0fe 100644 --- a/garden-service/src/cli/cli.ts +++ b/garden-service/src/cli/cli.ts @@ -7,12 +7,12 @@ */ import * as sywac from "sywac" -import { intersection, merge, range } from "lodash" +import { intersection, merge } from "lodash" import { resolve } from "path" import { safeDump } from "js-yaml" import { coreCommands } from "../commands/commands" import { DeepPrimitiveMap } from "../config/common" -import { getEnumKeys, shutdown, sleep } from "../util/util" +import { shutdown, sleep } from "../util/util" import { BooleanParameter, ChoicesParameter, @@ -22,7 +22,7 @@ import { Parameter, StringParameter, } from "../commands/base" -import { GardenError, InternalError, PluginError, toGardenError } from "../exceptions" +import { GardenError, PluginError, toGardenError } from "../exceptions" import { Garden, GardenOpts } from "../garden" import { getLogger, Logger, LoggerType } from "../logger/logger" import { LogLevel } from "../logger/log-node" @@ -43,6 +43,8 @@ import { prepareOptionConfig, styleConfig, getPackageVersion, + getLogLevelChoices, + parseLogLevel, } from "./helpers" import { GardenConfig } from "../config/base" import { defaultEnvironments } from "../config/project" @@ -87,11 +89,6 @@ export const MOCK_CONFIG: GardenConfig = { }, } -const getLogLevelNames = () => getEnumKeys(LogLevel) -const getNumericLogLevels = () => range(getLogLevelNames().length) -// Allow string or numeric log levels as CLI choices -const getLogLevelChoices = () => [...getLogLevelNames(), ...getNumericLogLevels().map(String)] - export const GLOBAL_OPTIONS = { root: new StringParameter({ alias: "r", @@ -125,23 +122,6 @@ export const GLOBAL_OPTIONS = { }), } -function parseLogLevel(level: string): LogLevel { - let lvl: LogLevel - const parsed = parseInt(level, 10) - if (parsed) { - lvl = parsed - } else { - lvl = LogLevel[level] - } - if (!getNumericLogLevels().includes(lvl)) { - throw new InternalError( - `Unexpected log level, expected one of ${getLogLevelChoices().join(", ")}, got ${level}`, - {}, - ) - } - return lvl -} - function initLogger({ level, logEnabled, loggerType, emoji }: { level: LogLevel, logEnabled: boolean, loggerType: LoggerType, emoji: boolean, }) { @@ -402,7 +382,7 @@ export class GardenCli { } export async function run(): Promise { - let code + let code: number | undefined try { const cli = new GardenCli() const result = await cli.parse() diff --git a/garden-service/src/cli/helpers.ts b/garden-service/src/cli/helpers.ts index 519ec2d423..fb5f74b828 100644 --- a/garden-service/src/cli/helpers.ts +++ b/garden-service/src/cli/helpers.ts @@ -7,7 +7,7 @@ */ import chalk from "chalk" -import { difference, flatten, reduce } from "lodash" +import { difference, flatten, range, reduce } from "lodash" import { ChoicesParameter, ParameterValues, @@ -16,6 +16,8 @@ import { import { InternalError, } from "../exceptions" +import { LogLevel } from "../logger/log-node" +import { getEnumKeys } from "../util/util" // Parameter types T which map between the Parameter class and the Sywac cli library. // In case we add types that aren't supported natively by Sywac, see: http://sywac.io/docs/sync-config.html#custom @@ -108,6 +110,30 @@ export function getArgSynopsis(key: string, param: Parameter) { return param.required ? `<${key}>` : `[${key}]` } +const getLogLevelNames = () => getEnumKeys(LogLevel) +const getNumericLogLevels = () => range(getLogLevelNames().length) +// Allow string or numeric log levels as CLI choices +export const getLogLevelChoices = () => [...getLogLevelNames(), ...getNumericLogLevels().map(String)] + +export function parseLogLevel(level: string): LogLevel { + let lvl: LogLevel + const parsed = parseInt(level, 10) + // Level is numeric + if (parsed || parsed === 0) { + lvl = parsed + // Level is a string + } else { + lvl = LogLevel[level] + } + if (!getNumericLogLevels().includes(lvl)) { + throw new InternalError( + `Unexpected log level, expected one of ${getLogLevelChoices().join(", ")}, got ${level}`, + {}, + ) + } + return lvl +} + export function prepareArgConfig(param: Parameter) { return { desc: param.help, diff --git a/garden-service/src/logger/log-entry.ts b/garden-service/src/logger/log-entry.ts index 34aef24d4c..fb0e0acdef 100644 --- a/garden-service/src/logger/log-entry.ts +++ b/garden-service/src/logger/log-entry.ts @@ -127,7 +127,7 @@ export class LogEntry extends LogNode { this.root.onGraphChange(node) } - placeholder(level: LogLevel = LogLevel.info, param? : CreateParam): LogEntry { + placeholder(level: LogLevel = LogLevel.info, param?: CreateParam): LogEntry { // Ensure placeholder child entries align with parent context const indent = Math.max((this.opts.indent || 0) - 1, - 1) return this.appendNode(level, { ...resolveParam(param), indent }) diff --git a/garden-service/src/logger/logger.ts b/garden-service/src/logger/logger.ts index f9de6b5fb3..e367df05ba 100644 --- a/garden-service/src/logger/logger.ts +++ b/garden-service/src/logger/logger.ts @@ -14,6 +14,7 @@ import { InternalError, ParameterError } from "../exceptions" import { LogLevel } from "./log-node" import { FancyTerminalWriter } from "./writers/fancy-terminal-writer" import { BasicTerminalWriter } from "./writers/basic-terminal-writer" +import { parseLogLevel } from "../cli/helpers" export enum LoggerType { quiet = "quiet", @@ -64,7 +65,18 @@ export class Logger extends LogNode { let instance: Logger - // If GARDEN_LOGGER_TYPE env variable is set it takes precedence over the config param + // GARDEN_LOG_LEVEL env variable takes precedence over the config param + if (process.env.GARDEN_LOG_LEVEL) { + try { + config.level = parseLogLevel(process.env.GARDEN_LOG_LEVEL) + } catch (err) { + // Log warning if level invalid but continue process. + // Using console logger since Garden logger hasn't been intialised. + console.warn("Warning:", err.message) + } + } + + // GARDEN_LOGGER_TYPE env variable takes precedence over the config param if (process.env.GARDEN_LOGGER_TYPE) { const loggerType = LoggerType[process.env.GARDEN_LOGGER_TYPE] diff --git a/garden-service/test/logger/log-entry.ts b/garden-service/test/logger/log-entry.ts new file mode 100644 index 0000000000..40a2e5cf1e --- /dev/null +++ b/garden-service/test/logger/log-entry.ts @@ -0,0 +1,113 @@ +import { expect } from "chai" + +import { getLogger } from "../../src/logger/logger" + +const logger = getLogger() + +beforeEach(() => { + (logger).children = [] +}) + +describe("LogEntry", () => { + it("should dedent placeholder log entries", () => { + const ph1 = logger.placeholder() + const ph2 = ph1.placeholder() + const nonEmpty = ph1.info("foo") + const nested = nonEmpty.info("foo") + const nestedPh = nested.placeholder() + const indents = [ + ph1.opts.indent, + ph2.opts.indent, + nonEmpty.opts.indent, + nested.opts.indent, + nestedPh.opts.indent, + ] + expect(indents).to.eql([-1, -1, 0, 1, 0]) + }) + it("should indent nested log entries", () => { + const entry = logger.info("hello") + const nested = entry.info("nested") + const deepNested = nested.info("deep nested") + const deepDeepNested = deepNested.info("deep deep inside") + const deepDeepPh = deepDeepNested.placeholder() + const deepDeepNested2 = deepDeepPh.info("") + const indents = [ + entry.opts.indent, + nested.opts.indent, + deepNested.opts.indent, + deepDeepNested.opts.indent, + deepDeepPh.opts.indent, + deepDeepNested2.opts.indent, + ] + expect(indents).to.eql([undefined, 1, 2, 3, 2, 3]) + }) + context("preserveLevel is set to true", () => { + it("should create a log entry whose children inherit the parent level", () => { + const verbose = logger.verbose({ preserveLevel: true }) + const error = verbose.error("") + const silly = verbose.silly("") + const deepError = error.error("") + const deepSillyError = silly.error("") + const deepSillySilly = silly.silly("") + const levels = [ + verbose.warn("").level, + verbose.info("").level, + verbose.verbose("").level, + verbose.debug("").level, + verbose.silly("").level, + deepError.level, + deepSillyError.level, + deepSillySilly.level, + ] + expect(levels).to.eql([3, 3, 3, 4, 5, 3, 3, 5]) + }) + }) + describe("setState", () => { + it("should update entry state and optionally append new msg to previous msg", () => { + const entry = logger.info("") + 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("setState", () => { + it("should preserve status", () => { + const entry = logger.info("") + entry.setSuccess() + entry.setState("change text") + expect(entry.opts.status).to.equal("success") + }) + }) + describe("setDone", () => { + it("should update entry state and set status to done", () => { + const entry = logger.info("") + entry.setDone() + expect(entry.opts.status).to.equal("done") + }) + }) + describe("setSuccess", () => { + it("should update entry state and set status and symbol to success", () => { + const entry = logger.info("") + entry.setSuccess() + expect(entry.opts.status).to.equal("success") + expect(entry.opts.symbol).to.equal("success") + }) + }) + describe("setError", () => { + it("should update entry state and set status and symbol to error", () => { + const entry = logger.info("") + entry.setError() + expect(entry.opts.status).to.equal("error") + expect(entry.opts.symbol).to.equal("error") + }) + }) + describe("setWarn", () => { + it("should update entry state and set status and symbol to warn", () => { + const entry = logger.info("") + entry.setWarn() + expect(entry.opts.status).to.equal("warn") + expect(entry.opts.symbol).to.equal("warning") + }) + }) +}) diff --git a/garden-service/test/logger/log-node.ts b/garden-service/test/logger/log-node.ts new file mode 100644 index 0000000000..78efeb45c7 --- /dev/null +++ b/garden-service/test/logger/log-node.ts @@ -0,0 +1,31 @@ +import { expect } from "chai" +import { getLogger } from "../../src/logger/logger" + +const logger = getLogger() + +beforeEach(() => { + (logger).children = [] +}) + +describe("LogNode", () => { + describe("appendNode", () => { + it("should add new child entries to the respective node", () => { + logger.error("error") + logger.warn("warn") + logger.info("info") + logger.verbose("verbose") + logger.debug("debug") + logger.silly("silly") + + const prevLength = logger.children.length + const entry = logger.children[0] + const nested = entry.info("nested") + const deepNested = nested.info("deep") + + expect(logger.children[0].children).to.have.lengthOf(1) + expect(logger.children[0].children[0]).to.eql(nested) + expect(logger.children[0].children[0].children[0]).to.eql(deepNested) + expect(logger.children).to.have.lengthOf(prevLength) + }) + }) +}) diff --git a/garden-service/test/logger/logger.ts b/garden-service/test/logger/logger.ts new file mode 100644 index 0000000000..bff3c1443b --- /dev/null +++ b/garden-service/test/logger/logger.ts @@ -0,0 +1,60 @@ +import { expect } from "chai" + +import { LogLevel } from "../../src/logger/log-node" +import { getLogger } from "../../src/logger/logger" + +const logger = getLogger() + +beforeEach(() => { + (logger).children = [] +}) + +describe("Logger", () => { + describe("findById", () => { + it("should return the first log entry with a matching id and undefined otherwise", () => { + 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", () => { + 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.id)).to.eql(["a", "b"]) + expect(sEmpty).to.eql([]) + }) + }) + + describe("getLogEntries", () => { + it("should return an ordered list of log entries", () => { + logger.error("error") + logger.warn("warn") + logger.info("info") + logger.verbose("verbose") + logger.debug("debug") + logger.silly("silly") + + const entries = logger.getLogEntries() + const levels = entries.map(e => e.level) + + expect(entries).to.have.lengthOf(6) + expect(levels).to.eql([ + LogLevel.error, + LogLevel.warn, + LogLevel.info, + LogLevel.verbose, + LogLevel.debug, + LogLevel.silly, + ]) + }) + }) + +}) diff --git a/garden-service/test/logger/renderers.ts b/garden-service/test/logger/renderers.ts new file mode 100644 index 0000000000..6d9f6e1805 --- /dev/null +++ b/garden-service/test/logger/renderers.ts @@ -0,0 +1,59 @@ +import { expect } from "chai" + +import { getLogger } from "../../src/logger/logger" +import { + renderMsg, + msgStyle, + errorStyle, + formatForTerminal, +} from "../../src/logger/renderers" + +const logger = getLogger() + +beforeEach(() => { + (logger).children = [] +}) + +describe("renderers", () => { + describe("renderMsg", () => { + it("should return an empty string if the entry is empty", () => { + const entry = logger.placeholder() + expect(renderMsg(entry)).to.equal("") + }) + it("should render the message with the message style", () => { + const entry = logger.info({ msg: "hello message" }) + expect(renderMsg(entry)).to.equal(msgStyle("hello message")) + }) + it("should join an array of messages with an arrow symbol and render with the message style", () => { + const entry = logger.info({ msg: ["message a", "message b"] }) + expect(renderMsg(entry)).to.equal(msgStyle("message a") + msgStyle(" → ") + msgStyle("message b")) + }) + it("should render the message without styles if the entry is from an intercepted stream", () => { + const entry = logger.info({ fromStdStream: true, msg: "hello stream" }) + expect(renderMsg(entry)).to.equal("hello stream") + }) + it("should join an array of messages and render without styles if the entry is from an intercepted stream", () => { + const entry = logger.info({ fromStdStream: true, msg: ["stream a", "stream b"] }) + expect(renderMsg(entry)).to.equal("stream a stream b") + }) + it("should render the message with the error style if the entry has error status", () => { + const entry = logger.info({ msg: "hello error", status: "error" }) + expect(renderMsg(entry)).to.equal(errorStyle("hello error")) + }) + it("should join an array of messages with an arrow symbol and render with the error style" + + " if the entry has error status", () => { + const entry = logger.info({ msg: ["error a", "error b"], status: "error" }) + expect(renderMsg(entry)).to.equal(errorStyle("error a") + errorStyle(" → ") + errorStyle("error b")) + }) + }) + describe("formatForTerminal", () => { + it("should return the entry as a formatted string with a new line character", () => { + const entry = logger.info("") + expect(formatForTerminal(entry)).to.equal("\n") + }) + it("should return an empty string without a new line if the entry is empty", () => { + const entry = logger.placeholder() + expect(formatForTerminal(entry)).to.equal("") + }) + }) +}) diff --git a/garden-service/test/logger/util.ts b/garden-service/test/logger/util.ts new file mode 100644 index 0000000000..9a1682cf31 --- /dev/null +++ b/garden-service/test/logger/util.ts @@ -0,0 +1,40 @@ +import { expect } from "chai" + +import { getChildNodes } from "../../src/logger/util" + +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]) + }) + }) +}) diff --git a/garden-service/test/logger/writers/basic-terminal-writer.ts b/garden-service/test/logger/writers/basic-terminal-writer.ts new file mode 100644 index 0000000000..ce3847334c --- /dev/null +++ b/garden-service/test/logger/writers/basic-terminal-writer.ts @@ -0,0 +1,47 @@ +import { expect } from "chai" + +import { LogLevel } from "../../../src/logger/log-node" +import { BasicTerminalWriter } from "../../../src/logger/writers/basic-terminal-writer" +import { getLogger } from "../../../src/logger/logger" +import { formatForTerminal } from "../../../src/logger/renderers" + +const logger = getLogger() + +beforeEach(() => { + (logger).children = [] +}) + +describe("BasicTerminalWriter", () => { + describe("render", () => { + it("should return a formatted message if level is geq than entry level", () => { + const writer = new BasicTerminalWriter() + const entry = logger.info("hello logger") + const out = writer.render(entry, logger) + expect(out).to.eql(formatForTerminal(entry)) + }) + it("should return a new line if message is an empty string", () => { + const writer = new BasicTerminalWriter() + const entry = logger.info("") + const out = writer.render(entry, logger) + expect(out).to.eql("\n") + }) + it("should return null if entry level is geq to writer level", () => { + const writer = new BasicTerminalWriter() + const entry = logger.verbose("abc") + const out = writer.render(entry, logger) + expect(out).to.eql(null) + }) + it("should override root level if level is set", () => { + const writer = new BasicTerminalWriter({ level: LogLevel.verbose }) + const entry = logger.verbose("") + const out = writer.render(entry, logger) + expect(out).to.eql("\n") + }) + it("should return an empty string if entry is empty", () => { + const writer = new BasicTerminalWriter() + const entry = logger.placeholder() + const out = writer.render(entry, logger) + expect(out).to.eql("") + }) + }) +}) diff --git a/garden-service/test/logger/writers/fancy-terminal-writer.ts b/garden-service/test/logger/writers/fancy-terminal-writer.ts new file mode 100644 index 0000000000..f926547856 --- /dev/null +++ b/garden-service/test/logger/writers/fancy-terminal-writer.ts @@ -0,0 +1,48 @@ +import { expect } from "chai" + +import { LogLevel } from "../../../src/logger/log-node" +import { FancyTerminalWriter } from "../../../src/logger/writers/fancy-terminal-writer" +import { getLogger } from "../../../src/logger/logger" + +const logger = getLogger() + +beforeEach(() => { + (logger).children = [] +}) + +describe("FancyTerminalWriter", () => { + describe("toTerminalEntries", () => { + const writer = new FancyTerminalWriter() + const verboseWriter = new FancyTerminalWriter({ level: LogLevel.verbose }) + writer.stop() + verboseWriter.stop() + it("should map a LogNode into an array of entries with line numbers and spinner positions", () => { + logger.info("1 line") // 0 + logger.info("2 lines\n") // 1 + logger.info("1 line") // 3 + logger.info("3 lines\n\n") // 4 + const spinner = logger.info({ msg: "spinner", status: "active" }) // 7 + spinner.info({ msg: "nested spinner", status: "active" }) // 8 + const terminalEntries = writer.toTerminalEntries(logger) + const lineNumbers = terminalEntries.map(e => e.lineNumber) + const spinners = terminalEntries.filter(e => !!e.spinnerCoords).map(e => e.spinnerCoords) + expect(lineNumbers).to.eql([0, 1, 3, 4, 7, 8]) + expect(spinners).to.eql([[0, 7], [3, 8]]) + }) + it("should override root level if level is set", () => { + const entry = logger.verbose("") + const terminalEntries = verboseWriter.toTerminalEntries(logger) + expect(terminalEntries[0].key).to.eql(entry.key) + }) + it("should skip entry if entry level is geq to writer level", () => { + logger.verbose("") + const terminalEntries = writer.toTerminalEntries(logger) + expect(terminalEntries).to.eql([]) + }) + it("should skip entry if entry is empty", () => { + logger.placeholder() + const terminalEntries = writer.toTerminalEntries(logger) + expect(terminalEntries).to.eql([]) + }) + }) +}) diff --git a/garden-service/test/logger/writers/file-writer.ts b/garden-service/test/logger/writers/file-writer.ts new file mode 100644 index 0000000000..d538808553 --- /dev/null +++ b/garden-service/test/logger/writers/file-writer.ts @@ -0,0 +1,32 @@ +import { expect } from "chai" +import chalk from "chalk" +import * as stripAnsi from "strip-ansi" + +import { LogLevel } from "../../../src/logger/log-node" +import { getLogger } from "../../../src/logger/logger" +import { renderError } from "../../../src/logger/renderers" +import { render } from "../../../src/logger/writers/file-writer" + +const logger = getLogger() + +beforeEach(() => { + (logger).children = [] +}) + +describe("FileWriter", () => { + describe("render", () => { + it("should render message without ansi characters", () => { + const entry = logger.info(chalk.red("hello")) + expect(render(LogLevel.info, entry)).to.equal("hello") + }) + it("should render error message if entry level is error", () => { + const entry = logger.error("error") + const expectedOutput = stripAnsi(renderError(entry)) + expect(render(LogLevel.info, entry)).to.equal(expectedOutput) + }) + it("should return null if entry level is geq to writer level", () => { + const entry = logger.silly("silly") + expect(render(LogLevel.info, entry)).to.equal(null) + }) + }) +}) diff --git a/garden-service/test/src/cli/helpers.ts b/garden-service/test/src/cli/helpers.ts index 73642dd0ef..59dc597bf9 100644 --- a/garden-service/test/src/cli/helpers.ts +++ b/garden-service/test/src/cli/helpers.ts @@ -7,13 +7,40 @@ */ import { expect } from "chai" -import { getPackageVersion } from "../../../src/cli/helpers" +import { getPackageVersion, parseLogLevel, getLogLevelChoices } from "../../../src/cli/helpers" +import { expectError } from "../../helpers" describe("helpers", () => { + const validLogLevels = ["error", "warn", "info", "verbose", "debug", "silly", "0", "1", "2", "3", "4", "5"] + describe("getPackageVersion", () => { - it("returns the version in package.json", async () => { + it("should return the version in package.json", async () => { const version = require("../../../package.json").version expect(getPackageVersion()).to.eq(version) }) }) + + describe("getLogLevelChoices", () => { + it("should return all valid log levels as strings", async () => { + const choices = getLogLevelChoices().sort() + const sorted = [...validLogLevels].sort() + expect(choices).to.eql(sorted) + }) + }) + + describe("parseLogLevel", () => { + it("should return a level integer if valid", async () => { + const parsed = validLogLevels.map(el => parseLogLevel(el)) + expect(parsed).to.eql([0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5]) + }) + it("should throw if level is not valid", async () => { + await expectError(() => parseLogLevel("banana"), "internal") + }) + it("should throw if level is not valid", async () => { + await expectError(() => parseLogLevel("-1"), "internal") + }) + it("should throw if level is not valid", async () => { + await expectError(() => parseLogLevel(""), "internal") + }) + }) }) diff --git a/garden-service/test/src/logger.ts b/garden-service/test/src/logger.ts deleted file mode 100644 index b31e36a8a9..0000000000 --- a/garden-service/test/src/logger.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { expect } from "chai" -import chalk from "chalk" -import * as stripAnsi from "strip-ansi" - -import { LogLevel } from "../../src/logger/log-node" -import { BasicTerminalWriter } from "../../src/logger/writers/basic-terminal-writer" -import { FancyTerminalWriter } from "../../src/logger/writers/fancy-terminal-writer" -import { getLogger } from "../../src/logger/logger" -import { getChildNodes } from "../../src/logger/util" -import { - renderMsg, - msgStyle, - errorStyle, - formatForTerminal, - renderError, -} from "../../src/logger/renderers" -import { render } from "../../src/logger/writers/file-writer" - -const logger = getLogger() - -beforeEach(() => { - (logger).children = [] -}) - -describe("LogNode", () => { - - describe("findById", () => { - it("should return the first log entry with a matching id and undefined otherwise", () => { - 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", () => { - 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.id)).to.eql(["a", "b"]) - expect(sEmpty).to.eql([]) - }) - }) - - describe("appendNode", () => { - it("should add new child entries to the respective node", () => { - logger.error("error") - logger.warn("warn") - logger.info("info") - logger.verbose("verbose") - logger.debug("debug") - logger.silly("silly") - - const prevLength = logger.children.length - const entry = logger.children[0] - const nested = entry.info("nested") - const deepNested = nested.info("deep") - - expect(logger.children[0].children).to.have.lengthOf(1) - expect(logger.children[0].children[0]).to.eql(nested) - expect(logger.children[0].children[0].children[0]).to.eql(deepNested) - expect(logger.children).to.have.lengthOf(prevLength) - }) - }) - -}) - -describe("RootLogNode", () => { - describe("getLogEntries", () => { - it("should return an ordered list of log entries", () => { - logger.error("error") - logger.warn("warn") - logger.info("info") - logger.verbose("verbose") - logger.debug("debug") - logger.silly("silly") - - const entries = logger.getLogEntries() - const levels = entries.map(e => e.level) - - expect(entries).to.have.lengthOf(6) - expect(levels).to.eql([ - LogLevel.error, - LogLevel.warn, - LogLevel.info, - LogLevel.verbose, - LogLevel.debug, - LogLevel.silly, - ]) - }) - }) - -}) - -describe("Writers", () => { - describe("BasicTerminalWriter", () => { - describe("render", () => { - it("should return a formatted message if level is geq than entry level", () => { - const writer = new BasicTerminalWriter() - const entry = logger.info("hello logger") - const out = writer.render(entry, logger) - expect(out).to.eql(formatForTerminal(entry)) - }) - it("should return a new line if message is an empty string", () => { - const writer = new BasicTerminalWriter() - const entry = logger.info("") - const out = writer.render(entry, logger) - expect(out).to.eql("\n") - }) - it("should return null if entry level is geq to writer level", () => { - const writer = new BasicTerminalWriter() - const entry = logger.verbose("abc") - const out = writer.render(entry, logger) - expect(out).to.eql(null) - }) - it("should override root level if level is set", () => { - const writer = new BasicTerminalWriter({ level: LogLevel.verbose }) - const entry = logger.verbose("") - const out = writer.render(entry, logger) - expect(out).to.eql("\n") - }) - it("should return an empty string if entry is empty", () => { - const writer = new BasicTerminalWriter() - const entry = logger.placeholder() - const out = writer.render(entry, logger) - expect(out).to.eql("") - }) - }) - }) - - describe("FancyTerminalWriter", () => { - describe("toTerminalEntries", () => { - const writer = new FancyTerminalWriter() - const verboseWriter = new FancyTerminalWriter({ level: LogLevel.verbose }) - writer.stop() - verboseWriter.stop() - it("should map a LogNode into an array of entries with line numbers and spinner positions", () => { - logger.info("1 line") // 0 - logger.info("2 lines\n") // 1 - logger.info("1 line") // 3 - logger.info("3 lines\n\n") // 4 - const spinner = logger.info({ msg: "spinner", status: "active" }) // 7 - spinner.info({ msg: "nested spinner", status: "active" }) // 8 - const terminalEntries = writer.toTerminalEntries(logger) - const lineNumbers = terminalEntries.map(e => e.lineNumber) - const spinners = terminalEntries.filter(e => !!e.spinnerCoords).map(e => e.spinnerCoords) - expect(lineNumbers).to.eql([0, 1, 3, 4, 7, 8]) - expect(spinners).to.eql([[0, 7], [3, 8]]) - }) - it("should override root level if level is set", () => { - const entry = logger.verbose("") - const terminalEntries = verboseWriter.toTerminalEntries(logger) - expect(terminalEntries[0].key).to.eql(entry.key) - }) - it("should skip entry if entry level is geq to writer level", () => { - logger.verbose("") - const terminalEntries = writer.toTerminalEntries(logger) - expect(terminalEntries).to.eql([]) - }) - it("should skip entry if entry is empty", () => { - logger.placeholder() - const terminalEntries = writer.toTerminalEntries(logger) - expect(terminalEntries).to.eql([]) - }) - }) - }) - - describe("FileWriter", () => { - describe("render", () => { - it("should render message without ansi characters", () => { - const entry = logger.info(chalk.red("hello")) - expect(render(LogLevel.info, entry)).to.equal("hello") - }) - it("should render error message if entry level is error", () => { - const entry = logger.error("error") - const expectedOutput = stripAnsi(renderError(entry)) - expect(render(LogLevel.info, entry)).to.equal(expectedOutput) - }) - it("should return null if entry level is geq to writer level", () => { - const entry = logger.silly("silly") - expect(render(LogLevel.info, entry)).to.equal(null) - }) - }) - }) -}) - -describe("LogEntry", () => { - it("should dedent placeholder log entries", () => { - const ph1 = logger.placeholder() - const ph2 = ph1.placeholder() - const nonEmpty = ph1.info("foo") - const nested = nonEmpty.info("foo") - const nestedPh = nested.placeholder() - const indents = [ - ph1.opts.indent, - ph2.opts.indent, - nonEmpty.opts.indent, - nested.opts.indent, - nestedPh.opts.indent, - ] - expect(indents).to.eql([-1, -1, 0, 1, 0]) - }) - it("should indent nested log entries", () => { - const entry = logger.info("hello") - const nested = entry.info("nested") - const deepNested = nested.info("deep nested") - const deepDeepNested = deepNested.info("deep deep inside") - const deepDeepPh = deepDeepNested.placeholder() - const deepDeepNested2 = deepDeepPh.info("") - const indents = [ - entry.opts.indent, - nested.opts.indent, - deepNested.opts.indent, - deepDeepNested.opts.indent, - deepDeepPh.opts.indent, - deepDeepNested2.opts.indent, - ] - expect(indents).to.eql([undefined, 1, 2, 3, 2, 3]) - }) - it("should create a log entry with level geq to its parent", () => { - const verbose = logger.verbose("") - const levels = [ - verbose.error("").level, - verbose.warn("").level, - verbose.info("").level, - verbose.verbose("").level, - verbose.debug("").level, - verbose.silly("").level, - ] - expect(levels).to.eql([3, 3, 3, 3, 4, 5]) - }) - describe("setState", () => { - it("should update entry state and optionally append new msg to previous msg", () => { - const entry = logger.info("") - 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("setState", () => { - it("should preserve status", () => { - const entry = logger.info("") - entry.setSuccess() - entry.setState("change text") - expect(entry.opts.status).to.equal("success") - }) - }) - describe("setDone", () => { - it("should update entry state and set status to done", () => { - const entry = logger.info("") - entry.setDone() - expect(entry.opts.status).to.equal("done") - }) - }) - describe("setSuccess", () => { - it("should update entry state and set status and symbol to success", () => { - const entry = logger.info("") - entry.setSuccess() - expect(entry.opts.status).to.equal("success") - expect(entry.opts.symbol).to.equal("success") - }) - }) - describe("setError", () => { - it("should update entry state and set status and symbol to error", () => { - const entry = logger.info("") - entry.setError() - expect(entry.opts.status).to.equal("error") - expect(entry.opts.symbol).to.equal("error") - }) - }) - describe("setWarn", () => { - it("should update entry state and set status and symbol to warn", () => { - const entry = logger.info("") - entry.setWarn() - expect(entry.opts.status).to.equal("warn") - expect(entry.opts.symbol).to.equal("warning") - }) - }) -}) - -describe("renderers", () => { - describe("renderMsg", () => { - it("should return an empty string if the entry is empty", () => { - const entry = logger.placeholder() - expect(renderMsg(entry)).to.equal("") - }) - it("should render the message with the message style", () => { - const entry = logger.info({ msg: "hello message" }) - expect(renderMsg(entry)).to.equal(msgStyle("hello message")) - }) - it("should join an array of messages with an arrow symbol and render with the message style", () => { - const entry = logger.info({ msg: ["message a", "message b"] }) - expect(renderMsg(entry)).to.equal(msgStyle("message a") + msgStyle(" → ") + msgStyle("message b")) - }) - it("should render the message without styles if the entry is from an intercepted stream", () => { - const entry = logger.info({ fromStdStream: true, msg: "hello stream" }) - expect(renderMsg(entry)).to.equal("hello stream") - }) - it("should join an array of messages and render without styles if the entry is from an intercepted stream", () => { - const entry = logger.info({ fromStdStream: true, msg: ["stream a", "stream b"] }) - expect(renderMsg(entry)).to.equal("stream a stream b") - }) - it("should render the message with the error style if the entry has error status", () => { - const entry = logger.info({ msg: "hello error", status: "error" }) - expect(renderMsg(entry)).to.equal(errorStyle("hello error")) - }) - it("should join an array of messages with an arrow symbol and render with the error style" + - " if the entry has error status", () => { - const entry = logger.info({ msg: ["error a", "error b"], status: "error" }) - expect(renderMsg(entry)).to.equal(errorStyle("error a") + errorStyle(" → ") + errorStyle("error b")) - }) - }) - describe("formatForTerminal", () => { - it("should return the entry as a formatted string with a new line character", () => { - const entry = logger.info("") - expect(formatForTerminal(entry)).to.equal("\n") - }) - it("should return an empty string without a new line if the entry is empty", () => { - const entry = logger.placeholder() - expect(formatForTerminal(entry)).to.equal("") - }) - }) -}) - -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]) - }) - }) -})