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:
Joseph Doherty
2026-05-17 01:55:28 -04:00
parent 69f02fed7f
commit a25593a9c6
1044 changed files with 365 additions and 343 deletions

View File

@@ -0,0 +1,194 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests;
/// <summary>
/// Exercises the AST walker that extracts static tag dependencies from user scripts
/// + rejects every form of non-literal path. Locks the parse shape the virtual-tag
/// engine's change-trigger scheduler will depend on (Phase 7 plan Stream A.2).
/// </summary>
[Trait("Category", "Unit")]
public sealed class DependencyExtractorTests
{
[Fact]
public void Extracts_single_literal_read()
{
var result = DependencyExtractor.Extract(
"""return ctx.GetTag("Line1/Speed").Value;""");
result.IsValid.ShouldBeTrue();
result.Reads.ShouldContain("Line1/Speed");
result.Writes.ShouldBeEmpty();
result.Rejections.ShouldBeEmpty();
}
[Fact]
public void Extracts_multiple_distinct_reads()
{
var result = DependencyExtractor.Extract(
"""
var a = ctx.GetTag("Line1/A").Value;
var b = ctx.GetTag("Line1/B").Value;
return (double)a + (double)b;
""");
result.IsValid.ShouldBeTrue();
result.Reads.Count.ShouldBe(2);
result.Reads.ShouldContain("Line1/A");
result.Reads.ShouldContain("Line1/B");
}
[Fact]
public void Deduplicates_identical_reads_across_the_script()
{
var result = DependencyExtractor.Extract(
"""
if (((double)ctx.GetTag("X").Value) > 0)
return ctx.GetTag("X").Value;
return 0;
""");
result.IsValid.ShouldBeTrue();
result.Reads.Count.ShouldBe(1);
result.Reads.ShouldContain("X");
}
[Fact]
public void Tracks_virtual_tag_writes_separately_from_reads()
{
var result = DependencyExtractor.Extract(
"""
var v = (double)ctx.GetTag("InTag").Value;
ctx.SetVirtualTag("OutTag", v * 2);
return v;
""");
result.IsValid.ShouldBeTrue();
result.Reads.ShouldContain("InTag");
result.Writes.ShouldContain("OutTag");
result.Reads.ShouldNotContain("OutTag");
result.Writes.ShouldNotContain("InTag");
}
[Fact]
public void Rejects_variable_path()
{
var result = DependencyExtractor.Extract(
"""
var path = "Line1/Speed";
return ctx.GetTag(path).Value;
""");
result.IsValid.ShouldBeFalse();
result.Rejections.Count.ShouldBe(1);
result.Rejections[0].Message.ShouldContain("string literal");
}
[Fact]
public void Rejects_concatenated_path()
{
var result = DependencyExtractor.Extract(
"""return ctx.GetTag("Line1/" + "Speed").Value;""");
result.IsValid.ShouldBeFalse();
result.Rejections[0].Message.ShouldContain("string literal");
}
[Fact]
public void Rejects_interpolated_path()
{
var result = DependencyExtractor.Extract(
"""
var n = 1;
return ctx.GetTag($"Line{n}/Speed").Value;
""");
result.IsValid.ShouldBeFalse();
result.Rejections[0].Message.ShouldContain("string literal");
}
[Fact]
public void Rejects_method_returned_path()
{
var result = DependencyExtractor.Extract(
"""
string BuildPath() => "Line1/Speed";
return ctx.GetTag(BuildPath()).Value;
""");
result.IsValid.ShouldBeFalse();
result.Rejections[0].Message.ShouldContain("string literal");
}
[Fact]
public void Rejects_empty_literal_path()
{
var result = DependencyExtractor.Extract(
"""return ctx.GetTag("").Value;""");
result.IsValid.ShouldBeFalse();
result.Rejections[0].Message.ShouldContain("empty");
}
[Fact]
public void Rejects_whitespace_only_path()
{
var result = DependencyExtractor.Extract(
"""return ctx.GetTag(" ").Value;""");
result.IsValid.ShouldBeFalse();
}
[Fact]
public void Ignores_non_ctx_method_named_GetTag()
{
// Scripts are free to define their own helper called "GetTag" — as long as it's
// not on the ctx instance, the extractor doesn't pick it up. The sandbox
// compile will still reject any path that isn't on the ScriptContext type.
var result = DependencyExtractor.Extract(
"""
string helper_GetTag(string p) => p;
return helper_GetTag("NotATag");
""");
result.IsValid.ShouldBeTrue();
result.Reads.ShouldBeEmpty();
}
[Fact]
public void Empty_source_is_a_no_op()
{
DependencyExtractor.Extract("").IsValid.ShouldBeTrue();
DependencyExtractor.Extract(" ").IsValid.ShouldBeTrue();
DependencyExtractor.Extract(null!).IsValid.ShouldBeTrue();
}
[Fact]
public void Rejection_carries_source_span_for_UI_pointing()
{
// Offending path at column 23-29 in the source — Admin UI uses Span to
// underline the exact token.
const string src = """return ctx.GetTag(path).Value;""";
var result = DependencyExtractor.Extract(src);
result.IsValid.ShouldBeFalse();
result.Rejections[0].Span.Start.ShouldBeGreaterThan(0);
result.Rejections[0].Span.Length.ShouldBeGreaterThan(0);
}
[Fact]
public void Multiple_bad_paths_all_reported_in_one_pass()
{
var result = DependencyExtractor.Extract(
"""
var p1 = "A"; var p2 = "B";
return ctx.GetTag(p1).Value.ToString() + ctx.GetTag(p2).Value.ToString();
""");
result.IsValid.ShouldBeFalse();
result.Rejections.Count.ShouldBe(2);
}
[Fact]
public void Nested_literal_GetTag_inside_expression_is_extracted()
{
// Supports patterns like ctx.GetTag("A") > ctx.GetTag("B") — both literal args
// are captured even when the enclosing expression is complex.
var result = DependencyExtractor.Extract(
"""
return ((double)ctx.GetTag("A").Value) > ((double)ctx.GetTag("B").Value);
""");
result.IsValid.ShouldBeTrue();
result.Reads.Count.ShouldBe(2);
}
}