diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5aa74aa8..6ceef12f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,14 +24,14 @@ jobs: dotnet test src/Serein.Tests --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" - name: Generate coverage - continue-on-error: true - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + id: coverage + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (failure() || success()) run: | dotnet tool install --global JetBrains.dotCover.CommandLineTools dotnet dotcover cover-dotnet --Output=AppCoverageReport.xml --ReportType=DetailedXML -- test --no-build - name: Run codacy-coverage-reporter - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + if: steps.coverage.conclusion != 'skipped' uses: codacy/codacy-coverage-reporter-action@v1.3.0 with: api-token: ${{ secrets.CODACY_API_TOKEN }} diff --git a/README.md b/README.md index eb205da2..7d83731d 100644 --- a/README.md +++ b/README.md @@ -59,4 +59,4 @@ dotnet build ## 📄 License -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FSereinDev%2FSerein.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FSereinDev%2FSerein?ref=badge_large) \ No newline at end of file +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FSereinDev%2FSerein.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FSereinDev%2FSerein?ref=badge_large) diff --git a/src/Serein.Core/AppType.cs b/src/Serein.Core/AppType.cs index 3e131e15..e0f88395 100644 --- a/src/Serein.Core/AppType.cs +++ b/src/Serein.Core/AppType.cs @@ -8,5 +8,6 @@ public enum AppType Unknown, Cli, Lite, - Plus + Plus, + Tests, } diff --git a/src/Serein.Core/Models/Plugins/Js/JsPlugin.cs b/src/Serein.Core/Models/Plugins/Js/JsPlugin.cs index 126f4415..0a00f105 100644 --- a/src/Serein.Core/Models/Plugins/Js/JsPlugin.cs +++ b/src/Serein.Core/Models/Plugins/Js/JsPlugin.cs @@ -72,7 +72,22 @@ public void Dispose() GC.SuppressFinalize(this); } - public bool Invoke(Event @event, CancellationToken cancellationToken, params object[] args) + internal void SetListener(Event @event, Function? func) + { + lock (_eventHandlers) + { + if (func is null) + { + _eventHandlers.TryRemove(@event, out _); + } + else + { + _eventHandlers[@event] = func; + } + } + } + + internal bool Invoke(Event @event, CancellationToken cancellationToken, params object[] args) { var entered = false; try diff --git a/src/Serein.Core/Models/Plugins/Js/JsPluginConfig.cs b/src/Serein.Core/Models/Plugins/Js/JsPluginConfig.cs index 17e1cb1b..ecc959ca 100644 --- a/src/Serein.Core/Models/Plugins/Js/JsPluginConfig.cs +++ b/src/Serein.Core/Models/Plugins/Js/JsPluginConfig.cs @@ -1,6 +1,6 @@ namespace Serein.Core.Models.Plugins.Js; -public class JsPluginConfig +public sealed class JsPluginConfig { public static readonly JsPluginConfig Default = new(); diff --git a/src/Serein.Core/SereinApp.cs b/src/Serein.Core/SereinApp.cs index 887d04e6..a4a70dbd 100644 --- a/src/Serein.Core/SereinApp.cs +++ b/src/Serein.Core/SereinApp.cs @@ -21,6 +21,7 @@ static SereinApp() "Serein.Cli" => AppType.Cli, "Serein.Lite" => AppType.Lite, "Serein.Plus" => AppType.Plus, + "Serein.Tests" => AppType.Tests, _ => AppType.Unknown, }; diff --git a/src/Serein.Core/Services/CoreService.cs b/src/Serein.Core/Services/CoreService.cs index cbc19f8d..9b962c01 100644 --- a/src/Serein.Core/Services/CoreService.cs +++ b/src/Serein.Core/Services/CoreService.cs @@ -78,6 +78,8 @@ public Task StartAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("正在停止"); + _updateChecker.Dispose(); + return Task.CompletedTask; } diff --git a/src/Serein.Core/Services/Network/UpdateChecker.cs b/src/Serein.Core/Services/Network/UpdateChecker.cs index 3059bdb1..7c31d276 100644 --- a/src/Serein.Core/Services/Network/UpdateChecker.cs +++ b/src/Serein.Core/Services/Network/UpdateChecker.cs @@ -9,7 +9,7 @@ namespace Serein.Core.Services.Network; -public sealed class UpdateChecker +public sealed class UpdateChecker : IDisposable { private readonly ILogger _logger; private readonly System.Timers.Timer _timer; @@ -84,4 +84,10 @@ public async Task StartAsync() await CheckAsync(); } } + + public void Dispose() + { + _timer.Stop(); + _timer.Dispose(); + } } diff --git a/src/Serein.Core/Services/Plugins/EventDispatcher.cs b/src/Serein.Core/Services/Plugins/EventDispatcher.cs index 1e8cb364..9a3dd95a 100644 --- a/src/Serein.Core/Services/Plugins/EventDispatcher.cs +++ b/src/Serein.Core/Services/Plugins/EventDispatcher.cs @@ -29,6 +29,10 @@ JsPluginLoader jsPluginLoader private readonly NetPluginLoader _netPluginLoader = netPluginLoader; private readonly JsPluginLoader _jsPluginLoader = jsPluginLoader; + /// + /// 分发事件 + /// + /// 如果此事件被拦截则返回false internal bool Dispatch(Event @event, params object[] args) { _logger.LogDebug("分发事件:{}", @event); @@ -61,7 +65,7 @@ internal bool Dispatch(Event @event, params object[] args) } cancellationTokenSource.Cancel(); - return tasks.Select((t) => !t.IsCompleted || t.Result).Any((b) => !b); + return !tasks.Where((task) => task.IsCompleted).Any((task) => !task.Result); } private void DispatchToJsPlugins( diff --git a/src/Serein.Core/Services/Plugins/Js/Properties/CommandProperty.cs b/src/Serein.Core/Services/Plugins/Js/Properties/CommandProperty.cs index 0844ec24..9ca26128 100644 --- a/src/Serein.Core/Services/Plugins/Js/Properties/CommandProperty.cs +++ b/src/Serein.Core/Services/Plugins/Js/Properties/CommandProperty.cs @@ -27,7 +27,7 @@ public Command Parse(string? command) } #pragma warning restore CA1822 - public void SetVariable(string key, object? value) + public void SetVariable(string key, string? value) { _pluginManager.SetCommandVariable(key, value); } diff --git a/src/Serein.Core/Services/Plugins/Js/ScriptInstance.cs b/src/Serein.Core/Services/Plugins/Js/ScriptInstance.cs index 8058f1b2..9aec8e19 100644 --- a/src/Serein.Core/Services/Plugins/Js/ScriptInstance.cs +++ b/src/Serein.Core/Services/Plugins/Js/ScriptInstance.cs @@ -18,6 +18,8 @@ using Serein.Core.Services.Plugins.Js.Properties; using Console = Serein.Core.Services.Plugins.Js.Properties.Console; +using Jint.Native.Function; +using Serein.Core.Models.Plugins; namespace Serein.Core.Services.Plugins.Js; @@ -100,7 +102,19 @@ public object GetService(string type) var t = Type.GetType(type); return t?.IsPublic == true ? _serviceProvider.GetRequiredService(t) - : throw new InvalidOperationException("无法获取指定的类型"); + : throw new ArgumentException("无法获取指定的类型", nameof(type)); + } + + public void SetListener(string eventName, JsValue jsValue) + { + ArgumentException.ThrowIfNullOrEmpty(eventName); + + if (!Enum.TryParse(eventName, true, out var @event)) + { + throw new ArgumentException("无效的事件名称", nameof(eventName)); + } + + _jsPlugin.SetListener(@event, jsValue as Function); } public string Resolve(params string[] paths) => PluginManager.Resolve(_jsPlugin, paths); diff --git a/src/Serein.Core/Services/Plugins/PluginManager.cs b/src/Serein.Core/Services/Plugins/PluginManager.cs index 406cf5a9..0e8fc109 100644 --- a/src/Serein.Core/Services/Plugins/PluginManager.cs +++ b/src/Serein.Core/Services/Plugins/PluginManager.cs @@ -3,6 +3,7 @@ using System.IO; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -20,7 +21,7 @@ namespace Serein.Core.Services.Plugins; -public sealed class PluginManager( +public sealed partial class PluginManager( ILogger logger, IPluginLogger pluginLogger, JsPluginLoader jsPluginLoader, @@ -30,6 +31,9 @@ public sealed class PluginManager( PermissionManager permissionManager ) { + [GeneratedRegex(@"^[a-zA-Z][a-zA-Z0-9\-]{2,24}$")] + private static partial Regex GetIdRegex(); + private static readonly Regex IdRegex = GetIdRegex(); private static readonly JsonSerializerOptions Options = new(JsonSerializerOptionsFactory.Common) { @@ -50,35 +54,41 @@ PermissionManager permissionManager public event EventHandler? PluginsReloading; public event EventHandler? PluginsLoaded; - public bool Loading { get; private set; } - public bool Reloading { get; private set; } + public bool IsLoading { get; private set; } + public bool IsReloading { get; private set; } public void ExportVariables(string key, object? value) { ExportedVariables.AddOrUpdate(key, value, (_, _) => value); } - public void SetCommandVariable(string key, object? value) + public void SetCommandVariable(string key, string? value) { - var str = value?.ToString() ?? string.Empty; - CommandVariables.AddOrUpdate(key, str, (_, _) => str); + if (value == null) + { + CommandVariables.TryRemove(key, out _); + } + else + { + CommandVariables.AddOrUpdate(key, value, (_, _) => value); + } } public void Load() { try { - if (Loading) + if (IsLoading) { throw new InvalidOperationException("正在加载插件"); } - Loading = true; + IsLoading = true; if (!Directory.Exists(PathConstants.PluginsDirectory)) { Directory.CreateDirectory(PathConstants.PluginsDirectory); - Loading = false; + IsLoading = false; return; } @@ -101,10 +111,15 @@ public void Load() Options ) ?? throw new InvalidDataException("插件信息为空"); - if (string.IsNullOrWhiteSpace(pluginInfo.Id)) + if (string.IsNullOrEmpty(pluginInfo.Id)) { throw new InvalidOperationException("Id不可为空"); } + + if(!IdRegex.IsMatch(pluginInfo.Id)) + { + throw new InvalidOperationException("Id不符合规范"); + } } catch (Exception e) { @@ -166,12 +181,12 @@ public void Load() } catch (Exception e) { - _logger.LogDebug(e, "[{}] 插件加载时出现异常", nameof(PluginManager)); + _logger.LogDebug(e, "插件加载时出现异常"); throw; } finally { - Loading = false; + IsLoading = false; _logger.LogDebug("插件加载结束"); } } @@ -190,12 +205,12 @@ public void Unload() public void Reload() { - if (Reloading || Loading) + if (IsReloading || IsLoading) { throw new InvalidOperationException("正在加载插件"); } - Reloading = true; + IsReloading = true; try { @@ -206,7 +221,7 @@ public void Reload() } finally { - Reloading = false; + IsReloading = false; } } diff --git a/src/Serein.Core/Services/SentryReporter.cs b/src/Serein.Core/Services/SentryReporter.cs index 88333404..53279d05 100644 --- a/src/Serein.Core/Services/SentryReporter.cs +++ b/src/Serein.Core/Services/SentryReporter.cs @@ -29,7 +29,7 @@ public void Initialize() options.AutoSessionTracking = true; options.TracesSampleRate = 1; options.ProfilesSampleRate = 0.5; - options.AddIntegration(new ProfilingIntegration(TimeSpan.FromMilliseconds(1000))); + options.AddIntegration(new ProfilingIntegration(TimeSpan.FromMilliseconds(500))); #if DEBUG // options.Debug = true; diff --git a/src/Serein.Core/Services/Servers/ServerPluginManager.cs b/src/Serein.Core/Services/Servers/ServerPluginManager.cs index 0c6d9f07..ebc32c31 100644 --- a/src/Serein.Core/Services/Servers/ServerPluginManager.cs +++ b/src/Serein.Core/Services/Servers/ServerPluginManager.cs @@ -38,7 +38,7 @@ public sealed class ServerPluginManager /// /// 禁用插件的扩展名 /// - public const string DisabledPluginExtension = ".disabled"; + public static readonly string DisabledPluginExtension = ".disabled"; public event EventHandler? Updated; public IReadOnlyList Plugins => _plugins; diff --git a/src/Serein.Core/Utils/CrashHelper.cs b/src/Serein.Core/Utils/CrashHelper.cs index cc488534..981a2b44 100644 --- a/src/Serein.Core/Utils/CrashHelper.cs +++ b/src/Serein.Core/Utils/CrashHelper.cs @@ -11,7 +11,11 @@ internal static class CrashHelper { public static string CreateLog(Exception e) { - SentrySdk.CaptureException(e); + if (SentrySdk.IsEnabled) + { + SentrySdk.CaptureException(e); + } + try { Directory.CreateDirectory(PathConstants.LogDirectory + "/crash"); diff --git a/src/Serein.Core/Utils/UrlConstants.cs b/src/Serein.Core/Utils/UrlConstants.cs index d99196e8..cbc7aabc 100644 --- a/src/Serein.Core/Utils/UrlConstants.cs +++ b/src/Serein.Core/Utils/UrlConstants.cs @@ -22,15 +22,15 @@ public static class UrlConstants public static readonly string Docs = "https://sereindev.github.io/"; - public static readonly string DocsPlugins = "https://sereindev.github.io/docs/guidance/plugins"; + public static readonly string DocsArgument = "https://sereindev.github.io/docs/more/agreement"; - public static readonly string DocsVariables = - "https://sereindev.github.io/docs/guidance/variables"; + public static readonly string DocsPlugins = "https://sereindev.github.io/docs/guidance/plugins"; public static readonly string DocsMatch = "https://sereindev.github.io/docs/guidance/match"; public static readonly string DocsSchedule = "https://sereindev.github.io/docs/guidance/schedule"; - public static readonly string DocsArgument = "https://sereindev.github.io/docs/more/agreement"; + public static readonly string DocsVariables = + "https://sereindev.github.io/docs/guidance/variables"; } diff --git a/src/Serein.Lite/Ui/Function/PluginPage.cs b/src/Serein.Lite/Ui/Function/PluginPage.cs index 39ca59c2..1096fae8 100644 --- a/src/Serein.Lite/Ui/Function/PluginPage.cs +++ b/src/Serein.Lite/Ui/Function/PluginPage.cs @@ -103,7 +103,7 @@ private void DisableToolStripMenuItem_Click(object sender, EventArgs e) private void ReloadToolStripMenuItem_Click(object sender, EventArgs e) { - if (!_pluginManager.Loading && !_pluginManager.Reloading) + if (!_pluginManager.IsLoading && !_pluginManager.IsReloading) { Task.Run(_pluginManager.Reload); } @@ -125,7 +125,7 @@ private void LookUpDocsToolStripMenuItem_Click(object sender, EventArgs e) private void ReloadButton_Click(object sender, EventArgs e) { - if (!_pluginManager.Loading && !_pluginManager.Reloading) + if (!_pluginManager.IsLoading && !_pluginManager.IsReloading) { Task.Run(_pluginManager.Reload); } diff --git a/src/Serein.Plugins.Demo/Demo.cs b/src/Serein.Plugins.Demo/Demo.cs index b7828235..5b694cea 100644 --- a/src/Serein.Plugins.Demo/Demo.cs +++ b/src/Serein.Plugins.Demo/Demo.cs @@ -2,8 +2,6 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -using Newtonsoft.Json; - using Serein.Core.Models.Plugins.Net; namespace Serein.Plugins.Demo; @@ -13,7 +11,6 @@ public class Demo : PluginBase public Demo(IServiceProvider serviceProvider) { Call(); - Console.WriteLine(JsonConvert.DeserializeObject("{}")); } public override void Dispose() diff --git a/src/Serein.Plugins.Demo/Serein.Plugins.Demo.csproj b/src/Serein.Plugins.Demo/Serein.Plugins.Demo.csproj index 3b5609b0..49c9445d 100644 --- a/src/Serein.Plugins.Demo/Serein.Plugins.Demo.csproj +++ b/src/Serein.Plugins.Demo/Serein.Plugins.Demo.csproj @@ -6,7 +6,6 @@ - false all diff --git a/src/Serein.Tests/Serein.Tests.csproj b/src/Serein.Tests/Serein.Tests.csproj index 1d6f18a8..aab45d91 100644 --- a/src/Serein.Tests/Serein.Tests.csproj +++ b/src/Serein.Tests/Serein.Tests.csproj @@ -12,6 +12,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/src/Serein.Tests/Services/NetPlugin/EventTests.cs b/src/Serein.Tests/Services/NetPlugin/EventTests.cs new file mode 100644 index 00000000..283b419d --- /dev/null +++ b/src/Serein.Tests/Services/NetPlugin/EventTests.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +using Serein.Core.Models.Plugins.Info; +using Serein.Core.Services.Plugins.Net; +using Serein.Core.Services.Servers; +using Serein.Core.Utils; +using Serein.Core.Utils.Json; +using Serein.Tests.Utils; + +using Xunit; + +namespace Serein.Tests.Services.NetPlugin; + +[Collection(nameof(Serein))] +public sealed class EventTests : IDisposable +{ + private readonly IHost _host; + private readonly NetPluginLoader _netPluginLoader; + + public EventTests() + { + _host = HostFactory.BuildNew(); + _netPluginLoader = _host.Services.GetRequiredService(); + + Directory.CreateDirectory(PathConstants.PluginsDirectory); + } + + public void Dispose() + { + _host.StopAsync(); + _host.Dispose(); + } + + [Fact] + public async Task ShouldInvokePluginLoadedEvent() + { + CSharpCompilationOptionsHelper.Compile( + """ + using System; + using System.Threading.Tasks; + using Serein.Core.Models.Plugins.Net; + + namespace MyPlugin; + + public class Plugin: PluginBase + { + public bool IsInvoked { get; private set; } + public override void Dispose() { } + + protected override Task OnPluginsLoaded() + { + IsInvoked = true; + return Task.CompletedTask; + } + } + """, + nameof(ShouldInvokePluginLoadedEvent), + Path.Join( + PathConstants.PluginsDirectory, + "test", + nameof(ShouldInvokePluginLoadedEvent) + ".dll" + ) + ); + + File.WriteAllText( + Path.Join(PathConstants.PluginsDirectory, "test", PathConstants.PluginInfoFileName), + JsonSerializer.Serialize( + new PluginInfo + { + Id = "test1", + Name = "test", + Type = PluginType.Net, + EntryFile = nameof(ShouldInvokePluginLoadedEvent) + ".dll", + }, + JsonSerializerOptionsFactory.Common + ) + ); + await _host.StartAsync(); + + dynamic d = _netPluginLoader.NetPlugins.First().Value; + Assert.True(d.IsInvoked); + } + + [Fact] + public async Task ShouldInvokeServerEvent() + { + CSharpCompilationOptionsHelper.Compile( + """ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Serein.Core.Models.Plugins.Net; + using Serein.Core.Services.Servers; + + namespace MyPlugin; + + public class Plugin: PluginBase + { + public List Events { get; } = []; + public override void Dispose() { } + + protected override Task OnServerStarting(Server server) + { + Events.Add(nameof(OnServerStarting)); + return Task.FromResult(true); + } + + protected override Task OnServerStarted(Server server) + { + Events.Add(nameof(OnServerStarted)); + return Task.CompletedTask; + } + + protected override Task OnServerStopping(Server server) + { + Events.Add(nameof(OnServerStopping)); + return Task.FromResult(true); + } + + protected override Task OnServerExited(Server server, int exitcode, DateTime exitTime) + { + Events.Add(nameof(OnServerExited)); + return Task.CompletedTask; + } + + protected override Task OnServerOutput(Server server, string line) + { + Events.Add(nameof(OnServerOutput)); + return Task.FromResult(true); + } + + protected override Task OnServerRawOutput(Server server, string line) + { + Events.Add(nameof(OnServerRawOutput)); + return Task.FromResult(true); + } + + protected override Task OnServerInput(Server server, string line) + { + Events.Add(nameof(OnServerInput)); + return Task.CompletedTask; + } + } + """, + nameof(ShouldInvokeServerEvent), + Path.Join( + PathConstants.PluginsDirectory, + "test", + nameof(ShouldInvokeServerEvent) + ".dll" + ) + ); + + File.WriteAllText( + Path.Join(PathConstants.PluginsDirectory, "test", PathConstants.PluginInfoFileName), + JsonSerializer.Serialize( + new PluginInfo + { + Id = "test1", + Name = "test", + Type = PluginType.Net, + EntryFile = nameof(ShouldInvokeServerEvent) + ".dll", + }, + JsonSerializerOptionsFactory.Common + ) + ); + await _host.StartAsync(); + + var serverManager = _host.Services.GetRequiredService(); + var server = serverManager.Add( + "abcdefg", + new() { FileName = "cmd", OutputEncoding = EncodingMap.EncodingType.GBK } + ); + server.Start(); + server.Input("HELP"); + server.Stop(); + server.Terminate(); + await Task.Delay(1000); + + dynamic d = _netPluginLoader.NetPlugins.First().Value; + var list = (List)d.Events; + Assert.Contains("OnServerStarting", list); + Assert.Contains("OnServerStarted", list); + Assert.Contains("OnServerStopping", list); + Assert.Contains("OnServerExited", list); + Assert.Contains("OnServerOutput", list); + Assert.Contains("OnServerRawOutput", list); + Assert.Contains("OnServerInput", list); + } +} diff --git a/src/Serein.Tests/Services/NetPlugin/LoadTests.cs b/src/Serein.Tests/Services/NetPlugin/LoadTests.cs new file mode 100644 index 00000000..2afd2c73 --- /dev/null +++ b/src/Serein.Tests/Services/NetPlugin/LoadTests.cs @@ -0,0 +1,195 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +using Serein.Core.Models.Plugins.Info; +using Serein.Core.Services.Plugins.Net; +using Serein.Core.Utils; +using Serein.Core.Utils.Json; +using Serein.Tests.Utils; + +using Xunit; + +namespace Serein.Tests.Services.NetPlugin; + +[Collection(nameof(Serein))] +public sealed class LoadTests : IDisposable +{ + private readonly IHost _host; + private readonly NetPluginLoader _netPluginLoader; + + public LoadTests() + { + _host = HostFactory.BuildNew(); + _netPluginLoader = _host.Services.GetRequiredService(); + + Directory.CreateDirectory(PathConstants.PluginsDirectory); + Directory.CreateDirectory(Path.Join(PathConstants.PluginsDirectory, "test")); + } + + public void Dispose() + { + _host.StopAsync(); + _host.Dispose(); + } + + [Fact] + public async Task ShouldNotLoadNetPluginWithoutValidPluginClass() + { + CSharpCompilationOptionsHelper.Compile( + """ + namespace EmptyPlugin; + public class EmptyClass{} + """, + nameof(ShouldNotLoadNetPluginWithoutValidPluginClass), + Path.Join(PathConstants.PluginsDirectory, "test", "output.dll") + ); + + File.WriteAllText( + Path.Join(PathConstants.PluginsDirectory, "test", PathConstants.PluginInfoFileName), + JsonSerializer.Serialize( + new PluginInfo + { + Id = "test1", + Name = "test", + EntryFile = "output.dll", + Type = PluginType.Net, + }, + JsonSerializerOptionsFactory.Common + ) + ); + + await _host.StartAsync(); + await Task.Delay(100); + + Assert.Empty(_netPluginLoader.Plugins); + } + + [Fact] + public async Task ShouldNotLoadNetPluginWithTwoPluginClasses() + { + CSharpCompilationOptionsHelper.Compile( + """ + using Serein.Core.Models.Plugins.Net; + + namespace MyPlugin; + + public class Plugin1: PluginBase + { + public override void Dispose() { } + } + + public class Plugin2: PluginBase + { + public override void Dispose() { } + } + """, + nameof(ShouldNotLoadNetPluginWithTwoPluginClasses), + Path.Join( + PathConstants.PluginsDirectory, + "test", + nameof(ShouldNotLoadNetPluginWithTwoPluginClasses) + ".dll" + ) + ); + + File.WriteAllText( + Path.Join(PathConstants.PluginsDirectory, "test", PathConstants.PluginInfoFileName), + JsonSerializer.Serialize( + new PluginInfo + { + Id = "test1", + Name = "test", + EntryFile = nameof(ShouldNotLoadNetPluginWithTwoPluginClasses) + ".dll", + Type = PluginType.Net, + }, + JsonSerializerOptionsFactory.Common + ) + ); + + await _host.StartAsync(); + await Task.Delay(100); + + Assert.Empty(_netPluginLoader.Plugins); + } + + [Fact] + public async Task ShouldLoadNetPluginWithValidAssembly() + { + CSharpCompilationOptionsHelper.Compile( + """ + using Serein.Core.Models.Plugins.Net; + + namespace MyPlugin; + public class Plugin: PluginBase + { + public override void Dispose() { } + } + """, + nameof(ShouldLoadNetPluginWithValidAssembly), + Path.Join(PathConstants.PluginsDirectory, "test", "test111.dll") + ); + + File.WriteAllText( + Path.Join(PathConstants.PluginsDirectory, "test", PathConstants.PluginInfoFileName), + JsonSerializer.Serialize( + new PluginInfo + { + Id = "test1", + Name = "test", + EntryFile = "test111.dll", + Type = PluginType.Net, + }, + JsonSerializerOptionsFactory.Common + ) + ); + + await _host.StartAsync(); + await Task.Delay(100); + + Assert.Single(_netPluginLoader.Plugins); + Assert.Equal("test1", _netPluginLoader.Plugins.First().Key); + } + + [Fact] + public async Task ShouldLoadNetPluginWithoutSpecifyingEntryFile() + { + CSharpCompilationOptionsHelper.Compile( + """ + using System; + using Serein.Core.Models.Plugins.Net; + + namespace MyPlugin; + + public class Plugin: PluginBase + { + public override void Dispose() { } + } + """, + nameof(ShouldLoadNetPluginWithoutSpecifyingEntryFile), + Path.Join(PathConstants.PluginsDirectory, "test", "test1.dll") + ); + + File.WriteAllText( + Path.Join(PathConstants.PluginsDirectory, "test", PathConstants.PluginInfoFileName), + JsonSerializer.Serialize( + new PluginInfo + { + Id = "test1", + Name = "test", + Type = PluginType.Net, + }, + JsonSerializerOptionsFactory.Common + ) + ); + + await _host.StartAsync(); + await Task.Delay(100); + + Assert.Single(_netPluginLoader.Plugins); + } +} diff --git a/src/Serein.Tests/Utils/CSharpCompilationOptionsHelper.cs b/src/Serein.Tests/Utils/CSharpCompilationOptionsHelper.cs new file mode 100644 index 00000000..c79c748f --- /dev/null +++ b/src/Serein.Tests/Utils/CSharpCompilationOptionsHelper.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Serein.Tests.Utils; + +#pragma warning disable IL3000 + +public static class CSharpCompilationOptionsHelper +{ + private static readonly CSharpCompilationOptions Default = new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary + ) + .WithOptimizationLevel(OptimizationLevel.Release) + .WithPlatform(Platform.AnyCpu); + + public static void Compile( + string code, + string assemblyName, + string path, + params MetadataReference[] metadataReferences + ) + { + var syntaxTree = CSharpSyntaxTree.ParseText( + code, + CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest) + ); + var references = metadataReferences + .Concat( + AppDomain + .CurrentDomain.GetAssemblies() + .Where((assembly) => File.Exists(assembly.Location)) + .Select((assembly) => MetadataReference.CreateFromFile(assembly.Location)) + ) + .Distinct(); + + var compilation = CSharpCompilation.Create(assemblyName, [syntaxTree], references, Default); + var emitResult = compilation.Emit(path); + + if (!emitResult.Success) + { + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("Compilation failed:"); + foreach (var diagnostic in emitResult.Diagnostics) + { + stringBuilder.AppendLine(" " + diagnostic.ToString()); + } + throw new InvalidOperationException(stringBuilder.ToString()); + } + } +}