Files
compiler-explorer/test/utils-tests.ts
Matt Godbolt c425932877 Mask temp paths by the CE marker, not the live os.tmpdir() (#8920)
maskRootdir() built its strip-regex from this process's os.tmpdir(), so
it only masked paths whose root matched the current tmpdir. That is
fragile in two real cases:

- The path being masked is recorded when the compile runs and need not
share the masking process's tmpdir. Snapshots/fixtures that freeze
/tmp/... paths only mask on hosts where os.tmpdir() is /tmp (they fail
where it is e.g. /usr/tmp).
- On macOS os.tmpdir() returns /var/folders/... while real temp paths
often resolve via /private/var/folders/..., so masking silently missed.
- A configured temp root (temp dirs created outside os.tmpdir()) was
never matched at all.

Key the regex off the distinctive ce_temp_prefix marker instead,
matching whatever path segments precede it. This is tmpdir-independent
and unifies the Windows and non-Windows branches into one rule (also
masking embedded temp paths like `-I/tmp/.../include` on Windows, which
the old anchored branch missed).

Adds unit tests for maskRootdir (previously untested) covering /tmp,
/usr/tmp, macOS /private/var, Windows, the -I include case, and non-temp
paths left untouched.

<!-- THIS COMMENT IS INVISIBLE IN THE FINAL PR, BUT FEEL FREE TO REMOVE
IT
Thanks for taking the time to improve CE. We really appreciate it.
Before opening the PR, please make sure that the tests & linter pass
their checks,
  by running `make check`.
In the best case scenario, you are also adding tests to back up your
changes,
  but don't sweat it if you don't. We can discuss them at a later date.
Feel free to append your name to the CONTRIBUTORS.md file
Thanks again, we really appreciate this!
-->

---------

Co-authored-by: Matt Godbolt <mattgodbolt@hudson-trading.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-14 15:30:54 +01:00

847 lines
32 KiB
TypeScript

// 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 fs from 'node:fs/promises';
import path from 'node:path';
import {fileURLToPath} from 'node: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 {newTempDir} 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: '<source>: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: '<source>: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: '<source>(1) Oh noes',
},
]);
});
it('does not treat c2rust AST node lines as source locations', () => {
expect(utils.parseOutput('CTypeId(42): Located {', 'example.c')).toEqual([{text: 'CTypeId(42): Located {'}]);
});
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 <source>', severity: 3, file: 'bob.cpp'},
text: '<source>:1 error in <source>',
},
]);
});
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: '<source>: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: '<source>:1:5: note Line two',
},
]);
});
it('treats <stdin> as if it were the compiler source', () => {
expect(
utils.parseOutput("<stdin>: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: "<source>: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'",
},
]);
});
it('removes hyperlink escape sequences', () => {
expect(
utils.parseOutput(
't.c:3:1: warning: control reaches end of non-void function [\x1B]8;;https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Warning-Options.html#index-Wno-return-type\x1B\\-Wreturn-type\x1B]8;;\x1B\\]',
),
).toEqual([
{
tag: {
file: 't.c',
line: 3,
column: 1,
text: 'warning: control reaches end of non-void function [-Wreturn-type]',
severity: 2,
},
text: 't.c:3:1: warning: control reaches end of non-void function [\x1B]8;;https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/Warning-Options.html#index-Wno-return-type\x1B\\-Wreturn-type\x1B]8;;\x1B\\]',
},
]);
});
});
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: '<source>(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: '<source>(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: '<source>(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:0\nUnrelated', 'bob.rs')).toEqual([
{text: 'Unrelated'},
{
tag: {file: 'bob.rs', column: 0, line: 1, text: 'Line one', severity: 3},
text: 'Line one',
},
{
tag: {file: 'bob.rs', column: 0, line: 1, text: '', severity: 3},
text: ' --> <source>:1:0',
},
{text: 'Unrelated'},
]);
expect(utils.parseRustOutput('Line one\n --> bob.rs:1:5', 'bob.rs')).toEqual([
{
tag: {file: 'bob.rs', column: 5, line: 1, text: 'Line one', severity: 3},
text: 'Line one',
},
{
tag: {file: 'bob.rs', column: 5, line: 1, text: '', severity: 3},
text: ' --> <source>:1:5',
},
]);
expect(utils.parseRustOutput('Multiple spaces\n --> bob.rs:1:5', 'bob.rs')).toEqual([
{
tag: {file: 'bob.rs', column: 5, line: 1, text: 'Multiple spaces', severity: 3},
text: 'Multiple spaces',
},
{
tag: {file: 'bob.rs', column: 5, line: 1, text: '', severity: 3},
text: ' --> <source>:1:5',
},
]);
});
it('replaces all references to input source', () => {
expect(utils.parseRustOutput('error: Error in bob.rs\n --> bob.rs:1:42', 'bob.rs')).toEqual([
{
tag: {file: 'bob.rs', column: 42, line: 1, text: 'error: Error in <source>', severity: 3},
text: 'error: Error in <source>',
},
{
tag: {file: 'bob.rs', column: 42, line: 1, text: '', severity: 3},
text: ' --> <source>:1:42',
},
]);
});
it('treats <stdin> as if it were the compiler source', () => {
expect(utils.parseRustOutput('error: <stdin> is sad\n --> <stdin>:120:25', 'bob.rs')).toEqual([
{
tag: {file: 'bob.rs', column: 25, line: 120, text: 'error: <source> is sad', severity: 3},
text: 'error: <source> is sad',
},
{
tag: {file: 'bob.rs', column: 25, line: 120, text: '', severity: 3},
text: ' --> <source>:120:25',
},
]);
});
it('removes hyperlink escape sequences', () => {
expect(
utils.parseRustOutput(
'error[\x1B]8;;https://doc.rust-lang.org/error_codes/E0425.html\x07E0425\x1B]8;;\x07]: cannot find value `x` in this scope\n --> <source>:42:27',
),
).toEqual([
{
tag: {
line: 42,
column: 27,
text: 'error[E0425]: cannot find value `x` in this scope',
severity: 3,
},
text: 'error[\x1B]8;;https://doc.rust-lang.org/error_codes/E0425.html\x07E0425\x1B]8;;\x07]: cannot find value `x` in this scope',
},
{
tag: {
line: 42,
column: 27,
text: '',
severity: 3,
},
text: ' --> <source>:42:27',
},
]);
});
it('emits quickfixes', () => {
expect(utils.parseRustOutput('error\n --> <source>:42:27\n15 + use std::collections::HashMap;')).toEqual([
{
tag: {
line: 42,
column: 27,
text: 'error',
severity: 3,
},
text: 'error',
},
{
tag: {
line: 42,
column: 27,
text: '',
severity: 3,
},
text: ' --> <source>:42:27',
},
{
text: '15 + use std::collections::HashMap;',
tag: {
line: 42,
column: 27,
text: 'error',
severity: 3,
fixes: [
{
title: 'Add import for `std::collections::HashMap`',
edits: [
{
line: 15,
column: 1,
endline: 15,
endcolumn: 1,
text: 'use std::collections::HashMap;\n',
},
],
},
],
},
},
]);
expect(
utils.parseRustOutput(
'error\n --> <source>:42:27\n = help: add `#![feature(num_midpoint_signed)]` to the crate attributes to enable',
),
).toEqual([
{
tag: {
line: 42,
column: 27,
text: 'error',
severity: 3,
},
text: 'error',
},
{
tag: {
line: 42,
column: 27,
text: '',
severity: 3,
},
text: ' --> <source>:42:27',
},
{
text: ' = help: add `#![feature(num_midpoint_signed)]` to the crate attributes to enable',
tag: {
line: 42,
column: 27,
text: 'error',
severity: 3,
fixes: [
{
title: 'Add feature flag `num_midpoint_signed`',
edits: [
{
line: 1,
column: 1,
endline: 1,
endcolumn: 1,
text: '#![feature(num_midpoint_signed)]\n',
},
],
},
],
},
},
]);
});
it('attaches filenames to errors from multiple files', () => {
// https://godbolt.org/z/nWE3PTeGf
expect(
utils.parseRustOutput(
`warning: function \`f1\` is never used
--> <source>:3:4
warning: function \`f2\` is never used
--> /app/m.rs:1:4
warning: 2 warnings emitted`,
'example.rs',
),
).toEqual([
{
text: 'warning: function `f1` is never used',
tag: {
line: 3,
column: 4,
file: 'example.rs',
severity: 2,
text: 'warning: function `f1` is never used',
},
},
{
text: ' --> <source>:3:4',
tag: {
line: 3,
column: 4,
file: 'example.rs',
severity: 3,
text: '',
},
},
{text: ''},
{
text: 'warning: function `f2` is never used',
tag: {
line: 1,
column: 4,
file: 'm.rs',
severity: 2,
text: 'warning: function `f2` is never used',
},
},
{
text: ' --> /app/m.rs:1:4',
tag: {
line: 1,
column: 4,
file: 'm.rs',
severity: 3,
text: '',
},
},
{text: ''},
{text: 'warning: 2 warnings emitted'},
]);
});
});
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: '<source>: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: "<source>: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: '<source>:1:1: Fatal: There were 1 errors compiling module, stopping',
},
]);
});
});
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 = JSON.parse(await fs.readFile('test/example-states/default-state.json', 'utf-8'));
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('encodes in our version of base32', () => {
function doTest(original: string, expected: string) {
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');
});
});
const thisFilename = fileURLToPath(import.meta.url);
describe('fileExists', () => {
it('Returns true for files that exists', async () => {
await expect(utils.fileExists(thisFilename)).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(thisFilename)))).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('tries to load text file', () => {
it('should load files that exist', async () => {
const selfText = await utils.tryReadTextFile(thisFilename);
expect(selfText).toContain('should load files that exist');
});
it('should be undefined for non-existent files', async () => {
const selfText = await utils.tryReadTextFile(thisFilename + '.doesntexist');
expect(selfText).to.be.undefined;
});
});
describe('output files', async () => {
const tmpDir = newTempDir();
it('should work in simple cases', async () => {
const filepath = path.join(tmpDir, 'file.txt');
await utils.outputTextFile(filepath, 'hello');
expect(await utils.tryReadTextFile(filepath)).toEqual('hello');
});
it('should create subddirectories ok', async () => {
const filepath = path.join(tmpDir, 'a', 'b', 'file.txt');
await utils.outputTextFile(filepath, 'hello');
expect(await utils.tryReadTextFile(filepath)).toEqual('hello');
});
it('should ensure files exist', async () => {
const filepath = path.join(tmpDir, 'moo', 'foo', 'file.txt');
await utils.ensureFileExists(filepath);
expect(await utils.tryReadTextFile(filepath)).toEqual('');
await utils.outputTextFile(filepath, 'hello');
expect(await utils.tryReadTextFile(filepath)).toEqual('hello');
await utils.ensureFileExists(filepath);
expect(await utils.tryReadTextFile(filepath)).toEqual('hello');
});
});
describe('maskRootdir', () => {
it('masks a CE temp path down to the user-facing filename', () => {
expect(utils.maskRootdir('/tmp/compiler-explorer-compiler123-4-abc/example.cpp')).toEqual('example.cpp');
});
it('keeps /app/ when the temp path is embedded (e.g. a -I include flag)', () => {
expect(utils.maskRootdir('-I/tmp/compiler-explorer-compiler123-4-abc/include')).toEqual('-I/app/include');
});
// The recorded path's tmpdir root need not match this process's os.tmpdir(), so
// masking keys off the ce_temp_prefix marker, not the root.
it.each([
['/tmp', '/tmp/compiler-explorer-compilerXYZ/example.cpp'],
['/usr/tmp', '/usr/tmp/compiler-explorer-compilerXYZ/example.cpp'],
['macOS /private/var', '/private/var/folders/ab/xyz/T/compiler-explorer-compilerXYZ/example.cpp'],
['Windows', 'C:/Users/ce/AppData/Local/Temp/compiler-explorer-compilerXYZ/example.cpp'],
])('masks a temp path rooted at %s regardless of the local tmpdir', (_root, input) => {
expect(utils.maskRootdir(input)).toEqual('example.cpp');
});
it('leaves non-temp paths untouched', () => {
expect(utils.maskRootdir('/usr/include/stdio.h')).toEqual('/usr/include/stdio.h');
});
// The marker segment must be followed by `/`, so a bare dir with no trailing slash
// is left alone. (Real inputs always name a file inside the dir.)
it('leaves a bare temp dir with no trailing slash untouched', () => {
expect(utils.maskRootdir('/tmp/compiler-explorer-compiler123-4-abc')).toEqual(
'/tmp/compiler-explorer-compiler123-4-abc',
);
});
it('masks a bare temp dir with a trailing slash to empty', () => {
expect(utils.maskRootdir('/tmp/compiler-explorer-compiler123-4-abc/')).toEqual('');
});
it('leaves paths with invalid marker suffix chars untouched', () => {
expect(utils.maskRootdir('/tmp/compiler-explorer-compiler+bad/example.cpp')).toEqual(
'/tmp/compiler-explorer-compiler+bad/example.cpp',
);
});
it('handles long non-matching path-like inputs without altering them', () => {
const input = `-I/${'segment/'.repeat(4000)}not-temp/include`;
expect(utils.maskRootdir(input)).toEqual(input);
});
// The regex stops at whitespace (so multi-token output lines aren't over-masked),
// so a temp root containing a space is only masked from the last space onward.
it('masks only from the last space in a spaced temp root (documented limitation)', () => {
expect(utils.maskRootdir('/tmp/with space/compiler-explorer-compilerXXX/example.cpp')).toEqual(
'/tmp/with space/app/example.cpp',
);
});
it('passes empty input through unchanged', () => {
expect(utils.maskRootdir('')).toEqual('');
});
});