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
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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 NUnit.Framework;
using SIL.LCModel;
using SIL.LCModel.Core.Text;
using SIL.LCModel.Core.WritingSystems;
using FeatVals = System.Collections.Generic.Dictionary<string, string>;

namespace SIL.FieldWorks.XWorks.MorphologyEditor
{
/// <summary>
/// Which feature-based natural classes count as carrying a name the user gave them. This is
/// the single input deciding whether a rule formula draws a class as an abbreviation or as a
/// feature list, so each shape of name a project can hold is pinned here.
/// </summary>
[TestFixture]
public class RuleFormulaControlNaturalClassNameTests : RuleFormulaVcTestBase
{
[Test]
public void NoClass_IsNotUserDefined()
{
Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(null), Is.False);
}

/// <summary>
/// The check answers whether a class carries a name the user gave it, and an unnamed
/// class carries none. Set Phonological Features relies on that answer to keep
/// offering feature edits on unnamed classes (LT-22576).
/// </summary>
[Test]
public void ClassWithNoName_IsNotUserDefined()
{
IPhNCFeatures natClass = AddFeatureNaturalClass(null, "Vd",
new FeatVals { { "vd", "+" } });

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.False,
"an unnamed class carries no name the user gave it");
}

[Test]
public void ClassWithAnEmptyName_IsNotUserDefined()
{
IPhNCFeatures natClass = AddFeatureNaturalClass(string.Empty, "Vd",
new FeatVals { { "vd", "+" } });

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.False,
"an empty name is not a name the user gave the class");
}

[Test]
public void ClassNamedForARule_IsNotUserDefined()
{
IPhNCFeatures natClass = AddRuleNamedFeatureNaturalClass("s to n",
new FeatVals { { "vd", "+" } });

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.False);
}

/// <summary>
/// Projects hold rule-generated names that carry a prefix ahead of the generated text, so
/// the generated text is recognised wherever it sits in the name.
/// </summary>
[Test]
public void ClassNamedForARuleBehindAPrefix_IsNotUserDefined()
{
IPhNCFeatures natClass = AddFeatureNaturalClass(null, null,
new FeatVals { { "vd", "+" } });
natClass.Name.SetUserWritingSystem("Phonemes s and t - "
+ string.Format(MEStrings.ksRuleNCFeatsName, "s to n"));

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.False);
}

[Test]
public void NamedClass_IsUserDefined()
{
IPhNCFeatures natClass = AddFeatureNaturalClass("Voiced", "Vd",
new FeatVals { { "vd", "+" } });

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.True);
}

/// <summary>
/// The natural class editor writes a name into the analysis writing systems while the
/// check reads the user writing system. A project whose analysis language differs from
/// its interface language must still have its named classes qualify.
/// </summary>
[Test]
public void ClassNamedInAnAnalysisWritingSystemOtherThanTheUserOne_IsUserDefined()
{
Cache.ServiceLocator.WritingSystemManager.GetOrSet("de",
out CoreWritingSystemDefinition german);
Cache.LanguageProject.AddToCurrentAnalysisWritingSystems(german);
IPhNCFeatures natClass = AddFeatureNaturalClass(null, "Vd",
new FeatVals { { "vd", "+" } });
natClass.Name.set_String(german.Handle,
TsStringUtils.MakeString("Stimmhaft", german.Handle));
Assert.That(natClass.Name.UserDefaultWritingSystem.Text, Is.EqualTo("Stimmhaft"),
"the name must reach the check through the analysis writing system, "
+ "not through a placeholder");

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.True);
}

[Test]
public void NamedClassWithNoFeatures_IsUserDefined()
{
IPhNCFeatures natClass = AddFeatureNaturalClass("Voiced", "Vd", new FeatVals());

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.True,
"a name qualifies a class whatever it carries for features");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// 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 NUnit.Framework;
using SIL.LCModel;
using FeatVals = System.Collections.Generic.Dictionary<string, string>;

