Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
122 lines
5.2 KiB
C#
122 lines
5.2 KiB
C#
using Serilog; // resolves Serilog.ILogger explicitly in signatures
|
|
using Serilog.Core;
|
|
using Serilog.Events;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Admin.Services;
|
|
|
|
/// <summary>
|
|
/// Dry-run harness for the Phase 7 scripting UI. Takes a script + a synthetic input
|
|
/// map + evaluates once, returns the output (or rejection / exception) plus any
|
|
/// logger emissions the script produced. Per Phase 7 plan decision #22: only inputs
|
|
/// the <see cref="DependencyExtractor"/> identified can be supplied, so a dependency
|
|
/// the harness can't prove statically surfaces as a harness error, not a runtime
|
|
/// surprise later.
|
|
/// </summary>
|
|
public sealed class ScriptTestHarnessService
|
|
{
|
|
/// <summary>
|
|
/// Evaluate <paramref name="source"/> as a virtual-tag script (return value is the
|
|
/// tag's new value). <paramref name="inputs"/> supplies synthetic
|
|
/// <see cref="DataValueSnapshot"/>s for every path the extractor found.
|
|
/// </summary>
|
|
public async Task<ScriptTestResult> RunVirtualTagAsync(
|
|
string source, IDictionary<string, DataValueSnapshot> inputs, CancellationToken ct)
|
|
{
|
|
var deps = DependencyExtractor.Extract(source);
|
|
if (!deps.IsValid)
|
|
return ScriptTestResult.DependencyRejections(deps.Rejections);
|
|
|
|
var missing = deps.Reads.Where(r => !inputs.ContainsKey(r)).ToArray();
|
|
if (missing.Length > 0)
|
|
return ScriptTestResult.MissingInputs(missing);
|
|
|
|
var extra = inputs.Keys.Where(k => !deps.Reads.Contains(k)).ToArray();
|
|
if (extra.Length > 0)
|
|
return ScriptTestResult.UnknownInputs(extra);
|
|
|
|
ScriptEvaluator<HarnessVirtualTagContext, object?> evaluator;
|
|
try
|
|
{
|
|
evaluator = ScriptEvaluator<HarnessVirtualTagContext, object?>.Compile(source);
|
|
}
|
|
catch (Exception compileEx)
|
|
{
|
|
return ScriptTestResult.Threw(compileEx.Message, []);
|
|
}
|
|
var capturing = new CapturingSink();
|
|
var logger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(capturing).CreateLogger();
|
|
var ctx = new HarnessVirtualTagContext(inputs, logger);
|
|
|
|
try
|
|
{
|
|
var result = await evaluator.RunAsync(ctx, ct);
|
|
return ScriptTestResult.Ok(result, ctx.Writes, capturing.Events);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return ScriptTestResult.Threw(ex.Message, capturing.Events);
|
|
}
|
|
}
|
|
|
|
// Public so Roslyn's script compilation can reference the context type through the
|
|
// ScriptGlobals<T> surface. The harness instantiates this directly; operators never see it.
|
|
public sealed class HarnessVirtualTagContext(
|
|
IDictionary<string, DataValueSnapshot> inputs, Serilog.ILogger logger) : ScriptContext
|
|
{
|
|
public Dictionary<string, object?> Writes { get; } = [];
|
|
public override DataValueSnapshot GetTag(string path) =>
|
|
inputs.TryGetValue(path, out var v)
|
|
? v
|
|
: new DataValueSnapshot(null, Ua.StatusCodes.BadNotFound, null, DateTime.UtcNow);
|
|
public override void SetVirtualTag(string path, object? value) => Writes[path] = value;
|
|
public override DateTime Now => DateTime.UtcNow;
|
|
public override Serilog.ILogger Logger => logger;
|
|
}
|
|
|
|
private sealed class CapturingSink : ILogEventSink
|
|
{
|
|
public List<LogEvent> Events { get; } = [];
|
|
public void Emit(LogEvent e) => Events.Add(e);
|
|
}
|
|
}
|
|
|
|
/// <summary>Harness outcome: outputs, write-set, logger events, or a rejection/throw reason.</summary>
|
|
public sealed record ScriptTestResult(
|
|
ScriptTestOutcome Outcome,
|
|
object? Output,
|
|
IReadOnlyDictionary<string, object?> Writes,
|
|
IReadOnlyList<LogEvent> LogEvents,
|
|
IReadOnlyList<string> Errors)
|
|
{
|
|
public static ScriptTestResult Ok(object? output, IReadOnlyDictionary<string, object?> writes, IReadOnlyList<LogEvent> logs) =>
|
|
new(ScriptTestOutcome.Success, output, writes, logs, []);
|
|
public static ScriptTestResult Threw(string reason, IReadOnlyList<LogEvent> logs) =>
|
|
new(ScriptTestOutcome.Threw, null, new Dictionary<string, object?>(), logs, [reason]);
|
|
public static ScriptTestResult DependencyRejections(IReadOnlyList<DependencyRejection> rejs) =>
|
|
new(ScriptTestOutcome.DependencyRejected, null, new Dictionary<string, object?>(), [],
|
|
rejs.Select(r => r.Message).ToArray());
|
|
public static ScriptTestResult MissingInputs(string[] paths) =>
|
|
new(ScriptTestOutcome.MissingInputs, null, new Dictionary<string, object?>(), [],
|
|
paths.Select(p => $"Missing synthetic input: {p}").ToArray());
|
|
public static ScriptTestResult UnknownInputs(string[] paths) =>
|
|
new(ScriptTestOutcome.UnknownInputs, null, new Dictionary<string, object?>(), [],
|
|
paths.Select(p => $"Input '{p}' is not referenced by the script — remove it").ToArray());
|
|
}
|
|
|
|
public enum ScriptTestOutcome
|
|
{
|
|
Success,
|
|
Threw,
|
|
DependencyRejected,
|
|
MissingInputs,
|
|
UnknownInputs,
|
|
}
|
|
|
|
file static class Ua
|
|
{
|
|
// Mirrors OPC UA StatusCodes.BadNotFound without pulling the OPC stack into Admin.
|
|
public static class StatusCodes { public const uint BadNotFound = 0x803E0000; }
|
|
}
|