287 lines
10 KiB
C#
287 lines
10 KiB
C#
using Robotico.Nexus.Client.Tests.TestSupport;
|
|
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 AllOperations_Fail_AfterDispose()
|
|
{
|
|
_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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|