import { Arr } from '@ephox/katamari'; export enum TestLogEntryState { Original, Started, Finished } export interface TestLogEntry { message: string; entries: TestLogEntry[ ]; state: TestLogEntryState; trace: any; } export interface TestLogs { history: TestLogEntry[ ]; } const DISABLE_LOGGING = false; // Pop level needs to change the parent. This would be so much easier with zippers. const modifyStartedEntryTo = (entries: TestLogEntry[], f): TestLogEntry[] => Arr.last(entries).fold( () => entries, (lastEntry) => { // If the last entry has started, and has entries, if (lastEntry.state === TestLogEntryState.Started) { return Arr.last(lastEntry.entries).fold( // We have no entries, so just modify us () => entries.slice(0, entries.length - 1).concat([ f(lastEntry) ]), // Great name! (lastEntryLastEntry) => { if (lastEntryLastEntry.state === TestLogEntryState.Started) { // Need to keep going. return entries.slice(0, entries.length - 1).concat([{ message: lastEntry.message, state: lastEntry.state, trace: lastEntry.trace, entries: modifyStartedEntryTo(lastEntry.entries, f) }]); } else { // We have no further nesting, so just modify us return entries.slice(0, entries.length - 1).concat([ f(lastEntry) ]); } } ); } else { return entries.slice(0, entries.length - 1).concat([ f(lastEntry) ]); } } ); const modifyStartedEntry = (logs: TestLogs, f): TestLogs => ({ history: modifyStartedEntryTo(logs.history, f) }); const modifyLastEntryTo = (entries: TestLogEntry[], f): TestLogEntry[] => // Consider consolidating with modifyStartedEntryTo Arr.last(entries).fold( () => [ f({ message: 'Unknown', state: TestLogEntryState.Original, entries: [ ], trace: null }) ], (lastEntry) => { if (lastEntry.state === TestLogEntryState.Started) { return entries.slice(0, entries.length - 1).concat([{ message: lastEntry.message, state: lastEntry.state, entries: modifyLastEntryTo(lastEntry.entries, f), trace: lastEntry.trace }]); } else { return entries.slice(0, entries.length - 1).concat([ f(lastEntry) ]); } } ); const modifyLastEntry = (logs: TestLogs, f): TestLogs => ({ history: modifyLastEntryTo(logs.history, f) }); // Determine if we are inside a subentry const addLogEntryTo = (entries: TestLogEntry[], newEntry: TestLogEntry): TestLogEntry[] => { if (entries.length === 0) { return [ newEntry ]; } else { const lastEntry = entries[entries.length - 1]; if (lastEntry.state === TestLogEntryState.Started) { const before = entries.slice(0, entries.length - 1); const newLastEntries = addLogEntryTo(lastEntry.entries, newEntry); return before.concat([{ message: lastEntry.message, entries: newLastEntries, state: lastEntry.state, trace: lastEntry.trace }]); } else { return entries.concat([ newEntry ]); } } }; // TODO: Make a Cons List for efficiency export const addLogEntry = (logs: TestLogs, message: string): TestLogs => { if (DISABLE_LOGGING) { return logs; } const newEntry = { message, trace: null, state: TestLogEntryState.Original, entries: [ ] }; return { history: addLogEntryTo(logs.history, newEntry) }; }; export const pushLogLevel = (logs: TestLogs): TestLogs => { if (DISABLE_LOGGING) { return logs; } return modifyLastEntry(logs, (entry) => ({ message: entry.message, entries: entry.entries, state: TestLogEntryState.Started, trace: entry.trace })); }; export const popLogLevel = (logs: TestLogs): TestLogs => { if (DISABLE_LOGGING) { return logs; } return modifyStartedEntry(logs, (entry) => ({ message: entry.message, entries: entry.entries, state: TestLogEntryState.Finished, trace: entry.trace })); }; export const addStackTrace = (logs: TestLogs, err: { stack: any }): TestLogs => { if (DISABLE_LOGGING) { return logs; } return modifyLastEntry(logs, (entry) => ({ message: entry.message, trace: err.stack, state: entry.state, entries: entry.entries })); }; const init = (): TestLogs => initLogsWith([ ]); const initLogsWith = (history: TestLogEntry[]): TestLogs => ({ history }); const concat = (logs1: TestLogs, logs2: TestLogs): TestLogs => initLogsWith(Arr.flatten([ logs1.history, logs2.history ])); const single = (message: string): TestLogs => addLogEntry(init(), message); export const TestLogs = { getOrInit: (logs?: TestLogs): TestLogs => logs !== undefined ? logs : initLogsWith([ ]), init, initLogsWith, concat, addLogEntry, single };