Phase 0 — mechanical rename ZB.MOM.WW.LmxOpcUa.* → ZB.MOM.WW.OtOpcUa.*
Renames all 11 projects (5 src + 6 tests), the .slnx solution file, all source-file namespaces, all axaml namespace references, and all v1 documentation references in CLAUDE.md and docs/*.md (excluding docs/v2/ which is already in OtOpcUa form). Also updates the TopShelf service registration name from "LmxOpcUa" to "OtOpcUa" per Phase 0 Task 0.6.
Preserves runtime identifiers per Phase 0 Out-of-Scope rules to avoid breaking v1/v2 client trust during coexistence: OPC UA `ApplicationUri` defaults (`urn:{GalaxyName}:LmxOpcUa`), server `EndpointPath` (`/LmxOpcUa`), `ServerName` default (feeds cert subject CN), `MxAccessConfiguration.ClientName` default (defensive — stays "LmxOpcUa" for MxAccess audit-trail consistency), client OPC UA identifiers (`ApplicationName = "LmxOpcUaClient"`, `ApplicationUri = "urn:localhost:LmxOpcUaClient"`, cert directory `%LocalAppData%\LmxOpcUaClient\pki\`), and the `LmxOpcUaServer` class name (class rename out of Phase 0 scope per Task 0.5 sed pattern; happens in Phase 1 alongside `LmxNodeManager → GenericDriverNodeManager` Core extraction). 23 LmxOpcUa references retained, all enumerated and justified in `docs/v2/implementation/exit-gate-phase-0.md`.
Build clean: 0 errors, 30 warnings (lower than baseline 167). Tests at strict improvement over baseline: 821 passing / 1 failing vs baseline 820 / 2 (one flaky pre-existing failure passed this run; the other still fails — both pre-existing and unrelated to the rename). `Client.UI.Tests`, `Historian.Aveva.Tests`, `Client.Shared.Tests`, `IntegrationTests` all match baseline exactly. Exit gate compliance results recorded in `docs/v2/implementation/exit-gate-phase-0.md` with all 7 checks PASS or DEFERRED-to-PR-review (#7 service install verification needs Windows service permissions on the reviewer's box).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Tests.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that application configuration binds correctly from appsettings and that validation catches invalid bridge
|
||||
/// settings.
|
||||
/// </summary>
|
||||
public class ConfigurationLoadingTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads the application configuration from the repository appsettings file for binding tests.
|
||||
/// </summary>
|
||||
/// <returns>The bound application configuration snapshot.</returns>
|
||||
private static AppConfiguration LoadFromJson()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.json", false)
|
||||
.Build();
|
||||
|
||||
var config = new AppConfiguration();
|
||||
configuration.GetSection("OpcUa").Bind(config.OpcUa);
|
||||
configuration.GetSection("MxAccess").Bind(config.MxAccess);
|
||||
configuration.GetSection("GalaxyRepository").Bind(config.GalaxyRepository);
|
||||
configuration.GetSection("Dashboard").Bind(config.Dashboard);
|
||||
configuration.GetSection("Security").Bind(config.Security);
|
||||
configuration.GetSection("Historian").Bind(config.Historian);
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the OPC UA section binds the endpoint and session settings expected by the bridge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OpcUa_Section_BindsCorrectly()
|
||||
{
|
||||
var config = LoadFromJson();
|
||||
config.OpcUa.BindAddress.ShouldBe("0.0.0.0");
|
||||
config.OpcUa.Port.ShouldBe(4840);
|
||||
config.OpcUa.EndpointPath.ShouldBe("/LmxOpcUa");
|
||||
config.OpcUa.ServerName.ShouldBe("LmxOpcUa");
|
||||
config.OpcUa.GalaxyName.ShouldBe("ZB");
|
||||
config.OpcUa.MaxSessions.ShouldBe(100);
|
||||
config.OpcUa.SessionTimeoutMinutes.ShouldBe(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the MXAccess section binds runtime timeout and reconnect settings correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MxAccess_Section_BindsCorrectly()
|
||||
{
|
||||
var config = LoadFromJson();
|
||||
config.MxAccess.ClientName.ShouldBe("LmxOpcUa");
|
||||
config.MxAccess.ReadTimeoutSeconds.ShouldBe(5);
|
||||
config.MxAccess.WriteTimeoutSeconds.ShouldBe(5);
|
||||
config.MxAccess.MaxConcurrentOperations.ShouldBe(10);
|
||||
config.MxAccess.MonitorIntervalSeconds.ShouldBe(5);
|
||||
config.MxAccess.AutoReconnect.ShouldBe(true);
|
||||
config.MxAccess.ProbeStaleThresholdSeconds.ShouldBe(60);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the Galaxy repository section binds connection and polling settings correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GalaxyRepository_Section_BindsCorrectly()
|
||||
{
|
||||
var config = LoadFromJson();
|
||||
config.GalaxyRepository.ConnectionString.ShouldContain("ZB");
|
||||
config.GalaxyRepository.ChangeDetectionIntervalSeconds.ShouldBe(30);
|
||||
config.GalaxyRepository.CommandTimeoutSeconds.ShouldBe(30);
|
||||
config.GalaxyRepository.ExtendedAttributes.ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that extended-attribute loading defaults to disabled when not configured.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GalaxyRepository_ExtendedAttributes_DefaultsFalse()
|
||||
{
|
||||
var config = new GalaxyRepositoryConfiguration();
|
||||
config.ExtendedAttributes.ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the extended-attribute flag can be enabled through configuration binding.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GalaxyRepository_ExtendedAttributes_BindsFromJson()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.json", false)
|
||||
.AddInMemoryCollection(new[]
|
||||
{ new KeyValuePair<string, string>("GalaxyRepository:ExtendedAttributes", "true") })
|
||||
.Build();
|
||||
|
||||
var config = new GalaxyRepositoryConfiguration();
|
||||
configuration.GetSection("GalaxyRepository").Bind(config);
|
||||
config.ExtendedAttributes.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the dashboard section binds operator-dashboard settings correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Dashboard_Section_BindsCorrectly()
|
||||
{
|
||||
var config = LoadFromJson();
|
||||
config.Dashboard.Enabled.ShouldBe(true);
|
||||
config.Dashboard.Port.ShouldBe(8081);
|
||||
config.Dashboard.RefreshIntervalSeconds.ShouldBe(10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the default configuration objects start with the expected bridge defaults.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DefaultValues_AreCorrect()
|
||||
{
|
||||
var config = new AppConfiguration();
|
||||
config.OpcUa.BindAddress.ShouldBe("0.0.0.0");
|
||||
config.OpcUa.Port.ShouldBe(4840);
|
||||
config.MxAccess.ClientName.ShouldBe("LmxOpcUa");
|
||||
config.GalaxyRepository.ChangeDetectionIntervalSeconds.ShouldBe(30);
|
||||
config.Dashboard.Enabled.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that BindAddress can be overridden to a specific hostname or IP.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OpcUa_BindAddress_CanBeOverridden()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("OpcUa:BindAddress", "localhost")
|
||||
})
|
||||
.Build();
|
||||
|
||||
var config = new OpcUaConfiguration();
|
||||
configuration.GetSection("OpcUa").Bind(config);
|
||||
config.BindAddress.ShouldBe("localhost");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a valid configuration passes startup validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_ValidConfig_ReturnsTrue()
|
||||
{
|
||||
var config = LoadFromJson();
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that an invalid OPC UA port is rejected by startup validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_InvalidPort_ReturnsFalse()
|
||||
{
|
||||
var config = new AppConfiguration();
|
||||
config.OpcUa.Port = 0;
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that an empty Galaxy name is rejected because the bridge requires a namespace target.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_EmptyGalaxyName_ReturnsFalse()
|
||||
{
|
||||
var config = new AppConfiguration();
|
||||
config.OpcUa.GalaxyName = "";
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the Security section binds profile list from appsettings.json.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Security_Section_BindsProfilesCorrectly()
|
||||
{
|
||||
var config = LoadFromJson();
|
||||
config.Security.Profiles.ShouldContain("None");
|
||||
config.Security.AutoAcceptClientCertificates.ShouldBe(true);
|
||||
config.Security.MinimumCertificateKeySize.ShouldBe(2048);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stability review 2026-04-13 Finding 3: MxAccess.RequestTimeoutSeconds must be at
|
||||
/// least 1. Zero or negative values disable the safety bound and are rejected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_MxAccessRequestTimeoutZero_ReturnsFalse()
|
||||
{
|
||||
var config = LoadFromJson();
|
||||
config.MxAccess.RequestTimeoutSeconds = 0;
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stability review 2026-04-13 Finding 3: Historian.RequestTimeoutSeconds must be at
|
||||
/// least 1 when historian is enabled.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_HistorianRequestTimeoutZero_ReturnsFalse()
|
||||
{
|
||||
var config = LoadFromJson();
|
||||
config.Historian.Enabled = true;
|
||||
config.Historian.ServerName = "localhost";
|
||||
config.Historian.RequestTimeoutSeconds = 0;
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms the bound AppConfiguration carries non-zero default request timeouts.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_DefaultRequestTimeouts_AreSensible()
|
||||
{
|
||||
var config = new AppConfiguration();
|
||||
config.MxAccess.RequestTimeoutSeconds.ShouldBeGreaterThanOrEqualTo(1);
|
||||
config.Historian.RequestTimeoutSeconds.ShouldBeGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a minimum key size below 2048 is rejected by the validator.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_InvalidMinKeySize_ReturnsFalse()
|
||||
{
|
||||
var config = new AppConfiguration();
|
||||
config.Security.MinimumCertificateKeySize = 1024;
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that a valid configuration with security defaults passes validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validator_DefaultSecurityConfig_ReturnsTrue()
|
||||
{
|
||||
var config = LoadFromJson();
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that custom security profiles can be bound from in-memory configuration.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Security_Section_BindsCustomProfiles()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Security:Profiles:0", "None"),
|
||||
new KeyValuePair<string, string>("Security:Profiles:1", "Basic256Sha256-SignAndEncrypt"),
|
||||
new KeyValuePair<string, string>("Security:AutoAcceptClientCertificates", "false"),
|
||||
new KeyValuePair<string, string>("Security:MinimumCertificateKeySize", "4096")
|
||||
})
|
||||
.Build();
|
||||
|
||||
// Clear default list before binding to match production behavior
|
||||
var config = new AppConfiguration();
|
||||
config.Security.Profiles.Clear();
|
||||
configuration.GetSection("Security").Bind(config.Security);
|
||||
|
||||
config.Security.Profiles.Count.ShouldBe(2);
|
||||
config.Security.Profiles.ShouldContain("None");
|
||||
config.Security.Profiles.ShouldContain("Basic256Sha256-SignAndEncrypt");
|
||||
config.Security.AutoAcceptClientCertificates.ShouldBe(false);
|
||||
config.Security.MinimumCertificateKeySize.ShouldBe(4096);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Redundancy_Section_BindsFromJson()
|
||||
{
|
||||
var config = LoadFromJson();
|
||||
config.Redundancy.Enabled.ShouldBe(false);
|
||||
config.Redundancy.Mode.ShouldBe("Warm");
|
||||
config.Redundancy.Role.ShouldBe("Primary");
|
||||
config.Redundancy.ServiceLevelBase.ShouldBe(200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Redundancy_Section_BindsCustomValues()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Redundancy:Enabled", "true"),
|
||||
new KeyValuePair<string, string>("Redundancy:Mode", "Hot"),
|
||||
new KeyValuePair<string, string>("Redundancy:Role", "Secondary"),
|
||||
new KeyValuePair<string, string>("Redundancy:ServiceLevelBase", "180"),
|
||||
new KeyValuePair<string, string>("Redundancy:ServerUris:0", "urn:a"),
|
||||
new KeyValuePair<string, string>("Redundancy:ServerUris:1", "urn:b")
|
||||
})
|
||||
.Build();
|
||||
|
||||
var config = new AppConfiguration();
|
||||
configuration.GetSection("Redundancy").Bind(config.Redundancy);
|
||||
|
||||
config.Redundancy.Enabled.ShouldBe(true);
|
||||
config.Redundancy.Mode.ShouldBe("Hot");
|
||||
config.Redundancy.Role.ShouldBe("Secondary");
|
||||
config.Redundancy.ServiceLevelBase.ShouldBe(180);
|
||||
config.Redundancy.ServerUris.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validator_RedundancyEnabled_NoApplicationUri_ReturnsFalse()
|
||||
{
|
||||
var config = new AppConfiguration();
|
||||
config.Redundancy.Enabled = true;
|
||||
config.Redundancy.ServerUris.Add("urn:a");
|
||||
config.Redundancy.ServerUris.Add("urn:b");
|
||||
// OpcUa.ApplicationUri is null
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validator_InvalidServiceLevelBase_ReturnsFalse()
|
||||
{
|
||||
var config = new AppConfiguration();
|
||||
config.Redundancy.ServiceLevelBase = 0;
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpcUa_ApplicationUri_DefaultsToNull()
|
||||
{
|
||||
var config = new OpcUaConfiguration();
|
||||
config.ApplicationUri.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpcUa_ApplicationUri_BindsFromConfig()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("OpcUa:ApplicationUri", "urn:test:app")
|
||||
})
|
||||
.Build();
|
||||
|
||||
var config = new OpcUaConfiguration();
|
||||
configuration.GetSection("OpcUa").Bind(config);
|
||||
config.ApplicationUri.ShouldBe("urn:test:app");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Historian_Section_BindsFromJson()
|
||||
{
|
||||
var config = LoadFromJson();
|
||||
config.Historian.Enabled.ShouldBe(false);
|
||||
config.Historian.ServerName.ShouldBe("localhost");
|
||||
config.Historian.IntegratedSecurity.ShouldBe(true);
|
||||
config.Historian.Port.ShouldBe(32568);
|
||||
config.Historian.CommandTimeoutSeconds.ShouldBe(30);
|
||||
config.Historian.MaxValuesPerRead.ShouldBe(10000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Historian_Section_BindsCustomValues()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("Historian:Enabled", "true"),
|
||||
new KeyValuePair<string, string>("Historian:ServerName", "historian-server"),
|
||||
new KeyValuePair<string, string>("Historian:IntegratedSecurity", "false"),
|
||||
new KeyValuePair<string, string>("Historian:UserName", "testuser"),
|
||||
new KeyValuePair<string, string>("Historian:Password", "testpass"),
|
||||
new KeyValuePair<string, string>("Historian:Port", "12345"),
|
||||
new KeyValuePair<string, string>("Historian:CommandTimeoutSeconds", "60"),
|
||||
new KeyValuePair<string, string>("Historian:MaxValuesPerRead", "5000")
|
||||
})
|
||||
.Build();
|
||||
|
||||
var config = new HistorianConfiguration();
|
||||
configuration.GetSection("Historian").Bind(config);
|
||||
|
||||
config.Enabled.ShouldBe(true);
|
||||
config.ServerName.ShouldBe("historian-server");
|
||||
config.IntegratedSecurity.ShouldBe(false);
|
||||
config.UserName.ShouldBe("testuser");
|
||||
config.Password.ShouldBe("testpass");
|
||||
config.Port.ShouldBe(12345);
|
||||
config.CommandTimeoutSeconds.ShouldBe(60);
|
||||
config.MaxValuesPerRead.ShouldBe(5000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validator_HistorianEnabled_EmptyServerName_ReturnsFalse()
|
||||
{
|
||||
var config = new AppConfiguration();
|
||||
config.Historian.Enabled = true;
|
||||
config.Historian.ServerName = "";
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validator_HistorianEnabled_InvalidPort_ReturnsFalse()
|
||||
{
|
||||
var config = new AppConfiguration();
|
||||
config.Historian.Enabled = true;
|
||||
config.Historian.Port = 0;
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validator_HistorianEnabled_NoIntegratedSecurity_EmptyUserName_ReturnsFalse()
|
||||
{
|
||||
var config = new AppConfiguration();
|
||||
config.Historian.Enabled = true;
|
||||
config.Historian.IntegratedSecurity = false;
|
||||
config.Historian.UserName = "";
|
||||
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Tests.Configuration
|
||||
{
|
||||
public class HistorianConfigurationTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultConfig_Disabled()
|
||||
{
|
||||
var config = new HistorianConfiguration();
|
||||
config.Enabled.ShouldBe(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultConfig_ServerNameLocalhost()
|
||||
{
|
||||
var config = new HistorianConfiguration();
|
||||
config.ServerName.ShouldBe("localhost");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultConfig_IntegratedSecurityTrue()
|
||||
{
|
||||
var config = new HistorianConfiguration();
|
||||
config.IntegratedSecurity.ShouldBe(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultConfig_UserNameNull()
|
||||
{
|
||||
var config = new HistorianConfiguration();
|
||||
config.UserName.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultConfig_PasswordNull()
|
||||
{
|
||||
var config = new HistorianConfiguration();
|
||||
config.Password.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultConfig_Port32568()
|
||||
{
|
||||
var config = new HistorianConfiguration();
|
||||
config.Port.ShouldBe(32568);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultConfig_CommandTimeout30()
|
||||
{
|
||||
var config = new HistorianConfiguration();
|
||||
config.CommandTimeoutSeconds.ShouldBe(30);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultConfig_MaxValuesPerRead10000()
|
||||
{
|
||||
var config = new HistorianConfiguration();
|
||||
config.MaxValuesPerRead.ShouldBe(10000);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user