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 { Level } from '../src/shared/interfaces'; 20 21describe('log-source', () => { 22 let logSourceA, logSourceB; 23 const logEntry = { 24 level: '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 it('converts severity fields to level', async () => { 95 const severityLogEntry = { 96 severity: 'INFO', 97 timestamp: new Date(Date.now()), 98 fields: [ 99 { key: 'message', value: 'Log message' }, 100 { key: 'severity', value: 'INFO' }, 101 ], 102 }; 103 104 const eventType = 'log-entry'; 105 let receivedData = null; 106 107 const listener = (event) => { 108 receivedData = event.data; 109 }; 110 logSourceA.addEventListener(eventType, listener); 111 logSourceA.publishLogEntry(severityLogEntry); 112 expect(receivedData.severity).to.equal(undefined); 113 expect(receivedData.level).to.equal('INFO'); 114 expect(receivedData.fields[2].key).to.equal('level'); 115 }); 116}); 117 118describe('browser-log-source', () => { 119 let browserLogSource; 120 let originalConsoleMethods; 121 122 beforeEach(() => { 123 originalConsoleMethods = { 124 log: console.log, 125 info: console.info, 126 warn: console.warn, 127 error: console.error, 128 debug: console.debug, 129 }; 130 browserLogSource = new BrowserLogSource(); 131 browserLogSource.start(); 132 browserLogSource.publishLogEntry = spy(); 133 }); 134 135 afterEach(() => { 136 browserLogSource.stop(); 137 138 console.log = originalConsoleMethods.log; 139 console.info = originalConsoleMethods.info; 140 console.warn = originalConsoleMethods.warn; 141 console.error = originalConsoleMethods.error; 142 console.debug = originalConsoleMethods.debug; 143 }); 144 145 it('captures and formats console.log messages with substitutions correctly', () => { 146 browserLogSource.publishLogEntry.resetHistory(); 147 148 console.log("Hello, %s. You've called me %d times.", 'Alice', 5); 149 const expectedMessage = "Hello, Alice. You've called me 5 times."; 150 151 expect(browserLogSource.publishLogEntry.calledOnce).to.be.true; 152 153 const callArgs = browserLogSource.publishLogEntry.getCall(0).args[0]; 154 expect(callArgs.level).to.equal(Level.INFO); 155 156 const messageField = callArgs.fields.find( 157 (field) => field.key === 'message', 158 ); 159 expect(messageField).to.exist; 160 expect(messageField.value).to.equal(expectedMessage); 161 }); 162 163 ['log', 'info', 'warn', 'error', 'debug'].forEach((method) => { 164 it(`captures and formats console.${method} messages`, () => { 165 const expectedLevel = mapMethodToLevel(method); 166 167 // For test, log-source.test.js:XXX needs to match next line number 168 console[method]('Test message (%s)', method); 169 expect(browserLogSource.publishLogEntry).to.have.been.calledWithMatch({ 170 timestamp: match.instanceOf(Date), 171 level: expectedLevel, 172 fields: [ 173 { key: 'level', value: expectedLevel }, 174 { key: 'time', value: match.typeOf('string') }, 175 { key: 'message', value: `Test message (${method})` }, 176 { key: 'file', value: 'log-source.test.js:168' }, 177 ], 178 }); 179 }); 180 }); 181 182 function mapMethodToLevel(method) { 183 switch (method) { 184 case 'log': 185 case 'info': 186 return Level.INFO; 187 case 'warn': 188 return Level.WARNING; 189 case 'error': 190 return Level.ERROR; 191 case 'debug': 192 return Level.DEBUG; 193 default: 194 return Level.INFO; 195 } 196 } 197 198 it('captures and formats multiple arguments correctly', () => { 199 console.log('This is a test', 42, { type: 'answer' }); 200 201 const expectedMessage = 'This is a test 42 {"type":"answer"}'; 202 203 expect(browserLogSource.publishLogEntry.calledOnce).to.be.true; 204 const callArgs = browserLogSource.publishLogEntry.getCall(0).args[0]; 205 expect(callArgs.level).to.equal(Level.INFO); 206 207 const messageField = callArgs.fields.find( 208 (field) => field.key === 'message', 209 ); 210 expect(messageField).to.exist; 211 expect(messageField.value).to.equal(expectedMessage); 212 }); 213 214 it('restores original console methods after stop is called', () => { 215 browserLogSource.stop(); 216 expect(console.log).to.equal(originalConsoleMethods.log); 217 expect(console.info).to.equal(originalConsoleMethods.info); 218 expect(console.warn).to.equal(originalConsoleMethods.warn); 219 expect(console.error).to.equal(originalConsoleMethods.error); 220 expect(console.debug).to.equal(originalConsoleMethods.debug); 221 }); 222}); 223