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