Skip to content
Merged
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
70 changes: 70 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Правила для GitHub Copilot

- Всегда отвечай, используя русский язык
- Всегда пиши комментарии в коде на русском языке

## Комментарии
- Короткие пояснительные комментарии располагай в конце той же строки, что и код // кратко по делу
- Старайся избегать тривиальных комментариев

## XML‑документация
- Документируй классы, структуры, делегаты, перечисления и их члены только XML‑комментариями
- Одинарное предложение пиши в одной строке внутри тега и без точки в конце
- Каждый тег XML‑комментария располагай на отдельной строке
- Порядок тегов: `<summary>` → `<param>` → `<returns>` → `<exception>` → `<remarks>` → `<example>`
- Для сложных публичных метдов генерируй блок с простым примером использования кода внутри тега `<example>`

Примеры:
- `<summary>Краткое описание сущности</summary>`
- `<param name="Value">Описание параметра</param>`
- `<returns>Описание возвращаемого значения</returns>`

## Синтаксис и минимализм
- При генерации кода используй современные конструкции языка, совместимые с целевыми платформами проекта
- Стремись минимизировать количество фигурных скобок за счёт expression‑bodied членов и switch‑выражений
- Не убирай фигурные скобки в многострочных конструкциях ради читаемости
- Всегда старайся минимизировать размер кода, если не запрошено иное

Разрешённые современные приёмы (когда поддерживается целевой платформой):
- file‑scoped namespace
- expression‑bodied члены
- switch‑выражения и pattern matching
- target‑typed `new`
- collection expressions и инициализаторы коллекций
- `using var` и `await using`
- операторы `??`, `??=`, `is not`, `with`
- упрощение nullable-присвоения `target?.Property = 15;` вместо `if(target is not null) target.Property = 15;`

## Именование
- Локальные переменные: `snake_case`
- Параметры методов: `PascalCase`
- Поля экземпляров: `_PascalCase`
- Статические поля: `__PascalCase`
- Константы: `PascalCase`
- Публичные типы и члены API: `PascalCase`
- Предпочитай английский язык при именовании переменных, методов, классов и прочих сущностей

## Инициализация и объявления
- При инициализации массивов, списков и словарей используй выражения инициализации массивов/коллекций
- При объявлении переменных предпочитай использовать ключевое слово `var` (кроме случаев, когда явный тип заметно повышает понятность)

## Форматирование
- Короткие системные комментарии пиши компактно в одну строку
- Удаляй неиспользуемые `using`, сортируй и группируй директивы `using`
- Разделяй логические блоки пустыми строками по мере необходимости, избегай лишних переносов

## Практики .NET
- Включай `#nullable enable` там, где это поддерживается
- Используй guard‑выражения, например `ArgumentNullException.ThrowIfNull(x)`
- Предпочитай Try‑паттерны для контроля потока вместо исключений
- При генерации метода добавляй в его начале блок проверки входных параметров. Отделяй этот блок пустой строкой от остального тела метода
- При генерации публичных свойств у моделей-представления MVVM (классов, реализующих INotifyPropertyChanged) используй следующий формат (в одну строку):
```csharp
/// <summary>Описание свойства</summary>
public string PropertyName { get; set => Set(ref field, value); }
```
- Для простых лаконичных методов используй expression‑bodied синтаксис, записанный в одну строку.

## Совместимость целей
- В рабочем пространстве используются целевые платформы: `.NET Standard 2.0` и `.NET 10`
- Применяй современные возможности языка и платформы только если они доступны для соответствующей целевой платформы проекта
10 changes: 5 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
dotnet-version: 10.0.x

- name: Cache NuGet
uses: actions/cache@v3
Expand Down Expand Up @@ -61,10 +61,10 @@ jobs:
- name: Install .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
dotnet-version: 10.0.x

- name: Get artifact
uses: actions/download-artifact@v3.0.1
uses: actions/download-artifact@v4.1.7
id: download
with:
name: Release
Expand All @@ -82,10 +82,10 @@ jobs:
- name: Install .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
dotnet-version: 10.0.x

- name: Get artifact
uses: actions/download-artifact@v3.0.1
uses: actions/download-artifact@v4.1.7
id: download
with:
name: Release
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
dotnet-version: 10.0.x

