Skip to content

Commit

Permalink
all working unit tests for SqliteStateDbRepositoryFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
woutervanranst committed Aug 26, 2024
1 parent 5a31b97 commit 9cb8333
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 423 deletions.
3 changes: 1 addition & 2 deletions src/Arius.Core.Domain/DirectoryInfoExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

public static class DirectoryInfoExtensions
{
public static string GetFullFileName(this DirectoryInfo directoryInfo, string fileName)
public static string GetFullName(this DirectoryInfo directoryInfo, string fileName)
{
return Path.Combine(directoryInfo.FullName, fileName);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ public interface IStateDbRepositoryFactory

public interface IStateDbRepository
{

RepositoryVersion Version { get; }
IAsyncEnumerable<PointerFileEntry> GetPointerFileEntries();
IAsyncEnumerable<string> GetBinaryEntries();
}
7 changes: 7 additions & 0 deletions src/Arius.Core.Infrastructure/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
global using Microsoft.Extensions.Logging;
global using System;
global using System.Linq;
global using System.Threading.Tasks;
global using WouterVanRanst.Utils;
global using WouterVanRanst.Utils.Extensions;
global using System.Threading;
8 changes: 8 additions & 0 deletions src/Arius.Core.Infrastructure/InternalsVisibleTo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* This is required to test the internals of the Arius.Core assembly
*/

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Arius.Core.New.UnitTests")]
[assembly: InternalsVisibleTo("Arius.ArchUnit")]
124 changes: 56 additions & 68 deletions src/Arius.Core.Infrastructure/Repositories/StateDbRepositoryFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
using Arius.Core.Domain.Storage;
using Azure;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using WouterVanRanst.Utils;

namespace Arius.Core.Infrastructure.Repositories;

Expand All @@ -30,79 +28,60 @@ public async Task<IStateDbRepository> CreateAsync(RepositoryOptions repositoryOp
{
// TODO Validation

var repository = storageAccountFactory.GetRepository(repositoryOptions);
var repository = storageAccountFactory.GetRepository(repositoryOptions);
var localStateDbFolder = config.GetLocalStateDbFolderForRepositoryName(repositoryOptions.ContainerName);

var fullName = await GetLocalRepositoryFullName();
version ??= await GetLatestVersionAsync(repository)
?? new RepositoryVersion { Name = $"{DateTime.UtcNow:s}" };

/* Database is locked -> Cache = shared as per https://docs.microsoft.com/en-us/dotnet/standard/data/sqlite/database-errors
* NOTE if it still fails, try 'pragma temp_store=memory'
*/
var optionsBuilder = new DbContextOptionsBuilder<SqliteStateDbContext>();
optionsBuilder.UseSqlite($"Data Source={fullName};Cache=Shared",
sqliteOptions =>
{
sqliteOptions.CommandTimeout(60); //set command timeout to 60s to avoid concurrency errors on 'table is locked'
});
var fullName = await GetLocalRepositoryFullNameAsync(repository, localStateDbFolder, version, repositoryOptions.Passphrase);

// Create the repository with the configured DbContext
return new StateDbRepository(optionsBuilder.Options);
var optionsBuilder = new DbContextOptionsBuilder<SqliteStateDbContext>()
.UseSqlite($"Data Source={fullName};Cache=Shared", sqliteOptions => sqliteOptions.CommandTimeout(60));

async Task<string> GetLocalRepositoryFullName()
return new StateDbRepository(optionsBuilder.Options, version);
}

private async Task<RepositoryVersion?> GetLatestVersionAsync(IRepository repository)
{
return await repository
.GetRepositoryVersions()
.OrderBy(b => b.Name)
.LastOrDefaultAsync();
}

private async Task<string> GetLocalRepositoryFullNameAsync(IRepository repository, DirectoryInfo stateDbFolder, RepositoryVersion version, string passphrase)
{
var localPath = stateDbFolder.GetFullName(version.GetFileSystemName());

if (File.Exists(localPath))
return localPath;

try
{
var blob = repository.GetRepositoryVersionBlob(version);
await repository.DownloadAsync(blob, localPath, passphrase);
return localPath;
}
catch (RequestFailedException e) when (e.ErrorCode == "BlobNotFound")
{
throw new ArgumentException("The requested version was not found", nameof(version), e);
}
catch (InvalidDataException e)
{
var localStateDbFolder = config.GetLocalStateDbFolderForRepositoryName(repositoryOptions.ContainerName);

if (version is null)
{
var latestVersion = await GetLatestVersionAsync();
if (latestVersion == null)
{
// No states yet remotely - this is a fresh archive
return localStateDbFolder.GetFullFileName($"{DateTime.UtcNow:s}");
}
return await GetLocallyCachedAsync(localStateDbFolder, latestVersion);
}
else
{
return await GetLocallyCachedAsync(localStateDbFolder, version);
}

async Task<RepositoryVersion?> GetLatestVersionAsync()
{
return await repository
.GetRepositoryVersions()
.OrderBy(b => b.Name)
.LastOrDefaultAsync();
}

async Task<string> GetLocallyCachedAsync(DirectoryInfo stateDbFolder, RepositoryVersion version)
{
var localPath = stateDbFolder.GetFullFileName(version.Name);

if (File.Exists(localPath))
{
// Cached locally, ASSUME it’s the same version
return localPath;
}

try
{
var blob = repository.GetRepositoryVersionBlob(version);
await repository.DownloadAsync(blob, localPath, repositoryOptions.Passphrase);
return localPath;
}
catch (RequestFailedException e) when (e.ErrorCode == "BlobNotFound")
{
throw new ArgumentException("The requested version was not found", nameof(version), e);
}
catch (InvalidDataException e)
{
throw new ArgumentException("Could not load the state database. Probably a wrong passphrase was used.", e);
}
}
throw new ArgumentException("Could not load the state database. Probably a wrong passphrase was used.", e);
}
}
}

public static class RepositoryVersionExtensions
{
public static string GetFileSystemName(this RepositoryVersion version)
{
return version.Name.Replace(":", "");
}
}

internal class SqliteStateDbContext : DbContext
{
public SqliteStateDbContext(DbContextOptions<SqliteStateDbContext> options)
Expand Down Expand Up @@ -191,14 +170,23 @@ internal class StateDbRepository : IStateDbRepository
{
private readonly DbContextOptions<SqliteStateDbContext> dbContextOptions;

public StateDbRepository(DbContextOptions<SqliteStateDbContext> dbContextOptions)
public StateDbRepository(DbContextOptions<SqliteStateDbContext> dbContextOptions, RepositoryVersion version)
{
Version = version;
this.dbContextOptions = dbContextOptions;

using var context = new SqliteStateDbContext(dbContextOptions);
context.Database.EnsureCreated();
context.Database.Migrate();
}

public RepositoryVersion Version { get; }

public IAsyncEnumerable<PointerFileEntry> GetPointerFileEntries()
{
using var context = new SqliteStateDbContext(dbContextOptions);
var context = new SqliteStateDbContext(dbContextOptions); // not with using, maybe detach them all?
return context.PointerFileEntries.ToAsyncEnumerable();
}

public IAsyncEnumerable<string> GetBinaryEntries() => AsyncEnumerable.Empty<string>();
}
9 changes: 9 additions & 0 deletions src/Arius.Core.New.UnitTests/DateTimeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Arius.Core.New.UnitTests;

internal static class DateTimeExtensions
{
public static DateTime TruncateToSeconds(this DateTime dateTime)
{
return dateTime.AddTicks(-(dateTime.Ticks % TimeSpan.TicksPerSecond));
}
}
Loading

0 comments on commit 9cb8333

Please sign in to comment.