From 7340049ed72c4130a3be82dcb62a1284052ff5ea Mon Sep 17 00:00:00 2001 From: khusainovilas Date: Sat, 20 Dec 2025 20:10:42 +0300 Subject: [PATCH] init proj --- HW5/HW5.sln | 48 ++++++ HW5/MyNUnit.Tests/Calculator.cs | 46 ++++++ HW5/MyNUnit.Tests/CalculatorTests.cs | 75 +++++++++ HW5/MyNUnit.Tests/MyNUnit.Tests.cs | 87 ++++++++++ HW5/MyNUnit.Tests/MyNUnit.Tests.csproj | 39 +++++ HW5/MyNUnit.Tests/stylecop.json | 9 + HW5/MyNUnit/Attributes.cs | 70 ++++++++ HW5/MyNUnit/ConsoleReporter.cs | 89 ++++++++++ HW5/MyNUnit/MyNUnit.csproj | 22 +++ HW5/MyNUnit/Program.cs | 39 +++++ HW5/MyNUnit/TestDiscovery.cs | 89 ++++++++++ HW5/MyNUnit/TestResult.cs | 59 +++++++ HW5/MyNUnit/TestRunner.cs | 220 +++++++++++++++++++++++++ HW5/MyNUnit/stylecop.json | 9 + 14 files changed, 901 insertions(+) create mode 100644 HW5/HW5.sln create mode 100644 HW5/MyNUnit.Tests/Calculator.cs create mode 100644 HW5/MyNUnit.Tests/CalculatorTests.cs create mode 100644 HW5/MyNUnit.Tests/MyNUnit.Tests.cs create mode 100644 HW5/MyNUnit.Tests/MyNUnit.Tests.csproj create mode 100644 HW5/MyNUnit.Tests/stylecop.json create mode 100644 HW5/MyNUnit/Attributes.cs create mode 100644 HW5/MyNUnit/ConsoleReporter.cs create mode 100644 HW5/MyNUnit/MyNUnit.csproj create mode 100644 HW5/MyNUnit/Program.cs create mode 100644 HW5/MyNUnit/TestDiscovery.cs create mode 100644 HW5/MyNUnit/TestResult.cs create mode 100644 HW5/MyNUnit/TestRunner.cs create mode 100644 HW5/MyNUnit/stylecop.json 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