Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/e-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const program = require('commander');
const evmConfig = require('./evm-config');
const { ensureNodeHeaders } = require('./utils/headers');
const { color, fatal } = require('./utils/logging');
const { ensureTestPrereqs } = require('./utils/prereqs');

function runSpecRunner(config, script, runnerArgs) {
const exec = process.execPath;
Expand Down Expand Up @@ -52,6 +53,9 @@ program
)
.action((specRunnerArgs, options) => {
try {
// Check for required Python modules on Linux before running tests
ensureTestPrereqs();

const config = evmConfig.current();
if (options.node && options.nan) {
fatal(
Expand Down
101 changes: 100 additions & 1 deletion src/utils/prereqs.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
const { execSync } = require('child_process');
const { fatal } = require('./logging');
const { color, fatal } = require('./logging');
const semver = require('semver');

const MINIMUM_PYTHON_VERSION = '3.9.0';
const MINIMUM_NODEJS_VERSION = '22.12.0';

/**
* Required Python modules for running Electron tests on Linux.
* These modules are needed for D-Bus mocking in the test suite.
* @see https://github.com/electron/build-tools/issues/790
*/
const LINUX_TEST_PYTHON_MODULES = [
{
name: 'dbusmock',
packageName: 'python-dbusmock',
description: 'D-Bus mock library for testing',
},
{
name: 'gi',
packageName: 'PyGObject',
description: 'Python GObject introspection bindings',
},
];

/**
* Check if Python is installed and meets minimum version requirements
* @returns {Object} Object with isValid boolean and version string
Expand Down Expand Up @@ -32,6 +50,48 @@ function checkPythonVersion() {
return false;
}

/**
* Get the available Python command
* @returns {string|null} The Python command or null if not found
*/
function getPythonCommand() {
const pythonCommands = ['python3', 'python'];

for (const command of pythonCommands) {
try {
execSync(`${command} --version`, {
encoding: 'utf8',
stdio: 'pipe',
});
return command;
} catch (error) {
continue;
}
}

return null;
}

/**
* Check if a Python module is installed
* @param {string} moduleName - The name of the Python module to check
* @returns {boolean} True if the module is installed, false otherwise
*/
function checkPythonModule(moduleName) {
const pythonCmd = getPythonCommand();
if (!pythonCmd) return false;

try {
execSync(`${pythonCmd} -c "import ${moduleName}"`, {
encoding: 'utf8',
stdio: 'pipe',
});
return true;
} catch (error) {
return false;
}
}

/**
* Check if Node.js is installed and meets minimum version requirements
* @returns {Object} Object with isValid boolean and version string
Expand Down Expand Up @@ -74,6 +134,45 @@ function ensurePrereqs() {
}
}

/**
* Ensure Python modules required for running Electron tests are installed.
* This check is only performed on Linux, as D-Bus mocking is Linux-specific.
* @see https://github.com/electron/build-tools/issues/790
*/
function ensureTestPrereqs() {
// D-Bus mocking is only required on Linux
if (process.platform !== 'linux') {
return;
}

const missingModules = [];

for (const module of LINUX_TEST_PYTHON_MODULES) {
if (!checkPythonModule(module.name)) {
missingModules.push(module);
}
}

if (missingModules.length > 0) {
const moduleList = missingModules
.map((m) => ` - ${color.cmd(m.packageName)} (${m.description})`)
.join('\n');

const installCmd = missingModules.map((m) => m.packageName).join(' ');

fatal(
`Missing Python modules required for running Electron tests on Linux:\n${moduleList}\n\n` +
`To install these modules, run:\n` +
` ${color.cmd(`pip install ${installCmd}`)}\n\n` +
`Note: ${color.cmd('PyGObject')} may require system dependencies. On Fedora/RHEL:\n` +
` ${color.cmd('sudo dnf install python3-devel gobject-introspection-devel cairo-gobject-devel')}\n` +
`On Ubuntu/Debian:\n` +
` ${color.cmd('sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev')}`,
);
}
}

module.exports = {
ensurePrereqs,
ensureTestPrereqs,
};
107 changes: 107 additions & 0 deletions tests/prereqs.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { execSync } from 'child_process';
import { beforeAll, afterAll, describe, expect, it, vi } from 'vitest';

const { ensurePrereqs, ensureTestPrereqs } = require('../src/utils/prereqs');

// Store original platform
const originalPlatform = process.platform;

/**
* Helper to mock process.platform
*/
function mockPlatform(platform) {
Object.defineProperty(process, 'platform', {
value: platform,
writable: true,
configurable: true,
});
}

/**
* Helper to restore process.platform
*/
function restorePlatform() {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
writable: true,
configurable: true,
});
}

describe('prereqs', () => {
describe('ensureTestPrereqs', () => {
afterAll(() => {
restorePlatform();
});

it('should skip checks on non-Linux platforms', () => {
// Mock process.platform to be darwin (macOS)
mockPlatform('darwin');

// Should not throw on non-Linux
expect(() => ensureTestPrereqs()).not.toThrow();

// Mock process.platform to be win32 (Windows)
mockPlatform('win32');

// Should not throw on non-Linux
expect(() => ensureTestPrereqs()).not.toThrow();

restorePlatform();
});

it('should check for required Python modules on Linux', () => {
// Only run this test on Linux
if (originalPlatform !== 'linux') {
return;
}

// Check if dbusmock is available
let dbusmockAvailable = false;
try {
execSync('python3 -c "import dbusmock"', { stdio: 'pipe' });
dbusmockAvailable = true;
} catch {
dbusmockAvailable = false;
}

// Check if gi is available
let giAvailable = false;
try {
execSync('python3 -c "import gi"', { stdio: 'pipe' });
giAvailable = true;
} catch {
giAvailable = false;
}

// If both modules are available, ensureTestPrereqs should not throw
// If any module is missing, it should throw with a helpful message
if (dbusmockAvailable && giAvailable) {
expect(() => ensureTestPrereqs()).not.toThrow();
} else {
// We expect it to exit with a fatal error
// Since fatal calls process.exit, we need to mock it
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
const mockError = vi.spyOn(console, 'error').mockImplementation(() => {});

expect(() => ensureTestPrereqs()).toThrow('process.exit called');

// Verify the error message contains helpful information
expect(mockError).toHaveBeenCalled();
const errorCall = mockError.mock.calls[0][0];

if (!dbusmockAvailable) {
expect(errorCall).toContain('python-dbusmock');
}
if (!giAvailable) {
expect(errorCall).toContain('PyGObject');
}

mockExit.mockRestore();
mockError.mockRestore();
}
});
});
});