567 lines
22 KiB
C#
567 lines
22 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using System.Text.RegularExpressions;
|
||
using System.Threading.Tasks;
|
||
using AibisDream.Framework;
|
||
using AibisDream.MiniGame.Language;
|
||
using AibisDream.Utility;
|
||
using NUnit.Framework;
|
||
using UnityEditor;
|
||
using UnityEditor.Localization;
|
||
using UnityEngine;
|
||
using UnityEngine.Localization;
|
||
using UnityEngine.Localization.Settings;
|
||
using UnityEngine.Localization.Tables;
|
||
using UnityEngine.TestTools;
|
||
|
||
namespace AibisDream.EditorTests.Huoshan
|
||
{
|
||
public sealed class ExpressionLocalizationTests
|
||
{
|
||
private const string CatalogPath =
|
||
"Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset";
|
||
private const string ParticleProfilePath =
|
||
"Assets/GameContent/Feature_Huoshan/Expression/ExpressionParticleLanguageProfile.asset";
|
||
|
||
[TestCase(null, false)]
|
||
[TestCase("", false)]
|
||
[TestCase("plain", false)]
|
||
[TestCase("L10N.key", false)]
|
||
[TestCase("l10n.key", true)]
|
||
public void LocalizedParamPrefix_IsStrict(string value, bool expected)
|
||
{
|
||
Assert.That(LocalizationKit.IsLocalizedParam(value), Is.EqualTo(expected));
|
||
}
|
||
|
||
[Test]
|
||
public void LocalizedParamKey_StripsOnlyKnownPrefix()
|
||
{
|
||
Assert.That(LocalizationKit.GetL10NParamKey("l10n.hs.exp.log1.target"),
|
||
Is.EqualTo("hs.exp.log1.target"));
|
||
Assert.That(LocalizationKit.GetL10NParamKey("raw"), Is.EqualTo("raw"));
|
||
}
|
||
|
||
[Test]
|
||
public void MissingParamResult_RequiresMatchingLocalizedReference()
|
||
{
|
||
Assert.That(
|
||
LocalizationKit.IsMissingParamResult(
|
||
"l10n.hs.exp.pool",
|
||
"⟦hs.exp.pool⟧"),
|
||
Is.True);
|
||
Assert.That(
|
||
LocalizationKit.IsMissingParamResult(
|
||
"l10n.hs.exp.pool",
|
||
"⟦another.key⟧"),
|
||
Is.False);
|
||
Assert.That(
|
||
LocalizationKit.IsMissingParamResult(
|
||
"raw",
|
||
"⟦raw⟧"),
|
||
Is.False);
|
||
}
|
||
|
||
[UnityTest]
|
||
public IEnumerator LocalizeParamAsync_UsesExplicitLocaleAndDoesNotFallback()
|
||
{
|
||
Locale chinese = LocalizationEditorSettings.GetLocale(
|
||
new LocaleIdentifier("zh-Hans"));
|
||
Locale english = LocalizationEditorSettings.GetLocale(
|
||
new LocaleIdentifier("en"));
|
||
Assert.That(chinese, Is.Not.Null);
|
||
Assert.That(english, Is.Not.Null);
|
||
|
||
Locale previous = LocalizationSettings.SelectedLocale;
|
||
LocalizationSettings.SelectedLocale = english;
|
||
Task<string> chineseTask = LocalizationKit.LocalizeParamAsync(
|
||
"l10n.hs.exp.log1.target",
|
||
chinese);
|
||
Task<string> englishTask = LocalizationKit.LocalizeParamAsync(
|
||
"l10n.hs.exp.log1.target",
|
||
english);
|
||
|
||
while (!chineseTask.IsCompleted || !englishTask.IsCompleted)
|
||
yield return null;
|
||
|
||
LocalizationSettings.SelectedLocale = previous;
|
||
Assert.That(chineseTask.Result, Is.EqualTo("我就是个笑话"));
|
||
Assert.That(englishTask.Result, Is.Empty);
|
||
}
|
||
|
||
[UnityTest]
|
||
public IEnumerator LocalizeParamAsync_ReturnsRawTextAndMissingKeyMarker()
|
||
{
|
||
Task<string> rawTask = LocalizationKit.LocalizeParamAsync("raw text");
|
||
while (!rawTask.IsCompleted)
|
||
yield return null;
|
||
Assert.That(rawTask.Result, Is.EqualTo("raw text"));
|
||
|
||
Locale chinese = LocalizationEditorSettings.GetLocale(
|
||
new LocaleIdentifier("zh-Hans"));
|
||
LogAssert.Expect(
|
||
LogType.Error,
|
||
new Regex(@"Params 缺少条目.*Key=hs\.exp\.missing.*Locale=zh-Hans"));
|
||
Task<string> missingTask = LocalizationKit.LocalizeParamAsync(
|
||
"l10n.hs.exp.missing",
|
||
chinese);
|
||
while (!missingTask.IsCompleted)
|
||
yield return null;
|
||
Assert.That(missingTask.Result, Is.EqualTo("⟦hs.exp.missing⟧"));
|
||
}
|
||
|
||
[Test]
|
||
public void LegacyCommonParams_ArePresentInUnityParams()
|
||
{
|
||
StringTableCollection collection =
|
||
LocalizationEditorSettings.GetStringTableCollection(ConstRef.ParamsTable);
|
||
Assert.That(collection, Is.Not.Null);
|
||
|
||
string[] legacyKeys = { "天空", "海报", "植物", "朋友", "情绪", "记忆", "逻辑" };
|
||
foreach (string key in legacyKeys)
|
||
Assert.That(collection.SharedData.GetEntry(key), Is.Not.Null, key);
|
||
|
||
Assert.That(AssetDatabase.LoadAssetAtPath<TextAsset>(
|
||
"Assets/StreamingAssets/Config/params.csv"), Is.Null);
|
||
}
|
||
|
||
[Test]
|
||
public void ExpressionCatalog_IsValidAndCaseSensitive()
|
||
{
|
||
ExpressionContentCatalog catalog =
|
||
AssetDatabase.LoadAssetAtPath<ExpressionContentCatalog>(CatalogPath);
|
||
Assert.That(catalog, Is.Not.Null);
|
||
Assert.That(catalog.GetValidationErrors(), Is.Empty);
|
||
Assert.That(catalog.Rounds.Select(round => round.Id),
|
||
Is.EqualTo(new[] { "log1", "log2", "log3" }));
|
||
|
||
Assert.That(catalog.TryGetRound("log1", out ExpressionRoundDefinition log1), Is.True);
|
||
Assert.That(log1.NonTargetParticleCount, Is.EqualTo(16));
|
||
Assert.That(catalog.TryGetRound("LOG1", out _), Is.False);
|
||
Assert.That(catalog.TryGetRound("missing", out _), Is.False);
|
||
}
|
||
|
||
[Test]
|
||
public void ExpressionCatalog_RejectsDuplicateIdsAndNonLocalizedReferences()
|
||
{
|
||
ExpressionContentCatalog catalog =
|
||
ScriptableObject.CreateInstance<ExpressionContentCatalog>();
|
||
try
|
||
{
|
||
var first = new ExpressionRoundDefinition();
|
||
var second = new ExpressionRoundDefinition();
|
||
ConfigureRound(first, "log1", "l10n.target", "l10n.tokens", 1);
|
||
ConfigureRound(second, "log1", "raw", "l10n.tokens", 1);
|
||
SetPrivateField(
|
||
catalog,
|
||
"rounds",
|
||
new List<ExpressionRoundDefinition> { first, second });
|
||
|
||
string errors = string.Join("\n", catalog.GetValidationErrors());
|
||
Assert.That(errors, Does.Contain("重复"));
|
||
Assert.That(errors, Does.Contain("l10n.*"));
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(catalog);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void TokenParser_TrimsDropsEmptyAndRejectsFullwidthSeparator()
|
||
{
|
||
Assert.That(
|
||
LanguageYarnCommand.TryParseExpressionTokens(
|
||
" 哈哈 | | 嘿嘿|呵呵 ",
|
||
out var tokens),
|
||
Is.True);
|
||
Assert.That(tokens, Is.EqualTo(new[] { "哈哈", "嘿嘿", "呵呵" }));
|
||
|
||
LogAssert.Expect(LogType.Error, new Regex("全角分隔符"));
|
||
Assert.That(
|
||
LanguageYarnCommand.TryParseExpressionTokens("哈哈|嘿嘿", out _),
|
||
Is.False);
|
||
Assert.That(
|
||
LanguageYarnCommand.TryParseExpressionTokens(" | | ", out _),
|
||
Is.False);
|
||
}
|
||
|
||
[Test]
|
||
public void CharacterPoolParser_PreservesUnicodeDuplicatesAndRejectsSeparators()
|
||
{
|
||
string combining = "e\u0301";
|
||
Assert.That(
|
||
LanguageYarnCommand.TryParseExpressionPool(
|
||
$" 中 中 あ {combining} 😀。 ",
|
||
out List<string> pool),
|
||
Is.True);
|
||
Assert.That(
|
||
pool,
|
||
Is.EqualTo(new[] { "中", "中", "あ", combining, "😀", "。" }));
|
||
Assert.That(
|
||
LanguageYarnCommand.TryParseExpressionPool("中|あ", out _),
|
||
Is.False);
|
||
Assert.That(
|
||
LanguageYarnCommand.TryParseExpressionPool("中|あ", out _),
|
||
Is.False);
|
||
Assert.That(
|
||
LanguageYarnCommand.TryParseExpressionPool(" \t ", out _),
|
||
Is.False);
|
||
}
|
||
|
||
[Test]
|
||
public void ParticleLanguageProfile_ResolvesExactPrefixAndFallbackModes()
|
||
{
|
||
ExpressionParticleLanguageProfile profile =
|
||
AssetDatabase.LoadAssetAtPath<ExpressionParticleLanguageProfile>(
|
||
ParticleProfilePath);
|
||
Assert.That(profile, Is.Not.Null);
|
||
Assert.That(profile.GetValidationErrors(), Is.Empty);
|
||
|
||
Assert.That(
|
||
profile.Resolve(new LocaleIdentifier("zh-Hans")).UnitMode,
|
||
Is.EqualTo(ExpressionParticleUnitMode.Grapheme));
|
||
Assert.That(
|
||
profile.Resolve(new LocaleIdentifier("ja-JP")).UnitMode,
|
||
Is.EqualTo(ExpressionParticleUnitMode.Grapheme));
|
||
|
||
foreach (string localeCode in new[] { "en", "es", "ru", "pt-BR" })
|
||
{
|
||
Assert.That(
|
||
profile.Resolve(new LocaleIdentifier(localeCode)).UnitMode,
|
||
Is.EqualTo(ExpressionParticleUnitMode.Word),
|
||
localeCode);
|
||
}
|
||
|
||
Assert.That(
|
||
profile.Resolve(new LocaleIdentifier("en-US")).LocaleCode,
|
||
Is.EqualTo("en"));
|
||
|
||
LogAssert.Expect(
|
||
LogType.Warning,
|
||
new Regex(@"Locale 'zz-ZZ'.*Grapheme"));
|
||
Assert.That(
|
||
profile.Resolve(new LocaleIdentifier("zz-ZZ")).UnitMode,
|
||
Is.EqualTo(ExpressionParticleUnitMode.Grapheme));
|
||
}
|
||
|
||
[Test]
|
||
public void WordTokenizer_UsesWhitespaceBoundariesAndKeepsPunctuation()
|
||
{
|
||
Assert.That(
|
||
ExpressionTextTokenizer.TokenizeText(
|
||
"I am not a joke",
|
||
ExpressionParticleUnitMode.Word),
|
||
Is.EqualTo(new[] { "I", "am", "not", "a", "joke" }));
|
||
Assert.That(
|
||
ExpressionTextTokenizer.TokenizeText(
|
||
"don't self-doubt ¿Por qué? веришь?",
|
||
ExpressionParticleUnitMode.Word),
|
||
Is.EqualTo(new[]
|
||
{
|
||
"don't",
|
||
"self-doubt",
|
||
"¿Por",
|
||
"qué?",
|
||
"веришь?"
|
||
}));
|
||
Assert.That(
|
||
ExpressionTextTokenizer.TokenizeText(
|
||
"one \t\r\n two",
|
||
ExpressionParticleUnitMode.Word),
|
||
Is.EqualTo(new[] { "one", "two" }));
|
||
}
|
||
|
||
[Test]
|
||
public void GraphemeTokenizer_PreservesCjkKanaAndCombiningCharacters()
|
||
{
|
||
const string combining = "e\u0301";
|
||
Assert.That(
|
||
ExpressionTextTokenizer.TokenizeText(
|
||
$"汉 あ {combining}",
|
||
ExpressionParticleUnitMode.Grapheme),
|
||
Is.EqualTo(new[] { "汉", "あ", combining }));
|
||
}
|
||
|
||
[Test]
|
||
public void WordPool_PreservesDuplicateWeightsAndRejectsPipeSeparators()
|
||
{
|
||
Assert.That(
|
||
LanguageYarnCommand.TryParseExpressionPool(
|
||
"noise doubt noise",
|
||
ExpressionParticleUnitMode.Word,
|
||
out List<string> pool),
|
||
Is.True);
|
||
Assert.That(pool, Is.EqualTo(new[] { "noise", "doubt", "noise" }));
|
||
Assert.That(
|
||
LanguageYarnCommand.TryParseExpressionPool(
|
||
"noise|doubt",
|
||
ExpressionParticleUnitMode.Word,
|
||
out _),
|
||
Is.False);
|
||
Assert.That(
|
||
LanguageYarnCommand.TryParseExpressionPool(
|
||
"noise|doubt",
|
||
ExpressionParticleUnitMode.Word,
|
||
out _),
|
||
Is.False);
|
||
}
|
||
|
||
[Test]
|
||
public void RoundSnapshot_IsParsedOnceAndKeepsItsLocaleAndWordUnits()
|
||
{
|
||
ExpressionParticleLanguageProfile languageProfile =
|
||
AssetDatabase.LoadAssetAtPath<ExpressionParticleLanguageProfile>(
|
||
ParticleProfilePath);
|
||
ExpressionParticleLocaleSettings english =
|
||
languageProfile.Resolve(new LocaleIdentifier("en"));
|
||
Assert.That(
|
||
ExpressionRoundTextSnapshot.TryCreate(
|
||
new LocaleIdentifier("en"),
|
||
english,
|
||
"I am not a joke",
|
||
new[] { "false alarm", "false alarm" },
|
||
"noise doubt noise",
|
||
out ExpressionRoundTextSnapshot snapshot,
|
||
out string error),
|
||
Is.True,
|
||
error);
|
||
|
||
Locale previous = LocalizationSettings.SelectedLocale;
|
||
try
|
||
{
|
||
LocalizationSettings.SelectedLocale =
|
||
LocalizationEditorSettings.GetLocale(
|
||
new LocaleIdentifier("zh-Hans"));
|
||
|
||
Assert.That(snapshot.Locale.Code, Is.EqualTo("en"));
|
||
Assert.That(
|
||
snapshot.TargetUnits,
|
||
Is.EqualTo(new[] { "I", "am", "not", "a", "joke" }));
|
||
Assert.That(
|
||
snapshot.InterferencePoolUnits,
|
||
Is.EqualTo(new[] { "false", "alarm", "false", "alarm" }));
|
||
Assert.That(
|
||
snapshot.DefaultPoolUnits,
|
||
Is.EqualTo(new[] { "noise", "doubt", "noise" }));
|
||
}
|
||
finally
|
||
{
|
||
LocalizationSettings.SelectedLocale = previous;
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void WordProfile_ScalesConfiguredNonTargetCounts()
|
||
{
|
||
ExpressionParticleLanguageProfile profile =
|
||
AssetDatabase.LoadAssetAtPath<ExpressionParticleLanguageProfile>(
|
||
ParticleProfilePath);
|
||
ExpressionParticleLocaleSettings english =
|
||
profile.Resolve(new LocaleIdentifier("en"));
|
||
|
||
Assert.That(english.ScaleNonTargetCount(16), Is.EqualTo(10));
|
||
Assert.That(english.ScaleNonTargetCount(28), Is.EqualTo(18));
|
||
Assert.That(english.ScaleNonTargetCount(38), Is.EqualTo(25));
|
||
}
|
||
|
||
[Test]
|
||
public void ParticleGeometry_UsesVisualEdgeDistanceAndShortestSeparationAxis()
|
||
{
|
||
var left = new Bounds(Vector3.zero, new Vector3(4f, 1f, 0.01f));
|
||
var right = new Bounds(
|
||
new Vector3(4.15f, 0f, 0f),
|
||
new Vector3(4f, 1f, 0.01f));
|
||
|
||
Assert.That(
|
||
ExpressionParticleGeometry.BoundsDistance(left, right),
|
||
Is.EqualTo(0.15f).Within(0.0001f));
|
||
|
||
right.center = new Vector3(3.8f, 0f, 0f);
|
||
Assert.That(
|
||
ExpressionParticleGeometry.TryGetSeparation(
|
||
left,
|
||
right,
|
||
0.1f,
|
||
out Vector2 direction,
|
||
out float overlap),
|
||
Is.True);
|
||
Assert.That(direction, Is.EqualTo(Vector2.left));
|
||
Assert.That(overlap, Is.EqualTo(0.3f).Within(0.0001f));
|
||
}
|
||
|
||
[Test]
|
||
public void TextParticle_VisualBoundsIncludesTheWholeWordForPointerDistance()
|
||
{
|
||
var gameObject = new GameObject("TextParticleBoundsTest");
|
||
try
|
||
{
|
||
CandidateParticle particle =
|
||
gameObject.AddComponent<CandidateParticle>();
|
||
particle.ForceSetUnitText("self-doubt");
|
||
Bounds bounds = particle.GetVisualWorldBounds();
|
||
|
||
Assert.That(bounds.size.x, Is.GreaterThan(0f));
|
||
Assert.That(
|
||
particle.DistanceToVisualBounds(
|
||
new Vector2(bounds.max.x, bounds.center.y)),
|
||
Is.EqualTo(0f).Within(0.0001f));
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(gameObject);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void Params_ContainsLocalizedExpressPoolForEveryLocale()
|
||
{
|
||
StringTableCollection collection =
|
||
LocalizationEditorSettings.GetStringTableCollection(ConstRef.ParamsTable);
|
||
Assert.That(collection, Is.Not.Null);
|
||
|
||
string key =
|
||
LocalizationKit.GetL10NParamKey(ConstRef.ExpressParticlePoolParam);
|
||
Assert.That(collection.SharedData.GetEntry(key), Is.Not.Null);
|
||
|
||
string[] localeCodes = { "zh-Hans", "en", "ja-JP", "es", "ru", "pt-BR" };
|
||
foreach (string localeCode in localeCodes)
|
||
{
|
||
StringTable table =
|
||
collection.GetTable(new LocaleIdentifier(localeCode)) as StringTable;
|
||
Assert.That(table, Is.Not.Null, localeCode);
|
||
Assert.That(table.GetEntry(key), Is.Not.Null, localeCode);
|
||
}
|
||
|
||
StringTable chinese =
|
||
collection.GetTable(new LocaleIdentifier("zh-Hans")) as StringTable;
|
||
Assert.That(
|
||
ExpressionTextTokenizer.GetVisibleElements(
|
||
chinese.GetEntry(key).LocalizedValue),
|
||
Is.Not.Empty);
|
||
|
||
foreach (string localeCode in localeCodes.Skip(1))
|
||
{
|
||
StringTable table =
|
||
collection.GetTable(new LocaleIdentifier(localeCode)) as StringTable;
|
||
Assert.That(table.GetEntry(key).LocalizedValue, Is.Empty, localeCode);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void TextParticle_UsesConfiguredRoleAndOverridePoolsWithoutFirstFrameFallback()
|
||
{
|
||
var gameObject = new GameObject("TextParticlePoolTest");
|
||
try
|
||
{
|
||
CandidateParticle particle = gameObject.AddComponent<CandidateParticle>();
|
||
Assert.That(particle.CurrentUnitText, Is.Null.Or.Empty);
|
||
|
||
string combining = "e\u0301";
|
||
particle.SetUnitPools(
|
||
new[] { "😀" },
|
||
new[] { combining });
|
||
|
||
particle.originalIsRed = true;
|
||
particle.InitializeRandomUnit();
|
||
Assert.That(particle.CurrentUnitText, Is.EqualTo("😀"));
|
||
|
||
particle.originalIsRed = false;
|
||
particle.InitializeRandomUnit();
|
||
Assert.That(particle.CurrentUnitText, Is.EqualTo(combining));
|
||
|
||
particle.SetOverrideUnitPool(new[] { "。" });
|
||
particle.InitializeRandomUnit();
|
||
Assert.That(particle.CurrentUnitText, Is.EqualTo("。"));
|
||
|
||
particle.SetOverrideUnitPool((IReadOnlyList<string>)null);
|
||
particle.InitializeRandomUnit();
|
||
Assert.That(particle.CurrentUnitText, Is.EqualTo(combining));
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(gameObject);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void UnicodeTokenizer_PreservesTextElementsAndDropsWhitespace()
|
||
{
|
||
string combining = "e\u0301";
|
||
var elements =
|
||
ExpressionTextTokenizer.GetVisibleElements($"中 A あ {combining} 😀。");
|
||
|
||
Assert.That(
|
||
elements,
|
||
Is.EqualTo(new[] { "中", "A", "あ", combining, "😀", "。" }));
|
||
}
|
||
|
||
[Test]
|
||
public void UnicodeLayout_PreservesWordGapWithoutWhitespaceParticle()
|
||
{
|
||
ExpressionTextTokenizer.Layout layout =
|
||
ExpressionTextTokenizer.BuildLayout("A B", 1f, 0.6f);
|
||
|
||
Assert.That(layout.VisibleElements, Is.EqualTo(new[] { "A", "B" }));
|
||
Assert.That(layout.Offsets[0], Is.EqualTo(-0.8f).Within(0.0001f));
|
||
Assert.That(layout.Offsets[1], Is.EqualTo(0.8f).Within(0.0001f));
|
||
}
|
||
|
||
[Test]
|
||
public void UnicodeSequenceMatch_UsesWholeTextElements()
|
||
{
|
||
var source = ExpressionTextTokenizer.GetVisibleElements("A😀e\u0301。");
|
||
var fragment = ExpressionTextTokenizer.GetVisibleElements("😀e\u0301");
|
||
Assert.That(ExpressionTextTokenizer.FindVisibleSequence(source, fragment),
|
||
Is.EqualTo(1));
|
||
Assert.That(
|
||
ExpressionTextTokenizer.FindVisibleSequence(
|
||
source,
|
||
ExpressionTextTokenizer.GetVisibleElements("😀x")),
|
||
Is.EqualTo(-1));
|
||
}
|
||
|
||
[Test]
|
||
public void ExpressContentValidation_HasNoBlockingErrors()
|
||
{
|
||
var issues = ExpressionLocalizationValidator.Validate();
|
||
string errors = string.Join(
|
||
"\n",
|
||
issues
|
||
.Where(issue =>
|
||
issue.Severity == ExpressionLocalizationIssueSeverity.Error)
|
||
.Select(issue => issue.Message));
|
||
|
||
Assert.That(errors, Is.Empty);
|
||
Assert.That(
|
||
issues.Any(issue =>
|
||
issue.Severity == ExpressionLocalizationIssueSeverity.Warning),
|
||
Is.True,
|
||
"未翻译 Locale 的空条目应作为待翻译警告保留。");
|
||
}
|
||
|
||
private static void ConfigureRound(
|
||
ExpressionRoundDefinition round,
|
||
string id,
|
||
string target,
|
||
string tokens,
|
||
int nonTargetCount)
|
||
{
|
||
SetPrivateField(round, "id", id);
|
||
SetPrivateField(round, "targetReference", target);
|
||
SetPrivateField(round, "tokenReference", tokens);
|
||
SetPrivateField(round, "nonTargetParticleCount", nonTargetCount);
|
||
}
|
||
|
||
private static void SetPrivateField(object target, string fieldName, object value)
|
||
{
|
||
FieldInfo field = target.GetType().GetField(
|
||
fieldName,
|
||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||
Assert.That(field, Is.Not.Null, fieldName);
|
||
field.SetValue(target, value);
|
||
}
|
||
}
|
||
}
|