/* * Copyright (C) 2024-present Puter Technologies Inc. * * This file is part of Puter. * * Puter is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import { AdvancedBase } from '@heyputer/putility'; import useapi from 'useapi'; import why from '../exports.js'; import { RuntimeModuleRegistry } from '../src/extension/RuntimeModuleRegistry.js'; import { Kernel } from '../src/Kernel.js'; import { Core2Module } from '../src/modules/core/Core2Module.js'; import { Container } from '../src/services/Container.js'; import { HTTPThumbnailService } from '../src/services/thumbnails/HTTPThumbnailService.js'; import { consoleLogManager } from '../src/util/consolelog.js'; import { Context } from '../src/util/context.js'; import { TestCoreModule } from '../src/modules/test-core/TestCoreModule.js'; const { BaseService, EssentialModules } = why; /** * A simple implementation of the log interface for the test kernel. */ class TestLogger { constructor () { console.log('\x1B[36;1mBoot logger started :)\x1B[0m'); } info (...args) { console.log('\x1B[36;1m[TESTKERNEL/INFO]\x1B[0m', ...args); } error (...args) { console.log('\x1B[31;1m[TESTKERNEL/ERROR]\x1B[0m', ...args); } } /** * TestKernel class extends AdvancedBase to provide a testing environment for Puter services * Implements a simplified version of the main Kernel for testing purposes, including: * - Module management and installation * - Service container initialization * - Custom logging functionality * - Context creation and management * Does not include full service initialization or legacy service support */ export class TestKernel extends AdvancedBase { /**@type {Context} */ root_context; constructor () { super(); this.modules = []; this.useapi = useapi(); /** * Initializes the useapi instance for the test kernel. * Defines base Module and Service classes in the useapi context. * @returns {void} */ this.useapi.withuse(() => { // eslint-disable-next-line no-undef def('Module', AdvancedBase); // eslint-disable-next-line no-undef def('Service', BaseService); }); this.logfn_ = (...a) => a; this.runtimeModuleRegistry = new RuntimeModuleRegistry(); } add_module (module) { this.modules.push(module); } /** * Adds a module to the test kernel's module list * @param {Module} module - The module instance to add * @description Stores the provided module in the kernel's internal modules array for later installation */ boot () { consoleLogManager.initialize_proxy_methods(); consoleLogManager.decorate_all(({ _manager, replace }, ...a) => { replace(...this.logfn_(...a)); }); this.testLogger = new TestLogger(); const services = new Container({ logger: this.testLogger }); this.services = services; // app.set('services', services); const root_context = Context.create({ services, useapi: this.useapi, ['runtime-modules']: this.runtimeModuleRegistry, args: {}, }, 'app'); this.root_context = root_context; globalThis.root_context = root_context; root_context.arun(async () => { await this._install_modules(); // await this._boot_services(); }); // Error.stackTraceLimit = Infinity; Error.stackTraceLimit = 200; } /** * Installs modules into the test kernel environment */ async _install_modules () { const { services } = this; const mod_install_root_context = Context.get(); for ( const module of this.modules ) { try { const mod_context = this._create_mod_context(mod_install_root_context, { name: module.constructor.name, ['module']: module, external: false, }); await this.root_context.arun(async () => { await module.install(mod_context); }); } catch (e) { console.log(e); throw e; } } // Real kernel initializes services here, but in this test kernel // we don't initialize any services. // Real kernel adds legacy services here but these will break // the test kernel. services.ready.resolve(); // provide services to helpers // const { tmp_provide_services } = require('../src/helpers'); // tmp_provide_services(services); } } TestKernel.prototype._create_mod_context = Kernel.prototype._create_mod_context; const do_after_tests_ = []; /** * Executes a function immediately and adds it to the list of functions to be executed after tests * * This is used to log things inline with console output from tests, and then * again later without those console outputs. * * @param {Function} fn - The function to execute and store for later */ const repeat_after = (fn) => { fn(); do_after_tests_.push(fn); }; let total_passed = 0; let total_failed = 0; /** * Tracks test results across all services * @type {number} total_passed - Count of all passed assertions * @type {number} total_failed - Count of all failed assertions */ const main = async () => { const k = new TestKernel(); for ( const mod of EssentialModules ) { k.add_module(new mod()); } k.add_module({ install: async (context) => { const services = context.get('services'); services.registerService('thumbs-http', HTTPThumbnailService); }, }); k.boot(); console.log('awaiting services ready'); await k.services.ready; console.log('services have become ready'); const service_names = process.argv.length > 2 ? process.argv.slice(2) : Object.keys(k.services.instances_); for ( const name of service_names ) { if ( ! k.services.instances_[name] ) { console.log(`\x1B[31;1mService not found: ${name}\x1B[0m`); process.exit(1); } const ins = k.services.instances_[name]; ins.construct(); if ( !ins._test || typeof ins._test !== 'function' ) { continue; } ins.log = k.testLogger; let passed = 0; let failed = 0; repeat_after(() => { console.log(`\x1B[33;1m=== [ Service :: ${name} ] ===\x1B[0m`); }); const testapi = { assert: (condition, name) => { name = name || condition.toString(); if ( condition() ) { passed++; repeat_after(() => console.log(`\x1B[32;1m ✔ ${name}\x1B[0m`)); } else { failed++; repeat_after(() => console.log(`\x1B[31;1m ✘ ${name}\x1B[0m`)); } }, }; testapi.assert.equal = (a, b, name) => { name = name || `${a} === ${b}`; if ( a === b ) { passed++; repeat_after(() => console.log(`\x1B[32;1m ✔ ${name}\x1B[0m`)); } else { failed++; repeat_after(() => { console.log(`\x1B[31;1m ✘ ${name}\x1B[0m`); console.log(`\x1B[31;1m Expected: ${b}\x1B[0m`); console.log(`\x1B[31;1m Got: ${a}\x1B[0m`); }); } }; await ins._test(testapi); total_passed += passed; total_failed += failed; } console.log('\x1B[36;1m<===\x1B[0m ' + 'ASSERTION OUTPUTS ARE REPEATED BELOW' + ' \x1B[36;1m===>\x1B[0m'); for ( const fn of do_after_tests_ ) { fn(); } console.log('\x1B[36;1m=== [ Summary ] ===\x1B[0m'); console.log(`Passed: ${total_passed}`); console.log(`Failed: ${total_failed}`); process.exit(total_failed ? 1 : 0); }; if ( import.meta.main ) { main(); } export const createTestKernel = async ({ serviceMap = {}, initLevelString = 'construct', extraSteps = true, testCore = false, serviceConfigOverrideMap = {}, globalConfigOverrideMap = {}, serviceMapArgs = {}, }) => { const initLevelMap = { CONSTRUCT: 1, INIT: 2 }; const initLevel = initLevelMap[(`${initLevelString}`).toUpperCase()]; const testKernel = new TestKernel(); testKernel.add_module(new Core2Module()); if ( testCore ) testKernel.add_module(new TestCoreModule()); for ( const [name, service] of Object.entries(serviceMap) ) { testKernel.add_module({ install: context => { const services = context.get('services'); services.registerService(name, service, serviceMapArgs[name] || undefined); }, }); } testKernel.boot(); await testKernel.services.ready; const service_names = Object.keys(testKernel.services.instances_); for ( const name of service_names ) { const serviceConfigOverride = serviceConfigOverrideMap[name] ; const globalConfigOverride = globalConfigOverrideMap[name] ; if ( serviceConfigOverride ) { const ins = testKernel.services.instances_[name]; // Apply service config overrides ins.config = { ...ins.config, ...serviceConfigOverride, }; } if ( globalConfigOverride ) { const ins = testKernel.services.instances_[name]; // Apply global config overrides ins.global_config = { ...ins.global_config, ...globalConfigOverride, }; } } for ( const name of service_names ) { const ins = testKernel.services.instances_[name]; // Fix context ins.context = testKernel.root_context; if ( initLevel >= initLevelMap.CONSTRUCT ) { await ins.construct(); } } for ( const name of service_names ) { const ins = testKernel.services.instances_[name]; if ( initLevel >= initLevelMap.INIT ) { await ins.init(); } } if ( extraSteps && testCore && initLevel >= initLevelMap.INIT ) { await testKernel.services?.get('su').__on('boot.consolidation', []); } return testKernel; };