mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-11 11:26:27 +00:00
Tests - Add Artemis.Core and profile editor test suites (#907)
This commit is contained in:
1 parent
6e7fae16d2
commit
504845c7d2
168 files changed
+22914
-7
No files matched your search
@@ -0,0 +1,160 @@
|
||||
using Artemis.Core.DryIoc;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.Core.DryIoc.Factories;
|
||||
using Artemis.Storage;
|
||||
using DryIoc;
|
||||
using DryIoc.Microsoft.DependencyInjection;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Artemis.Core.Tests.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A DryIoc container with the Artemis.Core services registered the way the app registers them, but backed by a fresh
|
||||
/// database in a temporary data folder and with logging going to the test output.
|
||||
/// <para>
|
||||
/// The database is a copy of one that is migrated once per test run, and the temporary folders are removed when the
|
||||
/// test run ends rather than after every test. Both would otherwise make up most of the time service tests take.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Services are created when they are first resolved, nothing is initialized or started. Avoid anything that works
|
||||
/// on <see cref="Constants.DataFolder" /> or <see cref="Constants.PluginsFolder" /> directly, such as loading plugins
|
||||
/// or starting the web server, those still point at your real Artemis folders.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class ServiceHarness : IDisposable
|
||||
{
|
||||
private static readonly string RunFolder = CreateRunFolder();
|
||||
private static readonly Lazy<string> TemplateDatabase = new(CreateTemplateDatabase);
|
||||
|
||||
private readonly List<ProfileService> _profileServices = [];
|
||||
private bool _disposed;
|
||||
|
||||
/// <param name="configure">Optionally replaces or adds registrations before anything is resolved</param>
|
||||
public ServiceHarness(Action<IContainer>? configure = null)
|
||||
{
|
||||
DataFolder = Path.Combine(RunFolder, Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(DataFolder);
|
||||
File.Copy(TemplateDatabase.Value, Path.Combine(DataFolder, "artemis.db"));
|
||||
|
||||
// Same rules as ArtemisBootstrapper
|
||||
Container = new Container(rules => rules
|
||||
.WithMicrosoftDependencyInjectionRules()
|
||||
.WithConcreteTypeDynamicRegistrations()
|
||||
.WithoutThrowOnRegisteringDisposableTransient());
|
||||
Container.RegisterCore();
|
||||
|
||||
// The real registrations use the database and log folder in your Artemis data folder
|
||||
Container.RegisterDelegate(CreateDbContext, Reuse.Transient, ifAlreadyRegistered: IfAlreadyRegistered.Replace);
|
||||
Container.Register<ILoggerFactory, TestLoggerFactory>(Reuse.Singleton, ifAlreadyRegistered: IfAlreadyRegistered.Replace);
|
||||
|
||||
// Remember profile services so their profiles can be disposed, see Dispose
|
||||
Container.RegisterInitializer<ProfileService>((service, _) => _profileServices.Add(service));
|
||||
|
||||
configure?.Invoke(Container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the temporary data folder containing the database, removed when the test run ends
|
||||
/// </summary>
|
||||
public string DataFolder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the container the services are registered in
|
||||
/// </summary>
|
||||
public IContainer Container { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a service, including protected services like the settings service which the container only hands to
|
||||
/// classes outside the plugins folder.
|
||||
/// </summary>
|
||||
public T Resolve<T>() where T : notnull
|
||||
{
|
||||
return Container.New<ServiceResolver<T>>().Service;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a database context on the database of this harness, for checking what a service stored
|
||||
/// </summary>
|
||||
public ArtemisDbContext CreateDbContext()
|
||||
{
|
||||
return new ArtemisDbContext {DataFolder = DataFolder};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
|
||||
// Profiles subscribe to static events of Core's stores, dispose them like the profile service does when it
|
||||
// deactivates a profile. Otherwise every profile of every test stays in memory and receives the store events of
|
||||
// later tests
|
||||
foreach (ProfileConfiguration configuration in _profileServices.SelectMany(s => s.ProfileCategories).SelectMany(c => c.ProfileConfigurations))
|
||||
{
|
||||
if (configuration.Profile is {Disposed: false} profile)
|
||||
profile.Dispose();
|
||||
configuration.Dispose();
|
||||
}
|
||||
|
||||
Container.Dispose();
|
||||
}
|
||||
|
||||
private static string CreateRunFolder()
|
||||
{
|
||||
string root = Path.Combine(Path.GetTempPath(), "Artemis.Core.Tests");
|
||||
|
||||
// Remove what interrupted test runs left behind, leaving recent folders alone since other test runs may be using them
|
||||
if (Directory.Exists(root))
|
||||
{
|
||||
foreach (DirectoryInfo folder in new DirectoryInfo(root).GetDirectories().Where(d => d.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1)))
|
||||
TryDelete(folder.FullName);
|
||||
}
|
||||
|
||||
string runFolder = Path.Combine(root, Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(runFolder);
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
|
||||
{
|
||||
// SQLite keeps the database files open in its connection pool
|
||||
SqliteConnection.ClearAllPools();
|
||||
TryDelete(runFolder);
|
||||
};
|
||||
return runFolder;
|
||||
}
|
||||
|
||||
private static string CreateTemplateDatabase()
|
||||
{
|
||||
// StorageManager only migrates the first database of a process, so migrate the template ourselves
|
||||
string folder = Path.Combine(RunFolder, "template");
|
||||
Directory.CreateDirectory(folder);
|
||||
using (ArtemisDbContext context = new() {DataFolder = folder})
|
||||
context.Database.Migrate();
|
||||
|
||||
// Close the template so it can be copied
|
||||
SqliteConnection.ClearAllPools();
|
||||
return Path.Combine(folder, "artemis.db");
|
||||
}
|
||||
|
||||
private static void TryDelete(string folder)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(folder, true);
|
||||
}
|
||||
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
// Left for the next test run, it is in the temp folder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receives a service through its constructor, the container hands protected services to it since it lives in the test
|
||||
/// assembly rather than in the plugins folder.
|
||||
/// </summary>
|
||||
internal sealed class ServiceResolver<T>(T service)
|
||||
{
|
||||
public T Service { get; } = service;
|
||||
}
|
||||
Reference in new issue
Block a user