Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions src/SIL.LCModel/DomainImpl/OverridesLing_MoClasses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4048,23 +4048,24 @@ protected override void SetDefaultValuesAfterInit()
}

/// <summary>
/// Gives an object an opportunity to do any class-specific side-effect work when it has
/// been cloned with DomainServices.CopyObject. In this case, the creation of a MoAffixProcess
/// adds default initial values that are not wanted in the cloned copy, so PostClone()
/// removes them.
/// Removes initialization defaults from this affix process's clone.
/// </summary>
/// <param name="copyMap">The map from source object identifiers to their clones.</param>
public override void PostClone(Dictionary<int, ICmObject> copyMap)
{
foreach (var cmObject in copyMap.Values)
{
var clonedProcess = cmObject as IMoAffixProcess;
if (clonedProcess == null)
return;
if (clonedProcess.InputOS.Count > 1)
clonedProcess.InputOS.RemoveAt(0);
if (clonedProcess.OutputOS.Count > 1)
clonedProcess.OutputOS.RemoveAt(0);
}
// The map can contain sibling clones; this source's identifier selects its own clone.
if (!copyMap.TryGetValue(Hvo, out var clone) || !(clone is IMoAffixProcess clonedProcess))
return;

// Factory-created clones contain leading defaults in addition to the source content.
var surplusInputs = clonedProcess.InputOS.Count - InputOS.Count;
for (var i = 0; i < surplusInputs; i++)
clonedProcess.InputOS.RemoveAt(0);

// Removing a default input can also remove its referenced default output.
var surplusOutputs = clonedProcess.OutputOS.Count - OutputOS.Count;
for (var i = 0; i < surplusOutputs; i++)
clonedProcess.OutputOS.RemoveAt(0);
}
/// <summary>
/// Gets all of the feature constraints in this rule.
Expand Down
126 changes: 126 additions & 0 deletions tests/SIL.LCModel.Tests/DomainImpl/AffixProcessCloneRoundTripTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) 2026 SIL International
// This software is licensed under the LGPL, version 2.1 or later
// (http://www.gnu.org/licenses/lgpl-2.1.html)

using System;
using System.IO;
using NUnit.Framework;
using SIL.LCModel.Core.Text;
using SIL.LCModel.Infrastructure;
using SIL.TestUtilities;

