using System.Diagnostics; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Scripting; using ZB.MOM.WW.OtOpcUa.Core.Scripting.OutOfProcess; namespace ZB.MOM.WW.OtOpcUa.ScriptWorker.Tests; /// /// lmxopcua#780: the Windows installs have no .NET runtime, so a self-contained Host publish ships a self-contained /// worker that the launcher must start through its apphost, while a framework-dependent worker (docker-dev, the /// tests) keeps going through the dotnet muxer. /// [Collection(ScriptWorkerCollection.Name)] [Trait("Category", "ScriptWorker")] public sealed class WorkerEntryTests { private static CancellationToken Ct => TestContext.Current.CancellationToken; private const string SelfContainedConfig = """ { "runtimeOptions": { "tfm": "net10.0", "includedFrameworks": [ { "name": "Microsoft.NETCore.App", "version": "10.0.0" } ] } } """; private const string FrameworkDependentConfig = """ { "runtimeOptions": { "tfm": "net10.0", "framework": { "name": "Microsoft.NETCore.App", "version": "10.0.0" } } } """; private static string ConfigPath(string folder) => Path.Combine(folder, "ZB.MOM.WW.OtOpcUa.ScriptWorker.runtimeconfig.json"); private static void Touch(string path) => File.WriteAllText(path, ""); [Fact] public void A_self_contained_worker_is_started_through_its_apphost() { using var dir = new Scripts.TempDir(); Touch(Path.Combine(dir.Path, OutOfProcessScriptRunner.WorkerAppHostFileName)); Touch(Path.Combine(dir.Path, OutOfProcessScriptRunner.WorkerAssemblyFileName)); File.WriteAllText(ConfigPath(dir.Path), SelfContainedConfig); OutOfProcessScriptRunner.IsSelfContainedWorker(dir.Path).ShouldBeTrue(); OutOfProcessScriptRunner.ResolveWorkerEntry(dir.Path) .ShouldBe(Path.Combine(Path.GetFullPath(dir.Path), OutOfProcessScriptRunner.WorkerAppHostFileName)); } [Fact] public void A_framework_dependent_worker_keeps_the_muxer_even_with_an_apphost() { using var dir = new Scripts.TempDir(); Touch(Path.Combine(dir.Path, OutOfProcessScriptRunner.WorkerAppHostFileName)); Touch(Path.Combine(dir.Path, OutOfProcessScriptRunner.WorkerAssemblyFileName)); File.WriteAllText(ConfigPath(dir.Path), FrameworkDependentConfig); OutOfProcessScriptRunner.IsSelfContainedWorker(dir.Path).ShouldBeFalse(); OutOfProcessScriptRunner.ResolveWorkerEntry(dir.Path).ShouldEndWith(OutOfProcessScriptRunner.WorkerAssemblyFileName); } [Theory] [InlineData(true, null)] // self-contained config but no apphost on disk [InlineData(false, "{ broken")] // apphost present, unreadable config [InlineData(false, null)] // apphost present, no config at all public void Anything_short_of_a_self_contained_apphost_falls_back_to_the_entry_assembly(bool omitAppHost, string? config) { using var dir = new Scripts.TempDir(); if (!omitAppHost) Touch(Path.Combine(dir.Path, OutOfProcessScriptRunner.WorkerAppHostFileName)); if (omitAppHost) File.WriteAllText(ConfigPath(dir.Path), SelfContainedConfig); else if (config is not null) File.WriteAllText(ConfigPath(dir.Path), config); OutOfProcessScriptRunner.ResolveWorkerEntry(dir.Path).ShouldEndWith(OutOfProcessScriptRunner.WorkerAssemblyFileName); } [Fact] public void An_explicit_executable_path_wins() { var path = Path.Combine(Path.GetTempPath(), "custom", "worker.exe"); OutOfProcessScriptRunner.ResolveWorkerPath(new ScriptWorkerOptions { ExecutablePath = path }).ShouldBe(Path.GetFullPath(path)); } /// The worker built beside the tests is framework-dependent, so the default stays the muxer path. [Fact] public void The_built_worker_resolves_to_the_entry_assembly() { OutOfProcessScriptRunner.ResolveWorkerPath(new ScriptWorkerOptions()) .ShouldEndWith(Path.Combine(OutOfProcessScriptRunner.WorkerFolderName, OutOfProcessScriptRunner.WorkerAssemblyFileName)); } /// /// Runs only when names a self-contained worker's apphost (the /// windev gate over a -r win-x64 --self-contained Host publish): the worker process IS the apphost — no /// dotnet muxer anywhere — and it evaluates. /// [Fact] public async Task A_published_self_contained_worker_runs_as_its_own_process() { var path = Scripts.WorkerUnderTest; if (path is null || !path.EndsWith(OutOfProcessScriptRunner.WorkerAppHostFileName, StringComparison.OrdinalIgnoreCase)) Assert.Skip($"set {Scripts.WorkerUnderTestVariable} to a self-contained worker apphost to run this gate"); OutOfProcessScriptRunner.IsSelfContainedWorker(Path.GetDirectoryName(path)!).ShouldBeTrue("the published folder is self-contained"); OutOfProcessScriptRunner.ResolveWorkerEntry(Path.GetDirectoryName(path)!).ShouldBe(Path.GetFullPath(path)); await using var runner = Scripts.Runner(Scripts.Options(workerPath: path)); (await runner.RunAsync(Scripts.Vt("vt", "return System.Math.Abs(-3);"), Ct)).Value.ShouldBe(3); using var worker = Process.GetProcessById(runner.Shards[0].Current!.Pid); Path.GetFullPath(worker.MainModule!.FileName).ShouldBe(Path.GetFullPath(path), StringCompareShould.IgnoreCase); } }