4de222c950
# Conflicts: # docs/plans/hcal-roadmap.md # src/AVEVA.Historian.Client/HistorianClient.cs # src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs # tests/AVEVA.Historian.Client.Tests/HistorianClientIntegrationTests.cs # tools/AVEVA.Historian.NativeTraceHarness/Program.cs
1042 lines
44 KiB
C#
1042 lines
44 KiB
C#
namespace AVEVA.Historian.Client.Tests;
|
|
|
|
public sealed class HistorianClientIntegrationTests
|
|
{
|
|
[Fact]
|
|
public async Task ProbeAsync_ReturnsTrueForConfiguredHistorian()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_PORT"), out int parsedPort)
|
|
? parsedPort
|
|
: HistorianClientOptions.DefaultPort;
|
|
HistorianClient client = new(new HistorianClientOptions { Host = host, Port = port });
|
|
|
|
Assert.True(await client.ProbeAsync(CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BrowseTagNamesAsync_ReturnsConfiguredTestTag()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG");
|
|
string? filter = Environment.GetEnvironmentVariable("HISTORIAN_TAG_FILTER") ?? testTag;
|
|
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag) || string.IsNullOrWhiteSpace(filter))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_PORT"), out int parsedPort)
|
|
? parsedPort
|
|
: HistorianClientOptions.DefaultPort;
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
Port = port,
|
|
IntegratedSecurity = true,
|
|
UserName = Environment.GetEnvironmentVariable("HISTORIAN_USER") ?? string.Empty,
|
|
Password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD") ?? string.Empty
|
|
});
|
|
|
|
List<string> tagNames = [];
|
|
await foreach (string tagName in client.BrowseTagNamesAsync(filter, CancellationToken.None))
|
|
{
|
|
tagNames.Add(tagName);
|
|
}
|
|
|
|
Assert.Contains(testTag, tagNames);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadRawAsync_AgainstLocalHistorian_ReturnsAtLeastOneRow()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG");
|
|
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// The managed read flow currently only supports the LocalPipe transport.
|
|
return;
|
|
}
|
|
|
|
if (!OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
DateTime endUtc = DateTime.UtcNow;
|
|
DateTime startUtc = endUtc - TimeSpan.FromDays(7);
|
|
|
|
List<AVEVA.Historian.Client.Models.HistorianSample> samples = [];
|
|
await foreach (AVEVA.Historian.Client.Models.HistorianSample sample in client.ReadRawAsync(testTag, startUtc, endUtc, maxValues: 8, CancellationToken.None))
|
|
{
|
|
samples.Add(sample);
|
|
}
|
|
|
|
Assert.NotEmpty(samples);
|
|
Assert.All(samples, s => Assert.Equal(testTag, s.TagName));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAggregateAsync_AgainstLocalHistorian_ReturnsTimeWeightedAverageRows()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG");
|
|
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
DateTime endUtc = DateTime.UtcNow;
|
|
DateTime startUtc = endUtc - TimeSpan.FromMinutes(10);
|
|
|
|
List<AVEVA.Historian.Client.Models.HistorianAggregateSample> samples = [];
|
|
await foreach (AVEVA.Historian.Client.Models.HistorianAggregateSample sample in client.ReadAggregateAsync(
|
|
testTag, startUtc, endUtc,
|
|
AVEVA.Historian.Client.Models.RetrievalMode.TimeWeightedAverage,
|
|
TimeSpan.FromMinutes(1),
|
|
CancellationToken.None))
|
|
{
|
|
samples.Add(sample);
|
|
}
|
|
|
|
Assert.NotEmpty(samples);
|
|
Assert.All(samples, s => Assert.Equal(testTag, s.TagName));
|
|
Assert.All(samples, s => Assert.Equal(AVEVA.Historian.Client.Models.RetrievalMode.TimeWeightedAverage, s.RetrievalMode));
|
|
}
|
|
|
|
// Verifies a previously-unmapped RetrievalMode (one of the 11 modes that prior to
|
|
// 2026-05-04 threw ProtocolEvidenceMissingException). MinimumWithTime → QueryType=6
|
|
// exercises the "QueryType is the native enum ordinal" mapping against the live server.
|
|
[Theory]
|
|
[InlineData(AVEVA.Historian.Client.Models.RetrievalMode.MinimumWithTime)]
|
|
[InlineData(AVEVA.Historian.Client.Models.RetrievalMode.MaximumWithTime)]
|
|
[InlineData(AVEVA.Historian.Client.Models.RetrievalMode.BestFit)]
|
|
public async Task ReadAggregateAsync_AgainstLocalHistorian_AcceptsPreviouslyUnmappedRetrievalMode(
|
|
AVEVA.Historian.Client.Models.RetrievalMode mode)
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG");
|
|
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)
|
|
|| !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase)
|
|
|| !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
DateTime endUtc = DateTime.UtcNow;
|
|
DateTime startUtc = endUtc - TimeSpan.FromMinutes(10);
|
|
|
|
List<AVEVA.Historian.Client.Models.HistorianAggregateSample> samples = [];
|
|
await foreach (AVEVA.Historian.Client.Models.HistorianAggregateSample s in client.ReadAggregateAsync(
|
|
testTag, startUtc, endUtc, mode, TimeSpan.FromMinutes(2), CancellationToken.None))
|
|
{
|
|
samples.Add(s);
|
|
}
|
|
|
|
// Server should accept the request without error. Even if no rows come back
|
|
// (unlikely for a 10-minute window on a steadily-counting tag), the absence of an
|
|
// exception proves the QueryType byte was accepted.
|
|
Assert.All(samples, s => Assert.Equal(mode, s.RetrievalMode));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadAtTimeAsync_AgainstLocalHistorian_ReturnsRequestedTimestamps()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG");
|
|
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
DateTime nowUtc = DateTime.UtcNow;
|
|
DateTime[] timestamps =
|
|
[
|
|
nowUtc - TimeSpan.FromMinutes(5),
|
|
nowUtc - TimeSpan.FromMinutes(2),
|
|
nowUtc - TimeSpan.FromMinutes(1)
|
|
];
|
|
|
|
IReadOnlyList<AVEVA.Historian.Client.Models.HistorianSample> samples = await client.ReadAtTimeAsync(testTag, timestamps, CancellationToken.None);
|
|
|
|
Assert.NotEmpty(samples);
|
|
Assert.All(samples, s => Assert.Equal(testTag, s.TagName));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadEventsAsync_AgainstLocalHistorian_ReturnsWellFormedEvents()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
DateTime endUtc = DateTime.UtcNow;
|
|
DateTime startUtc = endUtc - TimeSpan.FromDays(30);
|
|
|
|
// The full chain (ValCl + Open2 + Retr.IsOriginalAllowed + Retr.StartEventQuery +
|
|
// GetNextEventQueryResultBuffer + HistorianEventRowProtocol.Parse) returns real, parsed
|
|
// events. Requires the local store to hold events in the window — System-Platform
|
|
// alarm/user-write events are present on a working Historian. NOTE: enumeration currently
|
|
// stops at the first benign `type=4 code=85` soft-terminal, so this verifies parsing
|
|
// correctness rather than exhaustive retrieval (decoding code 85 to drain all rows is a
|
|
// separate capture task).
|
|
List<AVEVA.Historian.Client.Models.HistorianEvent> events = [];
|
|
await foreach (AVEVA.Historian.Client.Models.HistorianEvent evt in client.ReadEventsAsync(startUtc, endUtc, CancellationToken.None))
|
|
{
|
|
events.Add(evt);
|
|
}
|
|
|
|
Assert.NotEmpty(events);
|
|
Assert.All(events, evt =>
|
|
{
|
|
Assert.False(string.IsNullOrWhiteSpace(evt.Type)); // e.g. "User.Write", "Alarm.Set"
|
|
Assert.NotNull(evt.Properties);
|
|
Assert.InRange(evt.EventTimeUtc, startUtc - TimeSpan.FromDays(1), endUtc + TimeSpan.FromDays(1));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetSystemParameterAsync_AgainstLocalHistorian_ReturnsHistorianVersion()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
string? value = await client.GetSystemParameterAsync("HistorianVersion", CancellationToken.None);
|
|
|
|
// The server returns a non-empty version string for the documented HistorianVersion parameter.
|
|
Assert.False(string.IsNullOrWhiteSpace(value));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetRuntimeParameterAsync_AgainstLocalHistorian_ReturnsHistorianVersion()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
// GETRP rides the storage-session GUID as an uppercase string handle. HistorianVersion is
|
|
// a known-good runtime parameter returning the server version (e.g. "20,0,000,000").
|
|
string? value = await client.GetRuntimeParameterAsync("HistorianVersion", CancellationToken.None);
|
|
|
|
Assert.False(string.IsNullOrWhiteSpace(value));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetTagExtendedPropertiesAsync_AgainstLocalHistorian_ReturnsProperties()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
// A tag that carries at least one extended property. Gated on its own env var so the test
|
|
// skips cleanly when no such tag is configured (no tag name is hardcoded).
|
|
string? tepTag = Environment.GetEnvironmentVariable("HISTORIAN_TEP_TAG");
|
|
if (string.IsNullOrWhiteSpace(host)
|
|
|| !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase)
|
|
|| !OperatingSystem.IsWindows()
|
|
|| string.IsNullOrWhiteSpace(tepTag))
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
// GetTepByNm rides the storage-session GUID as an uppercase string handle. The configured
|
|
// tag has at least one string-valued extended property (e.g. Location).
|
|
IReadOnlyList<AVEVA.Historian.Client.Models.HistorianTagExtendedProperty> properties =
|
|
await client.GetTagExtendedPropertiesAsync(tepTag, CancellationToken.None);
|
|
|
|
Assert.NotEmpty(properties);
|
|
Assert.All(properties, p =>
|
|
{
|
|
Assert.False(string.IsNullOrWhiteSpace(p.Name));
|
|
Assert.NotNull(p.Value);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteSqlCommandAsync_AgainstLocalHistorian_ReturnsRecordSet()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
// ExeC/GetR ride the storage-session GUID as an uppercase string handle. A constant SELECT
|
|
// returns a single int column; the DataTable is decoded from the NRBF stream (no BinaryFormatter).
|
|
AVEVA.Historian.Client.Models.HistorianSqlResult result =
|
|
await client.ExecuteSqlCommandAsync("SELECT 1 AS ProbeValue", cancellationToken: CancellationToken.None);
|
|
|
|
AVEVA.Historian.Client.Models.HistorianSqlColumn column = Assert.Single(result.Columns);
|
|
Assert.Equal("ProbeValue", column.Name);
|
|
IReadOnlyList<object?> row = Assert.Single(result.Rows);
|
|
Assert.Equal(1, Assert.Single(row));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteSqlCommandAsync_AgainstLocalHistorian_MultiColumnMultiRow()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
// Fully synthetic query (no server data): two columns (int + string), two rows, one NULL —
|
|
// exercises the schema/diffgram parser beyond the single-cell case.
|
|
AVEVA.Historian.Client.Models.HistorianSqlResult result = await client.ExecuteSqlCommandAsync(
|
|
"SELECT 10 AS Num, 'alpha' AS Word UNION ALL SELECT 20, NULL",
|
|
cancellationToken: CancellationToken.None);
|
|
|
|
Assert.Equal(2, result.Columns.Count);
|
|
Assert.Equal("Num", result.Columns[0].Name);
|
|
Assert.Equal("Word", result.Columns[1].Name);
|
|
Assert.Equal(2, result.Rows.Count);
|
|
Assert.Equal(10, result.Rows[0][0]);
|
|
Assert.Equal("alpha", result.Rows[0][1]);
|
|
Assert.Equal(20, result.Rows[1][0]);
|
|
Assert.Null(result.Rows[1][1]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetConnectionStatusAsync_AgainstLocalHistorian_ReportsConnectedToServer()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
AVEVA.Historian.Client.Models.HistorianConnectionStatus status =
|
|
await client.GetConnectionStatusAsync(CancellationToken.None);
|
|
|
|
Assert.True(status.ConnectedToServer);
|
|
Assert.False(status.ErrorOccurred);
|
|
Assert.False(status.Pending);
|
|
Assert.Equal(host, status.ServerName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStoreForwardStatusAsync_AgainstLocalHistorian_ReturnsDefaults()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
AVEVA.Historian.Client.Models.HistorianStoreForwardStatus status =
|
|
await client.GetStoreForwardStatusAsync(CancellationToken.None);
|
|
|
|
// The synthesized status returns defaults — no store-forward sidecar to probe in this build.
|
|
Assert.False(status.ErrorOccurred);
|
|
Assert.False(status.Pending);
|
|
Assert.Equal(host, status.ServerName);
|
|
}
|
|
|
|
// The validator inside HistorianWcfTagClient now allows IntegratedSecurity=false WHEN
|
|
// explicit UserName + Password are provided (NTLM/Kerberos with non-current-user creds).
|
|
// It still rejects the no-credentials-at-all case since there's no way to authenticate
|
|
// against /Hist-Integrated.
|
|
[Fact]
|
|
public async Task GetTagMetadataAsync_NoAuthAndNoCredentials_Throws()
|
|
{
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = "localhost",
|
|
IntegratedSecurity = false,
|
|
UserName = string.Empty,
|
|
Password = string.Empty,
|
|
});
|
|
await Assert.ThrowsAsync<ProtocolEvidenceMissingException>(
|
|
() => client.GetTagMetadataAsync("anytag", CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian()
|
|
{
|
|
// Live verification of the explicit-creds tag-metadata path. Gated on
|
|
// HISTORIAN_USER + HISTORIAN_PASSWORD being set; skips cleanly otherwise. The path
|
|
// routes through WCF Windows transport security with Credentials.Windows.ClientCredential.
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG");
|
|
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
|
|
string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
|
|
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)
|
|
|| string.IsNullOrWhiteSpace(user) || string.IsNullOrWhiteSpace(password)
|
|
|| !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = false,
|
|
UserName = user,
|
|
Password = password,
|
|
});
|
|
|
|
AVEVA.Historian.Client.Models.HistorianTagMetadata? metadata =
|
|
await client.GetTagMetadataAsync(testTag, CancellationToken.None);
|
|
Assert.NotNull(metadata);
|
|
Assert.Equal(testTag, metadata.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetTagMetadataAsync_ReturnsConfiguredTestTagMetadata()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG");
|
|
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_PORT"), out int parsedPort)
|
|
? parsedPort
|
|
: HistorianClientOptions.DefaultPort;
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
Port = port,
|
|
IntegratedSecurity = true,
|
|
UserName = Environment.GetEnvironmentVariable("HISTORIAN_USER") ?? string.Empty,
|
|
Password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD") ?? string.Empty
|
|
});
|
|
|
|
AVEVA.Historian.Client.Models.HistorianTagMetadata? metadata =
|
|
await client.GetTagMetadataAsync(testTag, CancellationToken.None);
|
|
|
|
Assert.NotNull(metadata);
|
|
Assert.Equal(testTag, metadata.Name);
|
|
Assert.NotNull(metadata.Key);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnsureTagAsync_AndDeleteTagAsync_RoundTrip_AgainstLocalHistorian()
|
|
{
|
|
// Per docs/plans/write-commands-reverse-engineering.md safety rules: localhost only,
|
|
// sandbox tag name must start with "RetestSdkWrite", tag is created if missing and
|
|
// always deleted at the end so the test leaves zero residue.
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
string? sandboxTag = Environment.GetEnvironmentVariable("HISTORIAN_WRITE_SANDBOX_TAG");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(sandboxTag) || !sandboxTag.StartsWith("RetestSdkWrite", StringComparison.Ordinal))
|
|
{
|
|
return; // safety gate per the plan
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
AVEVA.Historian.Client.Models.HistorianTagDefinition definition = new()
|
|
{
|
|
TagName = sandboxTag,
|
|
Description = "SDK live integration test sandbox",
|
|
EngineeringUnit = "test",
|
|
DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float,
|
|
MinEU = 0.0,
|
|
MaxEU = 100.0,
|
|
};
|
|
|
|
// Both EnsureTagAsync and DeleteTagAsync now work end-to-end against the live
|
|
// Historian. Open2 must use write-enabled connectionMode 0x401 (not the default
|
|
// 0x402 read-only); the EnsT2 InBuff layout is corrected to native parity (144
|
|
// bytes incl 0x4E leading marker, no trailing 01 01 01 closing markers).
|
|
bool ensured = await client.EnsureTagAsync(definition, CancellationToken.None);
|
|
Assert.True(ensured, "EnsureTagAsync returned false against the live Historian.");
|
|
|
|
bool deleted = await client.DeleteTagAsync(sandboxTag, CancellationToken.None);
|
|
Assert.True(deleted, "DeleteTagAsync returned false against the live Historian.");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddTagExtendedPropertiesAsync_AgainstLocalHistorian_WritesAndReadsBack()
|
|
{
|
|
// Safety: localhost only, sandbox tag must start with "RetestSdkWrite". Creates the tag,
|
|
// adds an extended property, reads it back via R1.5, and deletes the tag. Gated on
|
|
// HISTORIAN_WRITE_SANDBOX_TAG.
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
string? sandboxTag = Environment.GetEnvironmentVariable("HISTORIAN_WRITE_SANDBOX_TAG");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(sandboxTag) || !sandboxTag.StartsWith("RetestSdkWrite", StringComparison.Ordinal))
|
|
{
|
|
return; // safety gate
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
await client.EnsureTagAsync(new AVEVA.Historian.Client.Models.HistorianTagDefinition
|
|
{
|
|
TagName = sandboxTag,
|
|
Description = "SDK ext-property write live test",
|
|
EngineeringUnit = "test",
|
|
DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float,
|
|
MinEU = 0.0,
|
|
MaxEU = 100.0,
|
|
}, CancellationToken.None);
|
|
|
|
try
|
|
{
|
|
const string propName = "SdkLiveTestProp";
|
|
const string propValue = "SdkLiveTestValue";
|
|
|
|
bool added = await client.AddTagExtendedPropertyAsync(sandboxTag, propName, propValue, CancellationToken.None);
|
|
Assert.True(added, "AddTagExtendedPropertyAsync returned false against the live Historian.");
|
|
|
|
// Read back via R1.5 (server may take a moment to surface the new property).
|
|
bool found = false;
|
|
for (int i = 0; i < 10 && !found; i++)
|
|
{
|
|
await Task.Delay(500);
|
|
var props = await client.GetTagExtendedPropertiesAsync(sandboxTag, CancellationToken.None);
|
|
found = props.Any(p =>
|
|
string.Equals(p.Name, propName, StringComparison.OrdinalIgnoreCase) &&
|
|
string.Equals(p.Value, propValue, StringComparison.Ordinal));
|
|
}
|
|
Assert.True(found, $"Extended property '{propName}={propValue}' was not read back after the write.");
|
|
}
|
|
finally
|
|
{
|
|
try { await client.DeleteTagAsync(sandboxTag, CancellationToken.None); } catch { }
|
|
}
|
|
}
|
|
|
|
// Extended-property DELETE (DelTep) has no live integration test: the wire format is captured and
|
|
// the serializer is golden-verified (WcfTagExtendedPropertyWriteProtocolTests), but the SDK
|
|
// cannot yet make the server accept the delete (the native single-connection working-set
|
|
// requirement isn't reproduced by per-service WCF channels). See
|
|
// docs/reverse-engineering/wcf-add-tag-extended-properties.md §Delete.
|
|
|
|
// Round-trip every live-verified analog data type + the non-default-range case. The
|
|
// sandbox tag name is suffixed per case so the runs don't collide. Always cleans up.
|
|
[Theory]
|
|
[InlineData("RetestSdkWriteFloatRT", AVEVA.Historian.Client.Models.HistorianDataType.Float, 0.0, 100.0, 0.0, 100.0)]
|
|
[InlineData("RetestSdkWriteDoubleRT", AVEVA.Historian.Client.Models.HistorianDataType.Double, 0.0, 100.0, 0.0, 100.0)]
|
|
[InlineData("RetestSdkWriteInt2RT", AVEVA.Historian.Client.Models.HistorianDataType.Int2, 0.0, 100.0, 0.0, 100.0)]
|
|
[InlineData("RetestSdkWriteInt4RT", AVEVA.Historian.Client.Models.HistorianDataType.Int4, 0.0, 100.0, 0.0, 100.0)]
|
|
[InlineData("RetestSdkWriteUInt4RT", AVEVA.Historian.Client.Models.HistorianDataType.UInt4, 0.0, 100.0, 0.0, 100.0)]
|
|
[InlineData("RetestSdkWriteFloatRangesRT", AVEVA.Historian.Client.Models.HistorianDataType.Float, -50.0, 200.0, 10.0, 4095.0)]
|
|
public async Task EnsureTagAsync_AndDeleteTagAsync_RoundTrip_PerDataTypeAndRange(
|
|
string sandboxTag,
|
|
AVEVA.Historian.Client.Models.HistorianDataType dataType,
|
|
double minEU, double maxEU, double minRaw, double maxRaw)
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
AVEVA.Historian.Client.Models.HistorianTagDefinition definition = new()
|
|
{
|
|
TagName = sandboxTag,
|
|
Description = $"SDK round-trip {dataType}",
|
|
EngineeringUnit = "test",
|
|
DataType = dataType,
|
|
MinEU = minEU,
|
|
MaxEU = maxEU,
|
|
MinRaw = minRaw,
|
|
MaxRaw = maxRaw,
|
|
};
|
|
|
|
try
|
|
{
|
|
bool ensured = await client.EnsureTagAsync(definition, CancellationToken.None);
|
|
Assert.True(ensured, $"EnsureTagAsync({dataType}) returned false against the live Historian.");
|
|
}
|
|
finally
|
|
{
|
|
// Always clean up — DeleteTagAsync returns true on a freshly-created tag.
|
|
await client.DeleteTagAsync(sandboxTag, CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnsureTagAsync_StorageTypeDelta_PersistsToTagTableAsTwo()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const string sandboxTag = "RetestSdkWriteStorageTypeDeltaRT";
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe,
|
|
});
|
|
|
|
try
|
|
{
|
|
bool ok = await client.EnsureTagAsync(new AVEVA.Historian.Client.Models.HistorianTagDefinition
|
|
{
|
|
TagName = sandboxTag,
|
|
Description = "SDK Delta round-trip",
|
|
EngineeringUnit = "test",
|
|
DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float,
|
|
StorageType = AVEVA.Historian.Client.Models.HistorianStorageType.Delta,
|
|
}, CancellationToken.None);
|
|
Assert.True(ok, "EnsureTagAsync(Delta) returned false");
|
|
|
|
using Microsoft.Data.SqlClient.SqlConnection sql = new("Server=.;Database=Runtime;Integrated Security=SSPI;Encrypt=False;TrustServerCertificate=True");
|
|
sql.Open();
|
|
using Microsoft.Data.SqlClient.SqlCommand cmd = sql.CreateCommand();
|
|
cmd.CommandText = "SELECT StorageType FROM Tag WHERE TagName = @t";
|
|
cmd.Parameters.AddWithValue("@t", sandboxTag);
|
|
object? st = cmd.ExecuteScalar();
|
|
Assert.NotNull(st);
|
|
Assert.Equal((int)AVEVA.Historian.Client.Models.HistorianStorageType.Delta, Convert.ToInt32(st));
|
|
}
|
|
finally
|
|
{
|
|
await client.DeleteTagAsync(sandboxTag, CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnsureTagAsync_NonDefaultStorageRate_PersistsToTagTable()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const string sandboxTag = "RetestSdkWriteStorageRateRT";
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe,
|
|
});
|
|
|
|
try
|
|
{
|
|
bool ok = await client.EnsureTagAsync(new AVEVA.Historian.Client.Models.HistorianTagDefinition
|
|
{
|
|
TagName = sandboxTag,
|
|
Description = "SDK StorageRate round-trip",
|
|
EngineeringUnit = "test",
|
|
DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float,
|
|
// Server only accepts quantized rates — 1000, 5000, 10000, 60000, 300000 ms.
|
|
StorageRateMs = 5000u,
|
|
}, CancellationToken.None);
|
|
Assert.True(ok, "EnsureTagAsync returned false");
|
|
|
|
using Microsoft.Data.SqlClient.SqlConnection sql = new("Server=.;Database=Runtime;Integrated Security=SSPI;Encrypt=False;TrustServerCertificate=True");
|
|
sql.Open();
|
|
using Microsoft.Data.SqlClient.SqlCommand cmd = sql.CreateCommand();
|
|
cmd.CommandText = "SELECT StorageRate FROM Tag WHERE TagName = @t";
|
|
cmd.Parameters.AddWithValue("@t", sandboxTag);
|
|
object? rate = cmd.ExecuteScalar();
|
|
Assert.NotNull(rate);
|
|
Assert.Equal(5000, Convert.ToInt32(rate));
|
|
}
|
|
finally
|
|
{
|
|
await client.DeleteTagAsync(sandboxTag, CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnsureTagAsync_CalledTwiceOnSameTag_UpdatesFieldsInPlace()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const string sandboxTag = "RetestSdkWriteIdempotencyRT";
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe,
|
|
});
|
|
|
|
try
|
|
{
|
|
bool firstOk = await client.EnsureTagAsync(new AVEVA.Historian.Client.Models.HistorianTagDefinition
|
|
{
|
|
TagName = sandboxTag,
|
|
Description = "First version",
|
|
EngineeringUnit = "test",
|
|
DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float,
|
|
MinEU = 0.0, MaxEU = 100.0, MinRaw = 0.0, MaxRaw = 100.0,
|
|
ApplyScaling = false,
|
|
}, CancellationToken.None);
|
|
Assert.True(firstOk, "First EnsureTagAsync returned false");
|
|
(string desc1, double minEU1, double maxEU1, double minRaw1, double maxRaw1, int scaling1) = ReadTagState(sandboxTag);
|
|
Assert.Equal("First version", desc1);
|
|
Assert.Equal(0.0, minEU1);
|
|
Assert.Equal(0, scaling1);
|
|
|
|
bool secondOk = await client.EnsureTagAsync(new AVEVA.Historian.Client.Models.HistorianTagDefinition
|
|
{
|
|
TagName = sandboxTag,
|
|
Description = "Second version",
|
|
EngineeringUnit = "kPa",
|
|
DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float,
|
|
MinEU = -50.0, MaxEU = 200.0, MinRaw = 10.0, MaxRaw = 4095.0,
|
|
ApplyScaling = true,
|
|
}, CancellationToken.None);
|
|
Assert.True(secondOk, "Second EnsureTagAsync returned false");
|
|
(string desc2, double minEU2, double maxEU2, double minRaw2, double maxRaw2, int scaling2) = ReadTagState(sandboxTag);
|
|
|
|
// EnsureTagAsync upserts: second call updates the existing row in place.
|
|
Assert.Equal("Second version", desc2);
|
|
Assert.Equal(-50.0, minEU2);
|
|
Assert.Equal(200.0, maxEU2);
|
|
Assert.Equal(10.0, minRaw2);
|
|
Assert.Equal(4095.0, maxRaw2);
|
|
Assert.Equal(1, scaling2);
|
|
}
|
|
finally
|
|
{
|
|
await client.DeleteTagAsync(sandboxTag, CancellationToken.None);
|
|
}
|
|
|
|
static (string desc, double minEU, double maxEU, double minRaw, double maxRaw, int scaling) ReadTagState(string tagName)
|
|
{
|
|
using Microsoft.Data.SqlClient.SqlConnection sql = new("Server=.;Database=Runtime;Integrated Security=SSPI;Encrypt=False;TrustServerCertificate=True");
|
|
sql.Open();
|
|
using Microsoft.Data.SqlClient.SqlCommand cmd = sql.CreateCommand();
|
|
cmd.CommandText = "SELECT t.[Description], a.MinEU, a.MaxEU, a.MinRaw, a.MaxRaw, a.Scaling FROM Tag t JOIN AnalogTag a ON a.TagName=t.TagName WHERE t.TagName=@t";
|
|
cmd.Parameters.AddWithValue("@t", tagName);
|
|
using Microsoft.Data.SqlClient.SqlDataReader r = cmd.ExecuteReader();
|
|
Assert.True(r.Read(), $"Tag {tagName} not found");
|
|
return (r.GetString(0), r.GetDouble(1), r.GetDouble(2), r.GetDouble(3), r.GetDouble(4), Convert.ToInt32(r.GetValue(5)));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnsureTagAsync_ApplyScalingTrue_PersistsDistinctMinRawAndMaxRaw()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const string sandboxTag = "RetestSdkWriteApplyScalingRT";
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe,
|
|
});
|
|
|
|
AVEVA.Historian.Client.Models.HistorianTagDefinition definition = new()
|
|
{
|
|
TagName = sandboxTag,
|
|
Description = "SDK ApplyScaling round-trip",
|
|
EngineeringUnit = "test",
|
|
DataType = AVEVA.Historian.Client.Models.HistorianDataType.Float,
|
|
MinEU = -50.0,
|
|
MaxEU = 200.0,
|
|
MinRaw = 10.0,
|
|
MaxRaw = 4095.0,
|
|
ApplyScaling = true,
|
|
};
|
|
|
|
try
|
|
{
|
|
bool ensured = await client.EnsureTagAsync(definition, CancellationToken.None);
|
|
Assert.True(ensured, "EnsureTagAsync(ApplyScaling=true) returned false against the live Historian.");
|
|
|
|
// Verify directly against the AnalogTag table — the read-path GetTagMetadataAsync
|
|
// surfaces only one of (MinRaw, MinEU); SQL is the unambiguous source of truth.
|
|
using Microsoft.Data.SqlClient.SqlConnection sql = new("Server=.;Database=Runtime;Integrated Security=SSPI;Encrypt=False;TrustServerCertificate=True");
|
|
sql.Open();
|
|
using Microsoft.Data.SqlClient.SqlCommand cmd = sql.CreateCommand();
|
|
cmd.CommandText = "SELECT MinEU, MaxEU, MinRaw, MaxRaw, Scaling FROM AnalogTag WHERE TagName = @t";
|
|
cmd.Parameters.AddWithValue("@t", sandboxTag);
|
|
using Microsoft.Data.SqlClient.SqlDataReader r = cmd.ExecuteReader();
|
|
Assert.True(r.Read(), $"AnalogTag row for {sandboxTag} not found after EnsureTag.");
|
|
Assert.Equal(-50.0, r.GetDouble(0));
|
|
Assert.Equal(200.0, r.GetDouble(1));
|
|
Assert.Equal(10.0, r.GetDouble(2));
|
|
Assert.Equal(4095.0, r.GetDouble(3));
|
|
Assert.Equal(1, Convert.ToInt32(r.GetValue(4)));
|
|
}
|
|
finally
|
|
{
|
|
await client.DeleteTagAsync(sandboxTag, CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetTagMetadataAsync_PopulatesDescriptionAndEuRangeForAnalogTag()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// SysTimeSec is a built-in analog UInt16 tag with non-empty Description, MaxEU,
|
|
// and an EngineeringUnit. Verifies the parser populates those new fields end-to-end.
|
|
const string analogTag = "SysTimeSec";
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
AVEVA.Historian.Client.Models.HistorianTagMetadata? metadata =
|
|
await client.GetTagMetadataAsync(analogTag, CancellationToken.None);
|
|
|
|
Assert.NotNull(metadata);
|
|
Assert.Equal(analogTag, metadata.Name);
|
|
Assert.False(string.IsNullOrWhiteSpace(metadata.Description));
|
|
Assert.NotNull(metadata.MaxRaw);
|
|
Assert.True(metadata.MaxRaw is > 0 and <= 1e15);
|
|
Assert.False(string.IsNullOrWhiteSpace(metadata.EngineeringUnit));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadEventsAsync_WithFilter_IsHonoredByServer()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
DateTime endUtc = DateTime.UtcNow;
|
|
DateTime startUtc = endUtc - TimeSpan.FromDays(30);
|
|
|
|
// A predicate that matches nothing must return zero events — proving the server applies
|
|
// the filter (not inert), unlike e.g. the analog-summary knobs.
|
|
List<AVEVA.Historian.Client.Models.HistorianEvent> noMatch = [];
|
|
await foreach (var evt in client.ReadEventsAsync(startUtc, endUtc,
|
|
new AVEVA.Historian.Client.Models.HistorianEventFilter("Type",
|
|
AVEVA.Historian.Client.Models.HistorianEventComparison.Equal, "ZZZ_NoSuchEventType"),
|
|
CancellationToken.None))
|
|
{
|
|
noMatch.Add(evt);
|
|
}
|
|
Assert.Empty(noMatch);
|
|
|
|
// A matching predicate returns events, all of the filtered Type.
|
|
List<AVEVA.Historian.Client.Models.HistorianEvent> matched = [];
|
|
await foreach (var evt in client.ReadEventsAsync(startUtc, endUtc,
|
|
new AVEVA.Historian.Client.Models.HistorianEventFilter("Type",
|
|
AVEVA.Historian.Client.Models.HistorianEventComparison.Equal, "User.Write"),
|
|
CancellationToken.None))
|
|
{
|
|
matched.Add(evt);
|
|
}
|
|
|
|
// Requires User.Write events in the window (present on a working Historian). If the store
|
|
// is empty in the window this asserts nothing was wrongly returned; otherwise every row
|
|
// must match the filtered type.
|
|
Assert.All(matched, evt => Assert.Equal("User.Write", evt.Type));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendEventAsync_AgainstLocalHistorian_AcceptedByServer()
|
|
{
|
|
string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST");
|
|
if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows())
|
|
{
|
|
return;
|
|
}
|
|
|
|
HistorianClient client = new(new HistorianClientOptions
|
|
{
|
|
Host = host,
|
|
IntegratedSecurity = true,
|
|
Transport = HistorianTransport.LocalPipe
|
|
});
|
|
|
|
Guid eventId = Guid.NewGuid();
|
|
DateTime eventTime = DateTime.UtcNow;
|
|
AVEVA.Historian.Client.Models.HistorianEvent evt = new(
|
|
Id: eventId,
|
|
EventTimeUtc: eventTime,
|
|
ReceivedTimeUtc: eventTime,
|
|
Type: "User.Write",
|
|
SourceName: string.Empty,
|
|
Namespace: "RetestSdkEventSend",
|
|
RevisionVersion: 0,
|
|
Properties: new Dictionary<string, object?>
|
|
{
|
|
["Source"] = "RetestSdkEventSend",
|
|
["TestMarker"] = "histsdk-R2.5-roundtrip",
|
|
});
|
|
|
|
// The full managed event-send chain (Open2 event-mode 0x501 → CM_EVENT RTag2/EnsT2 →
|
|
// AddS2) reaches the server and the server accepts the AddS2 delivery. NOTE: whether the
|
|
// event is then persisted to the queryable store depends on the historian's event
|
|
// ingestion pipeline being active — on this dev box new events are accepted but not
|
|
// persisted (the native client behaves identically), so this asserts acceptance, which
|
|
// is the SDK-level signal. Round-trip read-back is best-effort below.
|
|
bool accepted = await client.SendEventAsync(evt, CancellationToken.None);
|
|
Assert.True(accepted);
|
|
|
|
// Best-effort round-trip: if the event store persisted it, it should be readable in a
|
|
// tight time window. Not asserted hard because event persistence is environment-gated.
|
|
try
|
|
{
|
|
using Microsoft.Data.SqlClient.SqlConnection sql = new("Server=.;Database=Runtime;Integrated Security=SSPI;Encrypt=False;TrustServerCertificate=True");
|
|
await sql.OpenAsync();
|
|
using Microsoft.Data.SqlClient.SqlCommand cmd = sql.CreateCommand();
|
|
cmd.CommandText =
|
|
"SELECT COUNT(*) FROM v_AlarmEventHistory2 " +
|
|
"WHERE EventStampUTC BETWEEN @s AND @e AND Type = @t";
|
|
cmd.Parameters.AddWithValue("@s", eventTime.AddMinutes(-2));
|
|
cmd.Parameters.AddWithValue("@e", eventTime.AddMinutes(2));
|
|
cmd.Parameters.AddWithValue("@t", "User.Write");
|
|
int count = Convert.ToInt32(await cmd.ExecuteScalarAsync());
|
|
// If persistence is active the event is present; if not, count is 0 (env limitation).
|
|
Assert.True(count >= 0);
|
|
}
|
|
catch
|
|
{
|
|
// SQL read-back is diagnostic only; never fail the send test on a query issue.
|
|
}
|
|
}
|
|
}
|