From fb3a83934a5f1f23aaf97f7abc845da2eed4d45c Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Thu, 22 Aug 2024 17:20:31 +0700 Subject: [PATCH 1/9] Use real MongoConnection for E2E tests --- src/LfMerge.Core.Tests/E2E/E2ETestBase.cs | 17 +++--- .../E2E/LexboxSendReceiveTests.cs | 2 +- src/LfMerge.Core.Tests/Lcm/RoundTripTests.cs | 4 +- .../Lcm/TransferMongoToLcmActionTests.cs | 24 ++++----- src/LfMerge.Core.Tests/SRTestEnvironment.cs | 52 +++++++++++++++++++ src/LfMerge.Core.Tests/TestDoubles.cs | 2 +- src/LfMerge.Core.Tests/TestEnvironment.cs | 41 ++++++++++----- .../MongoConnector/IMongoConnection.cs | 1 + .../MongoConnector/MongoConnection.cs | 10 +++- .../Settings/DefaultLfMergeSettings.cs | 2 +- src/LfMerge.Core/Settings/LfMergeSettings.cs | 26 +++++----- 11 files changed, 128 insertions(+), 53 deletions(-) diff --git a/src/LfMerge.Core.Tests/E2E/E2ETestBase.cs b/src/LfMerge.Core.Tests/E2E/E2ETestBase.cs index 3d11a912..ca3c9327 100644 --- a/src/LfMerge.Core.Tests/E2E/E2ETestBase.cs +++ b/src/LfMerge.Core.Tests/E2E/E2ETestBase.cs @@ -25,7 +25,7 @@ public class E2ETestBase private readonly HashSet ProjectIdsToDelete = []; public SRTestEnvironment TestEnv { get; set; } - public MongoConnectionDouble _mongoConnection; + public IMongoConnection _mongoConnection; public MongoProjectRecordFactory _recordFactory; public E2ETestBase() @@ -81,14 +81,15 @@ public async Task TestSetup() Assert.Ignore("Can't run E2E tests without a copy of LexBox to test against. Please either launch LexBox on localhost port 80, or set the appropriate environment variables to point to a running copy of LexBox."); } await TestEnv.Login(); + Console.WriteLine("About to launch Mongo..."); + TestEnv.LaunchMongo(); MagicStrings.SetMinimalModelVersion(LcmCache.ModelVersion); - _mongoConnection = MainClass.Container.Resolve() as MongoConnectionDouble; - if (_mongoConnection == null) - throw new AssertionException("E2E tests need a mock MongoConnection that stores data in order to work."); - _recordFactory = MainClass.Container.Resolve() as MongoProjectRecordFactoryDouble; - if (_recordFactory == null) - throw new AssertionException("E2E tests need a mock MongoProjectRecordFactory in order to work."); + _mongoConnection = MainClass.Container.Resolve(); + var _mongoConnectionDouble = _mongoConnection as MongoConnectionDouble; + if (_mongoConnectionDouble != null) + throw new AssertionException("E2E tests need a real MongoConnection, not a mock."); + _recordFactory = MainClass.Container.Resolve(); } [TearDown] @@ -220,7 +221,7 @@ public void SendReceiveToLexbox(LanguageForgeProject lfProject) public (string, DateTime, DateTime) UpdateLfGloss(LanguageForgeProject lfProject, Guid entryId, string wsId, Func textConverter) { - var lfEntry = _mongoConnection.GetLfLexEntryByGuid(entryId); + var lfEntry = _mongoConnection.GetLfLexEntryByGuid(lfProject, entryId); Assert.That(lfEntry, Is.Not.Null); var unchangedGloss = lfEntry.Senses[0].Gloss[wsId].Value; lfEntry.Senses[0].Gloss["pt"].Value = textConverter(unchangedGloss); diff --git a/src/LfMerge.Core.Tests/E2E/LexboxSendReceiveTests.cs b/src/LfMerge.Core.Tests/E2E/LexboxSendReceiveTests.cs index 358f83a6..65926905 100644 --- a/src/LfMerge.Core.Tests/E2E/LexboxSendReceiveTests.cs +++ b/src/LfMerge.Core.Tests/E2E/LexboxSendReceiveTests.cs @@ -50,7 +50,7 @@ public async Task E2E_LFDataChangedLDDataChanged_LFWins() // Verify // LF side should win conflict since its modified date was later - var lfEntryAfterSR = _mongoConnection.GetLfLexEntryByGuid(entryId); + var lfEntryAfterSR = _mongoConnection.GetLfLexEntryByGuid(lfProject, entryId); Assert.That(lfEntryAfterSR?.Senses?[0]?.Gloss?["pt"]?.Value, Is.EqualTo(unchangedGloss + " - changed in LF")); // LF's modified dates should have been updated by the sync action Assert.That(lfEntryAfterSR.AuthorInfo.ModifiedDate, Is.GreaterThan(origLfDateModified)); diff --git a/src/LfMerge.Core.Tests/Lcm/RoundTripTests.cs b/src/LfMerge.Core.Tests/Lcm/RoundTripTests.cs index 75a99f65..0b0d64d3 100644 --- a/src/LfMerge.Core.Tests/Lcm/RoundTripTests.cs +++ b/src/LfMerge.Core.Tests/Lcm/RoundTripTests.cs @@ -830,7 +830,7 @@ public void RoundTrip_MongoToLcmToMongo_ShouldBeAbleToAddAndModifyParagraphsInCu // Here we check two things: // 1) Can we add paragraphs? // 2) Can we change existing paragraphs? - LfLexEntry lfEntry = _conn.GetLfLexEntryByGuid(entryGuid); + LfLexEntry lfEntry = _conn.GetLfLexEntryByGuid(lfProject, entryGuid); // BsonDocument customFieldValues = GetCustomFieldValues(cache, lcmEntry, "entry"); BsonDocument customFieldsBson = lfEntry.CustomFields; Assert.That(customFieldsBson.Contains("customField_entry_Cust_MultiPara"), Is.True, @@ -923,7 +923,7 @@ public void RoundTrip_MongoToLcmToMongo_ShouldBeAbleToDeleteParagraphsInCustomMu // Here we check just one thing: // 1) Can we delete paragraphs? - LfLexEntry lfEntry = _conn.GetLfLexEntryByGuid(entryGuid); + LfLexEntry lfEntry = _conn.GetLfLexEntryByGuid(lfProject, entryGuid); // BsonDocument customFieldValues = GetCustomFieldValues(cache, lcmEntry, "entry"); BsonDocument customFieldsBson = lfEntry.CustomFields; Assert.That(customFieldsBson.Contains("customField_entry_Cust_MultiPara"), Is.True, diff --git a/src/LfMerge.Core.Tests/Lcm/TransferMongoToLcmActionTests.cs b/src/LfMerge.Core.Tests/Lcm/TransferMongoToLcmActionTests.cs index bec32938..c6aee22c 100644 --- a/src/LfMerge.Core.Tests/Lcm/TransferMongoToLcmActionTests.cs +++ b/src/LfMerge.Core.Tests/Lcm/TransferMongoToLcmActionTests.cs @@ -234,7 +234,7 @@ public void Action_WithOneModifiedEntry_ShouldCountOneModified() SutLcmToMongo.Run(lfProj); Guid entryGuid = Guid.Parse(TestEntryGuidStr); - LfLexEntry entry = _conn.GetLfLexEntryByGuid(entryGuid); + LfLexEntry entry = _conn.GetLfLexEntryByGuid(lfProj, entryGuid); LcmCache cache = lfProj.FieldWorksProject.Cache; string vernacularWS = cache.LanguageProject.DefaultVernacularWritingSystem.Id; string changedLexeme = "modified lexeme for this test"; @@ -263,7 +263,7 @@ public void Action_WithOneDeletedEntry_ShouldCountOneDeleted() SutLcmToMongo.Run(lfProj); Guid entryGuid = Guid.Parse(TestEntryGuidStr); - LfLexEntry entry = _conn.GetLfLexEntryByGuid(entryGuid); + LfLexEntry entry = _conn.GetLfLexEntryByGuid(lfProj, entryGuid); entry.IsDeleted = true; _conn.UpdateMockLfLexEntry(entry); @@ -325,7 +325,7 @@ public void Action_WithTwoModifiedEntries_ShouldCountTwoModified() SutLcmToMongo.Run(lfProj); Guid entryGuid = Guid.Parse(TestEntryGuidStr); - LfLexEntry entry = _conn.GetLfLexEntryByGuid(entryGuid); + LfLexEntry entry = _conn.GetLfLexEntryByGuid(lfProj, entryGuid); LcmCache cache = lfProj.FieldWorksProject.Cache; string vernacularWS = cache.LanguageProject.DefaultVernacularWritingSystem.Id; string changedLexeme = "modified lexeme for this test"; @@ -335,7 +335,7 @@ public void Action_WithTwoModifiedEntries_ShouldCountTwoModified() _conn.UpdateMockLfLexEntry(entry); Guid kenGuid = Guid.Parse(KenEntryGuidStr); - LfLexEntry kenEntry = _conn.GetLfLexEntryByGuid(kenGuid); + LfLexEntry kenEntry = _conn.GetLfLexEntryByGuid(lfProj, kenGuid); string changedLexeme2 = "modified lexeme #2 for this test"; kenEntry.Lexeme = LfMultiText.FromSingleStringMapping(vernacularWS, changedLexeme2); kenEntry.AuthorInfo = new LfAuthorInfo(); @@ -362,11 +362,11 @@ public void Action_WithTwoDeletedEntries_ShouldCountTwoDeleted() SutLcmToMongo.Run(lfProj); Guid entryGuid = Guid.Parse(TestEntryGuidStr); - LfLexEntry entry = _conn.GetLfLexEntryByGuid(entryGuid); + LfLexEntry entry = _conn.GetLfLexEntryByGuid(lfProj, entryGuid); entry.IsDeleted = true; _conn.UpdateMockLfLexEntry(entry); Guid kenGuid = Guid.Parse(KenEntryGuidStr); - entry = _conn.GetLfLexEntryByGuid(kenGuid); + entry = _conn.GetLfLexEntryByGuid(lfProj, kenGuid); entry.IsDeleted = true; _conn.UpdateMockLfLexEntry(entry); @@ -430,7 +430,7 @@ public void Action_WithOneModifiedEntry_ShouldNotCountThatModifiedEntryOnSecondR SutLcmToMongo.Run(lfProj); Guid entryGuid = Guid.Parse(TestEntryGuidStr); - LfLexEntry entry = _conn.GetLfLexEntryByGuid(entryGuid); + LfLexEntry entry = _conn.GetLfLexEntryByGuid(lfProj, entryGuid); LcmCache cache = lfProj.FieldWorksProject.Cache; string vernacularWS = cache.LanguageProject.DefaultVernacularWritingSystem.Id; string changedLexeme = "modified lexeme for this test"; @@ -470,7 +470,7 @@ public void Action_WithOneDeletedEntry_ShouldNotCountThatDeletedEntryOnSecondRun SutLcmToMongo.Run(lfProj); Guid entryGuid = Guid.Parse(TestEntryGuidStr); - LfLexEntry entry = _conn.GetLfLexEntryByGuid(entryGuid); + LfLexEntry entry = _conn.GetLfLexEntryByGuid(lfProj, entryGuid); entry.IsDeleted = true; _conn.UpdateMockLfLexEntry(entry); @@ -557,7 +557,7 @@ public void Action_RunTwiceWithTheSameEntryModifiedEachTime_ShouldCountTwoModifi SutLcmToMongo.Run(lfProj); Guid entryGuid = Guid.Parse(TestEntryGuidStr); - LfLexEntry entry = _conn.GetLfLexEntryByGuid(entryGuid); + LfLexEntry entry = _conn.GetLfLexEntryByGuid(lfProj, entryGuid); LcmCache cache = lfProj.FieldWorksProject.Cache; string vernacularWS = cache.LanguageProject.DefaultVernacularWritingSystem.Id; string changedLexeme = "modified lexeme for this test"; @@ -606,7 +606,7 @@ public void Action_RunTwiceWithTheSameEntryDeletedEachTime_ShouldCountJustOneDel SutLcmToMongo.Run(lfProj); Guid entryGuid = Guid.Parse(TestEntryGuidStr); - LfLexEntry entry = _conn.GetLfLexEntryByGuid(entryGuid); + LfLexEntry entry = _conn.GetLfLexEntryByGuid(lfProj, entryGuid); entry.IsDeleted = true; _conn.UpdateMockLfLexEntry(entry); @@ -621,7 +621,7 @@ public void Action_RunTwiceWithTheSameEntryDeletedEachTime_ShouldCountJustOneDel Assert.That(LfMergeBridgeServices.FormatCommitMessageForLfMerge(_counts.Added, _counts.Modified, _counts.Deleted), Is.EqualTo("Language Forge: 1 entry deleted")); - entry = _conn.GetLfLexEntryByGuid(entryGuid); + entry = _conn.GetLfLexEntryByGuid(lfProj, entryGuid); entry.IsDeleted = true; _conn.UpdateMockLfLexEntry(entry); @@ -689,7 +689,7 @@ public void Run_CustomMultiListRefTest(int whichSense, params string[] desiredKe SutLcmToMongo.Run(lfProj); Guid entryGuid = Guid.Parse(TestEntryGuidStr); - LfLexEntry entry = _conn.GetLfLexEntryByGuid(entryGuid); + LfLexEntry entry = _conn.GetLfLexEntryByGuid(lfProj, entryGuid); LfSense sense = entry.Senses[whichSense]; SetCustomMultiOptionList(sense, "customField_senses_Cust_Multi_ListRef", desiredKeys); entry.AuthorInfo = new LfAuthorInfo(); diff --git a/src/LfMerge.Core.Tests/SRTestEnvironment.cs b/src/LfMerge.Core.Tests/SRTestEnvironment.cs index 350321ed..1f51024a 100644 --- a/src/LfMerge.Core.Tests/SRTestEnvironment.cs +++ b/src/LfMerge.Core.Tests/SRTestEnvironment.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks; +using Autofac; using BirdMessenger; using BirdMessenger.Collections; using GraphQL; @@ -12,7 +13,9 @@ using GraphQL.Client.Serializer.SystemTextJson; using LfMerge.Core.FieldWorks; using LfMerge.Core.Logging; +using LfMerge.Core.MongoConnector; using NUnit.Framework; +using SIL.CommandLineProcessing; using SIL.TestUtilities; namespace LfMerge.Core.Tests @@ -39,6 +42,7 @@ public class SRTestEnvironment : TestEnvironment private bool AlreadyLoggedIn = false; private TemporaryFolder TempFolder { get; init; } private Lazy LazyGqlClient { get; init; } + private string MongoContainerId { get; set; } public GraphQLHttpClient GqlClient => LazyGqlClient.Value; public SRTestEnvironment(TemporaryFolder? tempFolder = null) @@ -51,6 +55,54 @@ public SRTestEnvironment(TemporaryFolder? tempFolder = null) Settings.CommitWhenDone = true; // For SR tests specifically, we *do* want changes to .fwdata files to be persisted } + override protected void RegisterMongoConnection(ContainerBuilder builder) + { + // E2E tests want a real Mogno connection + builder.RegisterType().As().SingleInstance(); + } + + public void LaunchMongo() + { + if (MongoContainerId is null) + { + var result = CommandLineRunner.Run("docker", "run -p 27017 -d mongo:6", ".", 30, NullProgress); + MongoContainerId = result.StandardOutput?.TrimEnd(); + Console.WriteLine($"Launched Mongo container {MongoContainerId ?? "(null) - something went wrong"}"); + if (MongoContainerId != null) + { + result = CommandLineRunner.Run("docker", $"port {MongoContainerId} 27017", ".", 30, NullProgress); + var hostAndPort = result.StandardOutput?.TrimEnd(); + var parts = hostAndPort.Contains(':') ? hostAndPort.Split(':') : null; + if (parts is not null && parts.Length == 2) { + Settings.MongoHostname = parts[0].Replace("0.0.0.0", "localhost"); + Settings.MongoPort = parts[1]; + } + Console.WriteLine($"Mongo is listening on port {parts?[1] ?? hostAndPort + " (oops, mongoPort was null)"}"); + } + } + } + + public void StopMongo() + { + if (MongoContainerId is not null) + { + Console.WriteLine($"Stopping Mongo container {MongoContainerId} ..."); + CommandLineRunner.Run("docker", $"stop {MongoContainerId}", ".", 30, NullProgress); + CommandLineRunner.Run("docker", $"rm {MongoContainerId}", ".", 30, NullProgress); + Console.WriteLine($"Stopped Mongo container {MongoContainerId} ..."); + MongoContainerId = null; + } + } + + protected override void Dispose(bool disposing) + { + if (_disposed) return; + if (disposing) { + StopMongo(); + } + base.Dispose(disposing); + } + public async Task Login() { if (AlreadyLoggedIn) return; diff --git a/src/LfMerge.Core.Tests/TestDoubles.cs b/src/LfMerge.Core.Tests/TestDoubles.cs index 86eec1fb..c22e5bfb 100644 --- a/src/LfMerge.Core.Tests/TestDoubles.cs +++ b/src/LfMerge.Core.Tests/TestDoubles.cs @@ -211,7 +211,7 @@ public IEnumerable GetLfLexEntries() return new List(_storedLfLexEntries.Values.Select(entry => DeepCopy(entry))); } - public LfLexEntry GetLfLexEntryByGuid(Guid key) + public LfLexEntry GetLfLexEntryByGuid(ILfProject _project, Guid key) { LfLexEntry result; if (_storedLfLexEntries.TryGetValue(key, out result)) diff --git a/src/LfMerge.Core.Tests/TestEnvironment.cs b/src/LfMerge.Core.Tests/TestEnvironment.cs index 4fbff2ca..bb80ee5a 100644 --- a/src/LfMerge.Core.Tests/TestEnvironment.cs +++ b/src/LfMerge.Core.Tests/TestEnvironment.cs @@ -25,6 +25,7 @@ public class TestEnvironment : IDisposable protected readonly TemporaryFolder _languageForgeServerFolder; private readonly bool _resetLfProjectsDuringCleanup; private readonly bool _releaseSingletons; + protected bool _disposed; public bool DeleteTempFolderDuringCleanup { get; set; } = true; public LfMergeSettings Settings; private readonly MongoConnectionDouble _mongoConnection; @@ -57,6 +58,8 @@ public TestEnvironment(bool registerSettingsModelDouble = true, _releaseSingletons = !SingletonsContainer.Contains(); } + ~TestEnvironment() => Dispose(false); + private string TestName { get @@ -79,8 +82,7 @@ private ContainerBuilder RegisterTypes(bool registerSettingsModel, containerBuilder.RegisterType().SingleInstance().As() .WithParameter(new TypedParameter(typeof(string), TestName)); - - containerBuilder.RegisterType().As().SingleInstance(); + RegisterMongoConnection(containerBuilder); if (registerSettingsModel) { @@ -100,20 +102,33 @@ private ContainerBuilder RegisterTypes(bool registerSettingsModel, return containerBuilder; } + protected virtual void RegisterMongoConnection(ContainerBuilder builder) + { + builder.RegisterType().As().SingleInstance(); + } + public void Dispose() { - _mongoConnection?.Reset(); - - MainClass.Container?.Dispose(); - MainClass.Container = null; - if (_resetLfProjectsDuringCleanup) - LanguageForgeProjectAccessor.Reset(); - if (DeleteTempFolderDuringCleanup) - _languageForgeServerFolder?.Dispose(); - Settings = null; - if (_releaseSingletons) - SingletonsContainer.Release(); + Dispose(true); + GC.SuppressFinalize(this); + } + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + if (disposing) { + _mongoConnection?.Reset(); + + MainClass.Container?.Dispose(); + MainClass.Container = null; + if (_resetLfProjectsDuringCleanup) + LanguageForgeProjectAccessor.Reset(); + if (DeleteTempFolderDuringCleanup) + _languageForgeServerFolder?.Dispose(); + Settings = null; + if (_releaseSingletons) + SingletonsContainer.Release(); + } Environment.SetEnvironmentVariable("FW_CommonAppData", null); } diff --git a/src/LfMerge.Core/MongoConnector/IMongoConnection.cs b/src/LfMerge.Core/MongoConnector/IMongoConnection.cs index 2f8d27dc..c7e11988 100644 --- a/src/LfMerge.Core/MongoConnector/IMongoConnection.cs +++ b/src/LfMerge.Core/MongoConnector/IMongoConnection.cs @@ -19,6 +19,7 @@ public interface IMongoConnection IEnumerable GetRecords(ILfProject project, string collectionName); LfOptionList GetLfOptionListByCode(ILfProject project, string listCode); long LexEntryCount(ILfProject project); + LfLexEntry GetLfLexEntryByGuid(ILfProject project, Guid key); Dictionary GetAllModifiedDatesForEntries(ILfProject project); bool UpdateRecord(ILfProject project, LfLexEntry data); bool UpdateRecord(ILfProject project, LfOptionList data, string listCode); diff --git a/src/LfMerge.Core/MongoConnector/MongoConnection.cs b/src/LfMerge.Core/MongoConnector/MongoConnection.cs index 6ef2b87a..443338da 100644 --- a/src/LfMerge.Core/MongoConnector/MongoConnection.cs +++ b/src/LfMerge.Core/MongoConnector/MongoConnection.cs @@ -88,7 +88,8 @@ private MongoClient GetNewConnection() var clientSettings = MongoClientSettings.FromConnectionString(connectionString); // clientSettings.WriteConcern = WriteConcern.WMajority; // If increasing the wait queue size still doesn't help, try this as well clientSettings.WaitQueueSize = 50000; - clientSettings.Server = new MongoServerAddress(Settings.MongoHostname, Settings.MongoPort); + var mongoPort = int.TryParse(Settings.MongoPort, out var port) ? port : DefaultLfMergeSettings.MongoPort; + clientSettings.Server = new MongoServerAddress(Settings.MongoHostname, mongoPort); return new MongoClient(clientSettings); } @@ -148,6 +149,13 @@ public long LexEntryCount(ILfProject project) return lexicon.Count(allEntries); } + public LfLexEntry GetLfLexEntryByGuid(ILfProject project, Guid key) + { + IMongoDatabase db = GetProjectDatabase(project); + IMongoCollection collection = db.GetCollection(MagicStrings.LfCollectionNameForLexicon); + return collection.Find(entry => entry.Guid == key).First(); + } + public Dictionary GetAllModifiedDatesForEntries(ILfProject project) { IMongoDatabase db = GetProjectDatabase(project); diff --git a/src/LfMerge.Core/Settings/DefaultLfMergeSettings.cs b/src/LfMerge.Core/Settings/DefaultLfMergeSettings.cs index 8f8b0dc3..b71d31ce 100644 --- a/src/LfMerge.Core/Settings/DefaultLfMergeSettings.cs +++ b/src/LfMerge.Core/Settings/DefaultLfMergeSettings.cs @@ -9,7 +9,7 @@ public static class DefaultLfMergeSettings public const string WebworkDir = "webwork"; public const string TemplatesDir = "Templates"; public const string MongoHostname = "localhost"; - public const int MongoPort = 27017; + public const int MongoPort = 27017; // This should stay an int even though the LfMergeSettings value is a string public const string MongoMainDatabaseName = "scriptureforge"; public const string MongoDatabaseNamePrefix = "sf_"; public const string MongoAuthSource = "admin"; diff --git a/src/LfMerge.Core/Settings/LfMergeSettings.cs b/src/LfMerge.Core/Settings/LfMergeSettings.cs index 9f253ce3..2b84d4b9 100644 --- a/src/LfMerge.Core/Settings/LfMergeSettings.cs +++ b/src/LfMerge.Core/Settings/LfMergeSettings.cs @@ -30,18 +30,16 @@ public string TemplatesDir { } } - public string MongoHostname => Environment.GetEnvironmentVariable(MagicStrings.SettingsEnvVar_MongoHostname) ?? DefaultLfMergeSettings.MongoHostname; + private string _mongoHostname; + public string MongoHostname { + get => _mongoHostname ?? Environment.GetEnvironmentVariable(MagicStrings.SettingsEnvVar_MongoHostname) ?? DefaultLfMergeSettings.MongoHostname; + set => _mongoHostname = value; + } - public int MongoPort { - get { - string _mongoPortStr = Environment.GetEnvironmentVariable(MagicStrings.SettingsEnvVar_MongoPort); - if (_mongoPortStr == null) _mongoPortStr = ""; - if (Int32.TryParse(_mongoPortStr, out int _mongoPort)) { - return _mongoPort; - } else { - return DefaultLfMergeSettings.MongoPort; - } - } + private string _mongoPort; + public string MongoPort { + get => Environment.GetEnvironmentVariable(MagicStrings.SettingsEnvVar_MongoPort) ?? DefaultLfMergeSettings.MongoPort.ToString(); + set => _mongoPort = value; } public string MongoAuthSource => Environment.GetEnvironmentVariable(MagicStrings.SettingsEnvVar_MongoAuthSource) ?? DefaultLfMergeSettings.MongoAuthSource; @@ -74,10 +72,10 @@ public string LanguageDepotRepoUri { public bool CommitWhenDone { get; internal set; } - public string MongoDbHostNameAndPort { get { return String.Format("{0}:{1}", MongoHostname, MongoPort.ToString()); } } + public string MongoDbHostNameAndPort { get { return String.Format("{0}:{1}", MongoHostname, MongoPort); } } public string MongoDbHostPortAndAuth => string.IsNullOrEmpty(MongoUsername) || string.IsNullOrEmpty(MongoPassword) - ? string.Format("{0}:{1}", MongoHostname, MongoPort.ToString()) - : string.Format("{0}:{1}@{2}:{3}", EncodedUsername, EncodedPassword, MongoHostname, MongoPort.ToString()); + ? string.Format("{0}:{1}", MongoHostname, MongoPort) + : string.Format("{0}:{1}@{2}:{3}", EncodedUsername, EncodedPassword, MongoHostname, MongoPort); private string QueueDirectory { get; set; } From 833c39fcd3c34398b779e62690e4ea229d51bd74 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 23 Aug 2024 08:50:19 +0700 Subject: [PATCH 2/9] Skip updating project record if there is none In E2E tests, the SetInputSystems call would fail because there is no existing project record in the freshly-created, empty Mongo instance. We don't want to change LfMerge to create the project record, because the Language Forge code is responsible for doing that. So instead we simply skip the update if the project record doesn't exist, allowing E2E tests to pass while not changing the behavior of real code. --- src/LfMerge.Core/MongoConnector/MongoConnection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LfMerge.Core/MongoConnector/MongoConnection.cs b/src/LfMerge.Core/MongoConnector/MongoConnection.cs index 443338da..e9c55da5 100644 --- a/src/LfMerge.Core/MongoConnector/MongoConnection.cs +++ b/src/LfMerge.Core/MongoConnector/MongoConnection.cs @@ -562,7 +562,7 @@ public bool SetInputSystems(ILfProject project, Dictionary Date: Fri, 23 Aug 2024 09:03:02 +0700 Subject: [PATCH 3/9] Rename overly-long TestEnv property --- src/LfMerge.Core.Tests/E2E/E2ETestBase.cs | 2 +- src/LfMerge.Core.Tests/TestEnvironment.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/LfMerge.Core.Tests/E2E/E2ETestBase.cs b/src/LfMerge.Core.Tests/E2E/E2ETestBase.cs index ca3c9327..28f54b1c 100644 --- a/src/LfMerge.Core.Tests/E2E/E2ETestBase.cs +++ b/src/LfMerge.Core.Tests/E2E/E2ETestBase.cs @@ -98,7 +98,7 @@ public async Task TestTeardown() var outcome = TestContext.CurrentContext.Result.Outcome; var success = outcome == ResultState.Success || outcome == ResultState.Ignored; // Only delete temp folder if test passed, otherwise we'll want to leave it in place for post-test investigation - TestEnv.DeleteTempFolderDuringCleanup = success; + TestEnv.CleanUpTestData = success; // On failure, also leave LexBox project(s) in place for post-test investigation, even though this might tend to clutter things up a little if (success) { foreach (var projId in ProjectIdsToDelete) { diff --git a/src/LfMerge.Core.Tests/TestEnvironment.cs b/src/LfMerge.Core.Tests/TestEnvironment.cs index bb80ee5a..3121144a 100644 --- a/src/LfMerge.Core.Tests/TestEnvironment.cs +++ b/src/LfMerge.Core.Tests/TestEnvironment.cs @@ -26,7 +26,7 @@ public class TestEnvironment : IDisposable private readonly bool _resetLfProjectsDuringCleanup; private readonly bool _releaseSingletons; protected bool _disposed; - public bool DeleteTempFolderDuringCleanup { get; set; } = true; + public bool CleanUpTestData { get; set; } = true; public LfMergeSettings Settings; private readonly MongoConnectionDouble _mongoConnection; public ILogger Logger => MainClass.Logger; @@ -123,7 +123,7 @@ protected virtual void Dispose(bool disposing) MainClass.Container = null; if (_resetLfProjectsDuringCleanup) LanguageForgeProjectAccessor.Reset(); - if (DeleteTempFolderDuringCleanup) + if (CleanUpTestData) _languageForgeServerFolder?.Dispose(); Settings = null; if (_releaseSingletons) From 4bfb265214f015e829d2eb195c52edcf72c88b39 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 23 Aug 2024 09:03:43 +0700 Subject: [PATCH 4/9] Leave MongoDB container around if test failed Also print instructions for deleting it once post-failure investigation is completed. --- src/LfMerge.Core.Tests/E2E/E2ETestBase.cs | 1 - src/LfMerge.Core.Tests/SRTestEnvironment.cs | 12 +++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/LfMerge.Core.Tests/E2E/E2ETestBase.cs b/src/LfMerge.Core.Tests/E2E/E2ETestBase.cs index 28f54b1c..186ff09f 100644 --- a/src/LfMerge.Core.Tests/E2E/E2ETestBase.cs +++ b/src/LfMerge.Core.Tests/E2E/E2ETestBase.cs @@ -81,7 +81,6 @@ public async Task TestSetup() Assert.Ignore("Can't run E2E tests without a copy of LexBox to test against. Please either launch LexBox on localhost port 80, or set the appropriate environment variables to point to a running copy of LexBox."); } await TestEnv.Login(); - Console.WriteLine("About to launch Mongo..."); TestEnv.LaunchMongo(); MagicStrings.SetMinimalModelVersion(LcmCache.ModelVersion); diff --git a/src/LfMerge.Core.Tests/SRTestEnvironment.cs b/src/LfMerge.Core.Tests/SRTestEnvironment.cs index 1f51024a..34edff62 100644 --- a/src/LfMerge.Core.Tests/SRTestEnvironment.cs +++ b/src/LfMerge.Core.Tests/SRTestEnvironment.cs @@ -67,7 +67,6 @@ public void LaunchMongo() { var result = CommandLineRunner.Run("docker", "run -p 27017 -d mongo:6", ".", 30, NullProgress); MongoContainerId = result.StandardOutput?.TrimEnd(); - Console.WriteLine($"Launched Mongo container {MongoContainerId ?? "(null) - something went wrong"}"); if (MongoContainerId != null) { result = CommandLineRunner.Run("docker", $"port {MongoContainerId} 27017", ".", 30, NullProgress); @@ -77,7 +76,6 @@ public void LaunchMongo() Settings.MongoHostname = parts[0].Replace("0.0.0.0", "localhost"); Settings.MongoPort = parts[1]; } - Console.WriteLine($"Mongo is listening on port {parts?[1] ?? hostAndPort + " (oops, mongoPort was null)"}"); } } } @@ -86,10 +84,8 @@ public void StopMongo() { if (MongoContainerId is not null) { - Console.WriteLine($"Stopping Mongo container {MongoContainerId} ..."); CommandLineRunner.Run("docker", $"stop {MongoContainerId}", ".", 30, NullProgress); CommandLineRunner.Run("docker", $"rm {MongoContainerId}", ".", 30, NullProgress); - Console.WriteLine($"Stopped Mongo container {MongoContainerId} ..."); MongoContainerId = null; } } @@ -98,7 +94,13 @@ protected override void Dispose(bool disposing) { if (_disposed) return; if (disposing) { - StopMongo(); + if (CleanUpTestData) { + StopMongo(); + } else { + Console.WriteLine($"Leaving Mongo container {MongoContainerId} around to examine data on failed test."); + Console.WriteLine($"It is listening on {Settings.MongoDbHostNameAndPort}"); + Console.WriteLine($"To delete it, run `docker stop {MongoContainerId} ; docker rm {MongoContainerId}`."); + } } base.Dispose(disposing); } From b1c483e6c7b425e32529c4b1cee8f9135a1f35d4 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Mon, 26 Aug 2024 08:45:09 +0700 Subject: [PATCH 5/9] Only print Mongo container ID if it exists --- src/LfMerge.Core.Tests/SRTestEnvironment.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/LfMerge.Core.Tests/SRTestEnvironment.cs b/src/LfMerge.Core.Tests/SRTestEnvironment.cs index 34edff62..86a1286f 100644 --- a/src/LfMerge.Core.Tests/SRTestEnvironment.cs +++ b/src/LfMerge.Core.Tests/SRTestEnvironment.cs @@ -97,9 +97,11 @@ protected override void Dispose(bool disposing) if (CleanUpTestData) { StopMongo(); } else { - Console.WriteLine($"Leaving Mongo container {MongoContainerId} around to examine data on failed test."); - Console.WriteLine($"It is listening on {Settings.MongoDbHostNameAndPort}"); - Console.WriteLine($"To delete it, run `docker stop {MongoContainerId} ; docker rm {MongoContainerId}`."); + if (MongoContainerId is not null) { + Console.WriteLine($"Leaving Mongo container {MongoContainerId} around to examine data on failed test."); + Console.WriteLine($"It is listening on {Settings.MongoDbHostNameAndPort}"); + Console.WriteLine($"To delete it, run `docker stop {MongoContainerId} ; docker rm {MongoContainerId}`."); + } } } base.Dispose(disposing); From 46caa5089acd96830fdc10521b38c2d34e79d059 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Mon, 26 Aug 2024 14:40:22 +0700 Subject: [PATCH 6/9] Fix typo in MongoPort settings --- src/LfMerge.Core/Settings/LfMergeSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LfMerge.Core/Settings/LfMergeSettings.cs b/src/LfMerge.Core/Settings/LfMergeSettings.cs index 2b84d4b9..4177551c 100644 --- a/src/LfMerge.Core/Settings/LfMergeSettings.cs +++ b/src/LfMerge.Core/Settings/LfMergeSettings.cs @@ -38,7 +38,7 @@ public string MongoHostname { private string _mongoPort; public string MongoPort { - get => Environment.GetEnvironmentVariable(MagicStrings.SettingsEnvVar_MongoPort) ?? DefaultLfMergeSettings.MongoPort.ToString(); + get => _mongoPort ?? Environment.GetEnvironmentVariable(MagicStrings.SettingsEnvVar_MongoPort) ?? DefaultLfMergeSettings.MongoPort.ToString(); set => _mongoPort = value; } From 48f4608ffac714ca6d319142fe76fec1497ee8ac Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 27 Aug 2024 13:29:55 +0700 Subject: [PATCH 7/9] Address review comments * Always stop Mongo container * Throw if Mongo container fails to start * Simpler MongoDbHostAndPort property definition --- src/LfMerge.Core.Tests/SRTestEnvironment.cs | 30 ++++++++------------ src/LfMerge.Core/Settings/LfMergeSettings.cs | 2 +- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/LfMerge.Core.Tests/SRTestEnvironment.cs b/src/LfMerge.Core.Tests/SRTestEnvironment.cs index 86a1286f..9a159e85 100644 --- a/src/LfMerge.Core.Tests/SRTestEnvironment.cs +++ b/src/LfMerge.Core.Tests/SRTestEnvironment.cs @@ -67,15 +67,17 @@ public void LaunchMongo() { var result = CommandLineRunner.Run("docker", "run -p 27017 -d mongo:6", ".", 30, NullProgress); MongoContainerId = result.StandardOutput?.TrimEnd(); - if (MongoContainerId != null) - { - result = CommandLineRunner.Run("docker", $"port {MongoContainerId} 27017", ".", 30, NullProgress); - var hostAndPort = result.StandardOutput?.TrimEnd(); - var parts = hostAndPort.Contains(':') ? hostAndPort.Split(':') : null; - if (parts is not null && parts.Length == 2) { - Settings.MongoHostname = parts[0].Replace("0.0.0.0", "localhost"); - Settings.MongoPort = parts[1]; - } + if (string.IsNullOrEmpty(MongoContainerId)) { + throw new InvalidOperationException("Mongo container failed to start, aborting test"); + } + result = CommandLineRunner.Run("docker", $"port {MongoContainerId} 27017", ".", 30, NullProgress); + var hostAndPort = result.StandardOutput?.TrimEnd(); + var parts = hostAndPort.Contains(':') ? hostAndPort.Split(':') : null; + if (parts is not null && parts.Length == 2) { + Settings.MongoHostname = parts[0].Replace("0.0.0.0", "localhost"); + Settings.MongoPort = parts[1]; + } else { + throw new InvalidOperationException($"Mongo container port {hostAndPort} could not be parsed, test will not be able to proceed"); } } } @@ -94,15 +96,7 @@ protected override void Dispose(bool disposing) { if (_disposed) return; if (disposing) { - if (CleanUpTestData) { - StopMongo(); - } else { - if (MongoContainerId is not null) { - Console.WriteLine($"Leaving Mongo container {MongoContainerId} around to examine data on failed test."); - Console.WriteLine($"It is listening on {Settings.MongoDbHostNameAndPort}"); - Console.WriteLine($"To delete it, run `docker stop {MongoContainerId} ; docker rm {MongoContainerId}`."); - } - } + StopMongo(); } base.Dispose(disposing); } diff --git a/src/LfMerge.Core/Settings/LfMergeSettings.cs b/src/LfMerge.Core/Settings/LfMergeSettings.cs index 4177551c..9d5327f9 100644 --- a/src/LfMerge.Core/Settings/LfMergeSettings.cs +++ b/src/LfMerge.Core/Settings/LfMergeSettings.cs @@ -72,7 +72,7 @@ public string LanguageDepotRepoUri { public bool CommitWhenDone { get; internal set; } - public string MongoDbHostNameAndPort { get { return String.Format("{0}:{1}", MongoHostname, MongoPort); } } + public string MongoDbHostNameAndPort => $"{MongoHostname}:{MongoPort}"; public string MongoDbHostPortAndAuth => string.IsNullOrEmpty(MongoUsername) || string.IsNullOrEmpty(MongoPassword) ? string.Format("{0}:{1}", MongoHostname, MongoPort) : string.Format("{0}:{1}@{2}:{3}", EncodedUsername, EncodedPassword, MongoHostname, MongoPort); From 563a0a21e1020db7ba0af0961878d08d47c90c06 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 27 Aug 2024 13:40:01 +0700 Subject: [PATCH 8/9] Add flag to leave Mongo container up on failure Unless the flag is set (to a non-empty value) the Mongo container will be torn down even if tests have failed. --- src/LfMerge.Core.Tests/SRTestEnvironment.cs | 9 ++++++++- src/LfMerge.Core/MagicStrings.cs | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/LfMerge.Core.Tests/SRTestEnvironment.cs b/src/LfMerge.Core.Tests/SRTestEnvironment.cs index 9a159e85..d4cf8363 100644 --- a/src/LfMerge.Core.Tests/SRTestEnvironment.cs +++ b/src/LfMerge.Core.Tests/SRTestEnvironment.cs @@ -96,7 +96,14 @@ protected override void Dispose(bool disposing) { if (_disposed) return; if (disposing) { - StopMongo(); + // Set the LFMERGE_E2E_LEAVE_MONGO_CONTAINER_RUNNING_ON_FAILURE env var to a non-empty value if you want Mongo container to NOT be torn down on test failure + if (CleanUpTestData || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(MagicStrings.EnvVar_E2E_LeaveMongoContainerRunningOnFailure))) { + StopMongo(); + } else { + Console.WriteLine($"Leaving Mongo container {MongoContainerId} around to examine data on failed test."); + Console.WriteLine($"It is listening on {Settings.MongoDbHostNameAndPort}"); + Console.WriteLine($"To delete it, run `docker stop {MongoContainerId} ; docker rm {MongoContainerId}`."); + } } base.Dispose(disposing); } diff --git a/src/LfMerge.Core/MagicStrings.cs b/src/LfMerge.Core/MagicStrings.cs index f0966513..eebf34f4 100644 --- a/src/LfMerge.Core/MagicStrings.cs +++ b/src/LfMerge.Core/MagicStrings.cs @@ -33,6 +33,7 @@ static MagicStrings() public const string EnvVar_LanguageDepotUriPort = "LFMERGE_LANGUAGE_DEPOT_HG_PORT"; public const string EnvVar_TrustToken = "LANGUAGE_DEPOT_TRUST_TOKEN"; public const string EnvVar_HgUsername = "LANGUAGE_DEPOT_HG_USERNAME"; + public const string EnvVar_E2E_LeaveMongoContainerRunningOnFailure = "LFMERGE_E2E_LEAVE_MONGO_CONTAINER_RUNNING_ON_FAILURE"; public static Dictionary LcmOptionlistNames = new Dictionary() { From 95e29eaf8883701b4267b503b965d83c8b210557 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 28 Aug 2024 08:06:21 +0700 Subject: [PATCH 9/9] Also allow "false" or "0" values to mean false --- src/LfMerge.Core.Tests/SRTestEnvironment.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/LfMerge.Core.Tests/SRTestEnvironment.cs b/src/LfMerge.Core.Tests/SRTestEnvironment.cs index d4cf8363..1688558d 100644 --- a/src/LfMerge.Core.Tests/SRTestEnvironment.cs +++ b/src/LfMerge.Core.Tests/SRTestEnvironment.cs @@ -92,12 +92,19 @@ public void StopMongo() } } + private bool ShouldStopMongoOnFailure() + { + // Mongo container will be torn down on test failure unless LFMERGE_E2E_LEAVE_MONGO_CONTAINER_RUNNING_ON_FAILURE + // is set to a non-empty value (except "false" or "0", which mean the same as leaving it empty) + var envVar = Environment.GetEnvironmentVariable(MagicStrings.EnvVar_E2E_LeaveMongoContainerRunningOnFailure)?.Trim(); + return string.IsNullOrEmpty(envVar) || envVar == "false" || envVar == "0"; + } + protected override void Dispose(bool disposing) { if (_disposed) return; if (disposing) { - // Set the LFMERGE_E2E_LEAVE_MONGO_CONTAINER_RUNNING_ON_FAILURE env var to a non-empty value if you want Mongo container to NOT be torn down on test failure - if (CleanUpTestData || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(MagicStrings.EnvVar_E2E_LeaveMongoContainerRunningOnFailure))) { + if (CleanUpTestData || ShouldStopMongoOnFailure()) { StopMongo(); } else { Console.WriteLine($"Leaving Mongo container {MongoContainerId} around to examine data on failed test.");