1ead8f458a
v2-ci / unit-tests (push) Waiting to run
- A self-contained Host publish (-r win-x64 --self-contained, the Windows installs) now publishes the ScriptWorker self-contained for the same RID into script-worker/. The launcher starts its apphost directly when runtimeconfig.json lists includedFrameworks. A framework-dependent worker (docker-dev) keeps running through the dotnet muxer. - docker-dev/docker-compose.script-runner.yml: OutOfProcess for the central pair's VT + Calc seams. Live drill recorded in design §12.1 (runaway kill + quarantine, kill -9 recovery, open circuit with /health/ready still 200, metrics). - ScriptRunnerPerfTests (Category=Perf, opt-in via OTOPCUA_SCRIPT_PERF=1): InProcess vs OutOfProcess at K=1/2/4. The accepted 5k evals/s/node gate passes on macOS and windev. The host-CPU, RSS and Windows latency budget misses are tracked as #789. - Fix: the single-flight slot is released before the awaiting caller resumes. Before, a caller that started its next run right after the previous one returned could be refused with "previous evaluation still running", which the seams publish as Bad. - Fix: the worker compiles on dedicated threads, not the thread pool. A burst of cold loads starved the heartbeat, and the host killed the shard (found on windev). Load-timeout queueing is #790. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
106 lines
5.4 KiB
C#
106 lines
5.4 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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));
|
|
}
|
|
|
|
/// <summary>The worker built beside the tests is framework-dependent, so the default stays the muxer path.</summary>
|
|
[Fact]
|
|
public void The_built_worker_resolves_to_the_entry_assembly()
|
|
{
|
|
OutOfProcessScriptRunner.ResolveWorkerPath(new ScriptWorkerOptions())
|
|
.ShouldEndWith(Path.Combine(OutOfProcessScriptRunner.WorkerFolderName, OutOfProcessScriptRunner.WorkerAssemblyFileName));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs only when <see cref="Scripts.WorkerUnderTestVariable"/> names a self-contained worker's apphost (the
|
|
/// windev gate over a <c>-r win-x64 --self-contained</c> Host publish): the worker process IS the apphost — no
|
|
/// dotnet muxer anywhere — and it evaluates.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
}
|