add Backend.Translations.Tests

This commit is contained in:
Anton Suhorukov 2023-05-03 23:31:45 +03:00
parent 83a10be7e9
commit d265fb4373
6 changed files with 614 additions and 0 deletions

View File

@ -0,0 +1,4 @@
PUSHD %~dp0..
set dir=%~dp0..
echo %dir%
dotnet test common\Tests\Backend.Translations.Tests\Backend.Translations.Tests.csproj -l:html --environment "BASE_DIR=%dir%" --results-directory "%dir%/TestsResults"

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
<PackageReference Include="ResXResourceReader.NetStandard" Version="1.1.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33213.308
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Translations.Tests", "Backend.Translations.Tests.csproj", "{77474278-2624-496C-B5DB-BDDB9622FA70}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{77474278-2624-496C-B5DB-BDDB9622FA70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77474278-2624-496C-B5DB-BDDB9622FA70}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77474278-2624-496C-B5DB-BDDB9622FA70}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77474278-2624-496C-B5DB-BDDB9622FA70}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {07AB23F0-190B-4AE7-B4AB-ED8380DB1988}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,174 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using System.Globalization;
using System.Text.RegularExpressions;
namespace Backend.Translations.Tests;
public static class CheckRules
{
public static bool CompliesToRulePunctuationLead(string? neutralValue, string? value)
{
var reference = GetPunctuationSequence(neutralValue).ToArray();
var array = GetPunctuationSequence(value);
return !reference.SequenceEqual(array);
}
public static bool CompliesToRulePunctuationTail(string? neutralValue, string? value)
{
var reference = GetPunctuationSequence(neutralValue, true).ToArray();
var array = GetPunctuationSequence(value, true);
return !reference.SequenceEqual(array);
}
public static bool CompliesToRuleWhiteSpaceLead(string? neutralValue, string? value)
{
var reference = GetWhiteSpaceSequence(neutralValue);
var array = GetWhiteSpaceSequence(value);
return !reference.SequenceEqual(array);
}
public static bool CompliesToRuleWhiteSpaceTail(string? neutralValue, string? value)
{
var reference = GetWhiteSpaceSequence(neutralValue, true);
var array = GetWhiteSpaceSequence(value, true);
return !reference.SequenceEqual(array);
}
public static bool CompliesToRuleStringFormat(string? neutralValue, string? value)
{
var allValues = new[] { neutralValue, value }.ToList();
var indexedComply = GetStringFormatByIndexFlags(neutralValue) == GetStringFormatByIndexFlags(value);
var namedComply = GetStringFormatByPlaceholdersFingerprint(neutralValue) == GetStringFormatByPlaceholdersFingerprint(value);
return !(indexedComply && namedComply);
}
private static string GetStringFormatByPlaceholdersFingerprint(string? value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
return string.Join("|", ExtractPlaceholders(value).OrderBy(item => item));
}
private static readonly Regex _formatPlaceholderExpression = new(@"\$\{\s*(\w[.\w\d_]*)\s*\}");
public static IEnumerable<string> ExtractPlaceholders(string text)
{
var placeholders = _formatPlaceholderExpression.Matches(text)
.OfType<Match>()
.Select(m => m.Groups[1].Value)
.Distinct();
return placeholders;
}
private static readonly Regex _getStringFormatByIndexExpression = new(@"\{([0-9]+)(?:,-?[0-9]+)?(?::[^\}]+)?\}", RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static long GetStringFormatByIndexFlags(string? value)
{
if (string.IsNullOrEmpty(value))
return 0;
return _getStringFormatByIndexExpression.Matches(value)
.Cast<Match>()
.Where(m => m.Success)
.Aggregate(0L, (a, match) => a | ParseMatch(match));
}
private static long ParseMatch(Match match)
{
if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
return 1L << value;
return 0;
}
private static IEnumerable<char> GetWhiteSpaceSequence(string? value, bool revers = false)
{
return GetCharIterator(value, revers).TakeWhile(char.IsWhiteSpace);
}
private static IEnumerable<char> GetCharIterator(string value, bool revers) => revers ? value.Reverse() : value;
private static IEnumerable<char> GetPunctuationSequence(string? value, bool revers = false)
{
return GetCharIterator(NormalizeUnicode(value), revers)
.SkipWhile(char.IsWhiteSpace).
TakeWhile(IsPunctuation).
Select(NormalizePunctuation);
}
private static char NormalizePunctuation(char value)
{
switch ((int)value)
{
case 0x055C: return '!'; // ARMENIAN EXCLAMATION MARK
case 0x055D: return ','; // ARMENIAN COMMA
case 0x055E: return '?'; // ARMENIAN QUESTION MARK
case 0x0589: return '.'; // ARMENIAN FULL STOP
case 0x07F8: return ','; // NKO COMMA
case 0x07F9: return '!'; // NKO EXCLAMATION MARK
case 0x1944: return '!'; // LIMBU EXCLAMATION MARK
case 0x1945: return '?'; // LIMBU QUESTION MARK
case 0x3001: return ','; // IDEOGRAPHIC COMMA
case 0x3002: return '.'; // IDEOGRAPHIC FULL STOP
case 0xFF01: return '!'; // FULLWIDTH EXCLAMATION MARK
case 0xFF0C: return ','; // FULLWIDTH COMMA
case 0xFF0E: return '.'; // FULLWIDTH FULL STOP
case 0xFF1A: return ':'; // FULLWIDTH COLON
case 0xFF1B: return ';'; // FULLWIDTH SEMICOLON
case 0xFF1F: return '?'; // FULLWIDTH QUESTION MARK
case 0x061F: return '?'; // ARABIC QUESTION MARK
default: return value;
}
}
private static string NormalizeUnicode(string? value) => value?.Normalize() ?? string.Empty;
private static bool IsPunctuation(char value)
{
// exclude quotes, special chars (\#), hot-key prefixes (&_), language specifics with no common equivalent (¡¿).
const string excluded = "'\"\\#&_¡¿";
// ReSharper disable once SwitchStatementMissingSomeCases
switch (char.GetUnicodeCategory(value))
{
case UnicodeCategory.OtherPunctuation:
return !excluded.Contains(value, StringComparison.Ordinal);
case UnicodeCategory.DashPunctuation:
return true;
default:
return false;
}
}
}

View File

@ -0,0 +1,87 @@
using System.Globalization;
namespace ResXManager.Infrastructure;
public static class CultureHelper
{
public static bool IsValidCultureName(string? languageName)
{
try
{
if (string.IsNullOrEmpty(languageName))
return false;
// pseudo-locales:
if (languageName.StartsWith("qps-", StringComparison.Ordinal))
return true;
// #376: support Custom dialect resource
var culture = new CultureInfo(languageName);
while (!culture.IsNeutralCulture)
{
culture = culture.Parent;
}
return WellKnownNeutralCultures.Contains(culture.Name);
}
catch
{
return false;
}
}
private static class WellKnownNeutralCultures
{
private static readonly string[] _sortedNeutralCultureNames = GetSortedNeutralCultureNames();
public static bool Contains(string cultureName)
{
return Array.BinarySearch(_sortedNeutralCultureNames, cultureName, StringComparer.OrdinalIgnoreCase) >= 0;
}
private static string[] GetSortedNeutralCultureNames()
{
var allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
var cultureNames = allCultures.Select(culture => culture.IetfLanguageTag)
.Concat(allCultures.Select(culture => culture.Name))
.Distinct()
.ToArray();
Array.Sort(cultureNames, StringComparer.OrdinalIgnoreCase);
return cultureNames;
}
}
/// <summary>
/// Gets all system specific cultures.
/// </summary>
public static IEnumerable<CultureInfo> SpecificCultures => WellKnownSpecificCultures.Value;
private static class WellKnownSpecificCultures
{
public static readonly CultureInfo[] Value = GetSpecificCultures();
private static CultureInfo[] GetSpecificCultures()
{
var specificCultures = CultureInfo.GetCultures(CultureTypes.AllCultures)
.Where(c => c.GetAncestors().Any())
.OrderBy(c => c.DisplayName)
.ToArray();
return specificCultures;
}
}
public static IEnumerable<CultureInfo> GetAncestors(this CultureInfo self)
{
var item = self.Parent;
while (!string.IsNullOrEmpty(item.Name))
{
yield return item;
item = item.Parent;
}
}
}

View File

@ -0,0 +1,304 @@
// (c) Copyright Ascensio System SIA 2010-2022
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using System.Xml;
using NUnit.Framework;
using ResXManager.Infrastructure;
namespace Backend.Translations.Tests;
public class Tests
{
private Dictionary<FileInfo, IEnumerable<FileInfo>> _resources;
private readonly HashSet<string> _excludedDirectories = new(new[] { "bin", "obj", "node_modules", "thirdparty", "migration" }, StringComparer.OrdinalIgnoreCase);
[SetUp]
public void Setup()
{
var basePath = Environment.GetEnvironmentVariable("BASE_DIR") ?? Path.GetFullPath("../../../../../../");
var directory = new DirectoryInfo(basePath);
var resources = GetResources(directory);
var netralresources = resources.Where(f => IsNetral(f.Name));
_resources = new Dictionary<FileInfo, IEnumerable<FileInfo>>();
foreach(var resource in netralresources)
{
var nameWithoutExt = resource.FullName.Substring(0, resource.FullName.Length - 5);
_resources.Add(resource, resources.Where(r => r.FullName.StartsWith(nameWithoutExt)));
}
}
private bool IsNetral(string fileName)
{
var split = fileName.Split('.');
if (split.Length == 2)
{
return true;
}
else
{
var valid = CultureHelper.IsValidCultureName(split[split.Length - 2]);
return !valid;
}
}
private IEnumerable<FileInfo> GetResources(DirectoryInfo directory)
{
foreach (var file in directory.EnumerateFiles())
{
if (IsResourceFile(file.Name))
{
yield return file;
}
else
{
continue;
}
}
foreach (var subDirectory in directory.EnumerateDirectories())
{
var name = subDirectory.Name;
if (name.StartsWith(".", StringComparison.Ordinal) || _excludedDirectories.Contains(name))
continue;
foreach (var file in GetResources(subDirectory))
{
if (IsResourceFile(file.Name))
{
yield return file;
}
else
{
continue;
}
}
}
}
private bool IsResourceFile(string filePath, string? extension = null)
{
extension ??= Path.GetExtension(filePath);
if (extension == ".resx")
return true;
return false;
}
[Test, Order(1)]
public void ResourceFilesExist()
{
var all = new HashSet<string>();
var groupByFile = new Dictionary<FileInfo, HashSet<string>>();
var allExist = true;
var message = "resource files is not exist: \n";
foreach(var pair in _resources)
{
var resources = pair.Value;
var set = new HashSet<string>();
foreach (var resource in resources)
{
var split = resource.Name.Split('.');
if (split.Length == 2)
{
all.Add("netral");
set.Add("netral");
}
else
{
var culture = split[split.Length - 2];
var valid = CultureHelper.IsValidCultureName(split[split.Length - 2]);
if (valid)
{
all.Add(culture);
set.Add(culture);
}
else
{
all.Add("netral");
set.Add("netral");
}
}
}
groupByFile.Add(pair.Key, set);
}
foreach(var pair in groupByFile)
{
var notExist = all.Where(l => !pair.Value.Contains(l));
if(notExist.Count() > 0)
{
allExist = false;
message += $"{pair.Key.Name}: \n";
message += string.Join(',', notExist) + "\n\n";
}
}
Assert.True(allExist, message);
}
[Test, Order(2)]
public void ResoureFilesFilled()
{
var all = new Dictionary<FileInfo, HashSet<string>>();
var groupByFile = new Dictionary<FileInfo, Dictionary<FileInfo, HashSet<string>>>();
var allExist = true;
var message = "Next resources filled less then 100%: \n";
foreach (var pair in _resources)
{
var set = new HashSet<string>();
var dictionary = new Dictionary<FileInfo, HashSet<string>>();
foreach (var resource in pair.Value)
{
var innerSet = new HashSet<string>();
foreach (var entry in CreateTranslateDictionary(resource.FullName))
{
if (!string.IsNullOrEmpty(entry.Value.ToString()))
{
set.Add(entry.Key.ToString());
innerSet.Add(entry.Key.ToString());
}
}
dictionary.Add(resource, innerSet);
}
groupByFile.Add(pair.Key, dictionary);
all.Add(pair.Key, set);
}
foreach(var pair in groupByFile)
{
foreach(var keyValue in pair.Value)
{
var notExist = all[pair.Key].Where(l => !keyValue.Value.Contains(l));
if (notExist.Count() > 0)
{
allExist = false;
var x = notExist.Count();
var y = all[pair.Key].Count();
var percent = (int)((double)(y - x) / y * 100);
message += $"{keyValue.Key.Name}: {percent}%\n";
}
}
}
Assert.True(allExist, message);
}
[Test, Order(3)]
public void CompliesToRulePunctuationLead()
{
CompliesToRule("The punctuation at the start of the messages doesn't match up:\n", CheckRules.CompliesToRulePunctuationLead);
}
[Test, Order(4)]
public void CompliesToRulePunctuationTail()
{
CompliesToRule("The punctuation at the end of the messages doesn't match up:\n", CheckRules.CompliesToRulePunctuationTail);
}
[Test, Order(5)]
public void CompliesToRuleWhiteSpaceLead()
{
CompliesToRule("The whitespaces at the start of the sequence don't match up:\n", CheckRules.CompliesToRuleWhiteSpaceLead);
}
[Test, Order(6)]
public void CompliesToRuleWhiteSpaceTail()
{
CompliesToRule("The whitespaces at the end of the sequence don't match up:\n", CheckRules.CompliesToRuleWhiteSpaceTail);
}
[Test, Order(7)]
public void CompliesToRuleWhiteStringFormat()
{
CompliesToRule("This items contains string format parameter mismatches:\n", CheckRules.CompliesToRuleStringFormat);
}
private void CompliesToRule(string message, Func<string, string, bool> compliesToRile)
{
var allRuleCheck = true;
foreach (var pair in _resources)
{
var netral = CreateTranslateDictionary(pair.Key.FullName);
foreach (var resource in pair.Value)
{
var list = new List<string>();
foreach (var entry in CreateTranslateDictionary(resource.FullName))
{
if (netral.TryGetValue(entry.Key, out var value))
{
if (compliesToRile(value, entry.Value))
{
list.Add(entry.Key);
allRuleCheck = false;
}
}
}
if (list.Count > 0)
{
message += $"\n{resource.Name}: \n";
message += string.Join(',', list) + "\n";
}
}
}
Assert.True(allRuleCheck, message);
}
private Dictionary<string, string> CreateTranslateDictionary(string filePath)
{
var dictionary = new Dictionary<string, string>();
var name = "";
using (var reader = XmlReader.Create(filePath))
{
while (reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
if (reader.Name == "data" && string.IsNullOrEmpty(reader["type"]))
{
name = reader["name"] ?? "";
}
break;
case XmlNodeType.Text:
if (!string.IsNullOrEmpty(name))
{
dictionary.Add(name, reader.Value);
name = "";
}
break;
}
}
}
return dictionary;
}
}