- name: Building
run: dotnet build -c Debug
Expand Down
24 changes: 22 additions & 2 deletions MathCore.TestsExtensions.sln
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33103.184
# Visual Studio Version 18
VisualStudioVersion = 18.0.11205.157 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MathCore.TestsExtensions", "MathCore.TestsExtensions\MathCore.TestsExtensions.csproj", "{0F039156-E6CB-4EFB-A863-13A1AC2DD4AE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MathCore.TestsExtensions.Tests", "Tests\MathCore.TestsExtensions.Tests\MathCore.TestsExtensions.Tests.csproj", "{10093BD2-D872-4383-B075-92850E009647}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".Service", ".Service", "{29B29638-89EC-4383-B587-AE8C981DED4F}"
ProjectSection(SolutionItems) = preProject
.github\copilot-instructions.md = .github\copilot-instructions.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{D6A23A02-D860-4101-AB7B-0C7FB57331CB}"
ProjectSection(SolutionItems) = preProject
.github\copilot-instructions.md = .github\copilot-instructions.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{83B95A2F-1D99-43CB-BA0C-769D88078D6F}"
ProjectSection(SolutionItems) = preProject
.github\workflows\publish.yml = .github\workflows\publish.yml
.github\workflows\testing.yml = .github\workflows\testing.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -25,6 +41,10 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D6A23A02-D860-4101-AB7B-0C7FB57331CB} = {29B29638-89EC-4383-B587-AE8C981DED4F}
{83B95A2F-1D99-43CB-BA0C-769D88078D6F} = {D6A23A02-D860-4101-AB7B-0C7FB57331CB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5C7C0D00-B422-4B19-B6D4-19B55F78389C}
EndGlobalSection
Expand Down
68 changes: 62 additions & 6 deletions MathCore.TestsExtensions/Accuracy.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,73 @@
namespace Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>Утилиты для создания сравнителей точности и пользовательских сравнителей/проверок равенства</summary>
public static class Accuracy
{
/// <summary>Создаёт сравнитель чисел типа <see cref="double"/> с заданной абсолютной точностью</summary>
/// <param name="eps">Допустимое отклонение (ε). Должно быть неотрицательным и не NaN</param>
/// <returns>Экземпляр <see cref="IEqualityComparer{Double}"/> и <see cref="IComparer{Double}"/></returns>
/// <exception cref="ArgumentOutOfRangeException">Если <paramref name="eps"/> &lt; 0</exception>
/// <exception cref="ArgumentException">Если <paramref name="eps"/> равен <see cref="double.NaN"/></exception>
public static IEqualityComparer<double> Eps(double eps) => new AccuracyComparer(eps);

/// <summary>Создаёт сравнитель целых чисел типа <see cref="int"/> с заданной абсолютной точностью</summary>
/// <param name="eps">Допустимое отклонение (ε). Должно быть неотрицательным</param>
/// <returns>Экземпляр <see cref="IEqualityComparer{Int32}"/> и <see cref="IComparer{Int32}"/></returns>
/// <exception cref="ArgumentOutOfRangeException">Если <paramref name="eps"/> &lt; 0</exception>
public static IEqualityComparer<int> Eps(int eps) => new AccuracyComparer(eps);

public static IEqualityComparer<T> Equals<T>(Func<T, T, bool> Comparer, Func<T, int> Hasher) => new AccuracyEqualityComparer<T>(Comparer, Hasher);
public static IComparer<T> Compare<T>(Comparison<T> Comparer) => new AccuracyComparer<T>(Comparer);
/// <summary>Создаёт универсальный сравнитель равенства на основе делегатов</summary>
/// <typeparam name="T">Тип сравниваемых объектов</typeparam>
/// <param name="Comparer">Делегат, выполняющий проверку равенства</param>
/// <param name="Hasher">Делегат, вычисляющий хеш-код</param>
/// <returns><see cref="IEqualityComparer{T}"/> использующий предоставленные делегаты</returns>
/// <exception cref="ArgumentNullException">Если один из делегатов равен null</exception>
public static IEqualityComparer<T> Equals<T>(Func<T, T, bool> Comparer, Func<T, int> Hasher) =>
new AccuracyEqualityComparer<T>(Comparer ?? throw new ArgumentNullException(nameof(Comparer)),
Hasher ?? throw new ArgumentNullException(nameof(Hasher)));

/// <summary>Создаёт универсальный сравнитель порядка на основе делегата <see cref="Comparison{T}"/></summary>
/// <typeparam name="T">Тип сравниваемых объектов</typeparam>
/// <param name="Comparer">Делегат сравнения</param>
/// <returns><see cref="IComparer{T}"/> использующий предоставленный делегат</returns>
/// <exception cref="ArgumentNullException">Если <paramref name="Comparer"/> равен null</exception>
public static IComparer<T> Compare<T>(Comparison<T> Comparer) =>
new AccuracyComparer<T>(Comparer ?? throw new ArgumentNullException(nameof(Comparer)));
}

