Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Tests the classification of the natural class under the cursor that decides whether the
/// "Set Phonological Features" rule-formula command is offered.
/// </summary>
[TestFixture]
[Apartment(ApartmentState.STA)]
public class RuleFormulaSetFeaturesTests : MemoryOnlyBackendProviderRestoredForEachTestTestBase
{
/// <summary>
/// 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.
/// </summary>
private sealed class TestRuleFormulaControl : RuleFormulaControl
{
internal ICmObject ObjectUnderCursor { get; set; }

protected override ICmObject GetCmObject(SelectionHelper sel, SelectionHelper.SelLimitType limit)
{
return ObjectUnderCursor;
}
}

private TestRuleFormulaControl m_ruleFormulaControl;
private RuleFormulaSlice m_slice;

public override void TestSetup()
{
base.TestSetup();
m_ruleFormulaControl = new TestRuleFormulaControl();
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<IPhSimpleContextNCFactory>().Create();
Cache.LanguageProject.PhonologicalDataOA.ContextsOS.Add(ctxt);
ctxt.FeatureStructureRA = natClass;
return ctxt;
}

private IPhNCFeatures MakeFeatureClass(string name)
{
var natClass = Cache.ServiceLocator.GetInstance<IPhNCFeaturesFactory>().Create();
Cache.LanguageProject.PhonologicalDataOA.NaturalClassesOS.Add(natClass);
if (name != null)
natClass.Name.SetUserWritingSystem(name);
natClass.FeaturesOA = Cache.ServiceLocator.GetInstance<IFsFeatStrucFactory>().Create();
return natClass;
}

/// <summary>
/// Produces the natural class name that the rule formula controls write when the user sets

Check warning on line 76 in Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaSetFeaturesTests.cs

View workflow job for this annotation

GitHub Actions / Build Debug and run tests

comment-hygiene (comment-line-too-long)

99 columns (max 98): 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.
/// </summary>
private static string AutoGeneratedName(string ruleName)
{
return string.Format(MEStrings.ksRuleNCFeatsName, ruleName);
}

private IPhSimpleContextNC MakeSegmentClassContext()
{
var natClass = Cache.ServiceLocator.GetInstance<IPhNCSegmentsFactory>().Create();
Cache.LanguageProject.PhonologicalDataOA.NaturalClassesOS.Add(natClass);
natClass.Name.SetUserWritingSystem("Vowels");

var ctxt = Cache.ServiceLocator.GetInstance<IPhSimpleContextNCFactory>().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<IPhPhonemeSetFactory>().Create());
var phoneme = Cache.ServiceLocator.GetInstance<IPhPhonemeFactory>().Create();
phonData.PhonemeSetsOS[0].PhonemesOC.Add(phoneme);

var ctxt = Cache.ServiceLocator.GetInstance<IPhSimpleContextSegFactory>().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

Check warning on line 142 in Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaSetFeaturesTests.cs

View workflow job for this annotation

GitHub Actions / Build Debug and run tests

comment-hygiene (comment-line-too-long)

100 columns (max 98): 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

Check warning on line 168 in Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaSetFeaturesTests.cs

View workflow job for this annotation

GitHub Actions / Build Debug and run tests

comment-hygiene (comment-line-too-long)

104 columns (max 98): 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<IPhIterationContextFactory>().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<IPhSimpleContextNCFactory>().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);
}

}
}
18 changes: 9 additions & 9 deletions Src/LexText/Morphology/RuleFormulaControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -981,16 +981,13 @@
return -1;
}
/// <summary>
/// Returns a bool representing whether the currently selected feature-based natural class simple context is user defined.
/// </summary>
/// <comments>
/// Determines whether the currently selected feature-based natural class simple context is user defined.

Check warning on line 984 in Src/LexText/Morphology/RuleFormulaControl.cs

View workflow job for this annotation

GitHub Actions / Build Debug and run tests

comment-hygiene (comment-line-too-long)

