mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 09:23:52 -05:00
## Summary - Updates vitest and @vitest/coverage-v8 from 2.1.8 to 4.0.1 - Fixes test mocks to work with vitest 4.0's constructor support requirements ## Changes Vitest 4.0 now supports `vi.fn()` being used as constructors, which means arrow functions can no longer be used in mock implementations that will be called as constructors. ### Key refactorings: - **Class-based mocks in vi.mock()**: Defined mock classes directly in `vi.mock()` factories instead of using `.mockImplementation()` with arrow functions - **Shared mock implementations**: Created shared `vi.fn()` instances that can be configured per-test, avoiding the need for `.mockImplementation()` - **Biome linter compatibility**: The approach avoids `.mockImplementation()` entirely, preventing biome from converting `function` keywords back to arrow functions ### Files changed: - `package.json`, `package-lock.json`: vitest version updates - `test/app/config-tests.ts`: Refactored CompilerProps mocks with shared cePropsImpl - `test/app/main-tests.ts`: Refactored all constructor mocks (CompilerFinder, FormattingService, CompileHandler, etc.) ## Test plan - [x] All tests pass with `npm run test-min` - [x] Linter passes with `npm run lint` - [x] Type checking passes with `npm run ts-check` - [x] Pre-commit hooks pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
342 lines
13 KiB
TypeScript
342 lines
13 KiB
TypeScript
// Copyright (c) 2025, 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 express from 'express';
|
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
import {initialiseApplication} from '../../lib/app/main.js';
|
|
import * as server from '../../lib/app/server.js';
|
|
import {AppArguments} from '../../lib/app.interfaces.js';
|
|
import * as aws from '../../lib/aws.js';
|
|
import {CompilationQueue} from '../../lib/compilation-queue.js';
|
|
import * as exec from '../../lib/exec.js';
|
|
import {RemoteExecutionQuery} from '../../lib/execution/execution-query.js';
|
|
import * as execTriple from '../../lib/execution/execution-triple.js';
|
|
import * as execQueue from '../../lib/execution/sqs-execution-queue.js';
|
|
import {logger} from '../../lib/logger.js';
|
|
import {setupMetricsServer} from '../../lib/metrics-server.js';
|
|
import * as sentry from '../../lib/sentry.js';
|
|
import * as sponsors from '../../lib/sponsors.js';
|
|
import {getStorageTypeByKey} from '../../lib/storage/index.js';
|
|
|
|
// We need to mock all these modules to avoid actual API calls
|
|
// Vitest 4.0 requires constructor mocks to be actual classes/functions, not arrow functions.
|
|
// We define classes directly here because .mockImplementation() gets auto-converted to arrow
|
|
// functions by Biome, which breaks constructor calls. Do not refactor to arrow functions!
|
|
vi.mock('../../lib/aws.js');
|
|
vi.mock('../../lib/exec.js');
|
|
vi.mock('../../lib/execution/execution-query.js');
|
|
vi.mock('../../lib/execution/execution-triple.js');
|
|
vi.mock('../../lib/execution/sqs-execution-queue.js');
|
|
vi.mock('../../lib/compilation-env.js', () => ({
|
|
CompilationEnvironment: class {
|
|
setCompilerFinder = vi.fn();
|
|
},
|
|
}));
|
|
vi.mock('../../lib/compilation-queue.js');
|
|
// Shared mock implementations for CompilerFinder
|
|
const mockCompilerFinderFind = vi.fn();
|
|
const mockCompilerFinderLoadPrediscovered = vi.fn();
|
|
const mockCompilerFinderCompileHandler = {findCompiler: vi.fn()};
|
|
|
|
vi.mock('../../lib/compiler-finder.js', () => ({
|
|
CompilerFinder: class {
|
|
find = mockCompilerFinderFind;
|
|
loadPrediscovered = mockCompilerFinderLoadPrediscovered;
|
|
compileHandler = mockCompilerFinderCompileHandler;
|
|
},
|
|
}));
|
|
vi.mock('../../lib/formatting-service.js', () => ({
|
|
FormattingService: class {
|
|
initialize = vi.fn().mockResolvedValue(undefined);
|
|
},
|
|
}));
|
|
vi.mock('../../lib/handlers/compile.js', () => ({
|
|
CompileHandler: class {
|
|
findCompiler = vi.fn().mockReturnValue({
|
|
possibleArguments: {possibleArguments: []},
|
|
});
|
|
handle = vi.fn();
|
|
},
|
|
}));
|
|
vi.mock('../../lib/handlers/noscript.js', () => ({
|
|
NoScriptHandler: class {
|
|
initializeRoutes = vi.fn();
|
|
createRouter = vi.fn().mockReturnValue({});
|
|
},
|
|
}));
|
|
vi.mock('../../lib/handlers/route-api.js', () => ({
|
|
RouteAPI: class {
|
|
apiHandler = {
|
|
setCompilers: vi.fn(),
|
|
setLanguages: vi.fn(),
|
|
setOptions: vi.fn(),
|
|
};
|
|
initializeRoutes = vi.fn();
|
|
},
|
|
}));
|
|
vi.mock('../../lib/metrics-server.js');
|
|
vi.mock('../../lib/options-handler.js', () => ({
|
|
ClientOptionsHandler: class {
|
|
setCompilers = vi.fn().mockResolvedValue(undefined);
|
|
get = vi.fn();
|
|
getHash = vi.fn();
|
|
getJSON = vi.fn();
|
|
},
|
|
}));
|
|
vi.mock('../../lib/sentry.js');
|
|
vi.mock('../../lib/sponsors.js');
|
|
vi.mock('../../lib/storage/index.js');
|
|
vi.mock('../../lib/app/server.js');
|
|
vi.mock('node:fs/promises');
|
|
|
|
// Mock the routes-setup module with a simplified implementation that doesn't use the controllers
|
|
vi.mock('../../lib/app/routes-setup.js', () => ({
|
|
setupRoutesAndApi: vi.fn().mockImplementation(() => ({
|
|
apiHandler: {
|
|
setCompilers: vi.fn(),
|
|
setLanguages: vi.fn(),
|
|
setOptions: vi.fn(),
|
|
},
|
|
initializeRoutes: vi.fn(),
|
|
})),
|
|
}));
|
|
|
|
// Also mock the compiler-changes module to avoid the apiHandler issue
|
|
vi.mock('../../lib/app/compiler-changes.js', () => ({
|
|
setupCompilerChangeHandling: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
describe('Main module', () => {
|
|
const mockAppArgs: AppArguments = {
|
|
rootDir: '/test/root',
|
|
env: ['test'],
|
|
port: 10240,
|
|
gitReleaseName: 'test-release',
|
|
releaseBuildNumber: '123',
|
|
wantedLanguages: ['c++'],
|
|
doCache: true,
|
|
fetchCompilersFromRemote: true,
|
|
ensureNoCompilerClash: false,
|
|
prediscovered: undefined,
|
|
discoveryOnly: undefined,
|
|
staticPath: undefined,
|
|
metricsPort: undefined,
|
|
useLocalProps: true,
|
|
propDebug: false,
|
|
tmpDir: undefined,
|
|
isWsl: false,
|
|
devMode: false,
|
|
loggingOptions: {
|
|
debug: false,
|
|
suppressConsoleLog: false,
|
|
paperTrailIdentifier: 'test',
|
|
},
|
|
};
|
|
|
|
const mockConfig = {
|
|
ceProps: vi.fn((_key: string, defaultValue?: any) => defaultValue),
|
|
compilerProps: {
|
|
ceProps: vi.fn((_key: string, defaultValue?: any) => defaultValue),
|
|
},
|
|
languages: {},
|
|
staticMaxAgeSecs: 60,
|
|
maxUploadSize: '1mb',
|
|
extraBodyClass: '',
|
|
storageSolution: 'local',
|
|
httpRoot: '/',
|
|
staticRoot: '/static',
|
|
staticUrl: undefined,
|
|
};
|
|
|
|
const mockCompilers = [
|
|
{
|
|
id: 'gcc',
|
|
name: 'GCC',
|
|
lang: 'c++',
|
|
},
|
|
];
|
|
|
|
// Setup mocks
|
|
beforeEach(() => {
|
|
vi.spyOn(logger, 'info').mockImplementation(() => logger);
|
|
vi.spyOn(logger, 'warn').mockImplementation(() => logger);
|
|
vi.spyOn(logger, 'debug').mockImplementation(() => logger);
|
|
|
|
vi.mocked(aws.initConfig).mockResolvedValue();
|
|
vi.mocked(aws.getConfig).mockReturnValue('sentinel');
|
|
vi.mocked(sentry.SetupSentry).mockReturnValue();
|
|
vi.mocked(exec.startWineInit).mockReturnValue();
|
|
vi.mocked(RemoteExecutionQuery.initRemoteExecutionArchs).mockReturnValue();
|
|
|
|
// Classes are mocked with proper constructors in vi.mock above
|
|
// Configure the default behavior for CompilerFinder methods
|
|
mockCompilerFinderFind.mockResolvedValue({compilers: mockCompilers, foundClash: false});
|
|
mockCompilerFinderLoadPrediscovered.mockResolvedValue(mockCompilers);
|
|
mockCompilerFinderCompileHandler.findCompiler.mockReturnValue({possibleArguments: {possibleArguments: []}});
|
|
|
|
const mockCompilationQueue = {
|
|
queue: vi.fn(),
|
|
};
|
|
vi.mocked(CompilationQueue.fromProps).mockReturnValue(mockCompilationQueue as unknown as CompilationQueue);
|
|
|
|
const mockStorageType = vi.fn();
|
|
vi.mocked(getStorageTypeByKey).mockReturnValue(
|
|
mockStorageType as unknown as ReturnType<typeof getStorageTypeByKey>,
|
|
);
|
|
|
|
vi.mocked(mockConfig.ceProps).mockImplementation((key: string, defaultValue?: any) => {
|
|
if (key === 'execqueue.is_worker') return false;
|
|
if (key === 'healthCheckFilePath') return null;
|
|
if (key === 'sentrySlowRequestMs') return 0;
|
|
if (key === 'rescanCompilerSecs') return 0;
|
|
return defaultValue;
|
|
});
|
|
|
|
vi.mocked(sponsors.loadSponsorsFromString).mockResolvedValue({
|
|
getLevels: vi.fn().mockReturnValue([]),
|
|
pickTopIcons: vi.fn().mockReturnValue([]),
|
|
getAllTopIcons: vi.fn().mockReturnValue([]),
|
|
} as unknown as ReturnType<typeof sponsors.loadSponsorsFromString>);
|
|
vi.mocked(fs.readFile).mockResolvedValue('sponsors: []');
|
|
|
|
const mockRouter = {
|
|
use: vi.fn().mockReturnThis(),
|
|
};
|
|
|
|
const mockWebServerResult = {
|
|
webServer: {} as express.Express,
|
|
router: mockRouter as unknown as express.Router,
|
|
renderConfig: vi.fn(),
|
|
renderGoldenLayout: vi.fn(),
|
|
pugRequireHandler: vi.fn(),
|
|
};
|
|
vi.mocked(server.setupWebServer).mockResolvedValue(mockWebServerResult);
|
|
vi.mocked(server.startListening).mockImplementation(() => {});
|
|
// NoScriptHandler, RouteAPI, and ClientOptionsHandler are already properly mocked with classes
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should initialize and return a web server', async () => {
|
|
const result = await initialiseApplication({
|
|
appArgs: mockAppArgs,
|
|
config: mockConfig as any,
|
|
distPath: '/test/dist',
|
|
awsProps: vi.fn() as any,
|
|
});
|
|
|
|
// Verify the application was properly initialized
|
|
expect(result).toHaveProperty('webServer');
|
|
expect(aws.initConfig).toHaveBeenCalled();
|
|
expect(sentry.SetupSentry).toHaveBeenCalled();
|
|
expect(exec.startWineInit).toHaveBeenCalled();
|
|
expect(RemoteExecutionQuery.initRemoteExecutionArchs).toHaveBeenCalled();
|
|
expect(CompilationQueue.fromProps).toHaveBeenCalled();
|
|
expect(server.setupWebServer).toHaveBeenCalled();
|
|
expect(server.startListening).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should load prediscovered compilers if provided', async () => {
|
|
vi.mocked(fs.readFile).mockImplementation(async path => {
|
|
if (typeof path === 'string' && path.includes('prediscovered')) {
|
|
return JSON.stringify(mockCompilers);
|
|
}
|
|
return 'sponsors: []';
|
|
});
|
|
|
|
const result = await initialiseApplication({
|
|
appArgs: {
|
|
...mockAppArgs,
|
|
prediscovered: '/path/to/prediscovered.json',
|
|
},
|
|
config: mockConfig as any,
|
|
distPath: '/test/dist',
|
|
awsProps: vi.fn() as any,
|
|
});
|
|
|
|
expect(result).toHaveProperty('webServer');
|
|
// The actual prediscovered compiler loading is tested in compiler-discovery-tests.ts
|
|
});
|
|
|
|
it('should set up metrics server if configured', async () => {
|
|
await initialiseApplication({
|
|
appArgs: {
|
|
...mockAppArgs,
|
|
metricsPort: 9000,
|
|
},
|
|
config: mockConfig as any,
|
|
distPath: '/test/dist',
|
|
awsProps: vi.fn() as any,
|
|
});
|
|
|
|
expect(setupMetricsServer).toHaveBeenCalledWith(9000, undefined);
|
|
});
|
|
|
|
it('should initialize execution worker if configured', async () => {
|
|
vi.mocked(mockConfig.ceProps).mockImplementation((key: string, defaultValue?: any) => {
|
|
if (key === 'execqueue.is_worker') return true;
|
|
return defaultValue;
|
|
});
|
|
|
|
await initialiseApplication({
|
|
appArgs: mockAppArgs,
|
|
config: mockConfig as any,
|
|
distPath: '/test/dist',
|
|
awsProps: vi.fn() as any,
|
|
});
|
|
|
|
expect(execTriple.initHostSpecialties).toHaveBeenCalled();
|
|
expect(execQueue.startExecutionWorkerThread).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should throw an error if no compilers are found', async () => {
|
|
mockCompilerFinderFind.mockResolvedValue({compilers: [], foundClash: false});
|
|
|
|
await expect(
|
|
initialiseApplication({
|
|
appArgs: mockAppArgs,
|
|
config: mockConfig as any,
|
|
distPath: '/test/dist',
|
|
awsProps: vi.fn() as any,
|
|
}),
|
|
).rejects.toThrow('Unexpected failure, no compilers found!');
|
|
});
|
|
|
|
it('should throw an error if there are compiler clashes and ensureNoCompilerClash is set', async () => {
|
|
mockCompilerFinderFind.mockResolvedValue({compilers: mockCompilers, foundClash: true});
|
|
|
|
await expect(
|
|
initialiseApplication({
|
|
appArgs: {...mockAppArgs, ensureNoCompilerClash: true},
|
|
config: mockConfig as any,
|
|
distPath: '/test/dist',
|
|
awsProps: vi.fn() as any,
|
|
}),
|
|
).rejects.toThrow('Clashing compilers in the current environment found!');
|
|
});
|
|
});
|