diff --git a/src/e-test.js b/src/e-test.js index f0f559fb..0de4ef1d 100644 --- a/src/e-test.js +++ b/src/e-test.js @@ -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; @@ -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( diff --git a/src/utils/prereqs.js b/src/utils/prereqs.js index 730629b3..2ce7005a 100644 --- a/src/utils/prereqs.js +++ b/src/utils/prereqs.js @@ -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 @@ -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 @@ -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, }; diff --git a/tests/prereqs.spec.mjs b/tests/prereqs.spec.mjs new file mode 100644 index 00000000..ef6fa8f3 --- /dev/null +++ b/tests/prereqs.spec.mjs @@ -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(); + } + }); + }); +});