Browse Source

Add initial tests for log console.

Jason Grout 5 years ago
parent
commit
e465b50fc6

+ 24 - 5
packages/logconsole/src/logger.ts

@@ -42,11 +42,23 @@ type ILogOutput = nbformat.IOutput & {
   level: FullLogLevel;
 };
 
+export interface ILogOutputModel extends IOutputModel {
+  /**
+   * Date & time when output is logged.
+   */
+  readonly timestamp: Date;
+
+  /**
+   * Log level
+   */
+  readonly level: FullLogLevel;
+}
+
 /**
  * Log Output Model with timestamp which provides
  * item information for Output Area Model.
  */
-export class LogOutputModel extends OutputModel {
+export class LogOutputModel extends OutputModel implements ILogOutputModel {
   /**
    * Construct a LogOutputModel.
    *
@@ -55,19 +67,19 @@ export class LogOutputModel extends OutputModel {
   constructor(options: LogOutputModel.IOptions) {
     super(options);
 
-    this.timestamp = new Date(options.value.timestamp as number);
+    this.timestamp = new Date(options.value.timestamp);
     this.level = options.value.level;
   }
 
   /**
    * Date & time when output is logged.
    */
-  timestamp: Date = null;
+  readonly timestamp: Date = null;
 
   /**
    * Log level
    */
-  level: FullLogLevel;
+  readonly level: FullLogLevel;
 }
 
 /**
@@ -119,6 +131,13 @@ export class LoggerOutputAreaModel extends OutputAreaModel
     return this.length;
   }
 
+  /**
+   * Get an item at the specified index.
+   */
+  get(index: number): ILogOutputModel {
+    return super.get(index) as ILogOutputModel;
+  }
+
   /**
    * Maximum number of outputs to store in the model.
    */
@@ -198,7 +217,7 @@ export class Logger implements ILogger {
       output: {
         output_type: 'display_data',
         data: {
-          'text/html': `Log level set to ${newValue}`
+          'text/plain': `Log level set to ${newValue}`
         }
       },
       level: 'metadata'

+ 1 - 0
tests/test-logconsole/babel.config.js

@@ -0,0 +1 @@
+module.exports = require('@jupyterlab/testutils/lib/babel.config');

+ 2 - 0
tests/test-logconsole/jest.config.js

@@ -0,0 +1,2 @@
+const func = require('@jupyterlab/testutils/lib/jest-config');
+module.exports = func('logconsole', __dirname);

+ 30 - 0
tests/test-logconsole/package.json

@@ -0,0 +1,30 @@
+{
+  "name": "@jupyterlab/test-logconsole",
+  "version": "2.0.0-alpha.1",
+  "private": true,
+  "scripts": {
+    "build": "tsc -b",
+    "clean": "rimraf build && rimraf coverage",
+    "coverage": "python run.py --coverage",
+    "test": "python run.py",
+    "watch": "python run.py --debug",
+    "watch:all": "python run.py --debug --watchAll",
+    "watch:src": "tsc -b --watch"
+  },
+  "dependencies": {
+    "@jupyterlab/logconsole": "^1.0.0-alpha.1",
+    "@jupyterlab/testutils": "^2.0.0-alpha.1",
+    "@phosphor/coreutils": "^1.3.1",
+    "@phosphor/messaging": "^1.3.0",
+    "@phosphor/widgets": "^1.9.0",
+    "jest": "^24.7.1",
+    "jest-junit": "^6.3.0",
+    "ts-jest": "^24.0.2"
+  },
+  "devDependencies": {
+    "@types/chai": "^4.1.7",
+    "@types/jest": "^24.0.13",
+    "rimraf": "~2.6.2",
+    "typescript": "~3.5.1"
+  }
+}

+ 8 - 0
tests/test-logconsole/run.py

@@ -0,0 +1,8 @@
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
+import os.path as osp
+from jupyterlab.tests.test_app import run_jest
+
+if __name__ == '__main__':
+    run_jest(osp.dirname(osp.realpath(__file__)))

+ 376 - 0
tests/test-logconsole/src/logger.spec.ts

@@ -0,0 +1,376 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  Logger,
+  LoggerOutputAreaModel,
+  ILogPayload,
+  LogLevel
+} from '@jupyterlab/logconsole';
+
+import { RenderMimeRegistry } from '@jupyterlab/rendermime';
+
+import { Signal, ISignal } from '@phosphor/signaling';
+
+class SignalLogger<SENDER, ARGS> {
+  constructor(signal: ISignal<SENDER, ARGS>) {
+    signal.connect(this.slot, this);
+  }
+
+  slot(sender: SENDER, args: ARGS) {
+    this.args.push(args);
+  }
+
+  dispose() {
+    Signal.disconnectAll(this);
+  }
+  args: ARGS[] = [];
+}
+
+describe('LoggerOutputAreaModel', () => {
+  let model: LoggerOutputAreaModel;
+  beforeEach(() => {
+    model = new LoggerOutputAreaModel({ maxLength: 10 });
+  });
+  afterEach(() => {
+    model.dispose();
+  });
+
+  describe('#constructor()', () => {
+    it('should create an LoggerOutputAreaModel', () => {
+      expect(model).toBeInstanceOf(LoggerOutputAreaModel);
+    });
+
+    it('should set the max length', async () => {
+      const model = new LoggerOutputAreaModel({ maxLength: 10 });
+      expect(model.maxLength).toEqual(10);
+    });
+  });
+
+  describe('#maxLength', () => {
+    it('should set the maximum number of messages in the first-in first-out queue', () => {
+      for (let i = 0; i < 12; i++) {
+        model.add({
+          output_type: 'display_data',
+          data: { 'text/plain': i.toString() },
+          timestamp: Date.now(),
+          level: 'info'
+        });
+      }
+      expect(model.length).toEqual(10);
+      expect(model.get(0).data['text/plain']).toEqual('2');
+    });
+
+    it('setting maxLength should immediately apply and trim the message list', () => {
+      for (let i = 0; i < 12; i++) {
+        model.add({
+          output_type: 'display_data',
+          data: { 'text/plain': i.toString() },
+          timestamp: Date.now(),
+          level: 'info'
+        });
+      }
+      expect(model.maxLength).toEqual(10);
+      expect(model.length).toEqual(10);
+      model.maxLength = 5;
+      expect(model.maxLength).toEqual(5);
+      expect(model.length).toEqual(5);
+      expect(model.get(0).data['text/plain']).toEqual('7');
+    });
+  });
+});
+
+describe('Logger', () => {
+  let logger: Logger;
+  beforeEach(() => {
+    logger = new Logger({ source: 'test source', maxLength: 10 });
+  });
+
+  describe('#constructor()', () => {
+    it('should create a Logger with initial properties', () => {
+      expect(logger).toBeInstanceOf(Logger);
+      expect(logger.source).toEqual('test source');
+      expect(logger.maxLength).toEqual(10);
+    });
+  });
+
+  describe('#maxLength', () => {
+    it('should set the maximum number of messages in the first-in first-out queue', () => {
+      for (let i = 0; i < 12; i++) {
+        logger.log({ type: 'text', data: i.toString(), level: 'critical' });
+      }
+      expect(logger.length).toEqual(10);
+      expect(logger.outputAreaModel.get(0).data['text/plain']).toEqual('2');
+    });
+
+    it('setting maxLength should immediately apply and trim the message list', () => {
+      for (let i = 0; i < 12; i++) {
+        logger.log({ type: 'text', data: i.toString(), level: 'critical' });
+      }
+      const model = logger.outputAreaModel;
+      expect(logger.maxLength).toEqual(10);
+      expect(logger.length).toEqual(10);
+      logger.maxLength = 5;
+      expect(logger.maxLength).toEqual(5);
+      expect(logger.length).toEqual(5);
+      expect(model.get(0).data['text/plain']).toEqual('7');
+    });
+  });
+
+  describe('#level', () => {
+    let levels: LogLevel[] = ['critical', 'error', 'warning', 'info', 'debug'];
+    it('should default to "warning"', () => {
+      expect(logger.level).toEqual('warning');
+    });
+
+    it.each(levels)('filters for messages: %s', (level: LogLevel) => {
+      logger.level = level;
+      const messages: ILogPayload[] = levels.map(level => ({
+        type: 'text',
+        data: level,
+        level
+      }));
+      messages.forEach(m => logger.log({ ...m }));
+      const logged: string[] = [];
+      for (let i = 0; i < logger.length; i++) {
+        const msg = logger.outputAreaModel.get(i);
+        logged.push(msg.level);
+      }
+      const shouldInclude = levels.slice(0, levels.indexOf(level) + 1);
+      const shouldExclude = levels.slice(levels.indexOf(level) + 1);
+      shouldInclude.forEach(x => {
+        expect(logged).toContain(x);
+      });
+      shouldExclude.forEach(x => {
+        expect(logged).not.toContain(x);
+      });
+    });
+
+    it('logs a "metadata" level text message if changed', () => {
+      logger.level = 'info';
+      const msg = logger.outputAreaModel.get(0);
+      expect(msg.level).toBe('metadata');
+      expect(msg.data['text/plain']).toContain('info');
+    });
+
+    it('emits a stateChanged signal when changing', () => {
+      const s = new SignalLogger(logger.stateChanged);
+      logger.level = 'info';
+      expect(s.args).toEqual([
+        {
+          name: 'level',
+          oldValue: 'warning',
+          newValue: 'info'
+        }
+      ]);
+    });
+
+    it('setting to its current value has no effect', () => {
+      const s = new SignalLogger(logger.stateChanged);
+      logger.level = logger.level;
+      expect(s.args.length).toBe(0);
+      expect(logger.length).toBe(0);
+    });
+  });
+
+  describe('#length', () => {
+    it('records how many messages are stored', () => {
+      logger.log({ type: 'text', data: 'message 1', level: 'warning' });
+      logger.log({ type: 'text', data: 'message 2', level: 'warning' });
+      expect(logger.length).toBe(2);
+      logger.clear();
+      expect(logger.length).toBe(0);
+    });
+
+    it('may be less than the messages logged if messages were combined', () => {
+      logger.log({
+        type: 'output',
+        data: { output_type: 'stream', name: 'stdout', text: 'message 1' },
+        level: 'critical'
+      });
+      logger.log({
+        type: 'output',
+        data: { output_type: 'stream', name: 'stdout', text: 'message 2' },
+        level: 'critical'
+      });
+      expect(logger.length).toBe(1);
+    });
+  });
+
+  describe('#rendermime', () => {
+    it('initially is null', () => {
+      expect(logger.rendermime).toBe(null);
+    });
+
+    it('sets the rendermime attribute', () => {
+      const value = new RenderMimeRegistry();
+      logger.rendermime = value;
+      expect(logger.rendermime).toBe(value);
+    });
+    it('emits a stateChanged signal when changed', () => {
+      const oldValue = (logger.rendermime = new RenderMimeRegistry());
+      const newValue = oldValue.clone();
+      const s = new SignalLogger(logger.stateChanged);
+      logger.rendermime = newValue;
+      expect(s.args).toEqual([{ name: 'rendermime', oldValue, newValue }]);
+    });
+    it('setting to current value has no effect', () => {
+      logger.rendermime = new RenderMimeRegistry();
+      const s = new SignalLogger(logger.stateChanged);
+      logger.rendermime = logger.rendermime;
+      expect(s.args).toEqual([]);
+    });
+  });
+
+  describe('#version', () => {
+    it('starts at zero', () => {
+      expect(logger.version).toBe(0);
+    });
+
+    it('increments every time a message is logged', () => {
+      logger.log({ type: 'text', data: 'message 1', level: 'warning' });
+      logger.log({ type: 'text', data: 'message 2', level: 'warning' });
+      expect(logger.version).toBe(2);
+    });
+
+    it('increments even if messages are combined', () => {
+      logger.log({
+        type: 'output',
+        data: { output_type: 'stream', name: 'stdout', text: 'message 1' },
+        level: 'critical'
+      });
+      logger.log({
+        type: 'output',
+        data: { output_type: 'stream', name: 'stdout', text: 'message 2' },
+        level: 'critical'
+      });
+      expect(logger.length).toBe(1);
+      expect(logger.version).toBe(2);
+    });
+
+    it('does not increment on clearing messages', () => {
+      logger.log({ type: 'text', data: 'message 1', level: 'warning' });
+      logger.log({ type: 'text', data: 'message 2', level: 'warning' });
+      expect(logger.version).toBe(2);
+      logger.clear();
+      expect(logger.length).toBe(0);
+      expect(logger.version).toBe(2);
+    });
+  });
+
+  describe('#log()', () => {
+    it('logs text messages', () => {
+      logger.log({ type: 'text', data: 'message', level: 'warning' });
+      expect(logger.length).toBe(1);
+    });
+    it('logs html messages', () => {
+      logger.log({ type: 'html', data: 'message', level: 'warning' });
+      expect(logger.length).toBe(1);
+    });
+    it('logs output stream messages', () => {
+      logger.log({
+        type: 'output',
+        data: { output_type: 'stream', name: 'stdout', text: 'message' },
+        level: 'warning'
+      });
+      expect(logger.length).toBe(1);
+    });
+    it('logs display_data messages', () => {
+      logger.log({
+        type: 'output',
+        data: {
+          output_type: 'display_data',
+          data: { 'text/plain': 'message' }
+        },
+        level: 'warning'
+      });
+      expect(logger.length).toBe(1);
+    });
+    it('logs execute_result messages', () => {
+      logger.log({
+        type: 'output',
+        data: {
+          output_type: 'execute_result',
+          data: { 'text/plain': 'message', execution_count: 5 }
+        },
+        level: 'warning'
+      });
+      expect(logger.length).toBe(1);
+    });
+    it('logs error messages', () => {
+      logger.log({
+        type: 'output',
+        data: {
+          output_type: 'error',
+          ename: 'Error',
+          evalue: 'Error',
+          traceback: ['level 1', 'level 2']
+        },
+        level: 'warning'
+      });
+      expect(logger.length).toBe(1);
+    });
+    it('emits an "append" content changed signal', () => {
+      const s = new SignalLogger(logger.contentChanged);
+      logger.log({ type: 'text', data: 'message 1', level: 'warning' });
+      expect(s.args).toEqual(['append']);
+      s.dispose();
+    });
+
+    it('emits an "append" content changed signal', () => {
+      const s = new SignalLogger(logger.contentChanged);
+      logger.log({
+        type: 'output',
+        data: { output_type: 'stream', name: 'stdout', text: 'message 1' },
+        level: 'critical'
+      });
+      logger.log({
+        type: 'output',
+        data: { output_type: 'stream', name: 'stdout', text: 'message 2' },
+        level: 'critical'
+      });
+      expect(s.args).toEqual(['append', 'append']);
+      expect(logger.length).toBe(1);
+      s.dispose();
+    });
+
+    it('adds a timestamp to the message', () => {
+      const before = Date.now();
+      logger.log({ type: 'text', data: 'message 1', level: 'warning' });
+      const after = Date.now();
+      const msgTime = logger.outputAreaModel.get(0).timestamp.getTime();
+      expect(msgTime).toBeGreaterThanOrEqual(before);
+      expect(msgTime).toBeLessThanOrEqual(after);
+    });
+  });
+
+  describe('#clear()', () => {
+    it('clears messages', () => {
+      logger.log({ type: 'text', data: 'message 1', level: 'warning' });
+      logger.log({ type: 'text', data: 'message 2', level: 'warning' });
+      expect(logger.length).toBe(2);
+      logger.clear();
+      expect(logger.length).toBe(0);
+    });
+    it('emits a "clear" content changed signal', () => {
+      const s = new SignalLogger(logger.contentChanged);
+      logger.log({ type: 'text', data: 'message 1', level: 'warning' });
+      logger.clear();
+      expect(s.args).toEqual(['append', 'clear']);
+      s.dispose();
+    });
+  });
+
+  describe('#checkpoint()', () => {
+    it('adds a metadata message to the message list', () => {
+      logger.checkpoint();
+      expect(logger.outputAreaModel.get(0).level).toBe('metadata');
+    });
+    it('emits an "append" content changed signal', () => {
+      const s = new SignalLogger(logger.contentChanged);
+      logger.checkpoint();
+      expect(s.args).toEqual(['append']);
+      s.dispose();
+    });
+  });
+});

+ 18 - 0
tests/test-logconsole/tsconfig.json

@@ -0,0 +1,18 @@
+{
+  "extends": "../../tsconfigbase",
+  "compilerOptions": {
+    "outDir": "build",
+    "types": ["jest"],
+    "composite": false,
+    "rootDir": "src"
+  },
+  "include": ["src/*"],
+  "references": [
+    {
+      "path": "../../packages/logconsole"
+    },
+    {
+      "path": "../../testutils"
+    }
+  ]
+}