/// <summary>Сравнитель значений типов <see cref="double"/> и <see cref="int"/> с учётом допустимой абсолютной погрешности</summary>
/// <remarks>
/// Для чисел с плавающей точкой равенство определяется условием |x - y| ≤ ε.
/// Для операций хеширования используется нормализация по шагу ε (округление к сетке).
/// </remarks>
/// <param name="Eps">Допустимое отклонение (ε). Должно быть неотрицательным</param>
public readonly struct AccuracyComparer(double Eps) :
IEqualityComparer<double>, IComparer<double>,
IEqualityComparer<int>, IComparer<int>
{
/// <summary>Допустимое абсолютное отклонение</summary>
private double Eps { get; init; } = Eps switch
{
< 0 => throw new ArgumentOutOfRangeException(nameof(Eps), Eps, "Значение точности не должно быть меньше нуля"),
double.NaN => throw new ArgumentException("Значение точности не должно быть NaN", nameof(Eps)),
_ => Eps
};

/// <summary>Проверяет равенство двух значений <see cref="double"/> с учётом точности</summary>
public bool Equals(double x, double y) => Math.Abs(x - y) <= Eps;

public int GetHashCode(double x) => x is double.NaN
? x.GetHashCode()
/// <summary>Вычисляет хеш-код для значения <see cref="double"/>, нормализуя его относительно точности</summary>
/// <param name="x">Значение</param>
/// <returns>Хеш-код</returns>
public int GetHashCode(double x) => x is double.NaN
? x.GetHashCode()
: (Math.Round(x * Eps) / Eps).GetHashCode();

/// <summary>Сравнивает два значения <see cref="double"/> с учётом допустимой погрешности</summary>
/// <param name="x">Первое значение</param>
/// <param name="y">Второе значение</param>
/// <returns>0 если считаются равными; -1 или 1 в зависимости от знака разности</returns>
/// <exception cref="InvalidOperationException">Если одно из значений равно <see cref="double.NaN"/></exception>
public int Compare(double x, double y)
{
var delta = x - y;
Expand All @@ -38,15 +80,19 @@ public int Compare(double x, double y)
{ nameof(y), y },
}
};
return Math.Abs(delta) <= Eps
? 0
return Math.Abs(delta) <= Eps
? 0
: Math.Sign(delta);
}

/// <summary>Проверяет равенство двух значений <see cref="int"/> с учётом допустимого отклонения</summary>
public bool Equals(int x, int y) => Math.Abs(x - y) <= Eps;

/// <summary>Вычисляет хеш-код для целого значения с учётом нормализации к сетке точности</summary>
public int GetHashCode(int x) => (Math.Round(x * Eps) / Eps).GetHashCode();

/// <summary>Сравнивает два значения <see cref="int"/> с учётом допустимого отклонения</summary>
/// <returns>0 если считаются равными; -1 или 1 в зависимости от знака разности</returns>
public int Compare(int x, int y)
{
var delta = x - y;
Expand All @@ -56,14 +102,24 @@ public int Compare(int x, int y)
}
}

/// <summary>Универсальный сравнитель равенства на основе переданных делегатов</summary>
/// <typeparam name="T">Тип сравниваемых объектов</typeparam>
/// <param name="Comparer">Делегат проверки равенства</param>
/// <param name="Hasher">Делегат вычисления хеш-кода</param>
public class AccuracyEqualityComparer<T>(Func<T, T, bool> Comparer, Func<T, int> Hasher) : IEqualityComparer<T>
{
/// <summary>Проверяет равенство двух объектов</summary>
public bool Equals(T x, T y) => Comparer(x, y);

/// <summary>Вычисляет хеш-код объекта</summary>
public int GetHashCode(T obj) => Hasher(obj);
}

/// <summary>Универсальный сравнитель порядка на основе делегата <see cref="Comparison{T}"/></summary>
/// <typeparam name="T">Тип сравниваемых объектов</typeparam>
/// <param name="Comparer">Делегат сравнения</param>
public class AccuracyComparer<T>(Comparison<T> Comparer) : IComparer<T>
{
/// <summary>Сравнивает два значения</summary>
public int Compare(T x, T y) => Comparer(x, y);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global

using System.Runtime.CompilerServices;

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>Итерационное выполнение теста на основе данных с заданием числа итераций для набора статистики</summary>
/// <remarks>Инициализация итерационного теста на основе данных</remarks>
/// <param name="IterationsCount">Число итераций</param>
[AttributeUsage(AttributeTargets.Method)]
public class DataTestMethodIterativeAttribute(int IterationsCount) : TestMethodAttribute
public class DataTestMethodIterativeAttribute(int IterationsCount, [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = -1)
: TestMethodAttribute(callerFilePath, callerLineNumber)
{
/// <summary>Число итераций повторения теста</summary>
private readonly int _IterationsCount = IterationsCount;
Expand All @@ -17,13 +20,13 @@ public class DataTestMethodIterativeAttribute(int IterationsCount) : TestMethodA
public bool StopAtFirstFail { get; set; }

/// <inheritdoc />
public override TestResult[] Execute(ITestMethod TestMethod)
public override async Task<TestResult[]> ExecuteAsync(ITestMethod TestMethod)
{
var results = new List<TestResult>();
var stop_at_first_fail = this.StopAtFirstFail;
for (var count = 0; count < _IterationsCount; count++)
{
var test_results = base.Execute(TestMethod);
var test_results = await base.ExecuteAsync(TestMethod);
results.AddRange(test_results);
if (stop_at_first_fail && test_results.Any(r => r.TestFailureException != null)) break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ public override TestMethodAttribute GetTestMethodAttribute(TestMethodAttribute A
HandlePassed = HandlePassed,
};

return base.GetTestMethodAttribute(Attribute);
return base.GetTestMethodAttribute(Attribute!)!;
}
}
Loading