Files
compiler-explorer/test/options-handler.ts
Matt Godbolt 643b6db82a Avoid redundant serialisation work in the startup tail (#9043)
Profiling a production startup (2026-08-20, 6232 compilers) showed two
avoidable CPU costs in the last couple of seconds of startup:

- `setupCompilerChangeHandling`'s `onCompilerChange` ran
`JSON.stringify` over all 6232 compiler objects purely to detect whether
the compiler set had changed since the last discovery. The serialised
form is comparable in scale to the 74MB discovery JSON, and it was
computed (and retained in memory) even in production, where rescanning
is disabled (`rescanCompilerSecs` unset / `--prediscovered`) and so the
comparison can never trigger.
- The `OPTIONS HASH` log line appeared twice per startup:
`ClientOptionsHandler`'s constructor eagerly serialised and hashed the
options blob, only for the result to be immediately discarded and
recomputed by the first `setCompilers()` call.

Changes:

- `lib/app/compiler-changes.ts`: only do change detection when
rescanning is actually enabled, and compare a SHA-256 content hash
instead of retaining the full serialised compiler list. In production
the initial set is now applied with no serialisation at all; with
rescanning enabled, behaviour is unchanged (identical rescans are
skipped, changed sets applied) while dropping the tens-of-megabytes
retained string.
- `lib/options-handler.ts`: drop the constructor's
`_updateOptionsHash()` call. The hash/JSON are computed exactly once at
startup by the first `setCompilers()`, which always runs before the
server starts listening.

Tests: new `test/app/compiler-changes-tests.ts` covering initial
application, no rescan when prediscovered, unchanged rescans skipped,
and changed rescans applied; plus an options-handler test pinning that
the hash is only computed once compilers are set. `ts-check`, `lint`,
and the full test suite pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 19:34:52 -05:00

626 lines
25 KiB
TypeScript

// 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<typeof properties.fakeProps>;
let compilerProps: properties.CompilerProps;
let optionsHandler: ClientOptionsHandler;
let fakeMoreCompilerProps: ReturnType<typeof properties.fakeProps>;
let moreCompilerProps: properties.CompilerProps;
let moreOptionsHandler: ClientOptionsHandler;
let env: CompilationEnvironment;
function createClientOptions(libs: ReturnType<ClientOptionsHandler['parseLibraries']>) {
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 auto-discover libs when libs= is not set', () => {
const libs = optionsHandler.parseLibraries({fake: ''});
const libIds = Object.keys(libs.fake).sort();
expect(libIds).toEqual(['fakelib', 'fs', 'someotherlib']);
expect(libs.fake.fakelib.name).toEqual('fake lib');
expect(Object.keys(libs.fake.fakelib.versions).sort()).toEqual(['noPaths', 'onePath', 'twoPaths']);
});
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 only compute the options hash once compilers are set', async () => {
const handler = new ClientOptionsHandler([], compilerProps, {env: ['dev']} as unknown as AppArguments);
expect(handler.getHash()).toEqual('');
expect(handler.getJSON()).toEqual('');
await handler.setCompilers([]);
expect(handler.getHash()).not.toEqual('');
expect(JSON.parse(handler.getJSON())).toHaveProperty('compilers', []);
});
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<string, Partial<TestBaseTool>>
>;
_.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',
buildPath: '/gpu/api/compiler/g123remote/build',
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);
});
});
});
});