mishima-nexus-client-csharp/tests/Robotico.Nexus.Client.Tests/Time/NexusTimeSessionTests.cs

322 lines
12 KiB
C#
Raw Permalink Normal View History

using Robotico.Nexus.Client.Tests.TestSupport;
using Robotico.Nexus.Client.Time;
using Robotico.Result.Errors;
using Robotico.Time;
using Xunit;
namespace Robotico.Nexus.Client.Tests.Time;
public class NexusTimeSessionTests : IAsyncLifetime
{
private static readonly IReadOnlyDictionary<string, string> NoLabels = new Dictionary<string, string>();
private NexusTestHost _host = null!;
private NexusClient _client = null!;
private ITimeSession _session = null!;
public async Task InitializeAsync()
{
_host = await NexusTestHost.StartAsync();
Robotico.Result.Result<NexusClient> connected = NexusClient.Connect(_host.ConnectionUri(NexusUriSchemes.Time));
Assert.True(connected.IsSuccess(out NexusClient? client, out _));
_client = client!;
Robotico.Result.Result<ITimeSession> opened = _client.Time.Open("session-time-db");
Assert.True(opened.IsSuccess(out ITimeSession? session, out _));
_session = session!;
}
public async Task DisposeAsync()
{
_session.Dispose();
_client.Dispose();
await _host.DisposeAsync();
}
[Fact]
public void Doctor_ReturnsSyntheticIdentity_AndCounts()
{
Robotico.Result.Result<TimeDoctorReport> result = _session.Doctor();
Assert.True(result.IsSuccess(out TimeDoctorReport? report, out _));
Assert.StartsWith("nexus://", report!.PrimaryPath, StringComparison.Ordinal);
Assert.Equal(report.PrimaryPath, report.WalPath);
Assert.Equal(0, report.MaxActiveSeries);
}
[Fact]
public void RegisterSeries_Succeeds_AndReturnsId()
{
Robotico.Result.Result<ulong> result = _session.RegisterSeries(new TimeSeriesKey("cpu.load", NoLabels));
Assert.True(result.IsSuccess(out ulong seriesId, out _));
Assert.True(seriesId > 0);
}
[Fact]
public void RegisterSeries_Fails_WhenMetricNameBlank()
{
Robotico.Result.Result<ulong> result = _session.RegisterSeries(new TimeSeriesKey(" ", NoLabels));
Assert.True(result.IsError(out IError? error));
Assert.Equal(TimeErrors.InvalidArgument, error!.Code);
}
[Fact]
public void AppendSample_ThenQueryRange_RoundTrips()
{
ulong seriesId = RegisterSeries("cpu.load");
Robotico.Result.Result appended = _session.AppendSample(seriesId, 1000, 42.5);
Assert.True(appended.IsSuccess());
Robotico.Result.Result<IReadOnlyList<TimeSample>> queried = _session.QueryRange(seriesId, 0, 2000, 10);
Assert.True(queried.IsSuccess(out IReadOnlyList<TimeSample>? samples, out _));
Assert.Single(samples!);
Assert.Equal(42.5, samples![0].Value);
}
[Fact]
public void AppendSample_Fails_WhenValueNotFinite()
{
ulong seriesId = RegisterSeries("cpu.load");
Robotico.Result.Result result = _session.AppendSample(seriesId, 1000, double.NaN);
Assert.True(result.IsError(out IError? error));
Assert.Equal(TimeErrors.InvalidArgument, error!.Code);
}
[Fact]
public void AppendSampleBatch_Succeeds()
{
ulong seriesId = RegisterSeries("mem.used");
TimeSample[] samples = [new TimeSample(1000, 1.0), new TimeSample(2000, 2.0)];
Robotico.Result.Result result = _session.AppendSampleBatch(seriesId, samples);
Assert.True(result.IsSuccess());
Robotico.Result.Result<IReadOnlyList<TimeSample>> queried = _session.QueryRange(seriesId, 0, 3000, 10);
Assert.True(queried.IsSuccess(out IReadOnlyList<TimeSample>? results, out _));
Assert.Equal(2, results!.Count);
}
[Fact]
public void AppendSampleBatch_Fails_WhenEmpty()
{
ulong seriesId = RegisterSeries("mem.used");
Robotico.Result.Result result = _session.AppendSampleBatch(seriesId, []);
Assert.True(result.IsError(out IError? error));
Assert.Equal(TimeErrors.InvalidArgument, error!.Code);
}
[Fact]
public void AppendSampleBatch_Fails_WhenAnyValueNotFinite()
{
ulong seriesId = RegisterSeries("mem.used");
Robotico.Result.Result result = _session.AppendSampleBatch(seriesId, [new TimeSample(1000, double.PositiveInfinity)]);
Assert.True(result.IsError(out IError? error));
Assert.Equal(TimeErrors.InvalidArgument, error!.Code);
}
[Fact]
public void Last_ReturnsNull_WhenNoSamples()
{
ulong seriesId = RegisterSeries("disk.free");
Robotico.Result.Result<TimeSample?> result = _session.Last(seriesId);
Assert.True(result.IsSuccess(out TimeSample? sample, out _));
Assert.Null(sample);
}
[Fact]
public void Last_ReturnsMostRecentSample()
{
ulong seriesId = RegisterSeries("disk.free");
_session.AppendSample(seriesId, 1000, 1.0);
_session.AppendSample(seriesId, 2000, 2.0);
Robotico.Result.Result<TimeSample?> result = _session.Last(seriesId);
Assert.True(result.IsSuccess(out TimeSample? sample, out _));
Assert.Equal(2.0, sample!.Value.Value);
}
[Fact]
public void RegisterEventTable_Succeeds()
{
TimeEventTableSchema schema = new("requests", [new TimeEventField("path", "Utf8", TimeEventFieldIndexKind.Inverted)]);
Robotico.Result.Result<ulong> result = _session.RegisterEventTable(schema);
Assert.True(result.IsSuccess(out ulong tableId, out _));
Assert.True(tableId > 0);
}
[Fact]
public void RegisterEventTable_Fails_WhenNoFields()
{
TimeEventTableSchema schema = new("requests", []);
Robotico.Result.Result<ulong> result = _session.RegisterEventTable(schema);
Assert.True(result.IsError(out IError? error));
Assert.Equal(TimeErrors.InvalidArgument, error!.Code);
}
[Fact]
public void RegisterEventTable_Fails_WhenTableNameBlank()
{
TimeEventTableSchema schema = new(string.Empty, [new TimeEventField("path", "Utf8", TimeEventFieldIndexKind.None)]);
Robotico.Result.Result<ulong> result = _session.RegisterEventTable(schema);
Assert.True(result.IsError(out IError? error));
Assert.Equal(TimeErrors.InvalidArgument, error!.Code);
}
[Fact]
public void AppendEvent_ThenQueryEvents_RoundTrips()
{
ulong tableId = RegisterEventTable();
Robotico.Result.Result<ulong> appended = _session.AppendEvent(tableId, 1000, [new TimeCell("Utf8", Utf8: "/api/foo")]);
Assert.True(appended.IsSuccess());
Robotico.Result.Result<IReadOnlyList<TimeWideEvent>> queried = _session.QueryEvents(tableId, 0, 2000, 10);
Assert.True(queried.IsSuccess(out IReadOnlyList<TimeWideEvent>? events, out _));
Assert.Single(events!);
Assert.Equal("/api/foo", events![0].Cells[0].Utf8);
}
[Fact]
public void AppendEvent_Fails_WhenCellsEmpty()
{
ulong tableId = RegisterEventTable();
Robotico.Result.Result<ulong> result = _session.AppendEvent(tableId, 1000, []);
Assert.True(result.IsError(out IError? error));
Assert.Equal(TimeErrors.InvalidArgument, error!.Code);
}
[Fact]
public void LookupInverted_FindsExactMatch()
{
ulong tableId = RegisterEventTable();
_session.AppendEvent(tableId, 1000, [new TimeCell("Utf8", Utf8: "/api/foo")]);
_session.AppendEvent(tableId, 2000, [new TimeCell("Utf8", Utf8: "/api/bar")]);
Robotico.Result.Result<IReadOnlyList<TimeWideEvent>> result = _session.LookupInverted(tableId, 0, "/api/foo", 10);
Assert.True(result.IsSuccess(out IReadOnlyList<TimeWideEvent>? events, out _));
Assert.Single(events!);
}
[Fact]
public void SearchFullText_FindsSubstringMatch()
{
ulong tableId = RegisterEventTable();
_session.AppendEvent(tableId, 1000, [new TimeCell("Utf8", Utf8: "hello world")]);
Robotico.Result.Result<IReadOnlyList<TimeSearchHit>> result = _session.SearchFullText(tableId, 0, "world", 10);
Assert.True(result.IsSuccess(out IReadOnlyList<TimeSearchHit>? hits, out _));
Assert.Single(hits!);
}
[Fact]
public void ListSeries_GetCardinality_ApplyRetention_CatalogOps()
{
NexusTimeSession nexus = Assert.IsType<NexusTimeSession>(_session);
ulong s1 = RegisterSeries("cpu.load");
_session.AppendSample(s1, 100, 1.0);
_session.AppendSample(s1, 200, 2.0);
ulong s2 = RegisterSeries("cpu.load");
RegisterSeries("mem.used");
Assert.True(nexus.ListSeries(10).IsSuccess(out IReadOnlyList<TimeSeriesCatalogEntry>? listed, out _));
Assert.Equal(3, listed!.Count);
Assert.Contains(listed, e => e is { SeriesId: var id, MetricName: "cpu.load", SampleCount: 2 } && id == s1);
Assert.True(nexus.GetCardinality(10).IsSuccess(out TimeCardinalitySnapshot? card, out _));
Assert.Equal(3, card!.ActiveSeriesCount);
Assert.Equal(2, card.TotalSampleCount);
Assert.Contains(card.TopMetricsBySeriesCount, m => m is { MetricName: "cpu.load", SeriesCount: 2 });
Assert.True(nexus.ApplyRetention(150).IsSuccess(out long removed, out _));
Assert.True(removed >= 1);
Assert.True(nexus.ApplyRetention(0).IsError(out IError? retErr));
Assert.Equal(TimeErrors.InvalidArgument, retErr!.Code);
}
[Fact]
public void AllOperations_Fail_AfterDispose()
{
NexusTimeSession nexus = Assert.IsType<NexusTimeSession>(_session);
_session.Dispose();
Assert.True(_session.Doctor().IsError(out IError? e1));
Assert.Equal(NexusErrors.Disposed, e1!.Code);
Assert.True(_session.RegisterSeries(new TimeSeriesKey("x", NoLabels)).IsError(out IError? e2));
Assert.Equal(NexusErrors.Disposed, e2!.Code);
Assert.True(_session.AppendSample(1, 1, 1.0).IsError(out IError? e3));
Assert.Equal(NexusErrors.Disposed, e3!.Code);
Assert.True(_session.AppendSampleBatch(1, [new TimeSample(1, 1.0)]).IsError(out IError? e4));
Assert.Equal(NexusErrors.Disposed, e4!.Code);
Assert.True(_session.QueryRange(1, 0, 10, 10).IsError(out IError? e5));
Assert.Equal(NexusErrors.Disposed, e5!.Code);
Assert.True(_session.Last(1).IsError(out IError? e6));
Assert.Equal(NexusErrors.Disposed, e6!.Code);
Assert.True(_session.RegisterEventTable(new TimeEventTableSchema("t", [new TimeEventField("f", "Utf8", TimeEventFieldIndexKind.None)])).IsError(out IError? e7));
Assert.Equal(NexusErrors.Disposed, e7!.Code);
Assert.True(_session.AppendEvent(1, 1, [new TimeCell("Utf8", Utf8: "x")]).IsError(out IError? e8));
Assert.Equal(NexusErrors.Disposed, e8!.Code);
Assert.True(_session.QueryEvents(1, 0, 10, 10).IsError(out IError? e9));
Assert.Equal(NexusErrors.Disposed, e9!.Code);
Assert.True(_session.LookupInverted(1, 0, "x", 10).IsError(out IError? e10));
Assert.Equal(NexusErrors.Disposed, e10!.Code);
Assert.True(_session.SearchFullText(1, 0, "x", 10).IsError(out IError? e11));
Assert.Equal(NexusErrors.Disposed, e11!.Code);
Assert.True(nexus.ListSeries().IsError(out IError? e12));
Assert.Equal(NexusErrors.Disposed, e12!.Code);
Assert.True(nexus.GetCardinality().IsError(out IError? e13));
Assert.Equal(NexusErrors.Disposed, e13!.Code);
Assert.True(nexus.ApplyRetention(1).IsError(out IError? e14));
Assert.Equal(NexusErrors.Disposed, e14!.Code);
}
private ulong RegisterSeries(string metricName)
{
Robotico.Result.Result<ulong> result = _session.RegisterSeries(new TimeSeriesKey(metricName, NoLabels));
Assert.True(result.IsSuccess(out ulong id, out _));
return id;
}
private ulong RegisterEventTable()
{
TimeEventTableSchema schema = new("events-" + Guid.NewGuid(), [new TimeEventField("body", "Utf8", TimeEventFieldIndexKind.FullText)]);
Robotico.Result.Result<ulong> result = _session.RegisterEventTable(schema);
Assert.True(result.IsSuccess(out ulong id, out _));
return id;
}
}