chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
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>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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; }
|
||||
}
|
||||
Reference in New Issue
Block a user