// Copyright (c) 2017, Compiler Explorer Authors // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. import path from 'path'; import {fileURLToPath} from 'url'; import {describe, expect, it} from 'vitest'; import winston from 'winston'; import {makeLogStream} from '../lib/logger.js'; import * as utils from '../lib/utils.js'; import {fs} from './utils.js'; describe('Splits lines', () => { it('handles empty input', () => { expect(utils.splitLines('')).toEqual([]); }); it('handles a single line with no newline', () => { expect(utils.splitLines('A line')).toEqual(['A line']); }); it('handles a single line with a newline', () => { expect(utils.splitLines('A line\n')).toEqual(['A line']); }); it('handles multiple lines', () => { expect(utils.splitLines('A line\nAnother line\n')).toEqual(['A line', 'Another line']); }); it('handles multiple lines ending on a non-newline', () => { expect(utils.splitLines('A line\nAnother line\nLast line')).toEqual(['A line', 'Another line', 'Last line']); }); it('handles empty lines', () => { expect(utils.splitLines('A line\n\nA line after an empty')).toEqual(['A line', '', 'A line after an empty']); }); it('handles a single empty line', () => { expect(utils.splitLines('\n')).toEqual(['']); }); it('handles multiple empty lines', () => { expect(utils.splitLines('\n\n\n')).toEqual(['', '', '']); }); it('handles \\r\\n lines', () => { expect(utils.splitLines('Some\r\nLines\r\n')).toEqual(['Some', 'Lines']); }); }); describe('Expands tabs', () => { it('leaves non-tabs alone', () => { expect(utils.expandTabs('This has no tabs at all')).toEqual('This has no tabs at all'); }); it('at beginning of line', () => { expect(utils.expandTabs('\tOne tab')).toEqual(' One tab'); expect(utils.expandTabs('\t\tTwo tabs')).toEqual(' Two tabs'); }); it('mid-line', () => { expect(utils.expandTabs('0\t1234567A')).toEqual('0 1234567A'); expect(utils.expandTabs('01\t234567A')).toEqual('01 234567A'); expect(utils.expandTabs('012\t34567A')).toEqual('012 34567A'); expect(utils.expandTabs('0123\t4567A')).toEqual('0123 4567A'); expect(utils.expandTabs('01234\t567A')).toEqual('01234 567A'); expect(utils.expandTabs('012345\t67A')).toEqual('012345 67A'); expect(utils.expandTabs('0123456\t7A')).toEqual('0123456 7A'); expect(utils.expandTabs('01234567\tA')).toEqual('01234567 A'); }); }); describe('Parses compiler output', () => { it('handles simple cases', () => { expect(utils.parseOutput('Line one\nLine two', 'bob.cpp')).toEqual([{text: 'Line one'}, {text: 'Line two'}]); expect(utils.parseOutput('Line one\nbob.cpp:1 Line two', 'bob.cpp')).toEqual([ {text: 'Line one'}, { tag: {column: 0, line: 1, text: 'Line two', severity: 3, file: 'bob.cpp'}, text: ':1 Line two', }, ]); expect(utils.parseOutput('Line one\nbob.cpp:1:5: Line two', 'bob.cpp')).toEqual([ {text: 'Line one'}, { tag: {column: 5, line: 1, text: 'Line two', severity: 3, file: 'bob.cpp'}, text: ':1:5: Line two', }, ]); }); it('handles windows output', () => { expect(utils.parseOutput('bob.cpp(1) Oh noes', 'bob.cpp')).toEqual([ { tag: {column: 0, line: 1, text: 'Oh noes', severity: 3, file: 'bob.cpp'}, text: '(1) Oh noes', }, ]); }); it('replaces all references to input source', () => { expect(utils.parseOutput('bob.cpp:1 error in bob.cpp', 'bob.cpp')).toEqual([ { tag: {column: 0, line: 1, text: 'error in ', severity: 3, file: 'bob.cpp'}, text: ':1 error in ', }, ]); }); it('treats warnings and notes as the correct severity', () => { expect(utils.parseOutput('Line one\nbob.cpp:1:5: warning Line two', 'bob.cpp')).toEqual([ {text: 'Line one'}, { tag: {column: 5, line: 1, text: 'warning Line two', severity: 2, file: 'bob.cpp'}, text: ':1:5: warning Line two', }, ]); expect(utils.parseOutput('Line one\nbob.cpp:1:5: note Line two', 'bob.cpp')).toEqual([ {text: 'Line one'}, { tag: {column: 5, line: 1, text: 'note Line two', severity: 1, file: 'bob.cpp'}, text: ':1:5: note Line two', }, ]); }); it('treats as if it were the compiler source', () => { expect( utils.parseOutput(":120:25: error: variable or field 'transform_data' declared void", 'bob.cpp'), ).toEqual([ { tag: { column: 25, line: 120, text: "error: variable or field 'transform_data' declared void", severity: 3, file: 'bob.cpp', }, text: ":120:25: error: variable or field 'transform_data' declared void", }, ]); }); it('parser error with full path', () => { expect(utils.parseOutput("/app/example.cl:5:30: error: use of undeclared identifier 'ad'")).toEqual([ { tag: { file: 'example.cl', column: 30, line: 5, text: "error: use of undeclared identifier 'ad'", severity: 3, }, text: "example.cl:5:30: error: use of undeclared identifier 'ad'", }, ]); }); }); describe('Pascal compiler output', () => { it('recognize fpc identifier not found error', () => { expect(utils.parseOutput('output.pas(13,23) Error: Identifier not found "adsadasd"', 'output.pas')).toEqual([ { tag: { column: 23, line: 13, text: 'Error: Identifier not found "adsadasd"', severity: 3, file: 'output.pas', }, text: '(13,23) Error: Identifier not found "adsadasd"', }, ]); }); it('recognize fpc exiting error', () => { expect( utils.parseOutput('output.pas(17) Fatal: There were 1 errors compiling module, stopping', 'output.pas'), ).toEqual([ { tag: { column: 0, line: 17, text: 'Fatal: There were 1 errors compiling module, stopping', severity: 3, file: 'output.pas', }, text: '(17) Fatal: There were 1 errors compiling module, stopping', }, ]); }); it('removes the temp path', () => { expect( utils.parseOutput( 'Compiling /tmp/path/prog.dpr\noutput.pas(17) Fatal: There were 1 errors compiling module, stopping', 'output.pas', '/tmp/path/', ), ).toEqual([ { text: 'Compiling prog.dpr', }, { tag: { column: 0, line: 17, text: 'Fatal: There were 1 errors compiling module, stopping', severity: 3, file: 'output.pas', }, text: '(17) Fatal: There were 1 errors compiling module, stopping', }, ]); }); }); describe('Rust compiler output', () => { it('handles simple cases', () => { expect(utils.parseRustOutput('Line one\nLine two', 'bob.rs')).toEqual([{text: 'Line one'}, {text: 'Line two'}]); expect(utils.parseRustOutput('Unrelated\nLine one\n --> bob.rs:1\nUnrelated', 'bob.rs')).toEqual([ {text: 'Unrelated'}, { tag: {column: 0, line: 1, text: 'Line one', severity: 3}, text: 'Line one', }, { tag: {column: 0, line: 1, text: '', severity: 3}, text: ' --> :1', }, {text: 'Unrelated'}, ]); expect(utils.parseRustOutput('Line one\n --> bob.rs:1:5', 'bob.rs')).toEqual([ { tag: {column: 5, line: 1, text: 'Line one', severity: 3}, text: 'Line one', }, { tag: {column: 5, line: 1, text: '', severity: 3}, text: ' --> :1:5', }, ]); }); it('replaces all references to input source', () => { expect(utils.parseRustOutput('error: Error in bob.rs\n --> bob.rs:1', 'bob.rs')).toEqual([ { tag: {column: 0, line: 1, text: 'error: Error in ', severity: 3}, text: 'error: Error in ', }, { tag: {column: 0, line: 1, text: '', severity: 3}, text: ' --> :1', }, ]); }); it('treats as if it were the compiler source', () => { expect(utils.parseRustOutput('error: is sad\n --> :120:25', 'bob.rs')).toEqual([ { tag: {column: 25, line: 120, text: 'error: is sad', severity: 3}, text: 'error: is sad', }, { tag: {column: 25, line: 120, text: '', severity: 3}, text: ' --> :120:25', }, ]); }); }); describe('Tool output', () => { it('removes the relative path', () => { expect( utils.parseOutput( './example.cpp:1:1: Fatal: There were 1 errors compiling module, stopping', './example.cpp', ), ).toEqual([ { tag: { column: 1, line: 1, text: 'Fatal: There were 1 errors compiling module, stopping', severity: 3, file: 'example.cpp', }, text: ':1:1: Fatal: There were 1 errors compiling module, stopping', }, ]); }); it('removes fortran relative path', () => { expect( utils.parseOutput("./example.f90:5:22: error: No explicit type declared for 'y'", './example.f90'), ).toEqual([ { tag: { column: 22, line: 5, text: "error: No explicit type declared for 'y'", severity: 3, file: 'example.f90', }, text: ":5:22: error: No explicit type declared for 'y'", }, ]); }); it('removes the jailed path', () => { expect( utils.parseOutput( '/home/ubuntu/example.cpp:1:1: Fatal: There were 1 errors compiling module, stopping', './example.cpp', ), ).toEqual([ { tag: { column: 1, line: 1, text: 'Fatal: There were 1 errors compiling module, stopping', severity: 3, file: 'example.cpp', }, text: ':1:1: Fatal: There were 1 errors compiling module, stopping', }, ]); }); }); describe('Pads right', () => { it('works', () => { expect(utils.padRight('abcd', 8)).toEqual('abcd '); expect(utils.padRight('a', 8)).toEqual('a '); expect(utils.padRight('', 8)).toEqual(' '); expect(utils.padRight('abcd', 4)).toEqual('abcd'); expect(utils.padRight('abcd', 2)).toEqual('abcd'); }); }); describe('Trim right', () => { it('works', () => { expect(utils.trimRight(' ')).toEqual(''); expect(utils.trimRight('')).toEqual(''); expect(utils.trimRight(' ab ')).toEqual(' ab'); expect(utils.trimRight(' a b ')).toEqual(' a b'); expect(utils.trimRight('a ')).toEqual('a'); }); }); describe('Anonymizes all kind of IPs', () => { it('Ignores localhost', () => { expect(utils.anonymizeIp('localhost')).toEqual('localhost'); expect(utils.anonymizeIp('localhost:42')).toEqual('localhost:42'); }); it('Removes last octet from IPv4 addresses', () => { expect(utils.anonymizeIp('127.0.0.0')).toEqual('127.0.0.0'); expect(utils.anonymizeIp('127.0.0.10')).toEqual('127.0.0.0'); expect(utils.anonymizeIp('127.0.0.255')).toEqual('127.0.0.0'); }); it('Removes last 3 hextets from IPv6 addresses', () => { // Not necessarily valid addresses, we're interested in the format expect(utils.anonymizeIp('ffff:aaaa:dead:beef')).toEqual('ffff:0:0:0'); expect(utils.anonymizeIp('bad:c0de::')).toEqual('bad:0:0:0'); expect(utils.anonymizeIp(':1d7e::c0fe')).toEqual(':0:0:0'); }); }); describe('Logger functionality', () => { it('correctly logs streams split over lines', () => { const logs: {level: string; msg: string}[] = []; const fakeLog = {log: (level: string, msg: string) => logs.push({level, msg})} as any as winston.Logger; const infoStream = makeLogStream('info', fakeLog); infoStream.write('first\n'); infoStream.write('part'); infoStream.write('ial\n'); expect(logs).toEqual([ { level: 'info', msg: 'first', }, { level: 'info', msg: 'partial', }, ]); }); it('correctly logs streams to the right destination', () => { const logs: {level: string; msg: string}[] = []; const fakeLog = {log: (level: string, msg: string) => logs.push({level, msg})} as any as winston.Logger; const infoStream = makeLogStream('warn', fakeLog); infoStream.write('ooh\n'); expect(logs).toEqual([ { level: 'warn', msg: 'ooh', }, ]); }); }); describe('Hash interface', () => { it('correctly hashes strings', () => { const version = 'Compiler Explorer Tests Version 0'; expect(utils.getHash('cream cheese', version)).toEqual( 'cfff2d1f7a213e314a67cce8399160ae884f794a3ee9d4a01cd37a8c22c67d94', ); expect(utils.getHash('large eggs', version)).toEqual( '9144dec50b8df5bc5cc24ba008823cafd6616faf2f268af84daf49ac1d24feb0', ); expect(utils.getHash('sugar', version)).toEqual( 'afa3c89d0f6a61de6805314c9bd7c52d020425a3a3c7bbdfa7c0daec594e5ef1', ); }); it('correctly hashes objects', () => { expect( utils.getHash({ toppings: [ {name: 'raspberries', optional: false}, {name: 'ground cinnamon', optional: true}, ], }), ).toEqual('e205d63abd5db363086621fdc62c4c23a51b733bac5855985a8b56642d570491'); }); }); describe('GoldenLayout utils', () => { it('finds every editor & compiler', async () => { const state = await fs.readJson('test/example-states/default-state.json'); const contents = utils.glGetMainContents(state.content); expect(contents).toEqual({ editors: [ {source: 'Editor 1', language: 'c++'}, {source: 'Editor 2', language: 'c++'}, {source: 'Editor 3', language: 'c++'}, {source: 'Editor 4', language: 'c++'}, ], compilers: [ {compiler: 'clang_trunk'}, {compiler: 'gsnapshot'}, {compiler: 'clang_trunk'}, {compiler: 'gsnapshot'}, {compiler: 'rv32-clang'}, ], }); }); }); describe('squashes horizontal whitespace', () => { it('handles empty input', () => { expect(utils.squashHorizontalWhitespace('')).toEqual(''); expect(utils.squashHorizontalWhitespace(' ')).toEqual(''); expect(utils.squashHorizontalWhitespace(' ')).toEqual(''); }); it('handles leading spaces', () => { expect(utils.squashHorizontalWhitespace(' abc')).toEqual(' abc'); expect(utils.squashHorizontalWhitespace(' abc')).toEqual(' abc'); expect(utils.squashHorizontalWhitespace(' abc')).toEqual(' abc'); }); it('handles interline spaces', () => { expect(utils.squashHorizontalWhitespace('abc abc')).toEqual('abc abc'); expect(utils.squashHorizontalWhitespace('abc abc')).toEqual('abc abc'); expect(utils.squashHorizontalWhitespace('abc abc')).toEqual('abc abc'); }); it('handles leading and interline spaces', () => { expect(utils.squashHorizontalWhitespace(' abc abc')).toEqual(' abc abc'); expect(utils.squashHorizontalWhitespace(' abc abc')).toEqual(' abc abc'); expect(utils.squashHorizontalWhitespace(' abc abc')).toEqual(' abc abc'); expect(utils.squashHorizontalWhitespace(' abc abc')).toEqual(' abc abc'); }); }); describe('replaces all substrings', () => { it('works with no substitutions', () => { const string = 'This is a line with no replacements'; expect(utils.replaceAll(string, 'not present', "won't be substituted")).toEqual(string); }); it('handles odd cases', () => { expect(utils.replaceAll('', '', '')).toEqual(''); expect(utils.replaceAll('Hello', '', '')).toEqual('Hello'); }); it('works with single replacement', () => { expect(utils.replaceAll('This is a line with a mistook in it', 'mistook', 'mistake')).toEqual( 'This is a line with a mistake in it', ); expect(utils.replaceAll('This is a line with a mistook', 'mistook', 'mistake')).toEqual( 'This is a line with a mistake', ); expect(utils.replaceAll('Mistooks were made', 'Mistooks', 'Mistakes')).toEqual('Mistakes were made'); }); it('works with multiple replacements', () => { expect(utils.replaceAll('A mistook is a mistook', 'mistook', 'mistake')).toEqual('A mistake is a mistake'); expect(utils.replaceAll('aaaaaaaaaaaaaaaaaaaaaaaaaaa', 'a', 'b')).toEqual('bbbbbbbbbbbbbbbbbbbbbbbbbbb'); }); it('works with overlapping replacements', () => { expect(utils.replaceAll('aaaaaaaa', 'a', 'ba')).toEqual('babababababababa'); }); }); describe('encodes in our version of base32', () => { function doTest(original, expected) { expect(utils.base32Encode(Buffer.from(original))).toEqual(expected); } // Done by hand to check that they are valid it('works for empty strings', () => { doTest('', ''); }); it('works for lengths multiple of 5 bits', () => { doTest('aaaaa', '3Mn4ha7P'); }); it('works for lengths not multiple of 5 bits', () => { // 3 doTest('a', '35'); // 1 doTest('aa', '3Mn1'); // 4 doTest('aaa', '3Mn48'); // 2 doTest('aaaa', '3Mn4ha3'); }); it('works for some random strings', () => { // I also calculated this ones so lets put them doTest('foo', '8rrx8'); doTest('foobar', '8rrx8b7Pc5'); }); }); describe('fileExists', () => { it('Returns true for files that exists', async () => { await expect(utils.fileExists(fileURLToPath(import.meta.url))).resolves.toBe(true); }); it("Returns false for files that don't exist", async () => { await expect(utils.fileExists('./ABC-FileThatDoesNotExist.extension')).resolves.toBe(false); }); it('Returns false for directories that exist', async () => { await expect(utils.fileExists(path.resolve(path.dirname(fileURLToPath(import.meta.url))))).resolves.toBe(false); }); }); describe('safe semver', () => { it('should understand most kinds of semvers', () => { expect(utils.asSafeVer('0')).toEqual('0.0.0'); expect(utils.asSafeVer('1')).toEqual('1.0.0'); expect(utils.asSafeVer('1.0')).toEqual('1.0.0'); expect(utils.asSafeVer('1.1')).toEqual('1.1.0'); expect(utils.asSafeVer('1.1.0')).toEqual('1.1.0'); expect(utils.asSafeVer('1.1.1')).toEqual('1.1.1'); expect(utils.asSafeVer('trunk')).toEqual(utils.magic_semver.trunk); expect(utils.asSafeVer('(trunk)')).toEqual(utils.magic_semver.trunk); expect(utils.asSafeVer('(123.456.789 test)')).toEqual(utils.magic_semver.non_trunk); expect(utils.asSafeVer('0..0')).toEqual(utils.magic_semver.non_trunk); expect(utils.asSafeVer('0.0.')).toEqual(utils.magic_semver.non_trunk); expect(utils.asSafeVer('0.')).toEqual(utils.magic_semver.non_trunk); expect(utils.asSafeVer('.0.0')).toEqual(utils.magic_semver.non_trunk); expect(utils.asSafeVer('.0..')).toEqual(utils.magic_semver.non_trunk); expect(utils.asSafeVer('0..')).toEqual(utils.magic_semver.non_trunk); expect(utils.asSafeVer('123 TEXT')).toEqual('123.0.0'); expect(utils.asSafeVer('123.456 TEXT')).toEqual('123.456.0'); expect(utils.asSafeVer('123.456.789 TEXT')).toEqual('123.456.789'); }); }); describe('argument splitting', () => { it('should handle normal things', () => { expect(utils.splitArguments('-hello --world etc --std=c++20')).toEqual([ '-hello', '--world', 'etc', '--std=c++20', ]); }); it('should handle hash chars', () => { expect(utils.splitArguments('-Wno#warnings -Wno-#pragma-messages')).toEqual([ '-Wno#warnings', '-Wno-#pragma-messages', ]); }); it('should handle doublequoted args', () => { expect(utils.splitArguments('--hello "-world etc"')).toEqual(['--hello', '-world etc']); }); it('should handle singlequoted args', () => { expect(utils.splitArguments("--hello '-world etc'")).toEqual(['--hello', '-world etc']); }); it('should handle cheekyness part 1', () => { /* eslint-disable no-useless-escape */ expect(utils.splitArguments('hello #veryfancy etc')).toEqual(['hello', '#veryfancy', 'etc']); /* eslint-enable no-useless-escape */ }); it('should handle cheekyness part 2', () => { expect(utils.splitArguments('hello \\#veryfancy etc')).toEqual(['hello', '\\']); }); });