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