diff --git a/src/extension.ts b/src/extension.ts index 09e9b3d1..75c60395 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -515,34 +515,44 @@ export async function activate(context: ExtensionContext): Promise { - // This is the finder that is used by all the built in environment managers - const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context); - context.subscriptions.push(nativeFinder); - const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel); - sysPythonManager.resolve(sysMgr); - await Promise.all([ - registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr), - registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager), - registerPyenvFeatures(nativeFinder, context.subscriptions, projectManager), - registerPipenvFeatures(nativeFinder, context.subscriptions, projectManager), - registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager), - shellStartupVarsMgr.initialize(), - ]); - - await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api); - - // Register manager-agnostic terminal watcher for package-modifying commands - registerTerminalPackageWatcher(api, terminalActivation, outputChannel, context.subscriptions); - - // Register listener for interpreter settings changes for interpreter re-selection - context.subscriptions.push( - registerInterpreterSettingsChangeListener(envManagers, projectManager, nativeFinder, api), - ); + try { + // This is the finder that is used by all the built in environment managers + const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context); + context.subscriptions.push(nativeFinder); + const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel); + sysPythonManager.resolve(sysMgr); + await Promise.all([ + registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr), + registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager), + registerPyenvFeatures(nativeFinder, context.subscriptions, projectManager), + registerPipenvFeatures(nativeFinder, context.subscriptions, projectManager), + registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager), + shellStartupVarsMgr.initialize(), + ]); + + await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api); + + // Register manager-agnostic terminal watcher for package-modifying commands + registerTerminalPackageWatcher(api, terminalActivation, outputChannel, context.subscriptions); + + // Register listener for interpreter settings changes for interpreter re-selection + context.subscriptions.push( + registerInterpreterSettingsChangeListener(envManagers, projectManager, nativeFinder, api), + ); - sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime); - await terminalManager.initialize(api); - sendManagerSelectionTelemetry(projectManager); - await sendProjectStructureTelemetry(projectManager, envManagers); + sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime); + await terminalManager.initialize(api); + sendManagerSelectionTelemetry(projectManager); + await sendProjectStructureTelemetry(projectManager, envManagers); + } catch (error) { + traceError('Failed to initialize environment managers:', error); + // Show a user-friendly error message + window.showErrorMessage( + l10n.t( + 'Python Environments: Failed to initialize environment managers. Some features may not work correctly. Check the Output panel for details.', + ), + ); + } }); sendTelemetryEvent(EventNames.EXTENSION_ACTIVATION_DURATION, start.elapsedTime); diff --git a/src/test/smoke/functional.smoke.test.ts b/src/test/smoke/functional.smoke.test.ts new file mode 100644 index 00000000..e4f5ae2e --- /dev/null +++ b/src/test/smoke/functional.smoke.test.ts @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Smoke Test: Functional Checks + * + * PURPOSE: + * Verify that core extension features actually work, not just that they're registered. + * These tests require Python to be installed and may have side effects. + * + * WHAT THIS TESTS: + * 1. Environment discovery returns results + * 2. Projects API works correctly + * 3. Environment variables API works + * 4. Settings are not polluted on activation + */ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { PythonEnvironmentApi } from '../../api'; +import { ENVS_EXTENSION_ID, MAX_EXTENSION_ACTIVATION_TIME } from '../constants'; +import { waitForApiReady, waitForCondition } from '../testUtils'; + +suite('Smoke: Functional Checks', function () { + this.timeout(MAX_EXTENSION_ACTIVATION_TIME); + + let api: PythonEnvironmentApi; + let managersReady = false; + + suiteSetup(async function () { + const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID); + assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`); + + if (!extension.isActive) { + await extension.activate(); + await waitForCondition(() => extension.isActive, 30_000, 'Extension did not activate'); + } + + api = extension.exports; + assert.ok(api, 'API not exported'); + + // Wait for environment managers to register (happens async in setImmediate) + // This may fail in CI if the pet binary is not available + const result = await waitForApiReady(api, 45_000); + managersReady = result.ready; + if (!result.ready) { + console.log(`[WARN] Managers not ready: ${result.error}`); + console.log('[WARN] Tests requiring managers will be skipped'); + } + }); + + // ========================================================================= + // ENVIRONMENT DISCOVERY - Core feature must work + // ========================================================================= + + test('getEnvironments returns an array', async function () { + // Skip if managers aren't ready (e.g., pet binary not available in CI) + if (!managersReady) { + this.skip(); + return; + } + + // This test verifies discovery machinery works + // Even if no Python is installed, it should return an empty array, not throw + + const environments = await api.getEnvironments('all'); + + assert.ok(Array.isArray(environments), 'getEnvironments("all") should return an array'); + }); + + test('getEnvironments finds Python installations when available', async function () { + // Skip if managers aren't ready (e.g., pet binary not available in CI) + if (!managersReady) { + this.skip(); + return; + } + + // Skip this test if no Python is expected (CI without Python) + if (process.env.SKIP_PYTHON_TESTS) { + this.skip(); + return; + } + + const environments = await api.getEnvironments('all'); + + // On a typical dev machine, we expect at least one Python + // This test may need to be conditional based on CI environment + if (environments.length === 0) { + console.log('[WARN] No Python environments found - is Python installed?'); + // Don't fail - just warn. CI may not have Python. + return; + } + + // Verify environment structure + const env = environments[0]; + assert.ok(env.envId, 'Environment should have envId'); + assert.ok(env.envId.id, 'envId.id should be defined'); + assert.ok(env.envId.managerId, 'envId.managerId should be defined'); + assert.ok(env.name, 'Environment should have a name'); + assert.ok(env.version, 'Environment should have a version'); + assert.ok(env.environmentPath, 'Environment should have environmentPath'); + }); + + test('getEnvironments with scope "global" returns global interpreters', async function () { + // Skip if managers aren't ready (e.g., pet binary not available in CI) + if (!managersReady) { + this.skip(); + return; + } + + const globalEnvs = await api.getEnvironments('global'); + + assert.ok(Array.isArray(globalEnvs), 'getEnvironments("global") should return an array'); + + // Global environments are system Python installations + // They should be a subset of 'all' environments + const allEnvs = await api.getEnvironments('all'); + assert.ok(globalEnvs.length <= allEnvs.length, 'Global environments should be a subset of all environments'); + }); + + test('refreshEnvironments completes without error', async function () { + // Skip if managers aren't ready (e.g., pet binary not available in CI) + if (!managersReady) { + this.skip(); + return; + } + + // This should not throw + await api.refreshEnvironments(undefined); + + // Verify we can still get environments after refresh + const environments = await api.getEnvironments('all'); + assert.ok(Array.isArray(environments), 'Should be able to get environments after refresh'); + }); + + // ========================================================================= + // PROJECTS - Core project management features + // ========================================================================= + + test('getPythonProjects returns workspace folders by default', function () { + const projects = api.getPythonProjects(); + + assert.ok(Array.isArray(projects), 'getPythonProjects should return an array'); + + // By default, workspace folders are treated as projects + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + assert.ok(projects.length > 0, 'With workspace folders open, there should be at least one project'); + + // Verify project structure + const project = projects[0]; + assert.ok(project.name, 'Project should have a name'); + assert.ok(project.uri, 'Project should have a uri'); + } + }); + + test('getPythonProject returns undefined for non-existent path', function () { + const fakeUri = vscode.Uri.file('/this/path/does/not/exist/anywhere'); + const project = api.getPythonProject(fakeUri); + + // Should return undefined, not throw + assert.strictEqual(project, undefined, 'getPythonProject should return undefined for non-existent path'); + }); + + // ========================================================================= + // ENVIRONMENT SELECTION - Get/Set environment + // ========================================================================= + + test('getEnvironment returns undefined or a valid environment', async function () { + // Skip if managers aren't ready (e.g., pet binary not available in CI) + if (!managersReady) { + this.skip(); + return; + } + + // With no explicit selection, may return undefined or auto-selected env + const env = await api.getEnvironment(undefined); + + if (env !== undefined) { + // If an environment is returned, verify its structure + assert.ok(env.envId, 'Returned environment should have envId'); + assert.ok(env.name, 'Returned environment should have name'); + } + // undefined is also valid - no environment selected + }); + + // ========================================================================= + // ENVIRONMENT VARIABLES - .env file support + // ========================================================================= + + test('getEnvironmentVariables returns an object', async function () { + const envVars = await api.getEnvironmentVariables(undefined); + + assert.ok(envVars !== null, 'getEnvironmentVariables should not return null'); + assert.ok(typeof envVars === 'object', 'getEnvironmentVariables should return an object'); + + // Should at least contain PATH or similar system variables + // (merged from process.env by default) + const hasKeys = Object.keys(envVars).length > 0; + assert.ok(hasKeys, 'Environment variables object should have some entries'); + }); + + test('getEnvironmentVariables with workspace uri works', async function () { + const workspaceFolders = vscode.workspace.workspaceFolders; + + if (!workspaceFolders || workspaceFolders.length === 0) { + this.skip(); + return; + } + + const workspaceUri = workspaceFolders[0].uri; + const envVars = await api.getEnvironmentVariables(workspaceUri); + + assert.ok(envVars !== null, 'getEnvironmentVariables with workspace uri should not return null'); + assert.ok(typeof envVars === 'object', 'Should return an object'); + }); + + // ========================================================================= + // RESOLVE ENVIRONMENT - Detailed environment info + // ========================================================================= + + test('resolveEnvironment handles invalid path gracefully', async function () { + // Skip if managers aren't ready (e.g., pet binary not available in CI) + if (!managersReady) { + this.skip(); + return; + } + + const fakeUri = vscode.Uri.file('/this/is/not/a/python/installation'); + + // Should return undefined, not throw + const resolved = await api.resolveEnvironment(fakeUri); + assert.strictEqual(resolved, undefined, 'resolveEnvironment should return undefined for invalid path'); + }); + + test('resolveEnvironment returns full details for valid environment', async function () { + // Skip if managers aren't ready (e.g., pet binary not available in CI) + if (!managersReady) { + this.skip(); + return; + } + + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + // Try to resolve the first environment's path + const env = environments[0]; + const resolved = await api.resolveEnvironment(env.environmentPath); + + if (resolved) { + // Verify resolved environment has execution info + assert.ok(resolved.execInfo, 'Resolved environment should have execInfo'); + assert.ok(resolved.execInfo.run, 'execInfo should have run configuration'); + assert.ok(resolved.execInfo.run.executable, 'run should have executable path'); + } + }); + + // ========================================================================= + // PACKAGES - Package listing (read-only) + // ========================================================================= + + test('getPackages returns array or undefined for valid environment', async function () { + // Skip if managers aren't ready (e.g., pet binary not available in CI) + if (!managersReady) { + this.skip(); + return; + } + + const environments = await api.getEnvironments('all'); + + if (environments.length === 0) { + this.skip(); + return; + } + + const env = environments[0]; + const packages = await api.getPackages(env); + + // Should return array or undefined, not throw + assert.ok(packages === undefined || Array.isArray(packages), 'getPackages should return undefined or an array'); + + // If packages exist, verify structure + if (packages && packages.length > 0) { + const pkg = packages[0]; + assert.ok(pkg.pkgId, 'Package should have pkgId'); + assert.ok(pkg.name, 'Package should have name'); + } + }); +}); diff --git a/src/test/smoke/registration.smoke.test.ts b/src/test/smoke/registration.smoke.test.ts new file mode 100644 index 00000000..cf297b05 --- /dev/null +++ b/src/test/smoke/registration.smoke.test.ts @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Smoke Test: Registration Checks + * + * PURPOSE: + * Comprehensive verification that all commands, API methods, and events + * are properly registered and accessible. These are fast, deterministic + * checks with no side effects. + * + * WHAT THIS TESTS: + * 1. All extension commands are registered with VS Code + * 2. API surface is correct (methods exist and are functions) + * 3. Event emitters are properly exposed + * 4. Environment managers are registered + */ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { PythonEnvironmentApi } from '../../api'; +import { ENVS_EXTENSION_ID, MAX_EXTENSION_ACTIVATION_TIME } from '../constants'; +import { waitForApiReady, waitForCondition } from '../testUtils'; + +suite('Smoke: Registration Checks', function () { + this.timeout(MAX_EXTENSION_ACTIVATION_TIME); + + let api: PythonEnvironmentApi; + let managersReady = false; + + suiteSetup(async function () { + const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID); + assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`); + + if (!extension.isActive) { + await extension.activate(); + await waitForCondition(() => extension.isActive, 30_000, 'Extension did not activate'); + } + + api = extension.exports; + assert.ok(api, 'API not exported'); + + // Wait for environment managers to register (happens async in setImmediate) + // This may fail in CI if the pet binary is not available + const result = await waitForApiReady(api, 45_000); + managersReady = result.ready; + if (!result.ready) { + console.log(`[WARN] Managers not ready: ${result.error}`); + console.log('[WARN] Some tests will be skipped'); + } + }); + + // ========================================================================= + // COMMANDS - All extension commands must be registered + // ========================================================================= + + test('All extension commands are registered', async function () { + const allCommands = await vscode.commands.getCommands(true); + + // Complete list of commands from package.json + const requiredCommands = [ + // Environment management + 'python-envs.create', + 'python-envs.createAny', + 'python-envs.set', + 'python-envs.setEnv', + 'python-envs.setEnvSelected', + 'python-envs.remove', + 'python-envs.setEnvManager', + 'python-envs.setPkgManager', + 'python-envs.refreshAllManagers', + 'python-envs.clearCache', + 'python-envs.searchSettings', + + // Package management + 'python-envs.packages', + 'python-envs.refreshPackages', + 'python-envs.uninstallPackage', + + // Projects + 'python-envs.addPythonProject', + 'python-envs.addPythonProjectGivenResource', + 'python-envs.removePythonProject', + 'python-envs.createNewProjectFromTemplate', + 'python-envs.revealProjectInExplorer', + + // Terminal + 'python-envs.createTerminal', + 'python-envs.runInTerminal', + 'python-envs.runAsTask', + 'python-envs.terminal.activate', + 'python-envs.terminal.deactivate', + 'python-envs.terminal.revertStartupScriptChanges', + + // Utility + 'python-envs.copyEnvPath', + 'python-envs.copyEnvPathCopied', + 'python-envs.copyProjectPath', + 'python-envs.copyProjectPathCopied', + 'python-envs.revealEnvInManagerView', + 'python-envs.reportIssue', + 'python-envs.runPetInTerminal', + ]; + + const missingCommands: string[] = []; + + for (const cmd of requiredCommands) { + if (!allCommands.includes(cmd)) { + missingCommands.push(cmd); + } + } + + assert.strictEqual( + missingCommands.length, + 0, + `Missing commands:\n${missingCommands.map((c) => ` - ${c}`).join('\n')}\n\n` + + 'Check that each command is defined in package.json and registered in the extension.', + ); + }); + + // ========================================================================= + // API METHODS - All API methods must exist and be functions + // ========================================================================= + + test('Environment management API methods exist', function () { + // PythonEnvironmentsApi + assert.strictEqual(typeof api.refreshEnvironments, 'function', 'refreshEnvironments should be a function'); + assert.strictEqual(typeof api.getEnvironments, 'function', 'getEnvironments should be a function'); + assert.strictEqual(typeof api.resolveEnvironment, 'function', 'resolveEnvironment should be a function'); + + // PythonEnvironmentManagementApi + assert.strictEqual(typeof api.createEnvironment, 'function', 'createEnvironment should be a function'); + assert.strictEqual(typeof api.removeEnvironment, 'function', 'removeEnvironment should be a function'); + + // PythonProjectEnvironmentApi + assert.strictEqual(typeof api.setEnvironment, 'function', 'setEnvironment should be a function'); + assert.strictEqual(typeof api.getEnvironment, 'function', 'getEnvironment should be a function'); + + // PythonEnvironmentItemApi + assert.strictEqual( + typeof api.createPythonEnvironmentItem, + 'function', + 'createPythonEnvironmentItem should be a function', + ); + + // PythonEnvironmentManagerRegistrationApi + assert.strictEqual( + typeof api.registerEnvironmentManager, + 'function', + 'registerEnvironmentManager should be a function', + ); + }); + + test('Package management API methods exist', function () { + // PythonPackageGetterApi + assert.strictEqual(typeof api.refreshPackages, 'function', 'refreshPackages should be a function'); + assert.strictEqual(typeof api.getPackages, 'function', 'getPackages should be a function'); + + // PythonPackageManagementApi + assert.strictEqual(typeof api.managePackages, 'function', 'managePackages should be a function'); + + // PythonPackageItemApi + assert.strictEqual(typeof api.createPackageItem, 'function', 'createPackageItem should be a function'); + + // PythonPackageManagerRegistrationApi + assert.strictEqual( + typeof api.registerPackageManager, + 'function', + 'registerPackageManager should be a function', + ); + }); + + test('Project API methods exist', function () { + // PythonProjectGetterApi + assert.strictEqual(typeof api.getPythonProjects, 'function', 'getPythonProjects should be a function'); + assert.strictEqual(typeof api.getPythonProject, 'function', 'getPythonProject should be a function'); + + // PythonProjectModifyApi + assert.strictEqual(typeof api.addPythonProject, 'function', 'addPythonProject should be a function'); + assert.strictEqual(typeof api.removePythonProject, 'function', 'removePythonProject should be a function'); + + // PythonProjectCreationApi + assert.strictEqual( + typeof api.registerPythonProjectCreator, + 'function', + 'registerPythonProjectCreator should be a function', + ); + }); + + test('Execution API methods exist', function () { + // PythonTerminalCreateApi + assert.strictEqual(typeof api.createTerminal, 'function', 'createTerminal should be a function'); + + // PythonTerminalRunApi + assert.strictEqual(typeof api.runInTerminal, 'function', 'runInTerminal should be a function'); + assert.strictEqual( + typeof api.runInDedicatedTerminal, + 'function', + 'runInDedicatedTerminal should be a function', + ); + + // PythonTaskRunApi + assert.strictEqual(typeof api.runAsTask, 'function', 'runAsTask should be a function'); + + // PythonBackgroundRunApi + assert.strictEqual(typeof api.runInBackground, 'function', 'runInBackground should be a function'); + }); + + test('Environment variables API methods exist', function () { + assert.strictEqual( + typeof api.getEnvironmentVariables, + 'function', + 'getEnvironmentVariables should be a function', + ); + }); + + // ========================================================================= + // API EVENTS - All events must be defined + // ========================================================================= + + test('Environment events are defined', function () { + // Check events exist and have the expected shape + assert.ok(api.onDidChangeEnvironments, 'onDidChangeEnvironments should be defined'); + assert.ok(api.onDidChangeEnvironment, 'onDidChangeEnvironment should be defined'); + + // Events should be subscribable (have a function signature) + assert.strictEqual( + typeof api.onDidChangeEnvironments, + 'function', + 'onDidChangeEnvironments should be subscribable', + ); + assert.strictEqual( + typeof api.onDidChangeEnvironment, + 'function', + 'onDidChangeEnvironment should be subscribable', + ); + }); + + test('Package events are defined', function () { + assert.ok(api.onDidChangePackages, 'onDidChangePackages should be defined'); + assert.strictEqual(typeof api.onDidChangePackages, 'function', 'onDidChangePackages should be subscribable'); + }); + + test('Project events are defined', function () { + assert.ok(api.onDidChangePythonProjects, 'onDidChangePythonProjects should be defined'); + assert.strictEqual( + typeof api.onDidChangePythonProjects, + 'function', + 'onDidChangePythonProjects should be subscribable', + ); + }); + + test('Environment variables events are defined', function () { + assert.ok(api.onDidChangeEnvironmentVariables, 'onDidChangeEnvironmentVariables should be defined'); + assert.strictEqual( + typeof api.onDidChangeEnvironmentVariables, + 'function', + 'onDidChangeEnvironmentVariables should be subscribable', + ); + }); + + // ========================================================================= + // ENVIRONMENT MANAGERS - Built-in managers should be registered + // ========================================================================= + + test('Built-in environment managers are registered', async function () { + // Skip if managers aren't ready (e.g., pet binary not available in CI) + if (!managersReady) { + this.skip(); + return; + } + + // Get all environments to verify managers are working + const environments = await api.getEnvironments('all'); + + // We can't guarantee environments exist, but the call should succeed + assert.ok(Array.isArray(environments), 'getEnvironments should return an array'); + + // If environments exist, verify they have the expected shape + if (environments.length > 0) { + const env = environments[0]; + assert.ok(env.envId, 'Environment should have envId'); + assert.ok(env.envId.id, 'envId should have id'); + assert.ok(env.envId.managerId, 'envId should have managerId'); + assert.ok(env.name, 'Environment should have name'); + assert.ok(env.displayName, 'Environment should have displayName'); + } + }); + + // ========================================================================= + // PROJECTS - Project API should be callable + // ========================================================================= + + test('getPythonProjects is callable', function () { + // Should not throw, even if no projects are configured + const projects = api.getPythonProjects(); + assert.ok(Array.isArray(projects), 'getPythonProjects should return an array'); + }); +}); diff --git a/src/test/testUtils.ts b/src/test/testUtils.ts index 8e550bb5..5d0e4195 100644 --- a/src/test/testUtils.ts +++ b/src/test/testUtils.ts @@ -114,6 +114,59 @@ export async function retryUntilSuccess( throw new Error(`${errorMessage}: ${lastError?.message || 'validation failed'}`); } +/** + * Result of waiting for API readiness. + */ +export interface ApiReadyResult { + /** Whether the API is ready and managers have registered */ + ready: boolean; + /** Error message if not ready */ + error?: string; +} + +/** + * Wait for the API to be fully ready, including manager registration. + * + * This waits for the async initialization that happens in setImmediate() after + * extension.activate() returns. Without this, calls to getEnvironments() may + * hang waiting for managers that haven't registered yet. + * + * @param api - The Python environment API + * @param timeoutMs - Maximum time to wait (default: 30 seconds) + * @returns Result indicating if API is ready + * + * @example + * const result = await waitForApiReady(api, 30_000); + * if (!result.ready) { + * this.skip(); // Skip test if managers not available + * } + */ +export async function waitForApiReady( + api: { getEnvironments: (scope: 'all' | 'global') => Promise }, + timeoutMs: number = 30_000, +): Promise { + // Race the getEnvironments call against a timeout + // This ensures managers have registered before we proceed + const timeoutPromise = new Promise((_, reject) => { + setTimeout( + () => reject(new Error(`API not ready within ${timeoutMs}ms - managers may not have registered`)), + timeoutMs, + ); + }); + + try { + // If getEnvironments completes, managers are ready + await Promise.race([api.getEnvironments('all'), timeoutPromise]); + return { ready: true }; + } catch (error) { + // Return error info instead of throwing + return { + ready: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} + /** * Helper class to test events. *