插件系统
This commit is contained in:
396
packages/core/tests/Core/PluginSystem.test.ts
Normal file
396
packages/core/tests/Core/PluginSystem.test.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
import { Core } from '../../src/Core';
|
||||
import { IPlugin } from '../../src/Core/Plugin';
|
||||
import { PluginManager } from '../../src/Core/PluginManager';
|
||||
import type { ServiceContainer } from '../../src/Core/ServiceContainer';
|
||||
|
||||
describe('插件系统', () => {
|
||||
let core: Core;
|
||||
|
||||
beforeEach(() => {
|
||||
core = Core.create({ debug: false });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Core.destroy();
|
||||
});
|
||||
|
||||
describe('基本功能', () => {
|
||||
it('应该能够安装插件', async () => {
|
||||
class TestPlugin implements IPlugin {
|
||||
readonly name = 'test-plugin';
|
||||
readonly version = '1.0.0';
|
||||
installed = false;
|
||||
|
||||
install() {
|
||||
this.installed = true;
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
this.installed = false;
|
||||
}
|
||||
}
|
||||
|
||||
const plugin = new TestPlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
expect(plugin.installed).toBe(true);
|
||||
expect(Core.isPluginInstalled('test-plugin')).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能够卸载插件', async () => {
|
||||
class TestPlugin implements IPlugin {
|
||||
readonly name = 'test-plugin';
|
||||
readonly version = '1.0.0';
|
||||
installed = false;
|
||||
|
||||
install() {
|
||||
this.installed = true;
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
this.installed = false;
|
||||
}
|
||||
}
|
||||
|
||||
const plugin = new TestPlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
expect(plugin.installed).toBe(true);
|
||||
|
||||
await Core.uninstallPlugin('test-plugin');
|
||||
expect(plugin.installed).toBe(false);
|
||||
expect(Core.isPluginInstalled('test-plugin')).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能够获取已安装的插件', async () => {
|
||||
class TestPlugin implements IPlugin {
|
||||
readonly name = 'test-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install() {}
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
const plugin = new TestPlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
const installed = Core.getPlugin('test-plugin');
|
||||
expect(installed).toBe(plugin);
|
||||
expect(installed?.version).toBe('1.0.0');
|
||||
});
|
||||
|
||||
it('应该拒绝重复安装同一个插件', async () => {
|
||||
class TestPlugin implements IPlugin {
|
||||
readonly name = 'test-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install() {}
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
const plugin1 = new TestPlugin();
|
||||
const plugin2 = new TestPlugin();
|
||||
|
||||
await Core.installPlugin(plugin1);
|
||||
await Core.installPlugin(plugin2);
|
||||
|
||||
expect(Core.getPlugin('test-plugin')).toBe(plugin1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('依赖管理', () => {
|
||||
it('应该检查插件依赖', async () => {
|
||||
class BasePlugin implements IPlugin {
|
||||
readonly name = 'base-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install() {}
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
class DependentPlugin implements IPlugin {
|
||||
readonly name = 'dependent-plugin';
|
||||
readonly version = '1.0.0';
|
||||
readonly dependencies = ['base-plugin'] as const;
|
||||
|
||||
install() {}
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
const dependentPlugin = new DependentPlugin();
|
||||
|
||||
await expect(Core.installPlugin(dependentPlugin)).rejects.toThrow(
|
||||
'unmet dependencies'
|
||||
);
|
||||
});
|
||||
|
||||
it('应该允许按正确顺序安装有依赖的插件', async () => {
|
||||
class BasePlugin implements IPlugin {
|
||||
readonly name = 'base-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install() {}
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
class DependentPlugin implements IPlugin {
|
||||
readonly name = 'dependent-plugin';
|
||||
readonly version = '1.0.0';
|
||||
readonly dependencies = ['base-plugin'] as const;
|
||||
|
||||
install() {}
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
const basePlugin = new BasePlugin();
|
||||
const dependentPlugin = new DependentPlugin();
|
||||
|
||||
await Core.installPlugin(basePlugin);
|
||||
await Core.installPlugin(dependentPlugin);
|
||||
|
||||
expect(Core.isPluginInstalled('base-plugin')).toBe(true);
|
||||
expect(Core.isPluginInstalled('dependent-plugin')).toBe(true);
|
||||
});
|
||||
|
||||
it('应该防止卸载被依赖的插件', async () => {
|
||||
class BasePlugin implements IPlugin {
|
||||
readonly name = 'base-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install() {}
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
class DependentPlugin implements IPlugin {
|
||||
readonly name = 'dependent-plugin';
|
||||
readonly version = '1.0.0';
|
||||
readonly dependencies = ['base-plugin'] as const;
|
||||
|
||||
install() {}
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
await Core.installPlugin(new BasePlugin());
|
||||
await Core.installPlugin(new DependentPlugin());
|
||||
|
||||
await expect(Core.uninstallPlugin('base-plugin')).rejects.toThrow(
|
||||
'required by'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('服务注册', () => {
|
||||
it('应该允许插件注册服务', async () => {
|
||||
class TestService {
|
||||
public value = 42;
|
||||
dispose() {}
|
||||
}
|
||||
|
||||
class ServicePlugin implements IPlugin {
|
||||
readonly name = 'service-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer) {
|
||||
services.registerSingleton(TestService);
|
||||
}
|
||||
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
await Core.installPlugin(new ServicePlugin());
|
||||
|
||||
const service = Core.services.resolve(TestService);
|
||||
expect(service.value).toBe(42);
|
||||
});
|
||||
|
||||
it('应该允许插件访问Core和ServiceContainer', async () => {
|
||||
let capturedCore: Core | null = null;
|
||||
let capturedServices: ServiceContainer | null = null;
|
||||
|
||||
class TestPlugin implements IPlugin {
|
||||
readonly name = 'test-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer) {
|
||||
capturedCore = core;
|
||||
capturedServices = services;
|
||||
}
|
||||
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
await Core.installPlugin(new TestPlugin());
|
||||
|
||||
expect(capturedCore).toBe(Core.Instance);
|
||||
expect(capturedServices).toBe(Core.services);
|
||||
});
|
||||
});
|
||||
|
||||
describe('异步插件', () => {
|
||||
it('应该支持异步安装', async () => {
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
initialized = false;
|
||||
|
||||
async install() {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
const plugin = new AsyncPlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
expect(plugin.initialized).toBe(true);
|
||||
});
|
||||
|
||||
it('应该支持异步卸载', async () => {
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
cleaned = false;
|
||||
|
||||
install() {}
|
||||
|
||||
async uninstall() {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
this.cleaned = true;
|
||||
}
|
||||
}
|
||||
|
||||
const plugin = new AsyncPlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
await Core.uninstallPlugin('async-plugin');
|
||||
|
||||
expect(plugin.cleaned).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('应该捕获安装错误', async () => {
|
||||
class FailingPlugin implements IPlugin {
|
||||
readonly name = 'failing-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install() {
|
||||
throw new Error('安装失败');
|
||||
}
|
||||
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
await expect(Core.installPlugin(new FailingPlugin())).rejects.toThrow(
|
||||
'安装失败'
|
||||
);
|
||||
|
||||
expect(Core.isPluginInstalled('failing-plugin')).toBe(false);
|
||||
});
|
||||
|
||||
it('应该捕获卸载错误', async () => {
|
||||
class FailingPlugin implements IPlugin {
|
||||
readonly name = 'failing-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install() {}
|
||||
|
||||
uninstall() {
|
||||
throw new Error('卸载失败');
|
||||
}
|
||||
}
|
||||
|
||||
await Core.installPlugin(new FailingPlugin());
|
||||
|
||||
await expect(Core.uninstallPlugin('failing-plugin')).rejects.toThrow(
|
||||
'卸载失败'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PluginManager直接使用', () => {
|
||||
it('应该能够从ServiceContainer获取PluginManager', () => {
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
expect(pluginManager).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该能够获取所有插件', async () => {
|
||||
class Plugin1 implements IPlugin {
|
||||
readonly name = 'plugin1';
|
||||
readonly version = '1.0.0';
|
||||
install() {}
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
class Plugin2 implements IPlugin {
|
||||
readonly name = 'plugin2';
|
||||
readonly version = '2.0.0';
|
||||
install() {}
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
await Core.installPlugin(new Plugin1());
|
||||
await Core.installPlugin(new Plugin2());
|
||||
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
const allPlugins = pluginManager.getAllPlugins();
|
||||
|
||||
expect(allPlugins).toHaveLength(2);
|
||||
expect(allPlugins.map(p => p.name)).toContain('plugin1');
|
||||
expect(allPlugins.map(p => p.name)).toContain('plugin2');
|
||||
});
|
||||
|
||||
it('应该能够获取插件元数据', async () => {
|
||||
class TestPlugin implements IPlugin {
|
||||
readonly name = 'test-plugin';
|
||||
readonly version = '1.0.0';
|
||||
install() {}
|
||||
uninstall() {}
|
||||
}
|
||||
|
||||
await Core.installPlugin(new TestPlugin());
|
||||
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
const metadata = pluginManager.getMetadata('test-plugin');
|
||||
|
||||
expect(metadata).toBeDefined();
|
||||
expect(metadata?.name).toBe('test-plugin');
|
||||
expect(metadata?.version).toBe('1.0.0');
|
||||
expect(metadata?.state).toBe('installed');
|
||||
expect(metadata?.installedAt).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('生命周期', () => {
|
||||
it('应该在Core销毁时卸载所有插件', async () => {
|
||||
const uninstallCalls: string[] = [];
|
||||
|
||||
class Plugin1 implements IPlugin {
|
||||
readonly name = 'plugin1';
|
||||
readonly version = '1.0.0';
|
||||
install() {}
|
||||
uninstall() {
|
||||
uninstallCalls.push('plugin1');
|
||||
}
|
||||
}
|
||||
|
||||
class Plugin2 implements IPlugin {
|
||||
readonly name = 'plugin2';
|
||||
readonly version = '1.0.0';
|
||||
readonly dependencies = ['plugin1'] as const;
|
||||
install() {}
|
||||
uninstall() {
|
||||
uninstallCalls.push('plugin2');
|
||||
}
|
||||
}
|
||||
|
||||
await Core.installPlugin(new Plugin1());
|
||||
await Core.installPlugin(new Plugin2());
|
||||
|
||||
Core.destroy();
|
||||
|
||||
expect(uninstallCalls).toContain('plugin1');
|
||||
expect(uninstallCalls).toContain('plugin2');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user