namespace SIL.LCModel.DomainImpl
{
/// <summary>
/// Verifies affix-process clones using a file-backed cache.
/// </summary>
[TestFixture]
public class AffixProcessCloneRoundTripTests
{
private TemporaryFolder m_projectsFolder;
private ILcmDirectories m_lcmDirectories;

/// <summary />
[SetUp]
public void TestSetup()
{
m_projectsFolder = new TemporaryFolder("AffixProcessCloneRoundTrip" + Guid.NewGuid().ToString("N"));
m_lcmDirectories = new TestLcmDirectories(m_projectsFolder.Path);
}

/// <summary />
[TearDown]
public void TestTeardown()
{
m_projectsFolder.Dispose();
}

/// <summary>
/// Verifies a moved affix-process clone retains its rule after saving and reloading.
/// </summary>
[Test]
public void MoveSenseToCopy_AffixProcessClone_SurvivesSaveAndReload()
{
var projectName = "AffixProcessCloneRoundTrip" + new Random().Next(1000000);
var path = Path.Combine(m_projectsFolder.Path, LcmFileHelper.GetXmlDataFileName(projectName));
var projectId = new TestProjectId(BackendProviderType.kXMLWithMemoryOnlyWsMgr, path);

Guid newEntryGuid;
int expectedInputCount = 0;
int expectedOutputCount = 0;

using (var cache = LcmCache.CreateCacheWithNewBlankLangProj(projectId, "en", "fr", "en",
new DummyLcmUI(), m_lcmDirectories, new LcmSettings()))
{
ILexEntry entry = null;
ILexSense senseToMove = null;
UndoableUnitOfWorkHelper.Do("doit", "undoit", cache.ActionHandlerAccessor, () =>
{
var ws = cache.DefaultVernWs;
var entryFactory = cache.ServiceLocator.GetInstance<ILexEntryFactory>();
var senseFactory = cache.ServiceLocator.GetInstance<ILexSenseFactory>();

entry = entryFactory.Create();
var process = cache.ServiceLocator.GetInstance<IMoAffixProcessFactory>().Create();
entry.LexemeFormOA = process;
process.Form.set_String(ws, TsStringUtils.MakeString("ed", ws));
process.MorphTypeRA = cache.ServiceLocator.GetInstance<IMoMorphTypeRepository>()
.GetObject(MoMorphTypeTags.kguidMorphSuffix);

// Distinct types make ordering and reference errors observable.
process.InputOS.Clear();
process.OutputOS.Clear();
var ctxt = cache.ServiceLocator.GetInstance<IPhSimpleContextNCFactory>().Create();
process.InputOS.Add(ctxt);
var var1 = cache.ServiceLocator.GetInstance<IPhVariableFactory>().Create();
process.InputOS.Add(var1);
var copy = cache.ServiceLocator.GetInstance<IMoCopyFromInputFactory>().Create();
process.OutputOS.Add(copy);
copy.ContentRA = ctxt;
var modify = cache.ServiceLocator.GetInstance<IMoModifyFromInputFactory>().Create();
process.OutputOS.Add(modify);
modify.ContentRA = var1;

expectedInputCount = process.InputOS.Count;
expectedOutputCount = process.OutputOS.Count;

var sense1 = senseFactory.Create();
entry.SensesOS.Add(sense1);
senseToMove = senseFactory.Create();
entry.SensesOS.Add(senseToMove);
});

entry.MoveSenseToCopy(senseToMove);
newEntryGuid = senseToMove.Entry.Guid;

cache.ServiceLocator.GetInstance<IUndoStackManager>().Save();
}

using (var reloaded = LcmCache.CreateCacheFromExistingData(projectId, "en", new DummyLcmUI(),
m_lcmDirectories, new LcmSettings(), new DummyProgressDlg()))
{
var newEntry = (ILexEntry)reloaded.ServiceLocator.GetObject(newEntryGuid);
var clonedProcess = newEntry.LexemeFormOA as IMoAffixProcess;
Assert.That(clonedProcess, Is.Not.Null, "reloaded clone should still be an affix process");

Assert.That(clonedProcess.InputOS.Count, Is.EqualTo(expectedInputCount),
"after save/reload, the clone's InputOS should match what was created, with no leaked " +
"default and no real content lost");
Assert.That(clonedProcess.OutputOS.Count, Is.EqualTo(expectedOutputCount),
"after save/reload, the clone's OutputOS should match what was created, with no leaked " +
"default and no real content lost");
Assert.That(clonedProcess.InputOS[0].ClassID, Is.EqualTo(PhSimpleContextNCTags.kClassId),
"first input after reload should be the real natural-class context, not a leaked default PhVariable");
Assert.That(clonedProcess.InputOS[1].ClassID, Is.EqualTo(PhVariableTags.kClassId));
Assert.That(clonedProcess.OutputOS[0].ClassID, Is.EqualTo(MoCopyFromInputTags.kClassId));
Assert.That(clonedProcess.OutputOS[1].ClassID, Is.EqualTo(MoModifyFromInputTags.kClassId));
var copy = (IMoCopyFromInput)clonedProcess.OutputOS[0];
var modify = (IMoModifyFromInput)clonedProcess.OutputOS[1];
Assert.That(copy.ContentRA, Is.SameAs(clonedProcess.InputOS[0]),
"reloaded copy mapping should reference the cloned natural-class input");
Assert.That(modify.ContentRA, Is.SameAs(clonedProcess.InputOS[1]),
"reloaded modify mapping should reference the cloned variable input");
}
}
}
}
Loading
Loading