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);
}
}