// Copyright (c) 2018, 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 {fileURLToPath} from 'node:url'; import _ from 'underscore'; import {beforeAll, describe, expect, it} from 'vitest'; import {AppArguments} from '../lib/app.interfaces.js'; import {BaseCompiler} from '../lib/base-compiler.js'; import {CompilationEnvironment} from '../lib/compilation-env.js'; import {CompilerFinder} from '../lib/compiler-finder.js'; import {ClientOptionsHandler, ClientOptionsType} from '../lib/options-handler.js'; import * as properties from '../lib/properties.js'; import {BaseTool} from '../lib/tooling/base-tool.js'; import {getRemoteId} from '../shared/remote-utils.js'; import {UrlTestCases} from '../shared/url-testcases.js'; import {CompilerInfo} from '../types/compiler.interfaces.js'; import {LanguageKey} from '../types/languages.interfaces.js'; import {makeFakeCompilerInfo} from './utils.js'; const languages = { fake: { id: 'fake', }, }; // For tooling, we need to at least be able to stat a valid file. // This is the one we know exists const CURRENT_FILE_PATH = fileURLToPath(import.meta.url); const optionsProps = { libs: 'fakelib:fs:someotherlib', 'libs.fakelib.name': 'fake lib', 'libs.fakelib.description': 'Its is a real, fake lib!', 'libs.fakelib.versions': 'onePath:twoPaths:noPaths', 'libs.fakelib.url': 'https://godbolt.org', 'libs.fakelib.examples': 'abc:def', 'libs.fakelib.versions.onePath.version': 'one path', 'libs.fakelib.versions.onePath.path': '/dev/null', 'libs.fakelib.versions.onePath.libpath': '/lib/null', 'libs.fakelib.versions.onePath.liblink': 'hello', 'libs.fakelib.versions.twoPaths.version': 'two paths', 'libs.fakelib.versions.twoPaths.path': '/dev/null:/dev/urandom', 'libs.fakelib.versions.twoPaths.libpath': '/lib/null:/lib/urandom', 'libs.fakelib.versions.twoPaths.liblink': 'hello1:hello2', 'libs.fakelib.versions.noPaths.version': 'no paths', 'libs.fakelib.versions.noPaths.path': '', 'libs.fakelib.versions.noPaths.lookupversion': 'no-paths123', 'libs.fakelib.versions.noPaths.options': '-DHELLO123 -DETC "--some thing with spaces"', 'libs.fs.versions': 'std', 'libs.fs.versions.std.version': 'std', 'libs.fs.versions.std.staticliblink': 'c++fs:rt', 'libs.fs.versions.std.dependencies': 'pthread', 'libs.someotherlib.versions': 'trunk', 'libs.someotherlib.versions.trunk.version': 'trunk', 'libs.someotherlib.versions.trunk.staticliblink': 'someotherlib', 'libs.someotherlib.versions.trunk.dependencies': 'c++fs', 'libs.someotherlib.versions.trunk.alias': 'master', 'libs.someotherlib.versions.trunk.hidden': true, tools: 'faketool:someothertool', 'tools.faketool.name': 'Fake Tool', 'tools.faketool.exe': CURRENT_FILE_PATH, 'tools.faketool.type': 'independent', 'tools.faketool.class': 'testing-tool', 'tools.faketool.stdinHint': 'disabled', 'tools.someothertool.name': 'Some Other Tool', 'tools.someothertool.exe': CURRENT_FILE_PATH, 'tools.someothertool.type': 'independent', 'tools.someothertool.class': 'testing-tool', 'tools.someothertool.stdinHint': 'disabled', }; if (process.platform === 'win32') { optionsProps['libs.fakelib.versions.twoPaths.path'] = optionsProps['libs.fakelib.versions.twoPaths.path'].replace( ':', ';', ); optionsProps['libs.fakelib.versions.twoPaths.libpath'] = optionsProps[ 'libs.fakelib.versions.twoPaths.libpath' ].replace(':', ';'); } const moreLibProps = { libs: 'fs:someotherlib:yalib:fourthlib:autolib', 'libs.fs.versions': 'std', 'libs.fs.versions.std.version': 'std', 'libs.fs.versions.std.staticliblink': 'fsextra:c++fs:rt', 'libs.fs.versions.std.dependencies': 'pthread', 'libs.someotherlib.versions': 'trunk', 'libs.someotherlib.versions.trunk.version': 'trunk', 'libs.someotherlib.versions.trunk.staticliblink': 'someotherlib', 'libs.someotherlib.versions.trunk.dependencies': 'c++fs', 'libs.someotherlib.versions.trunk.alias': 'master', 'libs.yalib.versions': 'trunk', 'libs.yalib.versions.trunk.version': 'trunk', 'libs.yalib.versions.trunk.staticliblink': 'yalib', 'libs.yalib.versions.trunk.dependencies': 'someotherlib:c++fs', 'libs.fourthlib.versions': 'trunk', 'libs.fourthlib.versions.trunk.version': 'trunk', 'libs.fourthlib.versions.trunk.staticliblink': 'fourthlib:yalib:rt', 'libs.autolib.versions': 'autodetect', 'libs.autolib.versions.autodetect.version': 'autodetect', 'libs.autolib.versions.autodetect.staticliblink': 'hello', }; const fakeCompilerInfo = (id: string, lang: string, group: string, semver: string, isSemver: boolean) => { return makeFakeCompilerInfo({ id: id, exe: '/dev/null', name: id, lang: lang as LanguageKey, group: group, isSemVer: isSemver, semver: semver, libsArr: [], }); }; class TestBaseCompiler extends BaseCompiler { getSupportedLibrariesTest() { return this.supportedLibraries; } } describe('Options handler', () => { let fakeOptionProps: ReturnType; let compilerProps: properties.CompilerProps; let optionsHandler: ClientOptionsHandler; let fakeMoreCompilerProps: ReturnType; let moreCompilerProps: properties.CompilerProps; let moreOptionsHandler: ClientOptionsHandler; let env: CompilationEnvironment; function createClientOptions(libs: ReturnType) { return { libs: { 'c++': libs.fake, }, } as unknown as ClientOptionsType; } beforeAll(() => { fakeOptionProps = properties.fakeProps(optionsProps); compilerProps = new properties.CompilerProps(languages, fakeOptionProps); optionsHandler = new ClientOptionsHandler([], compilerProps, {env: ['dev']} as unknown as AppArguments); fakeMoreCompilerProps = properties.fakeProps(moreLibProps); moreCompilerProps = new properties.CompilerProps(languages, fakeMoreCompilerProps); moreOptionsHandler = new ClientOptionsHandler([], moreCompilerProps, { env: ['dev'], } as unknown as AppArguments); env = { ceProps: properties.fakeProps({}), compilerProps: () => {}, getCompilerPropsForLanguage: () => { return (prop: string, def: any) => def; }, } as unknown as CompilationEnvironment; }); it('should always return an array of paths', () => { const libs = optionsHandler.parseLibraries({fake: optionsProps.libs}); _.each(libs[languages.fake.id]['fakelib'].versions, version => { expect(Array.isArray(version.path)).toEqual(true); }); expect(libs).toEqual({ fake: { fakelib: { description: 'Its is a real, fake lib!', name: 'fake lib', url: 'https://godbolt.org', dependencies: [], liblink: [], staticliblink: [], examples: ['abc', 'def'], options: [], packagedheaders: false, versions: { noPaths: { $order: 2, path: [], version: 'no paths', liblink: [], libpath: [], staticliblink: [], dependencies: [], alias: [], lookupversion: 'no-paths123', options: ['-DHELLO123', '-DETC', '--some thing with spaces'], hidden: false, packagedheaders: false, }, onePath: { $order: 0, path: ['/dev/null'], version: 'one path', staticliblink: [], dependencies: [], liblink: ['hello'], libpath: ['/lib/null'], alias: [], options: [], hidden: false, packagedheaders: false, }, twoPaths: { $order: 1, path: ['/dev/null', '/dev/urandom'], staticliblink: [], dependencies: [], liblink: ['hello1', 'hello2'], libpath: ['/lib/null', '/lib/urandom'], version: 'two paths', alias: [], options: [], hidden: false, packagedheaders: false, }, }, }, fs: { description: undefined, name: undefined, url: undefined, dependencies: [], liblink: [], staticliblink: [], examples: [], options: [], packagedheaders: false, versions: { std: { $order: 0, libpath: [], path: [], version: 'std', alias: [], liblink: [], staticliblink: ['c++fs', 'rt'], dependencies: ['pthread'], options: [], hidden: false, packagedheaders: false, }, }, }, someotherlib: { description: undefined, name: undefined, url: undefined, dependencies: [], liblink: [], staticliblink: [], examples: [], options: [], packagedheaders: false, versions: { trunk: { $order: 0, libpath: [], path: [], version: 'trunk', alias: ['master'], liblink: [], staticliblink: ['someotherlib'], dependencies: ['c++fs'], options: [], hidden: true, packagedheaders: false, }, }, }, }, }); }); it('should order compilers as expected', () => { const compilers = [ fakeCompilerInfo('a1', languages.fake.id, 'a', '0.0.1', true), fakeCompilerInfo('a2', languages.fake.id, 'a', '0.2.0', true), fakeCompilerInfo('a3', languages.fake.id, 'a', '0.2.1', true), fakeCompilerInfo('b1', languages.fake.id, 'b', 'trunk', true), fakeCompilerInfo('b2', languages.fake.id, 'b', '1.0.0', true), fakeCompilerInfo('b3', languages.fake.id, 'b', '0.5.0', true), fakeCompilerInfo('c1', languages.fake.id, 'c', '3.0.0', true), fakeCompilerInfo('c2', languages.fake.id, 'c', '3.0.0', true), fakeCompilerInfo('c3', languages.fake.id, 'c', '3.0.0', true), fakeCompilerInfo('d1', languages.fake.id, 'd', '1', true), fakeCompilerInfo('d2', languages.fake.id, 'd', '2.0.0', true), fakeCompilerInfo('d3', languages.fake.id, 'd', '0.0.5', true), fakeCompilerInfo('e1', languages.fake.id, 'e', '0..0', false), fakeCompilerInfo('e2', languages.fake.id, 'e', '', false), fakeCompilerInfo('f1', languages.fake.id, 'f', '5', true), fakeCompilerInfo('f2', languages.fake.id, 'f', '5.1', true), fakeCompilerInfo('f3', languages.fake.id, 'f', '5.2', true), fakeCompilerInfo('g1', languages.fake.id, 'g', '5 a', true), fakeCompilerInfo('g2', languages.fake.id, 'g', '5.1 b d', true), fakeCompilerInfo('g3', languages.fake.id, 'g', '5.2 ce fg', true), ]; const expectedOrder = { a: { a1: -0, a2: -1, a3: -2, }, b: { b1: -2, b2: -1, b3: -0, }, c: { c1: -0, c2: -1, c3: -2, }, d: { d1: -1, d2: -2, d3: -0, }, e: { e1: undefined, e2: undefined, }, f: { f1: -0, f2: -1, f3: -2, }, g: { g1: -0, g2: -1, g3: -2, }, }; optionsHandler.setCompilers(compilers); _.each(optionsHandler.get().compilers, compiler => { expect(compiler['$order']).toEqual( expectedOrder[(compiler as CompilerInfo).group][(compiler as CompilerInfo).id], ); }); optionsHandler.setCompilers([]); }); it('should get static libraries', () => { const libs = optionsHandler.parseLibraries({fake: optionsProps.libs}); const compilerInfo = fakeCompilerInfo('g82', 'c++', 'cpp', '8.2', true); const clientOptions = createClientOptions(libs); const compiler = new BaseCompiler(compilerInfo, env); compiler.initialiseLibraries(clientOptions); const staticlinks = compiler.getStaticLibraryLinks([{id: 'fs', version: 'std'}]); expect(staticlinks).toEqual(['-lc++fs', '-lrt', '-lpthread']); const sharedlinks = compiler.getSharedLibraryLinks([{id: 'fs', version: 'std'}]); expect(sharedlinks).toEqual([]); }); it('should sort static libraries', () => { const libs = optionsHandler.parseLibraries({fake: optionsProps.libs}); const compilerInfo = fakeCompilerInfo('g82', 'c++', 'cpp', '8.2', true); const clientOptions = createClientOptions(libs); const compiler = new BaseCompiler(compilerInfo, env); compiler.initialiseLibraries(clientOptions); let staticlinks = compiler.getSortedStaticLibraries([{id: 'someotherlib', version: 'trunk'}]); expect(staticlinks).toEqual(['someotherlib', 'c++fs']); staticlinks = compiler.getSortedStaticLibraries([ {id: 'fs', version: 'std'}, {id: 'someotherlib', version: 'trunk'}, ]); expect(staticlinks).toEqual(['someotherlib', 'c++fs', 'rt', 'pthread']); }); it('library sort special case 1', () => { const libs = moreOptionsHandler.parseLibraries({fake: moreLibProps.libs}); const compilerInfo = fakeCompilerInfo('g82', 'c++', 'cpp', '8.2', true); const compiler = new BaseCompiler(compilerInfo, env); const clientOptions = createClientOptions(libs); compiler.initialiseLibraries(clientOptions); const staticlinks = compiler.getSortedStaticLibraries([{id: 'fs', version: 'std'}]); expect(staticlinks).toEqual(['fsextra', 'c++fs', 'rt', 'pthread']); }); it('library sort special case 2', () => { const libs = moreOptionsHandler.parseLibraries({fake: moreLibProps.libs}); const compilerInfo = fakeCompilerInfo('g82', 'c++', 'cpp', '8.2', true); const compiler = new BaseCompiler(compilerInfo, env); const clientOptions = createClientOptions(libs); compiler.initialiseLibraries(clientOptions); const staticlinks = compiler.getSortedStaticLibraries([ {id: 'yalib', version: 'trunk'}, {id: 'fs', version: 'std'}, {id: 'someotherlib', version: 'trunk'}, ]); expect(staticlinks).toEqual(['yalib', 'someotherlib', 'fsextra', 'c++fs', 'rt', 'pthread']); }); it('library sort special case 3', () => { const libs = moreOptionsHandler.parseLibraries({fake: moreLibProps.libs}); const compilerInfo = fakeCompilerInfo('g82', 'c++', 'cpp', '8.2', true); const compiler = new BaseCompiler(compilerInfo, env); const clientOptions = createClientOptions(libs); compiler.initialiseLibraries(clientOptions); const staticlinks = compiler.getSortedStaticLibraries([ {id: 'fourthlib', version: 'trunk'}, {id: 'fs', version: 'std'}, {id: 'someotherlib', version: 'trunk'}, ]); expect(staticlinks).toEqual(['fourthlib', 'yalib', 'someotherlib', 'fsextra', 'c++fs', 'rt', 'pthread']); }); it('filtered library list', () => { const libs = moreOptionsHandler.parseLibraries({fake: moreLibProps.libs}); const compilerInfo = fakeCompilerInfo('g82', 'c++', 'cpp', '8.2', true); compilerInfo.libsArr = ['fs.std', 'someotherlib']; const compiler = new TestBaseCompiler(compilerInfo, env); const clientOptions = createClientOptions(libs); compiler.initialiseLibraries(clientOptions); const libNames = _.keys(compiler.getSupportedLibrariesTest()); expect(libNames).toEqual(['fs', 'someotherlib']); }); it('can detect libraries from options', () => { const libs = moreOptionsHandler.parseLibraries({fake: moreLibProps.libs}); const compilerInfo = fakeCompilerInfo('g82', 'c++', 'cpp', '8.2', true); const compiler = new BaseCompiler(compilerInfo, env); const clientOptions = createClientOptions(libs); compiler.initialiseLibraries(clientOptions); const obj = { libraries: [{id: 'ctre', version: 'trunk'}], options: ['-O3', '--std=c++17', '-lhello'], }; expect(compiler.tryAutodetectLibraries(obj)).toEqual(true); expect(obj.libraries).toEqual([ {id: 'ctre', version: 'trunk'}, {id: 'autolib', version: 'autodetect'}, ]); expect(obj.options).toEqual(['-O3', '--std=c++17']); }); it("server-side library alias support (just in case client doesn't support it)", () => { const libs = moreOptionsHandler.parseLibraries({fake: moreLibProps.libs}); const compilerInfo = fakeCompilerInfo('g82', 'c++', 'cpp', '8.2', true); const env = { ceProps: properties.fakeProps({}), compilerProps: () => {}, getCompilerPropsForLanguage: () => { return (prop: string, def: any) => def; }, } as unknown as CompilationEnvironment; const compiler = new BaseCompiler(compilerInfo, env); const clientOptions = createClientOptions(libs); compiler.initialiseLibraries(clientOptions); const staticlinks = compiler.getSortedStaticLibraries([{id: 'someotherlib', version: 'master'}]); expect(staticlinks).toEqual(['someotherlib', 'c++fs']); }); it('should be able to parse basic tools', () => { class TestBaseTool extends BaseTool { // TestBaseTool is never instantiated, it's just used to trick ts into thinking this is public public override env = null as any; } const tools = optionsHandler.parseTools({fake: optionsProps.tools}) as unknown as Record< string, Record> >; _.each(tools.fake, tool => { delete tool.env; }); expect(tools).toEqual({ fake: { faketool: { id: 'faketool', type: 'independent', addOptionsToToolArgs: true, sandboxType: 'none', tool: { args: undefined, compilerLanguage: 'fake', exclude: [], exe: CURRENT_FILE_PATH, id: 'faketool', includeKey: undefined, languageId: undefined, monacoStdin: undefined, name: 'Fake Tool', options: [], stdinHint: 'disabled', type: 'independent', icon: undefined, darkIcon: undefined, }, }, someothertool: { id: 'someothertool', type: 'independent', addOptionsToToolArgs: true, sandboxType: 'none', tool: { args: undefined, compilerLanguage: 'fake', exclude: [], exe: CURRENT_FILE_PATH, id: 'someothertool', includeKey: undefined, languageId: undefined, monacoStdin: undefined, name: 'Some Other Tool', options: [], stdinHint: 'disabled', type: 'independent', icon: undefined, darkIcon: undefined, }, }, }, }); }); it('should correctly resolve remote urls', () => { const compilerName = 'godbolt.org@443/gpu'; const {host, port, uriBase} = CompilerFinder.getRemotePartsFromCompilerName(compilerName); expect(host).toEqual('godbolt.org'); expect(port).toEqual(443); expect(uriBase).toEqual('gpu'); const {uriSchema, uri, apiPath} = CompilerFinder.prepareRemoteUrlParts(host, port, uriBase, 'c++'); expect(uriSchema).toEqual('https'); expect(uri).toEqual('https://godbolt.org:443/gpu'); expect(apiPath).toEqual('/gpu/api/compilers/c++?fields=all'); const info = CompilerFinder.getRemoteInfo(uriSchema, host, port, uriBase, 'g123remote'); expect(info).toEqual({ target: 'https://godbolt.org:443', path: '/gpu/api/compiler/g123remote/compile', cmakePath: '/gpu/api/compiler/g123remote/cmake', basePath: '/gpu', }); const fullUrl = ClientOptionsHandler.getFullRemoteUrl(info); const librariesUrl = ClientOptionsHandler.getRemoteUrlForLibraries(fullUrl, 'c++'); expect(librariesUrl).toEqual('https://godbolt.org:443/gpu/api/libraries/c++'); }); describe('getRemoteId', () => { UrlTestCases.forEach(testCase => { it(`should generate remote ID for URL "${testCase.remoteUrl}" with language "${testCase.language}"`, () => { const result = getRemoteId(testCase.remoteUrl, testCase.language); expect(result).toBe(testCase.expectedId); }); }); }); });