From e100d6e7081741361216d54d84e08d2090588e32 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 7 Aug 2026 15:05:44 -0400 Subject: [PATCH] LT-22576b: Unit tests for set-phon-feats gate for user-def NCs Cover IsFeatureBasedNCNameUserDefined and RuleFormulaSlice.OnDisplayContextSetFeatures for user-named, auto-generated, unnamed, segment-based, iteration-wrapped, and empty-selection contexts. The test control overrides GetCmObject, the extension point concrete rule controls already implement, so the selection-to-context logic under test is the production code. Co-Authored-By: Claude Opus 5 (1M context) Change-Id: I5eb3da581ac087934f99e88e71cd31518ea69bce --- .../RuleFormulaSetFeaturesTests.cs | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaSetFeaturesTests.cs diff --git a/Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaSetFeaturesTests.cs b/Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaSetFeaturesTests.cs new file mode 100644 index 0000000000..0c6c3a7f40 --- /dev/null +++ b/Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaSetFeaturesTests.cs @@ -0,0 +1,302 @@ +// 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.Linq; +using System.Threading; +using NUnit.Framework; +using SIL.FieldWorks.Common.RootSites; +using SIL.LCModel; +using XCore; + +namespace SIL.FieldWorks.XWorks.MorphologyEditor +{ + /// + /// Tests the classification of the natural class under the cursor that decides whether the + /// "Set Phonological Features" rule-formula command is offered. + /// + [TestFixture] + [Apartment(ApartmentState.STA)] + public class RuleFormulaSetFeaturesTests : MemoryOnlyBackendProviderRestoredForEachTestTestBase + { + /// + /// Resolves the selection to a fixed object, the extension point concrete rule controls + /// implement, so the classification runs without a laid-out Views root box. + /// + private sealed class StubRuleFormulaControl : RuleFormulaControl + { + internal ICmObject ObjectUnderCursor { get; set; } + + protected override ICmObject GetCmObject(SelectionHelper sel, SelectionHelper.SelLimitType limit) + { + return ObjectUnderCursor; + } + } + + private StubRuleFormulaControl m_ruleFormulaControl; + private RuleFormulaSlice m_slice; + + public override void TestSetup() + { + base.TestSetup(); + m_ruleFormulaControl = new StubRuleFormulaControl(); + m_slice = new RuleFormulaSlice { Control = m_ruleFormulaControl }; + } + + public override void TestTearDown() + { + if (m_slice != null) + { + m_slice.Dispose(); + m_slice = null; + } + m_ruleFormulaControl = null; + base.TestTearDown(); + } + + private IPhSimpleContextNC MakeFeatureClassContext(IPhNCFeatures natClass) + { + var ctxt = Cache.ServiceLocator.GetInstance().Create(); + Cache.LanguageProject.PhonologicalDataOA.ContextsOS.Add(ctxt); + ctxt.FeatureStructureRA = natClass; + return ctxt; + } + + private IPhNCFeatures MakeFeatureClass(string name) + { + var natClass = Cache.ServiceLocator.GetInstance().Create(); + Cache.LanguageProject.PhonologicalDataOA.NaturalClassesOS.Add(natClass); + if (name != null) + natClass.Name.SetUserWritingSystem(name); + natClass.FeaturesOA = Cache.ServiceLocator.GetInstance().Create(); + return natClass; + } + + /// + /// Produces the natural class name that the rule formula controls write when the user sets + /// phonological features, so the tests stay tied to the shipped resource string. + /// + private static string AutoGeneratedName(string ruleName) + { + return string.Format(MEStrings.ksRuleNCFeatsName, ruleName); + } + + private IPhSimpleContextNC MakeSegmentClassContext() + { + var natClass = Cache.ServiceLocator.GetInstance().Create(); + Cache.LanguageProject.PhonologicalDataOA.NaturalClassesOS.Add(natClass); + natClass.Name.SetUserWritingSystem("Vowels"); + + var ctxt = Cache.ServiceLocator.GetInstance().Create(); + Cache.LanguageProject.PhonologicalDataOA.ContextsOS.Add(ctxt); + ctxt.FeatureStructureRA = natClass; + return ctxt; + } + + private IPhSimpleContextSeg MakePhonemeContext() + { + var phonData = Cache.LanguageProject.PhonologicalDataOA; + if (phonData.PhonemeSetsOS.Count == 0) + phonData.PhonemeSetsOS.Add(Cache.ServiceLocator.GetInstance().Create()); + var phoneme = Cache.ServiceLocator.GetInstance().Create(); + phonData.PhonemeSetsOS[0].PhonemesOC.Add(phoneme); + + var ctxt = Cache.ServiceLocator.GetInstance().Create(); + phonData.ContextsOS.Add(ctxt); + ctxt.FeatureStructureRA = phoneme; + return ctxt; + } + + private static UIItemDisplayProperties NewDisplayProperties() + { + return new UIItemDisplayProperties(null, "Set Phonological Features", true, null, true); + } + + private UIItemDisplayProperties AskSliceToDisplaySetFeatures() + { + UIItemDisplayProperties display = NewDisplayProperties(); + Assert.That(m_slice.OnDisplayContextSetFeatures(null, ref display), Is.True); + return display; + } + + [Test] + public void UserNamedFeatureClassIsUserDefined() + { + m_ruleFormulaControl.ObjectUnderCursor = MakeFeatureClassContext(MakeFeatureClass("Voiced obstruents")); + + Assert.That(m_ruleFormulaControl.IsFeatureBasedNCNameUserDefined(), Is.True); + } + + [Test] + public void FeatureClassGeneratedForARegularRuleIsNotUserDefined() + { + m_ruleFormulaControl.ObjectUnderCursor = + MakeFeatureClassContext(MakeFeatureClass(AutoGeneratedName("Intervocalic voicing"))); + + Assert.That(m_ruleFormulaControl.IsFeatureBasedNCNameUserDefined(), Is.False); + } + + [Test] + public void FeatureClassGeneratedForAnAffixRuleIsNotUserDefined() + { + // Affix rules substitute the affix form rather than a rule name, so only the fixed stub + // of the resource string is common to both kinds of generated name. + m_ruleFormulaControl.ObjectUnderCursor = MakeFeatureClassContext(MakeFeatureClass(AutoGeneratedName("-ed"))); + + Assert.That(m_ruleFormulaControl.IsFeatureBasedNCNameUserDefined(), Is.False); + } + + [Test] + public void UnnamedFeatureClassIsNotUserDefined() + { + m_ruleFormulaControl.ObjectUnderCursor = MakeFeatureClassContext(MakeFeatureClass(null)); + + Assert.That(m_ruleFormulaControl.IsFeatureBasedNCNameUserDefined(), Is.False); + } + + [Test] + public void FeatureClassWithAnEmptyNameIsNotUserDefined() + { + m_ruleFormulaControl.ObjectUnderCursor = MakeFeatureClassContext(MakeFeatureClass(string.Empty)); + + Assert.That(m_ruleFormulaControl.IsFeatureBasedNCNameUserDefined(), Is.False); + } + + [Test] + public void FeatureClassNamedOnlyInANonUserWritingSystemIsUserDefined() + { + // Generated names are always written in the user writing system, so a name that exists only + // elsewhere came from a person and the class must keep its features. + int otherWs = Cache.ServiceLocator.WritingSystems.AllWritingSystems + .Select(ws => ws.Handle).First(handle => handle != Cache.DefaultUserWs); + var natClass = MakeFeatureClass(null); + natClass.Name.set_String(otherWs, "Voiced obstruents"); + m_ruleFormulaControl.ObjectUnderCursor = MakeFeatureClassContext(natClass); + + Assert.That(m_ruleFormulaControl.IsFeatureBasedNCNameUserDefined(), Is.True); + } + + [Test] + public void FeatureClassNameMerelyResemblingTheGeneratedFormIsUserDefined() + { + m_ruleFormulaControl.ObjectUnderCursor = + MakeFeatureClassContext(MakeFeatureClass("Automatically created for the voicing rule")); + + Assert.That(m_ruleFormulaControl.IsFeatureBasedNCNameUserDefined(), Is.True); + } + + [Test] + public void FeatureClassInsideAnIterationContextIsClassifiedByItsMember() + { + var ncCtxt = MakeFeatureClassContext(MakeFeatureClass("Voiced obstruents")); + var iterCtxt = Cache.ServiceLocator.GetInstance().Create(); + Cache.LanguageProject.PhonologicalDataOA.ContextsOS.Add(iterCtxt); + iterCtxt.MemberRA = ncCtxt; + m_ruleFormulaControl.ObjectUnderCursor = iterCtxt; + + Assert.That(m_ruleFormulaControl.IsFeatureBasedNCNameUserDefined(), Is.True); + } + + [Test] + public void SegmentBasedNaturalClassIsNotReportedAsUserDefined() + { + m_ruleFormulaControl.ObjectUnderCursor = MakeSegmentClassContext(); + + Assert.That(m_ruleFormulaControl.IsFeatureBasedNCNameUserDefined(), Is.False); + } + + [Test] + public void PhonemeContextIsNotReportedAsUserDefined() + { + m_ruleFormulaControl.ObjectUnderCursor = MakePhonemeContext(); + + Assert.That(m_ruleFormulaControl.IsFeatureBasedNCNameUserDefined(), Is.False); + } + + [Test] + public void NaturalClassContextWithNoNaturalClassIsNotReportedAsUserDefined() + { + var ctxt = Cache.ServiceLocator.GetInstance().Create(); + Cache.LanguageProject.PhonologicalDataOA.ContextsOS.Add(ctxt); + m_ruleFormulaControl.ObjectUnderCursor = ctxt; + + Assert.That(m_ruleFormulaControl.IsFeatureBasedNCNameUserDefined(), Is.False); + } + + [Test] + public void EmptySelectionIsNotReportedAsUserDefined() + { + m_ruleFormulaControl.ObjectUnderCursor = null; + + Assert.That(m_ruleFormulaControl.IsFeatureBasedNCNameUserDefined(), Is.False); + } + + [Test] + public void SetFeaturesIsOfferedForAFeatureClassGeneratedForARule() + { + m_ruleFormulaControl.ObjectUnderCursor = + MakeFeatureClassContext(MakeFeatureClass(AutoGeneratedName("Intervocalic voicing"))); + + UIItemDisplayProperties display = AskSliceToDisplaySetFeatures(); + + Assert.That(display.Enabled, Is.True); + Assert.That(display.Visible, Is.True); + } + + [Test] + public void SetFeaturesIsOfferedForAnUnnamedFeatureClass() + { + m_ruleFormulaControl.ObjectUnderCursor = MakeFeatureClassContext(MakeFeatureClass(null)); + + UIItemDisplayProperties display = AskSliceToDisplaySetFeatures(); + + Assert.That(display.Enabled, Is.True); + Assert.That(display.Visible, Is.True); + } + + [Test] + public void SetFeaturesIsHiddenForAUserDefinedFeatureClass() + { + m_ruleFormulaControl.ObjectUnderCursor = MakeFeatureClassContext(MakeFeatureClass("Voiced obstruents")); + + UIItemDisplayProperties display = AskSliceToDisplaySetFeatures(); + + Assert.That(display.Enabled, Is.False); + Assert.That(display.Visible, Is.False); + } + + [Test] + public void SetFeaturesIsHiddenForASegmentBasedNaturalClass() + { + m_ruleFormulaControl.ObjectUnderCursor = MakeSegmentClassContext(); + + UIItemDisplayProperties display = AskSliceToDisplaySetFeatures(); + + Assert.That(display.Enabled, Is.False); + Assert.That(display.Visible, Is.False); + } + + [Test] + public void SetFeaturesIsHiddenForAPhonemeContext() + { + m_ruleFormulaControl.ObjectUnderCursor = MakePhonemeContext(); + + UIItemDisplayProperties display = AskSliceToDisplaySetFeatures(); + + Assert.That(display.Enabled, Is.False); + Assert.That(display.Visible, Is.False); + } + + [Test] + public void SetFeaturesIsHiddenForAnEmptySelection() + { + m_ruleFormulaControl.ObjectUnderCursor = null; + + UIItemDisplayProperties display = AskSliceToDisplaySetFeatures(); + + Assert.That(display.Enabled, Is.False); + Assert.That(display.Visible, Is.False); + } + + } +}