diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d68c7727..31aa4108 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,16 +12,16 @@ env: jobs: build: name: build - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Fetch all tags and branches run: git fetch --prune --unshallow - name: Build - run: ./build.ps1 + run: ./build.sh - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: path: artifacts/*.nupkg diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 09754584..08301029 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,18 +10,18 @@ env: jobs: publish: name: publish - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Fetch all tags and branches run: git fetch --prune --unshallow - name: Deploy env: NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} - run: ./build.ps1 publish + run: ./build.sh publish - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: path: artifacts/*.nupkg diff --git a/Directory.Build.props b/Directory.Build.props index 52d199dd..5cb6d1d6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,4 @@ - - Machine.Specifications - Machine Specifications @@ -17,4 +14,5 @@ + diff --git a/Machine.Specifications.sln b/Machine.Specifications.sln index 7dfb5900..690c67c1 100644 --- a/Machine.Specifications.sln +++ b/Machine.Specifications.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.12.35527.113 d17.12 +VisualStudioVersion = 17.12.35527.113 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Machine.Specifications", "src\Machine.Specifications\Machine.Specifications.csproj", "{EC054D80-8858-4A61-9FD9-0185EA3F4643}" EndProject @@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Machine.Specifications.Fake EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Machine.Specifications.Fakes.Specs", "src\Machine.Specifications.Fakes.Specs\Machine.Specifications.Fakes.Specs.csproj", "{B897126F-BF01-4A97-965D-C2DE1BC63460}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Machine.Specifications.Runner.VisualStudio", "src\Machine.Specifications.Runner.VisualStudio\Machine.Specifications.Runner.VisualStudio.csproj", "{D7689810-9604-4E3E-9821-28ABA130B28E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -81,6 +83,10 @@ Global {B897126F-BF01-4A97-965D-C2DE1BC63460}.Debug|Any CPU.Build.0 = Debug|Any CPU {B897126F-BF01-4A97-965D-C2DE1BC63460}.Release|Any CPU.ActiveCfg = Release|Any CPU {B897126F-BF01-4A97-965D-C2DE1BC63460}.Release|Any CPU.Build.0 = Release|Any CPU + {D7689810-9604-4E3E-9821-28ABA130B28E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7689810-9604-4E3E-9821-28ABA130B28E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7689810-9604-4E3E-9821-28ABA130B28E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7689810-9604-4E3E-9821-28ABA130B28E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Machine.Specifications.Analyzers.Tests/Machine.Specifications.Analyzers.Tests.csproj b/src/Machine.Specifications.Analyzers.Tests/Machine.Specifications.Analyzers.Tests.csproj index f5030e7a..9c570f19 100644 --- a/src/Machine.Specifications.Analyzers.Tests/Machine.Specifications.Analyzers.Tests.csproj +++ b/src/Machine.Specifications.Analyzers.Tests/Machine.Specifications.Analyzers.Tests.csproj @@ -2,9 +2,11 @@ net8.0 + Exe enable enable latest + true false @@ -13,15 +15,10 @@ - + - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + diff --git a/src/Machine.Specifications.Analyzers/Machine.Specifications.Analyzers.csproj b/src/Machine.Specifications.Analyzers/Machine.Specifications.Analyzers.csproj index e05b64db..9b077b5c 100644 --- a/src/Machine.Specifications.Analyzers/Machine.Specifications.Analyzers.csproj +++ b/src/Machine.Specifications.Analyzers/Machine.Specifications.Analyzers.csproj @@ -5,6 +5,7 @@ enable enable latest + false true true $(TargetsForTfmSpecificContentInPackage);PackageItems @@ -15,7 +16,7 @@ - + diff --git a/src/Machine.Specifications.Analyzers/Maintainability/AccessModifierShouldNotBeUsedCodeFixProvider.cs b/src/Machine.Specifications.Analyzers/Maintainability/AccessModifierShouldNotBeUsedCodeFixProvider.cs index 52d0b017..1d002082 100644 --- a/src/Machine.Specifications.Analyzers/Maintainability/AccessModifierShouldNotBeUsedCodeFixProvider.cs +++ b/src/Machine.Specifications.Analyzers/Maintainability/AccessModifierShouldNotBeUsedCodeFixProvider.cs @@ -87,7 +87,7 @@ private SyntaxNode HandleDeclaration(MemberDeclarationSyntax declaration) .WithLeadingTrivia(trivia); } - private SyntaxNode GetParentDeclaration(SyntaxNode declaration) + private SyntaxNode? GetParentDeclaration(SyntaxNode? declaration) { while (declaration != null) { diff --git a/src/Machine.Specifications.Core.Specs/Machine.Specifications.Core.Specs.csproj b/src/Machine.Specifications.Core.Specs/Machine.Specifications.Core.Specs.csproj index e0c4d44b..5c4485b5 100644 --- a/src/Machine.Specifications.Core.Specs/Machine.Specifications.Core.Specs.csproj +++ b/src/Machine.Specifications.Core.Specs/Machine.Specifications.Core.Specs.csproj @@ -1,18 +1,21 @@ - + net472;net8.0 + enable + enable + latest false - - + + diff --git a/src/Machine.Specifications.Core/Machine.Specifications.Core.csproj b/src/Machine.Specifications.Core/Machine.Specifications.Core.csproj index 689443a7..999fb2f0 100644 --- a/src/Machine.Specifications.Core/Machine.Specifications.Core.csproj +++ b/src/Machine.Specifications.Core/Machine.Specifications.Core.csproj @@ -2,8 +2,12 @@ net472;net6.0 + Machine.Specifications Machine.Specifications Machine.Specifications.Core + enable + enable + latest @@ -11,7 +15,7 @@ - + diff --git a/src/Machine.Specifications.Fakes.Specs/Machine.Specifications.Fakes.Specs.csproj b/src/Machine.Specifications.Fakes.Specs/Machine.Specifications.Fakes.Specs.csproj index 6c4b278c..517212a4 100644 --- a/src/Machine.Specifications.Fakes.Specs/Machine.Specifications.Fakes.Specs.csproj +++ b/src/Machine.Specifications.Fakes.Specs/Machine.Specifications.Fakes.Specs.csproj @@ -2,17 +2,20 @@ net8.0 + enable + enable + latest false - - + + diff --git a/src/Machine.Specifications.Fakes/Machine.Specifications.Fakes.csproj b/src/Machine.Specifications.Fakes/Machine.Specifications.Fakes.csproj index 5968fe38..ed8b9e57 100644 --- a/src/Machine.Specifications.Fakes/Machine.Specifications.Fakes.csproj +++ b/src/Machine.Specifications.Fakes/Machine.Specifications.Fakes.csproj @@ -2,11 +2,14 @@ net472;net6.0 + enable + enable + latest false - + diff --git a/src/Machine.Specifications.Fixtures/Machine.Specifications.Fixtures.csproj b/src/Machine.Specifications.Fixtures/Machine.Specifications.Fixtures.csproj index 06f17ff3..48182e80 100644 --- a/src/Machine.Specifications.Fixtures/Machine.Specifications.Fixtures.csproj +++ b/src/Machine.Specifications.Fixtures/Machine.Specifications.Fixtures.csproj @@ -2,13 +2,12 @@ net472;net8.0 + enable + enable + latest false - - - - diff --git a/src/Machine.Specifications.Runner.Utility.Specs/Machine.Specifications.Runner.Utility.Specs.csproj b/src/Machine.Specifications.Runner.Utility.Specs/Machine.Specifications.Runner.Utility.Specs.csproj index 95aee073..3c11e7ec 100644 --- a/src/Machine.Specifications.Runner.Utility.Specs/Machine.Specifications.Runner.Utility.Specs.csproj +++ b/src/Machine.Specifications.Runner.Utility.Specs/Machine.Specifications.Runner.Utility.Specs.csproj @@ -1,19 +1,22 @@ - + - net472;net8.0 + net8.0 + enable + enable + latest false - - - + + + diff --git a/src/Machine.Specifications.Runner.Utility/Machine.Specifications.Runner.Utility.csproj b/src/Machine.Specifications.Runner.Utility/Machine.Specifications.Runner.Utility.csproj index ff729e52..683d5bfa 100644 --- a/src/Machine.Specifications.Runner.Utility/Machine.Specifications.Runner.Utility.csproj +++ b/src/Machine.Specifications.Runner.Utility/Machine.Specifications.Runner.Utility.csproj @@ -2,6 +2,9 @@ net472;net6.0 + enable + enable + latest diff --git a/src/Machine.Specifications.Runner.VisualStudio.Fixtures/FixtureCode.cs b/src/Machine.Specifications.Runner.VisualStudio.Fixtures/FixtureCode.cs new file mode 100644 index 00000000..dfc5e2f9 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Fixtures/FixtureCode.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using Machine.Specifications; + +namespace SampleSpecs +{ + [Behaviors] + public class SampleBehavior + { + It sample_behavior_test = () => + true.ShouldBeTrue(); + } + + class BehaviorSampleSpec + { + Because of = () => { }; + + Behaves_like some_behavior; + } + + class CleanupSpec + { + static int cleanup_count; + + Because of = () => { }; + + Cleanup after = () => + cleanup_count++; + + It should_not_increment_cleanup = () => + cleanup_count.ShouldEqual(0); + + It should_have_no_cleanups = () => + cleanup_count.ShouldEqual(0); + } + + [AssertDelegate] + public delegate void They(); + + [ActDelegate] + public delegate void WhenDoing(); + + class CustomActAssertDelegateSpec + { + static string a; + static string b; + + static int resultA; + static int resultB; + + Establish context = () => + { + a = "foo"; + b = "foo"; + }; + + WhenDoing of = () => + { + resultA = a.GetHashCode(); + resultB = b.GetHashCode(); + }; + + They should_have_the_same_hash_code = () => resultA.ShouldEqual(resultB); + } + + class Parent + { + class NestedSpec + { + It should_remember_that_true_is_true = () => + true.ShouldBeTrue(); + } + } + + class StandardSpec + { + Because of = () => { }; + + It should_pass = () => + 1.ShouldEqual(1); + + [Ignore("reason")] + It should_be_ignored = () => { }; + + It should_fail = () => + 1.ShouldEqual(2); + + It unhandled_exception = () => + { + throw new NotImplementedException(); + }; + + It not_implemented; + } + + class When_something + { + Because of = () => { }; + + It should_pass = () => + 1.ShouldEqual(1); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Fixtures/Machine.Specifications.Runner.VisualStudio.Fixtures.csproj b/src/Machine.Specifications.Runner.VisualStudio.Fixtures/Machine.Specifications.Runner.VisualStudio.Fixtures.csproj new file mode 100644 index 00000000..98b564ca --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Fixtures/Machine.Specifications.Runner.VisualStudio.Fixtures.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0 + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/.editorconfig b/src/Machine.Specifications.Runner.VisualStudio.Specs/.editorconfig new file mode 100644 index 00000000..3683dcfb --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/.editorconfig @@ -0,0 +1,37 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://EditorConfig.org + +[*.cs] +dotnet_style_require_accessibility_modifiers = never +dotnet_style_readonly_field = false + +# Severities +dotnet_diagnostic.IDE0052.severity = none + +# Naming styles +dotnet_naming_style.snake_case.capitalization = all_lower +dotnet_naming_style.snake_case.word_separator = _ + +# Symbol specifications +dotnet_naming_symbols.static_field.applicable_kinds = field +dotnet_naming_symbols.static_field.applicable_accessibilities = * +dotnet_naming_symbols.static_field.required_modifiers = static + +dotnet_naming_symbols.specs_delegate.applicable_kinds = field +dotnet_naming_symbols.specs_delegate.applicable_accessibilities = * + +dotnet_naming_symbols.specs_class.applicable_kinds = class +dotnet_naming_symbols.specs_class.applicable_accessibilities = private + +# Naming rules +dotnet_naming_rule.static_field_should_be_snake_case.severity = error +dotnet_naming_rule.static_field_should_be_snake_case.symbols = static_field +dotnet_naming_rule.static_field_should_be_snake_case.style = snake_case + +dotnet_naming_rule.machine_delegate_should_be_snake_case.severity = error +dotnet_naming_rule.machine_delegate_should_be_snake_case.symbols = specs_delegate +dotnet_naming_rule.machine_delegate_should_be_snake_case.style = snake_case + +dotnet_naming_rule.specs_class_should_be_snake_case.severity = error +dotnet_naming_rule.specs_class_should_be_snake_case.symbols = specs_class +dotnet_naming_rule.specs_class_should_be_snake_case.style = snake_case diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/BehaviorsSpecs.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/BehaviorsSpecs.cs new file mode 100644 index 00000000..d1df543e --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/BehaviorsSpecs.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using Machine.Specifications.Runner.VisualStudio.Discovery; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Discovery +{ + class BehaviorsSpecs : WithDiscoverySetup + { + It should_pick_up_the_behavior = () => + { + var discoveredSpec = results.SingleOrDefault(x => + "sample_behavior_test".Equals(x.SpecificationName, StringComparison.Ordinal) && + "BehaviorSampleSpec".Equals(x.ClassName, StringComparison.Ordinal)); + + discoveredSpec.ShouldNotBeNull(); + + discoveredSpec.LineNumber.ShouldEqual(10); + discoveredSpec.CodeFilePath.EndsWith("BehaviorSample.cs", StringComparison.Ordinal); + }; + + It should_pick_up_the_behavior_field_type_and_name = () => + { + var discoveredSpec = results.SingleOrDefault(x => + "sample_behavior_test".Equals(x.SpecificationName, StringComparison.Ordinal) && + "BehaviorSampleSpec".Equals(x.ClassName, StringComparison.Ordinal)); + + discoveredSpec.ShouldNotBeNull(); + + discoveredSpec.BehaviorFieldName.ShouldEqual("some_behavior"); + discoveredSpec.BehaviorFieldType.ShouldEqual("SampleSpecs.SampleBehavior"); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/CustomActAssertSpecs.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/CustomActAssertSpecs.cs new file mode 100644 index 00000000..dacc1f5f --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/CustomActAssertSpecs.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using Machine.Specifications.Runner.VisualStudio.Discovery; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Discovery +{ + class CustomActAssertSpecs : WithDiscoverySetup + { + It should_find_some = () => + { + var discoveredSpec = results.SingleOrDefault(x => + "should_have_the_same_hash_code".Equals(x.SpecificationName, StringComparison.Ordinal) && + "CustomActAssertDelegateSpec".Equals(x.ClassName, StringComparison.Ordinal)); + + discoveredSpec.ShouldNotBeNull(); + + discoveredSpec.LineNumber.ShouldEqual(63); + discoveredSpec.CodeFilePath.EndsWith("CustomActAssertDelegateSpec.cs", StringComparison.Ordinal); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/DisabledTestNameOffSpecs.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/DisabledTestNameOffSpecs.cs new file mode 100644 index 00000000..da1f39b8 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/DisabledTestNameOffSpecs.cs @@ -0,0 +1,31 @@ +using System; +using System.Reflection; +using Machine.Fakes; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using SampleSpecs; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Discovery +{ + class DisabledTestNameOffSpecs : WithFakes + { + static Assembly assembly; + + Establish context = () => + assembly = typeof(StandardSpec).Assembly; + + Because of = () => + The() + .DiscoverTests(new[] {assembly.GetType("SampleSpecs.When_something").GetTypeInfo().Assembly.Location}, + An(), + An(), + The()); + + + It should_use_full_type_and_field_name_for_display_name = () => + The() + .WasToldTo(d => d.SendTestCase(Param.Matches(t => t.DisplayName.Equals("When something it should pass", StringComparison.Ordinal)))) + .OnlyOnce(); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/DiscoverySpecs.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/DiscoverySpecs.cs new file mode 100644 index 00000000..b9a35c94 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/DiscoverySpecs.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using Machine.Specifications.Runner.VisualStudio.Discovery; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Discovery +{ + class DiscoverySpecs : WithDiscoverySetup + { + It should_find_spec = () => + { + var discoveredSpec = results.SingleOrDefault(x => + "should_pass".Equals(x.SpecificationName, StringComparison.Ordinal) && + "StandardSpec".Equals(x.ClassName, StringComparison.Ordinal)); + + discoveredSpec.ShouldNotBeNull(); + + discoveredSpec.LineNumber.ShouldEqual(79); + discoveredSpec.CodeFilePath.EndsWith("StandardSpec.cs", StringComparison.Ordinal); + }; + + It should_find_empty_spec = () => + { + var discoveredSpec = results.SingleOrDefault(x => + "should_be_ignored".Equals(x.SpecificationName, StringComparison.Ordinal) && + "StandardSpec".Equals(x.ClassName, StringComparison.Ordinal)); + + discoveredSpec.ShouldNotBeNull(); + + discoveredSpec.LineNumber.ShouldEqual(83); + discoveredSpec.CodeFilePath.EndsWith("StandardSpec.cs", StringComparison.Ordinal); + }; + + It should_find_ignored_spec_but_will_not_find_line_number = () => + { + var discoveredSpec = results.SingleOrDefault(x => + "not_implemented".Equals(x.SpecificationName, StringComparison.Ordinal) && + "StandardSpec".Equals(x.ClassName, StringComparison.Ordinal)); + + discoveredSpec.ShouldNotBeNull(); + + discoveredSpec.LineNumber.ShouldEqual(0); + discoveredSpec.CodeFilePath.ShouldBeNull(); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/NestedTypesDiscoverySpecs.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/NestedTypesDiscoverySpecs.cs new file mode 100644 index 00000000..b5d7cd4d --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/NestedTypesDiscoverySpecs.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; +using Machine.Specifications.Runner.VisualStudio.Discovery; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Discovery.BuiltIn +{ + class NestedTypesDiscoverySpecs : WithDiscoverySetup + { + It should_discover_the_sample_behavior = () => + { + var discoveredSpec = results.SingleOrDefault(x => + "should_remember_that_true_is_true".Equals(x.SpecificationName, StringComparison.Ordinal) && + "NestedSpec".Equals(x.ClassName, StringComparison.Ordinal)); + + discoveredSpec.ShouldNotBeNull(); + + discoveredSpec.ContextDisplayName.ShouldEqual("Parent NestedSpec"); + + discoveredSpec.LineNumber.ShouldEqual(70); + discoveredSpec.CodeFilePath.EndsWith("NestedSpecSample.cs", StringComparison.Ordinal); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/WithDiscoverySetup.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/WithDiscoverySetup.cs new file mode 100644 index 00000000..f9a17eca --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Discovery/WithDiscoverySetup.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Reflection; +using Machine.Specifications.Runner.VisualStudio.Discovery; +using SampleSpecs; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Discovery +{ + public abstract class WithDiscoverySetup + where TDiscoverer : ISpecificationDiscoverer, new() + { + static ISpecificationDiscoverer discoverer; + + static Assembly assembly; + + protected static IEnumerable results; + + Establish context = () => + { + discoverer = new TDiscoverer(); + + assembly = typeof(StandardSpec).Assembly; + }; + + Because of = () => + results = discoverer.DiscoverSpecs(assembly.Location); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/DiscoveryUnhandledErrorSpecs.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/DiscoveryUnhandledErrorSpecs.cs new file mode 100644 index 00000000..18e29661 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/DiscoveryUnhandledErrorSpecs.cs @@ -0,0 +1,31 @@ +using System; +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Discovery; +using Machine.Specifications.Runner.VisualStudio.Execution; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +namespace Machine.Specifications.Runner.VisualStudio.Specs +{ + class DiscoveryUnhandledErrorSpecs : WithFakes + { + static MspecTestRunner runner; + + Establish context = () => + { + The() + .WhenToldTo(d => d.DiscoverSpecs(Param.IsAnything)) + .Return(() => throw new InvalidOperationException()); + + runner = new MspecTestRunner(The(), An(), An()); + }; + + Because of = () => + runner.DiscoverTests(new[] { "bla" }, An(), The(), An()); + + It should_send_an_error_notification_to_visual_studio = () => + The() + .WasToldTo(logger => logger.SendMessage(TestMessageLevel.Error, Param.IsNotNull)) + .OnlyOnce(); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/RunListener/WhenSpecificationEndsWithAFail.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/RunListener/WhenSpecificationEndsWithAFail.cs new file mode 100644 index 00000000..ed7ba0b6 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/RunListener/WhenSpecificationEndsWithAFail.cs @@ -0,0 +1,53 @@ +using System; +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Execution; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution.RunListener +{ + [Subject(typeof(ProxyAssemblySpecificationRunListener))] + class WhenSpecificationEndsWithAFail : WithFakes + { + static ProxyAssemblySpecificationRunListener run_listener; + + protected static TestCase test_case; + + static SpecificationInfo specification_info = new SpecificationInfo("leader", "field name", "ContainingType", "field_name"); + + Establish context = () => + { + The() + .WhenToldTo(f => f.RecordEnd(Param.IsAnything, Param.IsAnything)) + .Callback((TestCase testCase, TestOutcome outcome) => test_case = testCase); + + run_listener = new ProxyAssemblySpecificationRunListener("assemblyPath", The(), new Uri("bla://executorUri")); + }; + + + Because of = () => + run_listener.OnSpecificationEnd(specification_info, Result.Failure(new NotImplementedException())); + + It should_notify_visual_studio_of_the_test_outcome = () => + The() + .WasToldTo(f => f.RecordEnd(Param.IsNotNull, Param.Matches(outcome => outcome == TestOutcome.Failed))) + .OnlyOnce(); + + It should_notify_visual_studio_of_the_test_result = () => + The() + .WasToldTo(f => f.RecordResult(Param.Matches(result => + result.Outcome == TestOutcome.Failed && + result.ComputerName == Environment.MachineName && + result.ErrorMessage == new NotImplementedException().Message && + !string.IsNullOrWhiteSpace(result.ErrorStackTrace) + ))) + .OnlyOnce(); + + It should_provide_correct_details_to_visual_studio = () => + { + test_case.FullyQualifiedName.ShouldEqual("ContainingType::field_name"); + test_case.ExecutorUri.ShouldEqual(new Uri("bla://executorUri")); + test_case.Source.ShouldEqual("assemblyPath"); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/RunListener/WhenSpecificationEndsWithAPass.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/RunListener/WhenSpecificationEndsWithAPass.cs new file mode 100644 index 00000000..c1d20bab --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/RunListener/WhenSpecificationEndsWithAPass.cs @@ -0,0 +1,52 @@ +using System; +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Execution; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution.RunListener +{ + [Subject(typeof(ProxyAssemblySpecificationRunListener))] + class WhenSpecificationEndsWithAPass : WithFakes + { + static ProxyAssemblySpecificationRunListener run_listener; + + protected static TestCase test_case; + + static SpecificationInfo specification_info = new SpecificationInfo("leader", "field name", "ContainingType", "field_name"); + + Establish context = () => + { + The() + .WhenToldTo(f => f.RecordEnd(Param.IsAnything, Param.IsAnything)) + .Callback((TestCase testCase, TestOutcome outcome) => test_case = testCase); + + run_listener = new ProxyAssemblySpecificationRunListener("assemblyPath", The(), new Uri("bla://executorUri")); + }; + + Because of = () => + run_listener.OnSpecificationEnd(specification_info, Result.Pass()); + + It should_notify_visual_studio_of_the_test_outcome = () => + The() + .WasToldTo(f => f.RecordEnd(Param.IsNotNull, Param.Matches(outcome => outcome == TestOutcome.Passed))) + .OnlyOnce(); + + It should_notify_visual_studio_of_the_test_result = () => + The() + .WasToldTo(f => f.RecordResult(Param.Matches(result => + result.Outcome == TestOutcome.Passed && + result.ComputerName == Environment.MachineName && + result.ErrorMessage == null && + result.ErrorStackTrace == null + ))) + .OnlyOnce(); + + It should_provide_correct_details_to_visual_studio = () => + { + test_case.FullyQualifiedName.ShouldEqual("ContainingType::field_name"); + test_case.ExecutorUri.ShouldEqual(new Uri("bla://executorUri")); + test_case.Source.ShouldEqual("assemblyPath"); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/RunListener/WhenSpecificationStarts.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/RunListener/WhenSpecificationStarts.cs new file mode 100644 index 00000000..b3da426b --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/RunListener/WhenSpecificationStarts.cs @@ -0,0 +1,38 @@ +using System; +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Execution; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution.RunListener +{ + [Subject(typeof(ProxyAssemblySpecificationRunListener))] + class WhenSpecificationStarts : WithFakes + { + static ProxyAssemblySpecificationRunListener run_listener; + + protected static TestCase test_case; + + Establish context = () => + { + The() + .WhenToldTo(f => f.RecordStart(Param.IsAnything)) + .Callback((TestCase testCase) => test_case = testCase); + + run_listener = new ProxyAssemblySpecificationRunListener("assemblyPath", The(), new Uri("bla://executorUri")); + }; + + Because of = () => + run_listener.OnSpecificationStart(new SpecificationInfo("leader", "field name", "ContainingType", "field_name")); + + It should_notify_visual_studio = () => + The().WasToldTo(f => f.RecordStart(Param.IsNotNull)); + + It should_provide_correct_details_to_visual_studio = () => + { + test_case.FullyQualifiedName.ShouldEqual("ContainingType::field_name"); + test_case.ExecutorUri.ShouldEqual(new Uri("bla://executorUri")); + test_case.Source.ShouldEqual("assemblyPath"); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/RunListener/WhenThereIsAnErrorReported.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/RunListener/WhenThereIsAnErrorReported.cs new file mode 100644 index 00000000..6c6578cc --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/RunListener/WhenThereIsAnErrorReported.cs @@ -0,0 +1,27 @@ +using System; +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Execution; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution.RunListener +{ + [Subject(typeof(ProxyAssemblySpecificationRunListener))] + class WhenThereIsAnErrorReported : WithFakes + { + static ProxyAssemblySpecificationRunListener run_listener; + + Establish context = () => + run_listener = new ProxyAssemblySpecificationRunListener("assemblyPath", The(), new Uri("bla://executorUri")); + + Because of = () => + run_listener.OnFatalError(new ExceptionResult(new InvalidOperationException())); + + It should_notify_visual_studio_of_the_error_outcome = () => + The() + .WasToldTo(f => f.SendMessage( + Param.Matches(level => level == TestMessageLevel.Error), + Param.Matches(message => message.Contains("InvalidOperationException")))) + .OnlyOnce(); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenCleaningUpAContext.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenCleaningUpAContext.cs new file mode 100644 index 00000000..803740f1 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenCleaningUpAContext.cs @@ -0,0 +1,31 @@ +using System.Linq; +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + class WhenCleaningUpAContext : WithMultipleSpecExecutionSetup + { + Establish context = () => + specifications_to_run = new[] + { + new VisualStudioTestIdentifier("SampleSpecs.CleanupSpec", "should_not_increment_cleanup"), + new VisualStudioTestIdentifier("SampleSpecs.CleanupSpec", "should_have_no_cleanups") + }; + + It should_tell_visual_studio_it_passed = () => + { + The() + .WasToldTo(x => x.RecordEnd( + Param.Matches(y => specifications_to_run.Contains(y.ToVisualStudioTestIdentifier())), + Param.Matches(y => y == TestOutcome.Passed))) + .Twice(); + + The() + .WasToldTo(x =>x.RecordResult(Param.Matches(y => y.Outcome == TestOutcome.Passed))) + .Twice(); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningANestedSpecPasses.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningANestedSpecPasses.cs new file mode 100644 index 00000000..d22cfac9 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningANestedSpecPasses.cs @@ -0,0 +1,27 @@ +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + class WhenRunningANestedSpecPasses : WithSingleSpecExecutionSetup + { + Establish context = () => + specification_to_run = new VisualStudioTestIdentifier("SampleSpecs.Parent+NestedSpec", "should_remember_that_true_is_true"); + + It should_tell_visual_studio_it_passed = () => + { + The() + .WasToldTo(handle => + handle.RecordEnd( + Param.Matches(x => x.ToVisualStudioTestIdentifier().Equals(specification_to_run)), + Param.Matches(x => x == TestOutcome.Passed))) + .OnlyOnce(); + + The() + .WasToldTo(x => x.RecordResult(Param.Matches(result => result.Outcome == TestOutcome.Passed))) + .OnlyOnce(); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASingleBehaviorPasses.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASingleBehaviorPasses.cs new file mode 100644 index 00000000..25926a5c --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASingleBehaviorPasses.cs @@ -0,0 +1,27 @@ +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + class WhenRunningASingleBehaviorPasses : WithSingleSpecExecutionSetup + { + Establish context = () => + specification_to_run = new VisualStudioTestIdentifier("SampleSpecs.BehaviorSampleSpec", "sample_behavior_test"); + + It should_tell_visual_studio_it_passed = () => + { + The() + .WasToldTo(handle => + handle.RecordEnd(Param.Matches(testCase => testCase.ToVisualStudioTestIdentifier().Equals(specification_to_run)), + Param.Matches(outcome => outcome == TestOutcome.Passed))) + .OnlyOnce(); + + The() + .WasToldTo(handle => + handle.RecordResult(Param.Matches(result => result.Outcome == TestOutcome.Passed))) + .OnlyOnce(); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatFails.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatFails.cs new file mode 100644 index 00000000..111297f3 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatFails.cs @@ -0,0 +1,27 @@ +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + class WhenRunningASpecThatFails : WithSingleSpecExecutionSetup + { + Establish context = () => + specification_to_run = new VisualStudioTestIdentifier("SampleSpecs.StandardSpec", "should_fail"); + + It should_tell_visual_studio_it_failed= () => + { + The() + .WasToldTo(handle => + handle.RecordEnd(Param.Matches(testCase => testCase.ToVisualStudioTestIdentifier().Equals(specification_to_run)), + Param.Matches(outcome => outcome == TestOutcome.Failed))) + .OnlyOnce(); + + The() + .WasToldTo(handle => + handle.RecordResult(Param.Matches(result => result.Outcome == TestOutcome.Failed))) + .OnlyOnce(); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatIsIgnored.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatIsIgnored.cs new file mode 100644 index 00000000..cea8b674 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatIsIgnored.cs @@ -0,0 +1,27 @@ +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + class WhenRunningASpecThatIsIgnored : WithSingleSpecExecutionSetup + { + Establish context = () => + specification_to_run = new VisualStudioTestIdentifier("SampleSpecs.StandardSpec", "should_be_ignored"); + + It should_tell_visual_studio_it_was_skipped = () => + { + The() + .WasToldTo(handle => + handle.RecordEnd(Param.Matches(testCase => testCase.ToVisualStudioTestIdentifier().Equals(specification_to_run)), + Param.Matches(outcome => outcome == TestOutcome.Skipped))) + .OnlyOnce(); + + The() + .WasToldTo(handle => + handle.RecordResult(Param.Matches(result => result.Outcome == TestOutcome.Skipped))) + .OnlyOnce(); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatIsNotImplemented.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatIsNotImplemented.cs new file mode 100644 index 00000000..a588baca --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatIsNotImplemented.cs @@ -0,0 +1,29 @@ +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + class WhenRunningASpecThatIsNotImplemented : WithSingleSpecExecutionSetup + { + Establish context = () => + specification_to_run = new VisualStudioTestIdentifier("SampleSpecs.StandardSpec", "not_implemented"); + + It should_tell_visual_studio_it_was_not_found = () => + { + The() + .WasToldTo(handle => + handle.RecordEnd( + Param.Matches(testCase => + testCase.ToVisualStudioTestIdentifier().Equals(specification_to_run)), + Param.Matches(outcome => outcome == TestOutcome.NotFound))) + .OnlyOnce(); + + The() + .WasToldTo(handle => + handle.RecordResult(Param.Matches(result => result.Outcome == TestOutcome.NotFound))) + .OnlyOnce(); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatPasses.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatPasses.cs new file mode 100644 index 00000000..b33dc695 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatPasses.cs @@ -0,0 +1,27 @@ +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + class WhenRunningASpecThatPasses : WithSingleSpecExecutionSetup + { + Establish context = () => + specification_to_run = new VisualStudioTestIdentifier("SampleSpecs.StandardSpec", "should_pass"); + + It should_tell_visual_studio_it_passed = () => + { + The() + .WasToldTo(handle => + handle.RecordEnd(Param.Matches(testCase => testCase.ToVisualStudioTestIdentifier().Equals(specification_to_run)), + Param.Matches(outcome => outcome == TestOutcome.Passed))) + .OnlyOnce(); + + The() + .WasToldTo(handle => + handle.RecordResult(Param.Matches(result => result.Outcome == TestOutcome.Passed))) + .OnlyOnce(); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatThrows.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatThrows.cs new file mode 100644 index 00000000..e3592b5c --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecThatThrows.cs @@ -0,0 +1,28 @@ +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + class WhenRunningASpecThatThrows : WithSingleSpecExecutionSetup + { + Establish context = () => + specification_to_run = new VisualStudioTestIdentifier("SampleSpecs.StandardSpec", "unhandled_exception"); + + It should_tell_visual_studio_it_failed = () => + { + The() + .WasToldTo(handle => + handle.RecordEnd( + Param.Matches(testCase => testCase.ToVisualStudioTestIdentifier().Equals(specification_to_run)), + Param.Matches(outcome => outcome == TestOutcome.Failed))) + .OnlyOnce(); + + The() + .WasToldTo(handle => + handle.RecordResult(Param.Matches(result => result.Outcome == TestOutcome.Failed))) + .OnlyOnce(); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecWithCustomActAssertDelegatesPasses.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecWithCustomActAssertDelegatesPasses.cs new file mode 100644 index 00000000..53da8edf --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningASpecWithCustomActAssertDelegatesPasses.cs @@ -0,0 +1,27 @@ +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + class WhenRunningASpecWithCustomActAssertDelegatesPasses : WithSingleSpecExecutionSetup + { + Establish context = () => + specification_to_run = new VisualStudioTestIdentifier("SampleSpecs.CustomActAssertDelegateSpec", "should_have_the_same_hash_code"); + + It should_tell_visual_studio_it_passed = () => + { + The() + .WasToldTo(handle => + handle.RecordEnd(Param.Matches(testCase => testCase.ToVisualStudioTestIdentifier().Equals(specification_to_run)), + Param.Matches(outcome => outcome == TestOutcome.Passed))) + .OnlyOnce(); + + The() + .WasToldTo(handle => + handle.RecordResult(Param.Matches(result => result.Outcome == TestOutcome.Passed))) + .OnlyOnce(); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningAnAssemblyWithBehaviors.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningAnAssemblyWithBehaviors.cs new file mode 100644 index 00000000..56849e58 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WhenRunningAnAssemblyWithBehaviors.cs @@ -0,0 +1,31 @@ +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + class WhenRunningAnAssemblyWithBehaviors : WithAssemblyExecutionSetup + { + static VisualStudioTestIdentifier specification_expected_to_run; + + Establish context = () => + specification_expected_to_run = new VisualStudioTestIdentifier("SampleSpecs.BehaviorSampleSpec", "sample_behavior_test"); + + It should_run_all_behaviors = () => + { + The() + .WasToldTo(handle => + handle.RecordEnd(Param.Matches(testCase => testCase.ToVisualStudioTestIdentifier().Equals(specification_expected_to_run)), + Param.Matches(outcome => outcome == TestOutcome.Passed))) + .OnlyOnce(); + + The() + .WasToldTo(handle => + handle.RecordResult(Param.Matches(result => + result.Outcome == TestOutcome.Passed && + result.TestCase.ToVisualStudioTestIdentifier().Equals(specification_expected_to_run)))) + .OnlyOnce(); + }; + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WithAssemblyExecutionSetup.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WithAssemblyExecutionSetup.cs new file mode 100644 index 00000000..5a8afb6b --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WithAssemblyExecutionSetup.cs @@ -0,0 +1,24 @@ +using System.Reflection; +using Machine.Fakes; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using SampleSpecs; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + public abstract class WithAssemblyExecutionSetup : WithFakes + { + static MspecTestRunner executor; + + static Assembly assembly; + + Establish context = () => + { + executor = new MspecTestRunner(); + + assembly = typeof(StandardSpec).Assembly; + }; + + Because of = () => + executor.RunTests(new[] { assembly.Location }, An(), The()); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WithMultipleSpecExecutionSetup.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WithMultipleSpecExecutionSetup.cs new file mode 100644 index 00000000..12186170 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WithMultipleSpecExecutionSetup.cs @@ -0,0 +1,29 @@ +using System; +using System.Reflection; +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Execution; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using SampleSpecs; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + public abstract class WithMultipleSpecExecutionSetup : WithFakes + { + static ISpecificationExecutor executor; + + static Assembly assembly; + + protected static VisualStudioTestIdentifier[] specifications_to_run; + + Establish context = () => + { + executor = new SpecificationExecutor(); + + assembly = typeof(StandardSpec).Assembly; + }; + + Because of = () => + executor.RunAssemblySpecifications(assembly.Location, specifications_to_run, new Uri("bla://executor"), The()); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WithSingleSpecExecutionSetup.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WithSingleSpecExecutionSetup.cs new file mode 100644 index 00000000..e9f6b9e6 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Execution/WithSingleSpecExecutionSetup.cs @@ -0,0 +1,29 @@ +using System; +using System.Reflection; +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Execution; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using SampleSpecs; + +namespace Machine.Specifications.Runner.VisualStudio.Specs.Execution +{ + public abstract class WithSingleSpecExecutionSetup : WithFakes + { + static ISpecificationExecutor executor; + + static Assembly assembly; + + protected static VisualStudioTestIdentifier specification_to_run; + + Establish context = () => + { + executor = new SpecificationExecutor(); + + assembly = typeof(StandardSpec).Assembly; + }; + + Because of = () => + executor.RunAssemblySpecifications(assembly.Location, new[] { specification_to_run }, new Uri("bla://executor"), The()); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/ExecutionUnhandledErrorSpecs.cs b/src/Machine.Specifications.Runner.VisualStudio.Specs/ExecutionUnhandledErrorSpecs.cs new file mode 100644 index 00000000..7826068d --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/ExecutionUnhandledErrorSpecs.cs @@ -0,0 +1,38 @@ +using System; +using Machine.Fakes; +using Machine.Specifications.Runner.VisualStudio.Discovery; +using Machine.Specifications.Runner.VisualStudio.Execution; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +namespace Machine.Specifications.Runner.VisualStudio.Specs +{ + class ExecutionUnhandledErrorSpecs : WithFakes + { + static MspecTestExecutor adapter; + + Establish context = () => + { + The() + .WhenToldTo(d => d.RunAssemblySpecifications( + Param.IsAnything, + Param.IsAnything, + Param.IsAnything, + Param.IsAnything)) + .Throw(new InvalidOperationException()); + + var adapterDiscoverer = new MspecTestDiscoverer(An()); + adapter = new MspecTestExecutor(The(), adapterDiscoverer, An()); + }; + + Because of = () => + adapter.RunTests(new[] {new TestCase("a", MspecTestRunner.Uri, "dll"), }, An(), The()); + + It should_send_an_error_notification_to_visual_studio = () => + The() + .WasToldTo(logger => logger.SendMessage(TestMessageLevel.Error, Param.IsNotNull)) + .OnlyOnce(); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio.Specs/Machine.Specifications.Runner.VisualStudio.Specs.csproj b/src/Machine.Specifications.Runner.VisualStudio.Specs/Machine.Specifications.Runner.VisualStudio.Specs.csproj new file mode 100644 index 00000000..1e1cda41 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio.Specs/Machine.Specifications.Runner.VisualStudio.Specs.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1;net472 + false + + + + + + + + + + + + + + + diff --git a/src/Machine.Specifications.Runner.VisualStudio/Discovery/BuiltInSpecificationDiscoverer.cs b/src/Machine.Specifications.Runner.VisualStudio/Discovery/BuiltInSpecificationDiscoverer.cs new file mode 100644 index 00000000..56a18c92 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Discovery/BuiltInSpecificationDiscoverer.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using Machine.Specifications.Runner.VisualStudio.Helpers; + +namespace Machine.Specifications.Runner.VisualStudio.Discovery +{ + public class BuiltInSpecificationDiscoverer : ISpecificationDiscoverer + { + public IEnumerable DiscoverSpecs(string assemblyFilePath) + { +#if NETFRAMEWORK + using (var scope = new IsolatedAppDomainExecutionScope(assemblyFilePath)) + { + var discoverer = scope.CreateInstance(); +#else + var discoverer = new TestDiscoverer(); +#endif + return discoverer.DiscoverTests(assemblyFilePath).ToList(); +#if NETFRAMEWORK + } +#endif + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Discovery/ISpecificationDiscoverer.cs b/src/Machine.Specifications.Runner.VisualStudio/Discovery/ISpecificationDiscoverer.cs new file mode 100644 index 00000000..e0e723f4 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Discovery/ISpecificationDiscoverer.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Machine.Specifications.Runner.VisualStudio.Discovery +{ + public interface ISpecificationDiscoverer + { + IEnumerable DiscoverSpecs(string assemblyPath); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Discovery/SpecTestCase.cs b/src/Machine.Specifications.Runner.VisualStudio/Discovery/SpecTestCase.cs new file mode 100644 index 00000000..84e22f5d --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Discovery/SpecTestCase.cs @@ -0,0 +1,32 @@ +using System; + +namespace Machine.Specifications.Runner.VisualStudio.Discovery +{ +#if NETFRAMEWORK + [Serializable] +#endif + public class SpecTestCase + { + public string Subject { get; set; } + + public string ContextFullType { get; set; } + + public object ContextDisplayName { get; set; } + + public string ClassName { get; set; } + + public string SpecificationDisplayName { get; set; } + + public string SpecificationName { get; set; } + + public string BehaviorFieldName { get; set; } + + public string BehaviorFieldType { get; set; } + + public string CodeFilePath { get; set; } + + public int LineNumber { get; set; } + + public string[] Tags { get; set; } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Discovery/TestDiscoverer.cs b/src/Machine.Specifications.Runner.VisualStudio/Discovery/TestDiscoverer.cs new file mode 100644 index 00000000..829c00fa --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Discovery/TestDiscoverer.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Machine.Specifications.Explorers; +using Machine.Specifications.Model; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Machine.Specifications.Runner.VisualStudio.Navigation; + +namespace Machine.Specifications.Runner.VisualStudio.Discovery +{ + public class TestDiscoverer +#if NETFRAMEWORK + : MarshalByRefObject +#endif + { +#if NETFRAMEWORK + [System.Security.SecurityCritical] + public override object InitializeLifetimeService() + { + return null; + } +#endif + private readonly PropertyInfo behaviorProperty = typeof(BehaviorSpecification).GetProperty("BehaviorFieldInfo"); + + public IEnumerable DiscoverTests(string assemblyPath) + { + var assemblyExplorer = new AssemblyExplorer(); + + var assembly = AssemblyHelper.Load(assemblyPath); + var contexts = assemblyExplorer.FindContextsIn(assembly); + + using (var session = new NavigationSession(assemblyPath)) + { + return contexts.SelectMany(context => CreateTestCase(context, session)).ToList(); + } + } + + private IEnumerable CreateTestCase(Context context, NavigationSession session) + { + foreach (var spec in context.Specifications.ToList()) + { + var testCase = new SpecTestCase + { + ClassName = context.Type.Name, + ContextFullType = context.Type.FullName, + ContextDisplayName = GetContextDisplayName(context.Type), + SpecificationName = spec.FieldInfo.Name, + SpecificationDisplayName = spec.Name + }; + + string fieldDeclaringType; + + if (spec.FieldInfo.DeclaringType.GetTypeInfo().IsGenericType && !spec.FieldInfo.DeclaringType.GetTypeInfo().IsGenericTypeDefinition) + fieldDeclaringType = spec.FieldInfo.DeclaringType.GetGenericTypeDefinition().FullName; + else + fieldDeclaringType = spec.FieldInfo.DeclaringType.FullName; + + var locationInfo = session.GetNavigationData(fieldDeclaringType, spec.FieldInfo.Name); + + if (locationInfo != null) + { + testCase.CodeFilePath = locationInfo.CodeFile; + testCase.LineNumber = locationInfo.LineNumber; + } + + if (spec is BehaviorSpecification behaviorSpec) + PopulateBehaviorField(testCase, behaviorSpec); + + if (context.Tags != null) + testCase.Tags = context.Tags.Select(tag => tag.Name).ToArray(); + + if (context.Subject != null) + testCase.Subject = context.Subject.FullConcern; + + yield return testCase; + } + } + + private void PopulateBehaviorField(SpecTestCase testCase, BehaviorSpecification specification) + { + if (behaviorProperty?.GetValue(specification) is FieldInfo field) + { + testCase.BehaviorFieldName = field.Name; + testCase.BehaviorFieldType = field.FieldType.GenericTypeArguments.FirstOrDefault()?.FullName; + } + } + + private string GetContextDisplayName(Type contextType) + { + var displayName = contextType.Name.Replace("_", " "); + + if (contextType.IsNested) + { + return GetContextDisplayName(contextType.DeclaringType) + " " + displayName; + } + + return displayName; + } + } + +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Execution/AssemblyLocationAwareRunListener.cs b/src/Machine.Specifications.Runner.VisualStudio/Execution/AssemblyLocationAwareRunListener.cs new file mode 100644 index 00000000..6bdd0e75 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Execution/AssemblyLocationAwareRunListener.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Machine.Specifications.Runner.VisualStudio.Execution +{ + public class AssemblyLocationAwareRunListener : ISpecificationRunListener + { + private readonly IEnumerable assemblies; + + public AssemblyLocationAwareRunListener(IEnumerable assemblies) + { + this.assemblies = assemblies ?? throw new ArgumentNullException(nameof(assemblies)); + } + + public void OnAssemblyStart(AssemblyInfo assembly) + { + var loadedAssembly = assemblies.FirstOrDefault(a => a.GetName().Name.Equals(assembly.Name, StringComparison.OrdinalIgnoreCase)); + + Directory.SetCurrentDirectory(Path.GetDirectoryName(loadedAssembly.Location)); + } + + public void OnAssemblyEnd(AssemblyInfo assembly) + { + } + + public void OnRunStart() + { + } + + public void OnRunEnd() + { + } + + public void OnContextStart(ContextInfo context) + { + } + + public void OnContextEnd(ContextInfo context) + { + } + + public void OnSpecificationStart(SpecificationInfo specification) + { + } + + public void OnSpecificationEnd(SpecificationInfo specification, Result result) + { + } + + public void OnFatalError(ExceptionResult exception) + { + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Execution/IFrameworkLogger.cs b/src/Machine.Specifications.Runner.VisualStudio/Execution/IFrameworkLogger.cs new file mode 100644 index 00000000..36ab2734 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Execution/IFrameworkLogger.cs @@ -0,0 +1,9 @@ +using System; + +namespace Machine.Specifications.Runner.VisualStudio.Execution +{ + public interface IFrameworkLogger + { + void SendErrorMessage(string message, Exception exception); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Execution/ISpecificationExecutor.cs b/src/Machine.Specifications.Runner.VisualStudio/Execution/ISpecificationExecutor.cs new file mode 100644 index 00000000..77e538c6 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Execution/ISpecificationExecutor.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Execution +{ + public interface ISpecificationExecutor + { + void RunAssemblySpecifications(string assemblyPath, IEnumerable specifications, Uri adapterUri, IFrameworkHandle frameworkHandle); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Execution/ISpecificationFilterProvider.cs b/src/Machine.Specifications.Runner.VisualStudio/Execution/ISpecificationFilterProvider.cs new file mode 100644 index 00000000..d7e1735e --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Execution/ISpecificationFilterProvider.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Execution +{ + public interface ISpecificationFilterProvider + { + IEnumerable FilteredTests(IEnumerable testCases, IRunContext runContext, IFrameworkHandle frameworkHandle); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Execution/ProxyAssemblySpecificationRunListener.cs b/src/Machine.Specifications.Runner.VisualStudio/Execution/ProxyAssemblySpecificationRunListener.cs new file mode 100644 index 00000000..ee35c6d8 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Execution/ProxyAssemblySpecificationRunListener.cs @@ -0,0 +1,160 @@ +using System; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +namespace Machine.Specifications.Runner.VisualStudio.Execution +{ + public class ProxyAssemblySpecificationRunListener : +#if NETFRAMEWORK + MarshalByRefObject, +#endif + ISpecificationRunListener, IFrameworkLogger + { + private readonly IFrameworkHandle frameworkHandle; + + private readonly string assemblyPath; + + private readonly Uri executorUri; + + private ContextInfo currentContext; + + private RunStats currentRunStats; + + public ProxyAssemblySpecificationRunListener(string assemblyPath, IFrameworkHandle frameworkHandle, Uri executorUri) + { + this.frameworkHandle = frameworkHandle ?? throw new ArgumentNullException(nameof(frameworkHandle)); + this.assemblyPath = assemblyPath ?? throw new ArgumentNullException(nameof(assemblyPath)); + this.executorUri = executorUri ?? throw new ArgumentNullException(nameof(executorUri)); + } + +#if NETFRAMEWORK + [System.Security.SecurityCritical] + public override object InitializeLifetimeService() + { + return null; + } +#endif + + public void OnFatalError(ExceptionResult exception) + { + if (currentRunStats != null) + { + currentRunStats.Stop(); + currentRunStats = null; + } + + frameworkHandle.SendMessage(TestMessageLevel.Error, + "Machine Specifications Visual Studio Test Adapter - Fatal error while executing test." + + Environment.NewLine + exception); + } + + public void OnSpecificationStart(SpecificationInfo specification) + { + var testCase = ConvertSpecificationToTestCase(specification); + frameworkHandle.RecordStart(testCase); + currentRunStats = new RunStats(); + } + + public void OnSpecificationEnd(SpecificationInfo specification, Result result) + { + if (currentRunStats != null) + { + currentRunStats.Stop(); + } + + var testCase = ConvertSpecificationToTestCase(specification); + + frameworkHandle.RecordEnd(testCase, MapSpecificationResultToTestOutcome(result)); + frameworkHandle.RecordResult(ConverResultToTestResult(testCase, result, currentRunStats)); + } + + public void OnContextStart(ContextInfo context) + { + currentContext = context; + } + + public void OnContextEnd(ContextInfo context) + { + currentContext = null; + } + + private TestCase ConvertSpecificationToTestCase(SpecificationInfo specification) + { + var vsTestId = specification.ToVisualStudioTestIdentifier(currentContext); + + return new TestCase(vsTestId.FullyQualifiedName, executorUri, assemblyPath) + { + DisplayName = $"{currentContext?.TypeName}.{specification.FieldName}", + }; + } + + private static TestOutcome MapSpecificationResultToTestOutcome(Result result) + { + switch (result.Status) + { + case Status.Failing: + return TestOutcome.Failed; + + case Status.Passing: + return TestOutcome.Passed; + + case Status.Ignored: + return TestOutcome.Skipped; + + case Status.NotImplemented: + return TestOutcome.NotFound; + + default: + return TestOutcome.None; + } + } + + private static TestResult ConverResultToTestResult(TestCase testCase, Result result, RunStats runStats) + { + var testResult = new TestResult(testCase) + { + ComputerName = Environment.MachineName, + Outcome = MapSpecificationResultToTestOutcome(result), + DisplayName = testCase.DisplayName + }; + + if (result.Exception != null) + { + testResult.ErrorMessage = result.Exception.Message; + testResult.ErrorStackTrace = result.Exception.ToString(); + } + + if (runStats != null) + { + testResult.StartTime = runStats.Start; + testResult.EndTime = runStats.End; + testResult.Duration = runStats.Duration; + } + + return testResult; + } + + public void OnAssemblyEnd(AssemblyInfo assembly) + { + } + + public void OnAssemblyStart(AssemblyInfo assembly) + { + } + + public void OnRunEnd() + { + } + + public void OnRunStart() + { + } + + public void SendErrorMessage(string message, Exception exception) + { + frameworkHandle?.SendMessage(TestMessageLevel.Error, message + Environment.NewLine + exception); + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Execution/RunStats.cs b/src/Machine.Specifications.Runner.VisualStudio/Execution/RunStats.cs new file mode 100644 index 00000000..f669b178 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Execution/RunStats.cs @@ -0,0 +1,29 @@ +using System; +using System.Diagnostics; + +namespace Machine.Specifications.Runner.VisualStudio.Execution +{ + public class RunStats + { + private readonly Stopwatch stopwatch = new Stopwatch(); + + public RunStats() + { + stopwatch.Start(); + Start = DateTime.Now; + } + + public DateTimeOffset Start { get; } + + public DateTimeOffset End { get; private set; } + + public TimeSpan Duration => stopwatch.Elapsed; + + public void Stop() + { + stopwatch.Stop(); + + End = Start + stopwatch.Elapsed; + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Execution/SingleBehaviorTestRunListenerWrapper.cs b/src/Machine.Specifications.Runner.VisualStudio/Execution/SingleBehaviorTestRunListenerWrapper.cs new file mode 100644 index 00000000..86649d7c --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Execution/SingleBehaviorTestRunListenerWrapper.cs @@ -0,0 +1,82 @@ +using System; +using Machine.Specifications.Runner.VisualStudio.Helpers; + +namespace Machine.Specifications.Runner.VisualStudio.Execution +{ + /// + /// The purpose of this class is to ignore everything, but a single specification's notifications. + /// Also because [Behavior] It's get reported as belonging to the Behavior class rather than test class + /// we need to map from one to the other for Visual Studio to capture the results. + /// + public class SingleBehaviorTestRunListenerWrapper : ISpecificationRunListener + { + private readonly ISpecificationRunListener runListener; + + private readonly VisualStudioTestIdentifier listenFor; + + private ContextInfo currentContext; + + public SingleBehaviorTestRunListenerWrapper(ISpecificationRunListener runListener, VisualStudioTestIdentifier listenFor) + { + this.runListener = runListener ?? throw new ArgumentNullException(nameof(runListener)); + this.listenFor = listenFor ?? throw new ArgumentNullException(nameof(listenFor)); + } + + public void OnContextEnd(ContextInfo context) + { + currentContext = null; + runListener.OnContextEnd(context); + } + + public void OnContextStart(ContextInfo context) + { + currentContext = context; + runListener.OnContextStart(context); + } + + public void OnSpecificationEnd(SpecificationInfo specification, Result result) + { + if (listenFor != null && !listenFor.Equals(specification.ToVisualStudioTestIdentifier(currentContext))) + { + return; + } + + runListener.OnSpecificationEnd(specification, result); + } + + public void OnSpecificationStart(SpecificationInfo specification) + { + if (listenFor != null && !listenFor.Equals(specification.ToVisualStudioTestIdentifier(currentContext))) + { + return; + } + + runListener.OnSpecificationStart(specification); + } + + public void OnAssemblyEnd(AssemblyInfo assembly) + { + runListener.OnAssemblyEnd(assembly); + } + + public void OnAssemblyStart(AssemblyInfo assembly) + { + runListener.OnAssemblyStart(assembly); + } + + public void OnFatalError(ExceptionResult exception) + { + runListener.OnFatalError(exception); + } + + public void OnRunEnd() + { + runListener.OnRunEnd(); + } + + public void OnRunStart() + { + runListener.OnRunStart(); + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Execution/SpecificationExecutor.cs b/src/Machine.Specifications.Runner.VisualStudio/Execution/SpecificationExecutor.cs new file mode 100644 index 00000000..055468d3 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Execution/SpecificationExecutor.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Machine.Specifications.Runner.VisualStudio.Execution +{ + public class SpecificationExecutor : ISpecificationExecutor + { + public void RunAssemblySpecifications(string assemblyPath, + IEnumerable specifications, + Uri adapterUri, + IFrameworkHandle frameworkHandle) + { + assemblyPath = Path.GetFullPath(assemblyPath); + +#if NETFRAMEWORK + using (var scope = new IsolatedAppDomainExecutionScope(assemblyPath)) + { + var executor = scope.CreateInstance(); +#else + var executor = new TestExecutor(); +#endif + var listener = new ProxyAssemblySpecificationRunListener(assemblyPath, frameworkHandle, adapterUri); + + executor.RunTestsInAssembly(assemblyPath, specifications, listener); +#if NETFRAMEWORK + } +#endif + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Execution/SpecificationFilterProvider.cs b/src/Machine.Specifications.Runner.VisualStudio/Execution/SpecificationFilterProvider.cs new file mode 100644 index 00000000..e076449d --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Execution/SpecificationFilterProvider.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Machine.Specifications.Model; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +namespace Machine.Specifications.Runner.VisualStudio.Execution +{ + public class SpecificationFilterProvider : ISpecificationFilterProvider + { + private static readonly TestProperty TagProperty = + TestProperty.Register(nameof(Tag), nameof(Tag), typeof(string), typeof(TestCase)); + + private static readonly TestProperty SubjectProperty = + TestProperty.Register(nameof(Subject), nameof(Subject), typeof(string), typeof(TestCase)); + + private readonly Dictionary testCaseProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [TestCaseProperties.FullyQualifiedName.Id] = TestCaseProperties.FullyQualifiedName, + [TestCaseProperties.DisplayName.Id] = TestCaseProperties.DisplayName + }; + + private readonly Dictionary traitProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [TagProperty.Id] = TagProperty, + [SubjectProperty.Id] = SubjectProperty + }; + + private readonly string[] supportedProperties; + + public SpecificationFilterProvider() + { + supportedProperties = testCaseProperties.Keys + .Concat(traitProperties.Keys) + .ToArray(); + } + + public IEnumerable FilteredTests(IEnumerable testCases, IRunContext runContext, IFrameworkHandle handle) + { + var filterExpression = runContext.GetTestCaseFilter(supportedProperties, propertyName => + { + if (testCaseProperties.TryGetValue(propertyName, out var testProperty)) + { + return testProperty; + } + if (traitProperties.TryGetValue(propertyName, out var traitProperty)) + { + return traitProperty; + } + return null; + }); + + handle?.SendMessage(TestMessageLevel.Informational, $"Machine Specifications Visual Studio Test Adapter - Filter property set '{filterExpression?.TestCaseFilterValue}'"); + + if (filterExpression == null) + { + return testCases; + } + + var filteredTests = testCases + .Where(x => filterExpression.MatchTestCase(x, propertyName => GetPropertyValue(propertyName, x))); + + return filteredTests; + } + + private object GetPropertyValue(string propertyName, TestObject testCase) + { + if (testCaseProperties.TryGetValue(propertyName, out var testProperty)) + { + if (testCase.Properties.Contains(testProperty)) + { + return testCase.GetPropertyValue(testProperty); + } + } + + if (traitProperties.TryGetValue(propertyName, out var traitProperty)) + { + var val = TraitContains(testCase, traitProperty.Id); + + if (val.Length == 1) + { + return val[0]; + } + + if (val.Length > 1) + { + return val; + } + } + + return null; + } + + private static string[] TraitContains(TestObject testCase, string traitName) + { + return testCase?.Traits? + .Where(x => x.Name == traitName) + .Select(x => x.Value) + .ToArray(); + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Execution/TestExecutor.cs b/src/Machine.Specifications.Runner.VisualStudio/Execution/TestExecutor.cs new file mode 100644 index 00000000..9958339c --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Execution/TestExecutor.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Machine.Specifications.Runner.Impl; +using Machine.Specifications.Runner.VisualStudio.Helpers; + +namespace Machine.Specifications.Runner.VisualStudio.Execution +{ + public class TestExecutor +#if NETFRAMEWORK + : MarshalByRefObject +#endif + { +#if NETFRAMEWORK + [System.Security.SecurityCritical] + public override object InitializeLifetimeService() + { + return null; + } +#endif + + private DefaultRunner CreateRunner(Assembly assembly, ISpecificationRunListener specificationRunListener) + { + var listener = new AggregateRunListener(new[] { + specificationRunListener, + new AssemblyLocationAwareRunListener(new[] { assembly }) + }); + + return new DefaultRunner(listener, RunOptions.Default); + } + + public void RunTestsInAssembly(string pathToAssembly, IEnumerable specsToRun, ISpecificationRunListener specificationRunListener) + { + DefaultRunner mspecRunner = null; + Assembly assemblyToRun = null; + + try + { + assemblyToRun = AssemblyHelper.Load(pathToAssembly); + mspecRunner = CreateRunner(assemblyToRun, specificationRunListener); + + var specsByContext = specsToRun.GroupBy(x => x.ContainerTypeFullName); + + mspecRunner.StartRun(assemblyToRun); + + foreach (var specs in specsByContext) + { + var fields = specs.Select(x => x.FieldName); + + mspecRunner.RunType(assemblyToRun, assemblyToRun.GetType(specs.Key), fields.ToArray()); + } + } + catch (Exception e) + { + specificationRunListener.OnFatalError(new ExceptionResult(e)); + } + finally + { + try + { + if (mspecRunner != null && assemblyToRun != null) + { + mspecRunner.EndRun(assemblyToRun); + } + } + catch (Exception exception) + { + try + { + var frameworkLogger = specificationRunListener as IFrameworkLogger; + + frameworkLogger?.SendErrorMessage("Machine Specifications Visual Studio Test Adapter - Error Ending Test Run.", exception); + } + catch + { + // ignored + } + } + } + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Helpers/AssemblyHelper.cs b/src/Machine.Specifications.Runner.VisualStudio/Helpers/AssemblyHelper.cs new file mode 100644 index 00000000..9712a802 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Helpers/AssemblyHelper.cs @@ -0,0 +1,25 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Machine.Specifications.Runner.VisualStudio.Helpers +{ + internal static class AssemblyHelper + { + public static Assembly Load(string path) + { + try + { +#if NETCOREAPP + return Assembly.Load(new AssemblyName(Path.GetFileNameWithoutExtension(path))); +#else + return Assembly.LoadFile(path); +#endif + } catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Helpers/IsolatedAppDomainExecutionScope.cs b/src/Machine.Specifications.Runner.VisualStudio/Helpers/IsolatedAppDomainExecutionScope.cs new file mode 100644 index 00000000..a622c547 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Helpers/IsolatedAppDomainExecutionScope.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Reflection.Metadata; +using System.Threading; + +namespace Machine.Specifications.Runner.VisualStudio.Helpers +{ +#if NETFRAMEWORK + public class IsolatedAppDomainExecutionScope : IDisposable + where T : MarshalByRefObject, new() + { + private readonly string assemblyPath; + + private readonly string appName = typeof(IsolatedAppDomainExecutionScope<>).Assembly.GetName().Name; + + private AppDomain appDomain; + + public IsolatedAppDomainExecutionScope(string assemblyPath) + { + if (string.IsNullOrEmpty(assemblyPath)) + { + throw new ArgumentException($"{nameof(assemblyPath)} is null or empty.", nameof(assemblyPath)); + } + + this.assemblyPath = assemblyPath; + } + + public T CreateInstance() + { + if (appDomain == null) + { + // Because we need to copy files around - we create a global cross-process mutex here to avoid multi-process race conditions + // in the case where both of those are true: + // 1. VSTest is told to run tests in parallel, so it spawns multiple processes + // 2. There are multiple test assemblies in the same directory + using (var mutex = new Mutex(false, $"{appName}_{Path.GetDirectoryName(assemblyPath).Replace(Path.DirectorySeparatorChar, '_')}")) + { + try + { + mutex.WaitOne(TimeSpan.FromMinutes(1)); + } + catch (AbandonedMutexException) + { + } + + try + { + appDomain = CreateAppDomain(assemblyPath, appName); + } + finally + { + try + { + mutex.ReleaseMutex(); + } + catch + { + } + } + } + } + + return (T)appDomain.CreateInstanceAndUnwrap(typeof(T).Assembly.FullName, typeof(T).FullName); + } + + + private static AppDomain CreateAppDomain(string assemblyPath, string appName) + { + // This is needed in the following two scenarios, so that the target test dll and its dependencies are loaded correctly: + // + // 1. pre-.NET Standard (old) .csproj and Visual Studio IDE Test Explorer run + // 2. vstest.console.exe run against .dll which is not in the build output folder (e.g. packaged build artifact) + // + CopyRequiredRuntimeDependencies(new[] + { + typeof(IsolatedAppDomainExecutionScope<>).Assembly, + typeof(MetadataReaderProvider).Assembly + }, Path.GetDirectoryName(assemblyPath)); + + var setup = new AppDomainSetup(); + setup.ApplicationName = appName; + setup.ShadowCopyFiles = "true"; + setup.ApplicationBase = setup.PrivateBinPath = Path.GetDirectoryName(assemblyPath); + setup.CachePath = Path.Combine(Path.GetTempPath(), appName, Guid.NewGuid().ToString()); + setup.ConfigurationFile = Path.Combine(Path.GetDirectoryName(assemblyPath), (Path.GetFileName(assemblyPath) + ".config")); + + return AppDomain.CreateDomain($"{appName}.dll", null, setup); + } + + private static void CopyRequiredRuntimeDependencies(IEnumerable assemblies, string destination) + { + foreach (Assembly assembly in assemblies) + { + var sourceAssemblyFile = assembly.Location; + var destinationAssemblyFile = Path.Combine(destination, Path.GetFileName(sourceAssemblyFile)); + + // file doesn't exist or is older + if (!File.Exists(destinationAssemblyFile) || File.GetLastWriteTimeUtc(sourceAssemblyFile) > File.GetLastWriteTimeUtc(destinationAssemblyFile)) + { + CopyWithoutLockingSourceFile(sourceAssemblyFile, destinationAssemblyFile); + } + } + } + + private static void CopyWithoutLockingSourceFile(string sourceFile, string destinationFile) + { + const int bufferSize = 10 * 1024; + + using (var inputFile = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize)) + using (var outputFile = new FileStream(destinationFile, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize)) + { + var buffer = new byte[bufferSize]; + int bytes; + + while ((bytes = inputFile.Read(buffer, 0, buffer.Length)) > 0) + { + outputFile.Write(buffer, 0, bytes); + } + } + } + + public void Dispose() + { + if (appDomain != null) + { + try + { + var cacheDirectory = appDomain.SetupInformation.CachePath; + + AppDomain.Unload(appDomain); + appDomain = null; + + if (Directory.Exists(cacheDirectory)) + { + Directory.Delete(cacheDirectory, true); + } + } + catch + { + // TODO: Logging here + } + } + } + } +#endif +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Helpers/NamingConversionExtensions.cs b/src/Machine.Specifications.Runner.VisualStudio/Helpers/NamingConversionExtensions.cs new file mode 100644 index 00000000..86d4dc6a --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Helpers/NamingConversionExtensions.cs @@ -0,0 +1,30 @@ +using System.Globalization; +using Machine.Specifications.Model; +using Machine.Specifications.Runner.VisualStudio.Discovery; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; + +namespace Machine.Specifications.Runner.VisualStudio.Helpers +{ + public static class NamingConversionExtensions + { + public static VisualStudioTestIdentifier ToVisualStudioTestIdentifier(this SpecificationInfo specification, ContextInfo context) + { + return new VisualStudioTestIdentifier(string.Format(CultureInfo.InvariantCulture, "{0}::{1}", context?.TypeName ?? specification.ContainingType, specification.FieldName)); + } + + public static VisualStudioTestIdentifier ToVisualStudioTestIdentifier(this SpecTestCase specification) + { + return new VisualStudioTestIdentifier(string.Format(CultureInfo.InvariantCulture, "{0}::{1}", specification.ContextFullType, specification.SpecificationName)); + } + + public static VisualStudioTestIdentifier ToVisualStudioTestIdentifier(this TestCase testCase) + { + return new VisualStudioTestIdentifier(testCase.FullyQualifiedName); + } + + public static VisualStudioTestIdentifier ToVisualStudioTestIdentifier(this Specification specification, Context context) + { + return new VisualStudioTestIdentifier(string.Format(CultureInfo.InvariantCulture, "{0}::{1}", context.Type.FullName, specification.FieldInfo.Name)); + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Helpers/SpecTestHelper.cs b/src/Machine.Specifications.Runner.VisualStudio/Helpers/SpecTestHelper.cs new file mode 100644 index 00000000..6b571dd8 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Helpers/SpecTestHelper.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics; +using Machine.Specifications.Runner.VisualStudio.Discovery; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; + +namespace Machine.Specifications.Runner.VisualStudio.Helpers +{ + public static class SpecTestHelper + { + public static TestCase GetTestCaseFromMspecTestCase(string source, SpecTestCase mspecTestCase, Uri testRunnerUri) + { + var vsTest = mspecTestCase.ToVisualStudioTestIdentifier(); + + var testCase = new TestCase(vsTest.FullyQualifiedName, testRunnerUri, source) + { + DisplayName = $"{mspecTestCase.ContextDisplayName} it {mspecTestCase.SpecificationDisplayName}", + CodeFilePath = mspecTestCase.CodeFilePath, + LineNumber = mspecTestCase.LineNumber + }; + + var classTrait = new Trait("ClassName", mspecTestCase.ClassName); + var subjectTrait = new Trait("Subject", string.IsNullOrEmpty(mspecTestCase.Subject) ? "No Subject" : mspecTestCase.Subject); + + testCase.Traits.Add(classTrait); + testCase.Traits.Add(subjectTrait); + + if (mspecTestCase.Tags != null) + { + foreach (var tag in mspecTestCase.Tags) + { + if (!string.IsNullOrEmpty(tag)) + { + var tagTrait = new Trait("Tag", tag); + testCase.Traits.Add(tagTrait); + } + } + } + + if (!string.IsNullOrEmpty(mspecTestCase.BehaviorFieldName)) + { + testCase.Traits.Add(new Trait("BehaviorField", mspecTestCase.BehaviorFieldName)); + } + + if (!string.IsNullOrEmpty(mspecTestCase.BehaviorFieldType)) + { + testCase.Traits.Add(new Trait("BehaviorType", mspecTestCase.BehaviorFieldType)); + } + + Debug.WriteLine($"TestCase {testCase.FullyQualifiedName}"); + + return testCase; + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Helpers/VisualStudioTestIdentifier.cs b/src/Machine.Specifications.Runner.VisualStudio/Helpers/VisualStudioTestIdentifier.cs new file mode 100644 index 00000000..39fb4919 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Helpers/VisualStudioTestIdentifier.cs @@ -0,0 +1,58 @@ +using System; +using System.Globalization; + +namespace Machine.Specifications.Runner.VisualStudio.Helpers +{ +#if NETFRAMEWORK + [Serializable] +#endif + public class VisualStudioTestIdentifier + { + public VisualStudioTestIdentifier() + { + } + + public VisualStudioTestIdentifier(string containerTypeFullName, string fieldName) + : this(string.Format(CultureInfo.InvariantCulture, "{0}::{1}", containerTypeFullName, fieldName)) + { + } + + public VisualStudioTestIdentifier(string fullyQualifiedName) + { + FullyQualifiedName = fullyQualifiedName; + } + + public string FullyQualifiedName { get; private set; } + + public string FieldName + { + get + { + return FullyQualifiedName.Split(new[] { "::" }, StringSplitOptions.RemoveEmptyEntries)[1]; + } + } + + public string ContainerTypeFullName + { + get + { + return FullyQualifiedName.Split(new[] { "::" }, StringSplitOptions.RemoveEmptyEntries)[0]; + } + } + + public override bool Equals(object obj) + { + if (obj is VisualStudioTestIdentifier test) + { + return FullyQualifiedName.Equals(test.FullyQualifiedName, StringComparison.Ordinal); + } + + return base.Equals(obj); + } + + public override int GetHashCode() + { + return FullyQualifiedName.GetHashCode(); + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Machine.Specifications.Runner.VisualStudio.csproj b/src/Machine.Specifications.Runner.VisualStudio/Machine.Specifications.Runner.VisualStudio.csproj new file mode 100644 index 00000000..8d9f6653 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Machine.Specifications.Runner.VisualStudio.csproj @@ -0,0 +1,41 @@ + + + + net472;net6.0 + Machine.Specifications.Runner.VisualStudio + Machine.Specifications.Runner.VisualStudio.TestAdapter + NU5127,NU5128 + false + + + + + + + + + + + + + + + + $(TargetsForTfmSpecificContentInPackage);NetCorePackageItems;NetFrameworkPackageItems + + + + + + + + + + + + + + + + + diff --git a/src/Machine.Specifications.Runner.VisualStudio/Machine.Specifications.Runner.VisualStudio.props b/src/Machine.Specifications.Runner.VisualStudio/Machine.Specifications.Runner.VisualStudio.props new file mode 100644 index 00000000..425b3009 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Machine.Specifications.Runner.VisualStudio.props @@ -0,0 +1,10 @@ + + + + + Machine.Specifications.Runner.VisualStudio.TestAdapter.dll + PreserveNewest + False + + + diff --git a/src/Machine.Specifications.Runner.VisualStudio/MspecTestDiscoverer.cs b/src/Machine.Specifications.Runner.VisualStudio/MspecTestDiscoverer.cs new file mode 100644 index 00000000..01059e05 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/MspecTestDiscoverer.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Machine.Specifications.Runner.VisualStudio.Discovery; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +namespace Machine.Specifications.Runner.VisualStudio +{ + public class MspecTestDiscoverer + { + private readonly ISpecificationDiscoverer discoverer; + + public MspecTestDiscoverer(ISpecificationDiscoverer discoverer) + { + this.discoverer = discoverer; + } + + public void DiscoverTests(IEnumerable sources, IMessageLogger logger, ITestCaseDiscoverySink discoverySink) + { + DiscoverTests(sources, logger, discoverySink.SendTestCase); + } + + public void DiscoverTests(IEnumerable sources, IMessageLogger logger, Action discoverySinkAction) + { + logger.SendMessage(TestMessageLevel.Informational, "Machine Specifications Visual Studio Test Adapter - Discovering Specifications."); + + var discoveredSpecCount = 0; + var sourcesWithSpecs = 0; + + var sourcesArray = sources.Distinct().ToArray(); + + foreach (var assemblyPath in sourcesArray) + { + try + { +#if NETFRAMEWORK + if (!File.Exists(Path.Combine(Path.GetDirectoryName(Path.GetFullPath(assemblyPath)), "Machine.Specifications.dll"))) + continue; +#endif + + sourcesWithSpecs++; + + logger.SendMessage(TestMessageLevel.Informational, $"Machine Specifications Visual Studio Test Adapter - Discovering...looking in {assemblyPath}"); + + var specs = discoverer.DiscoverSpecs(assemblyPath) + .Select(spec => SpecTestHelper.GetTestCaseFromMspecTestCase(assemblyPath, spec, MspecTestRunner.Uri)) + .ToList(); + + foreach (var discoveredTest in specs) + { + discoveredSpecCount++; + discoverySinkAction(discoveredTest); + } + } + catch (Exception discoverException) + { + logger.SendMessage(TestMessageLevel.Error, $"Machine Specifications Visual Studio Test Adapter - Error while discovering specifications in assembly {assemblyPath}." + Environment.NewLine + discoverException); + } + } + + logger.SendMessage(TestMessageLevel.Informational, $"Machine Specifications Visual Studio Test Adapter - Discovery Complete - {discoveredSpecCount} specifications in {sourcesWithSpecs} of {sourcesArray.Length} assemblies scanned."); + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/MspecTestExecutor.cs b/src/Machine.Specifications.Runner.VisualStudio/MspecTestExecutor.cs new file mode 100644 index 00000000..df400bde --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/MspecTestExecutor.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Machine.Specifications.Runner.VisualStudio.Execution; +using Machine.Specifications.Runner.VisualStudio.Helpers; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +namespace Machine.Specifications.Runner.VisualStudio +{ + public class MspecTestExecutor + { + private readonly ISpecificationExecutor executor; + + private readonly MspecTestDiscoverer discover; + + private readonly ISpecificationFilterProvider specificationFilterProvider; + + public MspecTestExecutor(ISpecificationExecutor executor, MspecTestDiscoverer discover, ISpecificationFilterProvider specificationFilterProvider) + { + this.executor = executor; + this.discover = discover; + this.specificationFilterProvider = specificationFilterProvider; + } + + public void RunTests(IEnumerable sources, IRunContext runContext, IFrameworkHandle frameworkHandle) + { + frameworkHandle.SendMessage(TestMessageLevel.Informational, "Machine Specifications Visual Studio Test Adapter - Executing Source Specifications."); + + var testsToRun = new List(); + + DiscoverTests(sources, frameworkHandle, testsToRun); + RunTests(testsToRun, runContext, frameworkHandle); + + frameworkHandle.SendMessage(TestMessageLevel.Informational, "Machine Specifications Visual Studio Test Adapter - Executing Source Specifications Complete."); + } + + public void RunTests(IEnumerable tests, IRunContext runContext, IFrameworkHandle frameworkHandle) + { + frameworkHandle.SendMessage(TestMessageLevel.Informational, "Machine Specifications Visual Studio Test Adapter - Executing Test Specifications."); + + var totalSpecCount = 0; + var executedSpecCount = 0; + var currentAssembly = string.Empty; + + try + { + var testCases = tests.ToArray(); + + foreach (var grouping in testCases.GroupBy(x => x.Source)) + { + currentAssembly = grouping.Key; + totalSpecCount += grouping.Count(); + + var filteredTests = specificationFilterProvider.FilteredTests(grouping.AsEnumerable(), runContext, frameworkHandle); + + var testsToRun = filteredTests + .Select(test => test.ToVisualStudioTestIdentifier()) + .ToArray(); + + frameworkHandle.SendMessage(TestMessageLevel.Informational, $"Machine Specifications Visual Studio Test Adapter - Executing {testsToRun.Length} tests in '{currentAssembly}'."); + + executor.RunAssemblySpecifications(grouping.Key, testsToRun, MspecTestRunner.Uri, frameworkHandle); + + executedSpecCount += testsToRun.Length; + } + + frameworkHandle.SendMessage(TestMessageLevel.Informational, $"Machine Specifications Visual Studio Test Adapter - Execution Complete - {executedSpecCount} of {totalSpecCount} specifications in {testCases.GroupBy(x => x.Source).Count()} assemblies."); + } + catch (Exception exception) + { + frameworkHandle.SendMessage(TestMessageLevel.Error, $"Machine Specifications Visual Studio Test Adapter - Error while executing specifications in assembly '{currentAssembly}'." + Environment.NewLine + exception); + } + } + + private void DiscoverTests(IEnumerable sources, IMessageLogger logger, List testsToRun) + { + discover.DiscoverTests(sources, logger, testsToRun.Add); + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/MspecTestRunner.cs b/src/Machine.Specifications.Runner.VisualStudio/MspecTestRunner.cs new file mode 100644 index 00000000..844c0b6f --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/MspecTestRunner.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using Machine.Specifications.Runner.VisualStudio.Discovery; +using Machine.Specifications.Runner.VisualStudio.Execution; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +namespace Machine.Specifications.Runner.VisualStudio +{ + [FileExtension(".exe")] + [FileExtension(".dll")] + [ExtensionUri(ExecutorUri)] + [DefaultExecutorUri(ExecutorUri)] + public class MspecTestRunner : ITestDiscoverer, ITestExecutor + { + private const string ExecutorUri = "executor://machine.vstestadapter"; + + public static readonly Uri Uri = new Uri(ExecutorUri); + + private readonly MspecTestDiscoverer testDiscoverer; + + private readonly MspecTestExecutor testExecutor; + + public MspecTestRunner() + : this(new BuiltInSpecificationDiscoverer(), new SpecificationExecutor(), new SpecificationFilterProvider()) + { + } + + public MspecTestRunner(ISpecificationDiscoverer discoverer, ISpecificationExecutor executor, ISpecificationFilterProvider specificationFilterProvider) + { + testDiscoverer = new MspecTestDiscoverer(discoverer); + testExecutor = new MspecTestExecutor(executor, testDiscoverer, specificationFilterProvider); + } + + public void DiscoverTests(IEnumerable sources, IDiscoveryContext discoveryContext, IMessageLogger logger, ITestCaseDiscoverySink discoverySink) + { + testDiscoverer.DiscoverTests(sources, logger, discoverySink); + } + + public void RunTests(IEnumerable tests, IRunContext runContext, IFrameworkHandle frameworkHandle) + { + testExecutor.RunTests(tests, runContext, frameworkHandle); + } + + public void RunTests(IEnumerable sources, IRunContext runContext, IFrameworkHandle frameworkHandle) + { + testExecutor.RunTests(sources, runContext, frameworkHandle); + } + + public void Cancel() + { + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Navigation/INavigationSession.cs b/src/Machine.Specifications.Runner.VisualStudio/Navigation/INavigationSession.cs new file mode 100644 index 00000000..c7297541 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Navigation/INavigationSession.cs @@ -0,0 +1,9 @@ +using System; + +namespace Machine.Specifications.Runner.VisualStudio.Navigation +{ + public interface INavigationSession : IDisposable + { + NavigationData GetNavigationData(string typeName, string fieldName); + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Navigation/NavigationData.cs b/src/Machine.Specifications.Runner.VisualStudio/Navigation/NavigationData.cs new file mode 100644 index 00000000..bf61fbe8 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Navigation/NavigationData.cs @@ -0,0 +1,17 @@ +namespace Machine.Specifications.Runner.VisualStudio.Navigation +{ + public class NavigationData + { + public static NavigationData Unknown { get; } = new NavigationData(null, 0); + + public NavigationData(string codeFile, int lineNumber) + { + CodeFile = codeFile; + LineNumber = lineNumber; + } + + public string CodeFile { get; } + + public int LineNumber { get; } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Navigation/NavigationSession.cs b/src/Machine.Specifications.Runner.VisualStudio/Navigation/NavigationSession.cs new file mode 100644 index 00000000..4fe24c69 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Navigation/NavigationSession.cs @@ -0,0 +1,50 @@ +using System.Linq; +using System.Reflection.Emit; +using Machine.Specifications.Runner.VisualStudio.Reflection; + +namespace Machine.Specifications.Runner.VisualStudio.Navigation +{ + public class NavigationSession : INavigationSession + { + private readonly AssemblyData assembly; + + public NavigationSession(string assemblyPath) + { + assembly = AssemblyData.Read(assemblyPath); + } + + public NavigationData GetNavigationData(string typeName, string fieldName) + { + var type = assembly.Types.FirstOrDefault(x => x.TypeName == typeName); + var method = type?.Constructors.FirstOrDefault(); + + if (method == null) + { + return NavigationData.Unknown; + } + + var instruction = method.Instructions + .Where(x => x.OperandType == OperandType.InlineField) + .FirstOrDefault(x => x.Name == fieldName); + + while (instruction != null) + { + var sequencePoint = method.GetSequencePoint(instruction); + + if (sequencePoint != null && !sequencePoint.IsHidden) + { + return new NavigationData(sequencePoint.FileName, sequencePoint.StartLine); + } + + instruction = instruction.Previous; + } + + return NavigationData.Unknown; + } + + public void Dispose() + { + assembly.Dispose(); + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Reflection/AssemblyData.cs b/src/Machine.Specifications.Runner.VisualStudio/Reflection/AssemblyData.cs new file mode 100644 index 00000000..7aadc489 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Reflection/AssemblyData.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +namespace Machine.Specifications.Runner.VisualStudio.Reflection +{ + public class AssemblyData : IDisposable + { + private readonly PEReader reader; + + private readonly MetadataReader metadata; + + private readonly SymbolReader symbolReader; + + private readonly object sync = new object(); + + private ReadOnlyCollection types; + + private AssemblyData(string assembly) + { + reader = new PEReader(File.OpenRead(assembly)); + metadata = reader.GetMetadataReader(); + symbolReader = new SymbolReader(assembly); + } + + public static AssemblyData Read(string assembly) + { + return new AssemblyData(assembly); + } + + public IReadOnlyCollection Types + { + get + { + if (types != null) + { + return types; + } + + lock (sync) + { + types = ReadTypes().AsReadOnly(); + } + + return types; + } + } + + public void Dispose() + { + reader.Dispose(); + } + + private List ReadTypes() + { + var values = new List(); + + foreach (var typeHandle in metadata.TypeDefinitions) + { + ReadType(values, typeHandle); + } + + return values; + } + + private void ReadType(List values, TypeDefinitionHandle typeHandle, string namespaceName = null) + { + var typeDefinition = metadata.GetTypeDefinition(typeHandle); + + var typeNamespace = string.IsNullOrEmpty(namespaceName) + ? metadata.GetString(typeDefinition.Namespace) + : namespaceName; + + var typeName = string.IsNullOrEmpty(namespaceName) + ? $"{typeNamespace}.{metadata.GetString(typeDefinition.Name)}" + : $"{typeNamespace}+{metadata.GetString(typeDefinition.Name)}"; + + values.Add(new TypeData(typeName, reader, metadata, symbolReader, typeDefinition)); + + foreach (var nestedTypeHandle in typeDefinition.GetNestedTypes()) + { + ReadType(values, nestedTypeHandle, typeName); + } + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Reflection/CodeReader.cs b/src/Machine.Specifications.Runner.VisualStudio/Reflection/CodeReader.cs new file mode 100644 index 00000000..0fc28f8f --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Reflection/CodeReader.cs @@ -0,0 +1,154 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Machine.Specifications.Runner.VisualStudio.Reflection +{ + public class CodeReader + { + private static readonly OperandType[] OperandTypes = Enumerable.Repeat((OperandType) 0xff, 0x11f).ToArray(); + + private static readonly string[] OperandNames = new string[0x11f]; + + static CodeReader() + { + foreach (var field in typeof(OpCodes).GetFields()) + { + var opCode = (OpCode) field.GetValue(null); + var index = (ushort) (((opCode.Value & 0x200) >> 1) | opCode.Value & 0xff); + + OperandTypes[index] = opCode.OperandType; + OperandNames[index] = opCode.Name; + } + } + + public IEnumerable GetInstructions(MetadataReader reader, ref BlobReader blob) + { + var instructions = new List(); + + InstructionData previous = null; + + while (blob.RemainingBytes > 0) + { + var offset = blob.Offset; + + var opCode = ReadOpCode(ref blob); + var opCodeName = GetDisplayName(opCode); + var operandType = GetOperandType(opCode); + + var name = operandType != OperandType.InlineNone + ? ReadOperand(reader, ref blob, operandType) + : null; + + previous = new InstructionData(opCode, opCodeName, operandType, offset, previous, name); + + instructions.Add(previous); + } + + return instructions; + } + + private string ReadOperand(MetadataReader reader, ref BlobReader blob, OperandType operandType) + { + var name = string.Empty; + + switch (operandType) + { + case OperandType.InlineI8: + case OperandType.InlineR: + blob.Offset += 8; + break; + + case OperandType.InlineBrTarget: + case OperandType.InlineI: + case OperandType.InlineSig: + case OperandType.InlineString: + case OperandType.InlineTok: + case OperandType.InlineType: + case OperandType.ShortInlineR: + blob.Offset += 4; + break; + + case OperandType.InlineField: + case OperandType.InlineMethod: + var handle = MetadataTokens.EntityHandle(blob.ReadInt32()); + + name = LookupToken(reader, handle); + break; + + case OperandType.InlineSwitch: + var length = blob.ReadInt32(); + blob.Offset += length * 4; + break; + + case OperandType.InlineVar: + blob.Offset += 2; + break; + + case OperandType.ShortInlineVar: + case OperandType.ShortInlineBrTarget: + case OperandType.ShortInlineI: + blob.Offset++; + break; + } + + return name; + } + + private string LookupToken(MetadataReader reader, EntityHandle handle) + { + if (handle.Kind == HandleKind.FieldDefinition) + { + var field = reader.GetFieldDefinition((FieldDefinitionHandle) handle); + + return reader.GetString(field.Name); + } + + if (handle.Kind == HandleKind.MethodDefinition) + { + var method = reader.GetMethodDefinition((MethodDefinitionHandle) handle); + + return reader.GetString(method.Name); + } + + return string.Empty; + } + + private ILOpCode ReadOpCode(ref BlobReader blob) + { + var opCodeByte = blob.ReadByte(); + + var value = opCodeByte == 0xfe + ? 0xfe00 + blob.ReadByte() + : opCodeByte; + + return (ILOpCode) value; + } + + private OperandType GetOperandType(ILOpCode opCode) + { + var index = (ushort) ((((int) opCode & 0x200) >> 1) | ((int) opCode & 0xff)); + + if (index >= OperandTypes.Length) + { + return (OperandType) 0xff; + } + + return OperandTypes[index]; + } + + private string GetDisplayName(ILOpCode opCode) + { + var index = (ushort) ((((int) opCode & 0x200) >> 1) | ((int) opCode & 0xff)); + + if (index >= OperandNames.Length) + { + return string.Empty; + } + + return OperandNames[index]; + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Reflection/InstructionData.cs b/src/Machine.Specifications.Runner.VisualStudio/Reflection/InstructionData.cs new file mode 100644 index 00000000..6acc2727 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Reflection/InstructionData.cs @@ -0,0 +1,65 @@ +using System.Reflection.Emit; +using System.Reflection.Metadata; +using System.Text; + +namespace Machine.Specifications.Runner.VisualStudio.Reflection +{ + public class InstructionData + { + public InstructionData(ILOpCode opCode, string opCodeName, OperandType operandType, int offset, InstructionData previous, string name = null) + { + OpCode = opCode; + OpCodeName = opCodeName; + OperandType = operandType; + Offset = offset; + Previous = previous; + Name = name; + } + + public ILOpCode OpCode { get; } + + public string OpCodeName { get; } + + public OperandType OperandType { get; } + + public string Name { get; } + + public int Offset { get; } + + public InstructionData Previous { get; } + + public override string ToString() + { + var value = new StringBuilder(); + + AppendLabel(value); + + value.Append(": "); + value.Append(OpCode); + + if (!string.IsNullOrEmpty(Name)) + { + value.Append(" "); + + if (OperandType == OperandType.InlineString) + { + value.Append("\""); + value.Append(Name); + value.Append("\""); + } + else + { + value.Append(Name); + } + } + + return value.ToString(); + } + + private void AppendLabel(StringBuilder value) + { + value.Append("IL_"); + value.Append(Offset.ToString("x4")); + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Reflection/MethodData.cs b/src/Machine.Specifications.Runner.VisualStudio/Reflection/MethodData.cs new file mode 100644 index 00000000..bcabc13a --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Reflection/MethodData.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +namespace Machine.Specifications.Runner.VisualStudio.Reflection +{ + public class MethodData + { + private readonly PEReader reader; + + private readonly MetadataReader metadata; + + private readonly SymbolReader symbolReader; + + private readonly MethodDefinition definition; + + private readonly MethodDefinitionHandle handle; + + private readonly object sync = new object(); + + private ReadOnlyCollection instructions; + + private List sequencePoints; + + public MethodData(string name, PEReader reader, MetadataReader metadata, SymbolReader symbolReader, MethodDefinition definition, MethodDefinitionHandle handle) + { + this.reader = reader; + this.metadata = metadata; + this.symbolReader = symbolReader; + this.definition = definition; + this.handle = handle; + + Name = name; + } + + public string Name { get; } + + public IReadOnlyCollection Instructions + { + get + { + if (instructions != null) + { + return instructions; + } + + lock (sync) + { + instructions = GetInstructions().AsReadOnly(); + } + + return instructions; + } + } + + public SequencePointData GetSequencePoint(InstructionData instruction) + { + if (sequencePoints == null) + { + lock (sync) + { + sequencePoints = GetSequencePoints().ToList(); + } + } + + return sequencePoints.FirstOrDefault(x => x.Offset == instruction.Offset); + } + + public override string ToString() + { + return Name; + } + + private List GetInstructions() + { + var blob = reader + .GetMethodBody(definition.RelativeVirtualAddress) + .GetILReader(); + + var codeReader = new CodeReader(); + + return codeReader.GetInstructions(metadata, ref blob).ToList(); + } + + private IEnumerable GetSequencePoints() + { + return symbolReader.ReadSequencePoints(handle); + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Reflection/SequencePointData.cs b/src/Machine.Specifications.Runner.VisualStudio/Reflection/SequencePointData.cs new file mode 100644 index 00000000..719c2fad --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Reflection/SequencePointData.cs @@ -0,0 +1,24 @@ +namespace Machine.Specifications.Runner.VisualStudio.Reflection +{ + public class SequencePointData + { + public SequencePointData(string fileName, int startLine, int endLine, int offset, bool isHidden) + { + FileName = fileName; + StartLine = startLine; + EndLine = endLine; + Offset = offset; + IsHidden = isHidden; + } + + public string FileName { get; } + + public int StartLine { get; } + + public int EndLine { get; } + + public int Offset { get; } + + public bool IsHidden { get; } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Reflection/SymbolReader.cs b/src/Machine.Specifications.Runner.VisualStudio/Reflection/SymbolReader.cs new file mode 100644 index 00000000..5d65532a --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Reflection/SymbolReader.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; + +namespace Machine.Specifications.Runner.VisualStudio.Reflection +{ + public class SymbolReader + { + private readonly MetadataReader reader; + + public SymbolReader(string assembly) + { + var symbols = Path.ChangeExtension(assembly, "pdb"); + + if (File.Exists(symbols)) + { + reader = MetadataReaderProvider + .FromPortablePdbStream(File.OpenRead(symbols)) + .GetMetadataReader(); + } + } + + public IEnumerable ReadSequencePoints(MethodDefinitionHandle method) + { + if (reader == null) + { + return Enumerable.Empty(); + } + + return reader + .GetMethodDebugInformation(method) + .GetSequencePoints() + .Select(x => + { + var document = reader.GetDocument(x.Document); + var fileName = reader.GetString(document.Name); + + return new SequencePointData(fileName, x.StartLine, x.EndLine, x.Offset, x.IsHidden); + }); + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Reflection/TypeData.cs b/src/Machine.Specifications.Runner.VisualStudio/Reflection/TypeData.cs new file mode 100644 index 00000000..36ceaae9 --- /dev/null +++ b/src/Machine.Specifications.Runner.VisualStudio/Reflection/TypeData.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +namespace Machine.Specifications.Runner.VisualStudio.Reflection +{ + public class TypeData + { + private readonly PEReader reader; + + private readonly MetadataReader metadata; + + private readonly SymbolReader symbolReader; + + private readonly TypeDefinition definition; + + private readonly object sync = new object(); + + private ReadOnlyCollection methods; + + public TypeData(string typeName, PEReader reader, MetadataReader metadata, SymbolReader symbolReader, TypeDefinition definition) + { + this.reader = reader; + this.metadata = metadata; + this.symbolReader = symbolReader; + this.definition = definition; + + TypeName = typeName; + } + + public string TypeName { get; } + + public IReadOnlyCollection Constructors + { + get + { + if (methods != null) + { + return methods; + } + + lock (sync) + { + methods = GetConstructors().AsReadOnly(); + } + + return methods; + } + } + + public override string ToString() + { + return TypeName; + } + + private List GetConstructors() + { + var values = new List(); + + foreach (var methodHandle in definition.GetMethods()) + { + var methodDefinition = metadata.GetMethodDefinition(methodHandle); + var parameters = methodDefinition.GetParameters(); + + var methodName = metadata.GetString(methodDefinition.Name); + + if (IsConstructor(methodDefinition, methodName) && parameters.Count == 0) + { + values.Add(new MethodData(methodName, reader, metadata, symbolReader, methodDefinition, methodHandle)); + } + } + + return values; + } + + private bool IsConstructor(MethodDefinition method, string name) + { + return method.Attributes.HasFlag(MethodAttributes.RTSpecialName) && + method.Attributes.HasFlag(MethodAttributes.SpecialName) && + name == ".ctor"; + } + } +} diff --git a/src/Machine.Specifications.Runner.VisualStudio/Resources/Machine.png b/src/Machine.Specifications.Runner.VisualStudio/Resources/Machine.png new file mode 100644 index 00000000..20b80d34 Binary files /dev/null and b/src/Machine.Specifications.Runner.VisualStudio/Resources/Machine.png differ diff --git a/src/Machine.Specifications.Should.Specs/Machine.Specifications.Should.Specs.csproj b/src/Machine.Specifications.Should.Specs/Machine.Specifications.Should.Specs.csproj index 8dc0dfb7..412662ea 100644 --- a/src/Machine.Specifications.Should.Specs/Machine.Specifications.Should.Specs.csproj +++ b/src/Machine.Specifications.Should.Specs/Machine.Specifications.Should.Specs.csproj @@ -1,17 +1,20 @@ - + net472;net8.0 + enable + enable + latest false - - + + diff --git a/src/Machine.Specifications.Should.Specs/ShouldBeLikeSpecs.cs b/src/Machine.Specifications.Should.Specs/ShouldBeLikeSpecs.cs index d294d5e8..83177e58 100644 --- a/src/Machine.Specifications.Should.Specs/ShouldBeLikeSpecs.cs +++ b/src/Machine.Specifications.Should.Specs/ShouldBeLikeSpecs.cs @@ -414,7 +414,8 @@ public class Dummy public int[] Prop1 { get; set; } } - Establish context = () => obj1 = new Dummy { Prop1 = new[] { 1, 1, 1 } }; + Establish context = () => + obj1 = new Dummy { Prop1 = new[] { 1, 1, 1 } }; class and_the_objects_are_similar { @@ -466,15 +467,17 @@ class and_the_objects_are_different_and_have_null_values exception.ShouldBeOfExactType(); It should_contain_message = () => - exception.Message.ShouldEqual( - @"""Prop1"":" + Environment.NewLine + - @" Expected: [null]" + Environment.NewLine + - @" But was: System.Int32[]:" + Environment.NewLine + - @"{" + Environment.NewLine + - @" [1]," + Environment.NewLine + - @" [1]," + Environment.NewLine + - @" [1]" + Environment.NewLine + - @"}"); + exception.Message.Trim().ShouldEqual( + """ + "Prop1": + Expected: [null] + But was: System.Int32[]: + { + [1], + [1], + [1] + } + """.Trim()); } class and_the_objects_are_different_and_the_actual_object_has_a_null_value diff --git a/src/Machine.Specifications.Should/Machine.Specifications.Should.csproj b/src/Machine.Specifications.Should/Machine.Specifications.Should.csproj index e68fa443..c71e8b45 100644 --- a/src/Machine.Specifications.Should/Machine.Specifications.Should.csproj +++ b/src/Machine.Specifications.Should/Machine.Specifications.Should.csproj @@ -2,6 +2,9 @@ net472;net6.0 + enable + enable + latest diff --git a/src/Machine.Specifications.Should/Utility/Internal/PrettyPrintingExtensions.cs b/src/Machine.Specifications.Should/Utility/Internal/PrettyPrintingExtensions.cs index 8785c1f2..abaf20f4 100644 --- a/src/Machine.Specifications.Should/Utility/Internal/PrettyPrintingExtensions.cs +++ b/src/Machine.Specifications.Should/Utility/Internal/PrettyPrintingExtensions.cs @@ -170,17 +170,17 @@ public static string EachToUsefulString(this IEnumerable enumerable) var sb = new StringBuilder(); sb.AppendLine("{"); - sb.Append(string.Join(",\r\n", array.Select(x => x.ToUsefulString().Tab()).Take(10).ToArray())); + sb.Append(string.Join($",{Environment.NewLine}", array.Select(x => x.ToUsefulString().Tab()).Take(10).ToArray())); if (array.Length > 10) { if (array.Length > 11) { - sb.AppendLine($",\r\n ...({array.Length - 10} more elements)"); + sb.AppendLine($",{Environment.NewLine} ...({array.Length - 10} more elements)"); } else { - sb.AppendLine(",\r\n" + array.Last().ToUsefulString().Tab()); + sb.AppendLine($",{Environment.NewLine}" + array.Last().ToUsefulString().Tab()); } } else @@ -214,7 +214,7 @@ internal static string ToUsefulString(this object obj) { var enumerable = items.Cast(); - return items.GetType() + ":\r\n" + enumerable.EachToUsefulString(); + return items.GetType() + $":{Environment.NewLine}" + enumerable.EachToUsefulString(); } var str = obj.ToString();