feat(raw): B3-WP3 rename-warning script scan + historized-override refine
Extend RawTreeService.BuildRenameWarningsAsync so a tag/ancestor rename now
raises three warning kinds over the affected (prefix-scanned) tags:
(a) refined — historized WITHOUT a historianTagname override (default tagname
is the moving RawPath, so history forks); a pinned (override) tag no longer
warns. Count/message adjusted.
(b) unchanged — UNS-referenced (names the equipment).
(c) NEW — substring-scan every Script body for the affected tag's OLD RawPath
(ordinal, false-positives tolerated by design; no AST). OLD RawPaths are
recomputed from the pre-save in-DB topology via RawPathResolver.
CollectTagsBeneath* + TagsForDevices now carry (DeviceId, TagGroupId, Name) so
the OLD RawPath can be rebuilt. Contained entirely within RawTreeService.cs;
IRawTreeService + RawTree.razor untouched (warnings flow through the existing
RawRenameResult.Warnings). Note: a direct tag rename goes through UpdateTagAsync
(UnsMutationResult, no warning surface) so it is out of scope without widening
the interface.
Tests: new RawTreeServiceRenameWarningTests (4) — all-three, pinned-no-warn,
unrelated-empty, ancestor-rename script match. AdminUI.Tests 642 green.
Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -1282,24 +1282,41 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the non-blocking rename warnings answerable from this batch's schema: historized tags and
|
||||
/// equipment-referenced tags beneath the renamed node (whose RawPath — and thus historian tagname /
|
||||
/// UNS projection path — changes with the rename). Batch 3 appends the script-substring scan here.
|
||||
/// A raw tag affected by a rename, carrying the fields needed to (a) read its platform intents and
|
||||
/// (b) recompute its OLD RawPath (still the in-DB state at warning time — the rename is not yet saved).
|
||||
/// </summary>
|
||||
private readonly record struct AffectedTag(
|
||||
string TagId, string TagConfig, string DeviceId, string? TagGroupId, string Name);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the non-blocking rename warnings answerable from this batch's schema for the tags beneath the
|
||||
/// renamed node (whose RawPath — and thus historian tagname / UNS projection path / script literal —
|
||||
/// changes with the rename): (a) historized tags WITHOUT a <c>historianTagname</c> override (their
|
||||
/// history forks, since the default tagname is the moving RawPath; pinned tags are unaffected),
|
||||
/// (b) equipment-referenced tags, and (c) tags whose OLD RawPath appears as a literal substring in any
|
||||
/// <see cref="Script"/> body (tolerates false positives by design — no AST).
|
||||
/// </summary>
|
||||
private static async Task<IReadOnlyList<string>> BuildRenameWarningsAsync(
|
||||
OtOpcUaConfigDbContext db, IReadOnlyList<(string TagId, string TagConfig)> tags, CancellationToken ct)
|
||||
OtOpcUaConfigDbContext db, IReadOnlyList<AffectedTag> tags, CancellationToken ct)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
if (tags.Count == 0) return warnings;
|
||||
|
||||
var historized = tags.Count(t => TagConfigIntent.Parse(t.TagConfig).IsHistorized);
|
||||
if (historized > 0)
|
||||
// (a) Historized WITHOUT a historianTagname override: the default tagname is the RawPath, which is
|
||||
// moving, so history forks on the next deploy. A tag WITH the override is pinned and does not warn.
|
||||
var forking = tags.Count(t =>
|
||||
{
|
||||
warnings.Add(historized == 1
|
||||
? "1 historized tag beneath this node will change its RawPath (historian tagname). Update the historian if it keys on the old path."
|
||||
: $"{historized} historized tags beneath this node will change their RawPath (historian tagname). Update the historian if it keys on the old paths.");
|
||||
var intent = TagConfigIntent.Parse(t.TagConfig);
|
||||
return intent.IsHistorized && intent.HistorianTagname is null;
|
||||
});
|
||||
if (forking > 0)
|
||||
{
|
||||
warnings.Add(forking == 1
|
||||
? "1 historized tag beneath this node has no historianTagname override; its history forks to a new tagname when its RawPath changes on the next deploy. Set a historianTagname to pin it."
|
||||
: $"{forking} historized tags beneath this node have no historianTagname override; their history forks to new tagnames when their RawPaths change on the next deploy. Set a historianTagname to pin them.");
|
||||
}
|
||||
|
||||
// (b) UNS-referenced: name the referencing equipment.
|
||||
var tagIds = tags.Select(t => t.TagId).ToList();
|
||||
var referencingEquipmentIds = await db.UnsTagReferences.AsNoTracking()
|
||||
.Where(r => tagIds.Contains(r.TagId))
|
||||
@@ -1313,12 +1330,94 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
warnings.Add($"Tags beneath this node are referenced by equipment {display}; the UNS projection path will change with the rename.");
|
||||
}
|
||||
|
||||
// Batch 3 extension point: scan scripts for substrings of the affected tags' RawPaths and append
|
||||
// a "N script(s) reference tags beneath this node" warning to this same list.
|
||||
// (c) Script literals: substring-scan every Script body for the affected tags' OLD RawPaths.
|
||||
var scriptWarning = await BuildScriptReferenceWarningAsync(db, tags, ct);
|
||||
if (scriptWarning is not null) warnings.Add(scriptWarning);
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans every <see cref="Script"/> body for the OLD RawPath of any affected tag as a plain ordinal
|
||||
/// substring (no AST — false positives tolerated by design; <c>{{equip}}</c>-relative refs are NOT
|
||||
/// scanned, they resolve through references and the deploy gate catches breakage). Returns a warning
|
||||
/// naming the matched scripts, or <see langword="null"/> when none match.
|
||||
/// </summary>
|
||||
private static async Task<string?> BuildScriptReferenceWarningAsync(
|
||||
OtOpcUaConfigDbContext db, IReadOnlyList<AffectedTag> tags, CancellationToken ct)
|
||||
{
|
||||
var oldPaths = await ComputeOldRawPathsAsync(db, tags, ct);
|
||||
if (oldPaths.Count == 0) return null;
|
||||
|
||||
var scripts = await db.Scripts.AsNoTracking()
|
||||
.Select(s => new { s.ScriptId, s.Name, s.SourceCode })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var matched = scripts
|
||||
.Where(s => oldPaths.Any(p => s.SourceCode.Contains(p, StringComparison.Ordinal)))
|
||||
.OrderBy(s => s.Name, StringComparer.Ordinal)
|
||||
.Select(s => s.Name)
|
||||
.ToList();
|
||||
if (matched.Count == 0) return null;
|
||||
|
||||
var names = string.Join(", ", matched.Select(n => $"'{n}'"));
|
||||
return matched.Count == 1
|
||||
? $"1 script ({names}) references a tag beneath this node by its raw path; the reference will break unless the script is updated."
|
||||
: $"{matched.Count} scripts ({names}) reference tags beneath this node by their raw paths; the references will break unless the scripts are updated.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recomputes the OLD RawPath of each affected tag from the current (pre-save) in-DB raw topology,
|
||||
/// dropping any tag whose ancestry chain is broken (a null RawPath). Loads only the topology the affected
|
||||
/// tags need: their devices + drivers, the drivers' clusters' folders, and the devices' tag groups.
|
||||
/// </summary>
|
||||
private static async Task<IReadOnlyList<string>> ComputeOldRawPathsAsync(
|
||||
OtOpcUaConfigDbContext db, IReadOnlyList<AffectedTag> tags, CancellationToken ct)
|
||||
{
|
||||
var deviceIds = tags.Select(t => t.DeviceId).Distinct(StringComparer.Ordinal).ToList();
|
||||
|
||||
var deviceRows = await db.Devices.AsNoTracking()
|
||||
.Where(d => deviceIds.Contains(d.DeviceId))
|
||||
.Select(d => new { d.DeviceId, d.DriverInstanceId, d.Name })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var driverIds = deviceRows.Select(d => d.DriverInstanceId).Distinct(StringComparer.Ordinal).ToList();
|
||||
var driverRows = await db.DriverInstances.AsNoTracking()
|
||||
.Where(d => driverIds.Contains(d.DriverInstanceId))
|
||||
.Select(d => new { d.DriverInstanceId, d.RawFolderId, d.Name, d.ClusterId })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var clusterIds = driverRows.Select(d => d.ClusterId).Distinct(StringComparer.Ordinal).ToList();
|
||||
var folderRows = await db.RawFolders.AsNoTracking()
|
||||
.Where(f => clusterIds.Contains(f.ClusterId))
|
||||
.Select(f => new { f.RawFolderId, f.ParentRawFolderId, f.Name })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var groupRows = await db.TagGroups.AsNoTracking()
|
||||
.Where(g => deviceIds.Contains(g.DeviceId))
|
||||
.Select(g => new { g.TagGroupId, g.ParentTagGroupId, g.Name })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var folders = folderRows.ToDictionary(
|
||||
f => f.RawFolderId, f => ((string?)f.ParentRawFolderId, f.Name), StringComparer.Ordinal);
|
||||
var drivers = driverRows.ToDictionary(
|
||||
d => d.DriverInstanceId, d => ((string?)d.RawFolderId, d.Name), StringComparer.Ordinal);
|
||||
var devices = deviceRows.ToDictionary(
|
||||
d => d.DeviceId, d => (d.DriverInstanceId, d.Name), StringComparer.Ordinal);
|
||||
var groups = groupRows.ToDictionary(
|
||||
g => g.TagGroupId, g => ((string?)g.ParentTagGroupId, g.Name), StringComparer.Ordinal);
|
||||
|
||||
var resolver = new RawPathResolver(folders, drivers, devices, groups);
|
||||
|
||||
var paths = new List<string>(tags.Count);
|
||||
foreach (var t in tags)
|
||||
{
|
||||
var path = resolver.TryBuildTagPath(t.DeviceId, t.TagGroupId, t.Name);
|
||||
if (path is not null) paths.Add(path);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
/// <summary>Renders a friendly "<c>Name (Id)</c>, …" list for the referencing equipment ids.</summary>
|
||||
private static async Task<string> DescribeEquipmentAsync(
|
||||
OtOpcUaConfigDbContext db, IReadOnlyList<string> equipmentIds, CancellationToken ct)
|
||||
@@ -1336,11 +1435,11 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
|
||||
// --- Subtree tag collectors (walk the in-DB subtree for rename impact) ---
|
||||
|
||||
private static async Task<IReadOnlyList<(string TagId, string TagConfig)>> CollectTagsBeneathFolderAsync(
|
||||
private static async Task<IReadOnlyList<AffectedTag>> CollectTagsBeneathFolderAsync(
|
||||
OtOpcUaConfigDbContext db, string rawFolderId, CancellationToken ct)
|
||||
{
|
||||
var folder = await db.RawFolders.AsNoTracking().FirstOrDefaultAsync(f => f.RawFolderId == rawFolderId, ct);
|
||||
if (folder is null) return Array.Empty<(string, string)>();
|
||||
if (folder is null) return Array.Empty<AffectedTag>();
|
||||
|
||||
// All folders in the cluster, walked in-memory to the descendant set (incl. self) via ParentRawFolderId.
|
||||
var clusterFolders = await db.RawFolders.AsNoTracking()
|
||||
@@ -1359,44 +1458,44 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
.Where(d => d.RawFolderId != null && folderIds.Contains(d.RawFolderId))
|
||||
.Select(d => d.DriverInstanceId)
|
||||
.ToListAsync(ct);
|
||||
if (driverIds.Count == 0) return Array.Empty<(string, string)>();
|
||||
if (driverIds.Count == 0) return Array.Empty<AffectedTag>();
|
||||
|
||||
var deviceIds = await db.Devices.AsNoTracking()
|
||||
.Where(dev => driverIds.Contains(dev.DriverInstanceId))
|
||||
.Select(dev => dev.DeviceId)
|
||||
.ToListAsync(ct);
|
||||
if (deviceIds.Count == 0) return Array.Empty<(string, string)>();
|
||||
if (deviceIds.Count == 0) return Array.Empty<AffectedTag>();
|
||||
|
||||
return await TagsForDevicesAsync(db, deviceIds, ct);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<(string TagId, string TagConfig)>> CollectTagsBeneathDriverAsync(
|
||||
private static async Task<IReadOnlyList<AffectedTag>> CollectTagsBeneathDriverAsync(
|
||||
OtOpcUaConfigDbContext db, string driverInstanceId, CancellationToken ct)
|
||||
{
|
||||
var deviceIds = await db.Devices.AsNoTracking()
|
||||
.Where(dev => dev.DriverInstanceId == driverInstanceId)
|
||||
.Select(dev => dev.DeviceId)
|
||||
.ToListAsync(ct);
|
||||
if (deviceIds.Count == 0) return Array.Empty<(string, string)>();
|
||||
if (deviceIds.Count == 0) return Array.Empty<AffectedTag>();
|
||||
return await TagsForDevicesAsync(db, deviceIds, ct);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<(string TagId, string TagConfig)>> CollectTagsBeneathDeviceAsync(
|
||||
private static async Task<IReadOnlyList<AffectedTag>> CollectTagsBeneathDeviceAsync(
|
||||
OtOpcUaConfigDbContext db, string deviceId, CancellationToken ct)
|
||||
{
|
||||
return (await db.Tags.AsNoTracking()
|
||||
.Where(t => t.DeviceId == deviceId)
|
||||
.Select(t => new { t.TagId, t.TagConfig })
|
||||
.Select(t => new { t.TagId, t.TagConfig, t.DeviceId, t.TagGroupId, t.Name })
|
||||
.ToListAsync(ct))
|
||||
.Select(t => (t.TagId, t.TagConfig))
|
||||
.Select(t => new AffectedTag(t.TagId, t.TagConfig, t.DeviceId, t.TagGroupId, t.Name))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<(string TagId, string TagConfig)>> CollectTagsBeneathGroupAsync(
|
||||
private static async Task<IReadOnlyList<AffectedTag>> CollectTagsBeneathGroupAsync(
|
||||
OtOpcUaConfigDbContext db, string tagGroupId, CancellationToken ct)
|
||||
{
|
||||
var group = await db.TagGroups.AsNoTracking().FirstOrDefaultAsync(g => g.TagGroupId == tagGroupId, ct);
|
||||
if (group is null) return Array.Empty<(string, string)>();
|
||||
if (group is null) return Array.Empty<AffectedTag>();
|
||||
|
||||
var deviceGroups = await db.TagGroups.AsNoTracking()
|
||||
.Where(g => g.DeviceId == group.DeviceId)
|
||||
@@ -1412,20 +1511,20 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
|
||||
return (await db.Tags.AsNoTracking()
|
||||
.Where(t => t.TagGroupId != null && groupIds.Contains(t.TagGroupId))
|
||||
.Select(t => new { t.TagId, t.TagConfig })
|
||||
.Select(t => new { t.TagId, t.TagConfig, t.DeviceId, t.TagGroupId, t.Name })
|
||||
.ToListAsync(ct))
|
||||
.Select(t => (t.TagId, t.TagConfig))
|
||||
.Select(t => new AffectedTag(t.TagId, t.TagConfig, t.DeviceId, t.TagGroupId, t.Name))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<(string TagId, string TagConfig)>> TagsForDevicesAsync(
|
||||
private static async Task<IReadOnlyList<AffectedTag>> TagsForDevicesAsync(
|
||||
OtOpcUaConfigDbContext db, IReadOnlyList<string> deviceIds, CancellationToken ct)
|
||||
{
|
||||
return (await db.Tags.AsNoTracking()
|
||||
.Where(t => deviceIds.Contains(t.DeviceId))
|
||||
.Select(t => new { t.TagId, t.TagConfig })
|
||||
.Select(t => new { t.TagId, t.TagConfig, t.DeviceId, t.TagGroupId, t.Name })
|
||||
.ToListAsync(ct))
|
||||
.Select(t => (t.TagId, t.TagConfig))
|
||||
.Select(t => new AffectedTag(t.TagId, t.TagConfig, t.DeviceId, t.TagGroupId, t.Name))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
/// <summary>
|
||||
/// Covers the B3-WP3 rename-warning extension of <see cref="RawTreeService"/>: the three warning kinds a
|
||||
/// tag/ancestor rename can raise — (a) historized WITHOUT a <c>historianTagname</c> override (history
|
||||
/// forks), (b) UNS-referenced (names the equipment), and (c) matched by a substring scan of a
|
||||
/// <c>Script</c> body for the affected tag's OLD RawPath.
|
||||
/// <para>
|
||||
/// Seeds a bespoke InMemory slice (independent of the shared <c>RawTreeTestDb</c>) so each device holds
|
||||
/// exactly one tag with a controlled property mix. The EF InMemory provider does not enforce
|
||||
/// <c>RowVersion</c> tokens, so renames commit against the seeded row versions.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class RawTreeServiceRenameWarningTests
|
||||
{
|
||||
private const string ClusterId = "MAIN";
|
||||
private const string FolderId = "RF-plant";
|
||||
private const string DriverId = "DRV-modbus";
|
||||
|
||||
// Dev-all → the "everything" tag: historized (no override) + referenced + named in a script literal.
|
||||
private const string DevAllId = "DEV-all";
|
||||
private const string FlowTagId = "TAG-flow";
|
||||
private const string FlowRawPath = "Plant/modbus-1/Dev-all/flow";
|
||||
|
||||
// Dev-pinned → a historized tag WITH a historianTagname override (pinned; must NOT warn).
|
||||
private const string DevPinnedId = "DEV-pinned";
|
||||
private const string TempTagId = "TAG-temp";
|
||||
|
||||
// Dev-plain → a plain tag, unreferenced + absent from every script (must warn nothing).
|
||||
private const string DevPlainId = "DEV-plain";
|
||||
private const string IdleTagId = "TAG-idle";
|
||||
|
||||
private const string EquipmentId = "EQ-000000000001";
|
||||
private const string EquipmentName = "packer-1";
|
||||
private const string ScriptName = "AreaCalc";
|
||||
|
||||
private static (RawTreeService Service, string DbName) Seeded()
|
||||
{
|
||||
var name = $"rawwarn-{Guid.NewGuid():N}";
|
||||
using var db = CreateNamed(name);
|
||||
Seed(db);
|
||||
return (new RawTreeService(new NamedFactory(name)), name);
|
||||
}
|
||||
|
||||
private static OtOpcUaConfigDbContext CreateNamed(string name) =>
|
||||
new(new DbContextOptionsBuilder<OtOpcUaConfigDbContext>().UseInMemoryDatabase(name).Options);
|
||||
|
||||
private static byte[] DeviceRowVersion(string dbName, string deviceId)
|
||||
{
|
||||
using var db = CreateNamed(dbName);
|
||||
return db.Devices.Single(d => d.DeviceId == deviceId).RowVersion;
|
||||
}
|
||||
|
||||
private static byte[] DriverRowVersion(string dbName, string driverId)
|
||||
{
|
||||
using var db = CreateNamed(dbName);
|
||||
return db.DriverInstances.Single(d => d.DriverInstanceId == driverId).RowVersion;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------- Scenarios
|
||||
|
||||
[Fact]
|
||||
public async Task Rename_of_ancestor_of_a_historized_referenced_scripted_tag_raises_all_three_warnings()
|
||||
{
|
||||
var (service, dbName) = Seeded();
|
||||
var rv = DeviceRowVersion(dbName, DevAllId);
|
||||
|
||||
var result = await service.RenameDeviceAsync(DevAllId, "Dev-renamed", rv);
|
||||
|
||||
result.Ok.ShouldBeTrue();
|
||||
result.Warnings.ShouldContain(w => w.Contains("historian")); // (a)
|
||||
result.Warnings.ShouldContain(w => w.Contains(EquipmentName)); // (b)
|
||||
result.Warnings.ShouldContain(w => w.Contains("script") && w.Contains(ScriptName)); // (c)
|
||||
result.Warnings.Count.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Rename_of_a_pinned_historized_tag_raises_no_historized_warning()
|
||||
{
|
||||
var (service, dbName) = Seeded();
|
||||
var rv = DeviceRowVersion(dbName, DevPinnedId);
|
||||
|
||||
var result = await service.RenameDeviceAsync(DevPinnedId, "Dev-pinned2", rv);
|
||||
|
||||
result.Ok.ShouldBeTrue();
|
||||
// A historianTagname override pins the tagname, so its history does NOT fork — no (a) warning; and
|
||||
// the pinned tag is neither referenced nor scripted, so nothing else fires either.
|
||||
result.Warnings.ShouldNotContain(w => w.Contains("historian"));
|
||||
result.Warnings.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Rename_of_an_unrelated_tags_ancestor_raises_no_warnings()
|
||||
{
|
||||
var (service, dbName) = Seeded();
|
||||
var rv = DeviceRowVersion(dbName, DevPlainId);
|
||||
|
||||
var result = await service.RenameDeviceAsync(DevPlainId, "Dev-plain2", rv);
|
||||
|
||||
result.Ok.ShouldBeTrue();
|
||||
result.Warnings.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Script_scan_matches_when_an_ancestor_is_renamed_and_the_old_path_is_in_a_script_body()
|
||||
{
|
||||
var (service, dbName) = Seeded();
|
||||
var rv = DriverRowVersion(dbName, DriverId);
|
||||
|
||||
// Renaming the DRIVER (an ancestor of the scripted tag) still computes the tag's unchanged OLD
|
||||
// RawPath (rename not yet saved) and finds it as a substring of the script body.
|
||||
var result = await service.RenameDriverAsync(DriverId, "modbus-renamed", rv);
|
||||
|
||||
result.Ok.ShouldBeTrue();
|
||||
result.Warnings.ShouldContain(w => w.Contains("script") && w.Contains(ScriptName));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------ Seed
|
||||
|
||||
private static void Seed(OtOpcUaConfigDbContext db)
|
||||
{
|
||||
db.ServerClusters.Add(new ServerCluster
|
||||
{
|
||||
ClusterId = ClusterId,
|
||||
Name = "Main",
|
||||
Enterprise = "zb",
|
||||
Site = "warsaw-west",
|
||||
RedundancyMode = RedundancyMode.None,
|
||||
CreatedBy = "test",
|
||||
});
|
||||
|
||||
db.RawFolders.Add(new RawFolder
|
||||
{
|
||||
RawFolderId = FolderId,
|
||||
ClusterId = ClusterId,
|
||||
ParentRawFolderId = null,
|
||||
Name = "Plant",
|
||||
});
|
||||
db.DriverInstances.Add(new DriverInstance
|
||||
{
|
||||
DriverInstanceId = DriverId,
|
||||
ClusterId = ClusterId,
|
||||
RawFolderId = FolderId,
|
||||
Name = "modbus-1",
|
||||
DriverType = "Modbus",
|
||||
DriverConfig = "{}",
|
||||
});
|
||||
|
||||
db.Devices.Add(new Device { DeviceId = DevAllId, DriverInstanceId = DriverId, Name = "Dev-all", DeviceConfig = "{}" });
|
||||
db.Devices.Add(new Device { DeviceId = DevPinnedId, DriverInstanceId = DriverId, Name = "Dev-pinned", DeviceConfig = "{}" });
|
||||
db.Devices.Add(new Device { DeviceId = DevPlainId, DriverInstanceId = DriverId, Name = "Dev-plain", DeviceConfig = "{}" });
|
||||
|
||||
// Historized, no historianTagname override → its default tagname is FlowRawPath, which moves.
|
||||
db.Tags.Add(new Tag
|
||||
{
|
||||
TagId = FlowTagId,
|
||||
DeviceId = DevAllId,
|
||||
TagGroupId = null,
|
||||
Name = "flow",
|
||||
DataType = "Float",
|
||||
AccessLevel = TagAccessLevel.Read,
|
||||
TagConfig = "{\"isHistorized\":true}",
|
||||
});
|
||||
// Historized WITH a historianTagname override → pinned, must not fork/warn.
|
||||
db.Tags.Add(new Tag
|
||||
{
|
||||
TagId = TempTagId,
|
||||
DeviceId = DevPinnedId,
|
||||
TagGroupId = null,
|
||||
Name = "temp",
|
||||
DataType = "Float",
|
||||
AccessLevel = TagAccessLevel.Read,
|
||||
TagConfig = "{\"isHistorized\":true,\"historianTagname\":\"Plant.Pinned.Temp\"}",
|
||||
});
|
||||
// Plain, unreferenced, absent from scripts.
|
||||
db.Tags.Add(new Tag
|
||||
{
|
||||
TagId = IdleTagId,
|
||||
DeviceId = DevPlainId,
|
||||
TagGroupId = null,
|
||||
Name = "idle",
|
||||
DataType = "Boolean",
|
||||
AccessLevel = TagAccessLevel.Read,
|
||||
TagConfig = "{}",
|
||||
});
|
||||
|
||||
db.Equipment.Add(new Equipment
|
||||
{
|
||||
EquipmentId = EquipmentId,
|
||||
EquipmentUuid = Guid.NewGuid(),
|
||||
UnsLineId = "LINE-1",
|
||||
Name = EquipmentName,
|
||||
MachineCode = "packer_001",
|
||||
});
|
||||
db.UnsTagReferences.Add(new UnsTagReference
|
||||
{
|
||||
UnsTagReferenceId = "REF-flow",
|
||||
EquipmentId = EquipmentId,
|
||||
TagId = FlowTagId,
|
||||
});
|
||||
|
||||
// A script whose body names the flow tag by its absolute RawPath literal.
|
||||
db.Scripts.Add(new Script
|
||||
{
|
||||
ScriptId = "SC-areacalc",
|
||||
Name = ScriptName,
|
||||
SourceCode = $"var v = ctx.GetTag(\"{FlowRawPath}\"); return v * 2;",
|
||||
SourceHash = "hash-areacalc",
|
||||
});
|
||||
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private sealed class NamedFactory(string name) : IDbContextFactory<OtOpcUaConfigDbContext>
|
||||
{
|
||||
public OtOpcUaConfigDbContext CreateDbContext() =>
|
||||
new(new DbContextOptionsBuilder<OtOpcUaConfigDbContext>().UseInMemoryDatabase(name).Options);
|
||||
|
||||
public Task<OtOpcUaConfigDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(CreateDbContext());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user