113 columns (max 98): Determines whether the currently selected feature-based natural class simple context is user defined.
/// A user-defined natural class has a name assigned by the user.
/// Natural classes are auto-generated when a user assigns phonological features, and they receive auto-generated names of a specific form.
/// </comments>
/// <returns>
/// Returns false if the natural class is not feature based, or if it has no name or an auto-generated name.
/// Returns true otherwise.
/// </returns>
/// When phonological features are assigned without defining/linking an existing natural class, FLEx

Check warning on line 986 in Src/LexText/Morphology/RuleFormulaControl.cs

View workflow job for this annotation

GitHub Actions / Build Debug and run tests

comment-hygiene (comment-line-too-long)

108 columns (max 98): When phonological features are assigned without defining/linking an existing natural class, FLEx
/// automatically generates an associated natural class and assigns it a name in a specific auto-generated form.

Check warning on line 987 in Src/LexText/Morphology/RuleFormulaControl.cs

View workflow job for this annotation

GitHub Actions / Build Debug and run tests

comment-hygiene (comment-line-too-long)

120 columns (max 98): automatically generates an associated natural class and assigns it a name in a specific auto-generated form.
/// Returns false if the natural class is not feature based, has no name, or has an auto-generated name;

Check warning on line 988 in Src/LexText/Morphology/RuleFormulaControl.cs

View workflow job for this annotation

GitHub Actions / Build Debug and run tests

comment-hygiene (comment-line-too-long)

112 columns (max 98): Returns false if the natural class is not feature based, has no name, or has an auto-generated name;
/// otherwise, returns true.
/// </summary>
public bool IsFeatureBasedNCNameUserDefined()
{
// If the class is not feature based, return false.
Expand All @@ -1009,6 +1006,9 @@
var name = natClass.Name;
bool isNamed = name != null && name.AvailableWritingSystemIds.Any(ws => !string.IsNullOrEmpty(name.get_String(ws).Text));
string ncName = isNamed ? name.UserDefaultWritingSystem?.Text : null;

// We are not going to try to handle the edge case where a natural class was auto-generated and then the user switched UI writing systems,

Check warning on line 1010 in Src/LexText/Morphology/RuleFormulaControl.cs

View workflow job for this annotation

GitHub Actions / Build Debug and run tests

comment-hygiene (comment-too-long)

263 chars (budget 200): We are not going to try to handle the edge case where a natural class was auto-generated and then the user switched UI writing systems,

Check warning on line 1010 in Src/LexText/Morphology/RuleFormulaControl.cs

View workflow job for this annotation

GitHub Actions / Build Debug and run tests

comment-hygiene (comment-line-too-long)

150 columns (max 98): We are not going to try to handle the edge case where a natural class was auto-generated and then the user switched UI writing systems,
// but note that in that case, the NC name will not contain the generated stub, even though it is an automatically generated class.

Check warning on line 1011 in Src/LexText/Morphology/RuleFormulaControl.cs

View workflow job for this annotation

GitHub Actions / Build Debug and run tests

comment-hygiene (comment-line-too-long)

143 columns (max 98): but note that in that case, the NC name will not contain the generated stub, even though it is an automatically generated class.
bool isAutoGeneratedOrNull = string.IsNullOrEmpty(ncName) || ncName.Contains(autoGeneratedNameStub);
return !isAutoGeneratedOrNull;
}
Expand Down
2 changes: 1 addition & 1 deletion Src/LexText/Morphology/RuleFormulaSlice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public bool OnDisplayContextSetFeatures(object commandObject, ref UIItemDisplayP
if (RuleFormulaControl.IsFeatsNCContextCurrent)
{
bool isUserDefinedNC = RuleFormulaControl.IsFeatureBasedNCNameUserDefined();
enable = !isUserDefinedNC; //enable is true iff IsFeatsNCContextCurrent is true and IsNCNameUserDefined is false
enable = !isUserDefinedNC;
}

display.Enabled = enable;
Expand Down
Loading