/// /** * Helpers and utilities for the Mocha tests. */ import * as path from 'path'; import {assert} from 'chai'; import {LogMessage, KeymanCompilerError, redirectLogMessagesTo, resetLogMessageHandler, LogLevel} from '../../dist/errors'; export interface CompilationResult { hasSyntaxError: boolean; modelConstructorName: string; error: Error; exportedModel: object; } /** * Builds a path to the fixture with the given path components. * * e.g., makePathToFixture('example.qaa.trivial') * e.g., makePathToFixture('example.qaa.trivial', 'model.ts') * * @param components One or more path components. */ export function makePathToFixture(...components: string[]): string { return path.join(__dirname, '..', 'fixtures', ...components); } /** * Given source code of a model (as produced by the compiler), this evaluates * it! * * That way, you can determine if the source code was syntactically-valid, * whether it raise an exception during construction, and you can even inspect * the resultant LexicalModel for yourself! * * @param code */ export function compileModelSourceCode(code: string) { assert.typeOf(code, 'string'); let error: Error | null = null; let exportedModel: object | null = null; let hasSyntaxError = false; let modelConstructorName: string | null = null; let module: ModuleType | null = null; try { module = new Function('LMLayerWorker', 'models', 'wordBreakers', code) as ModuleType; } catch (err) { if (err instanceof SyntaxError) { hasSyntaxError = true; } else { throw err; } } // Module had some sort of syntax error: if (module === null) { return { error, exportedModel, hasSyntaxError, modelConstructorName }; } let fakeLMLayerWorker = { loadModel(model: object) { exportedModel = model; } }; // We expect the compiled model to reach into the `models` namespace, // and get some sort of model constructor. Problem is, we don't know // what model constructors are defined! // This proxy allows us to intercept // models.* // meaning that when the compiled code tries to do this: // // new models.WhateverModel('foo', 'bar') // // ...we can intercept the name "WhateverModel" and assign it to // `modelConstructorName`. let modelsNamespace = new Proxy({}, { get(_target, property) { if (typeof property !== 'string') throw new Error(`Don't know how to handle non-string property: ${String(property)}`); return class DummyModel { constructor() { modelConstructorName = property as string; } } } }); // We expect the compiled model to reach into the `wordBreaker` namespace, // and maybe get a function. Problem is, we don't know // what functions are available! // This proxy allows us to intercept // wordBreakers.* // meaning that when the compiled code tries to do this: // // wordBreaker["someWordBreaker"] // // ...we can intercept the name "someWordBreaker". let wordBreakerNamespace = new Proxy({}, { get(_target, property) { if (typeof property !== 'string') throw new Error(`Don't know how to handle non-string property: ${String(property)}`); return function dummyWordBreaker() { } } }); try { module(fakeLMLayerWorker, modelsNamespace, wordBreakerNamespace); } catch (err) { error = err; } return { error, exportedModel, hasSyntaxError, modelConstructorName }; } /** * Enables one to query log messages after they have been logged. */ export class LogHoarder { private messages: LogMessage[] = []; /** * Get rid of all log messages seen. */ clear() { this.messages = []; } /** * Hoards a log message for later perusal. */ handleLog(log: LogMessage) { this.messages.push(log); } /** * Has an error message with this code been witnessed? */ hasSeenCode(code: KeymanCompilerError): boolean { return Boolean(this.messages.find(log => log.code === code)); } /** * Have any warnings been logged? */ hasSeenWarnings(): boolean { return this.messages .filter(log => log.level === LogLevel.CERR_WARNING) .length > 0; } /** * Overrides the global log handler, allowing one to browse log messages * later. * * Remember to uninstall the log handler afterwards! */ install(): this { redirectLogMessagesTo(this.handleLog.bind(this)); return this; } /** * Return the log message handler to its default. * * Note: You MUST uninstall the hoarder after use! * It's recommended you put this in an afterEach() callback. */ uninstall() { resetLogMessageHandler() } } type ModuleType = (a: LMLayerWorker, b: ModelsNamespace, c: WordBreakersNamespace) => any; interface LMLayerWorker { loadModel(model: object): void; } type ModelsNamespace = object; type WordBreakersNamespace = object;