namespace SIL.FieldWorks.XWorks.MorphologyEditor
{
/// <summary>
/// The display dependencies a rule formula registers on the natural classes it draws. A class
/// name decides whether the formula draws an abbreviation or a feature list, and both the
/// name and the abbreviation decide the cell size, so editing either has to rebuild the
/// formula. The collector environment does not re-run layout, so these assert the
/// registration that triggers the rebuild rather than the rebuild itself.
/// </summary>
[TestFixture]
public class RuleFormulaVcNaturalClassDependencyTests : RuleFormulaVcTestBase
{
[Test]
public void RegularRule_RegistersTheNameAndAbbreviationOfItsClass()
{
IPhNCFeatures natClass = AddFeatureNaturalClass("Voiced", "Vd",
new FeatVals { { "vd", "+" } });
IPhSegRuleRHS rhs = AddRegularRule("s to n");
AddStrucDescNCContext(rhs, natClass);

RecordingCollectorEnv env = DrawRegularRule(rhs);

Assert.That(env.DependsOn(natClass.Hvo, PhNaturalClassTags.kflidName), Is.True);
Assert.That(env.DependsOn(natClass.Hvo, PhNaturalClassTags.kflidAbbreviation), Is.True);
}

/// <summary>
/// Naming a class generated for a rule turns its feature list into an abbreviation,
/// so the formula has to depend on that class's name before the user names it.
/// </summary>
[Test]
public void RegularRule_RegistersTheNameOfARuleNamedClass()
{
IPhNCFeatures natClass = AddRuleNamedFeatureNaturalClass("s to n",
new FeatVals { { "vd", "+" } });
IPhSegRuleRHS rhs = AddRegularRule("s to n");
AddStrucDescNCContext(rhs, natClass);

RecordingCollectorEnv env = DrawRegularRule(rhs);

Assert.That(env.DependsOn(natClass.Hvo, PhNaturalClassTags.kflidName), Is.True);
}

[Test]
public void RegularRule_RegistersAClassInItsStructuralChange()
{
IPhNCFeatures natClass = AddFeatureNaturalClass("Nasal", "N",
new FeatVals { { "nasal", "+" } });
IPhSegRuleRHS rhs = AddRegularRule("s to n");
AddStrucChangeNCContext(rhs, natClass);

RecordingCollectorEnv env = DrawRegularRule(rhs);

Assert.That(env.DependsOn(natClass.Hvo, PhNaturalClassTags.kflidName), Is.True);
}

[Test]
public void RegularRule_RegistersAClassNestedInASequenceContext()
{
IPhNCFeatures natClass = AddFeatureNaturalClass("Voiced", "Vd",
new FeatVals { { "vd", "+" } });
IPhSegRuleRHS rhs = AddRegularRule("s to n");
rhs.LeftContextOA = AddSequenceContext(AddStandaloneNCContext(natClass));

RecordingCollectorEnv env = DrawRegularRule(rhs);

Assert.That(env.DependsOn(natClass.Hvo, PhNaturalClassTags.kflidName), Is.True);
Assert.That(env.DependsOn(natClass.Hvo, PhNaturalClassTags.kflidAbbreviation), Is.True);
}

[Test]
public void RegularRule_RegistersAClassInsideAnIterationContext()
{
IPhNCFeatures natClass = AddFeatureNaturalClass("Voiced", "Vd",
new FeatVals { { "vd", "+" } });
IPhSegRuleRHS rhs = AddRegularRule("s to n");
rhs.RightContextOA = AddIterationContext(AddStandaloneNCContext(natClass));

RecordingCollectorEnv env = DrawRegularRule(rhs);

Assert.That(env.DependsOn(natClass.Hvo, PhNaturalClassTags.kflidName), Is.True);
}

/// <summary>
/// An iteration context loses its member when the class inside it is deleted, and the
/// formula must still draw.
/// </summary>
[Test]
public void RegularRule_WithAnEmptyIterationContext_DrawsWithoutError()
{
IPhSegRuleRHS rhs = AddRegularRule("s to n");
rhs.LeftContextOA = AddIterationContext(null);

Assert.That(() => DrawRegularRule(rhs), Throws.Nothing);
}

[Test]
public void RegularRule_RegistersEveryClassItDraws()
{
IPhNCFeatures first = AddFeatureNaturalClass("Voiced", "Vd",
new FeatVals { { "vd", "+" } });
IPhNCFeatures second = AddFeatureNaturalClass("Nasal", "N",
new FeatVals { { "nasal", "+" } });
IPhSegRuleRHS rhs = AddRegularRule("s to n");
AddStrucDescNCContext(rhs, first);
AddStrucChangeNCContext(rhs, second);

RecordingCollectorEnv env = DrawRegularRule(rhs);

Assert.That(NamesRegistered(env), Is.EquivalentTo(new[] { first.Hvo, second.Hvo }));
}

[Test]
public void AffixProcessRule_RegistersTheClassInItsInput()
{
IPhNCFeatures natClass = AddFeatureNaturalClass("Voiced", "Vd",
new FeatVals { { "vd", "+" } });
IMoAffixProcess rule = AddAffixProcessRule();
AddInputNCContext(rule, natClass);

var vc = new AffixRuleFormulaVc(Cache, m_propertyTable);
var env = new RecordingCollectorEnv(Cache.MainCacheAccessor, rule.Hvo);
vc.Display(env, rule.Hvo, AffixRuleFormulaVc.kfragRule);

Assert.That(env.DependsOn(natClass.Hvo, PhNaturalClassTags.kflidName), Is.True);
Assert.That(env.DependsOn(natClass.Hvo, PhNaturalClassTags.kflidAbbreviation), Is.True);
}

[Test]
public void MetathesisRule_RegistersTheNameAndAbbreviationOfItsClass()
{
IPhNCFeatures natClass = AddFeatureNaturalClass("Voiced", "Vd",
new FeatVals { { "vd", "+" } });
IPhMetathesisRule rule = AddMetathesisRule("t s to s t");
AddMetathesisNCContext(rule, PhMetathesisRuleTags.kidxLeftSwitch, natClass);

RecordingCollectorEnv env = DrawMetathesisRule(rule);

Assert.That(env.Text, Does.Contain("[Vd]"), "the class is drawn in its switch cell");
Assert.That(env.DependsOn(natClass.Hvo, PhNaturalClassTags.kflidName), Is.True);
Assert.That(env.DependsOn(natClass.Hvo, PhNaturalClassTags.kflidAbbreviation), Is.True);
}

[Test]
public void MetathesisRule_RegistersEveryClassItDraws()
{
IPhNCFeatures first = AddFeatureNaturalClass("Voiced", "Vd",
new FeatVals { { "vd", "+" } });
IPhNCFeatures second = AddFeatureNaturalClass("Nasal", "N",
new FeatVals { { "nasal", "+" } });
IPhMetathesisRule rule = AddMetathesisRule("t s to s t");
AddMetathesisNCContext(rule, PhMetathesisRuleTags.kidxLeftSwitch, first);
AddMetathesisNCContext(rule, PhMetathesisRuleTags.kidxRightSwitch, second);

RecordingCollectorEnv env = DrawMetathesisRule(rule);

Assert.That(env.Text, Does.Contain("[Vd]"), "the left switch is drawn");
Assert.That(env.Text, Does.Contain("[N]"), "the right switch is drawn");
Assert.That(NamesRegistered(env), Is.EquivalentTo(new[] { first.Hvo, second.Hvo }));
}

private RecordingCollectorEnv DrawRegularRule(IPhSegRuleRHS rhs)
{
var vc = new RegRuleFormulaVc(Cache, m_propertyTable);
var env = new RecordingCollectorEnv(Cache.MainCacheAccessor, rhs.Hvo);
vc.Display(env, rhs.Hvo, RegRuleFormulaVc.kfragRHS);
return env;
}

private RecordingCollectorEnv DrawMetathesisRule(IPhMetathesisRule rule)
{
var vc = new MetaRuleFormulaVc(Cache, m_propertyTable);
var env = new RecordingCollectorEnv(Cache.MainCacheAccessor, rule.Hvo);
vc.Display(env, rule.Hvo, MetaRuleFormulaVc.kfragRule);
return env;
}

private static int[] NamesRegistered(RecordingCollectorEnv env)
{
return env.Dependencies
.SelectMany(call => Enumerable.Range(0, call.Count)
.Where(i => call.Tags[i] == PhNaturalClassTags.kflidName)
.Select(i => call.Hvos[i]))
.Distinct()
.ToArray();
}
}
}
Loading
Loading