• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2023 The Pigweed Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14
15import { expect } from '@open-wc/testing';
16import { spy, match } from 'sinon';
17import { LogSource } from '../src/log-source';
18import { BrowserLogSource } from '../src/custom/browser-log-source';
19import { Severity } from '../src/shared/interfaces';
20
21describe('log-source', () => {
22  let logSourceA, logSourceB;
23  const logEntry = {
24    severity: 'INFO',
25    timestamp: new Date(Date.now()),
26    fields: [{ key: 'message', value: 'Log message' }],
27  };
28
29  beforeEach(() => {
30    logSourceA = new LogSource('Log Source A');
31    logSourceB = new LogSource('Log Source B');
32  });
33
34  afterEach(() => {
35    logSourceA = null;
36    logSourceB = null;
37  });
38
39  it('emits events to registered listeners', () => {
40    const eventType = 'log-entry';
41    let receivedData = null;
42
43    const listener = (event) => {
44      receivedData = event.data;
45    };
46
47    logSourceA.addEventListener(eventType, listener);
48    logSourceA.publishLogEntry(logEntry);
49
50    expect(receivedData).to.equal(logEntry);
51  });
52
53  it("logs aren't dropped at high read frequencies", async () => {
54    const numLogs = 10;
55    const logEntries = [];
56    const eventType = 'log-entry';
57    const listener = () => {
58      // Simulate a slow listener
59      return new Promise((resolve) => {
60        setTimeout(() => {
61          resolve();
62        }, 100);
63      });
64    };
65
66    logSourceA.addEventListener(eventType, listener);
67
68    const emittedLogs = [];
69
70    for (let i = 0; i < numLogs; i++) {
71      logEntries.push(logEntry);
72
73      await logSourceA.publishLogEntry(logEntry);
74      emittedLogs.push(logEntry);
75    }
76
77    await new Promise((resolve) => setTimeout(resolve, 200));
78
79    expect(emittedLogs).to.deep.equal(logEntries);
80  });
81
82  it('throws an error for incorrect log entry structure', async () => {
83    const incorrectLogEntry = {
84      fields: [{ key: 'message', value: 'Log entry without timestamp' }],
85    };
86
87    try {
88      await logSourceA.publishLogEntry(incorrectLogEntry);
89    } catch (error) {
90      expect(error.message).to.equal('Invalid log entry structure');
91    }
92  });
93});
94
95describe('browser-log-source', () => {
96  let browserLogSource;
97  let originalConsoleMethods;
98
99  beforeEach(() => {
100    originalConsoleMethods = {
101      log: console.log,
102      info: console.info,
103      warn: console.warn,
104      error: console.error,
105      debug: console.debug,
106    };
107    browserLogSource = new BrowserLogSource();
108    browserLogSource.start();
109    browserLogSource.publishLogEntry = spy();
110  });
111
112  afterEach(() => {
113    browserLogSource.stop();
114
115    console.log = originalConsoleMethods.log;
116    console.info = originalConsoleMethods.info;
117    console.warn = originalConsoleMethods.warn;
118    console.error = originalConsoleMethods.error;
119    console.debug = originalConsoleMethods.debug;
120  });
121
122  it('captures and formats console.log messages with substitutions correctly', () => {
123    browserLogSource.publishLogEntry.resetHistory();
124
125    console.log("Hello, %s. You've called me %d times.", 'Alice', 5);
126    const expectedMessage = "Hello, Alice. You've called me 5 times.";
127
128    expect(browserLogSource.publishLogEntry.calledOnce).to.be.true;
129
130    const callArgs = browserLogSource.publishLogEntry.getCall(0).args[0];
131    expect(callArgs.severity).to.equal(Severity.INFO);
132
133    const messageField = callArgs.fields.find(
134      (field) => field.key === 'message',
135    );
136    expect(messageField).to.exist;
137    expect(messageField.value).to.equal(expectedMessage);
138  });
139
140  ['log', 'info', 'warn', 'error', 'debug'].forEach((method) => {
141    it(`captures and formats console.${method} messages`, () => {
142      const expectedSeverity = mapMethodToSeverity(method);
143      console[method]('Test message (%s)', method);
144      expect(browserLogSource.publishLogEntry).to.have.been.calledWithMatch({
145        timestamp: match.instanceOf(Date),
146        severity: expectedSeverity,
147        fields: [
148          { key: 'severity', value: expectedSeverity },
149          { key: 'time', value: match.typeOf('string') },
150          { key: 'message', value: `Test message (${method})` },
151          { key: 'file', value: 'log-source.test.js:143' },
152        ],
153      });
154    });
155  });
156
157  function mapMethodToSeverity(method) {
158    switch (method) {
159      case 'log':
160      case 'info':
161        return Severity.INFO;
162      case 'warn':
163        return Severity.WARNING;
164      case 'error':
165        return Severity.ERROR;
166      case 'debug':
167        return Severity.DEBUG;
168      default:
169        return Severity.INFO;
170    }
171  }
172
173  it('captures and formats multiple arguments correctly', () => {
174    console.log('This is a test', 42, { type: 'answer' });
175
176    const expectedMessage = 'This is a test 42 {"type":"answer"}';
177
178    expect(browserLogSource.publishLogEntry.calledOnce).to.be.true;
179    const callArgs = browserLogSource.publishLogEntry.getCall(0).args[0];
180    expect(callArgs.severity).to.equal(Severity.INFO);
181
182    const messageField = callArgs.fields.find(
183      (field) => field.key === 'message',
184    );
185    expect(messageField).to.exist;
186    expect(messageField.value).to.equal(expectedMessage);
187  });
188
189  it('restores original console methods after stop is called', () => {
190    browserLogSource.stop();
191    expect(console.log).to.equal(originalConsoleMethods.log);
192    expect(console.info).to.equal(originalConsoleMethods.info);
193    expect(console.warn).to.equal(originalConsoleMethods.warn);
194    expect(console.error).to.equal(originalConsoleMethods.error);
195    expect(console.debug).to.equal(originalConsoleMethods.debug);
196  });
197});
198