diff --git a/HW5/HW5.sln b/HW5/HW5.sln
new file mode 100644
index 0000000..4a5e3ec
--- /dev/null
+++ b/HW5/HW5.sln
@@ -0,0 +1,48 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyNUnit", "MyNUnit\MyNUnit.csproj", "{FEC922AC-BF64-4C53-AE57-27F93A60EA2B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyNUnit.Tests", "MyNUnit.Tests\MyNUnit.Tests.csproj", "{56F3864E-325F-4124-BC63-0AFE972FFAE1}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {FEC922AC-BF64-4C53-AE57-27F93A60EA2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FEC922AC-BF64-4C53-AE57-27F93A60EA2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FEC922AC-BF64-4C53-AE57-27F93A60EA2B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FEC922AC-BF64-4C53-AE57-27F93A60EA2B}.Debug|x64.Build.0 = Debug|Any CPU
+ {FEC922AC-BF64-4C53-AE57-27F93A60EA2B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FEC922AC-BF64-4C53-AE57-27F93A60EA2B}.Debug|x86.Build.0 = Debug|Any CPU
+ {FEC922AC-BF64-4C53-AE57-27F93A60EA2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FEC922AC-BF64-4C53-AE57-27F93A60EA2B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FEC922AC-BF64-4C53-AE57-27F93A60EA2B}.Release|x64.ActiveCfg = Release|Any CPU
+ {FEC922AC-BF64-4C53-AE57-27F93A60EA2B}.Release|x64.Build.0 = Release|Any CPU
+ {FEC922AC-BF64-4C53-AE57-27F93A60EA2B}.Release|x86.ActiveCfg = Release|Any CPU
+ {FEC922AC-BF64-4C53-AE57-27F93A60EA2B}.Release|x86.Build.0 = Release|Any CPU
+ {56F3864E-325F-4124-BC63-0AFE972FFAE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {56F3864E-325F-4124-BC63-0AFE972FFAE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {56F3864E-325F-4124-BC63-0AFE972FFAE1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {56F3864E-325F-4124-BC63-0AFE972FFAE1}.Debug|x64.Build.0 = Debug|Any CPU
+ {56F3864E-325F-4124-BC63-0AFE972FFAE1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {56F3864E-325F-4124-BC63-0AFE972FFAE1}.Debug|x86.Build.0 = Debug|Any CPU
+ {56F3864E-325F-4124-BC63-0AFE972FFAE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {56F3864E-325F-4124-BC63-0AFE972FFAE1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {56F3864E-325F-4124-BC63-0AFE972FFAE1}.Release|x64.ActiveCfg = Release|Any CPU
+ {56F3864E-325F-4124-BC63-0AFE972FFAE1}.Release|x64.Build.0 = Release|Any CPU
+ {56F3864E-325F-4124-BC63-0AFE972FFAE1}.Release|x86.ActiveCfg = Release|Any CPU
+ {56F3864E-325F-4124-BC63-0AFE972FFAE1}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/HW5/MyNUnit.Tests/Calculator.cs b/HW5/MyNUnit.Tests/Calculator.cs
new file mode 100644
index 0000000..d2a3fb2
--- /dev/null
+++ b/HW5/MyNUnit.Tests/Calculator.cs
@@ -0,0 +1,46 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+namespace MyNUnit.Tests;
+
+///
+/// Simple calculator class.
+///
+public static class Calculator
+{
+ ///
+ /// Adds two numbers.
+ ///
+ /// The first number.
+ /// The second number.
+ /// The sum of a and b.
+ public static int Add(int a, int b) => a + b;
+
+ ///
+ /// Subtracts second number from first.
+ ///
+ /// The number to subtract from.
+ /// The number to subtract.
+ /// The result of a minus b.
+ public static int Subtract(int a, int b) => a - b;
+
+ ///
+ /// Multiplies two numbers.
+ ///
+ /// The first number.
+ /// The second number.
+ /// The product of a and b.
+ public static int Multiply(int a, int b) => a * b;
+
+ ///
+ /// Divides first number by second.
+ ///
+ /// The dividend.
+ /// The divisor.
+ /// The result of a divided by b.
+ public static int Divide(int a, int b)
+ {
+ return b == 0 ? throw new DivideByZeroException() : a / b;
+ }
+}
diff --git a/HW5/MyNUnit.Tests/CalculatorTests.cs b/HW5/MyNUnit.Tests/CalculatorTests.cs
new file mode 100644
index 0000000..715f0fa
--- /dev/null
+++ b/HW5/MyNUnit.Tests/CalculatorTests.cs
@@ -0,0 +1,75 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+namespace MyNUnit.Tests;
+
+using System;
+
+///
+/// Tests for the class using MyNUnit.
+///
+public class CalculatorTests
+{
+ ///
+ /// Checks the addition of two numbers.
+ ///
+ [Test]
+ public void Calculator_Add_ShouldReturnSum()
+ {
+ var result = Calculator.Add(2, 3);
+ if (result != 5)
+ {
+ throw new InvalidOperationException($"Expected 5, but got {result}");
+ }
+ }
+
+ ///
+ /// Checks the subtraction of two numbers.
+ ///
+ [Test]
+ public void Calculator_Subtract_ShouldReturnDifference()
+ {
+ var result = Calculator.Subtract(10, 4);
+ if (result != 6)
+ {
+ throw new InvalidOperationException($"Expected 6, but got {result}");
+ }
+ }
+
+ ///
+ /// Checks division by zero (should throw an exception).
+ ///
+ [Test(Expected = typeof(DivideByZeroException))]
+ public void Calculator_Divide_ByZero_ShouldThrow()
+ {
+ Calculator.Divide(10, 0);
+ }
+
+ ///
+ /// Ignored test.
+ ///
+ [Test]
+ [Ignore("Example of a ignored test")]
+ public void Calculator_Multiply_ShouldBeIgnored()
+ {
+ var result = Calculator.Multiply(3, 4);
+ if (result != 12)
+ {
+ throw new InvalidOperationException($"Expected 12, but got {result}");
+ }
+ }
+
+ ///
+ /// A special test that intentionally crashes.
+ ///
+ [Test]
+ public void Calculator_FailedTest_ShouldFail()
+ {
+ var result = Calculator.Add(1, 1);
+ if (result != 3)
+ {
+ throw new InvalidOperationException("This test is supposed to fail");
+ }
+ }
+}
diff --git a/HW5/MyNUnit.Tests/MyNUnit.Tests.cs b/HW5/MyNUnit.Tests/MyNUnit.Tests.cs
new file mode 100644
index 0000000..80349f5
--- /dev/null
+++ b/HW5/MyNUnit.Tests/MyNUnit.Tests.cs
@@ -0,0 +1,87 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+namespace MyNUnit.Tests;
+
+using System;
+using System.Linq;
+using NUnitTest = NUnit.Framework.TestAttribute;
+
+///
+/// Tests which check the correct behavior MyNUnit.
+///
+public class MyNUnitRunnerTests
+{
+ private static readonly Type CalculatorTestsType = typeof(CalculatorTests);
+
+ ///
+ /// Test MyNUnit runs all tests.
+ ///
+ [NUnitTest]
+ public void MyNUnitRunnerTests_CalculatorTests_Run_ShouldReturnCorrectStatistics()
+ {
+ var results = TestRunner.Run([CalculatorTestsType]);
+
+ var passedCount = results.Count(r => r.Status == TestStatus.Passed);
+ var failedCount = results.Count(r => r.Status == TestStatus.Failed);
+ var ignoredCount = results.Count(r => r.Status == TestStatus.Ignored);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(passedCount, Is.EqualTo(3), "Passed tests count is incorrect");
+ Assert.That(failedCount, Is.EqualTo(1), "Failed tests count is incorrect");
+ Assert.That(ignoredCount, Is.EqualTo(1), "Ignored tests count is incorrect");
+ });
+ }
+
+ ///
+ /// Test MyNUnit with an expected exception.
+ ///
+ [NUnitTest]
+ public void MyNUnitRunnerTests_CalculatorTests_ExpectedException_ShouldPass()
+ {
+ var results = TestRunner.Run([CalculatorTestsType]);
+
+ var testResult = results.First(
+ r => r.TestName.EndsWith("Calculator_Divide_ByZero_ShouldThrow"));
+
+ Assert.That(testResult.Status, Is.EqualTo(TestStatus.Passed));
+ }
+
+ ///
+ /// Test MyNUnit ignored tests.
+ ///
+ [NUnitTest]
+ public void MyNUnitRunnerTests_CalculatorTests_IgnoredTest_ShouldBeIgnored()
+ {
+ var results = TestRunner.Run([CalculatorTestsType]);
+
+ var testResult = results.First(
+ r => r.TestName.EndsWith("Calculator_Multiply_ShouldBeIgnored"));
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(testResult.Status, Is.EqualTo(TestStatus.Ignored));
+ Assert.That(testResult.Message, Is.Not.Empty);
+ });
+ }
+
+ ///
+ /// Test MyNUnit a failing test.
+ ///
+ [NUnitTest]
+ public void MyNUnitRunnerTests_CalculatorTests_FailedTest_ShouldFail()
+ {
+ var results = TestRunner.Run([CalculatorTestsType]);
+
+ var testResult = results.First(
+ r => r.TestName.EndsWith("Calculator_FailedTest_ShouldFail"));
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(testResult.Status, Is.EqualTo(TestStatus.Failed));
+ Assert.That(testResult.Exception, Is.Not.Null);
+ });
+ }
+}
diff --git a/HW5/MyNUnit.Tests/MyNUnit.Tests.csproj b/HW5/MyNUnit.Tests/MyNUnit.Tests.csproj
new file mode 100644
index 0000000..5fd3579
--- /dev/null
+++ b/HW5/MyNUnit.Tests/MyNUnit.Tests.csproj
@@ -0,0 +1,39 @@
+
+
+
+ net9.0
+ latest
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HW5/MyNUnit.Tests/stylecop.json b/HW5/MyNUnit.Tests/stylecop.json
new file mode 100644
index 0000000..76c8e76
--- /dev/null
+++ b/HW5/MyNUnit.Tests/stylecop.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "documentationRules": {
+ "companyName": "khusainovilas",
+ "copyrightText": "Copyright (c) {companyName}. All rights reserved."
+ }
+ }
+}
\ No newline at end of file
diff --git a/HW5/MyNUnit/Attributes.cs b/HW5/MyNUnit/Attributes.cs
new file mode 100644
index 0000000..46f2bac
--- /dev/null
+++ b/HW5/MyNUnit/Attributes.cs
@@ -0,0 +1,70 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+namespace MyNUnit;
+
+///
+/// Marks the method as a test.
+///
+[AttributeUsage(AttributeTargets.Method)]
+public class TestAttribute : Attribute
+{
+ ///
+ /// Gets or sets the expected exception type.
+ ///
+ public Type? Expected { get; set; }
+}
+
+///
+/// Skip the test with an indication of the reason.
+///
+[AttributeUsage(AttributeTargets.Method)]
+public class IgnoreAttribute : Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The reason for ignoring the test.
+ public IgnoreAttribute(string reason)
+ {
+ this.Reason = reason ?? throw new ArgumentNullException(nameof(reason));
+ }
+
+ ///
+ /// Gets the reason why the test is ignored.
+ ///
+ public string Reason { get; }
+}
+
+///
+/// Runs once before all tests in the class.
+///
+[AttributeUsage(AttributeTargets.Method)]
+public class BeforeClassAttribute : Attribute
+{
+}
+
+///
+/// Runs once after all the tests in the class.
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+public class AfterClassAttribute : Attribute
+{
+}
+
+///
+/// Runs before each test.
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+public class BeforeAttribute : Attribute
+{
+}
+
+///
+/// Runs after each test.
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+public class AfterAttribute : Attribute
+{
+}
diff --git a/HW5/MyNUnit/ConsoleReporter.cs b/HW5/MyNUnit/ConsoleReporter.cs
new file mode 100644
index 0000000..5f72050
--- /dev/null
+++ b/HW5/MyNUnit/ConsoleReporter.cs
@@ -0,0 +1,89 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+namespace MyNUnit;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+///
+/// Prints test results to console.
+///
+public static class ConsoleReporter
+{
+ ///
+ /// Prints test execution report.
+ ///
+ /// A collection of objects to print.
+ public static void Print(IEnumerable results)
+ {
+ ArgumentNullException.ThrowIfNull(results);
+
+ var list = results.ToList();
+
+ foreach (var result in list.OrderBy(r => r.TestName))
+ {
+ PrintSingle(result);
+ }
+
+ PrintSummary(list);
+ }
+
+ private static void PrintSingle(TestResult result)
+ {
+ switch (result.Status)
+ {
+ case TestStatus.Passed:
+ Console.WriteLine(
+ $"[PASS] {result.TestName} ({Format(result.Duration)})");
+ break;
+
+ case TestStatus.Failed:
+ Console.WriteLine(
+ $"[FAIL] {result.TestName} ({Format(result.Duration)})");
+
+ if (!string.IsNullOrWhiteSpace(result.Message))
+ {
+ Console.WriteLine($" {result.Message}");
+ }
+
+ if (result.Exception != null)
+ {
+ Console.WriteLine(
+ $" {result.Exception.GetType().Name}: {result.Exception.Message}");
+ }
+
+ break;
+
+ case TestStatus.Ignored:
+ Console.WriteLine($"[IGNORED] {result.TestName}");
+ Console.WriteLine($" Reason: {result.Message}");
+ break;
+ }
+ }
+
+ private static void PrintSummary(IReadOnlyCollection results)
+ {
+ Console.WriteLine();
+ Console.WriteLine(new string('=', 40));
+
+ Console.WriteLine($"Total: {results.Count}");
+ Console.WriteLine($"Passed: {results.Count(r => r.Status == TestStatus.Passed)}");
+ Console.WriteLine($"Failed: {results.Count(r => r.Status == TestStatus.Failed)}");
+ Console.WriteLine($"Ignored: {results.Count(r => r.Status == TestStatus.Ignored)}");
+
+ var totalTime = TimeSpan.FromTicks(results.Sum(r => r.Duration.Ticks));
+ Console.WriteLine($"Time: {Format(totalTime)}");
+
+ Console.WriteLine(new string('=', 40));
+ }
+
+ private static string Format(TimeSpan time)
+ {
+ return time.TotalMilliseconds < 1
+ ? "<1 ms"
+ : $"{time.TotalMilliseconds:F0} ms";
+ }
+}
diff --git a/HW5/MyNUnit/MyNUnit.csproj b/HW5/MyNUnit/MyNUnit.csproj
new file mode 100644
index 0000000..3fef999
--- /dev/null
+++ b/HW5/MyNUnit/MyNUnit.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/HW5/MyNUnit/Program.cs b/HW5/MyNUnit/Program.cs
new file mode 100644
index 0000000..2299290
--- /dev/null
+++ b/HW5/MyNUnit/Program.cs
@@ -0,0 +1,39 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+using MyNUnit;
+
+if (args.Length != 1)
+{
+ PrintUsage();
+ return;
+}
+
+try
+{
+ var testClasses = TestDiscovery.Discover(args[0]);
+
+ if (testClasses.Count == 0)
+ {
+ Console.WriteLine("No tests found.");
+ return;
+ }
+
+ var results = TestRunner.Run(testClasses);
+ ConsoleReporter.Print(results);
+}
+catch (Exception ex)
+{
+ Console.WriteLine("[ERROR]");
+ Console.WriteLine(ex.Message);
+}
+
+static void PrintUsage()
+{
+ Console.WriteLine("Usage:");
+ Console.WriteLine(" MyNUnit ");
+ Console.WriteLine();
+ Console.WriteLine("Arguments:");
+ Console.WriteLine(" Path to directory or .dll file containing tests.");
+}
diff --git a/HW5/MyNUnit/TestDiscovery.cs b/HW5/MyNUnit/TestDiscovery.cs
new file mode 100644
index 0000000..8b9a2ab
--- /dev/null
+++ b/HW5/MyNUnit/TestDiscovery.cs
@@ -0,0 +1,89 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+namespace MyNUnit;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+///
+/// Searches for test classes and test methods in assemblies.
+///
+public static class TestDiscovery
+{
+ ///
+ /// Finds all test classes in the specified path.
+ ///
+ /// Path to directory or .dll file.
+ /// List of test class types.
+ public static IReadOnlyList Discover(string path)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(path);
+
+ var result = new List();
+
+ foreach (var assembly in LoadAssemblies(path))
+ {
+ foreach (var type in assembly.GetTypes())
+ {
+ if (ContainsTests(type))
+ {
+ result.Add(type);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private static bool ContainsTests(Type type)
+ {
+ return type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
+ .Any(m => m.GetCustomAttribute() != null);
+ }
+
+ private static IEnumerable LoadAssemblies(string path)
+ {
+ if (Directory.Exists(path))
+ {
+ foreach (var file in Directory.GetFiles(path, "*.dll", SearchOption.AllDirectories))
+ {
+ var assembly = TryLoadAssembly(file);
+ if (assembly != null)
+ {
+ yield return assembly;
+ }
+ }
+ }
+ else if (File.Exists(path) && path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
+ {
+ var assembly = TryLoadAssembly(path);
+ if (assembly != null)
+ {
+ yield return assembly;
+ }
+ }
+ else
+ {
+ throw new FileNotFoundException($"Path not found: {path}");
+ }
+ }
+
+ private static Assembly? TryLoadAssembly(string filePath)
+ {
+ try
+ {
+ return Assembly.LoadFrom(filePath);
+ }
+ catch (Exception ex) when (ex is BadImageFormatException or FileLoadException)
+ {
+ Console.WriteLine($"[Warning] Failed to load assembly: {filePath}");
+ Console.WriteLine($" {ex.Message}");
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/HW5/MyNUnit/TestResult.cs b/HW5/MyNUnit/TestResult.cs
new file mode 100644
index 0000000..2f0fa4f
--- /dev/null
+++ b/HW5/MyNUnit/TestResult.cs
@@ -0,0 +1,59 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+namespace MyNUnit;
+
+using System;
+
+///
+/// Represents the result of a single test execution.
+///
+public class TestResult
+{
+ ///
+ /// Gets test full name.
+ ///
+ public string TestName { get; init; } = string.Empty;
+
+ ///
+ /// Gets test execution status.
+ ///
+ public TestStatus Status { get; init; }
+
+ ///
+ /// Gets test execution duration.
+ ///
+ public TimeSpan Duration { get; init; }
+
+ ///
+ /// Gets additional message (failure reason or ignore reason).
+ ///
+ public string? Message { get; init; }
+
+ ///
+ /// Gets exception thrown by test.
+ ///
+ public Exception? Exception { get; init; }
+}
+
+///
+/// Test execution status.
+///
+public enum TestStatus
+{
+ ///
+ /// Test passed successfully.
+ ///
+ Passed,
+
+ ///
+ /// Test failed.
+ ///
+ Failed,
+
+ ///
+ /// Test was ignored.
+ ///
+ Ignored,
+}
\ No newline at end of file
diff --git a/HW5/MyNUnit/TestRunner.cs b/HW5/MyNUnit/TestRunner.cs
new file mode 100644
index 0000000..6bd4e40
--- /dev/null
+++ b/HW5/MyNUnit/TestRunner.cs
@@ -0,0 +1,220 @@
+//
+// Copyright (c) khusainovilas. All rights reserved.
+//
+
+namespace MyNUnit;
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+
+///
+/// Executes discovered tests.
+///
+public static class TestRunner
+{
+ ///
+ /// Runs all tests in given test classes.
+ ///
+ /// Collection of types containing test methods.
+ ///
+ /// Collection of results for all executed tests.
+ ///
+ public static IReadOnlyList Run(IEnumerable testClasses)
+ {
+ ArgumentNullException.ThrowIfNull(testClasses);
+
+ var results = new ConcurrentBag();
+
+ Parallel.ForEach(testClasses, testClass =>
+ {
+ RunTestsInClass(testClass, results);
+ });
+
+ return results.ToList();
+ }
+
+ private static void RunTestsInClass(Type testClass, ConcurrentBag results)
+ {
+ var beforeClass = GetMethods(testClass, true);
+ var afterClass = GetMethods(testClass, true);
+ var before = GetMethods(testClass, false);
+ var after = GetMethods(testClass, false);
+ var tests = GetMethods(testClass, false);
+
+ try
+ {
+ InvokeStatic(beforeClass);
+
+ foreach (var test in tests)
+ {
+ results.Add(RunSingleTest(testClass, test, before, after));
+ }
+ }
+ catch (Exception ex)
+ {
+ foreach (var test in tests)
+ {
+ results.Add(new TestResult
+ {
+ TestName = $"{testClass.FullName}.{test.Name}",
+ Status = TestStatus.Failed,
+ Message = "BeforeClass failed",
+ Exception = ex,
+ });
+ }
+ }
+ finally
+ {
+ InvokeStatic(afterClass);
+ }
+ }
+
+ private static TestResult RunSingleTest(Type testClass, MethodInfo testMethod, IReadOnlyList before, IReadOnlyList after)
+ {
+ var testName = $"{testClass.FullName}.{testMethod.Name}";
+ var ignore = testMethod.GetCustomAttribute();
+ var testAttr = testMethod.GetCustomAttribute();
+
+ if (ignore != null)
+ {
+ return new TestResult
+ {
+ TestName = testName,
+ Status = TestStatus.Ignored,
+ Message = ignore.Reason,
+ Duration = TimeSpan.Zero,
+ };
+ }
+
+ if (!IsValidTestMethod(testMethod))
+ {
+ return new TestResult
+ {
+ TestName = testName,
+ Status = TestStatus.Failed,
+ Message = "Test method must be public void with no parameters",
+ };
+ }
+
+ var stopwatch = Stopwatch.StartNew();
+ object? instance = null;
+
+ try
+ {
+ instance = Activator.CreateInstance(testClass) ?? throw new InvalidOperationException($"Failed to create instance of {testClass.FullName}");
+ InvokeInstance(before, instance);
+ testMethod.Invoke(instance, null);
+ InvokeInstance(after, instance);
+
+ stopwatch.Stop();
+
+ if (testAttr?.Expected != null)
+ {
+ return Fail(testName, "Expected exception was not thrown", stopwatch.Elapsed);
+ }
+
+ return Pass(testName, stopwatch.Elapsed);
+ }
+ catch (TargetInvocationException ex)
+ {
+ stopwatch.Stop();
+ InvokeAfterSafely(after, instance);
+
+ var actual = ex.InnerException!;
+
+ if (testAttr?.Expected != null &&
+ testAttr.Expected.IsAssignableFrom(actual.GetType()))
+ {
+ return Pass(testName, stopwatch.Elapsed);
+ }
+
+ return Fail(testName, actual.Message, stopwatch.Elapsed, actual);
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+ InvokeAfterSafely(after, instance);
+
+ return Fail(testName, ex.Message, stopwatch.Elapsed, ex);
+ }
+ }
+
+ private static bool IsValidTestMethod(MethodInfo method)
+ {
+ return method.IsPublic &&
+ !method.IsStatic &&
+ method.ReturnType == typeof(void) &&
+ method.GetParameters().Length == 0;
+ }
+
+ private static IReadOnlyList GetMethods(Type type, bool isStatic)
+ where T : Attribute => type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic |
+ (isStatic ? BindingFlags.Static : BindingFlags.Instance))
+ .Where(m => m.GetCustomAttribute() != null)
+ .ToList();
+
+ private static void InvokeStatic(IEnumerable methods)
+ {
+ foreach (var method in methods)
+ {
+ if (!method.IsStatic)
+ {
+ throw new InvalidOperationException("BeforeClass/AfterClass must be static");
+ }
+
+ method.Invoke(null, null);
+ }
+ }
+
+ private static void InvokeInstance(IEnumerable methods, object instance)
+ {
+ foreach (var method in methods)
+ {
+ method.Invoke(instance, null);
+ }
+ }
+
+ private static void InvokeAfterSafely(IEnumerable methods, object? instance)
+ {
+ if (instance == null)
+ {
+ return;
+ }
+
+ try
+ {
+ InvokeInstance(methods, instance);
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ private static TestResult Pass(string name, TimeSpan time)
+ {
+ return new TestResult
+ {
+ TestName = name,
+ Status = TestStatus.Passed,
+ Duration = time,
+ };
+ }
+
+ private static TestResult Fail(string name, string message, TimeSpan time = default, Exception? exception = null)
+ {
+ return new TestResult
+ {
+ TestName = name,
+ Status = TestStatus.Failed,
+ Duration = time,
+ Message = message,
+ Exception = exception,
+ };
+ }
+}
diff --git a/HW5/MyNUnit/stylecop.json b/HW5/MyNUnit/stylecop.json
new file mode 100644
index 0000000..76c8e76
--- /dev/null
+++ b/HW5/MyNUnit/stylecop.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "documentationRules": {
+ "companyName": "khusainovilas",
+ "copyrightText": "Copyright (c) {companyName}. All rights reserved."
+ }
+ }
+}
\ No newline at end of file