// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.DashboardService.Proto.V1;
using Aspire.Dashboard.Tests.Shared;
using Aspire.Shared;
using Google.Protobuf.Collections;
using Google.Protobuf.WellKnownTypes;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using OpenTelemetry.Proto.Logs.V1;
using Xunit;
using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers;
namespace Aspire.Dashboard.Tests.Model;
public sealed class DashboardDataSourceTests(ITestOutputHelper testOutputHelper)
{
[Fact]
public void RunDirectory_IsNestedUnderApplicationDirectoryAndRuns()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace, "My Dashboard");
using var runStore = CreateRunStore(options);
var applicationDirectoryName = DashboardRunStore.GetApplicationDirectoryName("My Dashboard");
var expectedRunsDirectory = Path.Combine(workspace.Path, applicationDirectoryName, "runs");
Assert.Equal(expectedRunsDirectory, Directory.GetParent(runStore.RunDirectory)!.FullName);
}
[Fact]
public void ApplicationDirectory_WithoutDataDirectory_UsesDashboardDirectoryInAspireHome()
{
var applicationDirectoryName = DashboardRunStore.GetApplicationDirectoryName("My Dashboard");
var expectedDirectory = Path.Combine(
AspireHomeDirectory.GetDefault(),
"dashboard",
applicationDirectoryName);
Assert.Equal(expectedDirectory, DashboardRunStore.GetApplicationDirectory(dataRoot: null, "My Dashboard"));
}
[Theory]
[InlineData(DashboardPersistenceMode.Run)]
[InlineData(DashboardPersistenceMode.Resume)]
public void PersistentApplicationDirectory_HasOwnerOnlyPermissionsOnUnix(DashboardPersistenceMode persistenceMode)
{
if (OperatingSystem.IsWindows())
{
return;
}
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var applicationDirectory = DashboardRunStore.GetApplicationDirectory(workspace.Path, "My Dashboard");
Directory.CreateDirectory(applicationDirectory);
File.SetUnixFileMode(
applicationDirectory,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
using var runStore = CreateRunStore(CreateOptions(workspace, "My Dashboard", persistenceMode));
Assert.Equal(
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute,
File.GetUnixFileMode(applicationDirectory));
}
[Fact]
public void RunId_IsUtcTimestampWithMillisecondPrecision()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 7, 20, 12, 34, 56, 789, TimeSpan.Zero));
using var runStore = CreateRunStore(CreateOptions(workspace), timeProvider);
Assert.Equal("20260720T123456789Z", runStore.RunId);
Assert.Equal(runStore.RunId, Path.GetFileName(runStore.RunDirectory));
}
[Fact]
public void RunMode_RejectsConcurrentTimestampCollision()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 7, 20, 12, 34, 56, 789, TimeSpan.Zero));
var options = CreateOptions(workspace);
using var firstRunStore = CreateRunStore(options, timeProvider);
var exception = Assert.Throws<InvalidOperationException>(() => CreateRunStore(options, timeProvider));
Assert.Equal($"Dashboard run '{firstRunStore.RunId}' is already in use by another dashboard process.", exception.Message);
}
[Fact]
public async Task RunMetadata_IsPublishedAfterApplicationStarted()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
using var runStore = CreateRunStore(options);
var metadataPath = Path.Combine(runStore.RunDirectory, "run.json");
using var lifetime = new TestHostApplicationLifetime();
Assert.False(File.Exists(metadataPath));
using var dataSourcePool = new DashboardDataSourcePool(runStore, CreateRepositoryFactory(options));
using var initializer = new DashboardDataSourceInitializer(
dataSourcePool,
lifetime,
NullLogger<DashboardDataSourceInitializer>.Instance);
await initializer.StartAsync(CancellationToken.None);
Assert.False(File.Exists(metadataPath));
lifetime.NotifyStarted();
using var metadata = JsonDocument.Parse(File.ReadAllText(metadataPath));
Assert.Equal(DashboardRunStore.SchemaVersion, metadata.RootElement.GetProperty("SchemaVersion").GetInt32());
Assert.Equal(DashboardRunStore.SchemaVersion, Assert.Single(runStore.GetRuns()).SchemaVersion);
}
[Fact]
public async Task CurrentRun_PinPersistsWhenRunBecomesHistorical()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
var startedAt = new DateTimeOffset(2026, 7, 29, 12, 0, 0, TimeSpan.Zero);
string pinnedRunId;
using (var currentRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt)))
{
await InitializeAndPublishRunAsync(currentRunStore);
var currentRun = Assert.Single(currentRunStore.GetRuns());
Assert.False(currentRun.IsPinned);
currentRunStore.SetRunPinned(currentRun, isPinned: true);
Assert.True(currentRun.IsPinned);
using var metadata = JsonDocument.Parse(File.ReadAllText(Path.Combine(currentRunStore.RunDirectory, "run.json")));
Assert.True(metadata.RootElement.GetProperty("IsPinned").GetBoolean());
pinnedRunId = currentRun.RunId;
}
using var nextRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt.AddMinutes(1)));
var historicalRun = nextRunStore.GetRuns().Single(run => string.Equals(run.RunId, pinnedRunId, StringComparison.Ordinal));
Assert.False(historicalRun.IsCurrent);
Assert.True(historicalRun.IsPinned);
}
[Fact]
public async Task RunMetadata_SchemaInitializationFailure_DoesNotPublishRun()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
using var runStore = CreateRunStore(options);
using (var connection = new SqliteConnection($"Data Source={runStore.DatabasePath};Pooling=False"))
{
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "CREATE TABLE dashboard_schema (version INTEGER NOT NULL); INSERT INTO dashboard_schema VALUES (1);";
command.ExecuteNonQuery();
}
using var dataSourcePool = new DashboardDataSourcePool(runStore, CreateRepositoryFactory(options));
await Assert.ThrowsAsync<InvalidOperationException>(() => dataSourcePool.InitializeAsync(CancellationToken.None));
Assert.False(File.Exists(Path.Combine(runStore.RunDirectory, "run.json")));
}
[Fact]
public void ConstructionAndGetRuns_LogResolvedStorageAndDiscoveredRuns()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var testSink = new TestSink();
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(LogLevel.Debug);
builder.AddProvider(new TestLoggerProvider(testSink));
});
using var runStore = new DashboardRunStore(
CreateOptions(workspace),
loggerFactory.CreateLogger<DashboardRunStore>(),
TimeProvider.System);
Assert.Single(runStore.GetRuns());
Assert.Collection(
testSink.Writes,
initializationLog =>
{
Assert.Equal(LogLevel.Debug, initializationLog.LogLevel);
Assert.Equal(
$"Dashboard run store initialized with persistence mode 'Run'. Run directory: '{runStore.RunDirectory}'. Database path: '{runStore.DatabasePath}'.",
initializationLog.Message);
},
discoveryLog =>
{
Assert.Equal(LogLevel.Debug, discoveryLog.LogLevel);
Assert.Equal(
$"Dashboard run discovery completed in directory '{Directory.GetParent(runStore.RunDirectory)!.FullName}'. Run count: 1. Run IDs: {runStore.RunId}.",
discoveryLog.Message);
});
}
[Fact]
public async Task NoneMode_UsesTemporaryDatabaseAndDeletesItOnDispose()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace, persistenceMode: DashboardPersistenceMode.None);
string runDirectory;
string databasePath;
using (var runStore = CreateRunStore(options))
{
runDirectory = runStore.RunDirectory;
databasePath = runStore.DatabasePath;
using var database = new DashboardSqliteDatabase(databasePath, pooling: false);
await database.InitializeSchemaAsync(cancellationToken: CancellationToken.None);
Assert.False(runStore.SupportsRunSelection);
Assert.False(runDirectory.StartsWith(workspace.Path, StringComparison.OrdinalIgnoreCase));
Assert.Collection(runStore.GetRuns(), run => Assert.True(run.IsCurrent));
Assert.True(File.Exists(databasePath));
}
Assert.False(Directory.Exists(runDirectory));
Assert.False(File.Exists(databasePath));
}
[Theory]
[InlineData(DashboardPersistenceMode.None)]
[InlineData(DashboardPersistenceMode.Run)]
[InlineData(DashboardPersistenceMode.Resume)]
public async Task ServiceProviderDisposal_ReleasesDatabaseAndDeletesUnpublishedDirectory(DashboardPersistenceMode persistenceMode)
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace, $"Dispose-{Guid.NewGuid():N}", persistenceMode);
var services = new ServiceCollection()
.AddSingleton(options)
.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance)
.AddSingleton<ILogger<DashboardRunStore>>(NullLogger<DashboardRunStore>.Instance)
.AddSingleton(TimeProvider.System)
.AddSingleton<PauseManager>()
.AddSingleton<IKnownPropertyLookup, MockKnownPropertyLookup>()
.AddSingleton<DashboardRunStore>()
.AddSingleton<IDashboardRunStore>(serviceProvider => serviceProvider.GetRequiredService<DashboardRunStore>())
.AddSingleton<IRepositoryFactory, RepositoryFactory>()
.AddSingleton<DashboardDataSourcePool>();
string? runDirectory = null;
try
{
using (var serviceProvider = services.BuildServiceProvider())
{
var runStore = serviceProvider.GetRequiredService<DashboardRunStore>();
// Starting the pool creates its current lease after the run store, so DI disposes the pool first.
// Schema initialization leaves a physical connection in the SQLite provider pool to be cleared.
await serviceProvider.GetRequiredService<DashboardDataSourcePool>().InitializeAsync(CancellationToken.None);
runDirectory = runStore.RunDirectory;
}
// None mode owns its temporary directory, and an unpublished Run directory represents a failed startup.
// Resume mode retains its storage. Deleting it here also verifies that SQLite released the database.
Assert.Equal(persistenceMode == DashboardPersistenceMode.Resume, Directory.Exists(runDirectory));
if (Directory.Exists(runDirectory))
{
Directory.Delete(runDirectory, recursive: true);
}
Assert.False(Directory.Exists(runDirectory));
}
finally
{
if (runDirectory is not null && Directory.Exists(runDirectory))
{
Directory.Delete(runDirectory, recursive: true);
}
}
}
[Fact]
public void NoneMode_DeletesAbandonedTemporaryDirectories()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var abandonedDirectory = Directory.CreateTempSubdirectory("aspire-dashboard-").FullName;
File.WriteAllText(Path.Combine(abandonedDirectory, "dashboard.db"), string.Empty);
try
{
using var runStore = CreateRunStore(CreateOptions(workspace, persistenceMode: DashboardPersistenceMode.None));
Assert.False(Directory.Exists(abandonedDirectory));
}
finally
{
if (Directory.Exists(abandonedDirectory))
{
Directory.Delete(abandonedDirectory, recursive: true);
}
}
}
[Fact]
public async Task NoneMode_DoesNotDeleteActiveTemporaryDirectories()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace, persistenceMode: DashboardPersistenceMode.None);
using var activeRunStore = CreateRunStore(options);
using var database = new DashboardSqliteDatabase(activeRunStore.DatabasePath, pooling: false);
await database.InitializeSchemaAsync(cancellationToken: CancellationToken.None);
using var secondRunStore = CreateRunStore(options);
Assert.True(Directory.Exists(activeRunStore.RunDirectory));
Assert.True(Directory.Exists(secondRunStore.RunDirectory));
}
[Fact]
public void NoneMode_DoesNotDeleteTemporaryDirectoriesWithOtherNames()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var otherDirectory = Directory.CreateTempSubdirectory("unrelated-").FullName;
File.WriteAllText(Path.Combine(otherDirectory, "dashboard.db"), string.Empty);
try
{
using var runStore = CreateRunStore(CreateOptions(workspace, persistenceMode: DashboardPersistenceMode.None));
Assert.True(Directory.Exists(otherDirectory));
}
finally
{
if (Directory.Exists(otherDirectory))
{
Directory.Delete(otherDirectory, recursive: true);
}
}
}
[Fact]
public void ResumeMode_LogsCreatingDatabase()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var testSink = new TestSink();
var logger = new TestLogger<DashboardRunStore>(new TestLoggerFactory(testSink, enabled: true));
using var runStore = new DashboardRunStore(
CreateOptions(workspace, $"Create-{Guid.NewGuid():N}", DashboardPersistenceMode.Resume),
logger,
TimeProvider.System);
var creationLog = Assert.Single(
testSink.Writes,
write => write.Message == $"Creating dashboard database at '{runStore.DatabasePath}'.");
Assert.Equal(LogLevel.Debug, creationLog.LogLevel);
}
[Fact]
public async Task ResumeMode_ReusesApplicationDatabaseWithoutRunSelection()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace, "My Dashboard", DashboardPersistenceMode.Resume);
string firstDatabasePath;
using (var firstRunStore = CreateRunStore(options))
{
firstDatabasePath = firstRunStore.DatabasePath;
using var database = new DashboardSqliteDatabase(firstDatabasePath);
await database.InitializeSchemaAsync(cancellationToken: CancellationToken.None);
}
var testSink = new TestSink();
var logger = new TestLogger<DashboardRunStore>(new TestLoggerFactory(testSink, enabled: true));
using var secondRunStore = new DashboardRunStore(options, logger, TimeProvider.System);
Assert.Equal(firstDatabasePath, secondRunStore.DatabasePath);
Assert.False(secondRunStore.SupportsRunSelection);
Assert.Collection(secondRunStore.GetRuns(), run => Assert.True(run.IsCurrent));
Assert.True(DashboardSqliteDatabase.IsCompatible(secondRunStore.DatabasePath));
var resumeLog = Assert.Single(
testSink.Writes,
write => write.Message == $"Resuming dashboard database at '{secondRunStore.DatabasePath}'.");
Assert.Equal(LogLevel.Debug, resumeLog.LogLevel);
}
[Fact]
public void ResumeMode_RejectsConcurrentDashboardForApplication()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace, "My Dashboard", DashboardPersistenceMode.Resume);
using var firstRunStore = CreateRunStore(options);
var exception = Assert.Throws<InvalidOperationException>(() => CreateRunStore(options));
Assert.Equal(
$"Dashboard data for application 'My Dashboard' is already in use by another dashboard process. Database path: '{firstRunStore.DatabasePath}'.",
exception.Message);
}
[Fact]
public async Task ResumeMode_DeletesIncompatibleDatabase()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace, persistenceMode: DashboardPersistenceMode.Resume);
string databasePath;
using (var firstRunStore = CreateRunStore(options))
{
databasePath = firstRunStore.DatabasePath;
using var connection = new SqliteConnection($"Data Source={databasePath};Pooling=False");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "CREATE TABLE dashboard_schema (version INTEGER NOT NULL); INSERT INTO dashboard_schema VALUES (1);";
command.ExecuteNonQuery();
}
var testSink = new TestSink();
var logger = new TestLogger<DashboardRunStore>(new TestLoggerFactory(testSink, enabled: true));
using var secondRunStore = new DashboardRunStore(options, logger, TimeProvider.System);
Assert.Equal(databasePath, secondRunStore.DatabasePath);
Assert.False(File.Exists(databasePath));
var incompatibleLog = Assert.Single(
testSink.Writes,
write => write.Message == $"Existing dashboard database at '{databasePath}' is incompatible with schema version {DashboardRunStore.SchemaVersion} and will be replaced.");
Assert.Equal(LogLevel.Information, incompatibleLog.LogLevel);
using var database = new DashboardSqliteDatabase(databasePath);
await database.InitializeSchemaAsync(cancellationToken: CancellationToken.None);
Assert.True(DashboardSqliteDatabase.IsCompatible(databasePath));
}
[Fact]
public void ResumeMode_PreservesDatabaseWhenCompatibilityProbeFails()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace, persistenceMode: DashboardPersistenceMode.Resume);
string databasePath;
using (var firstRunStore = CreateRunStore(options))
{
databasePath = firstRunStore.DatabasePath;
}
var databaseContents = "not a SQLite database"u8.ToArray();
var walContents = "existing WAL data"u8.ToArray();
var sharedMemoryContents = "existing shared-memory data"u8.ToArray();
File.WriteAllBytes(databasePath, databaseContents);
File.WriteAllBytes($"{databasePath}-wal", walContents);
File.WriteAllBytes($"{databasePath}-shm", sharedMemoryContents);
Assert.Throws<SqliteException>(() => CreateRunStore(options));
Assert.Equal(databaseContents, File.ReadAllBytes(databasePath));
Assert.True(File.Exists($"{databasePath}-wal"));
Assert.True(File.Exists($"{databasePath}-shm"));
// A second probe reaches SQLite instead of failing because the first constructor leaked the run lock.
Assert.Throws<SqliteException>(() => CreateRunStore(options));
}
[Fact]
public void IsCompatible_ReturnsFalseForMultipleSchemaVersions()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var databasePath = Path.Combine(workspace.Path, $"malformed-{Guid.NewGuid():N}.db");
using (var connection = new SqliteConnection($"Data Source={databasePath};Pooling=False"))
{
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "CREATE TABLE dashboard_schema (version INTEGER NOT NULL) STRICT; INSERT INTO dashboard_schema VALUES (8), (8);";
command.ExecuteNonQuery();
}
Assert.False(DashboardSqliteDatabase.IsCompatible(databasePath));
}
[Fact]
public void ApplicationDirectoryName_IsSafeBoundedAndUnique()
{
var firstName = new string('a', 300) + "/dashboard";
var secondName = new string('a', 300) + ":dashboard";
var firstDirectoryName = DashboardRunStore.GetApplicationDirectoryName(firstName);
var secondDirectoryName = DashboardRunStore.GetApplicationDirectoryName(secondName);
Assert.Equal(DashboardRunStore.MaxApplicationDirectoryNameLength, firstDirectoryName.Length);
Assert.Equal(DashboardRunStore.MaxApplicationDirectoryNameLength, secondDirectoryName.Length);
Assert.Matches("^[A-Za-z0-9_-]+-[0-9a-f]{16}$", firstDirectoryName);
Assert.Matches("^[A-Za-z0-9_-]+-[0-9a-f]{16}$", secondDirectoryName);
Assert.NotEqual(firstDirectoryName, secondDirectoryName);
Assert.Equal(firstDirectoryName, DashboardRunStore.GetApplicationDirectoryName(firstName));
}
[Fact]
public async Task GetRuns_ReturnsCurrentThenCompletedHistoricalRun()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
string historicalRunId;
using (var historicalRunStore = CreateRunStore(options))
{
historicalRunId = historicalRunStore.RunId;
using var telemetryContext = await CreateTelemetryRepositoryAsync(historicalRunStore.DatabasePath, options);
historicalRunStore.PublishRun();
}
using var currentRunStore = CreateRunStore(options);
using var currentTelemetryContext = await CreateTelemetryRepositoryAsync(currentRunStore.DatabasePath, options);
Assert.Collection(
currentRunStore.GetRuns(),
currentRun =>
{
Assert.True(currentRun.IsCurrent);
Assert.Equal(currentRunStore.RunId, currentRun.RunId);
},
historicalRun =>
{
Assert.False(historicalRun.IsCurrent);
Assert.True(historicalRun.CleanShutdown);
Assert.NotNull(historicalRun.EndedAtUtc);
Assert.Equal(historicalRunId, historicalRun.RunId);
});
}
[Fact]
public void GetRuns_ReusesLazySnapshot()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
using var runStore = CreateRunStore(CreateOptions(workspace));
var first = runStore.GetRuns();
var second = runStore.GetRuns();
Assert.Same(first, second);
}
[Fact]
public void GetCurrentRunAndGetRunById_ReturnSnapshotDescriptors()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
using var runStore = CreateRunStore(CreateOptions(workspace));
var currentRun = runStore.GetCurrentRun();
Assert.True(currentRun.IsCurrent);
Assert.Same(currentRun, runStore.GetRunById(currentRun.RunId));
Assert.Null(runStore.GetRunById("missing"));
}
[Fact]
public async Task GetRuns_ExcludesRunOwnedByAnotherDashboard()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
string historicalRunId;
using (var historicalRunStore = CreateRunStore(options))
{
historicalRunId = historicalRunStore.RunId;
using var historicalTelemetryContext = await CreateTelemetryRepositoryAsync(historicalRunStore.DatabasePath, options);
historicalRunStore.PublishRun();
}
using var activeRunStore = CreateRunStore(options);
using var activeTelemetryContext = await CreateTelemetryRepositoryAsync(activeRunStore.DatabasePath, options);
activeRunStore.PublishRun();
using var currentRunStore = CreateRunStore(options);
using var currentTelemetryContext = await CreateTelemetryRepositoryAsync(currentRunStore.DatabasePath, options);
Assert.Collection(
currentRunStore.GetRuns(),
currentRun =>
{
Assert.True(currentRun.IsCurrent);
Assert.Equal(currentRunStore.RunId, currentRun.RunId);
},
historicalRun =>
{
Assert.False(historicalRun.IsCurrent);
Assert.Equal(historicalRunId, historicalRun.RunId);
Assert.NotEqual(activeRunStore.RunId, historicalRun.RunId);
});
}
[Fact]
public async Task RunMode_DeletesOldestRunWhenLimitIsExceeded()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
var startedAt = new DateTimeOffset(2026, 8, 5, 12, 34, 56, TimeSpan.Zero);
var historicalRunDirectories = new List<string>();
foreach (var index in Enumerable.Range(1, DashboardRunStore.MaxRuns))
{
using var historicalRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt.AddDays(-index)));
historicalRunDirectories.Add(historicalRunStore.RunDirectory);
await InitializeAndPublishRunWithoutPruningAsync(historicalRunStore);
}
using var currentRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt));
await InitializeAndPublishRunAsync(currentRunStore);
var runsDirectory = Path.GetDirectoryName(currentRunStore.RunDirectory)!;
Assert.Equal(DashboardRunStore.MaxRuns, Directory.GetDirectories(runsDirectory).Length);
Assert.False(Directory.Exists(historicalRunDirectories[^1]));
Assert.All(historicalRunDirectories[..^1], directory => Assert.True(Directory.Exists(directory)));
Assert.True(Directory.Exists(currentRunStore.RunDirectory));
}
[Fact]
public async Task RunMode_DoesNotListRunsBeyondRetentionLimitWhenDiscoveredBeforePruning()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
var startedAt = new DateTimeOffset(2026, 8, 5, 12, 34, 56, TimeSpan.Zero);
var activeRunCount = 4;
foreach (var index in Enumerable.Range(0, DashboardRunStore.MaxRuns - activeRunCount))
{
using var historicalRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt.AddMilliseconds(index)));
await InitializeAndPublishRunAsync(historicalRunStore);
}
var activeRunStores = new List<DashboardRunStore>();
foreach (var index in Enumerable.Range(DashboardRunStore.MaxRuns - activeRunCount, activeRunCount))
{
var activeRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt.AddMilliseconds(index)));
using var database = new DashboardSqliteDatabase(activeRunStore.DatabasePath, pooling: false);
await database.InitializeSchemaAsync(cancellationToken: CancellationToken.None);
activeRunStore.PublishRun();
activeRunStores.Add(activeRunStore);
}
using var currentRunStore = CreateRunStore(
options,
new FixedTimeProvider(startedAt.AddMilliseconds(DashboardRunStore.MaxRuns)));
var runsBeforePruning = currentRunStore.GetRuns();
await InitializeAndPublishRunAsync(currentRunStore);
foreach (var activeRunStore in activeRunStores)
{
activeRunStore.Dispose();
}
var prunedRun = Assert.Single(runsBeforePruning, run => run.IsPruned);
Assert.False(File.Exists(prunedRun.DatabasePath));
var selectableRuns = currentRunStore.GetRuns();
Assert.Equal(DashboardRunStore.MaxRuns - activeRunCount, selectableRuns.Count);
Assert.All(selectableRuns, run => Assert.True(File.Exists(run.DatabasePath)));
}
[Fact]
public async Task RunMode_PinnedRunsDoNotCountTowardHistoricalLimitAndReloadFromMetadata()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
var startedAt = new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero);
var pinnedRunIds = new List<string>();
var pinnedRunDirectories = new List<string>();
var runIndex = 0;
for (var index = 0; index < 3; index++)
{
string runId;
using (var runStore = CreateRunStore(options, new FixedTimeProvider(startedAt.AddDays(runIndex++))))
{
runId = runStore.RunId;
await InitializeAndPublishRunAsync(runStore);
}
using var pinningRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt.AddDays(runIndex++)));
await InitializeAndPublishRunAsync(pinningRunStore);
var run = pinningRunStore.GetRuns().Single(run => string.Equals(run.RunId, runId, StringComparison.Ordinal));
pinningRunStore.SetRunPinned(run, isPinned: true);
pinnedRunIds.Add(runId);
pinnedRunDirectories.Add(Path.GetDirectoryName(run.DatabasePath)!);
using var metadata = JsonDocument.Parse(File.ReadAllText(Path.Combine(pinnedRunDirectories[^1], "run.json")));
Assert.True(metadata.RootElement.GetProperty("IsPinned").GetBoolean());
}
for (var index = 0; index < DashboardRunStore.MaxRuns - 1; index++)
{
using var runStore = CreateRunStore(options, new FixedTimeProvider(startedAt.AddDays(runIndex++)));
await InitializeAndPublishRunAsync(runStore);
}
using var finalRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt.AddDays(runIndex)));
await InitializeAndPublishRunAsync(finalRunStore);
var runs = finalRunStore.GetRuns();
Assert.Equal(
runs.OrderByDescending(run => run.IsCurrent).ThenByDescending(run => run.IsPinned).ThenByDescending(run => run.StartedAtUtc),
runs);
Assert.Single(runs, run => run.IsCurrent);
Assert.Equal(3, runs.Count(run => !run.IsCurrent && run.IsPinned));
Assert.Equal(DashboardRunStore.MaxRuns - 1, runs.Count(run => !run.IsCurrent && !run.IsPinned));
Assert.All(pinnedRunIds, runId => Assert.True(finalRunStore.GetRuns().Single(run => string.Equals(run.RunId, runId, StringComparison.Ordinal)).IsPinned));
Assert.All(pinnedRunDirectories, directory => Assert.True(Directory.Exists(directory)));
}
[Fact]
public async Task RunMode_DoesNotDeleteActiveExpiredRun()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
var startedAt = new DateTimeOffset(2026, 8, 5, 12, 34, 56, TimeSpan.Zero);
foreach (var index in Enumerable.Range(1, DashboardRunStore.MaxRuns - 1))
{
using var historicalRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt.AddDays(-index)));
await InitializeAndPublishRunWithoutPruningAsync(historicalRunStore);
}
using var activeRunStore = CreateRunStore(
options,
new FixedTimeProvider(startedAt.AddDays(-DashboardRunStore.MaxRuns)));
await InitializeAndPublishRunWithoutPruningAsync(activeRunStore);
using var currentRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt));
await InitializeAndPublishRunAsync(currentRunStore);
var runsDirectory = Path.GetDirectoryName(currentRunStore.RunDirectory)!;
Assert.True(Directory.Exists(activeRunStore.RunDirectory));
Assert.Equal(DashboardRunStore.MaxRuns + 1, Directory.GetDirectories(runsDirectory).Length);
}
[Fact]
public async Task SelectedHistoricalRun_HoldsLeaseUntilSelectionChanges()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
var startedAt = new DateTimeOffset(2026, 7, 26, 12, 34, 56, TimeSpan.Zero);
string historicalRunId;
string historicalRunDirectory;
using (var historicalRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt)))
{
historicalRunId = historicalRunStore.RunId;
historicalRunDirectory = historicalRunStore.RunDirectory;
using var historicalTelemetryContext = await CreateTelemetryRepositoryAsync(historicalRunStore.DatabasePath, options);
historicalRunStore.PublishRun();
}
using var currentRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt.AddMilliseconds(1)));
var repositoryFactory = CreateRepositoryFactory(options);
using var dataSourcePool = new DashboardDataSourcePool(currentRunStore, repositoryFactory);
using var dataSource = CreateDataSource(currentRunStore, dataSourcePool);
dataSource.SelectRun(historicalRunId);
foreach (var index in Enumerable.Range(1, DashboardRunStore.MaxRuns - 2))
{
using var additionalRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt.AddDays(index)));
await InitializeAndPublishRunWithoutPruningAsync(additionalRunStore);
}
var deletedRunDirectories = new List<string>();
using var pruningRunStore = new DashboardRunStore(
options,
NullLogger<DashboardRunStore>.Instance,
new FixedTimeProvider(startedAt.AddMilliseconds(2)),
deletedRunDirectories.Add);
await InitializeAndPublishRunAsync(pruningRunStore);
Assert.Empty(deletedRunDirectories);
Assert.True(Directory.Exists(historicalRunDirectory));
dataSource.SelectRun(runId: null);
using var nextPruningRunStore = new DashboardRunStore(
options,
NullLogger<DashboardRunStore>.Instance,
new FixedTimeProvider(startedAt.AddMilliseconds(3)),
deletedRunDirectories.Add);
await InitializeAndPublishRunAsync(nextPruningRunStore);
Assert.Equal(historicalRunDirectory, Assert.Single(deletedRunDirectories));
}
[Fact]
public async Task SelectedHistoricalRun_SharesDatabaseAcrossDataSources()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
string historicalRunId;
using (var historicalRunStore = CreateRunStore(options))
{
historicalRunId = historicalRunStore.RunId;
using var historicalTelemetryContext = await CreateTelemetryRepositoryAsync(historicalRunStore.DatabasePath, options);
historicalRunStore.PublishRun();
}
using var currentRunStore = CreateRunStore(options);
var historicalRun = currentRunStore.GetRuns().Single(run => string.Equals(run.RunId, historicalRunId, StringComparison.Ordinal));
var innerRepositoryFactory = CreateRepositoryFactory(options);
var repositoryFactory = new RecordingRepositoryFactory(innerRepositoryFactory);
using var dataSourcePool = new DashboardDataSourcePool(currentRunStore, repositoryFactory);
using var firstDataSource = CreateDataSource(currentRunStore, dataSourcePool);
using var secondDataSource = CreateDataSource(currentRunStore, dataSourcePool);
firstDataSource.SelectRun(historicalRunId);
secondDataSource.SelectRun(historicalRunId);
Assert.True(historicalRun.IsLeased);
var historicalDatabases = repositoryFactory.Databases.Where(database => database.IsReadOnly).ToList();
var sharedDatabase = Assert.IsType<DashboardSqliteDatabase>(historicalDatabases[0]);
Assert.Equal(4, historicalDatabases.Count);
Assert.All(historicalDatabases, database => Assert.Same(sharedDatabase, database));
Assert.All(historicalDatabases, database => Assert.Same(sharedDatabase.WriteLock, database.WriteLock));
Assert.Null(currentRunStore.TryAcquireRunLease(historicalRun));
firstDataSource.SelectRun(runId: null);
Assert.True(historicalRun.IsLeased);
Assert.Empty(secondDataSource.TelemetryRepository.GetResources());
Assert.Null(currentRunStore.TryAcquireRunLease(historicalRun));
secondDataSource.SelectRun(runId: null);
Assert.False(historicalRun.IsLeased);
using (var releasedRunLease = currentRunStore.TryAcquireRunLease(historicalRun))
{
Assert.NotNull(releasedRunLease);
Assert.True(historicalRun.IsLeased);
}
Assert.False(historicalRun.IsLeased);
}
[Fact]
public async Task SelectedHistoricalRun_CanBePinned()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
string historicalRunId;
using (var historicalRunStore = CreateRunStore(options))
{
historicalRunId = historicalRunStore.RunId;
using var historicalTelemetryContext = await CreateTelemetryRepositoryAsync(historicalRunStore.DatabasePath, options);
historicalRunStore.PublishRun();
}
using var currentRunStore = CreateRunStore(options);
var historicalRun = currentRunStore.GetRuns().Single(run => string.Equals(run.RunId, historicalRunId, StringComparison.Ordinal));
using var dataSourcePool = new DashboardDataSourcePool(currentRunStore, CreateRepositoryFactory(options));
using var dataSource = CreateDataSource(currentRunStore, dataSourcePool);
dataSource.SelectRun(historicalRunId);
Assert.True(historicalRun.IsLeased);
currentRunStore.SetRunPinned(historicalRun, isPinned: true);
Assert.True(historicalRun.IsPinned);
Assert.True(historicalRun.IsLeased);
using var metadata = JsonDocument.Parse(File.ReadAllText(Path.Combine(Path.GetDirectoryName(historicalRun.DatabasePath)!, "run.json")));
Assert.True(metadata.RootElement.GetProperty("IsPinned").GetBoolean());
}
[Fact]
public async Task RunMode_DeleteExpiredRunFails_LogsWarningAndContinues()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
var startedAt = new DateTimeOffset(2026, 8, 5, 12, 34, 56, TimeSpan.Zero);
var historicalRunDirectories = new List<string>();
foreach (var index in Enumerable.Range(1, DashboardRunStore.MaxRuns))
{
using var historicalRunStore = CreateRunStore(options, new FixedTimeProvider(startedAt.AddDays(-index)));
historicalRunDirectories.Add(historicalRunStore.RunDirectory);
await InitializeAndPublishRunWithoutPruningAsync(historicalRunStore);
}
var testSink = new TestSink();
using var loggerFactory = LoggerFactory.Create(builder => builder.AddProvider(new TestLoggerProvider(testSink)));
var logger = loggerFactory.CreateLogger<DashboardRunStore>();
var expiredRunDirectory = historicalRunDirectories[^1];
using var currentRunStore = new DashboardRunStore(
options,
logger,
new FixedTimeProvider(startedAt),
directory => throw new IOException($"The directory '{directory}' is in use."));
await InitializeAndPublishRunAsync(currentRunStore);
var warning = Assert.Single(testSink.Writes);
Assert.Equal(LogLevel.Warning, warning.LogLevel);
Assert.Equal(typeof(DashboardRunStore).FullName, warning.LoggerName);
Assert.Contains(expiredRunDirectory, warning.Message, StringComparison.Ordinal);
Assert.IsType<IOException>(warning.Exception);
Assert.True(Directory.Exists(expiredRunDirectory));
Assert.True(Directory.Exists(currentRunStore.RunDirectory));
}
[Fact]
public void SelectedHistoricalRun_SchemaValidationThrows_ReleasesRunLease()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
string malformedDatabasePath;
string malformedRunId;
using (var malformedRunStore = CreateRunStore(options))
{
malformedDatabasePath = malformedRunStore.DatabasePath;
malformedRunId = malformedRunStore.RunId;
using var connection = new SqliteConnection($"Data Source={malformedDatabasePath};Pooling=False");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "CREATE TABLE unrelated (value INTEGER NOT NULL);";
command.ExecuteNonQuery();
malformedRunStore.PublishRun();
}
using var currentRunStore = CreateRunStore(options);
var currentRun = Assert.Single(currentRunStore.GetRuns(), run => run.IsCurrent);
var malformedRun = currentRun with
{
RunId = malformedRunId,
DatabasePath = malformedDatabasePath,
IsCurrent = false
};
var runStore = new TestDashboardRunStore(
[currentRun, malformedRun],
TryOpenTestRunLease);
var repositoryFactory = CreateRepositoryFactory(options);
using var dataSourcePool = new DashboardDataSourcePool(runStore, repositoryFactory);
using var dataSource = CreateDataSource(runStore, dataSourcePool);
// A run directory whose database was never fully initialized has no dashboard_schema table. That is an
// incompatible database, not an internal error, so it must not surface the raw SQLite failure.
var exception = Assert.Throws<InvalidOperationException>(() => dataSource.SelectRun(malformedRunId));
Assert.Contains("does not match run metadata schema version", exception.Message, StringComparison.Ordinal);
using var runLease = Assert.IsType<FileStream>(TryOpenTestRunLease(malformedRun));
var malformedRunDirectory = Path.GetDirectoryName(malformedDatabasePath)!;
Directory.Delete(malformedRunDirectory, recursive: true);
Assert.False(Directory.Exists(malformedRunDirectory));
}
[Fact]
public async Task SelectedHistoricalRun_ReplacementValidationThrows_PreservesPreviousSelection()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
using var currentRunStore = CreateRunStore(options);
var historicalDirectory = Path.Combine(workspace.Path, "historical");
var historicalDatabasePath = Path.Combine(historicalDirectory, DashboardRunStore.DatabaseFileName);
using (var historicalTelemetryContext = await CreateTelemetryRepositoryAsync(historicalDatabasePath, options))
{
}
var malformedDirectory = Path.Combine(workspace.Path, "malformed");
var malformedDatabasePath = Path.Combine(malformedDirectory, DashboardRunStore.DatabaseFileName);
Directory.CreateDirectory(malformedDirectory);
using (var connection = new SqliteConnection($"Data Source={malformedDatabasePath};Pooling=False"))
{
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "CREATE TABLE unrelated (value INTEGER NOT NULL);";
command.ExecuteNonQuery();
}
var currentRun = Assert.Single(currentRunStore.GetRuns());
var historicalRun = currentRun with
{
RunId = "historical",
DatabasePath = historicalDatabasePath,
IsCurrent = false
};
var malformedRun = currentRun with
{
RunId = "malformed",
DatabasePath = malformedDatabasePath,
IsCurrent = false
};
var unavailableRun = currentRun with
{
RunId = "unavailable",
DatabasePath = Path.Combine(workspace.Path, "unavailable", DashboardRunStore.DatabaseFileName),
IsCurrent = false
};
var runStore = new TestDashboardRunStore(
[currentRun, historicalRun, malformedRun, unavailableRun],
TryOpenTestRunLease);
var repositoryFactory = CreateRepositoryFactory(options);
using var dataSourcePool = new DashboardDataSourcePool(runStore, repositoryFactory);
using var dataSource = CreateDataSource(runStore, dataSourcePool);
dataSource.SelectRun(historicalRun.RunId);
var historicalTelemetryRepository = dataSource.TelemetryRepository;
var historicalResourceRepository = dataSource.ResourceRepository;
Assert.Throws<InvalidOperationException>(() => dataSource.SelectRun(malformedRun.RunId));
Assert.Equal(historicalRun, dataSource.SelectedRun);
Assert.Same(historicalTelemetryRepository, dataSource.TelemetryRepository);
Assert.Same(historicalResourceRepository, dataSource.ResourceRepository);
Assert.Null(TryOpenTestRunLease(historicalRun));
dataSource.SelectRun(unavailableRun.RunId);
Assert.Equal(historicalRun, dataSource.SelectedRun);
Assert.Same(historicalTelemetryRepository, dataSource.TelemetryRepository);
Assert.Same(historicalResourceRepository, dataSource.ResourceRepository);
Assert.Null(TryOpenTestRunLease(historicalRun));
}
[Fact]
public void SqliteDatabase_ConfiguresLikeAndForeignKeys()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var database = new DashboardSqliteDatabase(Path.Combine(workspace.Path, "connection.db"));
using (var connection = database.OpenConnection())
using (var command = connection.CreateCommand())
{
Assert.True(new SqliteConnectionStringBuilder(connection.ConnectionString).Pooling);
Assert.Equal(5, connection.DefaultTimeout);
Assert.Equal(5, command.CommandTimeout);
command.CommandText = """
SELECT
'Dashboard' = 'dashboard' COLLATE NOCASE,
'CAFE au lait' LIKE '%fe AU%',
'Delta' LIKE 'dE%',
(SELECT foreign_keys FROM pragma_foreign_keys());
""";
using var reader = command.ExecuteReader();
Assert.True(reader.Read());
Assert.Equal(1, reader.GetInt64(0));
Assert.Equal(1, reader.GetInt64(1));
Assert.Equal(1, reader.GetInt64(2));
Assert.Equal(1, reader.GetInt64(3));
}
database.ClearPool();
}
[Fact]
public async Task SelectedHistoricalRun_ReplaysDataAndRejectsMutation()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
string historicalRunId;
using (var historicalRunStore = CreateRunStore(options))
{
historicalRunId = historicalRunStore.RunId;
using var telemetryContext = await CreateTelemetryRepositoryAsync(historicalRunStore.DatabasePath, options);
await telemetryContext.Repository.AddLogsAsync(new AddContext(), new RepeatedField<ResourceLogs>
{
new ResourceLogs
{
Resource = CreateResource(),
ScopeLogs =
{
new ScopeLogs
{
Scope = CreateScope("HistoricalLogger"),
LogRecords = { CreateLogRecord() }
}
}
}
});
using var resourceContext = CreateResourceRepository(historicalRunStore.DatabasePath);
await ((IResourceRepositoryWriter)resourceContext.Repository).ReplaceResourcesAsync([new Resource
{
Name = "api",
DisplayName = "API",
ResourceType = "Project",
CreatedAt = Timestamp.FromDateTime(DateTime.UnixEpoch)
}]);
historicalRunStore.PublishRun();
}
using var currentRunStore = CreateRunStore(options);
var repositoryFactory = CreateRepositoryFactory(options);
var testSink = new TestSink();
var logger = new TestLogger<DashboardDataSource>(new TestLoggerFactory(testSink, enabled: true));
using var dataSourcePool = new DashboardDataSourcePool(currentRunStore, repositoryFactory);
using var dataSource = CreateDataSource(currentRunStore, dataSourcePool, logger);
var currentTelemetryRepository = dataSource.TelemetryRepository;
var currentResourceRepository = dataSource.ResourceRepository;
Assert.Empty(dataSource.TelemetryRepository.GetResources());
Assert.False(dataSource.TelemetryRepository.IsReadOnly);
dataSource.SelectRun(historicalRunId);
var switchLog = Assert.Single(testSink.Writes);
Assert.Equal(LogLevel.Debug, switchLog.LogLevel);
Assert.Equal($"Switched dashboard run from '{currentRunStore.RunId}' to '{historicalRunId}'.", switchLog.Message);
Assert.True(dataSource.IsReadOnly);
Assert.True(dataSource.TelemetryRepository.IsReadOnly);
Assert.Equal(historicalRunId, dataSource.SelectedRun.RunId);
Assert.Equal("api", Assert.Single(dataSource.ResourceRepository.GetResources()).Name);
Assert.Equal("TestService", Assert.Single(dataSource.TelemetryRepository.GetResources()).ResourceName);
Assert.Equal("Test Value!", Assert.Single((await dataSource.TelemetryRepository.GetLogsAsync(new GetLogsContext
{
ResourceKeys = [],
StartIndex = 0,
Count = 10,
Filters = []
}, cancellationToken: CancellationToken.None)).Items).Message);
Assert.Empty(currentTelemetryRepository.GetResources());
Assert.Empty(currentResourceRepository.GetResources());
var exception = Assert.Throws<InvalidOperationException>(dataSource.EnsureWritable);
Assert.Equal("Historical dashboard data is read-only.", exception.Message);
using var activitySource = new DashboardActivitySource();
await using var currentClient = new DashboardClient(
activitySource,
NullLoggerFactory.Instance,
new ConfigurationManager(),
options,
new MockKnownPropertyLookup(),
new TestStringLocalizer<Resources.Resources>(),
(IResourceRepositoryWriter)currentResourceRepository);
IDashboardClient selectedClient = new SelectedDashboardClient(currentClient, dataSource);
var connectionStateChangedCount = 0;
selectedClient.ConnectionStateChanged += _ => connectionStateChangedCount++;
currentClient.SetConnectionStateForTesting(DashboardConnectionState.Disconnected);
Assert.True(selectedClient.IsEnabled);
Assert.True(selectedClient.WhenConnected.IsCompletedSuccessfully);
Assert.Equal(DashboardConnectionState.Connected, selectedClient.ConnectionState);
Assert.Equal(0, connectionStateChangedCount);
await selectedClient.ReconnectAsync();
var clearException = await Assert.ThrowsAsync<InvalidOperationException>(() => selectedClient.ClearConsoleLogsAsync(["api"], DateTime.UtcNow));
Assert.Equal("Historical dashboard data is read-only.", clearException.Message);
dataSource.SelectRun(runId: null);
Assert.Empty(dataSource.TelemetryRepository.GetResources());
Assert.False(dataSource.IsReadOnly);
Assert.False(dataSource.TelemetryRepository.IsReadOnly);
Action<DashboardConnectionState> handler = _ => connectionStateChangedCount++;
selectedClient.ConnectionStateChanged += handler;
dataSource.SelectRun(historicalRunId);
selectedClient.ConnectionStateChanged -= handler;
currentClient.SetConnectionStateForTesting(DashboardConnectionState.Connected);
Assert.Equal(0, connectionStateChangedCount);
}
[Fact]
public void UnknownRunId_SelectsCurrentRun()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
using var currentRunStore = CreateRunStore(options);
var repositoryFactory = CreateRepositoryFactory(options);
using var dataSourcePool = new DashboardDataSourcePool(currentRunStore, repositoryFactory);
using var dataSource = CreateDataSource(currentRunStore, dataSourcePool);
var currentTelemetryRepository = dataSource.TelemetryRepository;
var currentResourceRepository = dataSource.ResourceRepository;
dataSource.SelectRun("missing");
Assert.False(dataSource.IsReadOnly);
Assert.True(dataSource.SelectedRun.IsCurrent);
Assert.Equal(currentRunStore.RunId, dataSource.SelectedRun.RunId);
Assert.Same(currentResourceRepository, dataSource.ResourceRepository);
Assert.Same(currentTelemetryRepository, dataSource.TelemetryRepository);
}
[Fact]
public void UnavailableHistoricalRun_SelectsCurrentRun()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
var options = CreateOptions(workspace);
var historicalRunTime = new DateTimeOffset(2026, 7, 20, 12, 34, 56, TimeSpan.Zero);
string historicalRunId;
using (var historicalRunStore = CreateRunStore(options, new FixedTimeProvider(historicalRunTime)))
{
historicalRunId = historicalRunStore.RunId;
}
using var currentRunStore = CreateRunStore(options, new FixedTimeProvider(historicalRunTime.AddMilliseconds(1)));
var runStore = new TestDashboardRunStore(currentRunStore.GetRuns(), tryAcquireRunLease: _ => null);
var repositoryFactory = CreateRepositoryFactory(options);
var testSink = new TestSink();
var logger = new TestLogger<DashboardDataSource>(new TestLoggerFactory(testSink, enabled: true));
using var dataSourcePool = new DashboardDataSourcePool(runStore, repositoryFactory);
using var dataSource = CreateDataSource(runStore, dataSourcePool, logger);
var currentTelemetryRepository = dataSource.TelemetryRepository;
var currentResourceRepository = dataSource.ResourceRepository;
dataSource.SelectRun(historicalRunId);
Assert.False(dataSource.IsReadOnly);
Assert.True(dataSource.SelectedRun.IsCurrent);
Assert.Equal(currentRunStore.RunId, dataSource.SelectedRun.RunId);
Assert.Same(currentResourceRepository, dataSource.ResourceRepository);
Assert.Same(currentTelemetryRepository, dataSource.TelemetryRepository);
var failureLog = Assert.Single(testSink.Writes);
Assert.Equal(LogLevel.Warning, failureLog.LogLevel);
Assert.Equal($"Failed to switch to dashboard run '{historicalRunId}' because it is no longer available.", failureLog.Message);
}
private static IOptions<DashboardOptions> CreateOptions(
TemporaryWorkspace workspace,
string applicationName = "TestApp",
DashboardPersistenceMode persistenceMode = DashboardPersistenceMode.Run)
{
return Options.Create(new DashboardOptions
{
ApplicationName = applicationName,
Data = new DashboardDataOptions
{
Directory = workspace.Path,
PersistenceMode = persistenceMode
}
});
}
private static async Task<SqliteRepositoryTestContext<SqliteTelemetryRepository>> CreateTelemetryRepositoryAsync(
string databasePath,
IOptions<DashboardOptions> options)
{
var context = await SqliteRepositoryTestHelpers.CreateTelemetryRepositoryAsync(
databasePath,
pooling: true,
dashboardOptions: options);
return context;
}
private static async Task InitializeAndPublishRunAsync(DashboardRunStore runStore)
{
await InitializeAndPublishRunWithoutPruningAsync(runStore);
// Production defers pruning until the host has started so slow file system work stays off the startup
// path. Tests run both steps here so they observe the same end state.
runStore.PruneExpiredRuns();
}
private static async Task InitializeAndPublishRunWithoutPruningAsync(DashboardRunStore runStore)
{
using var database = new DashboardSqliteDatabase(runStore.DatabasePath, pooling: false);
await database.InitializeSchemaAsync(cancellationToken: CancellationToken.None);
runStore.PublishRun();
}
private static SqliteRepositoryTestContext<SqliteResourceRepository> CreateResourceRepository(
string databasePath)
{
var context = SqliteRepositoryTestHelpers.CreateResourceRepository(
databasePath,
new MockKnownPropertyLookup(),
pooling: true);
return context;
}
private static DashboardRunStore CreateRunStore(IOptions<DashboardOptions> options, TimeProvider? timeProvider = null)
{
return new DashboardRunStore(options, NullLogger<DashboardRunStore>.Instance, timeProvider ?? TimeProvider.System);
}
private static DashboardDataSource CreateDataSource(
IDashboardRunStore runStore,
DashboardDataSourcePool dataSourcePool,
ILogger<DashboardDataSource>? logger = null)
{
dataSourcePool.InitializeAsync(CancellationToken.None).GetAwaiter().GetResult();
return new DashboardDataSource(
runStore,
logger ?? NullLogger<DashboardDataSource>.Instance,
dataSourcePool);
}
private static FileStream? TryOpenTestRunLease(DashboardRunDescriptor run)
{
var runDirectory = Path.GetDirectoryName(run.DatabasePath)!;
try
{
var runLock = new FileStream(
DashboardRunStore.GetRunLockPath(runDirectory),
FileMode.OpenOrCreate,
FileAccess.ReadWrite,
FileShare.None,
bufferSize: 1,
FileOptions.DeleteOnClose);
if (!Directory.Exists(runDirectory))
{
runLock.Dispose();
return null;
}
return runLock;
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
return null;
}
}
private static RepositoryFactory CreateRepositoryFactory(IOptions<DashboardOptions> options)
{
var serviceProvider = new ServiceCollection()
.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance)
.AddSingleton(options)
.AddSingleton<PauseManager>()
.AddSingleton(TimeProvider.System)
.AddSingleton<IKnownPropertyLookup, MockKnownPropertyLookup>()
.BuildServiceProvider();
return new RepositoryFactory(serviceProvider);
}
private sealed class FixedTimeProvider(DateTimeOffset utcNow) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => utcNow;
}
private sealed class RecordingRepositoryFactory(IRepositoryFactory inner) : IRepositoryFactory
{
public List<DashboardSqliteDatabase> Databases { get; } = [];
public ITelemetryRepository CreateTelemetryRepository(DashboardSqliteDatabase database)
{
Databases.Add(database);
return inner.CreateTelemetryRepository(database);
}
public IResourceRepository CreateResourceRepository(DashboardSqliteDatabase database)
{
Databases.Add(database);
return inner.CreateResourceRepository(database);
}
}
}