|
@@ -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();
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|