Skip to content

Commit

Permalink
Add integration test for syncing sena-3
Browse files Browse the repository at this point in the history
  • Loading branch information
rmunn committed Nov 22, 2024
1 parent 622856d commit ceb7ade
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 4 deletions.
14 changes: 14 additions & 0 deletions backend/FwLite/FwLiteProjectSync.Tests/Fixtures/LexboxConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;

namespace FwLiteProjectSync.Tests.Fixtures;

public class LexboxConfig
{
[Required, Url, RegularExpression(@"^.+/$", ErrorMessage = "Must end with '/'")]
public required string LexboxUrl { get; set; }
public string HgWebUrl => $"{LexboxUrl}hg/";
[Required]
public required string LexboxUsername { get; set; }
[Required]
public required string LexboxPassword { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using SIL.CommandLineProcessing;
using SIL.PlatformUtilities;
using SIL.Progress;

namespace FwLiteProjectSync.Tests.Fixtures;

public static class MercurialTestHelper
{
public static string HgCommand =>
Path.Combine("Mercurial", Platform.IsWindows ? "hg.exe" : "hg");

private static string RunHgCommand(string repoPath, string args)
{
var result = CommandLineRunner.Run(HgCommand, args, repoPath, 120, new NullProgress());
if (result.ExitCode == 0) return result.StandardOutput;
throw new Exception(
$"hg {args} failed.\nStdOut: {result.StandardOutput}\nStdErr: {result.StandardError}");

}

public static void HgClean(string repoPath, string exclude)
{
RunHgCommand(repoPath, $"purge --no-confirm --exclude {exclude}");
}

public static void HgUpdate(string repoPath, string rev)
{
RunHgCommand(repoPath, $"update \"{rev}\"");
}
}

114 changes: 114 additions & 0 deletions backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System.IO.Compression;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using FwDataMiniLcmBridge;
using FwDataMiniLcmBridge.Api;
using FwDataMiniLcmBridge.LcmUtils;
using FwDataMiniLcmBridge.Tests.Fixtures;
using LcmCrdt;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MiniLcm;
using SIL.IO;
using SIL.Progress;

namespace FwLiteProjectSync.Tests.Fixtures;

public class Sena3Fixture : IAsyncLifetime
{
private readonly SyncFixture syncFixture;
public CrdtFwdataProjectSyncService SyncService =>
Services.GetRequiredService<CrdtFwdataProjectSyncService>();
public IServiceProvider Services => syncFixture.Services;
private readonly LexboxConfig lexboxConfig;
private readonly HttpClient http;
public CrdtMiniLcmApi CrdtApi { get; set; } = null!;
public FwDataMiniLcmApi FwDataApi { get; set; } = null!;
private bool AlreadyLoggedIn { get; set; } = false;

public Sena3Fixture()
{
syncFixture = SyncFixture.Create(services =>
{
services.AddOptions<LexboxConfig>()
.BindConfiguration("LexboxConfig")
.ValidateDataAnnotations()
.ValidateOnStart();
// TODO: How do I set default values if and only if they're not already set (e.g., via environment variables)?
services.Configure<LexboxConfig>(c =>
{
c.LexboxUrl = "http://localhost/";
c.LexboxUsername = "admin";
c.LexboxPassword = "pass";
});
}, nameof(Sena3Fixture)); // TODO: Or create the project name in the constructor rather than in InitializeAsync, then pass it in here
lexboxConfig = Services.GetRequiredService<IOptions<LexboxConfig>>().Value;
var factory = Services.GetRequiredService<IHttpClientFactory>();
http = factory.CreateClient(nameof(Sena3Fixture));
}

public async Task InitializeAsync()
{
var sena3MasterCopy = await DownloadSena3();
var projectName = "sena-3_" + Guid.NewGuid().ToString("N");

var projectsFolder = Services.GetRequiredService<IOptions<FwDataBridgeConfig>>().Value
.ProjectsFolder;
if (Path.Exists(projectsFolder)) Directory.Delete(projectsFolder, true);
Directory.CreateDirectory(projectsFolder);
var fwDataProject = new FwDataProject(projectName, projectsFolder);
var fwDataProjectPath = Path.Combine(fwDataProject.ProjectsPath, fwDataProject.Name);
DirectoryHelper.Copy(sena3MasterCopy, fwDataProjectPath);
File.Move(Path.Combine(fwDataProjectPath, "sena-3.fwdata"), fwDataProject.FilePath);

Services.GetRequiredService<IProjectLoader>().LoadCache(fwDataProject);
FwDataApi = Services.GetRequiredService<FwDataFactory>().GetFwDataMiniLcmApi(projectName, false);

var crdtProjectsFolder =
Services.GetRequiredService<IOptions<LcmCrdtConfig>>().Value.ProjectPath;
if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true);
Directory.CreateDirectory(crdtProjectsFolder);
var crdtProject = await Services.GetRequiredService<ProjectsService>()
.CreateProject(new(projectName, FwProjectId: FwDataApi.ProjectId, SeedNewProjectData: false));
CrdtApi = (CrdtMiniLcmApi) await Services.OpenCrdtProject(crdtProject);
}

public async Task DisposeAsync()
{
await syncFixture.DisposeAsync();
}

public async Task<Stream> DownloadProjectBackupStream(string code)
{
var backupUrl = new Uri($"{lexboxConfig.LexboxUrl}api/project/backupProject/{code}");
var result = await http.GetAsync(backupUrl);
return await result.Content.ReadAsStreamAsync();
}

public async Task LoginAs(string lexboxUsername, string lexboxPassword)
{
if (AlreadyLoggedIn) return;
await http.PostAsync($"{lexboxConfig.LexboxUrl}api/login", JsonContent.Create(new { EmailOrUsername=lexboxUsername, Password=lexboxPassword }));
AlreadyLoggedIn = true;
}

public async Task<string> DownloadSena3()
{

var tempFolder = Path.Combine(Path.GetTempPath(), nameof(Sena3Fixture));
var sena3MasterCopy = Path.Combine(tempFolder, "sena-3");
if (!Directory.Exists(sena3MasterCopy) || !File.Exists(Path.Combine(sena3MasterCopy, "sena-3.fwdata")))
{
await LoginAs(lexboxConfig.LexboxUsername, lexboxConfig.LexboxPassword);
Directory.CreateDirectory(sena3MasterCopy);
var zipStream = await DownloadProjectBackupStream("sena-3");
ZipFile.ExtractToDirectory(zipStream, sena3MasterCopy);
MercurialTestHelper.HgUpdate(sena3MasterCopy, "tip");
LfMergeBridge.LfMergeBridge.ReassembleFwdataFile(new NullProgress(), false, Path.Combine(sena3MasterCopy, "sena-3.fwdata"));
MercurialTestHelper.HgClean(sena3MasterCopy, "sena-3.fwdata");
}
return sena3MasterCopy;
}
}
11 changes: 7 additions & 4 deletions backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public class SyncFixture : IAsyncLifetime

public static SyncFixture Create([CallerMemberName] string projectName = "") => new(projectName);

private SyncFixture(string projectName)
public static SyncFixture Create(Action<IServiceCollection> extraServiceConfiguration, [CallerMemberName] string projectName = "") => new(projectName, extraServiceConfiguration);

private SyncFixture(string projectName, Action<IServiceCollection>? extraServiceConfiguration = null)
{
_projectName = projectName;
var crdtServices = new ServiceCollection()
Expand All @@ -34,9 +36,9 @@ private SyncFixture(string projectName)
.AddFwLiteProjectSync()
.Configure<FwDataBridgeConfig>(c => c.ProjectsFolder = Path.Combine(".", _projectName, "FwData"))
.Configure<LcmCrdtConfig>(c => c.ProjectPath = Path.Combine(".", _projectName, "LcmCrdt"))
.AddLogging(builder => builder.AddDebug())
.BuildServiceProvider();
_services = crdtServices.CreateAsyncScope();
.AddLogging(builder => builder.AddDebug());
if (extraServiceConfiguration is not null) extraServiceConfiguration(crdtServices);
_services = crdtServices.BuildServiceProvider().CreateAsyncScope();
}

public SyncFixture(): this("sena-3_" + Guid.NewGuid().ToString("N"))
Expand Down Expand Up @@ -64,6 +66,7 @@ public async Task InitializeAsync()

public async Task DisposeAsync()
{
// CAUTION: Do not assume that InitializeAsync() has been called
await _services.DisposeAsync();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Mercurial4ChorusDestDir>$(MSBuildProjectDirectory)</Mercurial4ChorusDestDir>
</PropertyGroup>

<ItemGroup>
Expand All @@ -28,6 +29,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1"/>
<PackageReference Include="SIL.ChorusPlugin.LfMergeBridge" Version="4.2.0-beta0028" />
<PackageReference Include="SIL.Chorus.Mercurial" Version="6.5.1.*" />
</ItemGroup>

<ItemGroup>
Expand All @@ -41,4 +44,12 @@
<ProjectReference Include="..\LocalWebApp\LocalWebApp.csproj" />
</ItemGroup>

<ItemGroup>
<!-- json files get imported by default in web projects, so exclude those here so they aren't imported again below -->
<!-- TODO: Determine if this step is actually necessary -->
<Content Remove="Mercurial\contrib\asv.conf.json"/>
<Content Include="Mercurial\**" CopyToOutputDirectory="Always" Watch="false" />
<Content Include="MercurialExtensions\**" CopyToOutputDirectory="Always" Watch="false" />
</ItemGroup>

</Project>
100 changes: 100 additions & 0 deletions backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using FluentAssertions.Equivalency;
using FwLiteProjectSync.Tests.Fixtures;
using LcmCrdt;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MiniLcm;
using MiniLcm.Models;
using SystemTextJsonPatch;

namespace FwLiteProjectSync.Tests;

public class Sena3SyncTests : IClassFixture<Sena3Fixture>, IAsyncLifetime
{
private readonly Sena3Fixture _fixture;
private readonly CrdtFwdataProjectSyncService _syncService;

private readonly Guid _complexEntryId = Guid.NewGuid();
private Entry _testEntry = new Entry
{
Id = Guid.NewGuid(),
LexemeForm = { Values = { { "en", "Apple" } } },
Note = { Values = { { "en", "this is a test note" } } },
Senses =
[
new Sense
{
Gloss = { Values = { { "en", "Apple" } } },
Definition = { Values = { { "en", "a round fruit with a hard, crisp skin" } } },
ExampleSentences =
[
new ExampleSentence { Sentence = { Values = { { "en", "I went to the store to buy an apple." } } } }
]
}
]
};

public async Task InitializeAsync()
{
await _fixture.FwDataApi.CreateEntry(_testEntry);
await _fixture.FwDataApi.CreateEntry(new Entry()
{
Id = _complexEntryId,
LexemeForm = { { "en", "Pineapple" } },
Components =
[
new ComplexFormComponent()
{
Id = Guid.NewGuid(),
ComplexFormEntryId = _complexEntryId,
ComplexFormHeadword = "Pineapple",
ComponentEntryId = _testEntry.Id,
ComponentHeadword = "Apple"
}
]
});
}

public async Task DisposeAsync()
{
await foreach (var entry in _fixture.FwDataApi.GetEntries())
{
await _fixture.FwDataApi.DeleteEntry(entry.Id);
}
foreach (var entry in await _fixture.CrdtApi.GetEntries().ToArrayAsync())
{
await _fixture.CrdtApi.DeleteEntry(entry.Id);
}
}

public Sena3SyncTests(Sena3Fixture fixture)
{
_fixture = fixture;
_syncService = _fixture.SyncService;
}

[Fact]
public async Task FirstSena3SyncJustDoesAnImport()
{
var crdtApi = _fixture.CrdtApi;
var fwdataApi = _fixture.FwDataApi;
await _syncService.Sync(crdtApi, fwdataApi);

var crdtEntries = await crdtApi.GetEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}

[Fact]
public async Task SecondSena3SyncDoesNothing()
{
var crdtApi = _fixture.CrdtApi;
var fwdataApi = _fixture.FwDataApi;
await _syncService.Sync(crdtApi, fwdataApi);
var secondSync = await _syncService.Sync(crdtApi, fwdataApi);
secondSync.CrdtChanges.Should().Be(0);
secondSync.FwdataChanges.Should().Be(0);
}
}

0 comments on commit ceb7ade

Please sign in to comment.