fix(adminui): AB forms read/write the factory-bound *Ms duration keys, migrate legacy TimeSpan strings (#576)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+67
-6
@@ -141,7 +141,7 @@ public sealed class AbCipDriverPageFormSerializationTests
|
||||
var json = AbCipDriverForm.Serialize(form.ToOptions(loaded.Devices));
|
||||
|
||||
json.ShouldContain($"\"transport\":\"{savedValue}\"");
|
||||
json.ShouldContain("\"timeout\":\"00:00:07\"");
|
||||
json.ShouldContain("\"timeoutMs\":7000");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -158,7 +158,7 @@ public sealed class AbCipDriverPageFormSerializationTests
|
||||
form.TimeoutSeconds = 7;
|
||||
var json = AbCipDriverForm.Serialize(form.ToOptions(loaded.Devices));
|
||||
json.ShouldNotContain("transport", Case.Insensitive);
|
||||
json.ShouldContain("\"timeout\":\"00:00:07\"");
|
||||
json.ShouldContain("\"timeoutMs\":7000");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -195,17 +195,18 @@ public sealed class AbCipDriverPageFormSerializationTests
|
||||
var root = System.Text.Json.Nodes.JsonNode.Parse(
|
||||
AbCipDriverForm.Serialize(form.ToOptions(loaded.Devices), FullConfig))!.AsObject();
|
||||
|
||||
root["timeout"]!.GetValue<string>().ShouldBe("00:00:09");
|
||||
root["timeoutMs"]!.GetValue<int>().ShouldBe(9000);
|
||||
root.ContainsKey("timeout").ShouldBeFalse();
|
||||
root["maxConcurrentReads"]!.GetValue<int>().ShouldBe(3); // was reset to 8 before #576
|
||||
root["transport"]!.GetValue<string>().ShouldBe("Native");
|
||||
root["enableControllerBrowse"]!.GetValue<bool>().ShouldBeTrue();
|
||||
root["enableDeclarationOnlyUdtGrouping"]!.GetValue<bool>().ShouldBeTrue();
|
||||
root["enableAlarmProjection"]!.GetValue<bool>().ShouldBeTrue();
|
||||
root["alarmPollInterval"]!.GetValue<string>().ShouldBe("00:00:02");
|
||||
root["alarmPollIntervalMs"]!.GetValue<int>().ShouldBe(2000);
|
||||
root["probeTimeoutSeconds"]!.GetValue<int>().ShouldBe(12);
|
||||
root["probe"]!["enabled"]!.GetValue<bool>().ShouldBeTrue();
|
||||
root["probe"]!["interval"]!.GetValue<string>().ShouldBe("00:00:10");
|
||||
root["probe"]!["timeout"]!.GetValue<string>().ShouldBe("00:00:03");
|
||||
root["probe"]!["intervalMs"]!.GetValue<int>().ShouldBe(10000);
|
||||
root["probe"]!["timeoutMs"]!.GetValue<int>().ShouldBe(3000);
|
||||
root["probe"]!["probeTagPath"]!.GetValue<string>().ShouldBe("Program:Main.Hb");
|
||||
// unknown keys at every level survive
|
||||
root["probe"]!["futureProbeKnob"]!.GetValue<int>().ShouldBe(7);
|
||||
@@ -245,4 +246,64 @@ public sealed class AbCipDriverPageFormSerializationTests
|
||||
json.ShouldContain("\"transport\":\"Libplctag\"");
|
||||
json.ShouldContain("futureOption");
|
||||
}
|
||||
|
||||
// ---- #576: the saved JSON must be what the driver FACTORY binds (integer-ms keys) ----
|
||||
|
||||
[Fact]
|
||||
public void An_edited_timeout_reaches_the_factory_bound_options()
|
||||
{
|
||||
var original = """{"timeoutMs":5000,"alarmPollIntervalMs":500,"probe":{"enabled":true,"intervalMs":7000,"timeoutMs":1500,"probeTagPath":"Program:Main.Hb"},"devices":[{"hostAddress":"ab://10.0.0.1/1,0"}]}""";
|
||||
var loaded = AbCipDriverForm.TryDeserialize(original)!;
|
||||
var form = AbCipDriverForm.FormModel.FromJson(original, loaded);
|
||||
form.TimeoutSeconds.ShouldBe(5); // the form SEES the factory-keyed value
|
||||
form.TimeoutSeconds = 11;
|
||||
|
||||
var saved = AbCipDriverForm.Serialize(form.ToOptions(loaded.Devices), original);
|
||||
var bound = ZB.MOM.WW.OtOpcUa.Driver.AbCip.AbCipDriverFactoryExtensions.ParseOptions("t", saved);
|
||||
|
||||
bound.Timeout.ShouldBe(TimeSpan.FromSeconds(11));
|
||||
// untouched durations keep their exact stored value, including sub-second ones
|
||||
bound.AlarmPollInterval.ShouldBe(TimeSpan.FromMilliseconds(500));
|
||||
bound.Probe.Interval.ShouldBe(TimeSpan.FromSeconds(7));
|
||||
bound.Probe.Timeout.ShouldBe(TimeSpan.FromMilliseconds(1500));
|
||||
bound.Probe.ProbeTagPath.ShouldBe("Program:Main.Hb");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_legacy_TimeSpan_string_key_is_loaded_then_migrated_to_ms_and_removed()
|
||||
{
|
||||
var original = """{"timeout":"00:00:05","alarmPollInterval":"00:00:02","probe":{"interval":"00:00:10","timeout":"00:00:03"}}""";
|
||||
var loaded = AbCipDriverForm.TryDeserialize(original)!;
|
||||
var form = AbCipDriverForm.FormModel.FromJson(original, loaded);
|
||||
form.TimeoutSeconds.ShouldBe(5);
|
||||
form.TimeoutSeconds = 6;
|
||||
|
||||
var saved = AbCipDriverForm.Serialize(form.ToOptions(loaded.Devices), original);
|
||||
var root = System.Text.Json.Nodes.JsonNode.Parse(saved)!.AsObject();
|
||||
root.ContainsKey("timeout").ShouldBeFalse();
|
||||
root.ContainsKey("alarmPollInterval").ShouldBeFalse();
|
||||
root["probe"]!.AsObject().ContainsKey("interval").ShouldBeFalse();
|
||||
root["probe"]!.AsObject().ContainsKey("timeout").ShouldBeFalse();
|
||||
|
||||
var bound = ZB.MOM.WW.OtOpcUa.Driver.AbCip.AbCipDriverFactoryExtensions.ParseOptions("t", saved);
|
||||
bound.Timeout.ShouldBe(TimeSpan.FromSeconds(6));
|
||||
bound.AlarmPollInterval.ShouldBe(TimeSpan.FromSeconds(2));
|
||||
bound.Probe.Interval.ShouldBe(TimeSpan.FromSeconds(10));
|
||||
bound.Probe.Timeout.ShouldBe(TimeSpan.FromSeconds(3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_form_saved_config_is_still_accepted_by_the_Test_Connect_probe_parse()
|
||||
{
|
||||
// The probe deserialises the typed options and skips unmapped keys, so the ms keys must not break it.
|
||||
var original = """{"timeoutMs":5000,"devices":[{"hostAddress":"ab://10.0.0.1/1,0"}]}""";
|
||||
var loaded = AbCipDriverForm.TryDeserialize(original)!;
|
||||
var form = AbCipDriverForm.FormModel.FromJson(original, loaded);
|
||||
form.TimeoutSeconds = 8;
|
||||
var saved = AbCipDriverForm.Serialize(form.ToOptions(loaded.Devices), original);
|
||||
|
||||
var opts = System.Text.Json.JsonSerializer.Deserialize<AbCipDriverOptions>(saved, TestJsonOpts);
|
||||
opts.ShouldNotBeNull();
|
||||
opts.Devices[0].HostAddress.ShouldBe("ab://10.0.0.1/1,0");
|
||||
}
|
||||
}
|
||||
|
||||
+47
-5
@@ -137,7 +137,7 @@ public sealed class AbLegacyDriverPageFormSerializationTests
|
||||
var json = AbLegacyDriverForm.Serialize(form.ToOptions(loaded.Devices));
|
||||
|
||||
json.ShouldContain($"\"transport\":\"{savedValue}\"");
|
||||
json.ShouldContain("\"timeout\":\"00:00:07\"");
|
||||
json.ShouldContain("\"timeoutMs\":7000");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -153,7 +153,7 @@ public sealed class AbLegacyDriverPageFormSerializationTests
|
||||
form.TimeoutSeconds = 7;
|
||||
var json = AbLegacyDriverForm.Serialize(form.ToOptions(loaded.Devices));
|
||||
json.ShouldNotContain("transport", Case.Insensitive);
|
||||
json.ShouldContain("\"timeout\":\"00:00:07\"");
|
||||
json.ShouldContain("\"timeoutMs\":7000");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -208,12 +208,13 @@ public sealed class AbLegacyDriverPageFormSerializationTests
|
||||
var root = System.Text.Json.Nodes.JsonNode.Parse(
|
||||
AbLegacyDriverForm.Serialize(form.ToOptions(loaded.Devices), FullConfig))!.AsObject();
|
||||
|
||||
root["timeout"]!.GetValue<string>().ShouldBe("00:00:09");
|
||||
root["timeoutMs"]!.GetValue<int>().ShouldBe(9000);
|
||||
root.ContainsKey("timeout").ShouldBeFalse();
|
||||
root["transport"]!.GetValue<string>().ShouldBe("Native");
|
||||
root["probeTimeoutSeconds"]!.GetValue<int>().ShouldBe(12);
|
||||
root["probe"]!["enabled"]!.GetValue<bool>().ShouldBeTrue();
|
||||
root["probe"]!["interval"]!.GetValue<string>().ShouldBe("00:00:10");
|
||||
root["probe"]!["timeout"]!.GetValue<string>().ShouldBe("00:00:03");
|
||||
root["probe"]!["intervalMs"]!.GetValue<int>().ShouldBe(10000);
|
||||
root["probe"]!["timeoutMs"]!.GetValue<int>().ShouldBe(3000);
|
||||
root["probe"]!["probeAddress"]!.GetValue<string>().ShouldBe("S:1");
|
||||
root["probe"]!["futureProbeKnob"]!.GetValue<int>().ShouldBe(7);
|
||||
root["devices"]![0]!["futureDeviceKey"]!.GetValue<string>().ShouldBe("keep");
|
||||
@@ -234,4 +235,45 @@ public sealed class AbLegacyDriverPageFormSerializationTests
|
||||
root.ContainsKey("transport").ShouldBeFalse();
|
||||
root["futureOption"].ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// ---- #576: the saved JSON must be what the driver FACTORY binds (integer-ms keys) ----
|
||||
|
||||
[Fact]
|
||||
public void An_edited_timeout_reaches_the_factory_bound_options()
|
||||
{
|
||||
var original = """{"timeoutMs":5000,"probe":{"enabled":true,"intervalMs":7000,"timeoutMs":1500,"probeAddress":"S:1"},"devices":[{"hostAddress":"ab://10.0.0.1/1,0","plcFamily":"Slc500"}]}""";
|
||||
var loaded = AbLegacyDriverForm.TryDeserialize(original)!;
|
||||
var form = AbLegacyDriverForm.FormModel.FromJson(original, loaded);
|
||||
form.TimeoutSeconds.ShouldBe(5);
|
||||
form.TimeoutSeconds = 11;
|
||||
|
||||
var saved = AbLegacyDriverForm.Serialize(form.ToOptions(loaded.Devices), original);
|
||||
var bound = ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.AbLegacyDriverFactoryExtensions.ParseOptions("t", saved);
|
||||
|
||||
bound.Timeout.ShouldBe(TimeSpan.FromSeconds(11));
|
||||
bound.Probe.Interval.ShouldBe(TimeSpan.FromSeconds(7));
|
||||
bound.Probe.Timeout.ShouldBe(TimeSpan.FromMilliseconds(1500));
|
||||
bound.Probe.ProbeAddress.ShouldBe("S:1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_legacy_TimeSpan_string_key_is_loaded_then_migrated_to_ms_and_removed()
|
||||
{
|
||||
var original = """{"timeout":"00:00:05","probe":{"interval":"00:00:10","timeout":"00:00:03"}}""";
|
||||
var loaded = AbLegacyDriverForm.TryDeserialize(original)!;
|
||||
var form = AbLegacyDriverForm.FormModel.FromJson(original, loaded);
|
||||
form.TimeoutSeconds.ShouldBe(5);
|
||||
form.TimeoutSeconds = 6;
|
||||
|
||||
var saved = AbLegacyDriverForm.Serialize(form.ToOptions(loaded.Devices), original);
|
||||
var root = System.Text.Json.Nodes.JsonNode.Parse(saved)!.AsObject();
|
||||
root.ContainsKey("timeout").ShouldBeFalse();
|
||||
root["probe"]!.AsObject().ContainsKey("interval").ShouldBeFalse();
|
||||
root["probe"]!.AsObject().ContainsKey("timeout").ShouldBeFalse();
|
||||
|
||||
var bound = ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.AbLegacyDriverFactoryExtensions.ParseOptions("t", saved);
|
||||
bound.Timeout.ShouldBe(TimeSpan.FromSeconds(6));
|
||||
bound.Probe.Interval.ShouldBe(TimeSpan.FromSeconds(10));
|
||||
bound.Probe.Timeout.ShouldBe(TimeSpan.FromSeconds(3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
project referencing a runtime driver is fine — tests are not in the AdminUI's deploy
|
||||
graph, which is the whole thing #526 was about. -->
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Sql\ZB.MOM.WW.OtOpcUa.Driver.Sql.csproj"/>
|
||||
<!-- #576: the AB form tests deserialise the saved JSON through the drivers' own factory parse. -->
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbCip\ZB.MOM.WW.OtOpcUa.Driver.AbCip.csproj"/>
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user