diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1adad1ecb9..8c2c0f9438 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,7 +8,7 @@ The codebase ships several distinct (but related) products. Knowing which produc - `src/Platform/Microsoft.Testing.Platform` — Microsoft.Testing.Platform (MTP), a lightweight, in-process test host that replaces VSTest. Most other folders under `src/Platform/` are MTP extensions (`TrxReport`, `CrashDump`, `HangDump`, `HotReload`, `Retry`, `Telemetry`, `HtmlReport`, `AzureDevOpsReport`, `MSBuild`, `VSTestBridge`, …). - `src/TestFramework` — MSTest itself: the public `Microsoft.VisualStudio.TestTools.UnitTesting` API (attributes, `Assert`, `TestContext`, …) plus `TestFramework.Extensions`. -- `src/Adapter` — bridges MSTest to test hosts: `MSTest.TestAdapter` (VSTest adapter), `MSTestAdapter.PlatformServices` (platform-services abstraction shared by both hosts), and `MSTest.Engine` (MTP-native execution engine used by source-generated tests). +- `src/Adapter` — bridges MSTest to test hosts: `MSTest.TestAdapter` (VSTest adapter) and `MSTestAdapter.PlatformServices` (platform-services abstraction shared by both hosts). - `src/Analyzers` — Roslyn analyzers and code fixes shipped as `MSTest.Analyzers`. - `src/Package/MSTest.Sdk` — the MSBuild project SDK that wires the pieces together for consumers. - `test/UnitTests/.UnitTests` — fast unit tests for each project. diff --git a/.github/workflows/add-tests.md b/.github/workflows/add-tests.md index 97fa7d8369..18a41f30ec 100644 --- a/.github/workflows/add-tests.md +++ b/.github/workflows/add-tests.md @@ -68,7 +68,6 @@ Analyze the pull request diff to identify source files that were added or modifi - `src/TestFramework/` → `test/UnitTests/TestFramework.UnitTests/` - `src/Adapter/MSTest.TestAdapter/` → `test/UnitTests/MSTestAdapter.UnitTests/` - `src/Adapter/MSTestAdapter.PlatformServices/` → `test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/` - - `src/Adapter/MSTest.Engine/` → `test/UnitTests/MSTest.Engine.UnitTests/` - `src/Analyzers/MSTest.Analyzers/` → `test/UnitTests/MSTest.Analyzers.Tests/` (if exists) - `src/Analyzers/MSTest.SourceGeneration/` → `test/UnitTests/MSTest.SourceGeneration.UnitTests/` - `src/Platform/` → `test/UnitTests/` (find matching test project by name) diff --git a/Directory.Build.props b/Directory.Build.props index fe3901dfe7..ce51a5be1e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -50,12 +50,12 @@ - alpha + alpha - 2.0.0 + 2.0.0 diff --git a/MSTest.slnf b/MSTest.slnf index 357d59f90a..3c3f4c624a 100644 --- a/MSTest.slnf +++ b/MSTest.slnf @@ -6,7 +6,6 @@ "samples\\FxExtensibility\\FxExtensibility.csproj", "samples\\NUnitPlayground\\NUnitPlayground.csproj", "samples\\Playground\\Playground.csproj", - "src\\Adapter\\MSTest.Engine\\MSTest.Engine.csproj", "src\\Adapter\\MSTest.TestAdapter\\MSTest.TestAdapter.csproj", "src\\Adapter\\MSTestAdapter.PlatformServices\\MSTestAdapter.PlatformServices.csproj", "src\\Analyzers\\MSTest.Analyzers.CodeFixes\\MSTest.Analyzers.CodeFixes.csproj", diff --git a/NonWindowsTests.slnf b/NonWindowsTests.slnf index 30129ee81a..babc740760 100644 --- a/NonWindowsTests.slnf +++ b/NonWindowsTests.slnf @@ -2,7 +2,6 @@ "solution": { "path": "TestFx.slnx", "projects": [ - "src\\Adapter\\MSTest.Engine\\MSTest.Engine.csproj", "src\\Adapter\\MSTest.TestAdapter\\MSTest.TestAdapter.csproj", "src\\Adapter\\MSTestAdapter.PlatformServices\\MSTestAdapter.PlatformServices.csproj", "src\\Analyzers\\MSTest.Analyzers.CodeFixes\\MSTest.Analyzers.CodeFixes.csproj", diff --git a/TestFx.slnx b/TestFx.slnx index 09a883489c..68f70e5c5f 100644 --- a/TestFx.slnx +++ b/TestFx.slnx @@ -55,7 +55,6 @@ - @@ -133,7 +132,6 @@ - diff --git a/docs/README.md b/docs/README.md index 7116f24e5a..bda573971c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -37,6 +37,10 @@ For technical reasoning and implementation details, you can refer to the list of - [DataSource Attribute Vs ITestDataSource](RFCs/007-DataSource-Attribute-VS-ITestDataSource.md) - [Test case timeout via runsettings](RFCs/008-TestCase-Timeout.md) +## Design notes + +- [MSTest source generator — design](source-generator/design.md): scope, fallback categories, discovery limitations and trim/AOT story for the `MSTest.SourceGeneration` package. + ## Releases You can find all features and bugs fixed in all our releases by looking at [releases.md](releases.md). diff --git a/docs/source-generator/design.md b/docs/source-generator/design.md new file mode 100644 index 0000000000..1a41e6b0a1 --- /dev/null +++ b/docs/source-generator/design.md @@ -0,0 +1,481 @@ +# MSTest source generator — design + +This document describes the design, scope and current limitations of the MSTest source +generator that ships in the `MSTest.SourceGeneration` package. It is intentionally +opinionated: where there is a deliberate gap, the gap and its rationale are documented so +that adding new code paths does not silently change the contract. + +## Goals + +1. **Trim/Native AOT safety for test assemblies.** When a test project is published with + `PublishAot=true` or `PublishTrimmed=true`, MSTest must continue to discover and run + tests without surfacing IL2026 / IL3050 warnings to the user and without runtime + `MissingMethodException` failures caused by the trimmer removing test members. +2. **No user-visible API change.** Opting in is a single NuGet reference. Existing test + code keeps working. +3. **Reduced reflection cost.** Move the per-assembly `Assembly.GetTypes()` and per-class + `Type.GetMethods()` scans from startup to compile time. + +## What the generator emits today + +For every `[TestClass]` declared **directly** on a non-static, non-abstract, non-generic, +non-`file`-local, accessible type, the generator produces: + +1. A `[ModuleInitializer]`-decorated static method (`MSTestSourceGeneratedReflectionMetadata.Initialize`). +2. `[DynamicDependency(All, typeof(T))]` on that method for the test class **and every + accessible non-generic base type** in its inheritance chain (so members declared on an + abstract base — `[ClassInitialize]`, `[ClassCleanup]`, `[AssemblyInitialize]`, + `[AssemblyCleanup]`, `TestContext` setter — are preserved by the trimmer). +3. A `types` array containing the concrete test classes. +4. A `testMethods` dictionary mapping each test class to the `MethodInfo`s for its + `[TestMethod]`-annotated (or `TestMethod`-subclass-annotated) methods, including + methods inherited from base classes, deduped by signature. +5. A `ResolveMethod` helper that resolves each method by name + parameter types at module + initialization, throwing `MissingMethodException` if the lookup fails. +6. A call to `ReflectionMetadataHook.Register` that hands this data to the adapter and + replaces `ReflectionOperations` with `SourceGeneratedReflectionOperations` for the + lifetime of the process. + +The `[ModuleInitializer]` runs once per test assembly when the CLR first touches that +assembly. Multiple test assemblies in the same process register independently and are +merged into a `CompositeSourceGeneratedReflectionDataProvider`. + +## What the generator does NOT emit + +These fields exist on `SourceGeneratedReflectionDataProvider` but are not populated by the +current emitter. The adapter always falls back to runtime reflection for them. Closing +each gap is tractable engineering work — none of the fields is fundamentally blocked: + +| Field on the provider | Used by | Status | +|---|---|---| +| `TypeAttributes` | `IReflectionOperations.GetCustomAttributes(Type)` | Always falls back | +| `TypeMethodAttributes` | `IReflectionOperations.GetCustomAttributes(MethodInfo)` | Always falls back | +| `AssemblyAttributes` | `IReflectionOperations.GetCustomAttributes(Assembly, Type)` | Always falls back | +| `TypeConstructors` | `IReflectionOperations.GetDeclaredConstructors` | Always falls back | +| `TypeConstructorsInvoker` | `IReflectionOperations.CreateInstance` | Always falls back | +| `TypeProperties` | `IReflectionOperations.GetDeclaredProperties` | Always falls back | +| `TypePropertiesByName` | `IReflectionOperations.GetRuntimeProperty` | Always falls back | +| `TypeMethodLocations` | Source-location navigation | Returns empty (no navigation) | + +> **The source-gen path today is best understood as "type rooting + test-method +> pre-resolution + trimmer hints" rather than a full reflection replacement.** Attribute +> reads, constructor invocation and property reflection still hit the reflection path — +> the trimmer hints from `[DynamicDependency]` keep that path runnable under AOT/trimming. + +## Discovery limitations + +The following user-code shapes are silently skipped by the generator. They keep working +when source-gen is not active (because reflection sees them), but are invisible to the +source-gen registry. Where applicable, an analyzer warns about the limitation: + +| Shape | Skipped because | Workaround | Warning | +|---|---|---|---| +| Inherited `[TestClass]` (attribute applied only to the base) | `SyntaxValueProvider.ForAttributeWithMetadataName` does not follow inheritance | Apply `[TestClass]` directly to the derived class | MSTEST0069 | +| Open generic test class (`class Foo`) | `typeof(Foo)` is invalid at module-initializer scope | Make the class non-generic, or instantiate a concrete derived class | — | +| Generic test method (`void Test(T value)`) | `typeof(T)` is invalid at module-initializer scope | Use a non-generic test method that constructs the type itself | — | +| Test method with `ref` / `out` / `in` / `ref readonly` parameter | `typeof(T)` for by-ref parameters round-trips as `T&` and the resolver's `typeof(T) == ParameterType` check would fail | Use a wrapper type or a non-by-ref signature | — | +| `file`-local test class | The generated module initializer lives in a different file | Move the class out of file scope | — | +| Private / protected nested test class | The generated `internal` module initializer cannot reference it (CS0122) | Make the type `internal` or more visible | — | +| Static test class | Source-gen models instance-based test execution | Make the class non-static | — | +| Abstract test class | Not directly runnable; but its members are still rooted via `[DynamicDependency]` because of the per-base chain emission | Annotate a concrete derived class with `[TestClass]` | — | + +### Future direction: inherited `[TestClass]` + +Discovery of inherited `[TestClass]` is intentionally deferred. The plan is to introduce +an opt-in marker attribute (name TBD) that the user adds to the derived class. The +attribute carries no runtime behavior; it only signals the generator to emit the type as +a test class. This keeps the syntactic-attribute fast-path (`ForAttributeWithMetadataName`) +intact while letting users explicitly opt back into inheritance. + +## `SourceGeneratedReflectionOperations` fallbacks + +Every call site on `SourceGeneratedReflectionOperations` that can return reflection data +fits into one of three explicit categories. Each is marked with a `// Category X` comment +in the source to keep the design choice visible. + +### Category A — Generator-gap fallback + +The corresponding source-gen field is not populated by today's emitter. The method +always falls through. Closable by extending the emitter. + +- `GetCustomAttributes(MemberInfo)` for `Type` and `MethodInfo` +- `GetCustomAttributes(Assembly, Type)` +- `GetDeclaredConstructors` +- `GetDeclaredProperties` +- `GetRuntimeProperty` +- `CreateInstance` + +### Category B — Contract-mismatch fallback + +The interface contract demands every method (or similar), but the source generator +intentionally models only test methods. Always-delegate is the correct design; the +generator cannot pre-resolve methods it does not know about. + +- `GetDeclaredMethods` +- `GetRuntimeMethods` +- The not-found branch of `GetRuntimeMethod` + +### Category C — Cross-assembly fallback + +The lookup targets an assembly that did not opt into source generation (test framework, +adapter, extensions, test assets packed without the generator). No amount of generator +work eliminates this. + +- `GetType(string)` (always — `Type.GetType` resolves only assembly-qualified names) +- The no-match branch of `GetType(Assembly, string)` +- `GetDefinedTypes` for assemblies with no source-gen registration +- `GetCustomAttributes(MemberInfo)` for non-`Type`, non-`MethodInfo` members + +### Rule for new fallbacks + +When adding a new method that can fall back to reflection, mark the call site with a +`// Category A/B/C: ` comment. This keeps the design surface auditable: blind +corners are fallbacks that look intentional but were really oversights — labelling each +one prevents that. + +## Trim / Native AOT story + +This section answers the recurring question: *can we silence trim/AOT warnings without +the source generator "touching" the types?* + +The story has two distinct halves: the **warning** story (compile/publish time) and the +**runtime** story. + +### The warning story + +Already solved today **without** the source generator. The adapter's reflection paths in +`ReflectionOperations`, `AssemblyResolver`, `ManagedNameHelper`, +`DataSerializationHelper`, etc. are annotated with `[UnconditionalSuppressMessage]` +(IL2026 / IL3050) with the standard justification: + +> *"Native AOT support relies on MSTest source-generated reflection metadata, not on this +> code path."* + +Because user test code itself never calls reflection-flagged APIs (it just declares +classes and methods), no IL2026 / IL3050 warning propagates from MSTest into a user's +test project. The compile-time / publish-time experience is clean regardless of whether +the user references `MSTest.SourceGeneration`. + +### The runtime story + +Suppression is **not** preservation. The trimmer still removes unused members — the +suppression just stops the compiler from complaining about it. When the user runs an +AOT-published or trimmed test assembly, the reflection paths in `ReflectionOperations` +will execute and find that their target methods / types have been trimmed away — +typically surfacing as `MissingMethodException` or zero discovered tests. + +To keep tests runnable under AOT/trimming, *something* has to root the test types and +their members. The choices are: + +#### Option 1 — The current source generator + +Emits `[DynamicDependency(All, typeof(MyTests))]` per `[TestClass]` and per accessible +base type. Preserves exactly the test-related types and their members; everything else +in the assembly remains eligible for trimming. **Pros:** minimal binary size, no manual +configuration. **Cons:** generator work for every emitter gap (see Category A above). + +#### Option 2 — `` + +The user (or `MSTest.Sdk`, on their behalf) adds the test assembly as a trimmer root: + +```xml + + + +``` + +This tells the trimmer "do not trim anything in this assembly". Tests run because all +test types and members in the test assembly survive. + +This is a viable alternative for the *rooting* concern, but **it does not replace the +source generator**. The differences are concrete: + +- `TrimmerRootAssembly` is a build-time decision only. At runtime the adapter still + calls `Assembly.GetTypes()` + per-class `Type.GetMethods()` to discover tests, because + `ReflectionMetadataHook.Register` is never called and `SourceGeneratedReflectionOperations` + is never installed. The source generator replaces that scan with a pre-computed + registry handed to the adapter at module-init time. This is the headline performance + win — large test assemblies (and especially Native-AOT, where the type system is + slower than on CoreCLR) feel it most. +- `TrimmerRootAssembly Include="$(AssemblyName)"` only preserves *your* assembly. Base + classes that live in a shared library (a `TestCommon.dll` carrying + `[AssemblyInitialize]`-bearing fixtures, etc.) are still trimmable. The source + generator emits `[DynamicDependency(All, typeof(BaseInOtherAssembly))]` which the + trimmer honors across assemblies. +- `TrimmerRootAssembly` keeps the entire test assembly — helpers, mocks, fixtures, dead + code. Source generation roots only the test classes and their base chain; everything + else remains trimmable. Smaller published binary. +- Configuration cost: NuGet reference vs. a manually-added MSBuild item. (`MSTest.Sdk` + could of course automate the latter.) + +In return, `TrimmerRootAssembly` covers cases the generator skips today — inherited +`[TestClass]`, open-generic test classes, generic test methods, `file`-local classes — +because the reflection-fallback paths inside `SourceGeneratedReflectionOperations` will +still find their members. This is why the two are useful together (see Recommendation +below), but they are not interchangeable. + +> The repo already uses `TrimmerRootAssembly` to validate trim safety of the framework +> itself — see `test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs`. +> The same lever is available to consumers. + +#### Option 3 — Hand-written `[DynamicDependency]` + +The user puts `[DynamicDependency(All, typeof(MyTests))]` on a stub method per test +class. **Pros:** preserves exactly what's needed. **Cons:** tedious, easy to forget; the +analyzer warning surface to catch missing roots would essentially have to reinvent the +source generator. Not recommended in practice. + +#### Option 4 — Build-time MSBuild emission + +A target in `MSTest.Sdk` that scans `[TestClass]`-bearing types in compiled IL (e.g. via +a small post-compile MSBuild task) and emits a generated file with the right +`[DynamicDependency]` attributes. **Pros:** no Roslyn dependency. **Cons:** essentially +re-implements the source generator with a different host; loses the IDE-time incremental +benefit of `IIncrementalGenerator`. Not pursued. + +### Recommendation + +For **most** AOT/trimmed test scenarios, the recommended sequence is: + +1. Reference `MSTest.SourceGeneration` — this gives you the registry hand-off (skips + `Assembly.GetTypes()` at startup), fine-grained rooting, and cross-assembly base-type + preservation. +2. If a test shape is not yet supported by the generator (inherited `[TestClass]`, open + generics, etc.), add `` as a + backstop. The reflection fallback paths in `SourceGeneratedReflectionOperations` will + still work; the trimmer will now keep the members they read. + +The source generator and `` solve overlapping but distinct +problems. The generator is the discovery + rooting path; `TrimmerRootAssembly` is a +coarse rooting backstop. Pairing them is the most robust configuration today. + +## Roadmap + +In rough priority order: + +1. **Document every `// Category A/B/C` site in code** — done. +2. **This document** — done. +3. **Opt-in attribute for inherited `[TestClass]`** — a new attribute (name TBD) that + the user applies to a derived class to add it to the generator's discovery set + without re-applying `[TestClass]`. Replaces / refines MSTEST0069. +4. **Populate `TypeAttributes`** so type-attribute reads stop falling back. This is the + highest-value Category A gap because attribute reads happen for every test class at + discovery. +5. **Populate `TypeMethodAttributes`** for the same reason at the method level. +6. **Populate `TypeConstructors` + `TypeConstructorsInvoker`** so instance creation runs + through a generated invoker. This is the trim/AOT win that goes beyond just + "preserve the constructor": it also avoids `Activator.CreateInstance`. +7. **Populate `TypeProperties` + `TypePropertiesByName`** for `TestContext` and similar + well-known properties. +8. **Source-location data** for IDE navigation parity with the reflection path. + +Each item is small enough to be its own PR with its own tests. None of them blocks any +other. + +## Performance positioning vs. delegate-based source generators (e.g. TUnit) + +This is worth being explicit about because it shapes which roadmap items deliver real +user value. + +### What the current generator saves at startup + +- `Assembly.GetTypes()` — skipped. The registry already lists test types. (Inexpensive + on CoreCLR; meaningful on Native AOT where the type system is JIT-less.) +- Per-class `Type.GetMethods()` + `GetCustomAttribute` filtering — + skipped. The registry already lists test methods per class. +- `Type.GetMethod(name, paramTypes)` per test method — still reflection, but executed + once at module-init rather than at discovery time. + +For a large test assembly this is a real cold-start improvement. + +### What the current generator does NOT save at execution time + +Every test execution still goes through the same code paths as reflection-mode MSTest: + +- `Activator.CreateInstance(typeof(MyTests))` to construct the test instance. +- `MethodInfo.Invoke(instance, args)` to invoke the test body. +- `GetCustomAttributes(...)` for `[ExpectedException]`, `[Timeout]`, `[TestProperty]`, + etc. +- `PropertyInfo.SetValue(...)` for `TestContext` injection. + +The trimmer hints we emit keep those reflection calls working under AOT, but they do +not make them faster. **The per-test hot path is essentially the same speed as +reflection-mode MSTest.** + +### How a delegate-based source generator (TUnit-style) differs + +Frameworks like TUnit took a fundamentally different design. Instead of populating a +reflection registry that the existing execution engine consumes, they emit per-test +*delegates*: + +| Operation | TUnit-style | MSTest source-gen today | +|---|---|---| +| Construct test instance | `static () => new MyTests()` | `Activator.CreateInstance(typeof(MyTests))` | +| Invoke test method | `static (instance, args) => ((MyTests)instance).MyTest((int)args[0])` | `MethodInfo.Invoke(instance, args)` | +| Read `[Timeout(5000)]` | Generator reads attribute at compile time, bakes `Timeout = 5000` into a metadata record | `method.GetCustomAttribute().Timeout` | +| `DataRow` binding | Typed constants + typed casts inside the delegate | Reflection + `Convert.ChangeType` | +| `TestContext` injection | Baked property setter delegate | `PropertyInfo.SetValue` | + +`MethodInfo.Invoke` is roughly an order of magnitude slower than a direct delegate call +for trivial method bodies. For tests whose own body is fast (microseconds), +delegate-based generators measurably win on raw throughput. **We do not compete on +per-test execution throughput today.** + +### Why not just emit delegates here too? + +**The work has already started.** A proof-of-concept generator that emits exactly this +shape lives in `src/Analyzers/MSTest.AotReflection.SourceGeneration/` (marked +`false`, tracked by +[issue #1837](https://github.com/microsoft/testfx/issues/1837)). It emits a per-assembly +`MSTestReflectionMetadata` registry where each test class carries: + +- `Func Invoke` per constructor — replaces `Activator.CreateInstance`. +- `Func Invoke` per test method — replaces + `MethodInfo.Invoke`. +- `Func Get` / `Action Set` per property — replaces + `PropertyInfo.SetValue` / `GetValue`. +- Pre-materialized `Attribute[]` arrays — replaces `GetCustomAttributes(...)`. + +What is left is the *wiring* from that registry into the adapter, which can be staged: + +1. Merge / route the PoC's output through `MSTest.SourceGeneration` and feed it into + `SourceGeneratedReflectionDataProvider` (populate `TypeConstructorsInvoker`, + `TypeAttributes`, `TypeMethodAttributes`, etc.). The Category A fast paths in + `SourceGeneratedReflectionOperations` activate automatically — no engine change. +2. Replace the `MethodInfo` returned by `ITestMethod.MethodInfo` with a + `GeneratedTestMethodInfo` (new class, mirroring `ReflectionTestMethodInfo` in + `src/TestFramework/TestFramework/Internal/`) whose `Invoke` override calls the + generated `Func` instead of doing reflection. Because + `MethodInfoExtensions.InvokeAsSynchronousTask` calls `methodInfo.Invoke(...)` + polymorphically, **the execution engine itself needs no changes** — this is the + intentional seam behind the existing API contract: + + > *`ITestMethod.MethodInfo`: "Do not directly invoke the method using MethodInfo. Use + > `ITestMethod.Invoke` instead."* +3. Migrate `[DataRow]`, `[DynamicData]`, `[DataSource]` parameter binding to use the + compile-time parameter types instead of reflection-based `Convert.ChangeType`. + +The only `Activator.CreateInstance` site that does **not** fit into this story is +`TestSourceHost.CreateInstanceForType` — it instantiates arbitrary adapter host / +runner types, not user test classes, so the generator can't pre-resolve it. It is not +on the per-test hot path, so the impact is limited to host setup. + +The work is meaningful (a real refactor + thorough behaviour coverage so existing +extensions that consume `ITestMethod.MethodInfo` keep working) but **it is not "v2 of +the source generator from scratch"** — the architecture has been designed for it, the +seams exist, and the PoC generator output is ready to be wired in. + +### Recommended framing + +The current generator's value proposition, stated honestly, is: + +- ✅ Trim/Native AOT *correctness*: tests run at all after trimming/AOT publish. +- ✅ Cold-start *throughput*: skip the assembly + type scans. +- ⚠️ Per-test *throughput*: unchanged from reflection MSTest. Closing this requires + wiring the PoC delegate-emitting generator (`MSTest.AotReflection.SourceGeneration`, + issue #1837) into the adapter via the seams already in place (`ITestMethod.MethodInfo` + returning a delegate-backed `MethodInfo` subclass, populating `TypeConstructorsInvoker`, + etc.). + +Setting expectations this way avoids over-promising what the existing generator +delivers today and makes the case for wiring the PoC concrete when prioritising it. + +### What wiring the delegate generator unlocks beyond perf + +The case for wiring `MSTest.AotReflection.SourceGeneration` is **not just per-test +throughput**. Several non-perf design issues with the current source-gen story dissolve +once the registry holds delegates and pre-materialized attributes instead of +`MethodInfo` + name lookups. + +- **Source-gen mode and reflection mode become truly equivalent.** Today, attribute + reads on user test types fall back to `_fallback` because `TypeAttributes` / + `TypeMethodAttributes` are empty (Category A). That is a quiet behavior split: an + attribute the trimmer removed will simply not be returned under source-gen even though + the user expects parity. Baking the `Attribute[]` arrays into the registry makes the + paths converge. +- **The framework-wide `[UnconditionalSuppressMessage("IL2026"/"IL3050")]` becomes + truly justified.** The standard rationale — *"Native AOT support relies on MSTest + source-generated reflection metadata, not on this code path"* — is only partially + true today, because the registry doesn't supply enough data to avoid the fallbacks + in source-gen mode. Wiring the delegates makes the suppressed code paths really + unreachable for user test code under source-gen. +- **The `[DynamicDependency]` rooting becomes unnecessary.** A static delegate + `static (instance, args) => ((MyTests)instance).MyTest((int)args[0])` *is* the + rooting — the trimmer keeps every member statically reachable from the delegate body, + including inherited members. The whole abstract-base-type chain we currently walk to + emit `[DynamicDependency]` becomes obsolete in that mode. +- **Compile-time validation of attribute shapes becomes possible.** Reading + `[DataRow]` / `[DataSource]` / `[ExpectedException]` etc. at compile time to bake them + also lets the generator surface analyzer diagnostics for: + - `[DataRow(1, "two")]` against `void Test(int a, double b)` — today fails at runtime + via `Convert.ChangeType`; would become a compile error from the typed delegate cast. + - `[DataSource("MyMethod")]` where the source doesn't exist or has the wrong + signature — diagnostic instead of runtime failure. + - `[ExpectedException(typeof(NotAnException))]` — diagnostic. +- **`ref` / `out` / `in` parameter support.** Today these are silently skipped because + the registry's signature comparison uses `typeof(T)`, which round-trips as `T&` for + by-ref types. A generated delegate uses the call-site syntax directly and side-steps + the type comparison entirely. +- **IDE source navigation under source-gen mode.** The Roslyn generator knows each test + method's `SyntaxNode`, so `TypeMethodLocations` can be populated with file path + line + number constants. IDE "Go to test source" works in source-gen mode (today it returns + empty). +- **Cleaner extension surface.** Third-party `TestMethodAttribute` subclasses and + custom data sources have a typed registry to plug into instead of having to layer on + top of `MethodInfo.Invoke`. + +What it does *not* fix (be honest with prioritisation): + +- Inherited `[TestClass]` — still a `ForAttributeWithMetadataName` limitation; still + needs the opt-in marker attribute. +- Private / `file`-local nested test classes — generated code is `internal`, can't + reference them. +- Open-generic test classes / generic test methods — `typeof(T)` is still invalid at + module-init scope. +- Cross-assembly reflection (Category C) — when the target assembly didn't opt in, + reflection is still the only answer. +- Custom `TestMethodAttribute` subclasses via inheritance — same FAWMN limitation. + +## Sunset plan for the current generator + `MSTest.Engine` + +Once the delegate-emitting generator (`MSTest.AotReflection.SourceGeneration`) is wired +into `MSTest.TestAdapter` directly, the current architecture — open-source +`MSTest.SourceGeneration` package + closed-source `MSTest.Engine` runtime — becomes +redundant. The recommended sunset plan: + +1. **Do not gate the existing source-gen path behind a feature flag.** A conditional + "old vs new" code path doubles the maintenance surface (every refactor in + `SourceGeneratedReflectionOperations`, every fix to inheritance walking, every base- + type rooting tweak has to be validated against both paths) and the two paths will + drift. The recently added base-type `[DynamicDependency]` chain becomes obsolete + under the delegate approach (the delegate body roots inherited members + transitively); keeping both maintains rooting math that no one uses. +2. **Delete the source-gen path from `main` in a single PR.** The old code is preserved + in git history; a release tag (e.g. `mstest-aot-rewrite-base`) makes + re-introduction a `git restore` away if a regression surfaces. This is how + `dotnet/runtime` retires experimental APIs — git, not gated dead code, is the + archive. +3. **Sunset the published packages gracefully.** + - Ship one final `2.0.0-alpha.` of `MSTest.SourceGeneration` containing an + info-severity analyzer diagnostic (`MSTEST0NNN`) that reads + *"`MSTest.SourceGeneration` is being replaced by integrated AOT support in + `MSTest.TestAdapter` X.Y; see <link>"*. Then stop publishing. + - Stop publishing `MSTest.Engine` from its (closed-source) repo on the same + cadence. Users on the alpha packages pin the last version if they cannot move. +4. **Preserve the public API surface even after deleting the implementation.** Keep + `ReflectionMetadataHook.Register` and `SourceGeneratedReflectionDataProvider` as + public types. The *data* they hold changes (delegates instead of `MethodInfo` lookups), + but third-party adapters / extension authors may already depend on these types and + deserve a migration window. This is API stewardship, not feature-gating two parallel + implementations. + +The justification, condensed: + +- Both packages are still `1.0.0-alpha.*` / `2.0.0-alpha.*` — users on alpha already + opted into "things may change". Download counts confirm the blast radius is small. +- The new approach is strictly better, not "different tradeoffs": delegate emission + handles every case the current approach handles (and adds support for several it + silently skips today) while eliminating the rooting math entirely. There is no + partial-rollback story that makes engineering sense. +- Conditional gating in code is not a substitute for version control. Git tags preserve + the option; gated code only preserves the maintenance burden. diff --git a/samples/public/DemoMSTestSdk/ProjectWithNativeAOT/ProjectWithNativeAOT.csproj b/samples/public/DemoMSTestSdk/ProjectWithNativeAOT/ProjectWithNativeAOT.csproj index cdb413b816..ccdbef0b88 100644 --- a/samples/public/DemoMSTestSdk/ProjectWithNativeAOT/ProjectWithNativeAOT.csproj +++ b/samples/public/DemoMSTestSdk/ProjectWithNativeAOT/ProjectWithNativeAOT.csproj @@ -36,8 +36,8 @@ Below is the equivalent project configuration when not using MSTest.Sdk - + diff --git a/src/Adapter/MSTest.Engine/.editorconfig b/src/Adapter/MSTest.Engine/.editorconfig deleted file mode 100644 index 7d7bac49f4..0000000000 --- a/src/Adapter/MSTest.Engine/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*.{cs,vb}] -file_header_template = Copyright (c) Microsoft Corporation. All rights reserved.\nLicensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. diff --git a/src/Adapter/MSTest.Engine/Assertions/AssertFailedException.cs b/src/Adapter/MSTest.Engine/Assertions/AssertFailedException.cs deleted file mode 100644 index cad313aaad..0000000000 --- a/src/Adapter/MSTest.Engine/Assertions/AssertFailedException.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Runtime.Serialization; - -namespace Microsoft.Testing.Framework; - -/// -/// AssertFailedException class. Used to indicate failure for a test case. -/// -[Serializable] -public sealed class AssertFailedException : Exception -{ - /// - /// Creates AssertFailedException with a given message and metadata. - /// - /// Message to be reported to the user. - /// AssertFailedException. - internal static AssertFailedException Create(string message) => CreateInternal(message, expected: null, actual: null); - - /// - /// Creates AssertFailedException with a given message and metadata. - /// - /// Message to be reported to the user, it may, or may not include the values that were compared. If values are not included provide them to 'expected' and 'actual'. - /// The expected value, when that value is complex, and diffing it to actual makes it easier for user to see the difference. - /// The actual value, when that value is complex, and diffing it to expected makes it easier for user to see the difference. - /// AssertFailedException. - internal static AssertFailedException Create(string message, string expected, string actual) => CreateInternal(message, expected, actual); - - private static AssertFailedException CreateInternal(string message, string? expected, string? actual) - { -#pragma warning disable SYSLIB0051 // Type or member is obsolete - var ex = new AssertFailedException(message); -#pragma warning restore SYSLIB0051 // Type or member is obsolete - - if (expected != null) - { - ex.Data["assert.expected"] = expected; - } - - if (actual != null) - { - ex.Data["assert.actual"] = actual; - } - - return ex; - } - - /// - /// Initializes a new instance of the class. - /// -#if NET8_0_OR_GREATER - [Obsolete("Use Create instead", DiagnosticId = "SYSLIB0051")] -#endif - public AssertFailedException() - : base() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The exception message. -#if NET8_0_OR_GREATER - [Obsolete("Use Create instead", DiagnosticId = "SYSLIB0051")] -#endif - public AssertFailedException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - /// The inner exception. -#if NET8_0_OR_GREATER - [Obsolete("Use Create instead", DiagnosticId = "SYSLIB0051")] -#endif - public AssertFailedException(string message, Exception ex) - : base(message, ex) - { - } - -#if NET8_0_OR_GREATER - [Obsolete(DiagnosticId = "SYSLIB0051")] -#endif - private AssertFailedException(SerializationInfo serializationInfo, StreamingContext streamingContext) - { - } -} diff --git a/src/Adapter/MSTest.Engine/BannedSymbols.txt b/src/Adapter/MSTest.Engine/BannedSymbols.txt deleted file mode 100644 index f7256d3c54..0000000000 --- a/src/Adapter/MSTest.Engine/BannedSymbols.txt +++ /dev/null @@ -1,6 +0,0 @@ -M:System.Threading.Tasks.Task.Run(System.Action,System.Threading.CancellationToken); Use 'ITask' instead -M:System.Threading.Tasks.Task.Run(System.Func{System.Threading.Tasks.Task},System.Threading.CancellationToken); Use 'ITask' instead -M:System.String.IsNullOrEmpty(System.String); Use 'RoslynString.IsNullOrEmpty' instead -M:System.String.IsNullOrWhiteSpace(System.String); Use 'RoslynString.IsNullOrWhiteSpace' instead -M:System.Diagnostics.Debug.Assert(System.Boolean); Use 'RoslynDebug.Assert' instead -M:System.Diagnostics.Debug.Assert(System.Boolean,System.String); Use 'RoslynDebug.Assert' instead diff --git a/src/Adapter/MSTest.Engine/BuildInfo.cs.template b/src/Adapter/MSTest.Engine/BuildInfo.cs.template deleted file mode 100644 index a874326017..0000000000 --- a/src/Adapter/MSTest.Engine/BuildInfo.cs.template +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -/// -/// Repository version, created at build time. -/// -internal static class MSTestEngineRepositoryVersion -{ - public const string Version = "${Version}"; -} diff --git a/src/Adapter/MSTest.Engine/Configurations/ConfigurationExtensions.cs b/src/Adapter/MSTest.Engine/Configurations/ConfigurationExtensions.cs deleted file mode 100644 index d0111e1fd6..0000000000 --- a/src/Adapter/MSTest.Engine/Configurations/ConfigurationExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Configurations; - -namespace Microsoft.Testing.Framework.Configurations; - -public static class ConfigurationExtensions -{ - public static string GetTestResultDirectory(this IConfiguration configuration) - => GetConfigurationWrapper(configuration).WrappedConfiguration.GetTestResultDirectory(); - - private static ConfigurationWrapper GetConfigurationWrapper(IConfiguration configuration) - => configuration as ConfigurationWrapper - ?? throw new ArgumentException("Current configuration is not of expected type", nameof(configuration)); -} diff --git a/src/Adapter/MSTest.Engine/Configurations/ConfigurationWrapper.cs b/src/Adapter/MSTest.Engine/Configurations/ConfigurationWrapper.cs deleted file mode 100644 index 3255bb7175..0000000000 --- a/src/Adapter/MSTest.Engine/Configurations/ConfigurationWrapper.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using PlatformIConfiguration = Microsoft.Testing.Platform.Configurations.IConfiguration; - -namespace Microsoft.Testing.Framework.Configurations; - -/// -/// Wraps a platform IConfiguration instance. This is preferable as it allows us to avoid being dependent -/// on Platform if we need some specific API change. -/// -internal sealed class ConfigurationWrapper : IConfiguration -{ - public ConfigurationWrapper(PlatformIConfiguration configuration) - => WrappedConfiguration = configuration; - - internal PlatformIConfiguration WrappedConfiguration { get; } - - public string? this[string key] => WrappedConfiguration[key]; -} diff --git a/src/Adapter/MSTest.Engine/Configurations/IConfiguration.cs b/src/Adapter/MSTest.Engine/Configurations/IConfiguration.cs deleted file mode 100644 index 9eef90eb15..0000000000 --- a/src/Adapter/MSTest.Engine/Configurations/IConfiguration.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.Configurations; - -public interface IConfiguration -{ - string? this[string key] { get; } -} diff --git a/src/Adapter/MSTest.Engine/Configurations/TestFrameworkConfiguration.cs b/src/Adapter/MSTest.Engine/Configurations/TestFrameworkConfiguration.cs deleted file mode 100644 index ac307a5238..0000000000 --- a/src/Adapter/MSTest.Engine/Configurations/TestFrameworkConfiguration.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.Configurations; - -public sealed class TestFrameworkConfiguration(int maxParallelTests = int.MaxValue) -{ - public int MaxParallelTests { get; } = maxParallelTests; -} diff --git a/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs b/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs deleted file mode 100644 index 65911e9565..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Web; - -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.Requests; - -namespace Microsoft.Testing.Framework; - -internal sealed class BFSTestNodeVisitor -{ - private static readonly string PathSeparatorString = TreeNodeFilter.PathSeparator.ToString(); - - // Read-only; must never be mutated. Shared across all BFS traversals where ContainsPropertyFilters is false. - private static readonly PropertyBag EmptyPropertyBag = new(); - - private readonly IEnumerable _rootTestNodes; - private readonly ITestExecutionFilter _testExecutionFilter; - private readonly TestArgumentsManager _testArgumentsManager; - - public BFSTestNodeVisitor(IEnumerable rootTestNodes, ITestExecutionFilter testExecutionFilter, - TestArgumentsManager testArgumentsManager) - { - if (testExecutionFilter is not TreeNodeFilter and not TestNodeUidListFilter and not NopFilter) - { - throw new ArgumentOutOfRangeException(nameof(testExecutionFilter)); - } - - _rootTestNodes = rootTestNodes; - _testExecutionFilter = testExecutionFilter; - _testArgumentsManager = testArgumentsManager; - } - - internal KeyValuePair>[] DuplicatedNodes { get; private set; } = []; - - public async Task VisitAsync(Func onIncludedTestNodeAsync) - { - // Precompute a HashSet for O(1) UID lookups when filtering by UID list. - // Using string values directly avoids allocating a wrapper for each lookup. - HashSet? uidFilterSet = _testExecutionFilter is TestNodeUidListFilter listFilter - ? new HashSet(listFilter.TestNodeUids.Select(static uid => uid.Value), StringComparer.Ordinal) - : null; - - // This is case sensitive, and culture insensitive, to keep UIDs unique, and comparable between different system. - Dictionary> testNodesByUid = []; - - // Use string (instead of StringBuilder) in the queue to avoid allocating one StringBuilder per node. - // Each node's full path is an immutable string that can be shared as the base for child paths. - Queue<(TestNode CurrentNode, TestNodeUid? ParentNodeUid, string NodeFullPath)> queue = new(); - foreach (TestNode node in _rootTestNodes) - { - queue.Enqueue((node, null, string.Empty)); - } - - while (queue.Count > 0) - { - (TestNode currentNode, TestNodeUid? parentNodeUid, string nodeFullPath) = queue.Dequeue(); - - if (!testNodesByUid.TryGetValue(currentNode.StableUid, out List? testNodes)) - { - testNodes = []; - testNodesByUid.Add(currentNode.StableUid, testNodes); - } - - testNodes.Add(currentNode); - - // We want to encode the path fragment to avoid conflicts with the separator. We are using URL encoding because it is - // a well-known proven standard encoding that is reversible. - string encodedName = EncodeString(currentNode.OverriddenEdgeName ?? currentNode.DisplayName); - string currentNodeFullPath = nodeFullPath.Length == 0 || nodeFullPath[^1] != TreeNodeFilter.PathSeparator - ? string.Concat(nodeFullPath, PathSeparatorString, encodedName) - : string.Concat(nodeFullPath, encodedName); - - // When we are filtering as tree filter and the current node does not match the filter, we skip the node and its children. - if (_testExecutionFilter is TreeNodeFilter treeNodeFilter) - { - PropertyBag filterPropertyBag = treeNodeFilter.ContainsPropertyFilters - ? new PropertyBag(currentNode.Properties) - : EmptyPropertyBag; - if (!treeNodeFilter.MatchesFilter(currentNodeFullPath, filterPropertyBag)) - { - continue; - } - } - - // If the node is expandable, we expand it (replacing the original node) - if (TestArgumentsManager.IsExpandableTestNode(currentNode)) - { - currentNode = await _testArgumentsManager.ExpandTestNodeAsync(currentNode).ConfigureAwait(false); - } - - // If the node is not filtered out by the test execution filter, we call the callback with the node. - if (uidFilterSet is null - || uidFilterSet.Contains(currentNode.StableUid.Value)) - { - await onIncludedTestNodeAsync(currentNode, parentNodeUid).ConfigureAwait(false); - } - - foreach (TestNode childNode in currentNode.Tests) - { - queue.Enqueue((childNode, currentNode.StableUid, currentNodeFullPath)); - } - } - - DuplicatedNodes = [.. testNodesByUid.Where(x => x.Value.Count > 1)]; - } - - private static string EncodeString(string value) - => HttpUtility.UrlEncode(value); -} diff --git a/src/Adapter/MSTest.Engine/Engine/ITestArgumentsEntry.cs b/src/Adapter/MSTest.Engine/Engine/ITestArgumentsEntry.cs deleted file mode 100644 index 37a3a595c3..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/ITestArgumentsEntry.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface ITestArgumentsEntry -{ - string UidFragment { get; } - - string? DisplayNameFragment { get; } - - object? Arguments { get; } -} diff --git a/src/Adapter/MSTest.Engine/Engine/ITestArgumentsManager.cs b/src/Adapter/MSTest.Engine/Engine/ITestArgumentsManager.cs deleted file mode 100644 index b02742fc7a..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/ITestArgumentsManager.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface ITestArgumentsManager -{ - void RegisterTestArgumentsEntryProvider( - TestNodeUid testNodeStableUid, - Func> argumentsEntryProviderCallback); -} diff --git a/src/Adapter/MSTest.Engine/Engine/ITestExecutionContext.cs b/src/Adapter/MSTest.Engine/Engine/ITestExecutionContext.cs deleted file mode 100644 index 1f451eb449..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/ITestExecutionContext.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Configurations; - -namespace Microsoft.Testing.Framework; - -public interface ITestExecutionContext -{ - CancellationToken CancellationToken { get; } - - IConfiguration Configuration { get; } - - ITestInfo TestInfo { get; } - - void CancelTestExecution(); - - void CancelTestExecution(int millisecondsDelay); - - void CancelTestExecution(TimeSpan delay); - - void ReportException(Exception exception, CancellationToken? timeoutCancellationToken = null); - - Task AddTestAttachmentAsync(FileInfo file, string displayName, string? description = null); -} diff --git a/src/Adapter/MSTest.Engine/Engine/ITestInfo.cs b/src/Adapter/MSTest.Engine/Engine/ITestInfo.cs deleted file mode 100644 index 1349a163b8..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/ITestInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions.Messages; - -namespace Microsoft.Testing.Framework; - -public interface ITestInfo -{ - TestNodeUid StableUid { get; } - - string DisplayName { get; } - - IProperty[] Properties { get; } -} diff --git a/src/Adapter/MSTest.Engine/Engine/ITestSessionContext.cs b/src/Adapter/MSTest.Engine/Engine/ITestSessionContext.cs deleted file mode 100644 index 4a5968e93b..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/ITestSessionContext.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Configurations; - -namespace Microsoft.Testing.Framework; - -public interface ITestSessionContext -{ - CancellationToken CancellationToken { get; } - - IConfiguration Configuration { get; } - - Task AddTestAttachmentAsync(FileInfo file, string displayName, string? description = null); -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestArgumentsContext.cs b/src/Adapter/MSTest.Engine/Engine/TestArgumentsContext.cs deleted file mode 100644 index b716cd2149..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestArgumentsContext.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal sealed class TestArgumentsContext(object arguments, TestNode target) -{ - public object Arguments { get; } = arguments; - - public TestNode Target { get; } = target; -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestArgumentsEntry.cs b/src/Adapter/MSTest.Engine/Engine/TestArgumentsEntry.cs deleted file mode 100644 index 5fc0c05f8b..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestArgumentsEntry.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -/// Type of the input data. -public sealed class InternalUnsafeTestArgumentsEntry(TArguments arguments, string uidFragment, string? displayNameFragment = null) : ITestArgumentsEntry -{ - public TArguments Arguments { get; } = arguments; - - public string UidFragment { get; } = uidFragment; - - public string? DisplayNameFragment { get; } = displayNameFragment; - - object? ITestArgumentsEntry.Arguments => Arguments; -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestArgumentsManager.cs b/src/Adapter/MSTest.Engine/Engine/TestArgumentsManager.cs deleted file mode 100644 index b3ec62c771..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestArgumentsManager.cs +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestArgumentsManager : ITestArgumentsManager -{ - private readonly Dictionary> _testArgumentsEntryProviders = []; - private bool _isRegistrationFrozen; - - public void RegisterTestArgumentsEntryProvider( - TestNodeUid testNodeStableUid, - Func> argumentPropertiesProviderCallback) - { - if (_isRegistrationFrozen) - { - throw new InvalidOperationException("Cannot register TestArgumentsEntry provider after registration is frozen."); - } - - // Add will throw an exception if the key already exists, which is intended. - _testArgumentsEntryProviders.Add(testNodeStableUid, argumentPropertiesProviderCallback); - } - - internal void FreezeRegistration() => _isRegistrationFrozen = true; - - internal static bool IsExpandableTestNode(TestNode testNode) - => testNode is IExpandableTestNode - && !FrameworkEngineMetadataProperty.GetFromProperties(testNode.Properties).PreventArgumentsExpansion; - - internal async Task ExpandTestNodeAsync(TestNode currentNode) - { - RoslynDebug.Assert(IsExpandableTestNode(currentNode), "Test node is not expandable"); - - int argumentsRowIndex = -1; - bool isIndexArgumentPropertiesProvider = false; - if (!_testArgumentsEntryProviders.TryGetValue( - currentNode.StableUid, - out Func? argumentPropertiesProvider)) - { - isIndexArgumentPropertiesProvider = true; - argumentPropertiesProvider = argument => - { - string fragment = $"[{argumentsRowIndex}]"; - return new InternalUnsafeTestArgumentsEntry(argument.Arguments, fragment, fragment); - }; - } - - HashSet expandedTestNodeUids = []; - List expandedTestNodes = [.. currentNode.Tests]; - switch (currentNode) - { - case IParameterizedTestNode parameterizedTestNode: - foreach (object? arguments in parameterizedTestNode.GetArguments()) - { - ExpandNodeWithArguments(currentNode, arguments, ref argumentsRowIndex, expandedTestNodes, - expandedTestNodeUids, argumentPropertiesProvider, isIndexArgumentPropertiesProvider); - } - - break; - - case ITaskParameterizedTestNode parameterizedTestNode: - foreach (object? arguments in await parameterizedTestNode.GetArguments().ConfigureAwait(false)) - { - ExpandNodeWithArguments(currentNode, arguments, ref argumentsRowIndex, expandedTestNodes, - expandedTestNodeUids, argumentPropertiesProvider, isIndexArgumentPropertiesProvider); - } - - break; - -#if NET - case IAsyncParameterizedTestNode parameterizedTestNode: - await foreach (object? arguments in parameterizedTestNode.GetArguments().ConfigureAwait(false)) - { - ExpandNodeWithArguments(currentNode, arguments, ref argumentsRowIndex, expandedTestNodes, - expandedTestNodeUids, argumentPropertiesProvider, isIndexArgumentPropertiesProvider); - } - - break; -#endif - - default: - throw new InvalidOperationException($"Unexpected parameterized test node type: '{currentNode.GetType()}'"); - } - - // When the node is expandable, we need to create a new node that is not an action node, but a container - // node that contains the expanded nodes. This is this node that will be executed. - TestNode expandedNode = new() - { - StableUid = currentNode.StableUid, - DisplayName = currentNode.DisplayName, - OverriddenEdgeName = currentNode.OverriddenEdgeName, - Properties = currentNode.Properties, - Tests = [.. expandedTestNodes], - }; - - return expandedNode; - - // Local functions - static void ExpandNodeWithArguments(TestNode testNode, object arguments, ref int argumentsRowIndex, List expandedTestNodes, - HashSet expandedTestNodeUids, Func argumentPropertiesProvider, - bool isIndexArgumentPropertiesProvider) - { - // We need to increase the index before calling the argumentPropertiesProvider, because it is capturing it's value - argumentsRowIndex++; - bool shouldWrapInParenthesis = true; - if (arguments is not ITestArgumentsEntry testArgumentsEntry) - { - shouldWrapInParenthesis = !isIndexArgumentPropertiesProvider; - testArgumentsEntry = argumentPropertiesProvider(new(arguments, testNode)); - } - - string argumentFragmentUid = GetArgumentFragmentUid(testArgumentsEntry, shouldWrapInParenthesis); - string argumentFragmentDisplayName = GetArgumentFragmentDisplayName(testArgumentsEntry, shouldWrapInParenthesis); - TestNode expandedTestNode = ((IExpandableTestNode)testNode).GetExpandedTestNode(arguments, argumentFragmentUid, - argumentFragmentDisplayName); - if (!expandedTestNodeUids.Add(expandedTestNode.StableUid)) - { - throw new InvalidOperationException( - $"Expanded test node with UID '{expandedTestNode.StableUid}' is not unique for test node '{testNode.StableUid}'."); - } - - expandedTestNodes.Add(expandedTestNode); - } - - static string GetArgumentFragmentUid(ITestArgumentsEntry testArgumentsEntry, bool shouldWrapInParenthesis) - => CreateWrappedName(testArgumentsEntry.UidFragment, shouldWrapInParenthesis); - - static string GetArgumentFragmentDisplayName(ITestArgumentsEntry testArgumentsEntry, bool shouldWrapInParenthesis) - => CreateWrappedName(testArgumentsEntry.DisplayNameFragment ?? testArgumentsEntry.UidFragment, shouldWrapInParenthesis); - - static string CreateWrappedName(string name, bool shouldWrapInParenthesis) - => shouldWrapInParenthesis ? string.Concat("(", name, ")") : name; - } -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestContext.cs b/src/Adapter/MSTest.Engine/Engine/TestContext.cs deleted file mode 100644 index 46d22337cd..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestContext.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal readonly struct TestContext(CancellationToken cancellationToken) -{ - public CancellationToken CancellationToken { get; } = cancellationToken; -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestExecutionContext.cs b/src/Adapter/MSTest.Engine/Engine/TestExecutionContext.cs deleted file mode 100644 index c14e8add20..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestExecutionContext.cs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Extensions.TrxReport.Abstractions; -using Microsoft.Testing.Framework.Configurations; -using Microsoft.Testing.Framework.Helpers; -using Microsoft.Testing.Platform.Extensions.Messages; - -using PlatformTestNode = Microsoft.Testing.Platform.Extensions.Messages.TestNode; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestExecutionContext : ITestExecutionContext -{ - private readonly CancellationTokenSource _cancellationTokenSource; - private readonly PlatformTestNode _platformTestNode; - private readonly ITrxReportCapability? _trxReportCapability; - private readonly CancellationToken _originalCancellationToken; - - public TestExecutionContext(IConfiguration configuration, TestNode testNode, PlatformTestNode platformTestNode, - ITrxReportCapability? trxReportCapability, CancellationToken cancellationToken) - { - Configuration = configuration; - _platformTestNode = platformTestNode; - _trxReportCapability = trxReportCapability; - TestInfo = new TestInfo(testNode); - _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - _originalCancellationToken = cancellationToken; - } - - public CancellationToken CancellationToken => _cancellationTokenSource.Token; - - public IConfiguration Configuration { get; } - - public ITestInfo TestInfo { get; } - - public void CancelTestExecution() - => _cancellationTokenSource.Cancel(); - - public void CancelTestExecution(int millisecondsDelay) - => _cancellationTokenSource.CancelAfter(millisecondsDelay); - - public void CancelTestExecution(TimeSpan delay) - => _cancellationTokenSource.CancelAfter(delay); - - public void ReportException(Exception exception, CancellationToken? timeoutCancellationToken = null) - { - if (_trxReportCapability is not null && _trxReportCapability.IsSupported) - { - AddTrxExceptionInformation(_platformTestNode.Properties, exception); - } - - TestNodeStateProperty executionState = exception switch - { - // We want to consider user timeouts as failures if they didn't use our cancellation token - OperationCanceledException canceledException - when canceledException.CancellationToken == _originalCancellationToken || canceledException.CancellationToken == CancellationToken -#pragma warning disable CS0618, MTP0001 // Type or member is obsolete - => new CancelledTestNodeStateProperty(ExceptionFlattener.FlattenOrUnwrap(exception)), -#pragma warning restore CS0618, MTP0001 // Type or member is obsolete - OperationCanceledException canceledException when canceledException.CancellationToken == timeoutCancellationToken - => new TimeoutTestNodeStateProperty(ExceptionFlattener.FlattenOrUnwrap(exception)), - AssertFailedException => new FailedTestNodeStateProperty(ExceptionFlattener.FlattenOrUnwrap(exception), exception.Message), - - // TODO: Filter exceptions that are to be considered as failures and return ErrorReason for the others - _ => new ErrorTestNodeStateProperty(exception), - }; - - // TODO: We need to be able to modify the execution state of a test node - if (!_platformTestNode.Properties.Any()) - { - _platformTestNode.Properties.Add(executionState); - } - } - - public Task AddTestAttachmentAsync(FileInfo file, string displayName, string? description = null) - { - _platformTestNode.Properties.Add(new FileArtifactProperty(file, displayName, description)); - return Task.CompletedTask; - } - - private static void AddTrxExceptionInformation(PropertyBag propertyBag, Exception? exception) - { - Exception? flatException = exception != null - ? ExceptionFlattener.FlattenOrUnwrap(exception) - : null; - if (flatException is null) - { - return; - } - - propertyBag.Add(new TrxExceptionProperty(StringifyMessage(flatException), StringifyStackTrace(flatException))); - - static string StringifyMessage(Exception exception) - { - string message = exception.Message; - if (exception.Data["assert.expected"] is string expected) - { - message += $"{Environment.NewLine}Expected:{Environment.NewLine}{expected}"; - } - - if (exception.Data["assert.actual"] is string actual) - { - message += $"{Environment.NewLine}Actual:{Environment.NewLine}{actual}"; - } - - return message; - } - - static string StringifyStackTrace(Exception exception) - { - if (exception is not AggregateException aggregateException) - { - return exception.StackTrace ?? string.Empty; - } - - string separator = "---End of inner exception ---"; - StringBuilder builder = new(); - foreach (Exception ex in aggregateException.InnerExceptions) - { - builder.AppendLine(ex.StackTrace); - builder.AppendLine(separator); - } - - return builder.ToString(); - } - } -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestFixtureManager.cs b/src/Adapter/MSTest.Engine/Engine/TestFixtureManager.cs deleted file mode 100644 index 9e768fd9d9..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestFixtureManager.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Helpers; -using Microsoft.Testing.Platform.Helpers; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestFixtureManager -{ - private readonly Dictionary>> _fixtureInstancesByFixtureId = []; - private readonly Dictionary _fixtureIdsUsedByTestNode = []; - - // We could improve this by doing some optimistic lock but we expect a rather low contention on this. - // We use a dictionary as performance improvement because we know that when the registration is complete - // we will only read the collection (so no need for concurrency handling). - private readonly Dictionary _fixtureUses = []; - private bool _isUsageRegistrationFrozen; - - internal void RegisterFixtureUsage(TestNode testNode, string[] fixtureIds) - { - if (_isUsageRegistrationFrozen) - { - throw new InvalidOperationException("Cannot register fixture usage after registration is frozen"); - } - - if (fixtureIds.Length == 0) - { - return; - } - - _fixtureIdsUsedByTestNode.Add(testNode, [.. fixtureIds.Select(x => new FixtureId(x))]); - foreach (string fixtureId in fixtureIds) - { - if (!_fixtureUses.TryGetValue(fixtureId, out CountHolder? uses)) - { - uses = new(); - _fixtureUses.Add(fixtureId, uses); - } - - uses.Value++; - } - } - - internal void FreezeUsageRegistration() => _isUsageRegistrationFrozen = true; - - internal async Task SetupUsedFixturesAsync(TestNode testNode) - { - if (!_fixtureIdsUsedByTestNode.TryGetValue(testNode, out FixtureId[]? fixtureIds)) - { - return; - } - - foreach (FixtureId fixtureId in fixtureIds) - { - if (!_fixtureInstancesByFixtureId.TryGetValue(fixtureId, out Dictionary>? fixtureInstancesPerType)) - { - throw new InvalidOperationException($"Fixture with ID '{fixtureId}' is not registered"); - } - - foreach (AsyncLazy lazyFixture in fixtureInstancesPerType.Values) - { - if (!lazyFixture.IsValueCreated || !lazyFixture.Value.IsCompleted) - { - await lazyFixture.Value.ConfigureAwait(false); - } - } - } - } - - internal async Task CleanUnusedFixturesAsync(TestNode testNode) - { - if (!_fixtureIdsUsedByTestNode.TryGetValue(testNode, out FixtureId[]? fixtureIds)) - { - return; - } - - foreach (FixtureId fixtureId in fixtureIds) - { - CountHolder uses = _fixtureUses[fixtureId]; - int usesCount = uses.Value; - lock (uses) - { - uses.Value--; - usesCount = uses.Value; - } - - // It's important to use the captured value and to not check `uses.Value` again because - // another thread could have decremented the value in the meantime. We would then end up - // cleaning the fixture multiple times. - if (usesCount == 0) - { - await CleanupAndDisposeFixtureAsync(fixtureId).ConfigureAwait(false); - } - } - } - - private async Task CleanupAndDisposeFixtureAsync(FixtureId fixtureId) - { - if (!_fixtureInstancesByFixtureId.TryGetValue(fixtureId, out Dictionary>? fixtureInstancesPerType)) - { - throw new InvalidOperationException($"Fixture with ID '{fixtureId}' is not registered"); - } - - foreach (AsyncLazy lazyFixture in fixtureInstancesPerType.Values) - { - if (!lazyFixture.IsValueCreated || !lazyFixture.Value.IsCompleted) - { - throw new InvalidOperationException($"Fixture with ID '{fixtureId}' is not created"); - } - -#pragma warning disable VSTHRD103 // Call async methods when in an async method - object fixture = lazyFixture.Value.Result; -#pragma warning restore VSTHRD103 // Call async methods when in an async method - - await DisposeHelper.DisposeAsync(fixture).ConfigureAwait(false); - } - } - - /// - /// Integers are value types and we need a reference type to be able to lock on it. - /// - private sealed class CountHolder - { -#pragma warning disable SA1401 // Fields should be private - public int Value; -#pragma warning restore SA1401 // Fields should be private - } - - private sealed record FixtureId(string Value) - { - public static implicit operator FixtureId(string fixtureId) - => new(fixtureId); - } -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestFrameworkEngine.cs b/src/Adapter/MSTest.Engine/Engine/TestFrameworkEngine.cs deleted file mode 100644 index 8db3fc652a..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestFrameworkEngine.cs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Adapter; -using Microsoft.Testing.Framework.Configurations; -using Microsoft.Testing.Framework.Helpers; -using Microsoft.Testing.Platform; -using Microsoft.Testing.Platform.Capabilities.TestFramework; -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.Helpers; -using Microsoft.Testing.Platform.Messages; -using Microsoft.Testing.Platform.Requests; - -using PlatformIConfiguration = Microsoft.Testing.Platform.Configurations.IConfiguration; -using PlatformTestNode = Microsoft.Testing.Platform.Extensions.Messages.TestNode; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestFrameworkEngine : IDataProducer -{ - private readonly TestingFrameworkExtension _extension; - private readonly ITestFrameworkCapabilities _capabilities; - private readonly IClock _clock; - private readonly ITask _task; - private readonly TestFrameworkConfiguration _testFrameworkConfiguration; - private readonly ITestNodesBuilder[] _testNodesBuilders; - private readonly ConfigurationWrapper _configuration; - - public TestFrameworkEngine(TestFrameworkConfiguration testFrameworkConfiguration, ITestNodesBuilder[] testNodesBuilders, TestingFrameworkExtension extension, ITestFrameworkCapabilities capabilities, - IClock clock, ITask task, PlatformIConfiguration configuration) - { - _extension = extension; - _capabilities = capabilities; - _clock = clock; - _task = task; - _testFrameworkConfiguration = testFrameworkConfiguration; - _testNodesBuilders = testNodesBuilders; - _configuration = new(configuration); - } - - public Type[] DataTypesProduced { get; } = [typeof(TestNodeUpdateMessage)]; - - public string Uid => _extension.Uid; - - public string Version => _extension.Version; - - public string DisplayName => _extension.DisplayName; - - public string Description => _extension.Description; - - public async Task IsEnabledAsync() => await _extension.IsEnabledAsync().ConfigureAwait(false); - - public async Task ExecuteRequestAsync(TestExecutionRequest testExecutionRequest, IMessageBus messageBus, CancellationToken cancellationToken) - => testExecutionRequest switch - { - DiscoverTestExecutionRequest discoveryRequest => await ExecuteTestNodeDiscoveryAsync(discoveryRequest, messageBus, cancellationToken).ConfigureAwait(false), - RunTestExecutionRequest runRequest => await ExecuteTestNodeRunAsync(runRequest, messageBus, cancellationToken).ConfigureAwait(false), - _ => Result.Fail($"Unexpected request type: '{testExecutionRequest.GetType().FullName}'"), - }; - - private async Task ExecuteTestNodeRunAsync(RunTestExecutionRequest request, IMessageBus messageBus, - CancellationToken cancellationToken) - { - var fixtureManager = new TestFixtureManager(); - - try - { - BFSTestNodeVisitor testNodesVisitor = await BuildTestNodesVisitorAsync(request, messageBus, cancellationToken).ConfigureAwait(false); - ThreadPoolTestNodeRunner testNodeRunner = new(_testFrameworkConfiguration, _capabilities, _clock, _task, _configuration, request.Session.SessionUid, - data => PublishDataAsync(messageBus, data), fixtureManager, cancellationToken); - - await testNodesVisitor.VisitAsync((testNode, parentTestNodeUid) => - { - if (testNode is IActionableTestNode) - { - testNodeRunner.EnqueueTest(testNode, parentTestNodeUid); - } - - string[] fixtureIds = FrameworkEngineMetadataProperty.GetFromProperties(testNode.Properties).UsedFixtureIds - ?? []; - fixtureManager.RegisterFixtureUsage(testNode, fixtureIds); - - return Task.CompletedTask; - }).ConfigureAwait(false); - - Result? duplicateNodeResult = CreateDuplicateNodeErrorResult(testNodesVisitor); - if (duplicateNodeResult is not null) - { - return duplicateNodeResult; - } - - // Then, we want to freeze the registration of the fixtures, so that we can't add new fixtures. - fixtureManager.FreezeUsageRegistration(); - testNodeRunner.StartTests(); - - // Finally, we want to wait for all tests to complete. - return await testNodeRunner.WaitAllTestsAsync(cancellationToken).ConfigureAwait(false); - } - finally - { - await DisposeTestNodeBuildersAsync().ConfigureAwait(false); - } - } - - private async Task ExecuteTestNodeDiscoveryAsync(DiscoverTestExecutionRequest request, IMessageBus messageBus, - CancellationToken cancellationToken) - { - try - { - BFSTestNodeVisitor testNodesVisitor = await BuildTestNodesVisitorAsync(request, messageBus, cancellationToken).ConfigureAwait(false); - - await testNodesVisitor.VisitAsync(async (testNode, parentTestNodeUid) => - { - PlatformTestNode progressNode = testNode.ToPlatformTestNode(); - if (testNode is IActionableTestNode) - { - progressNode.Properties.Add(DiscoveredTestNodeStateProperty.CachedInstance); - } - - await messageBus.PublishAsync(this, new TestNodeUpdateMessage(request.Session.SessionUid, progressNode, - parentTestNodeUid?.ToPlatformTestNodeUid())).ConfigureAwait(false); - }).ConfigureAwait(false); - - Result? duplicateNodeResult = CreateDuplicateNodeErrorResult(testNodesVisitor); - if (duplicateNodeResult is not null) - { - return duplicateNodeResult; - } - } - finally - { - await DisposeTestNodeBuildersAsync().ConfigureAwait(false); - } - - return Result.Ok(); - } - - private async Task BuildTestNodesVisitorAsync(TestExecutionRequest executionRequest, IMessageBus messageBus, CancellationToken cancellationToken) - { - List allRootTestNodes = []; - var argumentsManager = new TestArgumentsManager(); - var testSessionContext = new TestSessionContext(_configuration, executionRequest.Session.SessionUid, data => PublishDataAsync(messageBus, data), cancellationToken); - - foreach (ITestNodesBuilder testNodeBuilder in _testNodesBuilders) - { - TestNode[] testNodes = await testNodeBuilder.BuildAsync(testSessionContext).ConfigureAwait(false); - allRootTestNodes.AddRange(testNodes); - } - - // We have built all test nodes, now we need to process them. Before that, we want to make sure to freeze managers - // to ensure that no new registrations are allowed. - argumentsManager.FreezeRegistration(); - - return new BFSTestNodeVisitor(allRootTestNodes, executionRequest.Filter, argumentsManager); - } - - private static Result? CreateDuplicateNodeErrorResult(BFSTestNodeVisitor testNodesVisitor) - { - if (testNodesVisitor.DuplicatedNodes.Length == 0) - { - return null; - } - - StringBuilder errorMessageBuilder = new(); - errorMessageBuilder.AppendLine("Found multiple test nodes with the same UID:"); - foreach (KeyValuePair> duplicate in testNodesVisitor.DuplicatedNodes) - { - errorMessageBuilder.Append(CultureInfo.InvariantCulture, $"- For {duplicate.Key}: tests "); - errorMessageBuilder.AppendLine(string.Join(", ", duplicate.Value.Select(x => x.DisplayName))); - } - - return Result.Fail(errorMessageBuilder.ToString()); - } - - private async Task DisposeTestNodeBuildersAsync() - { - foreach (ITestNodesBuilder testNodeBuilder in _testNodesBuilders) - { - await DisposeHelper.DisposeAsync(testNodeBuilder).ConfigureAwait(false); - } - } - - private Task PublishDataAsync(IMessageBus messageBus, IData data) - { - RoslynDebug.Assert(DataTypesProduced.Contains(data.GetType()), "Published data type hasn't been declared"); - - return messageBus.PublishAsync(this, data); - } -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestInfo.cs b/src/Adapter/MSTest.Engine/Engine/TestInfo.cs deleted file mode 100644 index fd30f6ae21..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestInfo.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions.Messages; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestInfo(TestNode testNode) : ITestInfo -{ - public TestNodeUid StableUid => TestNode.StableUid; - - public string DisplayName => TestNode.DisplayName; - - public IProperty[] Properties => TestNode.Properties; - - public TestNode TestNode { get; } = testNode; -} diff --git a/src/Adapter/MSTest.Engine/Engine/TestSessionContext.cs b/src/Adapter/MSTest.Engine/Engine/TestSessionContext.cs deleted file mode 100644 index 6aca48c849..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/TestSessionContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Configurations; -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.TestHost; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestSessionContext : ITestSessionContext -{ - private readonly SessionUid _sessionUid; - private readonly Func _publishDataAsync; - - public TestSessionContext( - IConfiguration configuration, - SessionUid sessionUid, - Func publishDataAsync, - CancellationToken cancellationToken) - { - Configuration = configuration; - - _sessionUid = sessionUid; - _publishDataAsync = publishDataAsync; - CancellationToken = cancellationToken; - } - - public CancellationToken CancellationToken { get; } - - public IConfiguration Configuration { get; } - - public async Task AddTestAttachmentAsync(FileInfo file, string displayName, string? description = null) - => await _publishDataAsync(new SessionFileArtifact(_sessionUid, file, displayName, description)).ConfigureAwait(false); -} diff --git a/src/Adapter/MSTest.Engine/Engine/ThreadPoolTestNodeRunner.cs b/src/Adapter/MSTest.Engine/Engine/ThreadPoolTestNodeRunner.cs deleted file mode 100644 index 3070130114..0000000000 --- a/src/Adapter/MSTest.Engine/Engine/ThreadPoolTestNodeRunner.cs +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Extensions.TrxReport.Abstractions; -using Microsoft.Testing.Framework.Configurations; -using Microsoft.Testing.Framework.Helpers; -using Microsoft.Testing.Platform.Capabilities.TestFramework; -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.Helpers; -using Microsoft.Testing.Platform.TestHost; - -using PlatformTestNode = Microsoft.Testing.Platform.Extensions.Messages.TestNode; - -namespace Microsoft.Testing.Framework; - -internal sealed class ThreadPoolTestNodeRunner : IDisposable -{ - private readonly SemaphoreSlim? _maxParallelTests; - private readonly ConcurrentBag> _runningTests = []; - private readonly ConcurrentDictionary _runningTestNodeUids = new(); - private readonly CountdownEvent _ensureTaskQueuedCountdownEvent = new(1); - private readonly Func _publishDataAsync; - private readonly TestFixtureManager _testFixtureManager; - private readonly CancellationToken _cancellationToken; - private readonly IClock _clock; - private readonly IConfiguration _configuration; - private readonly SessionUid _sessionUid; - private readonly ITask _task; - private readonly ITrxReportCapability? _trxReportCapability; - private readonly TaskCompletionSource _waitForStart = new(); - private bool _isDisposed; - - public ThreadPoolTestNodeRunner(TestFrameworkConfiguration testFrameworkConfiguration, ITestFrameworkCapabilities capabilities, IClock clock, ITask task, IConfiguration configuration, - SessionUid sessionUid, Func publishDataAsync, TestFixtureManager testFixtureManager, - CancellationToken cancellationToken) - { - _clock = clock; - _configuration = configuration; - _sessionUid = sessionUid; - _publishDataAsync = publishDataAsync; - _testFixtureManager = testFixtureManager; - _cancellationToken = cancellationToken; - _task = task; - _trxReportCapability = capabilities.GetCapability(); - if (testFrameworkConfiguration.MaxParallelTests != int.MaxValue) - { - _maxParallelTests = new SemaphoreSlim(testFrameworkConfiguration.MaxParallelTests); - } - - cancellationToken.Register(_waitForStart.SetCanceled); - } - - public void EnqueueTest(TestNode frameworkTestNode, TestNodeUid? parentTestNodeUid) - { - _ensureTaskQueuedCountdownEvent.AddCount(); - try - { - _runningTests.Add( - _task.Run( - async () => - { - try - { - // We don't have a timeout here because we can have really slow fixture and it's on user - // the decision on how much to wait for it. - await _waitForStart.Task.ConfigureAwait(false); - - // Handle the global parallelism. - if (_maxParallelTests is not null) - { - await _maxParallelTests.WaitAsync().ConfigureAwait(false); - } - - try - { - _runningTestNodeUids.AddOrUpdate(frameworkTestNode.StableUid, 1, (_, count) => count + 1); - - PlatformTestNode progressNode = frameworkTestNode.ToPlatformTestNode(); - progressNode.Properties.Add(InProgressTestNodeStateProperty.CachedInstance); - await _publishDataAsync(new TestNodeUpdateMessage(_sessionUid, progressNode, parentTestNodeUid?.ToPlatformTestNodeUid())).ConfigureAwait(false); - - Result result = await CreateTestRunTaskAsync(frameworkTestNode, parentTestNodeUid).ConfigureAwait(false); - - _runningTestNodeUids.TryRemove(frameworkTestNode.StableUid, out int count); - - return count > 1 - ? throw new InvalidOperationException($"Test node '{frameworkTestNode.StableUid}' was run {count} times") - : result; - } - finally - { - _maxParallelTests?.Release(); - } - } - catch (Exception ex) - { - Environment.FailFast($"Unhandled exception inside '{nameof(CreateTestRunTaskAsync)}'", ex); - throw; - } - }, - _cancellationToken)); - } - catch (OperationCanceledException ex) when (ex.CancellationToken == _cancellationToken) - { - // We are being cancelled, so we don't need to wait anymore - } - finally - { - // We will signal for the second counting inside CreateTestRunTaskAsync() after the test run. - _ensureTaskQueuedCountdownEvent.Signal(); - } - } - - public void StartTests() - => _waitForStart.SetResult(0); - - private async Task CreateTestRunTaskAsync(TestNode testNode, TestNodeUid? parentTestNodeUid) - { - try - { - await _testFixtureManager.SetupUsedFixturesAsync(testNode).ConfigureAwait(false); - } - catch (Exception ex) - { - StringBuilder errorBuilder = new(); - errorBuilder.AppendLine(CultureInfo.InvariantCulture, $"Error while initializing fixtures for test '{testNode.DisplayName}' (UID = {testNode.StableUid.Value})"); - errorBuilder.AppendLine(); - errorBuilder.AppendLine(ex.ToString()); - return Result.Fail(errorBuilder.ToString()); - } - - Result result = await InvokeTestNodeAndPublishResultAsync(testNode, parentTestNodeUid, - async (testNode, testExecutionContext) => - { - switch (testNode) - { - case IAsyncActionTestNode actionTestNode: - await actionTestNode.InvokeAsync(testExecutionContext).ConfigureAwait(false); - break; - - case IActionTestNode actionTestNode: - actionTestNode.Invoke(testExecutionContext); - break; - - case IParameterizedAsyncActionTestNode actionTestNode: - await actionTestNode.InvokeAsync( - testExecutionContext, - action => InvokeTestNodeAndPublishResultAsync(testNode, parentTestNodeUid, (_, _) => action(), skipPublishResult: false)).ConfigureAwait(false); - break; - - default: - break; - } - }, - // Because parameterized tests report multiple results (one per parameter set), we don't want to publish the result - // of the overall test node execution, but only the results of the individual parameterized tests. - skipPublishResult: testNode is IParameterizedAsyncActionTestNode).ConfigureAwait(false); - - // Try to cleanup the fixture is not more used. - try - { - await _testFixtureManager.CleanUnusedFixturesAsync(testNode).ConfigureAwait(false); - return result; - } - catch (Exception ex) - { - StringBuilder errorBuilder = new(); - errorBuilder.AppendLine(CultureInfo.InvariantCulture, $"Error while cleaning fixtures for test '{testNode.StableUid}'"); - errorBuilder.AppendLine(); - errorBuilder.AppendLine(ex.ToString()); - return result.WithError(errorBuilder.ToString()); - } - } - - public async Task WaitAllTestsAsync(CancellationToken cancellationToken) - { - try - { - _ensureTaskQueuedCountdownEvent.Signal(); - await _ensureTaskQueuedCountdownEvent.WaitAsync(cancellationToken).ConfigureAwait(false); - Result[] results = await Task.WhenAll(_runningTests).ConfigureAwait(false); - return Result.Combine(results); - } - catch (OperationCanceledException ex) when (ex.CancellationToken == cancellationToken) - { - // If the cancellation token is triggered, we don't want to report the cancellation as a failure - return Result.Ok("Cancelled by user"); - } - } - - public void Dispose() - { - if (!_isDisposed) - { - _ensureTaskQueuedCountdownEvent.Dispose(); - _isDisposed = true; - } - } - - private async Task InvokeTestNodeAndPublishResultAsync(TestNode testNode, TestNodeUid? parentTestNodeUid, - Func testNodeInvokeAction, bool skipPublishResult) - { - TimeSheet timesheet = new(_clock); - timesheet.RecordStart(); - - PlatformTestNode platformTestNode = testNode.ToPlatformTestNode(); - - if (_trxReportCapability is not null && _trxReportCapability.IsSupported) - { - platformTestNode.Properties.Add(new TrxFullyQualifiedTypeNameProperty(platformTestNode.Uid.Value[..platformTestNode.Uid.Value.LastIndexOf('.')])); - } - - TestExecutionContext testExecutionContext = new(_configuration, testNode, platformTestNode, _trxReportCapability, _cancellationToken); - try - { - // If we're already enqueued we cancel the test before the start - // The test could not use the cancellation and we should wait the end of the test self to cancel. - _cancellationToken.ThrowIfCancellationRequested(); - await testNodeInvokeAction(testNode, testExecutionContext).ConfigureAwait(false); - - if (!platformTestNode.Properties.Any()) - { - platformTestNode.Properties.Add(PassedTestNodeStateProperty.CachedInstance); - } - } - catch (MissingMethodException ex) - { - // In dotnet watch mode we can remove tests. - if (Environment.GetEnvironmentVariable("DOTNET_WATCH") == "1") - { - return Result.Ok().WithWarning("Under 'DOTNET_WATCH' cannot find some member." + Environment.NewLine + ex.StackTrace); - } - - throw; - } - catch (Exception ex) - { - testExecutionContext.ReportException(ex); - } - finally - { - timesheet.RecordStop(); - platformTestNode.Properties.Add(new TimingProperty(new TimingInfo(timesheet.StartTime, timesheet.StopTime, timesheet.Duration))); - } - - if (!skipPublishResult) - { - await _publishDataAsync(new TestNodeUpdateMessage(_sessionUid, platformTestNode, parentTestNodeUid?.ToPlatformTestNodeUid())).ConfigureAwait(false); - } - - return Result.Ok(); - } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/AsyncLazy.cs b/src/Adapter/MSTest.Engine/Helpers/AsyncLazy.cs deleted file mode 100644 index 2d41e7d5d3..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/AsyncLazy.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.Helpers; - -internal sealed class AsyncLazy : Lazy> -{ - public AsyncLazy(Func valueFactory, LazyThreadSafetyMode mode) - : base(() => Task.Run(valueFactory), mode) - { - } - - public AsyncLazy(Func> taskFactory, LazyThreadSafetyMode mode) - : base(() => Task.Factory.StartNew(() => taskFactory(), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default).Unwrap(), mode) - { - } - - public TaskAwaiter GetAwaiter() => Value.GetAwaiter(); -} diff --git a/src/Adapter/MSTest.Engine/Helpers/DynamicDataNameProvider.cs b/src/Adapter/MSTest.Engine/Helpers/DynamicDataNameProvider.cs deleted file mode 100644 index 705e34b296..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/DynamicDataNameProvider.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// Helper to provide a uids to user data, using the same logic that DynamicDataAttribute.GetDisplayName is using. This class is called by source generator. -/// -public static class DynamicDataNameProvider -{ - /// - /// Returns a stable fragment of uid by converting parameter types to strings, and suffixing them with index in brackets (e.g. [1]). - /// - /// Names of the parameters of the receiving method. - /// The data for each parameter. - /// Position in the collection. - /// Stable uid. - /// Arrays in both parameters need to have the same number of items. - public static string GetUidFragment(string[] parameterNames, object?[] data, int index) - { - if (parameterNames.Length != data.Length) - { - throw new ArgumentException($"Parameter count mismatch. The provided data ({string.Join(", ", data.Select(d => d?.ToString() ?? "null"))}) have {data.Length} items, but there are {parameterNames.Length} parameters."); - } - - StringBuilder stringBuilder = new StringBuilder().Append('('); - - for (int i = 0; i < data.Length; i++) - { - if (i > 0) - { - stringBuilder.Append(", "); - } - - stringBuilder.Append(parameterNames[i]).Append(": ").Append(data[i]?.ToString() ?? "null"); - } - - stringBuilder.Append(CultureInfo.InvariantCulture, $")[{index}]"); - return stringBuilder.ToString(); - } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/ErrorReason.cs b/src/Adapter/MSTest.Engine/Helpers/ErrorReason.cs deleted file mode 100644 index 8a4a7cb30d..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/ErrorReason.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal sealed class ErrorReason(string message) : IErrorReason -{ - internal ErrorReason(string message, Exception exception) - : this(message) - => Exception = exception; - - internal ErrorReason(Exception exception) - : this(exception.Message) - => Exception = exception; - - public Exception? Exception { get; } - - public string Message { get; } = message; -} diff --git a/src/Adapter/MSTest.Engine/Helpers/ExceptionFlattener.cs b/src/Adapter/MSTest.Engine/Helpers/ExceptionFlattener.cs deleted file mode 100644 index df26732702..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/ExceptionFlattener.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.Helpers; - -internal static class ExceptionFlattener -{ - /// - /// Returns the same exception for any exception that is not AggregateException. - /// For AggregateException it unwraps it when it holds just a single concrete exception, - /// otherwise it flattens the AggregateException and returns that. - /// - public static Exception FlattenOrUnwrap(Exception exception) - { - if (exception is AggregateException aggregateException) - { - if (aggregateException.InnerExceptions.Count == 1) - { - Exception innerException = aggregateException.InnerExceptions[0]; - if (innerException is not AggregateException) - { - return innerException; - } - } - - return aggregateException.Flatten(); - } - - return exception; - } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/IErrorReason.cs b/src/Adapter/MSTest.Engine/Helpers/IErrorReason.cs deleted file mode 100644 index 59d47b13ed..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/IErrorReason.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IErrorReason : IReason -{ - Exception? Exception { get; } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/IReason.cs b/src/Adapter/MSTest.Engine/Helpers/IReason.cs deleted file mode 100644 index 5f7f5a296c..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/IReason.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IReason -{ - string Message { get; } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/ISuccessReason.cs b/src/Adapter/MSTest.Engine/Helpers/ISuccessReason.cs deleted file mode 100644 index 5952b324bf..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/ISuccessReason.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface ISuccessReason : IReason; diff --git a/src/Adapter/MSTest.Engine/Helpers/IWarningReason.cs b/src/Adapter/MSTest.Engine/Helpers/IWarningReason.cs deleted file mode 100644 index e5a50b9aba..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/IWarningReason.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IWarningReason : IReason; diff --git a/src/Adapter/MSTest.Engine/Helpers/Result.cs b/src/Adapter/MSTest.Engine/Helpers/Result.cs deleted file mode 100644 index 881c9fa4d1..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/Result.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal sealed class Result -{ - private readonly List _reasons = []; - - private Result() - { - } - - public bool IsSuccess => !IsFailed; - - public bool IsFailed => _reasons.OfType().Any(); - - public IReadOnlyList Reasons => _reasons; - - public Result WithSuccess(ISuccessReason success) - { - _reasons.Add(success); - return this; - } - - public Result WithWarning(IWarningReason warning) - { - _reasons.Add(warning); - return this; - } - - public Result WithError(IErrorReason error) - { - _reasons.Add(error); - return this; - } - - public static Result Ok() => new(); - - public static Result Ok(string reason) => new Result().WithSuccess(new SuccessReason(reason)); - - public static Result Fail(Exception exception) => new Result().WithError(new ErrorReason(exception)); - - public static Result Fail(string reason) => new Result().WithError(new ErrorReason(reason)); - - public static Result Fail(string reason, Exception exception) => new Result().WithError(new ErrorReason(reason, exception)); - - public static Result Combine(IEnumerable results) - { - Result result = Ok(); - foreach (Result r in results) - { - result._reasons.AddRange(r.Reasons); - } - - return result; - } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/ResultExtensions.cs b/src/Adapter/MSTest.Engine/Helpers/ResultExtensions.cs deleted file mode 100644 index 19aa6962b7..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/ResultExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal static class ResultExtensions -{ - public static Result WithWarning(this Result result, string message) - => result.WithWarning(new WarningReason(message)); - - public static Result WithError(this Result result, string message) - => result.WithError(new ErrorReason(message)); - - public static Result WithError(this Result result, Exception exception) - => result.WithError(new ErrorReason(exception)); -} diff --git a/src/Adapter/MSTest.Engine/Helpers/SuccessReason.cs b/src/Adapter/MSTest.Engine/Helpers/SuccessReason.cs deleted file mode 100644 index ffea736c93..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/SuccessReason.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal sealed class SuccessReason(string message) : ISuccessReason -{ - public string Message { get; } = message; -} diff --git a/src/Adapter/MSTest.Engine/Helpers/TestApplicationBuilderExtensions.cs b/src/Adapter/MSTest.Engine/Helpers/TestApplicationBuilderExtensions.cs deleted file mode 100644 index 47bd3eaa28..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/TestApplicationBuilderExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Adapter; -using Microsoft.Testing.Framework.Configurations; -using Microsoft.Testing.Platform.Builder; -using Microsoft.Testing.Platform.Helpers; -using Microsoft.Testing.Platform.Services; - -namespace Microsoft.Testing.Framework; - -public static class TestApplicationBuilderExtensions -{ - public static void AddTestFramework(this ITestApplicationBuilder testApplicationBuilder, params ITestNodesBuilder[] testNodesBuilder) - => testApplicationBuilder.AddTestFramework(new(), testNodesBuilder); - - public static void AddTestFramework( - this ITestApplicationBuilder testApplicationBuilder, - TestFrameworkConfiguration? testFrameworkConfiguration = null, - params ITestNodesBuilder[] testNodesBuilder) - { - if (testApplicationBuilder is null) - { - throw new ArgumentNullException(nameof(testApplicationBuilder)); - } - - if (testNodesBuilder is null) - { - throw new ArgumentNullException(nameof(testNodesBuilder)); - } - - if (testNodesBuilder.Length == 0) - { - throw new ArgumentException("At least one test node builder must be provided.", nameof(testNodesBuilder)); - } - - testFrameworkConfiguration ??= new TestFrameworkConfiguration(); - var extension = new TestingFrameworkExtension(); - testApplicationBuilder.AddTreeNodeFilterService(extension); - testApplicationBuilder.RegisterTestFramework( - serviceProvider => new TestFrameworkCapabilities(testNodesBuilder, new MSTestEngineBannerCapability(serviceProvider.GetRequiredService())), - (capabilities, serviceProvider) => - new TestFramework(testFrameworkConfiguration, testNodesBuilder, extension, serviceProvider.GetSystemClock(), - serviceProvider.GetTask(), serviceProvider.GetConfiguration(), capabilities)); - } -} diff --git a/src/Adapter/MSTest.Engine/Helpers/TestFrameworkConstants.cs b/src/Adapter/MSTest.Engine/Helpers/TestFrameworkConstants.cs deleted file mode 100644 index 1ff235fcb4..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/TestFrameworkConstants.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal static class TestFrameworkConstants -{ - public const string DefaultSemVer = MSTestEngineRepositoryVersion.Version; -} diff --git a/src/Adapter/MSTest.Engine/Helpers/TestNodeExpansionHelper.cs b/src/Adapter/MSTest.Engine/Helpers/TestNodeExpansionHelper.cs deleted file mode 100644 index 8551be94a4..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/TestNodeExpansionHelper.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.Helpers; - -internal static class TestNodeExpansionHelper -{ - public static TestNodeUid GenerateStableUid(TestNodeUid testNodeUid, string dataId) - => new($"{testNodeUid.Value} {dataId}"); - - public static string GenerateDisplayName(string displayName, string dataId) - => $"{displayName} {dataId}"; -} diff --git a/src/Adapter/MSTest.Engine/Helpers/TestNodeExtensions.cs b/src/Adapter/MSTest.Engine/Helpers/TestNodeExtensions.cs deleted file mode 100644 index a62946b038..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/TestNodeExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions.Messages; - -using PlatformTestNode = Microsoft.Testing.Platform.Extensions.Messages.TestNode; -using PlatformTestNodeUid = Microsoft.Testing.Platform.Extensions.Messages.TestNodeUid; - -namespace Microsoft.Testing.Framework.Helpers; - -internal static class TestNodeExtensions -{ - public static PlatformTestNode ToPlatformTestNode(this TestNode testNode) - { - if (testNode.DisplayName is null) - { - throw new ArgumentException("TestNode must have a DisplayNameFragment", nameof(testNode)); - } - - if (testNode.StableUid is null) - { - throw new ArgumentException("TestNode must have a StableUid", nameof(testNode)); - } - - var platformTestNode = new PlatformTestNode - { - Uid = testNode.StableUid.ToPlatformTestNodeUid(), - DisplayName = testNode.DisplayName, - }; - - foreach (IProperty property in testNode.Properties) - { - platformTestNode.Properties.Add(property); - } - - return platformTestNode; - } - - public static PlatformTestNodeUid ToPlatformTestNodeUid(this TestNodeUid testNodeUid) - => new(testNodeUid.Value); -} diff --git a/src/Adapter/MSTest.Engine/Helpers/WarningReason.cs b/src/Adapter/MSTest.Engine/Helpers/WarningReason.cs deleted file mode 100644 index 9d6013fc64..0000000000 --- a/src/Adapter/MSTest.Engine/Helpers/WarningReason.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal sealed class WarningReason(string message) : IWarningReason -{ - public string Message { get; } = message; -} diff --git a/src/Adapter/MSTest.Engine/MSTest.Engine.csproj b/src/Adapter/MSTest.Engine/MSTest.Engine.csproj deleted file mode 100644 index 5dc49f9a2b..0000000000 --- a/src/Adapter/MSTest.Engine/MSTest.Engine.csproj +++ /dev/null @@ -1,100 +0,0 @@ - - - - netstandard2.0;$(SupportedNetFrameworks) - Microsoft.Testing.Framework - - - - License.txt - $(MSTestEngineVersionPrefix) - $(MSTestEnginePreReleaseVersionLabel) - true - - $(NoWarn);CS1591 - true - - true - - - - true - false - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - buildMultiTargeting - - - - build/$(TargetFramework) - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Adapter/MSTest.Engine/PACKAGE.md b/src/Adapter/MSTest.Engine/PACKAGE.md deleted file mode 100644 index 135a747edc..0000000000 --- a/src/Adapter/MSTest.Engine/PACKAGE.md +++ /dev/null @@ -1,11 +0,0 @@ -# Microsoft.Testing - -Microsoft Testing is a set of platform, framework and protocol intended to make it possible to run any test on any target or device. - -Documentation can be found at . - -## About - -This package provides the Microsoft specific Test Framework. - -Test Anywhere Test Framework is the first test framework to offer support for Native AOT and trimming scenarios. It is designed to avoid reflection and leverage modern tooling such as source generators. diff --git a/src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Shipped.txt b/src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Shipped.txt deleted file mode 100644 index 1a72d04b9d..0000000000 --- a/src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Shipped.txt +++ /dev/null @@ -1,98 +0,0 @@ -#nullable enable -Microsoft.Testing.Framework.AssertFailedException -Microsoft.Testing.Framework.AssertFailedException.AssertFailedException() -> void -Microsoft.Testing.Framework.AssertFailedException.AssertFailedException(string! message, System.Exception! ex) -> void -Microsoft.Testing.Framework.AssertFailedException.AssertFailedException(string! message) -> void -Microsoft.Testing.Framework.Configurations.ConfigurationExtensions -Microsoft.Testing.Framework.Configurations.IConfiguration -Microsoft.Testing.Framework.Configurations.IConfiguration.this[string! key].get -> string? -Microsoft.Testing.Framework.Configurations.TestFrameworkConfiguration -Microsoft.Testing.Framework.Configurations.TestFrameworkConfiguration.MaxParallelTests.get -> int -Microsoft.Testing.Framework.Configurations.TestFrameworkConfiguration.TestFrameworkConfiguration(int maxParallelTests = 2147483647) -> void -Microsoft.Testing.Framework.DynamicDataNameProvider -Microsoft.Testing.Framework.InternalUnsafeActionParameterizedTestNode -Microsoft.Testing.Framework.InternalUnsafeActionParameterizedTestNode.Body.get -> System.Action! -Microsoft.Testing.Framework.InternalUnsafeActionParameterizedTestNode.Body.init -> void -Microsoft.Testing.Framework.InternalUnsafeActionParameterizedTestNode.GetArguments.get -> System.Func!>! -Microsoft.Testing.Framework.InternalUnsafeActionParameterizedTestNode.GetArguments.init -> void -Microsoft.Testing.Framework.InternalUnsafeActionParameterizedTestNode.InternalUnsafeActionParameterizedTestNode() -> void -Microsoft.Testing.Framework.InternalUnsafeActionTaskParameterizedTestNode -Microsoft.Testing.Framework.InternalUnsafeActionTaskParameterizedTestNode.Body.get -> System.Action! -Microsoft.Testing.Framework.InternalUnsafeActionTaskParameterizedTestNode.Body.init -> void -Microsoft.Testing.Framework.InternalUnsafeActionTaskParameterizedTestNode.GetArguments.get -> System.Func!>!>! -Microsoft.Testing.Framework.InternalUnsafeActionTaskParameterizedTestNode.GetArguments.init -> void -Microsoft.Testing.Framework.InternalUnsafeActionTaskParameterizedTestNode.InternalUnsafeActionTaskParameterizedTestNode() -> void -Microsoft.Testing.Framework.InternalUnsafeActionTestNode -Microsoft.Testing.Framework.InternalUnsafeActionTestNode.Body.get -> System.Action! -Microsoft.Testing.Framework.InternalUnsafeActionTestNode.Body.init -> void -Microsoft.Testing.Framework.InternalUnsafeActionTestNode.InternalUnsafeActionTestNode() -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionParameterizedTestNode -Microsoft.Testing.Framework.InternalUnsafeAsyncActionParameterizedTestNode.Body.get -> System.Func! -Microsoft.Testing.Framework.InternalUnsafeAsyncActionParameterizedTestNode.Body.init -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionParameterizedTestNode.GetArguments.get -> System.Func!>! -Microsoft.Testing.Framework.InternalUnsafeAsyncActionParameterizedTestNode.GetArguments.init -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionParameterizedTestNode.InternalUnsafeAsyncActionParameterizedTestNode() -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTaskParameterizedTestNode -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTaskParameterizedTestNode.Body.get -> System.Func! -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTaskParameterizedTestNode.Body.init -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTaskParameterizedTestNode.GetArguments.get -> System.Func!>!>! -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTaskParameterizedTestNode.GetArguments.init -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTaskParameterizedTestNode.InternalUnsafeAsyncActionTaskParameterizedTestNode() -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTestNode -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTestNode.Body.get -> System.Func! -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTestNode.Body.init -> void -Microsoft.Testing.Framework.InternalUnsafeAsyncActionTestNode.InternalUnsafeAsyncActionTestNode() -> void -Microsoft.Testing.Framework.InternalUnsafeTestArgumentsEntry -Microsoft.Testing.Framework.InternalUnsafeTestArgumentsEntry.Arguments.get -> TArguments -Microsoft.Testing.Framework.InternalUnsafeTestArgumentsEntry.DisplayNameFragment.get -> string? -Microsoft.Testing.Framework.InternalUnsafeTestArgumentsEntry.InternalUnsafeTestArgumentsEntry(TArguments arguments, string! uidFragment, string? displayNameFragment = null) -> void -Microsoft.Testing.Framework.InternalUnsafeTestArgumentsEntry.UidFragment.get -> string! -Microsoft.Testing.Framework.ITestExecutionContext -Microsoft.Testing.Framework.ITestExecutionContext.AddTestAttachmentAsync(System.IO.FileInfo! file, string! displayName, string? description = null) -> System.Threading.Tasks.Task! -Microsoft.Testing.Framework.ITestExecutionContext.CancellationToken.get -> System.Threading.CancellationToken -Microsoft.Testing.Framework.ITestExecutionContext.CancelTestExecution() -> void -Microsoft.Testing.Framework.ITestExecutionContext.CancelTestExecution(int millisecondsDelay) -> void -Microsoft.Testing.Framework.ITestExecutionContext.CancelTestExecution(System.TimeSpan delay) -> void -Microsoft.Testing.Framework.ITestExecutionContext.Configuration.get -> Microsoft.Testing.Framework.Configurations.IConfiguration! -Microsoft.Testing.Framework.ITestExecutionContext.ReportException(System.Exception! exception, System.Threading.CancellationToken? timeoutCancellationToken = null) -> void -Microsoft.Testing.Framework.ITestExecutionContext.TestInfo.get -> Microsoft.Testing.Framework.ITestInfo! -Microsoft.Testing.Framework.ITestInfo -Microsoft.Testing.Framework.ITestInfo.DisplayName.get -> string! -Microsoft.Testing.Framework.ITestInfo.Properties.get -> Microsoft.Testing.Platform.Extensions.Messages.IProperty![]! -Microsoft.Testing.Framework.ITestInfo.StableUid.get -> Microsoft.Testing.Framework.TestNodeUid! -Microsoft.Testing.Framework.ITestNodesBuilder -Microsoft.Testing.Framework.ITestNodesBuilder.BuildAsync(Microsoft.Testing.Framework.ITestSessionContext! testSessionContext) -> System.Threading.Tasks.Task! -Microsoft.Testing.Framework.ITestSessionContext -Microsoft.Testing.Framework.ITestSessionContext.AddTestAttachmentAsync(System.IO.FileInfo! file, string! displayName, string? description = null) -> System.Threading.Tasks.Task! -Microsoft.Testing.Framework.ITestSessionContext.CancellationToken.get -> System.Threading.CancellationToken -Microsoft.Testing.Framework.ITestSessionContext.Configuration.get -> Microsoft.Testing.Framework.Configurations.IConfiguration! -Microsoft.Testing.Framework.TestApplicationBuilderExtensions -Microsoft.Testing.Framework.TestNode -Microsoft.Testing.Framework.TestNode.DisplayName.get -> string! -Microsoft.Testing.Framework.TestNode.DisplayName.init -> void -Microsoft.Testing.Framework.TestNode.OverriddenEdgeName.get -> string? -Microsoft.Testing.Framework.TestNode.OverriddenEdgeName.init -> void -Microsoft.Testing.Framework.TestNode.Properties.get -> Microsoft.Testing.Platform.Extensions.Messages.IProperty![]! -Microsoft.Testing.Framework.TestNode.Properties.init -> void -Microsoft.Testing.Framework.TestNode.StableUid.get -> Microsoft.Testing.Framework.TestNodeUid! -Microsoft.Testing.Framework.TestNode.StableUid.init -> void -Microsoft.Testing.Framework.TestNode.TestNode() -> void -Microsoft.Testing.Framework.TestNode.Tests.get -> Microsoft.Testing.Framework.TestNode![]! -Microsoft.Testing.Framework.TestNode.Tests.init -> void -Microsoft.Testing.Framework.TestNodeUid -Microsoft.Testing.Framework.TestNodeUid.$() -> Microsoft.Testing.Framework.TestNodeUid! -Microsoft.Testing.Framework.TestNodeUid.Deconstruct(out string! Value) -> void -Microsoft.Testing.Framework.TestNodeUid.Equals(Microsoft.Testing.Framework.TestNodeUid? other) -> bool -Microsoft.Testing.Framework.TestNodeUid.TestNodeUid(string! Value) -> void -Microsoft.Testing.Framework.TestNodeUid.Value.get -> string! -Microsoft.Testing.Framework.TestNodeUid.Value.init -> void -override Microsoft.Testing.Framework.TestNodeUid.Equals(object? obj) -> bool -override Microsoft.Testing.Framework.TestNodeUid.GetHashCode() -> int -override Microsoft.Testing.Framework.TestNodeUid.ToString() -> string! -static Microsoft.Testing.Framework.Configurations.ConfigurationExtensions.GetTestResultDirectory(this Microsoft.Testing.Framework.Configurations.IConfiguration! configuration) -> string! -static Microsoft.Testing.Framework.DynamicDataNameProvider.GetUidFragment(string![]! parameterNames, object?[]! data, int index) -> string! -static Microsoft.Testing.Framework.TestApplicationBuilderExtensions.AddTestFramework(this Microsoft.Testing.Platform.Builder.ITestApplicationBuilder! testApplicationBuilder, Microsoft.Testing.Framework.Configurations.TestFrameworkConfiguration? testFrameworkConfiguration = null, params Microsoft.Testing.Framework.ITestNodesBuilder![]! testNodesBuilder) -> void -static Microsoft.Testing.Framework.TestApplicationBuilderExtensions.AddTestFramework(this Microsoft.Testing.Platform.Builder.ITestApplicationBuilder! testApplicationBuilder, params Microsoft.Testing.Framework.ITestNodesBuilder![]! testNodesBuilder) -> void -static Microsoft.Testing.Framework.TestNodeUid.implicit operator Microsoft.Testing.Framework.TestNodeUid!(string! value) -> Microsoft.Testing.Framework.TestNodeUid! -static Microsoft.Testing.Framework.TestNodeUid.operator !=(Microsoft.Testing.Framework.TestNodeUid? left, Microsoft.Testing.Framework.TestNodeUid? right) -> bool -static Microsoft.Testing.Framework.TestNodeUid.operator ==(Microsoft.Testing.Framework.TestNodeUid? left, Microsoft.Testing.Framework.TestNodeUid? right) -> bool diff --git a/src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Unshipped.txt b/src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Unshipped.txt deleted file mode 100644 index 72ac54f720..0000000000 --- a/src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,3 +0,0 @@ -#nullable enable -Microsoft.Testing.Framework.InternalUnsafeParameterizedTestNodeBase -Microsoft.Testing.Framework.InternalUnsafeParameterizedTestNodeBase.InternalUnsafeParameterizedTestNodeBase() -> void diff --git a/src/Adapter/MSTest.Engine/PublicAPI/net/PublicAPI.Shipped.txt b/src/Adapter/MSTest.Engine/PublicAPI/net/PublicAPI.Shipped.txt deleted file mode 100644 index 7dc5c58110..0000000000 --- a/src/Adapter/MSTest.Engine/PublicAPI/net/PublicAPI.Shipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/src/Adapter/MSTest.Engine/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Adapter/MSTest.Engine/PublicAPI/net/PublicAPI.Unshipped.txt deleted file mode 100644 index 7dc5c58110..0000000000 --- a/src/Adapter/MSTest.Engine/PublicAPI/net/PublicAPI.Unshipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/src/Adapter/MSTest.Engine/PublicAPI/netstandard/PublicAPI.Shipped.txt b/src/Adapter/MSTest.Engine/PublicAPI/netstandard/PublicAPI.Shipped.txt deleted file mode 100644 index 074c6ad103..0000000000 --- a/src/Adapter/MSTest.Engine/PublicAPI/netstandard/PublicAPI.Shipped.txt +++ /dev/null @@ -1,2 +0,0 @@ -#nullable enable - diff --git a/src/Adapter/MSTest.Engine/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Adapter/MSTest.Engine/PublicAPI/netstandard/PublicAPI.Unshipped.txt deleted file mode 100644 index 7dc5c58110..0000000000 --- a/src/Adapter/MSTest.Engine/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/src/Adapter/MSTest.Engine/TestFramework/MSTestEngineBannerCapability.cs b/src/Adapter/MSTest.Engine/TestFramework/MSTestEngineBannerCapability.cs deleted file mode 100644 index cfc45645ee..0000000000 --- a/src/Adapter/MSTest.Engine/TestFramework/MSTestEngineBannerCapability.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Capabilities.TestFramework; -using Microsoft.Testing.Platform.Services; -using Microsoft.Testing.Shared; - -namespace Microsoft.Testing.Framework; - -internal sealed class MSTestEngineBannerCapability : IBannerMessageOwnerCapability -{ - private readonly IPlatformInformation _platformInformation; - - public MSTestEngineBannerCapability(IPlatformInformation platformInformation) => _platformInformation = platformInformation; - - public Task GetBannerMessageAsync() - => Task.FromResult(BannerMessageHelper.BuildBannerMessage(_platformInformation, "MSTest.Engine", MSTestEngineRepositoryVersion.Version)); -} diff --git a/src/Adapter/MSTest.Engine/TestFramework/TestFramework.cs b/src/Adapter/MSTest.Engine/TestFramework/TestFramework.cs deleted file mode 100644 index 88e34f3fea..0000000000 --- a/src/Adapter/MSTest.Engine/TestFramework/TestFramework.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Adapter; -using Microsoft.Testing.Framework.Configurations; -using Microsoft.Testing.Platform.Capabilities.TestFramework; -using Microsoft.Testing.Platform.Extensions.TestFramework; -using Microsoft.Testing.Platform.Helpers; -using Microsoft.Testing.Platform.Requests; -using Microsoft.Testing.Platform.TestHost; - -using IConfiguration = Microsoft.Testing.Platform.Configurations.IConfiguration; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestFramework : IDisposable, ITestFramework -#if NETCOREAPP -#pragma warning disable SA1001 // Commas should be spaced correctly - , IAsyncDisposable -#pragma warning restore SA1001 // Commas should be spaced correctly -#endif -{ - private readonly TestingFrameworkExtension _extension; - private readonly CountdownEvent _incomingRequestCounter = new(1); - private readonly TestFrameworkEngine _engine; - private readonly List _sessionWarningMessages = []; - private readonly List _sessionErrorMessages = []; - private SessionUid? _sessionId; - - public TestFramework(TestFrameworkConfiguration testFrameworkConfiguration, ITestNodesBuilder[] testNodesBuilders, TestingFrameworkExtension extension, - IClock clock, ITask task, IConfiguration configuration, ITestFrameworkCapabilities capabilities) - { - _extension = extension; - _engine = new(testFrameworkConfiguration, testNodesBuilders, extension, capabilities, clock, task, configuration); - } - - /// - public string Uid => _extension.Uid; - - /// - public string Version => _extension.Version; - - /// - public string DisplayName => _extension.DisplayName; - - /// - public string Description => _extension.Description; - - /// - public async Task IsEnabledAsync() => await _extension.IsEnabledAsync().ConfigureAwait(false); - - public Task CreateTestSessionAsync(CreateTestSessionContext context) - { - if (_sessionId is not null) - { - throw new InvalidOperationException("Session already created"); - } - - _sessionId = context.SessionUid; - _sessionWarningMessages.Clear(); - _sessionErrorMessages.Clear(); - return Task.FromResult(new CreateTestSessionResult { IsSuccess = true }); - } - - public async Task CloseTestSessionAsync(CloseTestSessionContext context) - { - _sessionId = null; - CloseTestSessionResult sessionResult = new(); - - try - { - // Ensure we have finished processing all requests. - _incomingRequestCounter.Signal(); - await _incomingRequestCounter.WaitAsync(context.CancellationToken).ConfigureAwait(false); - - if (_sessionErrorMessages.Count > 0) - { - StringBuilder errorBuilder = new(); - errorBuilder.AppendLine("Test session failed with the following errors:"); - for (int i = 0; i < _sessionErrorMessages.Count; i++) - { - errorBuilder.Append(" - ").AppendLine(_sessionErrorMessages[i]); - } - - sessionResult.ErrorMessage = errorBuilder.ToString(); - } - - if (_sessionWarningMessages.Count > 0) - { - StringBuilder errorBuilder = new(); - errorBuilder.AppendLine("Test session raised the following warnings:"); - for (int i = 0; i < _sessionWarningMessages.Count; i++) - { - errorBuilder.Append(" - ").AppendLine(_sessionWarningMessages[i]); - } - - sessionResult.WarningMessage = errorBuilder.ToString(); - } - - sessionResult.IsSuccess = _incomingRequestCounter.CurrentCount == 0 && _sessionErrorMessages.Count == 0; - return sessionResult; - } - catch (OperationCanceledException ex) when (ex.CancellationToken == context.CancellationToken) - { - // We are being cancelled, so we don't need to wait anymore - sessionResult.WarningMessage += - (sessionResult.WarningMessage?.Length > 0 ? Environment.NewLine : string.Empty) - + "Closing the test session was cancelled."; - sessionResult.IsSuccess = false; - return sessionResult; - } - } - - public async Task ExecuteRequestAsync(ExecuteRequestContext context) - { - _incomingRequestCounter.AddCount(); - try - { - if (context.Request is not TestExecutionRequest testExecutionRequest) - { - throw new InvalidOperationException($"Request type '{context.Request.GetType().FullName}' is not supported"); - } - - Result result = await _engine.ExecuteRequestAsync(testExecutionRequest, context.MessageBus, context.CancellationToken).ConfigureAwait(false); - - foreach (IReason reason in result.Reasons) - { - if (reason is IErrorReason errorReason) - { - _sessionErrorMessages.Add(errorReason.Exception?.ToString() ?? errorReason.Message); - } - else if (reason is IWarningReason warningReason) - { - _sessionWarningMessages.Add(warningReason.Message); - } - } - } - finally - { - _incomingRequestCounter.Signal(); - context.Complete(); - } - } - - public void Dispose() => _incomingRequestCounter.Dispose(); - -#if NETCOREAPP - - public ValueTask DisposeAsync() - { - _incomingRequestCounter.Dispose(); - return default; - } - -#endif -} diff --git a/src/Adapter/MSTest.Engine/TestFramework/TestFrameworkCapabilities.cs b/src/Adapter/MSTest.Engine/TestFramework/TestFrameworkCapabilities.cs deleted file mode 100644 index c7a853b973..0000000000 --- a/src/Adapter/MSTest.Engine/TestFramework/TestFrameworkCapabilities.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Extensions.TrxReport.Abstractions; -using Microsoft.Testing.Platform.Capabilities.TestFramework; - -namespace Microsoft.Testing.Framework; - -internal sealed class TestFrameworkCapabilities : ITestFrameworkCapabilities -{ - private readonly ITestNodesBuilder[] _testNodesBuilders; - private readonly ITestFrameworkCapability _bannerMessageOwnerCapability; - - public TestFrameworkCapabilities(ITestNodesBuilder[] testNodesBuilders, IBannerMessageOwnerCapability bannerMessageOwnerCapability) - { - _testNodesBuilders = testNodesBuilders; - _bannerMessageOwnerCapability = bannerMessageOwnerCapability; - } - - public IReadOnlyCollection Capabilities - => [new TestFrameworkCapabilitiesSet(_testNodesBuilders), _bannerMessageOwnerCapability]; -} - -internal sealed class TestFrameworkCapabilitiesSet : - ITestNodesTreeFilterTestFrameworkCapability, - ITrxReportCapability, - INamedFeatureCapability -{ - private const string MultiRequestSupport = "experimental_multiRequestSupport"; - private readonly ITestNodesBuilder[] _testNodesBuilders; - - public TestFrameworkCapabilitiesSet(ITestNodesBuilder[] testNodesBuilders) - { - IsTrxReportCapabilitySupported = testNodesBuilders.All(x => x.HasCapability()); - _testNodesBuilders = testNodesBuilders; - } - - public bool IsTrxReportEnabled { get; private set; } - - public bool IsTrxReportCapabilitySupported { get; } - - bool ITestNodesTreeFilterTestFrameworkCapability.IsSupported => true; - - bool ITrxReportCapability.IsSupported => IsTrxReportCapabilitySupported; - - public void Enable() - { - IsTrxReportEnabled = true; - foreach (ITestNodesBuilder item in _testNodesBuilders) - { - item.GetCapability()?.Enable(); - } - } - - bool INamedFeatureCapability.IsSupported(string featureName) => featureName == MultiRequestSupport; -} diff --git a/src/Adapter/MSTest.Engine/TestFramework/TestingFrameworkExtension.cs b/src/Adapter/MSTest.Engine/TestFramework/TestingFrameworkExtension.cs deleted file mode 100644 index 07fc54b2f2..0000000000 --- a/src/Adapter/MSTest.Engine/TestFramework/TestingFrameworkExtension.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions; - -namespace Microsoft.Testing.Framework.Adapter; - -internal sealed class TestingFrameworkExtension : IExtension -{ - public string Uid => "MSTestEngine"; - - public string Version => TestFrameworkConstants.DefaultSemVer; - - public string DisplayName => "MSTest AOT"; - - public string Description => "MSTest AOT. This framework allows you to test your code anywhere in any mode (all OSes, all platforms, all configurations...)."; - - public Task IsEnabledAsync() => Task.FromResult(true); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/FactoryTestNodesBuilder.cs b/src/Adapter/MSTest.Engine/TestNodes/FactoryTestNodesBuilder.cs deleted file mode 100644 index eaad6454de..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/FactoryTestNodesBuilder.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Capabilities; -using Microsoft.Testing.Platform.Capabilities.TestFramework; - -namespace Microsoft.Testing.Framework; - -internal sealed class FactoryTestNodesBuilder : ITestNodesBuilder -{ - private readonly Func _testNodesFactory; - - public FactoryTestNodesBuilder(Func testNodesFactory) - => _testNodesFactory = testNodesFactory; - - public bool IsSupportingTrxProperties { get; } - - IReadOnlyCollection ICapabilities.Capabilities => []; - - public Task BuildAsync(ITestSessionContext _) => Task.FromResult(_testNodesFactory()); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/FrameworkTestNodeProperties.cs b/src/Adapter/MSTest.Engine/TestNodes/FrameworkTestNodeProperties.cs deleted file mode 100644 index 41c23a0427..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/FrameworkTestNodeProperties.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions.Messages; - -namespace Microsoft.Testing.Framework; - -internal readonly struct FrameworkEngineMetadataProperty() : IProperty -{ - public bool PreventArgumentsExpansion { get; init; } - - public string[] UsedFixtureIds { get; init; } = []; - - public static FrameworkEngineMetadataProperty GetFromProperties(IProperty[] properties) - { - FrameworkEngineMetadataProperty? result = null; - foreach (IProperty property in properties) - { - if (property is FrameworkEngineMetadataProperty frameworkEngineMetadataProperty) - { - if (result is not null) - { - throw new InvalidOperationException("Sequence contains more than one matching element"); - } - - result = frameworkEngineMetadataProperty; - } - } - - return result ?? default; - } -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/IActionTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IActionTestNode.cs deleted file mode 100644 index 25624bfa12..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IActionTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IActionTestNode : IActionableTestNode -{ - void Invoke(ITestExecutionContext testExecutionContext); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/IActionableTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IActionableTestNode.cs deleted file mode 100644 index 2631b535d2..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IActionableTestNode.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IActionableTestNode; diff --git a/src/Adapter/MSTest.Engine/TestNodes/IAsyncActionTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IAsyncActionTestNode.cs deleted file mode 100644 index 0cfec0e29e..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IAsyncActionTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IAsyncActionTestNode : IActionableTestNode -{ - Task InvokeAsync(ITestExecutionContext testExecutionContext); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/IAsyncParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IAsyncParameterizedTestNode.cs deleted file mode 100644 index 06f76ad20a..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IAsyncParameterizedTestNode.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -#if NET -namespace Microsoft.Testing.Framework; - -internal interface IAsyncParameterizedTestNode : IExpandableTestNode -{ - Func> GetArguments { get; } -} -#endif diff --git a/src/Adapter/MSTest.Engine/TestNodes/IContextTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IContextTestNode.cs deleted file mode 100644 index 1cef2c26a8..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IContextTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IContextTestNode -{ - TContext Context { get; } -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/IExpandableTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IExpandableTestNode.cs deleted file mode 100644 index ddcbe0e38a..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IExpandableTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IExpandableTestNode -{ - TestNode GetExpandedTestNode(object arguments, string argumentFragmentUid, string argumentFragmentDisplayName); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/IParameterizedAsyncActionTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IParameterizedAsyncActionTestNode.cs deleted file mode 100644 index 8c40e0cc10..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IParameterizedAsyncActionTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IParameterizedAsyncActionTestNode : IActionableTestNode -{ - Task InvokeAsync(ITestExecutionContext testExecutionContext, Func, Task> safeInvoke); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/IParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/IParameterizedTestNode.cs deleted file mode 100644 index 4e0397809c..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/IParameterizedTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface IParameterizedTestNode : IExpandableTestNode -{ - Func GetArguments { get; } -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/ITaskParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/ITaskParameterizedTestNode.cs deleted file mode 100644 index 9734cc2081..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/ITaskParameterizedTestNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -internal interface ITaskParameterizedTestNode : IExpandableTestNode -{ - Func> GetArguments { get; } -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/ITestNodeBuilder.cs b/src/Adapter/MSTest.Engine/TestNodes/ITestNodeBuilder.cs deleted file mode 100644 index 17a81de037..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/ITestNodeBuilder.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Capabilities.TestFramework; - -namespace Microsoft.Testing.Framework; - -public interface ITestNodesBuilder : ITestFrameworkCapabilities -{ - Task BuildAsync(ITestSessionContext testSessionContext); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionParameterizedTestNode.cs deleted file mode 100644 index 0c2e544314..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionParameterizedTestNode.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -/// Type that holds the parameter data. -public sealed class InternalUnsafeActionParameterizedTestNode - : InternalUnsafeParameterizedTestNodeBase, IParameterizedTestNode -{ - public required Action Body { get; init; } - - public required Func> GetArguments { get; init; } - - Func IParameterizedTestNode.GetArguments => GetArguments; - - internal override Task> GetArgumentsAsync() - => Task.FromResult(GetArguments()); - - internal override Func CreateInvokeBody(ITestExecutionContext testExecutionContext) - => CreateInvokeBody(Body, testExecutionContext); - - internal override TestNode Expand(object arguments, string argumentFragmentUid, string argumentFragmentDisplayName) - => InternalUnsafeParameterizedTestNodeHelper.ExpandActionNode(this, arguments, argumentFragmentUid, argumentFragmentDisplayName, Body); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTaskParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTaskParameterizedTestNode.cs deleted file mode 100644 index 51f539c8a6..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTaskParameterizedTestNode.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -/// Type that holds the parameter data. -public sealed class InternalUnsafeActionTaskParameterizedTestNode - : InternalUnsafeParameterizedTestNodeBase, ITaskParameterizedTestNode -{ - public required Action Body { get; init; } - - public required Func>> GetArguments { get; init; } - - Func> ITaskParameterizedTestNode.GetArguments => async () => await GetArguments().ConfigureAwait(false); - - internal override Task> GetArgumentsAsync() - => GetArguments(); - - internal override Func CreateInvokeBody(ITestExecutionContext testExecutionContext) - => CreateInvokeBody(Body, testExecutionContext); - - internal override TestNode Expand(object arguments, string argumentFragmentUid, string argumentFragmentDisplayName) - => InternalUnsafeParameterizedTestNodeHelper.ExpandActionNode(this, arguments, argumentFragmentUid, argumentFragmentDisplayName, Body); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTestNode.cs deleted file mode 100644 index ff06966fd0..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTestNode.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -public sealed class InternalUnsafeActionTestNode : TestNode, IActionTestNode -{ - public required Action Body { get; init; } - - void IActionTestNode.Invoke(ITestExecutionContext testExecutionContext) - => Body(testExecutionContext); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionParameterizedTestNode.cs deleted file mode 100644 index 481bbabef6..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionParameterizedTestNode.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -/// Type that holds the parameter data. -public sealed class InternalUnsafeAsyncActionParameterizedTestNode - : InternalUnsafeParameterizedTestNodeBase, IParameterizedTestNode -{ - public required Func Body { get; init; } - - public required Func> GetArguments { get; init; } - - Func IParameterizedTestNode.GetArguments => GetArguments; - - internal override Task> GetArgumentsAsync() - => Task.FromResult(GetArguments()); - - internal override Func CreateInvokeBody(ITestExecutionContext testExecutionContext) - => item => Body(testExecutionContext, item); - - internal override TestNode Expand(object arguments, string argumentFragmentUid, string argumentFragmentDisplayName) - => InternalUnsafeParameterizedTestNodeHelper.ExpandAsyncActionNode(this, arguments, argumentFragmentUid, argumentFragmentDisplayName, Body); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTaskParameterizedTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTaskParameterizedTestNode.cs deleted file mode 100644 index f1730fc4c4..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTaskParameterizedTestNode.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -/// Type that holds the parameter data. -public sealed class InternalUnsafeAsyncActionTaskParameterizedTestNode - : InternalUnsafeParameterizedTestNodeBase, ITaskParameterizedTestNode -{ - public required Func Body { get; init; } - - public required Func>> GetArguments { get; init; } - - Func> ITaskParameterizedTestNode.GetArguments => async () => await GetArguments().ConfigureAwait(false); - - internal override Task> GetArgumentsAsync() - => GetArguments(); - - internal override Func CreateInvokeBody(ITestExecutionContext testExecutionContext) - => item => Body(testExecutionContext, item); - - internal override TestNode Expand(object arguments, string argumentFragmentUid, string argumentFragmentDisplayName) - => InternalUnsafeParameterizedTestNodeHelper.ExpandAsyncActionNode(this, arguments, argumentFragmentUid, argumentFragmentDisplayName, Body); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTestNode.cs deleted file mode 100644 index f6abe9f436..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTestNode.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -public sealed class InternalUnsafeAsyncActionTestNode : TestNode, IAsyncActionTestNode -{ - public required Func Body { get; init; } - - async Task IAsyncActionTestNode.InvokeAsync(ITestExecutionContext testExecutionContext) - => await Body(testExecutionContext).ConfigureAwait(false); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeParameterizedTestNodeBase.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeParameterizedTestNodeBase.cs deleted file mode 100644 index cf7718af79..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeParameterizedTestNodeBase.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// WARNING: This type is public, but is meant for use only by MSTest source generator. Unannounced breaking changes to this API may happen. -/// -/// Type that holds the parameter data. -public abstract class InternalUnsafeParameterizedTestNodeBase : TestNode, IParameterizedAsyncActionTestNode, IExpandableTestNode -{ - async Task IParameterizedAsyncActionTestNode.InvokeAsync(ITestExecutionContext testExecutionContext, Func, Task> safeInvoke) - => await InvokeWithArgumentsAsync(CreateInvokeBody(testExecutionContext), safeInvoke).ConfigureAwait(false); - - TestNode IExpandableTestNode.GetExpandedTestNode(object arguments, string argumentFragmentUid, string argumentFragmentDisplayName) - => Expand(arguments, argumentFragmentUid, argumentFragmentDisplayName); - - /// - /// Gets all argument items for this parameterized test node. - /// - /// Argument items for this parameterized test node. - internal abstract Task> GetArgumentsAsync(); - - /// - /// Creates the delegate that executes the test body for one argument item. - /// - /// Current test execution context. - /// Delegate that executes the test body. - internal abstract Func CreateInvokeBody(ITestExecutionContext testExecutionContext); - - /// - /// Invokes the provided body delegate for all arguments produced by the node. - /// - /// Delegate that executes the test body for one argument item. - /// Wrapper that safely executes each body invocation. - /// A task that completes when all arguments have been invoked. - internal Task InvokeWithArgumentsAsync(Func invokeBodyAsync, Func, Task> safeInvoke) - => InternalUnsafeParameterizedTestNodeHelper.InvokeAsync(GetArgumentsAsync, invokeBodyAsync, safeInvoke); - - internal static Func CreateInvokeBody(Action body, ITestExecutionContext testExecutionContext) - => item => - { - body(testExecutionContext, item); - return Task.CompletedTask; - }; - - /// - /// Expands the current parameterized node into a concrete test node for one argument item. - /// - /// Argument item used for the expansion. - /// UID fragment for the argument item. - /// Display name fragment for the argument item. - /// The expanded test node. - internal abstract TestNode Expand(object arguments, string argumentFragmentUid, string argumentFragmentDisplayName); -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeParameterizedTestNodeHelper.cs b/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeParameterizedTestNodeHelper.cs deleted file mode 100644 index 59c3b98416..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeParameterizedTestNodeHelper.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.Helpers; - -namespace Microsoft.Testing.Framework; - -internal static class InternalUnsafeParameterizedTestNodeHelper -{ - public static Task InvokeAsync( - Func> getArguments, - Func invokeBodyAsync, - Func, Task> safeInvoke) - => InvokeAsync(getArguments(), invokeBodyAsync, safeInvoke); - - public static async Task InvokeAsync( - Func>> getArgumentsAsync, - Func invokeBodyAsync, - Func, Task> safeInvoke) - => await InvokeAsync(await getArgumentsAsync().ConfigureAwait(false), invokeBodyAsync, safeInvoke).ConfigureAwait(false); - - public static async Task InvokeAsync( - IEnumerable arguments, - Func invokeBodyAsync, - Func, Task> safeInvoke) - { - foreach (TData item in arguments) - { - await safeInvoke(() => invokeBodyAsync(item)).ConfigureAwait(false); - } - } - - public static InternalUnsafeActionTestNode ExpandActionNode( - TestNode testNode, - object arguments, - string argumentFragmentUid, - string argumentFragmentDisplayName, - Action body) - => new() - { - StableUid = TestNodeExpansionHelper.GenerateStableUid(testNode.StableUid, argumentFragmentUid), - DisplayName = TestNodeExpansionHelper.GenerateDisplayName(testNode.DisplayName, argumentFragmentDisplayName), - Body = testExecutionContext => body(testExecutionContext, (TData)arguments), - Properties = testNode.Properties, - }; - - public static InternalUnsafeAsyncActionTestNode ExpandAsyncActionNode( - TestNode testNode, - object arguments, - string argumentFragmentUid, - string argumentFragmentDisplayName, - Func body) - => new() - { - StableUid = TestNodeExpansionHelper.GenerateStableUid(testNode.StableUid, argumentFragmentUid), - DisplayName = TestNodeExpansionHelper.GenerateDisplayName(testNode.DisplayName, argumentFragmentDisplayName), - Body = testExecutionContext => body(testExecutionContext, (TData)arguments), - Properties = testNode.Properties, - }; -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/TestNode.cs b/src/Adapter/MSTest.Engine/TestNodes/TestNode.cs deleted file mode 100644 index 8119973fde..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/TestNode.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions.Messages; - -namespace Microsoft.Testing.Framework; - -[DebuggerDisplay("StableUid = {StableUid.Value}, TestsCount = {Tests.Length}, PropertiesCount = {Properties.Length}")] -public class TestNode -{ - public required TestNodeUid StableUid { get; init; } - - public required string DisplayName { get; init; } - - /// - /// Gets the name of the edge that connects this node to its parent. - /// - public string? OverriddenEdgeName { get; init; } - - public IProperty[] Properties { get; init; } = []; - - public TestNode[] Tests { get; init; } = []; -} diff --git a/src/Adapter/MSTest.Engine/TestNodes/TestNodeUid.cs b/src/Adapter/MSTest.Engine/TestNodes/TestNodeUid.cs deleted file mode 100644 index c1da416bb5..0000000000 --- a/src/Adapter/MSTest.Engine/TestNodes/TestNodeUid.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework; - -/// -/// Represents a unique identifier for a test node. -/// -/// The unique identifier. -[DebuggerDisplay("{Value}")] -public sealed record TestNodeUid(string Value) -{ - /// - /// Implicitly converts a string to a . - /// - /// The unique identifier. - public static implicit operator TestNodeUid(string value) - => new(value); -} diff --git a/src/Adapter/MSTest.Engine/TimeSheet.cs b/src/Adapter/MSTest.Engine/TimeSheet.cs deleted file mode 100644 index e026329f66..0000000000 --- a/src/Adapter/MSTest.Engine/TimeSheet.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Platform.Helpers; - -namespace Microsoft.Testing.Framework; - -/// -/// Keeps track of time and duration of a test. -/// -internal sealed class TimeSheet -{ - private readonly Stopwatch _stopwatch; - private readonly IClock _clock; - - /// - /// Initializes a new instance of the class. - /// Creates new instance of this class, starts measuring time in queue. - /// - public TimeSheet(IClock clock) - { - _stopwatch = Stopwatch.StartNew(); - _clock = clock; - } - - /// - /// Gets when the test started in UTC. Not-precise, because we just capture the current DateTimeUtc. - /// - public DateTimeOffset StartTime { get; private set; } - - /// - /// Gets when the test stopped in UTC. Not-precise, because we just capture the current DateTimeUtc. - /// - public DateTimeOffset StopTime { get; private set; } - - /// - /// Gets how long we've spent executing the test. Precise, measured by Stopwatch. - /// - public TimeSpan Duration { get; private set; } - - /// - /// Record the start of the test, this will capture time spent in queue and start measuring duration of test. - /// - internal void RecordStart() - { - StartTime = _clock.UtcNow; - _stopwatch.Restart(); - } - - /// - /// Record the end of the test, this will capture time spent executing the test. - /// - internal void RecordStop() - { - StopTime = _clock.UtcNow; - Duration = _stopwatch.Elapsed; - } -} diff --git a/src/Adapter/MSTest.Engine/build/MSTest.Engine.props b/src/Adapter/MSTest.Engine/build/MSTest.Engine.props deleted file mode 100644 index 68ae1bd96f..0000000000 --- a/src/Adapter/MSTest.Engine/build/MSTest.Engine.props +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Adapter/MSTest.Engine/build/MSTest.Engine.targets b/src/Adapter/MSTest.Engine/build/MSTest.Engine.targets deleted file mode 100644 index f7451dd95f..0000000000 --- a/src/Adapter/MSTest.Engine/build/MSTest.Engine.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Adapter/MSTest.Engine/buildMultiTargeting/MSTest.Engine.props b/src/Adapter/MSTest.Engine/buildMultiTargeting/MSTest.Engine.props deleted file mode 100644 index ac5c535fa4..0000000000 --- a/src/Adapter/MSTest.Engine/buildMultiTargeting/MSTest.Engine.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - MSTest.SourceGeneration - Microsoft.Testing.Framework.SourceGeneration.SourceGeneratedTestingPlatformBuilderHook - - - diff --git a/src/Adapter/MSTest.Engine/buildMultiTargeting/MSTest.Engine.targets b/src/Adapter/MSTest.Engine/buildMultiTargeting/MSTest.Engine.targets deleted file mode 100644 index cf33b5d45c..0000000000 --- a/src/Adapter/MSTest.Engine/buildMultiTargeting/MSTest.Engine.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs index 7078c45b24..81c975c823 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs @@ -172,4 +172,15 @@ public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFul testContextImplementation.SetOutcome(outcome); return testContextImplementation; } + + /// + /// Swaps the cached and + /// instances for the source-generator-backed implementations. Used by + /// . + /// + internal void SetSourceGeneratedOperations(IReflectionOperations reflectionOperations, IFileOperations fileOperations) + { + ReflectionOperations = reflectionOperations ?? throw new ArgumentNullException(nameof(reflectionOperations)); + FileOperations = fileOperations ?? throw new ArgumentNullException(nameof(fileOperations)); + } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt b/src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt index 7dc5c58110..245e1d9c86 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.ReflectionMetadataHook +static Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.ReflectionMetadataHook.Register(System.Reflection.Assembly! assembly, System.Type![]! types, System.Collections.Generic.IReadOnlyDictionary! testMethods) -> void diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/CompositeSourceGeneratedReflectionDataProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/CompositeSourceGeneratedReflectionDataProvider.cs new file mode 100644 index 0000000000..66fdcbe2a6 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/CompositeSourceGeneratedReflectionDataProvider.cs @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration; + +/// +/// Merges multiple per-assembly instances into +/// a single provider so that more than one test assembly can be registered with +/// in a single process. +/// +/// +/// Thread-safety: every mutation rebuilds an immutable from scratch +/// and publishes it via a single atomic field write (Volatile.Write). Readers go through +/// / the per-provider lookup overrides which read the field with +/// Volatile.Read, so they always see a fully-constructed point-in-time view (no torn +/// dictionaries) even though they do not take a lock. Callers of +/// are expected to serialize themselves +/// (today via 's lock). +/// +internal sealed class CompositeSourceGeneratedReflectionDataProvider : SourceGeneratedReflectionDataProvider +{ + private CompositeState _state = CompositeState.Empty; + + public void Add(SourceGeneratedReflectionDataProvider provider) + { + CompositeState previous = Volatile.Read(ref _state); + CompositeState next = previous.With(provider); + Volatile.Write(ref _state, next); + } + + internal override SourceGeneratedReflectionDataProvider GetSnapshot() + => Volatile.Read(ref _state).MergedSnapshot; + + internal override bool TryGetAssembly(string assemblyPath, [NotNullWhen(true)] out Assembly? assembly) + { + string name = Path.GetFileNameWithoutExtension(assemblyPath); + CompositeState state = Volatile.Read(ref _state); + if (state.ProvidersByAssemblyName.TryGetValue(name, out SourceGeneratedReflectionDataProvider? provider) + && provider.Assembly is { } resolved) + { + assembly = resolved; + return true; + } + + assembly = null; + return false; + } + + internal override void GetNavigationData(string className, string methodName, out int minLineNumber, out string? fileName) + { + CompositeState state = Volatile.Read(ref _state); + foreach (SourceGeneratedReflectionDataProvider provider in state.Providers) + { + provider.GetNavigationData(className, methodName, out minLineNumber, out fileName); + if (fileName is not null) + { + return; + } + } + + minLineNumber = 0; + fileName = null; + } + + internal override object[] GetAssemblyAttributes(Assembly assembly) + { + CompositeState state = Volatile.Read(ref _state); + return state.ProvidersByAssembly.TryGetValue(assembly, out SourceGeneratedReflectionDataProvider? provider) + ? provider.AssemblyAttributes + : []; + } + + internal override bool TryGetTypeByName(Assembly assembly, string typeName, [NotNullWhen(true)] out Type? type) + { + CompositeState state = Volatile.Read(ref _state); + if (state.ProvidersByAssembly.TryGetValue(assembly, out SourceGeneratedReflectionDataProvider? provider) + && provider.TypesByName.TryGetValue(typeName, out type)) + { + return true; + } + + type = null; + return false; + } + + /// + /// Immutable snapshot of the merged state. A new instance is produced for every + /// and published as a single + /// atomic field write, so any reader that observes a non-default state observes a fully + /// rebuilt snapshot. + /// + private sealed class CompositeState + { +#pragma warning disable IDE0028 // Dictionary needs an explicit comparer + public static readonly CompositeState Empty = new( + providers: [], + providersByAssemblyName: new Dictionary(StringComparer.OrdinalIgnoreCase), + providersByAssembly: [], + mergedSnapshot: new SourceGeneratedReflectionDataProvider()); +#pragma warning restore IDE0028 + + private CompositeState( + IReadOnlyList providers, + Dictionary providersByAssemblyName, + Dictionary providersByAssembly, + SourceGeneratedReflectionDataProvider mergedSnapshot) + { + Providers = providers; + ProvidersByAssemblyName = providersByAssemblyName; + ProvidersByAssembly = providersByAssembly; + MergedSnapshot = mergedSnapshot; + } + + public IReadOnlyList Providers { get; } + + public Dictionary ProvidersByAssemblyName { get; } + + public Dictionary ProvidersByAssembly { get; } + + public SourceGeneratedReflectionDataProvider MergedSnapshot { get; } + + public CompositeState With(SourceGeneratedReflectionDataProvider added) + { + var providers = new List(Providers.Count + 1); + providers.AddRange(Providers); + providers.Add(added); + +#pragma warning disable IDE0028 // Dictionary needs an explicit comparer + var byName = new Dictionary(ProvidersByAssemblyName, StringComparer.OrdinalIgnoreCase); +#pragma warning restore IDE0028 + byName[added.AssemblyName] = added; + + var byAssembly = new Dictionary(ProvidersByAssembly); + if (added.Assembly is { } addedAssembly) + { + byAssembly[addedAssembly] = added; + } + + return new CompositeState( + providers, + byName, + byAssembly, + BuildMergedSnapshot(providers)); + } + + private static SourceGeneratedReflectionDataProvider BuildMergedSnapshot(IReadOnlyList providers) + { + // AssemblyName / Assembly do not make sense for a composite; leave at defaults. + // TypesByName is intentionally NOT merged: it is consulted only by + // SourceGeneratedReflectionOperations.GetType(Assembly, string), which now routes + // through the per-provider TryGetTypeByName override to avoid same-FQN collisions + // between assemblies shadowing each other. + var types = new List(); + var typeAttributes = new Dictionary(); + var assemblyAttributes = new List(); + var typeProperties = new Dictionary(); + var typeMethods = new Dictionary(); + var typeMethodLocations = new Dictionary(StringComparer.Ordinal); + var typeMethodAttributes = new Dictionary(); + var typeConstructors = new Dictionary(); + var typePropertiesByName = new Dictionary>(); + var typeConstructorsInvoker = new Dictionary(); + + foreach (SourceGeneratedReflectionDataProvider provider in providers) + { + types.AddRange(provider.Types); + MergeInto(typeAttributes, provider.TypeAttributes); + assemblyAttributes.AddRange(provider.AssemblyAttributes); + MergeInto(typeProperties, provider.TypeProperties); + MergeInto(typeMethods, provider.TypeMethods); + MergeInto(typeMethodLocations, provider.TypeMethodLocations); + MergeInto(typeMethodAttributes, provider.TypeMethodAttributes); + MergeInto(typeConstructors, provider.TypeConstructors); + MergeInto(typePropertiesByName, provider.TypePropertiesByName); + MergeInto(typeConstructorsInvoker, provider.TypeConstructorsInvoker); + } + + return new SourceGeneratedReflectionDataProvider + { + Types = [.. types], + TypeAttributes = typeAttributes, + AssemblyAttributes = [.. assemblyAttributes], + TypeProperties = typeProperties, + TypeMethods = typeMethods, + TypeMethodLocations = typeMethodLocations, + TypeMethodAttributes = typeMethodAttributes, + TypeConstructors = typeConstructors, + TypePropertiesByName = typePropertiesByName, + TypeConstructorsInvoker = typeConstructorsInvoker, + }; + } + + private static void MergeInto(Dictionary target, Dictionary source) + where TKey : notnull + { + foreach (KeyValuePair kvp in source) + { + target[kvp.Key] = kvp.Value; + } + } + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs new file mode 100644 index 0000000000..605a25b426 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.ComponentModel; + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration; + +/// +/// Infrastructure. Entry point used by the MSTest source generator to register pre-computed +/// reflection metadata for a test assembly. After a successful +/// call, MSTest's +/// discovery and execution paths read metadata from the source-generated data instead of doing +/// reflection at runtime. +/// +/// +/// +/// This type is not intended to be used directly from application code. It is public only +/// because the MSTest source generator emits a [ModuleInitializer] in the test assembly +/// that needs to call it across the assembly boundary, and module initializers cannot use +/// internal APIs from a different assembly. The signature and behaviour of this hook are +/// implementation details that may evolve with the generator without a major version bump; do +/// not hand-roll a call to from your own code. +/// +/// +/// Discovery limitation. The MSTest source generator only enumerates types that carry +/// [TestClass] declared directly on the type. Test classes that inherit +/// [TestClass] from a base class are not registered through this hook and will +/// not be discovered when the source-generated provider is the active reflection backend. +/// Apply [TestClass] directly to the derived class to opt it back into discovery. +/// Analyzer MSTEST0069 (shipped in the MSTest.SourceGeneration package) flags classes +/// that hit this limitation. +/// +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class ReflectionMetadataHook +{ +#if NET9_0_OR_GREATER + private static readonly Lock Lock = new(); +#else + private static readonly object Lock = new(); +#endif + private static readonly CompositeSourceGeneratedReflectionDataProvider Composite = new(); + + /// + /// Infrastructure. Publishes source-generated metadata for + /// to the MSTest adapter. Safe to call from multiple module initializers; later registrations + /// are merged with earlier ones. + /// + /// The test assembly the metadata describes. + /// All types directly annotated with [TestClass] in the assembly. + /// + /// A map from each test class to its [TestMethod]-annotated + /// set. The dictionary and arrays are copied defensively; the caller may mutate the inputs + /// after the call. + /// + /// + /// Do not call this method from hand-written code; it is meant to be invoked exclusively from + /// the [ModuleInitializer] emitted by the MSTest source generator. The signature is + /// not covered by API-stability guarantees. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static void Register(Assembly assembly, Type[] types, IReadOnlyDictionary testMethods) + { + if (assembly is null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + if (types is null) + { + throw new ArgumentNullException(nameof(types)); + } + + if (testMethods is null) + { + throw new ArgumentNullException(nameof(testMethods)); + } + + var typesCopy = (Type[])types.Clone(); + + var testMethodsCopy = new Dictionary(testMethods.Count); + foreach (KeyValuePair kvp in testMethods) + { + testMethodsCopy[kvp.Key] = (MethodInfo[])kvp.Value.Clone(); + } + + // TypesByName must always match Type.FullName at runtime (see comment in the source + // generator emitter): compute it on the runtime side from typeof(T).FullName so the + // generator emits less code and the same FullName conventions are honored for nested + // and generic types. + var typesByName = new Dictionary(typesCopy.Length, StringComparer.Ordinal); + foreach (Type type in typesCopy) + { + if (type.FullName is { } fullName) + { + typesByName[fullName] = type; + } + } + + var provider = new SourceGeneratedReflectionDataProvider + { + Assembly = assembly, + AssemblyName = assembly.GetName().Name ?? string.Empty, + Types = typesCopy, + TypesByName = typesByName, + TypeMethods = testMethodsCopy, + }; + + lock (Lock) + { + Composite.Add(provider); + + var reflectionOperations = new SourceGeneratedReflectionOperations(Composite); + var fileOperations = new SourceGeneratedFileOperations(Composite); + + if (PlatformServiceProvider.Instance is PlatformServiceProvider concreteProvider) + { + concreteProvider.SetSourceGeneratedOperations(reflectionOperations, fileOperations); + } + } + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedFileOperations.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedFileOperations.cs new file mode 100644 index 0000000000..b9f6a63f76 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedFileOperations.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration; + +/// +/// Source-generator-backed implementation of . Assembly loading is +/// served from the supplied ; the remaining +/// file-system operations are delegated to the regular implementation. +/// +internal sealed class SourceGeneratedFileOperations : IFileOperations +{ + private readonly FileOperations _inner = new(); + + public SourceGeneratedFileOperations(SourceGeneratedReflectionDataProvider dataProvider) + => DataProvider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider)); + + internal SourceGeneratedReflectionDataProvider DataProvider { get; } + + public Assembly LoadAssembly(string assemblyName) + => DataProvider.TryGetAssembly(assemblyName, out Assembly? assembly) + ? assembly + : _inner.LoadAssembly(assemblyName); + + public bool DoesFileExist(string assemblyFileName) => _inner.DoesFileExist(assemblyFileName); + + public string GetFullFilePath(string assemblyFileName) => _inner.GetFullFilePath(assemblyFileName); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionDataProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionDataProvider.cs new file mode 100644 index 0000000000..5419acb6f3 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionDataProvider.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration; + +/// +/// Holds the pre-computed metadata that the MSTest source generator emits for a test assembly. +/// This data backs the source-generated implementations of the platform reflection and file +/// services so that test discovery and execution do not depend on runtime reflection. +/// +/// +/// This type is intentionally internal: only the MSTest source generator constructs it (via +/// the public builder API). The shape may evolve from +/// one MSTest version to the next without notice. +/// +internal class SourceGeneratedReflectionDataProvider +{ + /// + /// Gets the test assembly described by this metadata snapshot. + /// + public Assembly Assembly { get; init; } = null!; + + /// + /// Gets the file-name (without extension) of the test assembly. + /// + public string AssemblyName { get; init; } = string.Empty; + + /// + /// Gets all defined types in the test assembly that participate in test discovery. + /// + public Type[] Types { get; init; } = []; + + /// + /// Gets a lookup of types by full name. + /// + public Dictionary TypesByName { get; init; } = []; + + /// + /// Gets attributes declared on each type. The array contains attribute instances + /// already inflated by the source generator so no reflection call is required to read them. + /// + public Dictionary TypeAttributes { get; init; } = []; + + /// + /// Gets attribute instances declared at the assembly level. + /// + public object[] AssemblyAttributes { get; init; } = []; + + /// + /// Gets the properties declared on each type that MSTest may inspect (for example + /// TestContext properties or properties referenced by DynamicData). + /// + public Dictionary TypeProperties { get; init; } = []; + + /// + /// Gets the methods declared on each type that the source generator was able to surface + /// (today: [TestMethod]-annotated methods only, including inherited ones). + /// + /// + /// This dictionary is intentionally partial — it never carries non-test methods, generic + /// methods, or methods with by-ref parameters. Consumers must NOT use it as an authoritative + /// source for BindingFlags.DeclaredOnly-style enumerations; the reflection-backed + /// fallback is responsible for completeness. + /// + public Dictionary TypeMethods { get; init; } = []; + + /// + /// Gets source-location data for each type's methods so navigation in the IDE works + /// without a PDB round-trip. + /// + public Dictionary TypeMethodLocations { get; init; } = []; + + /// + /// Gets attributes declared on each method, keyed by the instance + /// that the source-generator resolved at startup. Keying by + /// (rather than method name) preserves the ability to distinguish overloaded methods. + /// + public Dictionary TypeMethodAttributes { get; init; } = []; + + /// + /// Gets constructors declared on each type. These are returned by + /// GetDeclaredConstructors. + /// + public Dictionary TypeConstructors { get; init; } = []; + + /// + /// Gets a lookup of properties on a type by property name. + /// + public Dictionary> TypePropertiesByName { get; init; } = []; + + /// + /// Gets the constructor invokers that allow instantiating types without reflection. + /// + public Dictionary TypeConstructorsInvoker { get; init; } = []; + + /// + /// Returns the snapshot of merged metadata that callers should read. Single-assembly + /// providers return themselves; the composite returns its currently-published snapshot + /// (so readers always see a consistent point-in-time view even when another assembly is + /// being registered concurrently). + /// + internal virtual SourceGeneratedReflectionDataProvider GetSnapshot() => this; + + internal virtual bool TryGetAssembly(string assemblyPath, [NotNullWhen(true)] out Assembly? assembly) + { + if (Assembly is not null + && string.Equals(Path.GetFileNameWithoutExtension(assemblyPath), AssemblyName, StringComparison.OrdinalIgnoreCase)) + { + assembly = Assembly; + return true; + } + + assembly = null; + return false; + } + + internal virtual void GetNavigationData(string className, string methodName, out int minLineNumber, out string? fileName) + { + if (!TypeMethodLocations.TryGetValue(className, out TypeLocation? typeLocation) || typeLocation is null) + { + minLineNumber = 0; + fileName = null; + return; + } + + if (!typeLocation.MethodLocations.TryGetValue(methodName, out int lineNumber)) + { + minLineNumber = 0; + fileName = null; + return; + } + + fileName = typeLocation.FileName; + minLineNumber = lineNumber; + } + + /// + /// Returns the assembly-level attributes that belong to . Returns + /// the single-provider when the assembly matches this provider's + /// ; otherwise an empty array. The composite override fans out the lookup + /// to the owning provider. + /// + internal virtual object[] GetAssemblyAttributes(Assembly assembly) + => assembly.Equals(Assembly) ? AssemblyAttributes : []; + + /// + /// Looks up a type by scoped to a specific . + /// The composite override routes the call to the per-assembly provider so that two assemblies + /// containing a type with the same full name do not shadow each other in a merged dictionary. + /// + /// + /// Returning instructs the caller to fall back to reflection. This is + /// the right behavior when did not opt into source generation, when + /// the type was skipped by the generator (open generic, file-local, inaccessible), or when the + /// composite has no entry for the assembly yet. + /// + internal virtual bool TryGetTypeByName(Assembly assembly, string typeName, [NotNullWhen(true)] out Type? type) + { + if (assembly.Equals(Assembly) && TypesByName.TryGetValue(typeName, out type)) + { + return true; + } + + type = null; + return false; + } + + /// + /// Per-type source-location information used by IDE / explorer navigation. + /// + internal sealed class TypeLocation + { + public string FileName { get; init; } = string.Empty; + + public Dictionary MethodLocations { get; init; } = []; + } + + /// + /// Describes one constructor and a delegate that invokes it. Source-generated code emits + /// these to allow Activator.CreateInstance-free instantiation. + /// + internal sealed class ConstructorInvoker + { + public Type[] Parameters { get; init; } = []; + + public Func Invoker { get; init; } = null!; + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs new file mode 100644 index 0000000000..c54aebc392 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration; + +/// +/// Source-generator-backed implementation of . +/// +/// +/// +/// Scope of the source-generated data. The MSTest source generator's +/// [ModuleInitializer] calls with only: +/// the assembly, the [TestClass] types, and their [TestMethod]-annotated +/// s. Everything else (type attributes, method attributes, assembly +/// attributes, constructors, properties, navigation data) is intentionally not +/// populated today; reads of those fields fall back to runtime reflection. The source-gen +/// payload is therefore best understood as "type / test-method rooting + trimmer hints" +/// rather than a full reflection replacement. +/// +/// +/// Why a fallback exists at all. Each fallback in this class falls into one of three +/// categories. Cataloguing them here so that adding a new fallback is a deliberate choice, +/// not an accidental hidden one: +/// +/// +/// +/// +/// Category A — Generator-gap fallback. The corresponding field on +/// is not populated by today's +/// emitter, so every call falls back to runtime reflection. These are closable: extend +/// the emitter to populate the field and the fallback stops firing. Today this covers +/// (for and +/// ), , +/// , , +/// , and . +/// +/// +/// +/// +/// Category B — Contract-mismatch fallback. The interface returns "every method" +/// (or similar), but the source generator intentionally models only test methods. Always +/// delegating is correct here; the only way to avoid the fallback would be to either +/// change the contract or to enumerate all methods at compile time (which is exactly +/// what reflection already does at runtime). Today this covers +/// , , and +/// . +/// +/// +/// +/// +/// Category C — Cross-assembly fallback. The lookup targets an assembly that did +/// not participate in source generation (test framework, adapter, extensions, or assets +/// packed without the generator). No amount of generator work eliminates this — assemblies +/// we do not compile cannot register source-gen data. Today this covers +/// , the no-match branch of , +/// , and the non-/non- +/// branch of . +/// +/// +/// +/// +/// Trim/AOT safety. Because most reads still fall through to reflection at runtime, +/// trim safety relies on the [DynamicDependency(All, typeof(T))] attributes emitted +/// by the source generator for each [TestClass] and each of its accessible base +/// types. Those preserve the members reflection then enumerates. +/// +/// +/// Adding a new fallback? Mark the method with a // Category A/B/C comment and +/// explain which one and why. The blind-corner risk is fallbacks that look intentional but +/// were really just oversights — labelling each call site makes the design choice visible. +/// +/// +internal sealed class SourceGeneratedReflectionOperations : IReflectionOperations +{ + private readonly ConcurrentDictionary _attributeCache = []; + private readonly ReflectionOperations _fallback = new(); + + public SourceGeneratedReflectionOperations(SourceGeneratedReflectionDataProvider dataProvider) + => DataProvider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider)); + + internal SourceGeneratedReflectionDataProvider DataProvider { get; } + + // Category A for Type / MethodInfo (TypeAttributes / TypeMethodAttributes are not emitted). + // Category C for every other MemberInfo subtype — the source generator never models + // FieldInfo, EventInfo, ParameterInfo, etc. and we have no plans to. + [return: NotNullIfNotNull(nameof(memberInfo))] + public object[]? GetCustomAttributes(MemberInfo memberInfo) + => memberInfo switch + { + null => null, + Type type => GetTypeAttributes(type), + MethodInfo method => GetMethodAttributes(method), + _ => _fallback.GetCustomAttributes(memberInfo), + }; + + // Category A: AssemblyAttributes is not populated by today's emitter, so this always + // falls through unless a future emitter snapshots assembly-level attributes. + public object[] GetCustomAttributes(Assembly assembly, Type type) + { + object[] sourceGenAttributes = DataProvider.GetAssemblyAttributes(assembly); + return sourceGenAttributes.Length == 0 + ? _fallback.GetCustomAttributes(assembly, type) + : [.. sourceGenAttributes.Where(type.IsInstanceOfType)]; + } + + // Category A: TypeConstructors is not populated by today's emitter. + public ConstructorInfo[] GetDeclaredConstructors(Type classType) + { + SourceGeneratedReflectionDataProvider data = DataProvider.GetSnapshot(); + return data.TypeConstructors.TryGetValue(classType, out ConstructorInfo[]? constructors) + ? constructors + : _fallback.GetDeclaredConstructors(classType); + } + + // Category B: the source-generated TypeMethods dictionary is partial — today it only + // contains methods annotated with [TestMethod] (and inherited [TestMethod]s), so it + // cannot satisfy the GetDeclaredMethods contract which is expected to return every + // method declared on the type. Always delegate to the runtime fallback to preserve + // correctness; the source-generated data is still used elsewhere (e.g. attribute + // lookup) to avoid reflection at runtime. + public MethodInfo[] GetDeclaredMethods(Type classType) + => _fallback.GetDeclaredMethods(classType); + + // Category A: TypeProperties is not populated by today's emitter. + public PropertyInfo[] GetDeclaredProperties(Type type) + { + SourceGeneratedReflectionDataProvider data = DataProvider.GetSnapshot(); + return data.TypeProperties.TryGetValue(type, out PropertyInfo[]? properties) + ? properties + : _fallback.GetDeclaredProperties(type); + } + + // Category C: assemblies that did not opt into source generation (typically the test + // framework, the adapter, MSTest extensions, or test assets packed without the + // generator) have no entries in `data.Types`. Fall back so those assemblies behave the + // same as in non-source-gen mode. + public Type[] GetDefinedTypes(Assembly assembly) + { + SourceGeneratedReflectionDataProvider data = DataProvider.GetSnapshot(); + Type[] filtered = [.. data.Types.Where(t => t.Assembly.Equals(assembly))]; + return filtered.Length > 0 ? filtered : _fallback.GetDefinedTypes(assembly); + } + + // Category B: the source-generated TypeMethods dictionary is partial (only + // [TestMethod]-annotated methods, no generics or by-ref) and the runtime contract + // requires returning every runtime method. Always delegate to the fallback. + public MethodInfo[] GetRuntimeMethods(Type type) => _fallback.GetRuntimeMethods(type); + + // Category B: the source-generator does not pre-resolve arbitrary methods (the + // partial TypeMethods dictionary only carries [TestMethod]-annotated entries). Doing + // our own GetRuntimeMethods + parameter-match here would (a) duplicate the scan that + // the fallback already performs on miss and (b) diverge from Type.GetMethod's binder + // semantics (overload resolution, generic / by-ref handling). Delegate so callers get + // the same selection rules as reflection mode. + public MethodInfo? GetRuntimeMethod(Type declaringType, string methodName, Type[] parameters, bool includeNonPublic) + => _fallback.GetRuntimeMethod(declaringType, methodName, parameters, includeNonPublic); + + // Category A: TypePropertiesByName is not populated by today's emitter. + public PropertyInfo? GetRuntimeProperty(Type classType, string propertyName, bool includeNonPublic) + { + SourceGeneratedReflectionDataProvider data = DataProvider.GetSnapshot(); + if (data.TypePropertiesByName.TryGetValue(classType, out Dictionary? properties) + && properties.TryGetValue(propertyName, out PropertyInfo? property)) + { + if (!includeNonPublic) + { + bool isPublic = property.GetMethod?.IsPublic is true || property.SetMethod?.IsPublic is true; + if (!isPublic) + { + return null; + } + } + + return property; + } + + return _fallback.GetRuntimeProperty(classType, propertyName, includeNonPublic); + } + + // Category C: `Type.GetType(string)` only resolves assembly-qualified names (or types + // in the calling assembly / mscorlib). The source-generated `TypesByName` dictionary is + // keyed by full type name without assembly qualification and aggregates entries across + // every registered test assembly, so consulting it here could silently bind to a + // same-named type from the wrong assembly. Match the runtime contract by always + // delegating; callers that have an assembly in hand use the `GetType(Assembly, string)` + // overload below for source-generated lookups. + public Type? GetType(string typeName) + => _fallback.GetType(typeName); + + // Category C: assemblies that did not opt into source generation (and types in + // opted-in assemblies that the generator skipped, such as open generics) are not in + // TypesByName; fall back so cross-assembly lookups still resolve. The composite routes + // this through the per-provider TryGetTypeByName override so two assemblies with the + // same fully-qualified type name do not shadow each other. + public Type? GetType(Assembly assembly, string typeName) + => DataProvider.TryGetTypeByName(assembly, typeName, out Type? type) + ? type + : _fallback.GetType(assembly, typeName); + + // Category A: TypeConstructorsInvoker is not populated by today's emitter, so this + // method always falls through. When it is populated in the future, the invoker path + // avoids both Activator.CreateInstance and the trim-unfriendly constructor reflection. + public object? CreateInstance(Type type, object?[] parameters) + { + SourceGeneratedReflectionDataProvider data = DataProvider.GetSnapshot(); + if (!data.TypeConstructorsInvoker.TryGetValue(type, out SourceGeneratedReflectionDataProvider.ConstructorInvoker[]? invokers)) + { + return _fallback.CreateInstance(type, parameters); + } + + foreach (SourceGeneratedReflectionDataProvider.ConstructorInvoker invoker in invokers) + { + if (invoker.Parameters.Length != parameters.Length) + { + continue; + } + + bool matches = true; + for (int i = 0; i < parameters.Length; i++) + { + object? argument = parameters[i]; + Type expected = invoker.Parameters[i]; + + if (argument is null) + { + if (expected.IsValueType && Nullable.GetUnderlyingType(expected) is null) + { + matches = false; + break; + } + } + else if (!expected.IsInstanceOfType(argument)) + { + matches = false; + break; + } + } + + if (matches) + { + return invoker.Invoker(parameters); + } + } + + return _fallback.CreateInstance(type, parameters); + } + + public bool IsAttributeDefined(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute + => attributeProvider is null + ? throw new ArgumentNullException(nameof(attributeProvider)) + : GetCustomAttributesCached(attributeProvider).OfType().Any(); + + public TAttribute? GetFirstAttributeOrDefault(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute + => GetCustomAttributesCached(attributeProvider).OfType().FirstOrDefault(); + + public TAttribute? GetSingleAttributeOrDefault(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute + { + TAttribute[] matches = [.. GetCustomAttributesCached(attributeProvider).OfType().Take(2)]; + return matches.Length switch + { + 0 => null, + 1 => matches[0], + _ => throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Found multiple attributes of type '{0}' when only one was expected.", typeof(TAttribute))), + }; + } + + public IEnumerable GetAttributes(ICustomAttributeProvider attributeProvider) + where TAttributeType : Attribute + => GetCustomAttributesCached(attributeProvider).OfType(); + + public Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider) + => attributeProvider is null + ? throw new ArgumentNullException(nameof(attributeProvider)) + : attributeProvider is MemberInfo or Assembly + ? _attributeCache.GetOrAdd(attributeProvider, GetAttributesForProvider) + : throw new ArgumentException( + $"Unsupported attribute provider type: {attributeProvider.GetType()}. Only MemberInfo and Assembly are supported.", + nameof(attributeProvider)); + + private Attribute[] GetAttributesForProvider(ICustomAttributeProvider provider) + => provider switch + { + Type type => GetTypeAttributes(type), + MethodInfo method => GetMethodAttributes(method), + Assembly assembly => GetAssemblyAttributesForProvider(assembly), + MemberInfo memberInfo => GetMemberAttributesFromReflection(memberInfo), + _ => [], + }; + + private Attribute[] GetAssemblyAttributesForProvider(Assembly assembly) + { + object[] sourceGen = DataProvider.GetAssemblyAttributes(assembly); + return sourceGen.Length > 0 + ? [.. sourceGen.OfType()] + : GetAssemblyAttributesFromReflection(assembly); + } + + public bool IsMethodDeclaredInSameAssemblyAsType(MethodInfo method, Type type) + => method.DeclaringType?.Assembly.Equals(type.Assembly) ?? false; + + internal void ClearCache() => _attributeCache.Clear(); + + private Attribute[] GetTypeAttributes(Type type) + { + SourceGeneratedReflectionDataProvider data = DataProvider.GetSnapshot(); + return data.TypeAttributes.TryGetValue(type, out Attribute[]? attributes) + ? attributes + : GetMemberAttributesFromReflection(type); + } + + private Attribute[] GetMethodAttributes(MethodInfo method) + { + SourceGeneratedReflectionDataProvider data = DataProvider.GetSnapshot(); + return data.TypeMethodAttributes.TryGetValue(method, out Attribute[]? attributes) + ? attributes + : GetMemberAttributesFromReflection(method); + } + + // Bypass _fallback.GetCustomAttributesCached because its internal NotCachedReflectionAccessor + // routes through PlatformServiceProvider.Instance.ReflectionOperations, which after SetMetadata + // resolves back to this SourceGeneratedReflectionOperations instance — causing infinite mutual recursion. + // Use direct reflection (_fallback.GetCustomAttributes does not go through that indirection). + private Attribute[] GetMemberAttributesFromReflection(MemberInfo memberInfo) + { + object[]? attributes = _fallback.GetCustomAttributes(memberInfo); + return attributes switch + { + null => [], + Attribute[] attributeArray => attributeArray, + _ => [.. attributes.OfType()], + }; + } + + private Attribute[] GetAssemblyAttributesFromReflection(Assembly assembly) + { + object[] attributes = _fallback.GetCustomAttributes(assembly, typeof(Attribute)); + return attributes is Attribute[] attributeArray + ? attributeArray + : [.. attributes.OfType()]; + } +} diff --git a/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs b/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs index 0b77d20fe0..6774a4ef0b 100644 --- a/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs +++ b/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs @@ -73,4 +73,5 @@ internal static class DiagnosticIds public const string IgnoreShouldHaveJustificationRuleId = "MSTEST0066"; public const string AvoidThreadSleepAndTaskWaitInTestsRuleId = "MSTEST0067"; public const string CollectionAssertToAssertRuleId = "MSTEST0068"; + // public const string InheritedTestClassAttributeWithSourceGeneratorRuleId = "MSTEST0069"; - // Reserved. Owned by MSTest.SourceGeneration analyzer; don't reuse this ID. } diff --git a/src/Analyzers/MSTest.SourceGeneration/.editorconfig b/src/Analyzers/MSTest.SourceGeneration/.editorconfig deleted file mode 100644 index 7d7bac49f4..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*.{cs,vb}] -file_header_template = Copyright (c) Microsoft Corporation. All rights reserved.\nLicensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. diff --git a/src/Analyzers/MSTest.SourceGeneration/AnalyzerReleases.Shipped.md b/src/Analyzers/MSTest.SourceGeneration/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000000..f50bb1fe21 --- /dev/null +++ b/src/Analyzers/MSTest.SourceGeneration/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/src/Analyzers/MSTest.SourceGeneration/AnalyzerReleases.Unshipped.md b/src/Analyzers/MSTest.SourceGeneration/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000000..50c9d1883f --- /dev/null +++ b/src/Analyzers/MSTest.SourceGeneration/AnalyzerReleases.Unshipped.md @@ -0,0 +1,8 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +MSTEST0069 | Usage | Warning | InheritedTestClassAttributeWithSourceGeneratorAnalyzer diff --git a/src/Analyzers/MSTest.SourceGeneration/Analyzers/InheritedTestClassAttributeWithSourceGeneratorAnalyzer.cs b/src/Analyzers/MSTest.SourceGeneration/Analyzers/InheritedTestClassAttributeWithSourceGeneratorAnalyzer.cs new file mode 100644 index 0000000000..87beeb8302 --- /dev/null +++ b/src/Analyzers/MSTest.SourceGeneration/Analyzers/InheritedTestClassAttributeWithSourceGeneratorAnalyzer.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Analyzers; + +/// +/// MSTEST0069: warns when a class is intended as an MSTest test class via an inherited +/// [TestClass] attribute. The MSTest source generator only enumerates types that +/// declare [TestClass] directly on themselves (it uses +/// SyntaxValueProvider.ForAttributeWithMetadataName which does not follow inheritance), +/// so any test class that relies on an inherited [TestClass] will be silently skipped +/// from source-generated discovery and will not be runnable when the source-generated +/// provider is the active reflection backend. +/// +/// +/// This analyzer ships in the MSTest.SourceGeneration package, so it is only loaded for +/// projects that have opted into source generation. The diagnostic ID MSTEST0069 is +/// reserved in MSTest.Analyzers.Helpers.DiagnosticIds to avoid collisions with the +/// regular MSTest analyzers package. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InheritedTestClassAttributeWithSourceGeneratorAnalyzer : DiagnosticAnalyzer +{ + /// The diagnostic id reported by this analyzer. + public const string DiagnosticId = "MSTEST0069"; + + private const string TestClassAttributeMetadataName = "Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute"; + + private static readonly DiagnosticDescriptor Rule = new( + id: DiagnosticId, + title: "Inherited [TestClass] is ignored by the MSTest source generator", + messageFormat: "Class '{0}' inherits the [TestClass] attribute from base class '{1}'. The MSTest source generator only discovers types that declare [TestClass] directly; apply [TestClass] to '{0}' so it is discovered when source-generated discovery is active.", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "The MSTest source generator uses ForAttributeWithMetadataName to enumerate test classes, which does not follow inheritance. A class that relies on inheriting [TestClass] from a base type will be silently skipped from discovery in source-generation mode. Apply [TestClass] directly to the derived class to keep it discoverable.", + helpLinkUri: "https://aka.ms/mstest/analyzers"); + + /// + public override ImmutableArray SupportedDiagnostics { get; } + = ImmutableArray.Create(Rule); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationContext => + { + INamedTypeSymbol? testClassAttribute = compilationContext.Compilation.GetTypeByMetadataName(TestClassAttributeMetadataName); + if (testClassAttribute is null) + { + return; + } + + compilationContext.RegisterSymbolAction( + symbolContext => AnalyzeNamedType(symbolContext, testClassAttribute), + SymbolKind.NamedType); + }); + } + + private static void AnalyzeNamedType(SymbolAnalysisContext context, INamedTypeSymbol testClassAttribute) + { + var type = (INamedTypeSymbol)context.Symbol; + + // Mirror the source generator's filtering: it also skips static/abstract types, open + // generic types, and `file`-local types. Applying [TestClass] directly to any of those + // would not make them discoverable, so a MSTEST0069 warning would be actively misleading. + if (type.TypeKind != TypeKind.Class + || type.IsStatic + || type.IsAbstract + || type.IsImplicitClass + || type.IsGenericType + || type.IsFileLocal) + { + return; + } + + if (HasDirectAttribute(type, testClassAttribute)) + { + return; + } + + INamedTypeSymbol? baseWithAttribute = FindBaseTypeWithAttribute(type.BaseType, testClassAttribute); + if (baseWithAttribute is null) + { + return; + } + + Location location = type.Locations.Length > 0 ? type.Locations[0] : Location.None; + context.ReportDiagnostic(Diagnostic.Create( + Rule, + location, + type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + baseWithAttribute.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + + private static bool HasDirectAttribute(ISymbol symbol, INamedTypeSymbol attributeType) + => symbol.GetAttributes().Any(attribute => IsOrInheritsFrom(attribute.AttributeClass, attributeType)); + + private static INamedTypeSymbol? FindBaseTypeWithAttribute(INamedTypeSymbol? baseType, INamedTypeSymbol attributeType) + { + for (INamedTypeSymbol? current = baseType; current is not null; current = current.BaseType) + { + if (HasDirectAttribute(current, attributeType)) + { + return current; + } + } + + return null; + } + + private static bool IsOrInheritsFrom(INamedTypeSymbol? candidate, INamedTypeSymbol target) + { + for (INamedTypeSymbol? current = candidate; current is not null; current = current.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(current, target)) + { + return true; + } + } + + return false; + } +} diff --git a/src/Analyzers/MSTest.SourceGeneration/BannedSymbols.txt b/src/Analyzers/MSTest.SourceGeneration/BannedSymbols.txt index b46f825771..0b910a6768 100644 --- a/src/Analyzers/MSTest.SourceGeneration/BannedSymbols.txt +++ b/src/Analyzers/MSTest.SourceGeneration/BannedSymbols.txt @@ -1,2 +1,2 @@ -M:System.Text.StringBuilder.AppendLine(); Calls to 'Environment.NewLine' should be avoided in source generators +M:System.Text.StringBuilder.AppendLine(); Calls to 'Environment.NewLine' should be avoided in source generators M:System.Text.StringBuilder.AppendLine(System.String); Calls to 'Environment.NewLine' should be avoided in source generators diff --git a/src/Analyzers/MSTest.SourceGeneration/Emitters/ReflectionMetadataEmitter.cs b/src/Analyzers/MSTest.SourceGeneration/Emitters/ReflectionMetadataEmitter.cs new file mode 100644 index 0000000000..241b89aa49 --- /dev/null +++ b/src/Analyzers/MSTest.SourceGeneration/Emitters/ReflectionMetadataEmitter.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Models; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Emitters; + +/// +/// Produces the C# source that wires the test assembly into MSTest's source-generator runtime. +/// +internal static class ReflectionMetadataEmitter +{ + public const string GeneratedTypeName = "MSTestSourceGeneratedReflectionMetadata"; + + public static string Emit(TestAssemblyMetadata metadata) + { + var sb = new StringBuilder(); + + Append(sb, "// "); + Append(sb, "// This file was generated by the MSTest source generator."); + Append(sb, "// Do not edit it manually; changes will be lost on the next build."); + Append(sb, "#nullable enable"); + Append(sb, string.Empty); + Append(sb, "using System;"); + Append(sb, "using System.Collections.Generic;"); + Append(sb, "using System.Diagnostics.CodeAnalysis;"); + Append(sb, "using System.Reflection;"); + Append(sb, "using System.Runtime.CompilerServices;"); + Append(sb, string.Empty); + Append(sb, "namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Generated"); + Append(sb, "{"); + Append(sb, " /// Source-generated MSTest reflection metadata hook for this test assembly."); + Append(sb, $" internal static class {GeneratedTypeName}"); + Append(sb, " {"); + Append(sb, " [ModuleInitializer]"); + var emittedBaseTypeDependencies = new HashSet(StringComparer.Ordinal); + foreach (TestClassMetadata cls in metadata.Classes) + { + // Preserve the test class members at runtime even when the assembly is published with + // Native AOT / IL trimming. MSTest's adapter still needs to call GetConstructors, + // GetProperties and other reflection APIs on the class (the source generator only + // populates TypeMethods; constructors, properties, etc. fall back to runtime reflection). + // Without this hint the trimmer removes those members and discovery fails with + // "Cannot find a valid constructor for test class". + Append(sb, $" [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof({cls.FullyQualifiedName}))]"); + } + + // Emit a [DynamicDependency] for every accessible non-generic base type of every + // discovered [TestClass]. This roots members declared on the abstract base — most + // importantly [ClassInitialize] / [ClassCleanup] / [AssemblyInitialize] / [AssemblyCleanup] + // and any [TestContext] property — so the trimmer / Native AOT does not remove them. + // Dedupe across classes so a base shared by many derived classes is emitted once. + foreach (TestClassMetadata cls in metadata.Classes) + { + foreach (string baseTypeName in cls.BaseTypeFullyQualifiedNames) + { + if (emittedBaseTypeDependencies.Add(baseTypeName)) + { + Append(sb, $" [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof({baseTypeName}))]"); + } + } + } + + Append(sb, " internal static void Initialize()"); + Append(sb, " {"); + Append(sb, $" var assembly = typeof({GeneratedTypeName}).Assembly;"); + Append(sb, $" var types = {EmitTypesExpression(metadata)};"); + EmitTestMethodsLocal(sb, metadata); + Append(sb, $" {Constants.ReflectionMetadataHookFullName}.Register(assembly, types, testMethods);"); + Append(sb, " }"); + Append(sb, string.Empty); + Append(sb, " private static MethodInfo ResolveMethod([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string name, Type[] parameterTypes)"); + Append(sb, " {"); + // Include inherited methods (no DeclaredOnly) so test methods defined on an abstract base + // class are resolvable through the concrete test class's typeof(). + Append(sb, " const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;"); + Append(sb, " foreach (MethodInfo candidate in type.GetMethods(flags))"); + Append(sb, " {"); + Append(sb, " if (candidate.Name != name)"); + Append(sb, " {"); + Append(sb, " continue;"); + Append(sb, " }"); + Append(sb, string.Empty); + Append(sb, " ParameterInfo[] candidateParameters = candidate.GetParameters();"); + Append(sb, " if (candidateParameters.Length != parameterTypes.Length)"); + Append(sb, " {"); + Append(sb, " continue;"); + Append(sb, " }"); + Append(sb, string.Empty); + Append(sb, " bool match = true;"); + Append(sb, " for (int i = 0; i < candidateParameters.Length; i++)"); + Append(sb, " {"); + Append(sb, " if (candidateParameters[i].ParameterType != parameterTypes[i])"); + Append(sb, " {"); + Append(sb, " match = false;"); + Append(sb, " break;"); + Append(sb, " }"); + Append(sb, " }"); + Append(sb, string.Empty); + Append(sb, " if (match)"); + Append(sb, " {"); + Append(sb, " return candidate;"); + Append(sb, " }"); + Append(sb, " }"); + Append(sb, string.Empty); + Append(sb, " throw new MissingMethodException(type.FullName, name);"); + Append(sb, " }"); + Append(sb, " }"); + Append(sb, "}"); + + return sb.ToString(); + } + + private static string EmitTypesExpression(TestAssemblyMetadata metadata) + { + if (metadata.Classes.Count == 0) + { + return "Array.Empty()"; + } + + var sb = new StringBuilder(); + sb.Append("new Type[]").Append(Constants.NewLine); + sb.Append(" {").Append(Constants.NewLine); + foreach (TestClassMetadata cls in metadata.Classes) + { + sb.Append($" typeof({cls.FullyQualifiedName}),").Append(Constants.NewLine); + } + + sb.Append(" }"); + return sb.ToString(); + } + + private static void EmitTestMethodsLocal(StringBuilder sb, TestAssemblyMetadata metadata) + { + if (metadata.Classes.Count == 0) + { + Append(sb, " var testMethods = new Dictionary();"); + return; + } + + Append(sb, " var testMethods = new Dictionary"); + Append(sb, " {"); + foreach (TestClassMetadata cls in metadata.Classes) + { + Append(sb, $" [typeof({cls.FullyQualifiedName})] = new MethodInfo[]"); + Append(sb, " {"); + foreach (TestMethodMetadata method in cls.Methods) + { + string parameterTypesArray; + if (method.ParameterTypes.Count == 0) + { + parameterTypesArray = "Type.EmptyTypes"; + } + else + { + var parts = new List(method.ParameterTypes.Count); + foreach (string parameterType in method.ParameterTypes) + { + parts.Add($"typeof({parameterType})"); + } + + parameterTypesArray = "new Type[] { " + string.Join(", ", parts) + " }"; + } + + Append(sb, $" ResolveMethod(typeof({cls.FullyQualifiedName}), {ToCSharpLiteral(method.Name)}, {parameterTypesArray}),"); + } + + Append(sb, " },"); + } + + Append(sb, " };"); + } + + private static void Append(StringBuilder sb, string line) + { + sb.Append(line); + sb.Append(Constants.NewLine); + } + + private static string ToCSharpLiteral(string value) + => "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; +} diff --git a/src/Analyzers/MSTest.SourceGeneration/Generators/ReflectionMetadataGenerator.cs b/src/Analyzers/MSTest.SourceGeneration/Generators/ReflectionMetadataGenerator.cs new file mode 100644 index 0000000000..cedf0413c8 --- /dev/null +++ b/src/Analyzers/MSTest.SourceGeneration/Generators/ReflectionMetadataGenerator.cs @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Emitters; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Models; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Generators; + +/// +/// Incremental source generator that walks every [TestClass] in the compilation and emits +/// a [ModuleInitializer] that registers a SourceGeneratedReflectionDataProvider with +/// MSTest's runtime hook. When the test project opts in (by referencing this generator), MSTest +/// will read test metadata from the source-generated data instead of using runtime reflection. +/// +[Generator(LanguageNames.CSharp)] +public sealed class ReflectionMetadataGenerator : IIncrementalGenerator +{ + /// + /// Display format for parameter types in typeof(...) expressions. Mirrors + /// but omits UseSpecialTypes so + /// primitive types emit as global::System.Int32 rather than int, ensuring the + /// generated typeof() calls compile in any namespace context. + /// + private static readonly SymbolDisplayFormat ParameterTypeFormat = new( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers); + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider testClasses = context.SyntaxProvider.ForAttributeWithMetadataName( + Constants.TestClassAttributeFullName, + predicate: static (node, _) => node is TypeDeclarationSyntax, + transform: static (ctx, ct) => BuildTestClass(ctx, ct)); + + IncrementalValueProvider> collected = testClasses.Collect(); + + IncrementalValueProvider<(string? AssemblyName, ImmutableArray Classes)> source = + context.CompilationProvider + .Select(static (compilation, _) => compilation.AssemblyName) + .Combine(collected); + + context.RegisterImplementationSourceOutput(source, EmitMetadata); + } + + private static TestClassMetadata? BuildTestClass(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { + if (context.TargetSymbol is not INamedTypeSymbol typeSymbol) + { + return null; + } + + if (typeSymbol.IsAbstract || typeSymbol.IsStatic) + { + return null; + } + + // Skip open generic test classes: typeof(Generic) is not valid at the module-initializer + // scope where we emit the metadata, and reflecting on an unbound generic type by method + // signature is unreliable. Closed-constructed generics that the user writes are not + // top-level [TestClass] declarations, so they are not seen here either. + if (typeSymbol.IsGenericType) + { + return null; + } + + // Skip types the generated module initializer (emitted as `internal`) cannot reference, + // for example a private/protected nested [TestClass]. Emitting `typeof(Outer.PrivateNested)` + // would fail with CS0122 inside auto-generated code. + if (!IsAccessibleFromGeneratedCode(typeSymbol)) + { + return null; + } + + string fullyQualifiedName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + string? containingNamespace = typeSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns + ? ns.ToDisplayString() + : null; + + ImmutableArray.Builder methods = ImmutableArray.CreateBuilder(); + var seenSignatures = new HashSet(StringComparer.Ordinal); + ImmutableArray.Builder baseTypes = ImmutableArray.CreateBuilder(); + INamedTypeSymbol? currentType = typeSymbol; + while (currentType is not null && currentType.SpecialType != SpecialType.System_Object) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Capture every base type that the generated module initializer can reference, so we + // can root its members (ClassInitialize / ClassCleanup / AssemblyInitialize / + // AssemblyCleanup / TestContext setter) via [DynamicDependency] under trimming or + // Native AOT. Without this, those members live on the abstract base only and the + // trimmer removes them because [DynamicDependency(All, typeof(Concrete))] does not + // preserve base-type members. We intentionally do NOT add the base to types[] or + // testMethods{}; runtime discovery still flows through the concrete [TestClass]. + if (!SymbolEqualityComparer.Default.Equals(currentType, typeSymbol) + && !currentType.IsGenericType + && IsAccessibleFromGeneratedCode(currentType)) + { + baseTypes.Add(currentType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } + + foreach (ISymbol member in currentType.GetMembers()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (member is not IMethodSymbol method || method.MethodKind != MethodKind.Ordinary) + { + continue; + } + + if (!HasTestMethodAttribute(method)) + { + continue; + } + + // Skip generic test methods (e.g. `Test(T value)`). Their parameter types + // include method-level type parameters, and emitting `typeof(T)` inside the + // non-generic module initializer does not compile. Reflection mode handles these + // at runtime, so opting into the generator must not make a valid program fail + // to build. + if (method.IsGenericMethod) + { + continue; + } + + // Skip methods that take ref/out/in/ref-readonly parameters. The runtime + // ResolveMethod compares parameter types by `typeof(T) == ParameterType`, but + // for by-ref parameters `ParameterType` is `T&` (i.e. `MakeByRefType()`), so a + // naive `typeof(T)` match always fails and ResolveMethod would throw inside the + // module initializer — aborting test discovery for the whole assembly. The + // experimental source-gen path doesn't model by-ref signatures yet, so skip. + if (HasByRefParameter(method)) + { + continue; + } + + ImmutableArray.Builder parameterTypes = ImmutableArray.CreateBuilder(method.Parameters.Length); + foreach (IParameterSymbol parameter in method.Parameters) + { + parameterTypes.Add(parameter.Type.ToDisplayString(ParameterTypeFormat)); + } + + ImmutableArray parameterTypeArray = parameterTypes.MoveToImmutable(); + + // Dedupe inherited methods by (name, parameter-signature) so an override is emitted once. + string signature = method.Name + "(" + string.Join(",", parameterTypeArray) + ")"; + if (!seenSignatures.Add(signature)) + { + continue; + } + + methods.Add(new TestMethodMetadata(method.Name, new EquatableArray(parameterTypeArray))); + } + + currentType = currentType.BaseType; + } + + return new TestClassMetadata( + FullyQualifiedName: fullyQualifiedName, + DisplayName: typeSymbol.Name, + Namespace: containingNamespace, + Methods: new EquatableArray(methods.ToImmutable()), + BaseTypeFullyQualifiedNames: new EquatableArray(baseTypes.ToImmutable())); + } + + private static bool HasByRefParameter(IMethodSymbol method) + => method.Parameters.Any(parameter => parameter.RefKind != RefKind.None); + + private static bool IsAccessibleFromGeneratedCode(INamedTypeSymbol type) + { + // The generated module initializer lives in the same assembly, so anything visible at + // `internal` or above works. `NotApplicable` is the default for top-level types and is + // also fine. Anything stricter (private, protected, private protected) is rejected. + for (INamedTypeSymbol? current = type; current is not null; current = current.ContainingType) + { + // `file`-scoped types are only addressable from within their own source file, so the + // generated module initializer (which lives in a different file) cannot reference them. + if (current.IsFileLocal) + { + return false; + } + + switch (current.DeclaredAccessibility) + { + case Accessibility.Private: + case Accessibility.Protected: + case Accessibility.ProtectedAndInternal: + return false; + } + } + + return true; + } + + private static bool HasTestMethodAttribute(IMethodSymbol method) + { + foreach (INamedTypeSymbol? attributeClassCandidate in method.GetAttributes().Select(static a => a.AttributeClass)) + { + INamedTypeSymbol? attributeClass = attributeClassCandidate; + while (attributeClass is not null) + { + if (attributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + == "global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute") + { + return true; + } + + attributeClass = attributeClass.BaseType; + } + } + + return false; + } + + private static void EmitMetadata(SourceProductionContext context, (string? AssemblyName, ImmutableArray Classes) input) + { + string assemblyName = input.AssemblyName ?? "UnknownAssembly"; + ImmutableArray classes = input.Classes.IsDefault + ? ImmutableArray.Empty + : input.Classes.Where(static c => c is not null).Cast().ToImmutableArray(); + + // Skip emission entirely when the compilation has no test classes — there is nothing for + // the runtime hook to register and emitting a [ModuleInitializer] just for that adds cost. + if (classes.IsEmpty) + { + return; + } + + var metadata = new TestAssemblyMetadata( + AssemblyName: assemblyName, + Classes: new EquatableArray(classes)); + + string source = ReflectionMetadataEmitter.Emit(metadata); + context.AddSource($"{assemblyName}{Constants.GeneratedFileSuffix}", source); + } +} diff --git a/src/Analyzers/MSTest.SourceGeneration/Generators/TestNodesGenerator.cs b/src/Analyzers/MSTest.SourceGeneration/Generators/TestNodesGenerator.cs deleted file mode 100644 index a1ca1ed89c..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Generators/TestNodesGenerator.cs +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Testing.Framework.SourceGeneration.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -namespace Microsoft.Testing.Framework.SourceGeneration; - -[Generator] -internal sealed class TestNodesGenerator : IIncrementalGenerator -{ - public void Initialize(IncrementalGeneratorInitializationContext context) - { - IncrementalValuesProvider testClassesProvider = context.SyntaxProvider.ForAttributeWithMetadataName( - "Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute", - static (node, _) => - node is TypeDeclarationSyntax typeDeclarationSyntax - && (typeDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword) || typeDeclarationSyntax.Modifiers.Any(SyntaxKind.InternalKeyword)) - // No static classes. - && !typeDeclarationSyntax.Modifiers.Any(SyntaxKind.StaticKeyword), - static (context, _) => - { - WellKnownTypes wellKnownTypes = new(context.SemanticModel.Compilation); - var testClassInfo = TestTypeInfo.TryBuild(context, wellKnownTypes); - return testClassInfo; - }) - .WhereNotNull(); - - // Generate a file with one static class and one static TestNode field for all public classes we find - context.RegisterImplementationSourceOutput(testClassesProvider, AddTestClassNode); - - IncrementalValueProvider<(string? Left, ImmutableArray Right)> assemblyNamespacesProvider - = context.CompilationProvider.Select((compilation, _) => compilation.AssemblyName) - .Combine(testClassesProvider.Collect()); - - context.RegisterImplementationSourceOutput(assemblyNamespacesProvider, AddAssemblyTestNode); - } - - private static void AddAssemblyTestNode(SourceProductionContext context, (string? AssemblyName, ImmutableArray TestClasses) provider) - { - string assemblyName = provider.AssemblyName ?? ""; - ImmutableArray testClasses = provider.TestClasses; - - var sourceStringBuilder = new IndentedStringBuilder(); - sourceStringBuilder.AppendAutoGeneratedHeader(); - sourceStringBuilder.AppendLine(); - - TestNamespaceInfo[] uniqueUsedNamespaces = [.. testClasses - .Select(x => x.ContainingNamespace) - .Distinct()]; - - string? safeAssemblyName = null; - IDisposable? namespaceBlock = null; - try - { - if (!uniqueUsedNamespaces.Any(x => x.IsGlobalNamespace)) - { - safeAssemblyName = ToSafeNamespace(assemblyName); - - // TODO: We should look for the default namespace, if made visible to the compiler, or default to assembly name. - namespaceBlock = sourceStringBuilder.AppendBlock($"namespace {safeAssemblyName}"); - } - - foreach (TestNamespaceInfo usedNamespace in uniqueUsedNamespaces) - { - if (!usedNamespace.IsGlobalNamespace) - { - sourceStringBuilder.AppendLine($"using {usedNamespace.FullyQualifiedName};"); - } - } - - sourceStringBuilder.AppendLine("using ColGen = global::System.Collections.Generic;"); - sourceStringBuilder.AppendLine("using CA = global::System.Diagnostics.CodeAnalysis;"); - sourceStringBuilder.AppendLine("using Sys = global::System;"); - sourceStringBuilder.AppendLine("using Tasks = global::System.Threading.Tasks;"); - sourceStringBuilder.AppendLine("using Msg = global::Microsoft.Testing.Platform.Extensions.Messages;"); - sourceStringBuilder.AppendLine("using MSTF = global::Microsoft.Testing.Framework;"); - sourceStringBuilder.AppendLine("using Cap = global::Microsoft.Testing.Platform.Capabilities.TestFramework;"); - sourceStringBuilder.AppendLine("using TrxReport = global::Microsoft.Testing.Extensions.TrxReport.Abstractions;"); - sourceStringBuilder.AppendLine(); - - sourceStringBuilder.AppendLine("[CA::ExcludeFromCodeCoverage]"); - using (sourceStringBuilder.AppendBlock("public sealed class SourceGeneratedTestNodesBuilder : MSTF::ITestNodesBuilder")) - { - using (sourceStringBuilder.AppendBlock("private sealed class ClassCapabilities : TrxReport::ITrxReportCapability")) - { - string isTrxReportSupported = testClasses.IsEmpty ? "false" : "true"; - sourceStringBuilder.AppendLine($"bool TrxReport::ITrxReportCapability.IsSupported {{ get; }} = {isTrxReportSupported};"); - sourceStringBuilder.AppendLine("void TrxReport::ITrxReportCapability.Enable() {}"); - } - - sourceStringBuilder.AppendLine(); - sourceStringBuilder.AppendLine("public ColGen::IReadOnlyCollection Capabilities { get; } = new Cap::ITestFrameworkCapability[1] { new ClassCapabilities() };"); - sourceStringBuilder.AppendLine(); - - using (sourceStringBuilder.AppendBlock($"public Tasks::Task BuildAsync(MSTF::ITestSessionContext testSessionContext)")) - { - if (testClasses.IsEmpty) - { - sourceStringBuilder.AppendLine("return Tasks::Task.FromResult(Sys::Array.Empty());"); - } - else - { - AppendAssemblyTestNodeBuilderContent(sourceStringBuilder, assemblyName, testClasses); - } - } - } - } - finally - { - namespaceBlock?.Dispose(); - } - - string code = sourceStringBuilder.ToString(); - // DEBUG: Debug.WriteLine is useful to observe the code when changing the source code generator or applying it to a new test suite. - // VS is caching the generator, so start DebugView++ and just rebuild the TestContainer to make changes, - // and observe the compiler process only (csc.exe). - // Debug.WriteLine(code); - context.AddSource("SourceGeneratedTestNodesBuilder.g.cs", code); - - IndentedStringBuilder hookCode = new(); - hookCode.AppendAutoGeneratedHeader(); - using (hookCode.AppendBlock("namespace Microsoft.Testing.Framework.SourceGeneration")) - { - hookCode.AppendLine("[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"); - using (hookCode.AppendBlock("public static class SourceGeneratedTestingPlatformBuilderHook")) - { - using (hookCode.AppendBlock("public static void AddExtensions(Microsoft.Testing.Platform.Builder.ITestApplicationBuilder testApplicationBuilder, string[] _)")) - { - hookCode.AppendLine("testApplicationBuilder.AddTestFramework(new Microsoft.Testing.Framework.Configurations.TestFrameworkConfiguration(System.Environment.ProcessorCount),"); - hookCode.IndentationLevel++; - hookCode.AppendLine($"new {(safeAssemblyName is not null ? safeAssemblyName + "." : string.Empty)}SourceGeneratedTestNodesBuilder());"); - hookCode.IndentationLevel--; - } - } - } - - // Add a hook to the test platform builder to register the test framework to MSBuild. - context.AddSource("SourceGeneratedTestingPlatformBuilderHook.g.cs", hookCode.ToString()); - } - - private static void AppendAssemblyTestNodeBuilderContent(IndentedStringBuilder sourceStringBuilder, string assemblyName, - ImmutableArray testClasses) - { - Dictionary rootVariablesPerNamespace = []; - int variableIndex = 1; - IEnumerable> classesPerNamespaces = testClasses.GroupBy(x => x.ContainingNamespace); - foreach (IGrouping namespaceClasses in classesPerNamespaces) - { - string namespaceTestsVariableName = $"namespace{variableIndex}Tests"; - rootVariablesPerNamespace.Add(namespaceClasses.Key, namespaceTestsVariableName); - sourceStringBuilder.AppendLine($"ColGen::List {namespaceTestsVariableName} = new();"); - - foreach (TestTypeInfo testClassInfo in namespaceClasses) - { - sourceStringBuilder.AppendLine($"{namespaceTestsVariableName}.Add({testClassInfo.GeneratedTypeName}.TestNode);"); - } - - variableIndex++; - sourceStringBuilder.AppendLine(); - } - - sourceStringBuilder.Append("MSTF::TestNode root = "); - - using (sourceStringBuilder.AppendTestNode(assemblyName, assemblyName, [], ';')) - { - foreach (IGrouping group in classesPerNamespaces) - { - group.Key.AppendNamespaceTestNode(sourceStringBuilder, rootVariablesPerNamespace[group.Key]); - } - } - - sourceStringBuilder.AppendLine(); - sourceStringBuilder.AppendLine("return Tasks::Task.FromResult(new MSTF::TestNode[1] { root });"); - } - - private static void AddTestClassNode(SourceProductionContext context, TestTypeInfo testClassInfo) - { - var sourceStringBuilder = new IndentedStringBuilder(); - sourceStringBuilder.AppendAutoGeneratedHeader(); - sourceStringBuilder.AppendLine(); - - testClassInfo.AppendTestNode(sourceStringBuilder); - - string code = sourceStringBuilder.ToString(); - // DEBUG: Debug.WriteLine is useful to observe the code when changing the source code generator or applying it to a new test suite. - // VS is caching the generator, so start DebugView++ and just rebuild the TestContainer to make changes, - // and observe the compiler process only (csc.exe). - // Debug.WriteLine(code); - context.AddSource($"{testClassInfo.FullyQualifiedName}.g.cs", code); - } - - // Borrowed from https://github.com/dotnet/templating/blob/dad34814012bf29aa35eaf8e8013af4b10b997da/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueForms/DefaultSafeNamespaceValueFormFactory.cs#L10 - internal /* for testing purpose */ static string ToSafeNamespace(string value) - { - const char invalidCharacterReplacement = '_'; - - value = value ?? throw new ArgumentNullException(nameof(value)); - value = value.Trim(); - - StringBuilder safeValueStr = new(value.Length); - - for (int i = 0; i < value.Length; i++) - { - if (i < value.Length - 1 && char.IsSurrogatePair(value[i], value[i + 1])) - { - safeValueStr.Append(invalidCharacterReplacement); - // Skip both chars that make up this symbol. - i++; - continue; - } - - bool isFirstCharacterOfIdentifier = safeValueStr.Length == 0 || safeValueStr[safeValueStr.Length - 1] == '.'; - bool isValidFirstCharacter = UnicodeCharacterUtilities.IsIdentifierStartCharacter(value[i]); - bool isValidPartCharacter = UnicodeCharacterUtilities.IsIdentifierPartCharacter(value[i]); - - if (isFirstCharacterOfIdentifier && !isValidFirstCharacter && isValidPartCharacter) - { - // This character cannot be at the beginning, but is good otherwise. Prefix it with something valid. - safeValueStr.Append(invalidCharacterReplacement); - safeValueStr.Append(value[i]); - } - else if ((isFirstCharacterOfIdentifier && isValidFirstCharacter) - || (!isFirstCharacterOfIdentifier && isValidPartCharacter) - || (safeValueStr.Length > 0 && i < value.Length - 1 && value[i] == '.')) - { - // This character is allowed to be where it is. - safeValueStr.Append(value[i]); - } - else - { - safeValueStr.Append(invalidCharacterReplacement); - } - } - - return safeValueStr.ToString(); - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/GlobalSuppressions.cs b/src/Analyzers/MSTest.SourceGeneration/GlobalSuppressions.cs deleted file mode 100644 index 905a836dd9..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/GlobalSuppressions.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Style", "IDE0056:Use index operator", Justification = "Needed when we build with native AOT because there we force net8 target.", Scope = "member", Target = "~M:Microsoft.Testing.Framework.SourceGeneration.ObjectModels.DataRowTestMethodArgumentsInfo.AppendArguments(Microsoft.Testing.Framework.SourceGeneration.Helpers.IndentedStringBuilder)")] -[assembly: SuppressMessage("Style", "IDE0057:Use range operator", Justification = "Needed when we build with native AOT because there we force net8 target.", Scope = "member", Target = "~M:Microsoft.Testing.Framework.SourceGeneration.ObjectModels.DataRowTestMethodArgumentsInfo.AppendArguments(Microsoft.Testing.Framework.SourceGeneration.Helpers.IndentedStringBuilder)")] diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/AttributeDataExtensions.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/AttributeDataExtensions.cs deleted file mode 100644 index 20dccf541c..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/AttributeDataExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal static class AttributeDataExtensions -{ - public static bool TryGetTestExecutionTimeout(this AttributeData attribute, INamedTypeSymbol? executionTimeoutAttributeSymbol, - INamedTypeSymbol? timeSpanSymbol, out TimeSpan executionTimeout) - { - if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, executionTimeoutAttributeSymbol)) - { - executionTimeout = default; - return false; - } - - return TryGetTimeoutValue(attribute, timeSpanSymbol, out executionTimeout); - } - - private static bool TryGetTimeoutValue(this AttributeData attribute, INamedTypeSymbol? timeSpanSymbol, out TimeSpan timeout) - { - if (attribute.ConstructorArguments.Length == 1 - && attribute.ConstructorArguments[0].Type is { } executionTimeoutCtorArgType - && attribute.ConstructorArguments[0].Value is { } executionTimeoutCtorArgValue) - { - if (executionTimeoutCtorArgType.SpecialType is SpecialType.System_Int32 or SpecialType.System_Int64) - { - timeout = TimeSpan.FromMilliseconds((int)executionTimeoutCtorArgValue); - return true; - } - else if (SymbolEqualityComparer.Default.Equals(executionTimeoutCtorArgType, timeSpanSymbol)) - { - timeout = (TimeSpan)executionTimeoutCtorArgValue; - return true; - } - } - - timeout = default; - return false; - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/Constants.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/Constants.cs index 7a968202d0..970c61fda2 100644 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/Constants.cs +++ b/src/Analyzers/MSTest.SourceGeneration/Helpers/Constants.cs @@ -1,9 +1,19 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.Testing.Framework.SourceGeneration; +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Helpers; internal static class Constants { - public const string NewLine = "\r\n"; + /// + /// Use a constant newline to make the generator output stable across operating systems. + /// + public const string NewLine = "\n"; + + public const string TestClassAttributeFullName = "Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute"; + + public const string ReflectionMetadataHookFullName = + "global::Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.ReflectionMetadataHook"; + + public const string GeneratedFileSuffix = ".MSTestReflectionMetadata.g.cs"; } diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/DisposableAction.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/DisposableAction.cs deleted file mode 100644 index e3a0266363..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/DisposableAction.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal readonly struct DisposableAction : IDisposable -{ - public Action Action { get; } - - public DisposableAction(Action action) => Action = action; - - public void Dispose() => Action(); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/EnumerableExtensions.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/EnumerableExtensions.cs deleted file mode 100644 index 24830dd662..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/EnumerableExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal static class EnumerableExtensions -{ - private static readonly Func NotNullTest = x => x != null; - - public static IEnumerable WhereNotNull(this IEnumerable source) - where T : class - => source.Where((Func)NotNullTest)!; -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/EquatableArray{T}.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/EquatableArray{T}.cs deleted file mode 100644 index cbe7db49a9..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/EquatableArray{T}.cs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -namespace MSTest.SourceGeneration.Helpers; - -// Copy from https://github.com/Sergio0694/ComputeSharp/blob/120aff270539996ef9fc52fe46561d12da0b89d4/src/ComputeSharp.SourceGeneration/Helpers/EquatableArray%7BT%7D.cs - -/// -/// An immutable, equatable array. This is equivalent to but with value equality support. -/// -/// The type of values in the array. -/// The input to wrap. -internal readonly struct EquatableArray(ImmutableArray array) : IEquatable>, IEnumerable - where T : IEquatable -{ - /// - /// The underlying array. - /// - private readonly T[]? _array = Unsafe.As, T[]?>(ref array); - - /// - /// Gets a reference to an item at a specified position within the array. - /// - /// The index of the item to retrieve a reference to. - /// A reference to an item at a specified position within the array. - public ref readonly T this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => ref AsImmutableArray().ItemRef(index); - } - - /// - /// Gets a value indicating whether the current array is empty. - /// - public bool IsEmpty - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => AsImmutableArray().IsEmpty; - } - - /// - /// Gets a value indicating whether the current array is default or empty. - /// - public bool IsDefaultOrEmpty - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => AsImmutableArray().IsDefaultOrEmpty; - } - - /// - /// Gets the length of the current array. - /// - public int Length - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => AsImmutableArray().Length; - } - - /// - public bool Equals(EquatableArray array) - => AsSpan().SequenceEqual(array.AsSpan()); - - public override bool Equals(object? obj) - => obj is EquatableArray array && Equals(this, array); - - /// - public override unsafe int GetHashCode() - { - if (_array is not T[] array) - { - return 0; - } - - HashCode hashCode = default; - - if (typeof(T) == typeof(byte)) - { - ReadOnlySpan span = array; - ref T r0 = ref MemoryMarshal.GetReference(span); - ref byte r1 = ref Unsafe.As(ref r0); - - fixed (byte* p = &r1) - { - ReadOnlySpan bytes = new(p, span.Length); - - hashCode.AddBytes(bytes); - } - } - else - { - foreach (T item in array) - { - hashCode.Add(item); - } - } - - return hashCode.ToHashCode(); - } - - /// - /// Gets an instance from the current . - /// - /// The from the current . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ImmutableArray AsImmutableArray() - => Unsafe.As>(ref Unsafe.AsRef(in _array)); - - /// - /// Creates an instance from a given . - /// - /// The input instance. - /// An instance from a given . - public static EquatableArray FromImmutableArray(ImmutableArray array) - => new(array); - - /// - /// Returns a wrapping the current items. - /// - /// A wrapping the current items. - public ReadOnlySpan AsSpan() - => AsImmutableArray().AsSpan(); - - /// - /// Copies the contents of this instance. to a mutable array. - /// - /// The newly instantiated array. - public T[] ToArray() - => [.. AsImmutableArray()]; - - /// - /// Gets an value to traverse items in the current array. - /// - /// An value to traverse items in the current array. - public ImmutableArray.Enumerator GetEnumerator() - => AsImmutableArray().GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() - => ((IEnumerable)AsImmutableArray()).GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() - => ((IEnumerable)AsImmutableArray()).GetEnumerator(); - - /// - /// Implicitly converts an to . - /// - /// An instance from a given . - public static implicit operator EquatableArray(ImmutableArray array) => FromImmutableArray(array); - - /// - /// Implicitly converts an to . - /// - /// An instance from a given . - public static implicit operator ImmutableArray(EquatableArray array) => array.AsImmutableArray(); - - /// - /// Checks whether two values are the same. - /// - /// The first value. - /// The second value. - /// Whether and are equal. - public static bool operator ==(EquatableArray left, EquatableArray right) => left.Equals(right); - - /// - /// Checks whether two values are not the same. - /// - /// The first value. - /// The second value. - /// Whether and are not equal. - public static bool operator !=(EquatableArray left, EquatableArray right) => !left.Equals(right); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/ExceptionUtils.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/ExceptionUtils.cs deleted file mode 100644 index 084f032795..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/ExceptionUtils.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal static class ApplicationStateGuard -{ - internal static InvalidOperationException Unreachable([CallerFilePath] string? path = null, [CallerLineNumber] int line = 0) - => new($"This program location is thought to be unreachable. File='{path}' Line={line}"); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/FileHeaderUtils.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/FileHeaderUtils.cs deleted file mode 100644 index 3474a7585e..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/FileHeaderUtils.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal static class FileHeaderUtils -{ - public static void AppendAutoGeneratedHeader(this IndentedStringBuilder stringBuilder) - { - stringBuilder.AppendLine("//------------------------------------------------------------------------------"); - stringBuilder.AppendLine("// "); - stringBuilder.AppendLine("// This code was generated by Microsoft Testing Framework Generator."); - stringBuilder.AppendLine("// "); - stringBuilder.AppendLine("//------------------------------------------------------------------------------"); - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/HashCode.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/HashCode.cs deleted file mode 100644 index ec41552ded..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/HashCode.cs +++ /dev/null @@ -1,488 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.ComponentModel; -using System.Security.Cryptography; - -namespace System; - -/// -/// A polyfill type that mirrors some methods from on .7. -/// -internal struct HashCode -{ - private const uint Prime1 = 2654435761U; - private const uint Prime2 = 2246822519U; - private const uint Prime3 = 3266489917U; - private const uint Prime4 = 668265263U; - private const uint Prime5 = 374761393U; - - private static readonly uint Seed = GenerateGlobalSeed(); - - private uint _v1; - private uint _v2; - private uint _v3; - private uint _v4; - private uint _queue1; - private uint _queue2; - private uint _queue3; - private uint _length; - - /// - /// Initializes the default seed. - /// - /// A random seed. - private static unsafe uint GenerateGlobalSeed() - { - byte[] bytes = new byte[4]; - - RandomNumberGenerator.Create().GetBytes(bytes); - - return BitConverter.ToUInt32(bytes, 0); - } - - /// - /// Combines a value into a hash code. - /// - /// The type of the value to combine into the hash code. - /// The value to combine into the hash code. - /// The hash code that represents the value. - public static int Combine(T1 value) - { - uint hc1 = (uint)(value?.GetHashCode() ?? 0); - uint hash = MixEmptyState(); - - hash += 4; - hash = QueueRound(hash, hc1); - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines two values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hash = MixEmptyState(); - - hash += 8; - hash = QueueRound(hash, hc1); - hash = QueueRound(hash, hc2); - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines three values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The type of the third value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The third value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2, T3 value3) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hc3 = (uint)(value3?.GetHashCode() ?? 0); - uint hash = MixEmptyState(); - - hash += 12; - hash = QueueRound(hash, hc1); - hash = QueueRound(hash, hc2); - hash = QueueRound(hash, hc3); - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines four values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The type of the third value to combine into the hash code. - /// The type of the fourth value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The third value to combine into the hash code. - /// The fourth value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hc3 = (uint)(value3?.GetHashCode() ?? 0); - uint hc4 = (uint)(value4?.GetHashCode() ?? 0); - - Initialize(out uint v1, out uint v2, out uint v3, out uint v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - uint hash = MixState(v1, v2, v3, v4); - - hash += 16; - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines five values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The type of the third value to combine into the hash code. - /// The type of the fourth value to combine into the hash code. - /// The type of the fifth value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The third value to combine into the hash code. - /// The fourth value to combine into the hash code. - /// The fifth value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hc3 = (uint)(value3?.GetHashCode() ?? 0); - uint hc4 = (uint)(value4?.GetHashCode() ?? 0); - uint hc5 = (uint)(value5?.GetHashCode() ?? 0); - - Initialize(out uint v1, out uint v2, out uint v3, out uint v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - uint hash = MixState(v1, v2, v3, v4); - - hash += 20; - hash = QueueRound(hash, hc5); - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines six values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The type of the third value to combine into the hash code. - /// The type of the fourth value to combine into the hash code. - /// The type of the fifth value to combine into the hash code. - /// The type of the sixth value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The third value to combine into the hash code. - /// The fourth value to combine into the hash code. - /// The fifth value to combine into the hash code. - /// The sixth value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hc3 = (uint)(value3?.GetHashCode() ?? 0); - uint hc4 = (uint)(value4?.GetHashCode() ?? 0); - uint hc5 = (uint)(value5?.GetHashCode() ?? 0); - uint hc6 = (uint)(value6?.GetHashCode() ?? 0); - - Initialize(out uint v1, out uint v2, out uint v3, out uint v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - uint hash = MixState(v1, v2, v3, v4); - - hash += 24; - hash = QueueRound(hash, hc5); - hash = QueueRound(hash, hc6); - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines seven values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The type of the third value to combine into the hash code. - /// The type of the fourth value to combine into the hash code. - /// The type of the fifth value to combine into the hash code. - /// The type of the sixth value to combine into the hash code. - /// The type of the seventh value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The third value to combine into the hash code. - /// The fourth value to combine into the hash code. - /// The fifth value to combine into the hash code. - /// The sixth value to combine into the hash code. - /// The seventh value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hc3 = (uint)(value3?.GetHashCode() ?? 0); - uint hc4 = (uint)(value4?.GetHashCode() ?? 0); - uint hc5 = (uint)(value5?.GetHashCode() ?? 0); - uint hc6 = (uint)(value6?.GetHashCode() ?? 0); - uint hc7 = (uint)(value7?.GetHashCode() ?? 0); - - Initialize(out uint v1, out uint v2, out uint v3, out uint v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - uint hash = MixState(v1, v2, v3, v4); - - hash += 28; - hash = QueueRound(hash, hc5); - hash = QueueRound(hash, hc6); - hash = QueueRound(hash, hc7); - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Combines eight values into a hash code. - /// - /// The type of the first value to combine into the hash code. - /// The type of the second value to combine into the hash code. - /// The type of the third value to combine into the hash code. - /// The type of the fourth value to combine into the hash code. - /// The type of the fifth value to combine into the hash code. - /// The type of the sixth value to combine into the hash code. - /// The type of the seventh value to combine into the hash code. - /// The type of the eighth value to combine into the hash code. - /// The first value to combine into the hash code. - /// The second value to combine into the hash code. - /// The third value to combine into the hash code. - /// The fourth value to combine into the hash code. - /// The fifth value to combine into the hash code. - /// The sixth value to combine into the hash code. - /// The seventh value to combine into the hash code. - /// The eighth value to combine into the hash code. - /// The hash code that represents the values. - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) - { - uint hc1 = (uint)(value1?.GetHashCode() ?? 0); - uint hc2 = (uint)(value2?.GetHashCode() ?? 0); - uint hc3 = (uint)(value3?.GetHashCode() ?? 0); - uint hc4 = (uint)(value4?.GetHashCode() ?? 0); - uint hc5 = (uint)(value5?.GetHashCode() ?? 0); - uint hc6 = (uint)(value6?.GetHashCode() ?? 0); - uint hc7 = (uint)(value7?.GetHashCode() ?? 0); - uint hc8 = (uint)(value8?.GetHashCode() ?? 0); - - Initialize(out uint v1, out uint v2, out uint v3, out uint v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - v1 = Round(v1, hc5); - v2 = Round(v2, hc6); - v3 = Round(v3, hc7); - v4 = Round(v4, hc8); - - uint hash = MixState(v1, v2, v3, v4); - - hash += 32; - hash = MixFinal(hash); - - return (int)hash; - } - - /// - /// Adds a single value to the current hash. - /// - /// The type of the value to add into the hash code. - /// The value to add into the hash code. - public void Add(T value) - => Add(value?.GetHashCode() ?? 0); - - /// - /// Adds a single value to the current hash. - /// - /// The type of the value to add into the hash code. - /// The value to add into the hash code. - /// The instance to use. - public void Add(T value, IEqualityComparer? comparer) - => Add(value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); - - /// - /// Adds a span of bytes to the hash code. - /// - /// The span. - public void AddBytes(ReadOnlySpan value) - { - ref byte pos = ref MemoryMarshal.GetReference(value); - ref byte end = ref Unsafe.Add(ref pos, value.Length); - - while ((nint)Unsafe.ByteOffset(ref pos, ref end) >= sizeof(int)) - { - Add(Unsafe.ReadUnaligned(ref pos)); - pos = ref Unsafe.Add(ref pos, sizeof(int)); - } - - while (Unsafe.IsAddressLessThan(ref pos, ref end)) - { - Add((int)pos); - pos = ref Unsafe.Add(ref pos, 1); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) - { - v1 = Seed + Prime1 + Prime2; - v2 = Seed + Prime2; - v3 = Seed; - v4 = Seed - Prime1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint Round(uint hash, uint input) - => RotateLeft(hash + (input * Prime2), 13) * Prime1; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint QueueRound(uint hash, uint queuedValue) - => RotateLeft(hash + (queuedValue * Prime3), 17) * Prime4; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint MixState(uint v1, uint v2, uint v3, uint v4) - => RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint MixEmptyState() - => Seed + Prime5; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint MixFinal(uint hash) - { - hash ^= hash >> 15; - hash *= Prime2; - hash ^= hash >> 13; - hash *= Prime3; - hash ^= hash >> 16; - - return hash; - } - - private void Add(int value) - { - uint val = (uint)value; - uint previousLength = _length++; - uint position = previousLength % 4; - - if (position == 0) - { - _queue1 = val; - } - else if (position == 1) - { - _queue2 = val; - } - else if (position == 2) - { - _queue3 = val; - } - else - { - if (previousLength == 3) - { - Initialize(out _v1, out _v2, out _v3, out _v4); - } - - _v1 = Round(_v1, _queue1); - _v2 = Round(_v2, _queue2); - _v3 = Round(_v3, _queue3); - _v4 = Round(_v4, val); - } - } - - /// - /// Gets the resulting hashcode from the current instance. - /// - /// The resulting hashcode from the current instance. - public readonly int ToHashCode() - { - uint length = _length; - uint position = length % 4; - uint hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4); - - hash += length * 4; - - if (position > 0) - { - hash = QueueRound(hash, _queue1); - - if (position > 1) - { - hash = QueueRound(hash, _queue2); - - if (position > 2) - { - hash = QueueRound(hash, _queue3); - } - } - } - - hash = MixFinal(hash); - - return (int)hash; - } - - /// - [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] - [EditorBrowsable(EditorBrowsableState.Never)] -#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member - public override readonly int GetHashCode() -#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member - => throw new NotSupportedException(); - - /// - [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] - [EditorBrowsable(EditorBrowsableState.Never)] -#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member - public override readonly bool Equals(object? obj) -#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member - => throw new NotSupportedException(); - - /// - /// Rotates the specified value left by the specified number of bits. - /// Similar in behavior to the x86 instruction ROL. - /// - /// The value to rotate. - /// The number of bits to rotate by. - /// Any value outside the range [0..31] is treated as congruent mod 32. - /// The rotated value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint RotateLeft(uint value, int offset) - => (value << offset) | (value >> (32 - offset)); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/IMethodSymbolExtensions.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/IMethodSymbolExtensions.cs deleted file mode 100644 index 344b0a3da7..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/IMethodSymbolExtensions.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Testing.Framework.SourceGeneration; - -internal static class IMethodSymbolExtensions -{/// - /// Checks if the given method implements or overrides an implementation of . - /// - public static bool IsDisposeImplementation(this IMethodSymbol? method, INamedTypeSymbol? iDisposable) - { - if (method is null) - { - return false; - } - - if (method.IsOverride) - { - return method.OverriddenMethod.IsDisposeImplementation(iDisposable); - } - - // Identify the implementor of IDisposable.Dispose in the given method's containing type and check - // if it is the given method. - return method.ReturnsVoid - && method.Parameters.IsEmpty - && method.IsImplementationOfInterfaceMethod(null, iDisposable, "Dispose"); - } - - /// - /// Checks if the given method implements "IAsyncDisposable.Dispose" or overrides an implementation of "IAsyncDisposable.Dispose". - /// - public static bool IsAsyncDisposeImplementation(this IMethodSymbol? method, INamedTypeSymbol? iAsyncDisposable, INamedTypeSymbol? valueTaskType) - { - if (method is null) - { - return false; - } - - if (method.IsOverride) - { - return method.OverriddenMethod.IsAsyncDisposeImplementation(iAsyncDisposable, valueTaskType); - } - - // Identify the implementor of IAsyncDisposable.Dispose in the given method's containing type and check - // if it is the given method. - return SymbolEqualityComparer.Default.Equals(method.ReturnType, valueTaskType) - && method.Parameters.IsEmpty - && method.IsImplementationOfInterfaceMethod(null, iAsyncDisposable, "DisposeAsync"); - } - - /// - /// Checks if the given method is an implementation of the given interface method - /// Substituted with the given typeargument. - /// - public static bool IsImplementationOfInterfaceMethod(this IMethodSymbol method, ITypeSymbol? typeArgument, INamedTypeSymbol? interfaceType, string interfaceMethodName) - { - INamedTypeSymbol? constructedInterface = typeArgument != null ? interfaceType?.Construct(typeArgument) : interfaceType; - - return constructedInterface?.GetMembers(interfaceMethodName).FirstOrDefault() is IMethodSymbol interfaceMethod - && SymbolEqualityComparer.Default.Equals(method, method.ContainingType.FindImplementationForInterfaceMember(interfaceMethod)); - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/ISymbolExtensions.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/ISymbolExtensions.cs deleted file mode 100644 index a8a4823ca9..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/ISymbolExtensions.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; - -namespace Analyzers.Utilities; - -internal static class ISymbolExtensions -{ - public static SymbolVisibility GetResultantVisibility(this ISymbol symbol) - { - // Start by assuming it's visible. - SymbolVisibility visibility = SymbolVisibility.Public; - - switch (symbol.Kind) - { - case SymbolKind.Alias: - // Aliases are uber private. They're only visible in the same file that they - // were declared in. - return SymbolVisibility.Private; - - case SymbolKind.Parameter: - // Parameters are only as visible as their containing symbol - return symbol.ContainingSymbol.GetResultantVisibility(); - - case SymbolKind.TypeParameter: - // Type Parameters are private. - return SymbolVisibility.Private; - } - - while (symbol != null && symbol.Kind != SymbolKind.Namespace) - { - switch (symbol.DeclaredAccessibility) - { - // If we see anything private, then the symbol is private. - case Accessibility.NotApplicable: - case Accessibility.Private: - return SymbolVisibility.Private; - - // If we see anything internal, then knock it down from public to - // internal. - case Accessibility.Internal: - case Accessibility.ProtectedAndInternal: - visibility = SymbolVisibility.Internal; - break; - - // For anything else (Public, Protected, ProtectedOrInternal), the - // symbol stays at the level we've gotten so far. - } - - symbol = symbol.ContainingSymbol; - } - - return visibility; - } - - public static ITypeSymbol? GetMemberType(this ISymbol? symbol) - => symbol switch - { - IEventSymbol eventSymbol => eventSymbol.Type, - IFieldSymbol fieldSymbol => fieldSymbol.Type, - IMethodSymbol methodSymbol => methodSymbol.ReturnType, - IPropertySymbol propertySymbol => propertySymbol.Type, - _ => null, - }; -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/ITypeSymbolExtensions.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/ITypeSymbolExtensions.cs deleted file mode 100644 index 0af0a3975a..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/ITypeSymbolExtensions.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; - -namespace Analyzers.Utilities; - -internal static class ITypeSymbolExtensions -{ - /// - /// Returns the members of the given type, including inherited members. - /// - public static IEnumerable> GetAllMembers(this INamedTypeSymbol symbol) - { - INamedTypeSymbol? currentSymbol = symbol; - - while (currentSymbol != null) - { - yield return currentSymbol.GetMembers(); - currentSymbol = currentSymbol.BaseType; - } - } - - public static IEnumerable> GetAllMembers(this INamedTypeSymbol symbol, string name) - { - INamedTypeSymbol? currentSymbol = symbol; - - while (currentSymbol != null) - { - yield return currentSymbol.GetMembers(name); - currentSymbol = currentSymbol.BaseType; - } - } - - public static bool Inherits( - [NotNullWhen(returnValue: true)] this ITypeSymbol? type, - [NotNullWhen(returnValue: true)] ITypeSymbol? possibleBase) - { - if (type == null || possibleBase == null) - { - return false; - } - - switch (possibleBase.TypeKind) - { - case TypeKind.Class: - if (type.TypeKind == TypeKind.Interface) - { - return false; - } - - return DerivesFrom(type, possibleBase, baseTypesOnly: true); - - case TypeKind.Interface: - return DerivesFrom(type, possibleBase); - - default: - return false; - } - } - - public static bool DerivesFrom( - [NotNullWhen(returnValue: true)] this ITypeSymbol? symbol, - [NotNullWhen(returnValue: true)] ITypeSymbol? candidateBaseType, - bool baseTypesOnly = false, - bool checkTypeParameterConstraints = true) - { - if (candidateBaseType == null || symbol == null) - { - return false; - } - - if (!baseTypesOnly && candidateBaseType.TypeKind == TypeKind.Interface) - { - IEnumerable allInterfaces = symbol.AllInterfaces.OfType(); - if (SymbolEqualityComparer.Default.Equals(candidateBaseType.OriginalDefinition, candidateBaseType)) - { - // Candidate base type is not a constructed generic type, so use original definition for interfaces. - allInterfaces = allInterfaces.Select(i => i.OriginalDefinition); - } - - if (allInterfaces.Contains(candidateBaseType, SymbolEqualityComparer.Default)) - { - return true; - } - } - - if (checkTypeParameterConstraints && symbol.TypeKind == TypeKind.TypeParameter) - { - var typeParameterSymbol = (ITypeParameterSymbol)symbol; - foreach (ITypeSymbol constraintType in typeParameterSymbol.ConstraintTypes) - { - if (constraintType.DerivesFrom(candidateBaseType, baseTypesOnly, checkTypeParameterConstraints)) - { - return true; - } - } - } - - while (symbol != null) - { - if (SymbolEqualityComparer.Default.Equals(symbol, candidateBaseType)) - { - return true; - } - - symbol = symbol.BaseType; - } - - return false; - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/IncrementalValuesProviderExtensions.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/IncrementalValuesProviderExtensions.cs deleted file mode 100644 index bef8c4073b..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/IncrementalValuesProviderExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal static class IncrementalValuesProviderExtensions -{ - private static readonly Func NotNullTest = x => x != null; - - public static IncrementalValuesProvider WhereNotNull(this IncrementalValuesProvider source) - where T : class - => source.Where(NotNullTest)!; -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/IndentedStringBuilder.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/IndentedStringBuilder.cs index a9b2c407f4..6b7937ade5 100644 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/IndentedStringBuilder.cs +++ b/src/Analyzers/MSTest.SourceGeneration/Helpers/IndentedStringBuilder.cs @@ -1,8 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Helpers; +/// +/// Small helper that produces indented source text. Mirrors the helper used by the existing +/// MSTest.SourceGeneration project so that the generated output is consistent with the rest of +/// the MSTest source generators. +/// internal sealed class IndentedStringBuilder { private readonly StringBuilder _builder = new(); @@ -12,13 +17,6 @@ internal sealed class IndentedStringBuilder public int IndentationLevel { get; internal set; } - public void Append(char value) - { - MaybeAppendIndent(); - _builder.Append(value); - _needsIndent = false; - } - public void Append(string value) { MaybeAppendIndent(); @@ -32,53 +30,54 @@ public void AppendLine() _needsIndent = true; } - public void AppendLine(char value) - { - MaybeAppendIndent().Append(value).Append(Constants.NewLine); - _needsIndent = true; - } - public void AppendLine(string value) { - MaybeAppendIndent().Append(value).Append(Constants.NewLine); + MaybeAppendIndent(); + _builder.Append(value); + _builder.Append(Constants.NewLine); _needsIndent = true; } - public void AppendUnindentedLine(string value) - => _builder.Append(value).Append(Constants.NewLine); - - public IDisposable AppendBlock(string? value = null, char? closingBraceSuffixChar = null) + public IDisposable AppendBlock(string header) { - if (value is not null) - { - AppendLine(value); - } - - AppendLine('{'); + AppendLine(header); + AppendLine("{"); IndentationLevel++; return new DisposableAction(() => { IndentationLevel--; - Append('}'); - if (closingBraceSuffixChar is not null) - { - Append(closingBraceSuffixChar.Value); - } - - AppendLine(); + AppendLine("}"); }); } public override string ToString() => _builder.ToString(); - private StringBuilder MaybeAppendIndent() + private void MaybeAppendIndent() { if (_needsIndent) { _builder.Append(' ', IndentationLevel * 4); + _needsIndent = false; } + } + + private sealed class DisposableAction : IDisposable + { + private readonly Action _action; + private bool _disposed; + + public DisposableAction(Action action) => _action = action; - return _builder; + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _action(); + } } } diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/SymbolVisibility.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/SymbolVisibility.cs deleted file mode 100644 index 026a0adb48..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/SymbolVisibility.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -// Copied from https://github.com/dotnet/sdk/blob/main/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/Extensions/SymbolVisibility.cs -// (previously sourced from dotnet/roslyn-analyzers before that repo was archived). -namespace Analyzers.Utilities; - -#pragma warning disable CA1027 // Mark enums with FlagsAttribute -internal enum SymbolVisibility -#pragma warning restore CA1027 // Mark enums with FlagsAttribute -{ - Public = 0, - Internal = 1, - Private = 2, - Friend = Internal, -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/SystemPolyfills.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/SystemPolyfills.cs deleted file mode 100644 index 6a80a1ca13..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/SystemPolyfills.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -#if !NETCOREAPP -#pragma warning disable SA1403 // File may only contain a single namespace -#pragma warning disable SA1642 // Constructor summary documentation should begin with standard text -#pragma warning disable SA1623 // Property summary documentation should match accessors - -using System.ComponentModel; - -namespace System.Runtime.CompilerServices -{ - [EditorBrowsable(EditorBrowsableState.Never)] - internal static class IsExternalInit; -} - -// This was copied from https://github.com/dotnet/coreclr/blob/60f1e6265bd1039f023a82e0643b524d6aaf7845/src/System.Private.CoreLib/shared/System/Diagnostics/CodeAnalysis/NullableAttributes.cs -// and updated to have the scope of the attributes be internal. -namespace System.Diagnostics.CodeAnalysis -{ - /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] - internal sealed class NotNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - - /// Gets the return value condition. - public bool ReturnValue { get; } - } -} - -#endif diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/TestMethods.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/TestMethods.cs deleted file mode 100644 index 9b605bbc0c..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/TestMethods.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Testing.Framework.SourceGeneration; - -internal static class TestMethods -{ - public static SymbolDisplayFormat MethodIdentifierFullyQualifiedTypeFormat { get; } = - SymbolDisplayFormat.CSharpErrorMessageFormat.WithMiscellaneousOptions( - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | - SymbolDisplayMiscellaneousOptions.UseAsterisksInMultiDimensionalArrays | - SymbolDisplayMiscellaneousOptions.UseErrorTypeSymbolName | - SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); - - public static bool IsValidTestMethodShape(this IMethodSymbol methodSymbol, WellKnownTypes wellKnownTypes) - { - // We only look for public methods - if (methodSymbol.DeclaredAccessibility != Accessibility.Public) - { - return false; - } - - // We don't support generic test methods - if (!methodSymbol.TypeParameters.IsEmpty) - { - return false; - } - - // We don't support static test methods - if (methodSymbol.IsStatic) - { - return false; - } - - // We accept only simple methods - if (methodSymbol.MethodKind != MethodKind.Ordinary - || methodSymbol.IsAbstract - || methodSymbol.IsExtern - || methodSymbol.IsVirtual - || methodSymbol.IsOverride - || methodSymbol.IsImplicitlyDeclared - || methodSymbol.IsPartialDefinition) - { - return false; - } - - // We don't support async void - if (methodSymbol.ReturnsVoid && methodSymbol.IsAsync) - { - return false; - } - - // We support only void and Task return methods - if (!methodSymbol.ReturnsVoid - && !SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, wellKnownTypes.TaskSymbol) - && !SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, wellKnownTypes.ValueTaskSymbol)) - { - return false; - } - - // Method has correct shape to be a test method - return true; - } - - /// - /// Method has a test method shape but is known to not be a test method. - /// - public static bool IsKnownNonTestMethod(this IMethodSymbol methodSymbol, WellKnownTypes wellKnownTypes) - => methodSymbol.IsDisposeImplementation(wellKnownTypes.IDisposableSymbol) - || methodSymbol.IsAsyncDisposeImplementation(wellKnownTypes.IAsyncDisposableSymbol, wellKnownTypes.ValueTaskSymbol) - || methodSymbol.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, wellKnownTypes.IgnoreAttributeSymbol)); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/TestNodeHelpers.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/TestNodeHelpers.cs deleted file mode 100644 index f0d69f2b5e..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/TestNodeHelpers.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -internal static class TestNodeHelpers -{ - public static string GenerateEscapedName(string name) - => name.Replace('.', '_'); - - public static DisposableAction AppendTestNode(this IndentedStringBuilder nodeStringBuilder, string stableUid, string displayName, - ICollection properties, char testNodeBlockSuffixChar = ',') - { - IDisposable testNodeBlock = AppendTestNodeCommonPart(nodeStringBuilder, stableUid, displayName, properties, testNodeBlockSuffixChar); - IDisposable testsBlock = nodeStringBuilder.AppendBlock("Tests = new MSTF::TestNode[]", closingBraceSuffixChar: ','); - - return new DisposableAction(() => - { - testsBlock.Dispose(); - testNodeBlock.Dispose(); - }); - } - - public static DisposableAction AppendTestNode(this IndentedStringBuilder nodeStringBuilder, string stableUid, string displayName, - ICollection properties, string testsVariableName, char testNodeBlockSuffixChar = ',') - { - IDisposable testNodeBlock = AppendTestNodeCommonPart(nodeStringBuilder, stableUid, displayName, properties, testNodeBlockSuffixChar); - nodeStringBuilder.AppendLine($"Tests = {testsVariableName}.ToArray(),"); - - return new DisposableAction(testNodeBlock.Dispose); - } - - private static IDisposable AppendTestNodeCommonPart(IndentedStringBuilder nodeStringBuilder, string stableUid, string displayName, - ICollection properties, char testNodeBlockSuffixChar = ',') - { - IDisposable testNodeBlock = nodeStringBuilder.AppendBlock("new MSTF::TestNode", testNodeBlockSuffixChar); - nodeStringBuilder.AppendLine($"StableUid = \"{stableUid}\","); - nodeStringBuilder.AppendLine($"DisplayName = \"{displayName}\","); - - if (properties.Count > 0) - { - using (nodeStringBuilder.AppendBlock($"Properties = new Msg::IProperty[{properties.Count}]", closingBraceSuffixChar: ',')) - { - foreach (string property in properties) - { - nodeStringBuilder.AppendLine(property); - } - } - } - else - { - nodeStringBuilder.AppendLine("Properties = Sys::Array.Empty(),"); - } - - return testNodeBlock; - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/UnicodeCharacterUtilities.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/UnicodeCharacterUtilities.cs deleted file mode 100644 index 0f5dc97fa8..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/UnicodeCharacterUtilities.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.Helpers; - -/// -/// Defines a set of helper methods to classify Unicode characters. -/// -internal static class UnicodeCharacterUtilities -{ - public static bool IsIdentifierStartCharacter(char ch) - { - // identifier-start-character: - // letter-character - // _ (the underscore character U+005F) - if (ch < 'a') // '\u0061' - { - if (ch < 'A') // '\u0041' - { - return false; - } - - return ch is <= 'Z' // '\u005A' - or '_'; // '\u005F' - } - - if (ch <= 'z') // '\u007A' - { - return true; - } - - if (ch <= '\u007F') // max ASCII - { - return false; - } - - // Check if letter-character - return IsLetterChar(CharUnicodeInfo.GetUnicodeCategory(ch)); - } - - /// - /// Returns true if the Unicode character can be a part of an identifier. - /// - /// The Unicode character. - public static bool IsIdentifierPartCharacter(char ch) - { - // identifier-part-character: - // letter-character - // decimal-digit-character - // connecting-character - // combining-character - // formatting-character - if (ch < 'a') // '\u0061' - { - if (ch < 'A') // '\u0041' - { - return ch is >= '0' // '\u0030' - and <= '9'; // '\u0039' - } - - return ch is <= 'Z' // '\u005A' - or '_'; // '\u005F' - } - - if (ch <= 'z') // '\u007A' - { - return true; - } - - if (ch <= '\u007F') // max ASCII - { - return false; - } - - UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory(ch); - return IsLetterChar(cat) - || IsDecimalDigitChar(cat) - || IsConnectingChar(cat) - || IsCombiningChar(cat) - || IsFormattingChar(cat); - } - - private static bool IsLetterChar(UnicodeCategory cat) - // letter-character: - // A Unicode character of classes Lu, Ll, Lt, Lm, Lo, or Nl - // A Unicode-escape-sequence representing a character of classes Lu, Ll, Lt, Lm, Lo, or Nl - => cat switch - { - UnicodeCategory.UppercaseLetter or UnicodeCategory.LowercaseLetter or UnicodeCategory.TitlecaseLetter or UnicodeCategory.ModifierLetter or UnicodeCategory.OtherLetter or UnicodeCategory.LetterNumber => true, - _ => false, - }; - - private static bool IsCombiningChar(UnicodeCategory cat) - // combining-character: - // A Unicode character of classes Mn or Mc - // A Unicode-escape-sequence representing a character of classes Mn or Mc - => cat switch - { - UnicodeCategory.NonSpacingMark or UnicodeCategory.SpacingCombiningMark => true, - _ => false, - }; - - private static bool IsDecimalDigitChar(UnicodeCategory cat) - // decimal-digit-character: - // A Unicode character of the class Nd - // A unicode-escape-sequence representing a character of the class Nd - => cat == UnicodeCategory.DecimalDigitNumber; - - private static bool IsConnectingChar(UnicodeCategory cat) - // connecting-character: - // A Unicode character of the class Pc - // A unicode-escape-sequence representing a character of the class Pc - => cat == UnicodeCategory.ConnectorPunctuation; - - /// - /// Returns true if the Unicode character is a formatting character (Unicode class Cf). - /// - /// The Unicode character. - private static bool IsFormattingChar(UnicodeCategory cat) - // formatting-character: - // A Unicode character of the class Cf - // A unicode-escape-sequence representing a character of the class Cf - => cat == UnicodeCategory.Format; -} diff --git a/src/Analyzers/MSTest.SourceGeneration/Helpers/WellKnownTypes.cs b/src/Analyzers/MSTest.SourceGeneration/Helpers/WellKnownTypes.cs deleted file mode 100644 index 72e5f2724c..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/Helpers/WellKnownTypes.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Testing.Framework.SourceGeneration; - -/* - * IMPORTANT: Keep the constants, properties and constructor properties assignments in alphabetical order. - */ -internal sealed class WellKnownTypes -{ - private const string MicrosoftVisualStudioTestToolsUnitTestingDataRowAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.DataRowAttribute"; - private const string MicrosoftVisualStudioTestToolsUnitTestingDynamicDataAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataAttribute"; - private const string MicrosoftVisualStudioTestToolsUnitTestingIgnoreAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.IgnoreAttribute"; - private const string MicrosoftTestingFrameworkTestArgumentsEntry1 = "Microsoft.Testing.Framework.InternalUnsafeTestArgumentsEntry`1"; - private const string MicrosoftTestingFrameworkTestExecutionTimeoutAttribute = "Microsoft.Testing.Framework.TestExecutionTimeoutAttribute"; - private const string MicrosoftTestingFrameworkTestPropertyAttribute = "Microsoft.Testing.Framework.TestPropertyAttribute"; - private const string SystemCollectionsGenericIEnumerable1 = "System.Collections.Generic.IEnumerable`1"; - private const string SystemIAsyncDisposable = "System.IAsyncDisposable"; - private const string SystemIDisposable = "System.IDisposable"; - private const string SystemObsoleteAttribute = "System.ObsoleteAttribute"; - private const string SystemThreadingTasksTask = "System.Threading.Tasks.Task"; - private const string SystemThreadingTasksValueTask = "System.Threading.Tasks.ValueTask"; - private const string SystemTimeSpan = "System.TimeSpan"; - private const string MicrosoftVisualStudioTestToolsUnitTestingTestMethodAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute"; - - public WellKnownTypes(Compilation compilation) - { - DataRowAttributeSymbol = compilation.GetTypeByMetadataName(MicrosoftVisualStudioTestToolsUnitTestingDataRowAttribute); - DynamicDataAttributeSymbol = compilation.GetTypeByMetadataName(MicrosoftVisualStudioTestToolsUnitTestingDynamicDataAttribute); - IAsyncDisposableSymbol = compilation.GetTypeByMetadataName(SystemIAsyncDisposable); - IDisposableSymbol = compilation.GetTypeByMetadataName(SystemIDisposable); - IEnumerable1Symbol = compilation.GetTypeByMetadataName(SystemCollectionsGenericIEnumerable1); - IgnoreAttributeSymbol = compilation.GetTypeByMetadataName(MicrosoftVisualStudioTestToolsUnitTestingIgnoreAttribute); - SystemObsoleteAttributeSymbol = compilation.GetTypeByMetadataName(SystemObsoleteAttribute); - TaskSymbol = compilation.GetTypeByMetadataName(SystemThreadingTasksTask); - TestArgumentsEntrySymbol = compilation.GetTypeByMetadataName(MicrosoftTestingFrameworkTestArgumentsEntry1); - TestExecutionTimeoutAttributeSymbol = compilation.GetTypeByMetadataName(MicrosoftTestingFrameworkTestExecutionTimeoutAttribute); - TestPropertyAttributeSymbol = compilation.GetTypeByMetadataName(MicrosoftTestingFrameworkTestPropertyAttribute); - TimeSpanSymbol = compilation.GetTypeByMetadataName(SystemTimeSpan); - ValueTaskSymbol = compilation.GetTypeByMetadataName(SystemThreadingTasksValueTask); - TestMethodAttributeSymbol = compilation.GetTypeByMetadataName(MicrosoftVisualStudioTestToolsUnitTestingTestMethodAttribute); - } - - public INamedTypeSymbol? DataRowAttributeSymbol { get; } - - public INamedTypeSymbol? DynamicDataAttributeSymbol { get; } - - public INamedTypeSymbol? IAsyncDisposableSymbol { get; } - - public INamedTypeSymbol? IDisposableSymbol { get; } - - public INamedTypeSymbol? IEnumerable1Symbol { get; } - - public INamedTypeSymbol? IgnoreAttributeSymbol { get; } - - public INamedTypeSymbol? SystemObsoleteAttributeSymbol { get; } - - public INamedTypeSymbol? TaskSymbol { get; } - - public INamedTypeSymbol? TestArgumentsEntrySymbol { get; } - - public INamedTypeSymbol? TestExecutionTimeoutAttributeSymbol { get; } - - public INamedTypeSymbol? TestPropertyAttributeSymbol { get; } - - public INamedTypeSymbol? TimeSpanSymbol { get; } - - public INamedTypeSymbol? ValueTaskSymbol { get; } - - public INamedTypeSymbol? TestMethodAttributeSymbol { get; } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/MSTest.SourceGeneration.csproj b/src/Analyzers/MSTest.SourceGeneration/MSTest.SourceGeneration.csproj index 3eb6317ab6..fa088d0de4 100644 --- a/src/Analyzers/MSTest.SourceGeneration/MSTest.SourceGeneration.csproj +++ b/src/Analyzers/MSTest.SourceGeneration/MSTest.SourceGeneration.csproj @@ -5,32 +5,43 @@ netstandard2.0 false - NU5128 + $(NoWarn);NU5128 true - Microsoft.Testing.Framework.SourceGeneration + Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration + true + true + + + $(DefineConstants);EXCLUDE_RANGE_INDEX_POLYFILL + License.txt - $(MSTestEngineVersionPrefix) - $(MSTestEnginePreReleaseVersionLabel) + $(MSTestSourceGenerationVersionPrefix) + $(MSTestSourceGenerationPreReleaseVersionLabel) true true - true - +This package provides the C# source generators that emit reflection metadata so that MSTest test assemblies can run trim-safe and NativeAOT-compatible.]]> + + + + + + diff --git a/src/Analyzers/MSTest.SourceGeneration/Models/TestAssemblyMetadata.cs b/src/Analyzers/MSTest.SourceGeneration/Models/TestAssemblyMetadata.cs new file mode 100644 index 0000000000..bff432b2f9 --- /dev/null +++ b/src/Analyzers/MSTest.SourceGeneration/Models/TestAssemblyMetadata.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Models; + +/// +/// Equatable snapshot of a discovered test class. Source-generator pipeline values must be +/// value-equatable so that incremental caching works; this record carries primitive data only. +/// +internal sealed record TestClassMetadata( + string FullyQualifiedName, + string DisplayName, + string? Namespace, + EquatableArray Methods, + EquatableArray BaseTypeFullyQualifiedNames); + +/// +/// Equatable snapshot of a discovered test method. +/// +internal sealed record TestMethodMetadata(string Name, EquatableArray ParameterTypes); + +/// +/// Equatable snapshot for the full test assembly, used as the input to the emitter. +/// +internal sealed record TestAssemblyMetadata( + string AssemblyName, + EquatableArray Classes); + +/// +/// Minimal value-equatable wrapper around an . Source generators +/// need value equality on collections; only has reference equality +/// out of the box, which would cause cache misses on every compilation tick. +/// +/// The element type stored in the underlying . +internal readonly record struct EquatableArray(ImmutableArray Items) + where T : IEquatable +{ + public int Count => Items.IsDefault ? 0 : Items.Length; + + // Return ImmutableArray.Enumerator (a struct) by value so that 'foreach' uses the duck-typed + // enumerator and avoids boxing on the source-generator hot path. Returning IEnumerator here + // would force an interface dispatch and allocate on every iteration. + public ImmutableArray.Enumerator GetEnumerator() => (Items.IsDefault ? ImmutableArray.Empty : Items).GetEnumerator(); + + public bool Equals(EquatableArray other) + { + ImmutableArray a = Items.IsDefault ? ImmutableArray.Empty : Items; + ImmutableArray b = other.Items.IsDefault ? ImmutableArray.Empty : other.Items; + if (a.Length != b.Length) + { + return false; + } + + for (int i = 0; i < a.Length; i++) + { + if (!EqualityComparer.Default.Equals(a[i], b[i])) + { + return false; + } + } + + return true; + } + + public override int GetHashCode() + { + if (Items.IsDefault) + { + return 0; + } + + int hash = 17; + foreach (T item in Items) + { + hash = unchecked((hash * 31) + (item?.GetHashCode() ?? 0)); + } + + return hash; + } +} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/DataRowTestMethodArgumentsInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/DataRowTestMethodArgumentsInfo.cs deleted file mode 100644 index 7bc9a24b18..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/DataRowTestMethodArgumentsInfo.cs +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.Testing.Framework.SourceGeneration.Helpers; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -internal sealed class DataRowTestMethodArgumentsInfo : ITestMethodArgumentsInfo -{ - private readonly ImmutableArray> _argumentsRows; - private readonly TestMethodParametersInfo _parametersInfo; - - public bool IsTestArgumentsEntryReturnType => true; - - public string? GeneratorMethodFullName { get; } - - public DataRowTestMethodArgumentsInfo(ImmutableArray> argumentsRows, TestMethodParametersInfo parametersInfo) - { - _argumentsRows = argumentsRows; - _parametersInfo = parametersInfo; - } - - public static DataRowTestMethodArgumentsInfo? TryBuild(IMethodSymbol methodSymbol, IEnumerable argumentsAttributes, - TestMethodParametersInfo parametersInfo) - { - var argumentsRows = argumentsAttributes.Select(attr => GetInlineArguments(methodSymbol, attr).ToImmutableArray()).ToImmutableArray(); - - return argumentsRows.IsEmpty - ? null - : new(argumentsRows, parametersInfo); - } - - public void AppendArguments(IndentedStringBuilder nodeBuilder) - { - using (nodeBuilder.AppendBlock($"GetArguments = static () => new {TestMethodInfo.TestArgumentsEntryTypeName}<{_parametersInfo.ParametersTuple}>[]", closingBraceSuffixChar: ',')) - { - foreach (ImmutableArray arguments in _argumentsRows) - { - string argumentsEntry = arguments.Length > 1 - ? "(" + string.Join(", ", arguments) + ")" - : arguments[0]; - string argumentsUid = GetArgumentsUid([.. _parametersInfo.Parameters.Select(x => x.Name)], arguments); - nodeBuilder.AppendLine($"new {TestMethodInfo.TestArgumentsEntryTypeName}<{_parametersInfo.ParametersTuple}>({argumentsEntry}, \"{argumentsUid}\"),"); - } - } - } - - private static string GetArgumentsUid(string[] parameterNames, IList arguments) - { - StringBuilder argumentsUidBuilder = new(); - - for (int i = 0; i < arguments.Count; i++) - { - if (i < parameterNames.Length) - { - if (i != 0) - { - argumentsUidBuilder.Append(", "); - } - - argumentsUidBuilder.Append(parameterNames[i]); - argumentsUidBuilder.Append(": "); - } - - EscapeArgument(arguments[i], argumentsUidBuilder); - } - - return argumentsUidBuilder.ToString(); - } - - internal /* for testing purposes */ static void EscapeArgument(string argument, StringBuilder argumentsUidBuilder) - { - int escapeCharCount = 0; - for (int i = 0; i < argument.Length; i++) - { - char currentChar = argument[i]; - - if (currentChar == '\\') - { - escapeCharCount++; - } - else if (currentChar == '"' && escapeCharCount % 2 == 0) - { - argumentsUidBuilder.Append('\\'); - escapeCharCount = 0; - } - else - { - escapeCharCount = 0; - } - - argumentsUidBuilder.Append(argument[i]); - } - } - - private static IEnumerable GetInlineArguments(IMethodSymbol methodSymbol, AttributeData attr) - { - TypedConstant argumentsAttributeArguments = attr.ConstructorArguments[0]; - if (argumentsAttributeArguments.IsNull) - { - yield return "null"; - yield break; - } - - StringBuilder argumentsBuilder = new(); - Stack<(TypedConstant Argument, bool HasNextArg, bool IsInArray, int ClosingCurlyBraceCount)> argumentStack = new(); - - bool hasManyArgsButExpectsSingleArray = - methodSymbol.Parameters.Length == 1 - && methodSymbol.Parameters[0].Type.TypeKind == TypeKind.Array - && argumentsAttributeArguments.Values.Length > 1; - - if (hasManyArgsButExpectsSingleArray) - { - argumentStack.Push((argumentsAttributeArguments, HasNextArg: false, IsInArray: false, ClosingCurlyBraceCount: 0)); - } - else - { - // We are using the ctor with params object[]; - if (argumentsAttributeArguments.Kind == TypedConstantKind.Array) - { - for (int i = argumentsAttributeArguments.Values.Length - 1; i >= 0; i--) - { - argumentStack.Push((argumentsAttributeArguments.Values[i], - HasNextArg: i < argumentsAttributeArguments.Values.Length - 1, - IsInArray: false, - ClosingCurlyBraceCount: 0)); - } - } - else - { - argumentStack.Push((argumentsAttributeArguments, - HasNextArg: false, - IsInArray: false, - ClosingCurlyBraceCount: 0)); - } - } - - while (argumentStack.Count > 0) - { - (TypedConstant argument, bool hasNextArg, bool isInArray, int closingCurlyBraceCount) = argumentStack.Pop(); - - if (argument.Kind == TypedConstantKind.Array) - { - argumentsBuilder.Append("new "); - if (argument.Type is not null) - { - argumentsBuilder.Append(argument.Type.ToDisplayString()); - } - else - { - argumentsBuilder.Append("[]"); - } - - argumentsBuilder.Append(" { "); - - for (int i = argument.Values.Length - 1; i >= 0; i--) - { - argumentStack.Push((argument.Values[i], - HasNextArg: i < argument.Values.Length - 1 || hasNextArg, - IsInArray: true, - ClosingCurlyBraceCount: i == argument.Values.Length - 1 ? closingCurlyBraceCount + 1 : 0)); - } - } - else - { - if (argument.Kind == TypedConstantKind.Enum) - { - // We could cast the argument to TypedConstant and get the full name of the type - // with the global:: prefix from e.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - // but then we don't have an easy way to get the value in CSharp format without the type. - // So we just prepend. - argumentsBuilder.Append("global::" + argument.ToCSharpString()); - } - else - { - argumentsBuilder.Append(argument.ToCSharpString()); - } - - for (int i = 0; i < closingCurlyBraceCount; i++) - { - argumentsBuilder.Append(" }"); - } - - if (hasNextArg) - { - if (isInArray && closingCurlyBraceCount == 0) - { - argumentsBuilder.Append(", "); - } - else - { - yield return argumentsBuilder.ToString(); - argumentsBuilder.Clear(); - } - } - } - } - - yield return argumentsBuilder.ToString(); - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/DynamicDataTestMethodArgumentsInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/DynamicDataTestMethodArgumentsInfo.cs deleted file mode 100644 index 0d08f672a8..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/DynamicDataTestMethodArgumentsInfo.cs +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Analyzers.Utilities; - -using Microsoft.CodeAnalysis; -using Microsoft.Testing.Framework.SourceGeneration.Helpers; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -/// -/// Clone of ArgumentsProvider, that is implementing very similar functionality for DynamicData, because there we don't know what types the user is using, so we do basically the same -/// but we cast their data to the assumed types. -/// -internal sealed class DynamicDataTestMethodArgumentsInfo : ITestMethodArgumentsInfo -{ - // Based on DynamicDataSourceType in: - // https://github.com/microsoft/testfx/blob/8cf945ba740034e37e0a16efddad85f6b0fb67bc/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataAttribute.cs#L17-L36 - private const int DynamicDataSourceTypeProperty = 0; - private const int DynamicDataSourceTypeMethod = 1; - private const int DynamicDataSourceTypeAutoDetect = 2; - private const int DynamicDataSourceTypeField = 3; - - internal const string TestArgumentsEntryTypeName = "MSTF::InternalUnsafeTestArgumentsEntry"; - internal const string DynamicDataNameProviderTypeName = "MSTF::DynamicDataNameProvider"; - private const string TestArgumentsEntryProviderMethodName = nameof(TestArgumentsEntryProviderMethodName); - private const string TestArgumentsEntryProviderMethodType = nameof(TestArgumentsEntryProviderMethodType); - private readonly string _memberName; - private readonly string _memberFullType; - private readonly SymbolKind _memberKind; - private readonly TestMethodParametersInfo _testMethodParameters; - private readonly bool _targetMethodReturnsCollectionOfTestArgumentsEntry; - - private DynamicDataTestMethodArgumentsInfo(string memberName, string memberFullType, SymbolKind memberKind, TestMethodParametersInfo testMethodParameters, - bool targetMemberReturnsCollectionOfTestArgumentsEntry, string? generatorMethodFullName) - { - _memberName = memberName; - _memberFullType = memberFullType; - // This is always true, because it tells the source gen that GetParameters will return collection of TestArgumentsEntry. - // If the target member does not return that type, we will write code to adapt the result to this collection in AppendArguments method below. - IsTestArgumentsEntryReturnType = true; - _targetMethodReturnsCollectionOfTestArgumentsEntry = targetMemberReturnsCollectionOfTestArgumentsEntry; - _memberKind = memberKind; - GeneratorMethodFullName = generatorMethodFullName; - _testMethodParameters = testMethodParameters; - } - - public bool IsTestArgumentsEntryReturnType { get; } - - public string? GeneratorMethodFullName { get; } - - public static DynamicDataTestMethodArgumentsInfo? TryBuild(IMethodSymbol methodSymbol, List argumentsProviderAttributes, - WellKnownTypes wellKnownTypes) - { - // We don't support more than one provider on method at the same time. - if (argumentsProviderAttributes.Count != 1) - { - return null; - } - - // We collect all the providers that match, and they might conflict by parameters, but this also ties us to the actual type - // so user cannot subclass. - if (!SymbolEqualityComparer.Default.Equals(argumentsProviderAttributes[0].AttributeClass, wellKnownTypes.DynamicDataAttributeSymbol)) - { - return null; - } - - AttributeData attribute = argumentsProviderAttributes[0]; - INamedTypeSymbol memberTypeSymbol = methodSymbol.ContainingType; - string? memberName = null; - int memberKind = DynamicDataSourceTypeAutoDetect; - - foreach (TypedConstant arg in attribute.ConstructorArguments) - { - if (arg.Type?.SpecialType == SpecialType.System_String) - { - memberName = arg.Value?.ToString(); - } - else if (arg.Value is INamedTypeSymbol argTypeSymbol) - { - memberTypeSymbol = argTypeSymbol; - } - else if (arg.Value is int argValueAsInt) - { - memberKind = argValueAsInt; - } - } - - return memberName is null - ? null - : TryBuildFromDynamicData(memberTypeSymbol, memberName, ToSymbolKind(memberKind), wellKnownTypes, methodSymbol); - - static SymbolKind? ToSymbolKind(int memberKind) => - memberKind switch - { - DynamicDataSourceTypeProperty => SymbolKind.Property, - DynamicDataSourceTypeMethod => SymbolKind.Method, - DynamicDataSourceTypeField => SymbolKind.Field, - DynamicDataSourceTypeAutoDetect => null, - _ => throw ApplicationStateGuard.Unreachable(), - }; - } - - private static DynamicDataTestMethodArgumentsInfo? TryBuildFromDynamicData(INamedTypeSymbol memberTypeSymbol, string memberName, SymbolKind? symbolKind, - WellKnownTypes wellKnownTypes, IMethodSymbol testMethodSymbol) - { - // Dynamic data supports Properties, Methods, and Fields. - // null is also possible and means "AutoDetect" - if (symbolKind is not (SymbolKind.Property or SymbolKind.Method or SymbolKind.Field or null)) - { - return null; - } - - ISymbol? firstMatchingMember = memberTypeSymbol.GetAllMembers(memberName) - .SelectMany(x => x) - // DynamicData supports properties, methods, and fields. - // .Where(s => s.IsStatic && (s.Kind is SymbolKind.Field or SymbolKind.Property or SymbolKind.Method)) - // .Where(s => symbolKind == null || s.Kind == symbolKind) - .Where(s => s.IsStatic && (s.Kind == symbolKind || (symbolKind is null && s.Kind is SymbolKind.Property or SymbolKind.Method or SymbolKind.Field))) - .Select(s => s switch - { - IPropertySymbol propertySymbol => (ISymbol)propertySymbol, - IMethodSymbol methodSymbol => methodSymbol, - IFieldSymbol fieldSymbol => fieldSymbol, - _ => throw ApplicationStateGuard.Unreachable(), - }) - .OrderBy(tuple => tuple.Kind switch - { - SymbolKind.Property => 1, - SymbolKind.Method => 2, - SymbolKind.Field => 3, - _ => throw ApplicationStateGuard.Unreachable(), - }) - .FirstOrDefault(); - - if (firstMatchingMember is null - || firstMatchingMember.GetMemberType() is not { } returnMemberTypeSymbol) - { - return null; - } - - // We want to check if the member returns a type that implements IEnumerable> - var allInterfacesAndSelfIfInterface = new List(returnMemberTypeSymbol.AllInterfaces); - if (returnMemberTypeSymbol.TypeKind == TypeKind.Interface - && returnMemberTypeSymbol is INamedTypeSymbol namedInterfaceSymbol) - { - allInterfacesAndSelfIfInterface.Add(namedInterfaceSymbol); - } - - // If the return type is not IEnumerable> we will adapt it later. - // The implementation of https://github.com/microsoft/testfx/blob/main/src/TestFramework/TestFramework/Attributes/DataSource/DynamicDataAttribute.cs#L87 - // only allows IEnumerable so we will assume that is true. - bool targetMemberReturnsCollectionOfTestArgumentsEntry = allInterfacesAndSelfIfInterface.Any(i => - SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, wellKnownTypes.IEnumerable1Symbol) - && i.TypeArguments.Length == 1 - && SymbolEqualityComparer.Default.Equals(i.TypeArguments[0].OriginalDefinition, wellKnownTypes.TestArgumentsEntrySymbol)); - - string? generatorMethodFullName = null; - - // This way we could handle the additional named parameters on [DynamicData - // DynamicDataDisplayName - // and DynamicDataDisplayNameDeclaringType, but this is imperfect implementation for now. - // KeyValuePair argumentsEntryProviderMethodType = namedArguments.FirstOrDefault(x => x.Key == TestArgumentsEntryProviderMethodType); - // if (argumentsEntryProviderMethodType.Key == TestArgumentsEntryProviderMethodType - // && argumentsEntryProviderMethodType.Value.Value is INamedTypeSymbol generatorMethodTypeSymbol) - // { - // generatorMethodFullName = generatorMethodTypeSymbol.ToDisplayString(); - // } - // - // KeyValuePair argumentsEntryProviderMethodName = namedArguments.FirstOrDefault(x => x.Key == TestArgumentsEntryProviderMethodName); - // if (argumentsEntryProviderMethodName.Key == TestArgumentsEntryProviderMethodName - // && argumentsEntryProviderMethodName.Value.Value is string generatorMethodName) - // { - // generatorMethodFullName ??= methodTypeSymbol.ToDisplayString(); - // generatorMethodFullName = $"{generatorMethodFullName}.{generatorMethodName}"; - // } - var testMethodParameters = new TestMethodParametersInfo(testMethodSymbol.Parameters); - - return new(firstMatchingMember.Name, firstMatchingMember.ContainingType.ToDisplayString(), - firstMatchingMember.Kind, testMethodParameters, targetMemberReturnsCollectionOfTestArgumentsEntry, generatorMethodFullName); - } - - public void AppendArguments(IndentedStringBuilder nodeBuilder) - { - nodeBuilder.Append("GetArguments = static () => "); - - using (nodeBuilder.AppendBlock()) - { - if (_targetMethodReturnsCollectionOfTestArgumentsEntry) - { - // We just return the data as is. - nodeBuilder.Append(" return "); - } - else - { - // We need to convert the data to TestArgumentsEntry. - nodeBuilder.Append("var data = "); - } - - // Call the member. - nodeBuilder.Append($"{_memberFullType}.{_memberName}"); - - if (_memberKind is SymbolKind.Method) - { - nodeBuilder.Append("()"); - } - - nodeBuilder.AppendLine(';'); - - if (!_targetMethodReturnsCollectionOfTestArgumentsEntry) - { - string tupleType = $"{TestArgumentsEntryTypeName}<{_testMethodParameters.ParametersTuple}>"; - nodeBuilder.AppendLine($"var dataCollection = new ColGen.List<{tupleType}>();"); - nodeBuilder.AppendLine($"var index = 0;"); - string expand = string.Join(", ", _testMethodParameters.Parameters.Select((p, i) => $"({p.FullyQualifiedType}) item[{i}]")); - using (nodeBuilder.AppendBlock("foreach (var item in data)")) - { - IEnumerable parameterNames = _testMethodParameters.Parameters.Select(p => p.Name); - nodeBuilder.AppendLine($$"""string uidFragment = {{DynamicDataNameProviderTypeName}}.GetUidFragment(new string[] {"{{string.Join("\", \"", parameterNames)}}"}, item, index);"""); - nodeBuilder.AppendLine("index++;"); - nodeBuilder.AppendLine($"""dataCollection.Add(new(({expand}), uidFragment));"""); - } - - nodeBuilder.AppendLine("return dataCollection;"); - } - } - - nodeBuilder.AppendLine(','); - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/ITestMethodArgumentsInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/ITestMethodArgumentsInfo.cs deleted file mode 100644 index c7e19cb416..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/ITestMethodArgumentsInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.Testing.Framework.SourceGeneration.Helpers; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -internal interface ITestMethodArgumentsInfo -{ - bool IsTestArgumentsEntryReturnType { get; } - - string? GeneratorMethodFullName { get; } - - void AppendArguments(IndentedStringBuilder nodeBuilder); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestMethodInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestMethodInfo.cs deleted file mode 100644 index 1abc1ebf88..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestMethodInfo.cs +++ /dev/null @@ -1,343 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.Testing.Framework.SourceGeneration.Helpers; - -using MSTest.SourceGeneration.Helpers; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -internal sealed record class TestMethodInfo -{ - internal const string TestArgumentsEntryTypeName = "MSTF::InternalUnsafeTestArgumentsEntry"; - private const string CtorVariableName = "instance"; - private const string TestExecutionContextVariableName = "testExecutionContext"; - private const string DataVariableName = "data"; - private const string DataDotArgumentsMemberAccessName = DataVariableName + ".Arguments"; - private readonly EquatableArray<(string FilePath, int StartLine, int EndLine)> _declarationReferences; - private readonly string _methodName; - private readonly int _methodArity; - private readonly string _declaringAssemblyName; - private readonly string _usingTypeFullyQualifiedName; - private readonly bool _isAsync; - private readonly EquatableArray<(string Key, string? Value)> _testProperties; - private readonly TimeSpan? _testExecutionTimeout; - private readonly EquatableArray<(string RuleId, string Description)> _invocationPragmas; - private readonly string _methodIdentifierAssemblyName; - private readonly string _methodIdentifierNamespace; - private readonly string _methodIdentifierTypeName; - private readonly string _methodIdentifierReturnFullyQualifiedTypeName; - - private TestMethodInfo(IMethodSymbol methodSymbol, INamedTypeSymbol typeUsingMethod, ImmutableArray<(string Key, string? Value)> testProperties, - TestMethodParametersInfo parametersInfo, ITestMethodArgumentsInfo? argumentsInfo, - IEnumerable<(string RuleId, string Description)> invocationPragmas, TimeSpan? testExecutionTimeout) - { - // 'SymbolDisplayFormat.CSharpShortErrorMessageFormat' gives us the minimal name while preserving sub-classes - _methodIdentifierTypeName = methodSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat); - _methodIdentifierNamespace = methodSymbol.ContainingNamespace.IsGlobalNamespace - ? string.Empty - : methodSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); - _methodIdentifierReturnFullyQualifiedTypeName = methodSymbol.ReturnType.ToDisplayString(TestMethods.MethodIdentifierFullyQualifiedTypeFormat); - ArgumentsInfo = argumentsInfo; - _testExecutionTimeout = testExecutionTimeout; - _invocationPragmas = invocationPragmas.ToImmutableArray(); - _usingTypeFullyQualifiedName = typeUsingMethod.ToDisplayString(); - // NOTE: the method symbol containing type is the type declaring the method, not the type using the method. - string fullyQualifiedDisplayName = _usingTypeFullyQualifiedName - + "." - + methodSymbol.ToDisplayString().Substring(methodSymbol.ContainingType.ToDisplayString().Length + 1); - _declarationReferences = methodSymbol.DeclaringSyntaxReferences - .Select(x => (x.SyntaxTree.FilePath, x.SyntaxTree.GetLineSpan(x.Span))) - .Select(tuple => (tuple.FilePath, tuple.Item2.StartLinePosition.Line + 1, tuple.Item2.EndLinePosition.Line + 1)) - .ToImmutableArray(); - _methodName = methodSymbol.Name; - _methodArity = methodSymbol.Arity; - // 'SymbolDisplayFormat.FullyQualifiedFormat' would add version, culture and public key token to the assembly name. - _declaringAssemblyName = methodSymbol.ContainingAssembly.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); - _methodIdentifierAssemblyName = methodSymbol.ContainingAssembly.ToDisplayString(); - _isAsync = !methodSymbol.ReturnsVoid; - _testProperties = testProperties; - ParametersInfo = parametersInfo; - - TestMethodStableUid = $"\"{_declaringAssemblyName}.{fullyQualifiedDisplayName}\""; - } - - internal string TestMethodStableUid { get; } - - internal ITestMethodArgumentsInfo? ArgumentsInfo { get; } - - internal TestMethodParametersInfo ParametersInfo { get; } - - public static TestMethodInfo? TryBuild(IMethodSymbol methodSymbol, INamedTypeSymbol typeUsingMethod, WellKnownTypes wellKnownTypes) - { - try - { - // We don't need to be checking for resultant visibility here because we know parent is checking for it - if (!methodSymbol.IsValidTestMethodShape(wellKnownTypes) - || methodSymbol.IsKnownNonTestMethod(wellKnownTypes)) - { - return null; - } - - ImmutableArray attributes = methodSymbol.GetAttributes(); - - if (attributes.Length == 0 || !attributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, wellKnownTypes.TestMethodAttributeSymbol))) - { - return null; - } - - List dataRowAttributes = []; - List dynamicDataAttributes = []; - List testPropertyAttributes = []; - List<(string RuleId, string Description)> pragmas = []; - TimeSpan? testExecutionTimeout = null; - foreach (AttributeData attribute in attributes) - { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.DataRowAttributeSymbol) - && attribute.ConstructorArguments.Length == 1) - { - dataRowAttributes.Add(attribute); - } - else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.DynamicDataAttributeSymbol) - && attribute.ConstructorArguments.Length is 1 or 2 or 3) - { - dynamicDataAttributes.Add(attribute); - } - else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.TestPropertyAttributeSymbol) - && attribute.ConstructorArguments.Length == 2) - { - testPropertyAttributes.Add(attribute); - } - else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.SystemObsoleteAttributeSymbol)) - { - if (attribute.ConstructorArguments.Length == 0) - { - pragmas.Add(("CS0612", "Type or member is obsolete")); - } - else if (attribute.ConstructorArguments.Length == 1 - // We cannot suppress CS0619 as it's an error level - || (attribute.ConstructorArguments.Length == 2 && attribute.ConstructorArguments[1].Value?.Equals(false) == true)) - { - pragmas.Add(("CS0618", "Type or member is obsolete")); - } - } - else if (attribute.TryGetTestExecutionTimeout(wellKnownTypes.TestExecutionTimeoutAttributeSymbol, wellKnownTypes.TimeSpanSymbol, - out TimeSpan maybeTestExecutionTimeout)) - { - testExecutionTimeout = maybeTestExecutionTimeout; - } - } - - TestMethodParametersInfo parametersInfo = new(methodSymbol.Parameters); - - // TODO: This code is not handling the case where both DataRow and DynamicData attributes are present. - ITestMethodArgumentsInfo? argumentsInfo = - (ITestMethodArgumentsInfo?)DataRowTestMethodArgumentsInfo.TryBuild(methodSymbol, dataRowAttributes, parametersInfo) - ?? DynamicDataTestMethodArgumentsInfo.TryBuild(methodSymbol, dynamicDataAttributes, wellKnownTypes); - - ImmutableArray<(string Key, string? Value)> testProperties = testPropertyAttributes - .Where(attr => attr.ConstructorArguments[0].Value is not null) - .Select(attr => (attr.ConstructorArguments[0].Value!.ToString(), attr.ConstructorArguments[1].Value?.ToString())) - .ToImmutableArray(); - - // Method is valid test method - return new(methodSymbol, typeUsingMethod, testProperties, parametersInfo, argumentsInfo, pragmas, testExecutionTimeout); - } - catch (Exception ex) - { - throw new InvalidOperationException($"Failed for method {methodSymbol.ToDisplayString()}, with {ex}", ex); - } - } - - public void AppendTestNode(IndentedStringBuilder sourceStringBuilder, TestTypeInfo testTypeInfo) - { - bool useAsyncNode = _isAsync; - AppendTestNodeCtorDeclaration(sourceStringBuilder, useAsyncNode, ParametersInfo.ParametersTuple, ArgumentsInfo); - using (sourceStringBuilder.AppendBlock(closingBraceSuffixChar: ',')) - { - sourceStringBuilder.AppendLine($"StableUid = {TestMethodStableUid},"); - sourceStringBuilder.AppendLine($"DisplayName = \"{_methodName}\","); - - int propertiesCount = - 1 // properties that are always present - + _declarationReferences.Length - + _testProperties.Length; - using (sourceStringBuilder.AppendBlock($"Properties = new Msg::IProperty[{propertiesCount}]", closingBraceSuffixChar: ',')) - { - sourceStringBuilder.AppendLine("new Msg::TestMethodIdentifierProperty("); - sourceStringBuilder.IndentationLevel++; - sourceStringBuilder.AppendLine($"\"{_methodIdentifierAssemblyName}\","); - sourceStringBuilder.AppendLine($"\"{_methodIdentifierNamespace}\","); - sourceStringBuilder.AppendLine($"\"{_methodIdentifierTypeName}\","); - sourceStringBuilder.AppendLine($"\"{_methodName}\","); - sourceStringBuilder.AppendLine($"{_methodArity},"); - - if (ParametersInfo.ParametersMethodIdentifierFullyQualifiedTypes.Length > 0) - { - using (sourceStringBuilder.AppendBlock($"new string[{ParametersInfo.ParametersMethodIdentifierFullyQualifiedTypes.Length}]", closingBraceSuffixChar: ',')) - { - foreach (string parameterIdentifierType in ParametersInfo.ParametersMethodIdentifierFullyQualifiedTypes) - { - sourceStringBuilder.AppendLine($"\"{parameterIdentifierType}\","); - } - } - } - else - { - sourceStringBuilder.AppendLine("Sys::Array.Empty(),"); - } - - sourceStringBuilder.AppendLine($"\"{_methodIdentifierReturnFullyQualifiedTypeName}\"),"); - sourceStringBuilder.IndentationLevel--; - - foreach ((string filePath, int startLine, int endLine) in _declarationReferences) - { - sourceStringBuilder.AppendLine($"new Msg::TestFileLocationProperty(@\"{filePath}\", new(new({startLine}, -1), new({endLine}, -1))),"); - } - - foreach ((string key, string? value) in _testProperties) - { - sourceStringBuilder.AppendLine($"new Msg::TestMetadataProperty(\"{key}\", \"{value}\"),"); - } - } - - ArgumentsInfo?.AppendArguments(sourceStringBuilder); - sourceStringBuilder.Append("Body = static "); - if (useAsyncNode) - { - sourceStringBuilder.Append("async "); - } - - sourceStringBuilder.Append( - ParametersInfo.Parameters.IsEmpty - ? TestExecutionContextVariableName - : $"({TestExecutionContextVariableName}, {DataVariableName})"); - - using (sourceStringBuilder.AppendBlock(" =>", closingBraceSuffixChar: ',')) - { - MaybeAppendBodyCancellationTokenCreation(sourceStringBuilder, testTypeInfo); - AppendCtorCall(sourceStringBuilder, testTypeInfo); - AppendMethodCall(sourceStringBuilder); - } - } - } - - private void MaybeAppendBodyCancellationTokenCreation(IndentedStringBuilder sourceStringBuilder, TestTypeInfo testTypeInfo) - { - if (testTypeInfo.TestExecutionTimeout is null && _testExecutionTimeout is null) - { - return; - } - - TimeSpan minTimeout = (testTypeInfo.TestExecutionTimeout, _testExecutionTimeout) switch - { - (null, { } time) => time, - ({ } time, null) => time, - ({ } time1, { } time2) when time1 <= time2 => time1, - ({ } time1, { } time2) when time1 > time2 => time2, - _ => throw ApplicationStateGuard.Unreachable(), - }; - - sourceStringBuilder.AppendLine($"{TestExecutionContextVariableName}.CancelTestExecution(new global::System.TimeSpan({minTimeout.Ticks}));"); - } - - private static void AppendTestNodeCtorDeclaration(IndentedStringBuilder nodeBuilder, bool useAsyncNode, - string? parametersTuple, ITestMethodArgumentsInfo? argumentsInfo) - { - if (parametersTuple != null && argumentsInfo is null) - { - nodeBuilder.AppendLine("// The test method is parameterized but no argument was specified."); - nodeBuilder.AppendLine("// This is most often caused by using an unsupported arguments input."); - nodeBuilder.AppendLine("// Possible resolutions:"); - nodeBuilder.AppendLine("// - There is a mismatch between arguments from [DataRow] and the method parameters."); - nodeBuilder.AppendLine("// - There is a mismatch between arguments from [DynamicData] and the method parameters."); - nodeBuilder.AppendLine("// If nothing else worked, report the error and exclude this method by using [Ignore]."); - } - - nodeBuilder.Append("new MSTF::"); - nodeBuilder.Append((useAsyncNode, argumentsInfo) switch - { - (true, null) => "InternalUnsafeAsyncActionTestNode", - (true, _) => "InternalUnsafeAsyncActionParameterizedTestNode", - - (false, null) => "InternalUnsafeActionTestNode", - (false, _) => "InternalUnsafeActionParameterizedTestNode", - }); - nodeBuilder.AppendLine((parametersTuple, argumentsInfo?.IsTestArgumentsEntryReturnType ?? false) switch - { - (null, _) => string.Empty, - (_, false) => $"<{parametersTuple}>", - (_, true) => $"<{TestArgumentsEntryTypeName}<{parametersTuple}>>", - }); - } - - private static void AppendCtorCall(IndentedStringBuilder nodeBuilder, TestTypeInfo testTypeInfo) - { - if (testTypeInfo.IsIAsyncDisposable) - { - nodeBuilder.Append("await using "); - } - else if (testTypeInfo.IsIDisposable) - { - nodeBuilder.Append("using "); - } - - nodeBuilder.Append($"var {CtorVariableName} = new {testTypeInfo.ConstructorShortName}();"); - } - - private void AppendMethodCall(IndentedStringBuilder sourceStringBuilder) - { - sourceStringBuilder.AppendLine(); - IDisposable tryBlock = sourceStringBuilder.AppendBlock("try"); - - foreach ((string ruleId, string description) in _invocationPragmas) - { - sourceStringBuilder.AppendUnindentedLine($"#pragma warning disable {ruleId} // {description}"); - } - - if (_isAsync) - { - sourceStringBuilder.Append("await "); - } - - sourceStringBuilder.Append($"{CtorVariableName}.{_methodName}("); - - string dataVariable = ArgumentsInfo?.IsTestArgumentsEntryReturnType ?? false - ? DataDotArgumentsMemberAccessName - : DataVariableName; - - if (ParametersInfo.Parameters.Length == 1) - { - sourceStringBuilder.Append(dataVariable); - } - else - { - for (int i = 0; i < ParametersInfo.Parameters.Length; i++) - { - if (i > 0) - { - sourceStringBuilder.Append(", "); - } - - sourceStringBuilder.Append($"{dataVariable}.{ParametersInfo.Parameters[i].Name}"); - } - } - - sourceStringBuilder.AppendLine(");"); - - foreach ((string ruleId, string description) in _invocationPragmas) - { - sourceStringBuilder.AppendUnindentedLine($"#pragma warning restore {ruleId} // {description}"); - } - - tryBlock?.Dispose(); - - using (sourceStringBuilder.AppendBlock("catch (global::System.Exception ex)")) - { - sourceStringBuilder.AppendLine($"{TestExecutionContextVariableName}.ReportException(ex, null);"); - } - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestMethodParametersInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestMethodParametersInfo.cs deleted file mode 100644 index e92b79e650..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestMethodParametersInfo.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -internal sealed class TestMethodParametersInfo -{ - public TestMethodParametersInfo(ImmutableArray parameters) - { - Parameters = parameters - .Select(p => (p.Name, p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))) - .ToImmutableArray(); - ParametersTuple = BuildParametersTupleString(Parameters); - ParametersMethodIdentifierFullyQualifiedTypes = parameters.Select(p => p.Type.ToDisplayString(TestMethods.MethodIdentifierFullyQualifiedTypeFormat)).ToImmutableArray(); - } - - public ImmutableArray<(string Name, string FullyQualifiedType)> Parameters { get; } - - public string? ParametersTuple { get; } - - public ImmutableArray ParametersMethodIdentifierFullyQualifiedTypes { get; } - - private static string? BuildParametersTupleString(ImmutableArray<(string Name, string FullyQualifiedType)> parameters) - { - if (parameters.IsDefaultOrEmpty) - { - return null; - } - - if (parameters.Length == 1) - { - return parameters[0].FullyQualifiedType; - } - - var tupleTypeStringBuilder = new StringBuilder("("); - - for (int i = 0; i < parameters.Length; i++) - { - if (i > 0) - { - tupleTypeStringBuilder.Append(", "); - } - - tupleTypeStringBuilder.Append(parameters[i].FullyQualifiedType).Append(' ').Append(parameters[i].Name); - } - - return tupleTypeStringBuilder.Append(')').ToString(); - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestNamespaceInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestNamespaceInfo.cs deleted file mode 100644 index 308a04ebea..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestNamespaceInfo.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using Microsoft.CodeAnalysis; -using Microsoft.Testing.Framework.SourceGeneration.Helpers; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -internal sealed class TestNamespaceInfo : IEquatable -{ - private readonly string _nameOrGlobalNamespace; - private readonly string _containingAssembly; - - public string Name { get; } - - public string FullyQualifiedName { get; } - - public bool IsGlobalNamespace { get; } - - public TestNamespaceInfo(INamespaceSymbol namespaceSymbol) - { - _containingAssembly = namespaceSymbol.ContainingAssembly.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); - Name = namespaceSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); - _nameOrGlobalNamespace = namespaceSymbol.ToDisplayString(); - IsGlobalNamespace = namespaceSymbol.IsGlobalNamespace; - FullyQualifiedName = namespaceSymbol.IsGlobalNamespace - ? string.Empty - : namespaceSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); - } - - public void AppendNamespaceTestNode(IndentedStringBuilder nodeStringBuilder, string testsVariableName) - { - using (nodeStringBuilder.AppendTestNode(_containingAssembly + "." + _nameOrGlobalNamespace, _nameOrGlobalNamespace, [], testsVariableName)) - { - } - } - - public bool Equals(TestNamespaceInfo? other) - => other is not null - && other.FullyQualifiedName == FullyQualifiedName; - - public override bool Equals(object? obj) - => Equals(obj as TestNamespaceInfo); - - public override int GetHashCode() - => FullyQualifiedName.GetHashCode(); -} diff --git a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestTypeInfo.cs b/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestTypeInfo.cs deleted file mode 100644 index c1ec3a610e..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/ObjectModels/TestTypeInfo.cs +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under dual-license. See LICENSE.PLATFORMTOOLS.txt file in the project root for full license information. - -using System.Collections.Immutable; - -using Analyzers.Utilities; - -using Microsoft.CodeAnalysis; -using Microsoft.Testing.Framework.SourceGeneration.Helpers; - -using MSTest.SourceGeneration.Helpers; - -namespace Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -internal sealed record class TestTypeInfo -{ - private readonly string _name; - private readonly string _containingAssemblyName; - private readonly EquatableArray<(string FilePath, int StartLine, int EndLine)> _declarationReferences; - - internal EquatableArray TestMethodNodes { get; } - - public TimeSpan? TestExecutionTimeout { get; } - - internal string GeneratedTypeName { get; } - - internal string FullyQualifiedName { get; } - - internal TestNamespaceInfo ContainingNamespace { get; } - - internal bool IsIAsyncDisposable { get; } - - internal bool IsIDisposable { get; } - - internal string ConstructorShortName { get; } - - private TestTypeInfo(INamedTypeSymbol namedTypeSymbol, IMethodSymbol ctorToUse, - WellKnownTypes wellKnownTypes, ImmutableArray testMethodNodes, TimeSpan? testExecutionTimeout) - { - _name = namedTypeSymbol.Name; - _declarationReferences = namedTypeSymbol.DeclaringSyntaxReferences - .Select(x => (x.SyntaxTree.FilePath, x.SyntaxTree.GetLineSpan(x.Span))) - .Select(tuple => (tuple.FilePath, tuple.Item2.StartLinePosition.Line + 1, tuple.Item2.EndLinePosition.Line + 1)) - .ToImmutableArray(); - TestMethodNodes = testMethodNodes; - TestExecutionTimeout = testExecutionTimeout; - _containingAssemblyName = namedTypeSymbol.ContainingAssembly.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); - ContainingNamespace = new(namedTypeSymbol.ContainingNamespace); - FullyQualifiedName = namedTypeSymbol.ToDisplayString(); - string escapedFullyQualifiedName = TestNodeHelpers.GenerateEscapedName(FullyQualifiedName); - GeneratedTypeName = ContainingNamespace.IsGlobalNamespace - ? "_" + escapedFullyQualifiedName - : escapedFullyQualifiedName; - - // 'SymbolDisplayFormat.CSharpShortErrorMessageFormat' gives us the minimal name while preserving sub-classes - ConstructorShortName = ctorToUse.ContainingType.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat); - - IsIAsyncDisposable = namedTypeSymbol.AllInterfaces.Any(i => - SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, wellKnownTypes.IAsyncDisposableSymbol)); - IsIDisposable = namedTypeSymbol.AllInterfaces.Any(i => - SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, wellKnownTypes.IDisposableSymbol)); - } - - public static TestTypeInfo? TryBuild(GeneratorAttributeSyntaxContext context, WellKnownTypes wellKnownTypes) - { - if (context.TargetSymbol is not INamedTypeSymbol namedTypeSymbol) - { - return null; - } - - // The generator syntax checks should have already filtered out any types that are not public/internal but we still need - // to check because a public subclass of a non-public class is still not public. - if (namedTypeSymbol.GetResultantVisibility() is not SymbolVisibility.Public and not SymbolVisibility.Internal) - { - return null; - } - - // We only support simple classes - if (namedTypeSymbol.IsAbstract - || namedTypeSymbol.IsAnonymousType - || namedTypeSymbol.IsGenericType - || namedTypeSymbol.IsImplicitClass) - { - return null; - } - - if (!HasParameterlessConstructor(namedTypeSymbol, out IMethodSymbol? parameterlessCtor)) - { - return null; - } - - TimeSpan? testExecutionTimeout = null; - foreach (AttributeData attribute in namedTypeSymbol.GetAttributes()) - { - if (attribute.TryGetTestExecutionTimeout(wellKnownTypes.TestExecutionTimeoutAttributeSymbol, wellKnownTypes.TimeSpanSymbol, - out TimeSpan maybeExecutionTimeout)) - { - testExecutionTimeout = maybeExecutionTimeout; - } - } - - var testMethodNodes = namedTypeSymbol - .GetAllMembers() - .SelectMany(members => members) - .OfType() - .Select(method => TestMethodInfo.TryBuild(method, namedTypeSymbol, wellKnownTypes)) - .WhereNotNull() - .ToImmutableArray(); - - return new(namedTypeSymbol, parameterlessCtor, wellKnownTypes, testMethodNodes, testExecutionTimeout); - } - - private static bool HasParameterlessConstructor(INamedTypeSymbol namedTypeSymbol, [NotNullWhen(true)] out IMethodSymbol? parameterlessConstructor) - { - parameterlessConstructor = namedTypeSymbol.InstanceConstructors - .FirstOrDefault(ctor => ctor.DeclaredAccessibility == Accessibility.Public && ctor.Parameters.Length == 0); - - return parameterlessConstructor != null; - } - - public void AppendTestNode(IndentedStringBuilder sourceStringBuilder) - { - IDisposable? block = null; - - try - { - if (!ContainingNamespace.IsGlobalNamespace) - { - sourceStringBuilder.Append("namespace "); - // TODO: Understand how to retrieve assembly default namespace and use it instead of assembly name - block = sourceStringBuilder.AppendBlock(ContainingNamespace.FullyQualifiedName); - } - - sourceStringBuilder.AppendLine("using Threading = global::System.Threading;"); - sourceStringBuilder.AppendLine("using ColGen = global::System.Collections.Generic;"); - sourceStringBuilder.AppendLine("using CA = global::System.Diagnostics.CodeAnalysis;"); - sourceStringBuilder.AppendLine("using Sys = global::System;"); - - sourceStringBuilder.AppendLine("using Msg = global::Microsoft.Testing.Platform.Extensions.Messages;"); - sourceStringBuilder.AppendLine("using MSTF = global::Microsoft.Testing.Framework;"); - - sourceStringBuilder.AppendLine(); - - sourceStringBuilder.AppendLine("[CA::ExcludeFromCodeCoverage]"); - using (sourceStringBuilder.AppendBlock($"public static class {GeneratedTypeName}")) - { - sourceStringBuilder.Append("public static readonly MSTF::TestNode TestNode = "); - AppendTestNodeCreation(sourceStringBuilder); - } - } - finally - { - block?.Dispose(); - } - } - - private void AppendTestNodeCreation(IndentedStringBuilder sourceStringBuilder) - { - List properties = []; - foreach ((string filePath, int startLine, int endLine) in _declarationReferences) - { - properties.Add($"new Msg::TestFileLocationProperty(@\"{filePath}\", new(new({startLine}, -1), new({endLine}, -1))),"); - } - - using (sourceStringBuilder.AppendTestNode(_containingAssemblyName + "." + FullyQualifiedName, _name, properties, ';')) - { - foreach (TestMethodInfo testMethod in TestMethodNodes) - { - testMethod.AppendTestNode(sourceStringBuilder, this); - } - } - } -} diff --git a/src/Analyzers/MSTest.SourceGeneration/PACKAGE.md b/src/Analyzers/MSTest.SourceGeneration/PACKAGE.md deleted file mode 100644 index cdd8f5ba18..0000000000 --- a/src/Analyzers/MSTest.SourceGeneration/PACKAGE.md +++ /dev/null @@ -1,9 +0,0 @@ -# Microsoft.Testing - -Microsoft Testing is a set of platform, framework and protocol intended to make it possible to run any test on any target or device. - -Documentation can be found at . - -## About - -This package works with MSTest.Engine package to provide source generators. diff --git a/src/Package/MSTest.Sdk/MSTest.Sdk.csproj b/src/Package/MSTest.Sdk/MSTest.Sdk.csproj index a9207301b1..3218486287 100644 --- a/src/Package/MSTest.Sdk/MSTest.Sdk.csproj +++ b/src/Package/MSTest.Sdk/MSTest.Sdk.csproj @@ -36,13 +36,13 @@ - <_MSTestEnginePreReleaseVersionLabel>$(MSTestEnginePreReleaseVersionLabel) - <_MSTestEnginePreReleaseVersionLabel Condition="'$(ContinuousIntegrationBuild)' == 'true' and '$(OfficialBuild)' != 'true'">ci - <_MSTestEnginePreReleaseVersionLabel Condition="'$(ContinuousIntegrationBuild)' != 'true' and '$(OfficialBuild)' != 'true'">dev - <_MSTestEngineVersionSuffix>$(_MSTestEnginePreReleaseVersionLabel)$(_BuildNumberLabels) - <_MSTestEngineVersion>$(MSTestEngineVersionPrefix) - <_MSTestEngineVersion Condition="'$(_MSTestEngineVersionSuffix)' != ''">$(_MSTestEngineVersion)-$(_MSTestEngineVersionSuffix) - <_TemplateProperties>MSTestEngineVersion=$(_MSTestEngineVersion);MSTestVersion=$(Version);MicrosoftTestingPlatformVersion=$(Version.Replace('$(VersionPrefix)', '$(TestingPlatformVersionPrefix)'));MicrosoftNETTestSdkVersion=$(MicrosoftNETTestSdkVersion);MicrosoftTestingExtensionsCodeCoverageVersion=$(MicrosoftTestingExtensionsCodeCoverageVersion);MicrosoftPlaywrightVersion=$(MicrosoftPlaywrightVersion);AspireHostingTestingVersion=$(AspireHostingTestingVersion);MicrosoftTestingExtensionsFakesVersion=$(MicrosoftTestingExtensionsFakesVersion) + <_MSTestSourceGenerationPreReleaseVersionLabel>$(MSTestSourceGenerationPreReleaseVersionLabel) + <_MSTestSourceGenerationPreReleaseVersionLabel Condition="'$(ContinuousIntegrationBuild)' == 'true' and '$(OfficialBuild)' != 'true'">ci + <_MSTestSourceGenerationPreReleaseVersionLabel Condition="'$(ContinuousIntegrationBuild)' != 'true' and '$(OfficialBuild)' != 'true'">dev + <_MSTestSourceGenerationVersionSuffix>$(_MSTestSourceGenerationPreReleaseVersionLabel)$(_BuildNumberLabels) + <_MSTestSourceGenerationVersion>$(MSTestSourceGenerationVersionPrefix) + <_MSTestSourceGenerationVersion Condition="'$(_MSTestSourceGenerationVersionSuffix)' != ''">$(_MSTestSourceGenerationVersion)-$(_MSTestSourceGenerationVersionSuffix) + <_TemplateProperties>MSTestSourceGenerationVersion=$(_MSTestSourceGenerationVersion);MSTestVersion=$(Version);MicrosoftTestingPlatformVersion=$(Version.Replace('$(VersionPrefix)', '$(TestingPlatformVersionPrefix)'));MicrosoftNETTestSdkVersion=$(MicrosoftNETTestSdkVersion);MicrosoftTestingExtensionsCodeCoverageVersion=$(MicrosoftTestingExtensionsCodeCoverageVersion);MicrosoftPlaywrightVersion=$(MicrosoftPlaywrightVersion);AspireHostingTestingVersion=$(AspireHostingTestingVersion);MicrosoftTestingExtensionsFakesVersion=$(MicrosoftTestingExtensionsFakesVersion) + true + + @@ -27,18 +40,15 @@ - - $(MSTestEngineVersion) - - - - $(MSTestEngineVersion) + $(MSTestSourceGenerationVersion) - + + + + + $(MSTestVersion) + + diff --git a/src/Package/MSTest.Sdk/Sdk/Sdk.props.template b/src/Package/MSTest.Sdk/Sdk/Sdk.props.template index 776d1458bf..3703df287d 100644 --- a/src/Package/MSTest.Sdk/Sdk/Sdk.props.template +++ b/src/Package/MSTest.Sdk/Sdk/Sdk.props.template @@ -22,7 +22,7 @@ ${MicrosoftTestingExtensionsCodeCoverageVersion} ${MicrosoftTestingExtensionsFakesVersion} ${MicrosoftTestingPlatformVersion} - ${MSTestEngineVersion} + ${MSTestSourceGenerationVersion} ${MSTestVersion} diff --git a/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj b/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj index 11272e13ce..9aae122e9a 100644 --- a/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj +++ b/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj @@ -56,8 +56,6 @@ This package provides the core platform and the .NET implementation of the proto - - diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/NativeAotTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/NativeAotTests.cs index 3e53718f4d..d15202ac2d 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/NativeAotTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/NativeAotTests.cs @@ -10,8 +10,8 @@ namespace MSTest.Acceptance.IntegrationTests; public class NativeAotTests : AcceptanceTestBase { // Source code for a project that validates MSTest supporting Native AOT. - // Because MSTest is built on top of Microsoft.Testing.Platform, this also exercises - // additional MTP code paths beyond what the MTP-only NativeAOT test covers. + // Uses MSTest.SourceGeneration to emit reflection-free metadata so the runtime + // does not need to fall back to reflection for test discovery or invocation. private const string SourceCode = """ #file MSTestNativeAotTests.csproj @@ -22,37 +22,19 @@ public class NativeAotTests : AcceptanceTestBase Exe true preview + true true false - - + + -#file Program.cs -using System.Diagnostics; -using System.Reflection; -using System.Runtime.InteropServices; -using Microsoft.Testing.Framework; -using Microsoft.Testing.Internal.Framework; -using Microsoft.Testing.Platform.Builder; -using Microsoft.Testing.Platform.Capabilities; -using Microsoft.Testing.Platform.Capabilities.TestFramework; -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.Extensions.TestFramework; - -using MSTestNativeAotTests; - -ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); -builder.AddTestFramework(new SourceGeneratedTestNodesBuilder()); -using ITestApplication app = await builder.BuildAsync(); -return await app.RunAsync(); - #file TestClass1.cs using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -98,18 +80,34 @@ public async Task NativeAotTests_WillRunWithExitCodeZero(string tfm) .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion) .PatchCodeWithReplace("$TargetFramework$", tfm) .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) - .PatchCodeWithReplace("$MSTestEngineVersion$", MSTestEngineVersion), + .PatchCodeWithReplace("$MSTestSourceGenerationVersion$", MSTestSourceGenerationVersion), addPublicFeeds: true); + // Do NOT pass warnAsError: true here. MSTest.TestAdapter (required for the source-generator + // runtime hook host MSTestAdapter.PlatformServices.dll) transitively depends on the vstest + // Microsoft.TestPlatform.ObjectModel submodule and System.Private.DataContractSerialization, + // both of which emit trim/AOT warnings (IL20xx/IL30xx) outside this repo's control. Promoting + // them to errors would fail publish with NETSDK1144 before we can inspect the warning list. + // Instead, we assert below that MSTest-owned source files do not appear in publish output. DotnetMuxerResult compilationResult = await DotnetCli.RunAsync( $"publish {generator.TargetAssetPath} -r {RID} -f {tfm}", + warnAsError: false, cancellationToken: TestContext.CancellationToken); compilationResult.AssertOutputContains("Generating native code"); + // Source files in this repo (and the source-generator output filename) whose absence in + // publish output indicates MSTest itself is not surfacing trim/AOT warnings. Adding new MSTest + // code that produces ILxxxx warnings will cause its source file to show up here and fail this + // test. (The list mirrors TrimTests.Publish_WithTestAdapter_DoesNotSurfaceWarningsFromSuppressedSources.) + foreach (string fileName in TrimAndAotAssertions.MSTestOwnedSourceFiles) + { + compilationResult.AssertOutputDoesNotContain(fileName); + } + var testHost = TestHost.LocateFrom(generator.TargetAssetPath, "MSTestNativeAotTests", tfm, RID, Verb.publish); TestHostResult result = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); - result.AssertOutputContains($"MSTest.Engine v{MSTestEngineVersion}"); + result.AssertOutputContainsSummary(failed: 0, passed: 3, skipped: 0); result.AssertExitCodeIs(0); } diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs index e85040e40e..d6abc5d1a6 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs @@ -323,9 +323,19 @@ public async Task NativeAot_Smoke_Test() DotnetMuxerResult compilationResult = await DotnetCli.RunAsync( $"publish -r {RID} -f {TargetFrameworks.NetCurrent} {testAsset.TargetAssetPath}", + warnAsError: false, cancellationToken: TestContext.CancellationToken); compilationResult.AssertOutputContains("Generating native code"); + // MSTest.TestAdapter (referenced via MSTest.Sdk's NativeAOT runner) transitively pulls in + // vstest Microsoft.TestPlatform.ObjectModel and System.Private.DataContractSerialization which + // produce trim/AOT warnings outside this repo's control. Instead of failing on those warnings, + // we assert MSTest-owned source files do not appear in publish output. See TrimAndAotAssertions. + foreach (string fileName in TrimAndAotAssertions.MSTestOwnedSourceFiles) + { + compilationResult.AssertOutputDoesNotContain(fileName); + } + var testHost = TestHost.LocateFrom(testAsset.TargetAssetPath, AssetName, TargetFrameworks.NetCurrent, verb: Verb.publish); TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SourceGenerationNonAotTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SourceGenerationNonAotTests.cs new file mode 100644 index 0000000000..5f16d0f681 --- /dev/null +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SourceGenerationNonAotTests.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Acceptance.IntegrationTests; +using Microsoft.Testing.Platform.Acceptance.IntegrationTests.Helpers; + +namespace MSTest.Acceptance.IntegrationTests; + +/// +/// MSTest.SourceGeneration is most commonly used in Native AOT and trimming scenarios +/// (see and ), but the package itself +/// is a plain Roslyn source generator that should work in any SDK-style project. These +/// acceptance tests pin that behavior: referencing the package on a non-AOT, non-trimmed +/// project must still build, emit the generated metadata file, and successfully run tests +/// through the source-generated ReflectionMetadataHook module initializer. +/// +[TestClass] +public class SourceGenerationNonAotTests : AcceptanceTestBase +{ + private const string AssetName = "MSTestSourceGenNonAot"; + + // Source code for a non-AOT, non-trimmed test project that references MSTest.SourceGeneration. + // EmitCompilerGeneratedFiles is enabled so we can statically assert the generator ran and + // wrote out the expected metadata file alongside the assembly. + private const string SourceCode = """ +#file MSTestSourceGenNonAot.csproj + + + $TargetFramework$ + enable + enable + Exe + preview + true + + true + + + + + + + + + +#file TestClass1.cs +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MyTests; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void TestMethod1() + { + } + + [TestMethod] + [DataRow(0, 1)] + public void TestMethod2(int a, int b) + { + } +} +"""; + + [TestMethod] + [DynamicData(nameof(TargetFrameworks.NetForDynamicData), typeof(TargetFrameworks))] + public async Task SourceGenerationNonAot_BuildsAndRunsTests_WithExitCodeZero(string tfm) + { + using TestAsset generator = await TestAsset.GenerateAssetAsync( + $"{AssetName}_{tfm}", + SourceCode + .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion) + .PatchCodeWithReplace("$TargetFramework$", tfm) + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$MSTestSourceGenerationVersion$", MSTestSourceGenerationVersion), + addPublicFeeds: true); + + // Plain `dotnet build` — no -r RID, no publish, no PublishAot, no PublishTrimmed. + // This is the scenario the follow-up review surfaced: the source generator must also + // work for users who opt in to source-generated discovery without trimming/AOT. + DotnetMuxerResult buildResult = await DotnetCli.RunAsync( + $"build {generator.TargetAssetPath} -c {BuildConfiguration.Release} -f {tfm}", + cancellationToken: TestContext.CancellationToken); + buildResult.AssertExitCodeIs(0); + + // Static evidence the source generator actually ran in the build (not just that + // the package was restored). EmitCompilerGeneratedFiles writes the generator + // output under obj///generated///. + // The hint name uses the assembly name (from the csproj filename), not the asset + // directory name (which we suffix with tfm to keep parallel TFM runs isolated). + string objGenerated = Path.Combine(generator.TargetAssetPath, "obj", "Release", tfm, "generated"); + string[] generatedFiles = Directory.Exists(objGenerated) + ? Directory.GetFiles(objGenerated, $"{AssetName}.MSTestReflectionMetadata.g.cs", SearchOption.AllDirectories) + : []; + Assert.IsNotEmpty(generatedFiles, $"the source generator should have emitted '{AssetName}.MSTestReflectionMetadata.g.cs' under '{objGenerated}'"); + + // Behavioral evidence: tests still discover and run when the source-generated + // ReflectionMetadataHook is the only metadata provider wired in at module init. + // If the hook crashed during ModuleInitializer or swapped in a broken provider, the + // test host would fail before printing a summary. (We assert the full count to also + // catch silent discovery regressions where tests are not picked up.) + var testHost = TestHost.LocateFrom(generator.TargetAssetPath, AssetName, tfm, buildConfiguration: BuildConfiguration.Release); + TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); + testHostResult.AssertOutputContainsSummary(failed: 0, passed: 2, skipped: 0); + testHostResult.AssertExitCodeIs(0); + } + + public TestContext TestContext { get; set; } = null!; +} diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimAndAotAssertions.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimAndAotAssertions.cs new file mode 100644 index 0000000000..6c5b1a79da --- /dev/null +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimAndAotAssertions.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MSTest.Acceptance.IntegrationTests; + +/// +/// Shared allowlist of MSTest-owned source file names that must not appear in trim/AOT publish output. +/// +/// +/// MSTest's reflection-mode adapter transitively depends on the vstest +/// Microsoft.TestPlatform.ObjectModel submodule and System.Private.DataContractSerialization, both of +/// which emit trim warnings (IL20xx/IL30xx) outside this repo's control. We therefore cannot fail +/// publish on any trim warning. Instead, we assert that MSTest-owned source file names do not appear +/// in the publish output: the trimmer prints the originating source file path on every IL20xx/IL30xx +/// warning, so the absence of these file names in publish output is evidence that the suppressions in +/// this repo are still effective. Adding new MSTest code that produces trim warnings will surface its +/// source file here and fail any test that uses this list. +/// +internal static class TrimAndAotAssertions +{ + /// + /// MSTest-owned source files whose trim warnings are explicitly suppressed by this repo + /// (see https://github.com/microsoft/testfx/pull/8686), plus the source-generator output file + /// emitted by MSTest.SourceGeneration (see https://github.com/microsoft/testfx/pull/8586). + /// None of these should appear in the publish output of a trim/AOT-enabled consumer. + /// + public static readonly string[] MSTestOwnedSourceFiles = + [ + "TestSourceHost.cs", + "DeploymentUtilityBase.cs", + "ReflectionOperations.cs", + "AssemblyResolver.cs", + "DataSerializationHelper.cs", + "ManagedNameHelper.cs", + "MethodInfoExtensions.cs", + "TestMethodFilter.cs", + "SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs", + "ReflectionTestMethodInfo.cs", + + // MSTest.SourceGeneration emitted output (see Constants.GeneratedFileSuffix). Its presence + // would indicate the source generator emits IL-unsafe reflection calls. + "MSTestReflectionMetadata.g.cs", + ]; +} diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs index cfd469dded..2a4edea3b5 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TrimTests.cs @@ -19,47 +19,73 @@ public class TrimTests : AcceptanceTestBase $TargetFramework$ Exe + true true false - - + + - + -#file Program.cs -System.Console.WriteLine("This project validates trim/AOT compatibility via dotnet publish."); +#file UnitTest1.cs +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class UnitTest1 +{ + [TestMethod] + public void TestMethod1() + { + } +} """; [TestMethod] [DynamicData(nameof(TargetFrameworks.NetForDynamicData), typeof(TargetFrameworks))] - public async Task Publish_ShouldNotProduceTrimWarnings(string tfm) + public async Task Publish_WithSourceGeneration_DoesNotSurfaceMSTestOwnedTrimWarnings(string tfm) { - // See https://github.com/microsoft/testfx/issues/7153 - // This test forces deep trim analysis of MSTest assemblies using TrimmerRootAssembly - // to catch trim warnings that would not be caught by only testing reachable code paths. + // See https://github.com/microsoft/testfx/issues/7153. + // + // This test forces deep trim analysis of the MSTest.TestAdapter assembly using + // TrimmerRootAssembly to catch trim warnings that would not be caught by only testing the + // reachable code paths exercised at runtime. + // + // MSTest.TestAdapter transitively depends on the vstest Microsoft.TestPlatform.ObjectModel + // submodule and on System.Private.DataContractSerialization, both of which emit trim warnings + // outside this repo's control. We therefore do NOT promote warnings to errors here. Instead, + // we assert that MSTest-owned source file names do not appear in publish output. (Note: the + // test name and orientation changed in https://github.com/microsoft/testfx/pull/8586; the + // earlier "ShouldNotProduceTrimWarnings" name became inaccurate once MSTest.SourceGeneration + // started relying on MSTest.TestAdapter's reflection-mode adapter assemblies.) using TestAsset generator = await TestAsset.GenerateAssetAsync( $"MSTestTrimAnalysisTest_{tfm}", TrimAnalysisSourceCode .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion) .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) - .PatchCodeWithReplace("$MSTestEngineVersion$", MSTestEngineVersion) + .PatchCodeWithReplace("$MSTestSourceGenerationVersion$", MSTestSourceGenerationVersion) .PatchCodeWithReplace("$TargetFramework$", tfm), addPublicFeeds: true); - await DotnetCli.RunAsync( + DotnetMuxerResult result = await DotnetCli.RunAsync( $"publish {generator.TargetAssetPath} -r {RID} -f {tfm}", + warnAsError: false, cancellationToken: TestContext.CancellationToken); + + foreach (string fileName in TrimAndAotAssertions.MSTestOwnedSourceFiles) + { + result.AssertOutputDoesNotContain(fileName); + } } // Source code for a project that references MSTest.TestAdapter (the reflection-mode adapter) @@ -146,21 +172,7 @@ public async Task Publish_WithTestAdapter_DoesNotSurfaceWarningsFromSuppressedSo // Files in MSTest's own source whose trim warnings are suppressed by this PR. // The trimmer includes source file paths in its IL2xxx/IL3xxx warnings, so the absence // of these file names in publish output is evidence that the suppression attributes work. - string[] suppressedSourceFiles = - [ - "TestSourceHost.cs", - "DeploymentUtilityBase.cs", - "ReflectionOperations.cs", - "AssemblyResolver.cs", - "DataSerializationHelper.cs", - "ManagedNameHelper.cs", - "MethodInfoExtensions.cs", - "TestMethodFilter.cs", - "SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs", - "ReflectionTestMethodInfo.cs", - ]; - - foreach (string fileName in suppressedSourceFiles) + foreach (string fileName in TrimAndAotAssertions.MSTestOwnedSourceFiles) { result.AssertOutputDoesNotContain(fileName); } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs index 2f8329d525..7b5996a6d3 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/Helpers/AcceptanceTestBase.cs @@ -14,7 +14,7 @@ static AcceptanceTestBase() MSTestVersion = ExtractVersionFromPackage(Constants.ArtifactsPackagesShipping, "MSTest.TestFramework."); MicrosoftTestingPlatformVersion = ExtractVersionFromPackage(Constants.ArtifactsPackagesShipping, "Microsoft.Testing.Platform."); - MSTestEngineVersion = ExtractVersionFromPackage(Constants.ArtifactsPackagesShipping, "MSTest.Engine."); + MSTestSourceGenerationVersion = ExtractVersionFromPackage(Constants.ArtifactsPackagesShipping, "MSTest.SourceGeneration."); MicrosoftTestingExtensionsLoggingVersion = ExtractVersionFromPackage(Constants.ArtifactsPackagesShipping, "Microsoft.Testing.Extensions.Logging."); } @@ -29,7 +29,7 @@ static AcceptanceTestBase() public static string MSTestVersion { get; private set; } - public static string MSTestEngineVersion { get; private set; } + public static string MSTestSourceGenerationVersion { get; private set; } public static string MicrosoftNETTestSdkVersion { get; private set; } diff --git a/test/UnitTests/MSTest.Engine.UnitTests/Adapter_ExecuteRequestAsyncTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/Adapter_ExecuteRequestAsyncTests.cs deleted file mode 100644 index d7fb5bb4a5..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/Adapter_ExecuteRequestAsyncTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Testing.Platform.Configurations; -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.Helpers; -using Microsoft.Testing.Platform.Logging; -using Microsoft.Testing.Platform.Messages; -using Microsoft.Testing.Platform.Requests; -using Microsoft.Testing.Platform.Services; - -namespace Microsoft.Testing.Framework.UnitTests; - -[TestClass] -public class Adapter_ExecuteRequestAsyncTests : TestBase -{ - [TestMethod] - public async Task ExecutableNode_ThatDoesNotThrow_ShouldReportPassed() - { - // Arrange - var testNode = new InternalUnsafeActionTestNode - { - StableUid = "Microsoft.Testing.Framework.UnitTests.Adapter_ExecuteRequestAsyncTests.ExecutableNode_ThatDoesNotThrow_ShouldReportPassed()", - DisplayName = "Microsoft.Testing.Framework.UnitTests.Adapter_ExecuteRequestAsyncTests.ExecutableNode_ThatDoesNotThrow_ShouldReportPassed()", - Body = testExecutionContext => { }, - }; - - var services = new Services(); - var adapter = new TestFramework(new(), [new FactoryTestNodesBuilder(() => [testNode])], new(), - services.ServiceProvider.GetSystemClock(), services.ServiceProvider.GetTask(), services.ServiceProvider.GetConfiguration(), new Platform.Capabilities.TestFramework.TestFrameworkCapabilities()); - - CancellationToken cancellationToken = CancellationToken.None; - - // Act - await adapter.ExecuteRequestAsync(new( - new RunTestExecutionRequest(new(new("id"))), - services.ServiceProvider.GetRequiredService(), - new SemaphoreSlimRequestCompleteNotifier(new SemaphoreSlim(1)), - cancellationToken)); - - // Assert - IEnumerable nodeStateChanges = services.MessageBus.Messages.OfType(); - Assert.IsNotEmpty(nodeStateChanges, $"{nameof(nodeStateChanges)} should have at least 1 item."); - Platform.Extensions.Messages.TestNode lastNode = nodeStateChanges.Last().TestNode; - _ = lastNode.Properties.Single(); - } - - [TestMethod] - public async Task ExecutableNode_ThatThrows_ShouldReportError() - { - // Arrange - var testNode = new InternalUnsafeActionTestNode - { - StableUid = "Microsoft.Testing.Framework.UnitTests.Adapter_ExecuteRequestAsyncTests.ExecutableNode_ThatThrows_ShouldReportError()", - DisplayName = "Microsoft.Testing.Framework.UnitTests.Adapter_ExecuteRequestAsyncTests.ExecutableNode_ThatThrows_ShouldReportError()", - Body = testExecutionContext => throw new InvalidOperationException("Oh no!") { }, - }; - - var services = new Services(); - var fakeClock = (FakeClock)services.ServiceProvider.GetService(typeof(FakeClock))!; - - var adapter = new TestFramework(new(), [new FactoryTestNodesBuilder(() => [testNode])], new(), - services.ServiceProvider.GetSystemClock(), services.ServiceProvider.GetTask(), services.ServiceProvider.GetConfiguration(), new Platform.Capabilities.TestFramework.TestFrameworkCapabilities()); - CancellationToken cancellationToken = CancellationToken.None; - - // Act - await adapter.ExecuteRequestAsync(new( - new RunTestExecutionRequest(new(new("id"))), - services.ServiceProvider.GetRequiredService(), - new SemaphoreSlimRequestCompleteNotifier(new SemaphoreSlim(1)), - cancellationToken)); - - // Assert - IEnumerable nodeStateChanges = services.MessageBus.Messages.OfType(); - Assert.IsNotEmpty(nodeStateChanges, $"{nameof(nodeStateChanges)} should have at least 1 item."); - Platform.Extensions.Messages.TestNode lastNode = nodeStateChanges.Last().TestNode; - _ = lastNode.Properties.Single(); - Assert.AreEqual("Oh no!", lastNode.Properties.Single().Exception!.Message); - Assert.Contains( - nameof(ExecutableNode_ThatThrows_ShouldReportError), lastNode.Properties.Single().Exception!.StackTrace!, "lastNode properties should contain the name of the test"); - TimingProperty timingProperty = lastNode.Properties.Single(); - Assert.AreEqual(fakeClock.UsedTimes[0], timingProperty.GlobalTiming.StartTime); - Assert.IsLessThanOrEqualTo(timingProperty.GlobalTiming.EndTime, timingProperty.GlobalTiming.StartTime, "start time is before (or the same as) stop time"); - Assert.AreEqual(fakeClock.UsedTimes[1], timingProperty.GlobalTiming.EndTime); - Assert.IsGreaterThan(0, timingProperty.GlobalTiming.Duration.TotalMilliseconds, $"duration should be greater than 0"); - } - - private sealed class FakeClock : IClock - { - public List UsedTimes { get; } = []; - - public DateTimeOffset UtcNow - { - get - { - DateTimeOffset date = DateTimeOffset.UtcNow; - UsedTimes.Add(date); - return date; - } - } - } - - private sealed class Services - { - public Services() - { - MessageBus = new MessageBus(); - ServiceProvider.AddService(MessageBus); - ServiceProvider.AddService(new LoggerFactory()); - ServiceProvider.AddService(new FakeClock()); - ServiceProvider.AddService(new SystemTask()); - ServiceProvider.AddService(new AggregatedConfiguration([], new CurrentTestApplicationModuleInfo(new SystemEnvironment(), new SystemProcessHandler()), new SystemFileSystem(), new SystemEnvironment(), new(null, [], []))); - } - - public MessageBus MessageBus { get; } - - public ServiceProvider ServiceProvider { get; } = new(); - } - - private sealed class MessageBus : IMessageBus - { - public List Messages { get; } = []; - - public Task PublishAsync(IDataProducer dataProducer, IData data) - { - Messages.Add(data); - return Task.CompletedTask; - } - } - - private sealed class LoggerFactory : ILoggerFactory - { - public ILogger CreateLogger(string categoryName) => new NopLogger(); - } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/BFSTestNodeVisitorTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/BFSTestNodeVisitorTests.cs deleted file mode 100644 index ee0af6bd31..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/BFSTestNodeVisitorTests.cs +++ /dev/null @@ -1,413 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Testing.Platform.Extensions.Messages; -using Microsoft.Testing.Platform.Requests; - -namespace Microsoft.Testing.Framework.UnitTests; - -[TestClass] -public sealed class BFSTestNodeVisitorTests : TestBase -{ - [TestMethod] - public async Task Visit_WhenFilterDoesNotUseEncodedSlash_NodeIsNotIncluded() - { - // Arrange - var rootNode = new TestNode - { - StableUid = "ID1", - DisplayName = "A", - Tests = - [ - new TestNode - { - StableUid = "ID2", - DisplayName = "B/C", - }, - ], - }; - - var filter = new TreeNodeFilter("/A/B/C"); - var visitor = new BFSTestNodeVisitor(new[] { rootNode }, filter, null!); - - // Act - List includedTestNodes = []; - await visitor.VisitAsync((testNode, _) => - { - includedTestNodes.Add(testNode); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(1, includedTestNodes); - Assert.AreEqual("ID1", includedTestNodes[0].StableUid); - } - - [DataRow("/", "%2F")] - [DataRow("%2F", "%252F")] - [DataRow("//", "%2F%2F")] - [TestMethod] - public async Task Visit_WhenFilterUsesEncodedEntry_NodeIsIncluded(string nodeSpecialString, string filterEncodedSpecialString) - { - // Arrange - var rootNode = new TestNode - { - StableUid = "ID1", - DisplayName = "A", - Tests = - [ - new TestNode - { - StableUid = "ID2", - DisplayName = "B" + nodeSpecialString + "C", - }, - ], - }; - - var filter = new TreeNodeFilter("/A/B" + filterEncodedSpecialString + "C"); - var visitor = new BFSTestNodeVisitor(new[] { rootNode }, filter, null!); - - // Act - List includedTestNodes = []; - await visitor.VisitAsync((testNode, _) => - { - includedTestNodes.Add(testNode); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(2, includedTestNodes); - Assert.AreEqual("ID1", includedTestNodes[0].StableUid); - Assert.AreEqual("ID2", includedTestNodes[1].StableUid); - } - - [DataRow(nameof(TestNode))] - [DataRow(nameof(InternalUnsafeActionTestNode))] - [TestMethod] - public async Task Visit_WhenNodeIsNotParameterizedNode_DoesNotExpand(string nonParameterizedTestNode) - { - // Arrange - TestNode rootNode = nonParameterizedTestNode switch - { - nameof(TestNode) => new TestNode - { - StableUid = "ID1", - DisplayName = "A", - }, - nameof(InternalUnsafeActionTestNode) => new InternalUnsafeActionTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = _ => { }, - }, - _ => throw new ArgumentException($"Unknown test node type: {nonParameterizedTestNode}", nameof(nonParameterizedTestNode)), - }; - - var visitor = new BFSTestNodeVisitor(new[] { rootNode }, new NopFilter(), null!); - - // Act - List includedTestNodes = []; - await visitor.VisitAsync((testNode, _) => - { - includedTestNodes.Add(testNode); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(1, includedTestNodes); - Assert.AreEqual("ID1", includedTestNodes[0].StableUid); - } - - [DataRow(nameof(InternalUnsafeActionParameterizedTestNode<>), true)] - [DataRow(nameof(InternalUnsafeActionParameterizedTestNode<>), false)] - [DataRow(nameof(InternalUnsafeAsyncActionParameterizedTestNode<>), true)] - [DataRow(nameof(InternalUnsafeAsyncActionParameterizedTestNode<>), false)] - [DataRow(nameof(InternalUnsafeActionTaskParameterizedTestNode<>), true)] - [DataRow(nameof(InternalUnsafeActionTaskParameterizedTestNode<>), false)] - [DataRow(nameof(InternalUnsafeAsyncActionTaskParameterizedTestNode<>), true)] - [DataRow(nameof(InternalUnsafeAsyncActionTaskParameterizedTestNode<>), false)] - [TestMethod] - public async Task Visit_WhenNodeIsParameterizedNodeAndPropertyIsAbsentOrTrue_ExpandNode(string parameterizedTestNode, bool hasExpansionProperty) - { - // Arrange - TestNode rootNode = CreateParameterizedTestNode(parameterizedTestNode, hasExpansionProperty ? false : null); - var visitor = new BFSTestNodeVisitor(new[] { rootNode }, new NopFilter(), new TestArgumentsManager()); - - // Act - List<(TestNode Node, TestNodeUid? ParentNodeUid)> includedTestNodes = []; - await visitor.VisitAsync((testNode, parentNodeUid) => - { - includedTestNodes.Add((testNode, parentNodeUid)); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(3, includedTestNodes); - - Assert.AreEqual("ID1", includedTestNodes[0].Node.StableUid); - Assert.IsNull(includedTestNodes[0].ParentNodeUid); - - Assert.AreEqual("ID1 [0]", includedTestNodes[1].Node.StableUid); - Assert.AreEqual("ID1", includedTestNodes[1].ParentNodeUid); - - Assert.AreEqual("ID1 [1]", includedTestNodes[2].Node.StableUid); - Assert.AreEqual("ID1", includedTestNodes[2].ParentNodeUid); - } - - [DataRow(nameof(InternalUnsafeActionParameterizedTestNode<>))] - [DataRow(nameof(InternalUnsafeAsyncActionParameterizedTestNode<>))] - [DataRow(nameof(InternalUnsafeActionTaskParameterizedTestNode<>))] - [DataRow(nameof(InternalUnsafeAsyncActionTaskParameterizedTestNode<>))] - [TestMethod] - public async Task Visit_WhenNodeIsParameterizedNodeAndDoesNotAllowExpansion_DoesNotExpand(string parameterizedTestNode) - { - // Arrange - TestNode rootNode = CreateParameterizedTestNode(parameterizedTestNode, true); - var visitor = new BFSTestNodeVisitor(new[] { rootNode }, new NopFilter(), new TestArgumentsManager()); - - // Act - List<(TestNode Node, TestNodeUid? ParentNodeUid)> includedTestNodes = []; - await visitor.VisitAsync((testNode, parentNodeUid) => - { - includedTestNodes.Add((testNode, parentNodeUid)); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(1, includedTestNodes); - } - - [DataRow(nameof(InternalUnsafeActionParameterizedTestNode<>))] - [DataRow(nameof(InternalUnsafeAsyncActionParameterizedTestNode<>))] - [DataRow(nameof(InternalUnsafeActionTaskParameterizedTestNode<>))] - [DataRow(nameof(InternalUnsafeAsyncActionTaskParameterizedTestNode<>))] - [TestMethod] - public async Task ParameterizedNodes_InvokeAsync_InvokesBodyForEachArgument(string parameterizedTestNode) - { - // Arrange - List invokedArguments = []; - IParameterizedAsyncActionTestNode parameterizedNode = parameterizedTestNode switch - { - nameof(InternalUnsafeActionParameterizedTestNode<>) => new InternalUnsafeActionParameterizedTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = (_, argument) => invokedArguments.Add(argument), - GetArguments = () => new byte[] { 0, 1 }, - }, - nameof(InternalUnsafeAsyncActionParameterizedTestNode<>) => new InternalUnsafeAsyncActionParameterizedTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = (_, argument) => - { - invokedArguments.Add(argument); - return Task.CompletedTask; - }, - GetArguments = () => new byte[] { 0, 1 }, - }, - nameof(InternalUnsafeActionTaskParameterizedTestNode<>) => new InternalUnsafeActionTaskParameterizedTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = (_, argument) => invokedArguments.Add(argument), - GetArguments = static () => Task.FromResult>(new byte[] { 0, 1 }), - }, - nameof(InternalUnsafeAsyncActionTaskParameterizedTestNode<>) => new InternalUnsafeAsyncActionTaskParameterizedTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = (_, argument) => - { - invokedArguments.Add(argument); - return Task.CompletedTask; - }, - GetArguments = static () => Task.FromResult>(new byte[] { 0, 1 }), - }, - _ => throw new ArgumentException($"Unknown test node type: {parameterizedTestNode}", nameof(parameterizedTestNode)), - }; - - int safeInvokeCount = 0; - - // Act - await parameterizedNode.InvokeAsync( - null!, - async callback => - { - safeInvokeCount++; - await callback(); - }); - - // Assert - Assert.HasCount(2, invokedArguments); - Assert.AreEqual((byte)0, invokedArguments[0]); - Assert.AreEqual((byte)1, invokedArguments[1]); - Assert.AreEqual(2, safeInvokeCount); - } - - [TestMethod] - public async Task Visit_WithModuleNamespaceClassMethodLevelAndExpansion_DiscoverTestsWithCorrectParentsAndTypes() - { - // Arrange - var rootNode = new TestNode - { - StableUid = "MyModule", - DisplayName = "MyModule", - Tests = - [ - new TestNode - { - StableUid = "MyNamespace", - DisplayName = "MyNamespace", - Tests = - [ - new TestNode - { - StableUid = "MyType", - DisplayName = "MyType", - Tests = new[] - { - new InternalUnsafeActionParameterizedTestNode - { - StableUid = "MyMethod", - DisplayName = "MyMethod", - GetArguments = () => new byte[] { 0, 1, 2 }, - Body = (_, _) => { }, - }, - }, - }, - ], - }, - ], - }; - var visitor = new BFSTestNodeVisitor(new[] { rootNode }, new NopFilter(), new TestArgumentsManager()); - - // Act - List<(TestNode Node, TestNodeUid? ParentNodeUid)> includedTestNodes = []; - await visitor.VisitAsync((testNode, parentNodeUid) => - { - includedTestNodes.Add((testNode, parentNodeUid)); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(7, includedTestNodes); - - Assert.AreEqual("MyModule", includedTestNodes[0].Node.StableUid); - Assert.IsNull(includedTestNodes[0].ParentNodeUid); - Assert.AreEqual(typeof(TestNode), includedTestNodes[0].Node.GetType()); - - Assert.AreEqual("MyNamespace", includedTestNodes[1].Node.StableUid); - Assert.AreEqual("MyModule", includedTestNodes[1].ParentNodeUid); - Assert.AreEqual(typeof(TestNode), includedTestNodes[1].Node.GetType()); - - Assert.AreEqual("MyType", includedTestNodes[2].Node.StableUid); - Assert.AreEqual("MyNamespace", includedTestNodes[2].ParentNodeUid); - Assert.AreEqual(typeof(TestNode), includedTestNodes[2].Node.GetType()); - - Assert.AreEqual("MyMethod", includedTestNodes[3].Node.StableUid); - Assert.AreEqual("MyType", includedTestNodes[3].ParentNodeUid); - Assert.AreEqual(typeof(TestNode), includedTestNodes[3].Node.GetType()); - - Assert.AreEqual("MyMethod [0]", includedTestNodes[4].Node.StableUid); - Assert.AreEqual("MyMethod", includedTestNodes[4].ParentNodeUid); - Assert.AreEqual(typeof(InternalUnsafeActionTestNode), includedTestNodes[4].Node.GetType()); - - Assert.AreEqual("MyMethod [1]", includedTestNodes[5].Node.StableUid); - Assert.AreEqual("MyMethod", includedTestNodes[5].ParentNodeUid); - Assert.AreEqual(typeof(InternalUnsafeActionTestNode), includedTestNodes[5].Node.GetType()); - - Assert.AreEqual("MyMethod [2]", includedTestNodes[6].Node.StableUid); - Assert.AreEqual("MyMethod", includedTestNodes[6].ParentNodeUid); - Assert.AreEqual(typeof(InternalUnsafeActionTestNode), includedTestNodes[6].Node.GetType()); - } - - [TestMethod] - public async Task Visit_WhenFilterHasPropertyExpression_OnlyIncludesNodesMatchingProperty() - { - // Arrange — filter with a property expression (ContainsPropertyFilters == true) - var nodeWithMatchingTag = new TestNode - { - StableUid = "ID1", - DisplayName = "A", - Properties = [new TestMetadataProperty("Tag", "Fast")], - }; - var nodeWithNonMatchingTag = new TestNode - { - StableUid = "ID2", - DisplayName = "A", - Properties = [new TestMetadataProperty("Tag", "Slow")], - }; - - var filter = new TreeNodeFilter("/A[Tag=Fast]"); - var visitor = new BFSTestNodeVisitor(new[] { nodeWithMatchingTag, nodeWithNonMatchingTag }, filter, null!); - - // Act - List includedTestNodes = []; - await visitor.VisitAsync((testNode, _) => - { - includedTestNodes.Add(testNode); - return Task.CompletedTask; - }); - - // Assert - Assert.HasCount(1, includedTestNodes); - Assert.AreEqual("ID1", includedTestNodes[0].StableUid); - } - - private static TestNode CreateParameterizedTestNode(string parameterizedTestNode, bool? expansionPropertyValue) - { - TestNode rootNode = parameterizedTestNode switch - { - nameof(InternalUnsafeActionParameterizedTestNode<>) => new InternalUnsafeActionParameterizedTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = (_, _) => { }, - GetArguments = GetArguments, - Properties = GetProperties(expansionPropertyValue), - }, - nameof(InternalUnsafeAsyncActionParameterizedTestNode<>) => new InternalUnsafeAsyncActionParameterizedTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = static (_, _) => Task.CompletedTask, - GetArguments = GetArguments, - Properties = GetProperties(expansionPropertyValue), - }, - nameof(InternalUnsafeActionTaskParameterizedTestNode<>) => new InternalUnsafeActionTaskParameterizedTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = (_, _) => { }, - GetArguments = GetArgumentsAsync, - Properties = GetProperties(expansionPropertyValue), - }, - nameof(InternalUnsafeAsyncActionTaskParameterizedTestNode<>) => new InternalUnsafeAsyncActionTaskParameterizedTestNode - { - StableUid = "ID1", - DisplayName = "A", - Body = static (_, _) => Task.CompletedTask, - GetArguments = GetArgumentsAsync, - Properties = GetProperties(expansionPropertyValue), - }, - _ => throw new ArgumentException($"Unknown test node type: {parameterizedTestNode}", nameof(parameterizedTestNode)), - }; - - return rootNode; - - // Local functions - static IEnumerable GetArguments() => new byte[] { 0, 1 }; - static Task> GetArgumentsAsync() => Task.FromResult>(new byte[] { 0, 1 }); - static IProperty[] GetProperties(bool? hasExpansionProperty) - => hasExpansionProperty.HasValue - ? - [ - new FrameworkEngineMetadataProperty - { - PreventArgumentsExpansion = hasExpansionProperty.Value, - }, - ] - : []; - } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/BannedSymbols.txt b/test/UnitTests/MSTest.Engine.UnitTests/BannedSymbols.txt deleted file mode 100644 index ab9946cf24..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/BannedSymbols.txt +++ /dev/null @@ -1 +0,0 @@ -N:AwesomeAssertions; Use MSTest assertions instead. diff --git a/test/UnitTests/MSTest.Engine.UnitTests/DataRowTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/DataRowTests.cs deleted file mode 100644 index fb36b9924f..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/DataRowTests.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.Testing.Framework.UnitTests; - -/// -/// This class uses DataRows, to prove that running such tests works. -/// -[TestClass] -public class DataRowTests -{ - [DataRow(1, 2)] - [DataRow(2, 3)] - [TestMethod] - public void DataRowDataAreConsumed(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/DynamicDataNameProviderTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/DynamicDataNameProviderTests.cs deleted file mode 100644 index f6c7004659..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/DynamicDataNameProviderTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.Testing.Framework.UnitTests; - -[TestClass] -public class DynamicDataNameProviderTests -{ - [TestMethod] - public void NullTranslatesToNullString() - { - // Comment in DynamicDataAttribute says: - // We want to force call to `data.AsEnumerable()` to ensure that objects are casted to strings (using ToString()) - // so that null do appear as "null". If you remove the call, and do string.Join(",", new object[] { null, "a" }), - // you will get empty string while with the call you will get "null,a". - // - // check that this is still true: - string fragment = DynamicDataNameProvider.GetUidFragment(["parameter1", "parameter2"], [null, "a"], 0); - Assert.AreEqual("(parameter1: null, parameter2: a)[0]", fragment); - } - - [TestMethod] - public void ParameterMismatchShowsDataInMessage() - { - // Comment in DynamicDataAttribute says: - // We want to force call to `data.AsEnumerable()` to ensure that objects are casted to strings (using ToString()) - // so that null do appear as "null". If you remove the call, and do string.Join(",", new object[] { null, "a" }), - // you will get empty string while with the call you will get "null,a". - // - // check that this is still true: - ArgumentException exception = Assert.ThrowsExactly(() => DynamicDataNameProvider.GetUidFragment(["parameter1"], [null, "a"], 0)); - Assert.AreEqual("Parameter count mismatch. The provided data (null, a) have 2 items, but there are 1 parameters.", exception.Message); - } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/DynamicDataTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/DynamicDataTests.cs deleted file mode 100644 index 7c4ba68622..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/DynamicDataTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.Testing.Framework.UnitTests; - -/// -/// This class uses DynamicData, to prove that running such tests works. -/// -[TestClass] -public class DynamicDataTests -{ - public static IEnumerable IntDataProperty - => - [ - [1, 2], - [2, 3] - ]; - - [DynamicData(nameof(IntDataProperty))] - [TestMethod] - public void DynamicDataWithIntProperty(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); - - [DynamicData(nameof(IntDataProperty))] - [TestMethod] - public void DynamicDataWithIntPropertyAndExplicitSourceType(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); - - [DynamicData(nameof(IntDataMethod))] - [TestMethod] - public void DynamicDataWithIntMethod(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); - - [DynamicData(nameof(IntDataMethod))] - [TestMethod] - public void DynamicDataWithIntMethodAndExplicitSourceType(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); - - public static IEnumerable IntDataMethod() - => - [ - [1, 2], - [2, 3] - ]; - - [DynamicData(nameof(IntDataProperty), typeof(DataClass))] - [TestMethod] - public void DynamicDataWithIntPropertyOnSeparateClass(int expected, int actualPlus2) - => Assert.AreEqual(expected, actualPlus2 - 2); - - [DynamicData(nameof(IntDataMethod), typeof(DataClass))] - [TestMethod] - public void DynamicDataWithIntMethodOnSeparateClass(int expected, int actualPlus2) - => Assert.AreEqual(expected, actualPlus2 - 2); - - [DynamicData(nameof(UserDataProperty))] - [TestMethod] - public void DynamicDataWithUserProperty(User _, User _2) - { - } - - public static IEnumerable UserDataProperty - => - [ - [new User("Jakub"), new User("Amaury")], - [new User("Marco"), new User("Pavel")] - ]; - - [DynamicData(nameof(UserDataMethod))] - [TestMethod] - public void DynamicDataWithUserMethod(User _, User _2) - { - } - - public static IEnumerable UserDataMethod() - => - [ - [new User("Jakub"), new User("Amaury")], - [new User("Marco"), new User("Pavel")] - ]; -} - -public class DataClass -{ - public static IEnumerable IntDataProperty - => - [ - [1, 3], - [2, 4] - ]; - - public static IEnumerable IntDataMethod() - => - [ - [1, 3], - [2, 4] - ]; -} - -public class User -{ - public User(string name) - { - Name = name; - } - - public string Name { get; } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.csproj b/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.csproj deleted file mode 100644 index d31bcd6ae9..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - $(SupportedNetFrameworks);net462 - Microsoft.Testing.Framework.UnitTests - true - true - true - Exe - - - - - - - - - PreserveNewest - - - - - - - - MicrosoftTestingPlatformMSBuild - - - - - - - - Analyzer - false - - - - diff --git a/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.launcher.config.json b/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.launcher.config.json deleted file mode 100644 index 772598e7f9..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.launcher.config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "program": "MSTest.Engine.UnitTests.exe" -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.testingplatformconfig.json b/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.testingplatformconfig.json deleted file mode 100644 index 4c41cda3a3..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.testingplatformconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "testingplatform": { - "telemetry": { - "isDevelopmentRepository": true - }, - "exitProcessOnUnhandledException": true - } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/MSTestEngineBannerCapabilityTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/MSTestEngineBannerCapabilityTests.cs deleted file mode 100644 index 8e76f81f03..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/MSTestEngineBannerCapabilityTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Testing.Platform.Services; - -namespace Microsoft.Testing.Framework.UnitTests; - -[TestClass] -public sealed class MSTestEngineBannerCapabilityTests -{ - [TestMethod] - public async Task GetBannerMessageAsync_IncludesVersionAndBuildDate() - { - var buildDate = new DateTimeOffset(2026, 01, 02, 03, 04, 05, TimeSpan.Zero); - var sut = new MSTestEngineBannerCapability(new PlatformInformationStub(buildDate)); - - string? bannerMessage = await sut.GetBannerMessageAsync(); - - Assert.IsNotNull(bannerMessage); - Assert.StartsWith($"MSTest.Engine v{MSTestEngineRepositoryVersion.Version}", bannerMessage); - Assert.Contains($"(UTC {buildDate.UtcDateTime.ToShortDateString()})", bannerMessage); - } - - [TestMethod] - public async Task GetBannerMessageAsync_DoesNotIncludeBuildDate_WhenBuildDateIsNotAvailable() - { - var sut = new MSTestEngineBannerCapability(new PlatformInformationStub(null)); - - string? bannerMessage = await sut.GetBannerMessageAsync(); - - Assert.IsNotNull(bannerMessage); - Assert.StartsWith($"MSTest.Engine v{MSTestEngineRepositoryVersion.Version}", bannerMessage); - Assert.IsFalse(bannerMessage.Contains("(UTC ", StringComparison.Ordinal)); - } - - private sealed class PlatformInformationStub(DateTimeOffset? buildDate) : IPlatformInformation - { - public string Name => "Test"; - - public DateTimeOffset? BuildDate => buildDate; - - public string? Version => null; - - public string? CommitHash => null; - } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/Program.cs b/test/UnitTests/MSTest.Engine.UnitTests/Program.cs deleted file mode 100644 index de3b386a21..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/Program.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Testing.Extensions; - -using ExecutionScope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope; - -[assembly: Parallelize(Scope = ExecutionScope.MethodLevel, Workers = 0)] - -ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); -builder.AddMSTest(() => [Assembly.GetEntryAssembly()!]); - -#if ENABLE_CODECOVERAGE -builder.AddCodeCoverageProvider(); -#endif -builder.AddHangDumpProvider(); -builder.AddCrashDumpProvider(ignoreIfNotSupported: true); -builder.AddTrxReportProvider(); -builder.AddAppInsightsTelemetryProvider(); -builder.AddAzureDevOpsProvider(); - -using ITestApplication app = await builder.BuildAsync(); -return await app.RunAsync(); diff --git a/test/UnitTests/MSTest.Engine.UnitTests/Properties/launchSettings.json b/test/UnitTests/MSTest.Engine.UnitTests/Properties/launchSettings.json deleted file mode 100644 index c916cf8fb9..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "MSTest.Engine.UnitTests": { - "commandName": "Project", - "commandLineArgs": "", - "environmentVariables": { - //"TESTINGPLATFORM_HOTRELOAD_ENABLED": "1" - } - } - } -} diff --git a/test/UnitTests/MSTest.Engine.UnitTests/TestBase.cs b/test/UnitTests/MSTest.Engine.UnitTests/TestBase.cs deleted file mode 100644 index 0e748f47ce..0000000000 --- a/test/UnitTests/MSTest.Engine.UnitTests/TestBase.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.Testing.Framework.UnitTests; - -/// -/// Empty test base, because TestInfrastructure project depends on Testing.Framework, and we cannot use that. -/// -public abstract class TestBase; diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/DataRowAttributeGenerationTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/DataRowAttributeGenerationTests.cs deleted file mode 100644 index 4c46d80fea..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/DataRowAttributeGenerationTests.cs +++ /dev/null @@ -1,845 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Generators; - -[TestClass] -public sealed class DataRowAttributeGenerationTests : TestBase -{ - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task DataRowAttribute_HandlesPrimitiveTypes() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod] - [DataRow(true)] - public Task MethodWithBool(bool b) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithByte(byte b) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithSbyte(sbyte b) - => Task.CompletedTask; - - [TestMethod] - [DataRow('a')] - public Task MethodWithChar(char c) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithDecimal(decimal d) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithDouble(double d) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithFloat(float f) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithInt(int i) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithUInt(uint i) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithLong(long l) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithULong(ulong l) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithShort(short s) - => Task.CompletedTask; - - [TestMethod] - [DataRow(1)] - public Task MethodWithUShort(ushort s) - => Task.CompletedTask; - } - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(true, "b: true"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "b: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "b: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry('a', "c: 'a'"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "d: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "d: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "f: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "i: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "i: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "l: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "l: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "s: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(1, "s: 1"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_WhenGivenMultipleValues_GeneratesTupleData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow("a", 1)] - public Task TestMethod(string s, int i) - { - return Task.CompletedTask; - } - - [TestMethod, DataRow("a", 1, true, 1.0)] - public Task TestMethod(string s, int i, bool b, double d) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry<(string s, int i)>[] - { - new MSTF::InternalUnsafeTestArgumentsEntry<(string s, int i)>(("a", 1), "s: \"a\", i: 1"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry<(string s, int i, bool b, double d)>[] - { - new MSTF::InternalUnsafeTestArgumentsEntry<(string s, int i, bool b, double d)>(("a", 1, true, 1), "s: \"a\", i: 1, b: true, d: 1"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_HandlesEscapedStrings() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod] - [DataRow("\"abc\"")] - [DataRow(@"a\b\c")] - public Task TestMethod(string a) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry("\"abc\"", "a: \"\"abc\"\""), - new MSTF::InternalUnsafeTestArgumentsEntry("a\\b\\c", "a: \"a\\b\\c\""), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_WhenGivenTypeofOtherType_GeneratesDataWithFullType() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - internal class MyClass - { - - } - - [TestClass] - public class TestClass - { - [TestMethod] - [DataRow(typeof(MyClass))] - public Task TestMethod(Type a) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(typeof(MyNamespace.MyClass), "a: typeof(MyNamespace.MyClass)"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_WithEnumAsChildFromParentClass_GeneratesDataWithFullType() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class ParentClass - { - - public enum MyEnum - { - One, - } - - [TestClass] - public class TestClass - { - [TestMethod, DataRow(MyEnum.One)] - public Task TestMethod(MyEnum a) - { - return Task.CompletedTask; - } - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(global::MyNamespace.ParentClass.MyEnum.One, "a: global::MyNamespace.ParentClass.MyEnum.One"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_WithEnumAsSubNamespaceDoesNotShadowTypeFromAnotherNamespace_GeneratesDataWithFullGlobalType() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - // This gives us a way to reference to the MyEnum (1) - // in the DataRow. - using MyEnum = global::ConflictingNamespace.MyEnum; - - namespace ConflictingNamespace { - public enum MyEnum // 1 - { - One, - } - } - - namespace MyNamespace - { - namespace ConflictingNamespace { - public enum MyEnum // 2 - { - Two, - } - } - - [TestClass] - public class TestClass - { - // If the generated code from here emits just - // ConflictingNamespace.MyEnum.One, we will get an error - // saying that MyEnum does not have definition for One, - // because we are resolving the type by the relative namespace, - // and so we find MyNamespace.ConflictingNamespace.MyEnum, which is MyEnum (2). - [TestMethod, DataRow(MyEnum.One)] - public Task TestMethod(MyEnum a) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(global::ConflictingNamespace.MyEnum.One, "a: global::ConflictingNamespace.MyEnum.One"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_WithEnumAsSubNamespaceDoesNotShadowTypeFromAnotherNamespaceAndUsesChildType_GeneratesDataWithFullGlobalType() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace ConflictingNamespace { - public enum MyEnum // 1 - { - One, - } - } - - namespace MyNamespace - { - - namespace ConflictingNamespace { - public enum MyEnum // 2 - { - Two, - } - } - - [TestClass] - public class TestClass - { - // This refers to MyEnum (2), we should emit a full type - // with global:: into the code. - [TestMethod, DataRow(ConflictingNamespace.MyEnum.Two)] - public Task TestMethod(ConflictingNamespace.MyEnum a) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(global::MyNamespace.ConflictingNamespace.MyEnum.Two, "a: global::MyNamespace.ConflictingNamespace.MyEnum.Two"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_GivenNullValues_GeneratesCorrectData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(null)] - public Task TestMethod1(string a) - { - return Task.CompletedTask; - } - - [TestMethod, DataRow(null)] - public Task TestMethod1(object a) - { - return Task.CompletedTask; - } - - [TestMethod, DataRow(null, null)] - public Task TestMethod1(string s, object a) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(null, "a: null"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(null, "a: null"), - }, - """); - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry<(string s, object a)>[] - { - new MSTF::InternalUnsafeTestArgumentsEntry<(string s, object a)>((null, null), "s: null, a: null"), - }, - """); - } - - [TestMethod] - public async Task DataRowAttribute_WhenMissingAttribute_OutputsCommentAboveTheTestNodeAndFailsToCompile() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod] - public Task TestMethod2(string a, string b) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertFailedGeneration( - "*CS0308: The non-generic type 'InternalUnsafeAsyncActionTestNode' cannot be used with type arguments*", - "*CS9035: Required member 'TestNode.StableUid' must be set in the object initializer or attribute constructor.*", - "*CS9035: Required member 'TestNode.DisplayName' must be set in the object initializer or attribute constructor.*", - "*CS9035: Required member 'InternalUnsafeAsyncActionTestNode.Body' must be set in the object initializer or attribute constructor.*"); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - // The test method is parameterized but no argument was specified. - // This is most often caused by using an unsupported arguments input. - // Possible resolutions: - // - There is a mismatch between arguments from [DataRow] and the method parameters. - // - There is a mismatch between arguments from [DynamicData] and the method parameters. - // If nothing else worked, report the error and exclude this method by using [Ignore]. - new MSTF::InternalUnsafeAsyncActionTestNode<(string a, string b)> - """); - } - - [TestMethod] - public async Task Arguments_WithMisalignedDataTypes_ItOutputsTheCodeAndFailsToCompile() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow("a", "b", "c")] - public Task TestWithTooMuchData(string a, string b) - { - return Task.CompletedTask; - } - - [TestMethod, DataRow("a", "b")] - public Task TestWithNotEnoughData(string a, string b, string c) - { - return Task.CompletedTask; - } - - [TestMethod, DataRow(1, 1)] - public Task TestWithMismatchedDataTypes(string a, string b) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertFailedGeneration( - "*error CS1503: Argument 1: cannot convert from '(string, string, string)' to '(string a, string b)'*", - "*error CS1503: Argument 1: cannot convert from '(string, string)' to '(string a, string b, string c)'*", - "*error CS1503: Argument 1: cannot convert from '(int, int)' to '(string a, string b)'*"); - } - - [TestMethod] - public async Task Arguments_GivenAnArrayOfObjects_GeneratesCorrectData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(new object[] { 1, (object)"a" })] - public Task OneObjectArray(object[] args) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(new object[] { 1, "a" }, "args: new object[] { 1, \"a\" }"), - }, - """); - } - - [TestMethod] - public async Task Arguments_GivenAnArrayOfInt_GeneratesCorrectData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(new int[] { 1, 2 })] - public Task OneIntArray(int[] args) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(new int[] { 1, 2 }, "args: new int[] { 1, 2 }"), - }, - """); - } - - [TestMethod] - public async Task Arguments_GivenAnArrayOfString_GeneratesCorrectData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(new string[] { "a", "b" })] - public Task OneStringArray(string[] args) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(new string[] { "a", "b" }, "args: new string[] { \"a\", \"b\" }"), - }, - """); - } - - [TestMethod] - public async Task Arguments_GivenMultipleArgumentsAndMethodAcceptsSingleObjectArray_GeneratesCorrectData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(1, "a")] - public Task OneParamsObjectArray2(object[] args) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(new object?[] { 1, "a" }, "args: new object?[] { 1, \"a\" }"), - }, - """); - } - - [TestMethod] - public async Task Arguments_GivenArrayOfIntWhenMethodExpectsArrayOfObjects_FailsCompilation() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(new int[] { 1, 2 })] - public Task OneIntArray(object[] args) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertFailedGeneration("*error CS1503: Argument 1: cannot convert from 'int[]' to 'object[]'*"); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry[] - { - new MSTF::InternalUnsafeTestArgumentsEntry(new int[] { 1, 2 }, "args: new int[] { 1, 2 }"), - }, - """); - } - - [TestMethod] - public async Task Arguments_GivenMultipleArrays_GeneratesCorrectData() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod, DataRow(new object[] { 1, 2 }, new object[] { "a", 1 })] - public Task TwoObjectArrays(object[] args, object[] args2) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => new MSTF::InternalUnsafeTestArgumentsEntry<(object[] args, object[] args2)>[] - { - new MSTF::InternalUnsafeTestArgumentsEntry<(object[] args, object[] args2)>((new object[] { 1, 2 }, new object[] { "a", 1 }), "args: new object[] { 1, 2 }, args2: new object[] { \"a\", 1 }"), - }, - """); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/DynamicDataAttributeTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/DynamicDataAttributeTests.cs deleted file mode 100644 index 7ec15f78af..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/DynamicDataAttributeTests.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Generators; - -[TestClass] -public sealed class DynamicDataAttributeGenerationTests : TestBase -{ - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task DynamicDataAttribute_TakesDataFromProperty() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using System.Collections.Generic; - using Microsoft.VisualStudio.TestTools.UnitTesting; - using Microsoft.Testing.Framework; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [DynamicData(nameof(Data))] - [TestMethod] - public void TestMethod(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); - - public static IEnumerable Data => new[] - { - new object[] { 1, 2 }, - new object[] { 2, 3 }, - }; - } - - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => { - var data = MyNamespace.TestClass.Data; - var dataCollection = new ColGen.List>(); - var index = 0; - foreach (var item in data) - { - string uidFragment = MSTF::DynamicDataNameProvider.GetUidFragment(new string[] {"expected", "actualPlus1"}, item, index); - index++; - dataCollection.Add(new(((int) item[0], (int) item[1]), uidFragment)); - } - return dataCollection; - } - """); - } - - [TestMethod] - public async Task DynamicDataAttribute_TakesDataFromMethod() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using System.Collections.Generic; - using Microsoft.VisualStudio.TestTools.UnitTesting; - using Microsoft.Testing.Framework; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [DynamicData(nameof(Data), DynamicDataSourceType.Method)] - [TestMethod] - public void TestMethod(int expected, int actualPlus1) - => Assert.AreEqual(expected, actualPlus1 - 1); - - public static IEnumerable Data() => new[] - { - new object[] { 1, 2 }, - new object[] { 2, 3 }, - }; - } - - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode(""" - GetArguments = static () => { - var data = MyNamespace.TestClass.Data(); - var dataCollection = new ColGen.List>(); - var index = 0; - foreach (var item in data) - { - string uidFragment = MSTF::DynamicDataNameProvider.GetUidFragment(new string[] {"expected", "actualPlus1"}, item, index); - index++; - dataCollection.Add(new(((int) item[0], (int) item[1]), uidFragment)); - } - return dataCollection; - } - """); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/IgnoreAttributeGenerationTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/IgnoreAttributeGenerationTests.cs deleted file mode 100644 index aacf0c8eee..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/IgnoreAttributeGenerationTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Generators; - -[TestClass] -public sealed class IgnoreAttributeGenerationTests : TestBase -{ - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task IgnoreAttribute_OnMethodExcludesTheMethodFromCompilation() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod] - public void TestMethod1() { } - - [TestMethod] - [Ignore] - public void IgnoredVoidMethod() { } - - [TestMethod] - [Ignore] - public Task IgnoredTaskMethod() => Task.CompletedTask; - - - [TestMethod] - [Ignore] - public ValueTask IgnoredValueTaskMethod() => ValueTask.CompletedTask; - - [TestMethod] - [Ignore("reason")] - public void IgnoredVoidMethodWithReason() { } - - [TestMethod] - [Ignore("reason")] - public Task IgnoredTaskMethodWithReason() => Task.CompletedTask; - - - [TestMethod] - [Ignore("reason")] - public ValueTask IgnoredValueTaskMethodWithReason() => ValueTask.CompletedTask; - } - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode("""StableUid = "TestAssembly.MyNamespace.TestClass.TestMethod1()","""); - - testClass.Should().NotContain("Ignored", "because none of the ignored methods should be output."); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/StaticMethodGenerationTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/StaticMethodGenerationTests.cs deleted file mode 100644 index 42107642a3..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/StaticMethodGenerationTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Generators; - -[TestClass] -public sealed class StaticMethodGenerationTests -{ - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task StaticMethods_StaticMethodsWontGenerateTests() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod] - public void TestMethod1() { } - - [TestMethod] - public static void StaticTestMethod() { } - - [TestMethod] - public static Task StaticTaskMethod() => Task.CompletedTask; - - - [TestMethod] - public static ValueTask StaticValueTaskMethod() => ValueTask.CompletedTask; - } - } - """, CancellationToken.None); - generatorResult.AssertSuccessfulGeneration(); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode("""StableUid = "TestAssembly.MyNamespace.TestClass.TestMethod1()","""); - - testClass.Should().NotContain("Static", "because none of the static methods should be output."); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/TestNodesGeneratorTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/TestNodesGeneratorTests.cs deleted file mode 100644 index 6ceb1a9ed4..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Generators/TestNodesGeneratorTests.cs +++ /dev/null @@ -1,1230 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Generators; - -[TestClass] -public sealed class TestNodesGeneratorTests : TestBase -{ - public TestContext TestContext { get; set; } - - [DataRow("class", "public")] - [DataRow("class", "internal")] - [DataRow("record", "public")] - [DataRow("record", "internal")] - [DataRow("record class", "public")] - [DataRow("record class", "internal")] - [TestMethod] - public async Task When_TypeIsMarkedWithTestClass_ItGeneratesAGraphWithAssemblyNamespaceTypeAndMethod(string typeKind, string accessibility) - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - {{accessibility}} {{typeKind}} MyType - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(3); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myTypeSource.Should().ContainSourceCode(""" - public static readonly MSTF::TestNode TestNode = new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace.MyType", - DisplayName = "MyType", - Properties = new Msg::IProperty[1] - { - new Msg::TestFileLocationProperty(@"", new(new(6, -1), new(14, -1))), - }, - Tests = new MSTF::TestNode[] - { - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyType.TestMethod()", - DisplayName = "TestMethod", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "MyType", - "TestMethod", - 0, - Sys::Array.Empty(), - "System.Threading.Tasks.Task"), - new Msg::TestFileLocationProperty(@"", new(new(9, -1), new(13, -1))), - }, - Body = static async testExecutionContext => - { - var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - }, - }; - """); - - SourceText rootSource = await generatorResult.RunResult.GeneratedTrees[1].GetTextAsync(TestContext.CancellationToken); - rootSource.Should().ContainSourceCode(""" - MSTF::TestNode root = new MSTF::TestNode - { - StableUid = "TestAssembly", - DisplayName = "TestAssembly", - Properties = Sys::Array.Empty(), - Tests = new MSTF::TestNode[] - { - new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace", - DisplayName = "MyNamespace", - Properties = Sys::Array.Empty(), - Tests = namespace1Tests.ToArray(), - }, - }, - }; - """); - } - - [TestMethod] - public async Task When_TypeInheritsABaseClassAndIsParameterless_ItGeneratesATestNode() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - public class MyBaseClass - { - public MyBaseClass(string s) { } - } - - [TestClass] - public class MyType : MyBaseClass - { - public MyType() - : base("hello") - { - } - - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(3); - } - - [TestMethod] - public async Task When_TypeInheritsAnAbstractBaseClassAndIsParameterless_ItGeneratesATestNode() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - public abstract class MyBaseClass - { - public MyBaseClass(string s) { } - } - - [TestClass] - public class MyType : MyBaseClass - { - public MyType() - : base("hello") - { - } - - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(3); - } - - [DataRow(false)] - [DataRow(true)] - [TestMethod] - public async Task When_TypeInheritsABaseClassWithSomeTestMethodsButBaseIsNotTestClass_OnlyOneTestNodeTypeIsGenerated(bool isBaseClassAbstract) - { - string classModifier = isBaseClassAbstract ? "abstract " : string.Empty; - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - public {{classModifier}}class MyBaseClass - { - public MyBaseClass(string s) { } - - [TestMethod] - public void MyTestMethod() { } - } - - [TestClass] - public class MyType : MyBaseClass - { - public MyType() - : base("hello") - { - } - - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(3); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myTypeSource.Should().ContainSourceCode(""" - public static readonly MSTF::TestNode TestNode = new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace.MyType", - DisplayName = "MyType", - Properties = new Msg::IProperty[1] - { - new Msg::TestFileLocationProperty(@"", new(new(14, -1), new(27, -1))), - }, - Tests = new MSTF::TestNode[] - { - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyType.TestMethod()", - DisplayName = "TestMethod", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "MyType", - "TestMethod", - 0, - Sys::Array.Empty(), - "System.Threading.Tasks.Task"), - new Msg::TestFileLocationProperty(@"", new(new(22, -1), new(26, -1))), - }, - Body = static async testExecutionContext => - { - var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - new MSTF::InternalUnsafeActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyType.MyTestMethod()", - DisplayName = "MyTestMethod", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "MyBaseClass", - "MyTestMethod", - 0, - Sys::Array.Empty(), - "System.Void"), - new Msg::TestFileLocationProperty(@"", new(new(10, -1), new(11, -1))), - }, - Body = static testExecutionContext => - { - var instance = new MyType(); - try - { - instance.MyTestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - }, - }; - """); - } - - [TestMethod] - public async Task When_TypeInheritsABaseTestClassMarkedAsTestClassWithSomeTestMethods_TwoTestNodeTypesAreGenerated() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class MyBaseClass - { - [TestMethod] - public void MyTestMethod() { } - } - - [TestClass] - public class MyType : MyBaseClass - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(4); - - SourceText myBaseClassSource = await generatorResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myBaseClassSource.Should().ContainSourceCode(""" - public static readonly MSTF::TestNode TestNode = new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace.MyBaseClass", - DisplayName = "MyBaseClass", - Properties = new Msg::IProperty[1] - { - new Msg::TestFileLocationProperty(@"", new(new(6, -1), new(11, -1))), - }, - Tests = new MSTF::TestNode[] - { - new MSTF::InternalUnsafeActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyBaseClass.MyTestMethod()", - DisplayName = "MyTestMethod", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "MyBaseClass", - "MyTestMethod", - 0, - Sys::Array.Empty(), - "System.Void"), - new Msg::TestFileLocationProperty(@"", new(new(9, -1), new(10, -1))), - }, - Body = static testExecutionContext => - { - var instance = new MyBaseClass(); - try - { - instance.MyTestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - }, - }; - """); - - SourceText myTypeSource = await generatorResult.GeneratedTrees[1].GetTextAsync(TestContext.CancellationToken); - myTypeSource.Should().ContainSourceCode(""" - public static readonly MSTF::TestNode TestNode = new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace.MyType", - DisplayName = "MyType", - Properties = new Msg::IProperty[1] - { - new Msg::TestFileLocationProperty(@"", new(new(13, -1), new(21, -1))), - }, - Tests = new MSTF::TestNode[] - { - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyType.TestMethod()", - DisplayName = "TestMethod", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "MyType", - "TestMethod", - 0, - Sys::Array.Empty(), - "System.Threading.Tasks.Task"), - new Msg::TestFileLocationProperty(@"", new(new(16, -1), new(20, -1))), - }, - Body = static async testExecutionContext => - { - var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - new MSTF::InternalUnsafeActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyType.MyTestMethod()", - DisplayName = "MyTestMethod", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "MyBaseClass", - "MyTestMethod", - 0, - Sys::Array.Empty(), - "System.Void"), - new Msg::TestFileLocationProperty(@"", new(new(9, -1), new(10, -1))), - }, - Body = static testExecutionContext => - { - var instance = new MyType(); - try - { - instance.MyTestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - }, - }; - """); - } - - [TestMethod] - public async Task When_TestClassHasAliasForTestNode_ItWontConflictWithIdentifier() - { - // When class has alias for TestNode we should not see conflict in the compiled code. When this is not working correctly - // e.g. when in the class definition you use just TestNode TestNode for the test node property you will see - // "Namespace 'Microsoft.Testing.Framework' contains a definition conflicting with alias 'TestNode', but found False." - // compilation error. - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - using static System.ConsoleColor; - using TestNode = A.TestNode; - - namespace A - { - public class TestNode - { - } - } - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.RunResult.GeneratedTrees.Should().HaveCount(3); - - SourceText testClass = await generatorResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - - testClass.Should().ContainSourceCode( - "public static readonly MSTF::TestNode TestNode = new MSTF::TestNode", - "because using short name for TestNode type would conflict with the type alias"); - } - - [TestMethod] - public async Task When_TestClassIsNested_ItGeneratesNodesForIt() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - """ - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class TestClass - { - [TestClass] - public class TestSubClass - { - [TestMethod] - public Task TestMethod1() - { - return Task.CompletedTask; - } - } - - [TestMethod] - public Task TestMethod2() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.RunResult.GeneratedTrees.Should().HaveCount(4); - - SyntaxTree? testClassTree = generatorResult.GeneratedTrees.FirstOrDefault(r => r.FilePath.EndsWith("TestSubClass.g.cs", StringComparison.OrdinalIgnoreCase)); - testClassTree.Should().NotBeNull(); - - SourceText testClass = await testClassTree!.GetTextAsync(TestContext.CancellationToken); - testClass.Should().ContainSourceCode(""" - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.TestClass.TestSubClass.TestMethod1()", - DisplayName = "TestMethod1", - Properties = new Msg::IProperty[2] - { - new Msg::TestMethodIdentifierProperty( - "TestAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "MyNamespace", - "TestClass.TestSubClass", - "TestMethod1", - 0, - Sys::Array.Empty(), - "System.Threading.Tasks.Task"), - new Msg::TestFileLocationProperty(@"", new(new(12, -1), new(16, -1))), - }, - Body = static async testExecutionContext => - { - var instance = new TestClass.TestSubClass(); - try - { - await instance.TestMethod1(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - }, - """); - } - - [TestMethod] - public async Task When_TypeIsInGlobalNamespace_ItGeneratesATestNode() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - [TestClass] - public class MyType - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.RunResult.GeneratedTrees.Should().HaveCount(3); - - SourceText myTypeSource = await generatorResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myTypeSource.Should().ContainSourceCode(""" - public static readonly MSTF::TestNode TestNode = new MSTF::TestNode - { - StableUid = "TestAssembly.MyType", - DisplayName = "MyType", - Properties = new Msg::IProperty[1] - { - new Msg::TestFileLocationProperty(@"", new(new(4, -1), new(12, -1))), - }, - """); - - SourceText rootSource = await generatorResult.GeneratedTrees[1].GetTextAsync(TestContext.CancellationToken); - rootSource.Should().ContainSourceCode(""" - MSTF::TestNode root = new MSTF::TestNode - { - StableUid = "TestAssembly", - DisplayName = "TestAssembly", - Properties = Sys::Array.Empty(), - Tests = new MSTF::TestNode[] - { - new MSTF::TestNode - { - StableUid = "TestAssembly.", - DisplayName = "", - Properties = Sys::Array.Empty(), - Tests = namespace1Tests.ToArray(), - }, - }, - }; - """); - } - - [TestMethod] - public async Task When_MultipleClassesFromSameNamespace_ItGeneratesASingleNamespaceTestNode() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - [ - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class MyType1 - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """, - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class MyType2 - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - } - } - """ - ], CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(4); - - SourceText rootSource = await generatorResult.RunResult.GeneratedTrees[2].GetTextAsync(TestContext.CancellationToken); - rootSource.Should().ContainSourceCode(""" - ColGen::List namespace1Tests = new(); - namespace1Tests.Add(MyNamespace_MyType1.TestNode); - namespace1Tests.Add(MyNamespace_MyType2.TestNode); - - MSTF::TestNode root = new MSTF::TestNode - { - StableUid = "TestAssembly", - DisplayName = "TestAssembly", - Properties = Sys::Array.Empty(), - Tests = new MSTF::TestNode[] - { - new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace", - DisplayName = "MyNamespace", - Properties = Sys::Array.Empty(), - Tests = namespace1Tests.ToArray(), - }, - }, - }; - """); - } - - [DataRow("class")] - [DataRow("struct")] - [DataRow("record")] - [DataRow("record struct")] - [DataRow("record class")] - [TestMethod] - [Ignore("Initialize is not supported yet.")] - public async Task When_TypeIsIAsyncInitializable_GeneratedTestNodeIsAsExpected(string typeKind) - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public {{typeKind}} MyType : IAsyncInitializable - { - [TestMethod] - public Task TestMethod() - { - return Task.CompletedTask; - } - - [TestInitialize] - public Task InitializeAsync(InitializationContext context) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(2); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - - // The test node for the type should not have a test node for the InitializeAsync method. - myTypeSource.Should().NotContain("StableUid = \"TestAssembly.MyNamespace.MyType.InitializeAsync\""); - - // The body of the test node for the method should call InitializeAsync before calling the test method. - myTypeSource.Should().ContainSourceCode(""" - Body = static async testExecutionContext => - { - var instance = new MyType(); - try - { - await instance.InitializeAsync(new MSTF::InitializationContext(testExecutionContext.CancellationToken)); - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - """); - } - - [DataRow("class")] - [DataRow("struct")] - [DataRow("record")] - [DataRow("record struct")] - [DataRow("record class")] - [TestMethod] - [Ignore("Initialize is not supported yet.")] - public async Task When_TypeIsIAsyncCleanable_GeneratedTestNodeIsAsExpected(string typeKind) - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public {{typeKind}} MyType : IAsyncCleanable - { - public Task TestMethod() - { - return Task.CompletedTask; - } - - public Task CleanupAsync(CleanupContext context) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(2); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - - // The test node for the type should not have a test node for the CleanupAsync method. - myTypeSource.Should().NotContain("StableUid = \"TestAssembly.MyNamespace.MyType.CleanupAsync\""); - - // The body of the test node for the method should call CleanupAsync after calling the test method. - myTypeSource.Should().ContainSourceCode(""" - Body = static async testExecutionContext => - { - var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - try - { - await instance.CleanupAsync(new MSTF::CleanupContext(testExecutionContext.CancellationToken)); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - """); - } - - [DataRow("class")] - [DataRow("struct")] - [DataRow("record")] - [DataRow("record struct")] - [DataRow("record class")] - [TestMethod] - [Ignore("Initialize is not supported yet.")] - public async Task When_TypeIsIDisposable_GeneratedTestNodeIsAsExpected(string typeKind) - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public {{typeKind}} MyType : IDisposable - { - public Task TestMethod() - { - return Task.CompletedTask; - } - - public void Dispose() - { - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(2); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - - // The test node for the type should not have a test node for the Dispose method. - myTypeSource.Should().NotContain("StableUid = \"TestAssembly.MyNamespace.MyType.Dispose\""); - - // The body of the test node for the method should call Dispose after calling the test method. - myTypeSource.Should().ContainSourceCode(""" - Body = static async testExecutionContext => - { - using var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - """); - } - - [DataRow("class")] - [DataRow("struct")] - [DataRow("record")] - [DataRow("record struct")] - [DataRow("record class")] - [TestMethod] - [Ignore("Initialize is not supported yet.")] - public async Task When_TypeIsIAsyncDisposable_GeneratedTestNodeIsAsExpected(string typeKind) - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public {{typeKind}} MyType : IAsyncDisposable - { - public Task TestMethod() - { - return Task.CompletedTask; - } - - public ValueTask DisposeAsync() - { - return ValueTask.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(2); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - - // The test node for the type should not have a test node for the DisposeAsync method. - myTypeSource.Should().NotContain("StableUid = \"TestAssembly.MyNamespace.MyType.DisposeAsync\""); - - // The body of the test node for the method should call DisposeAsync after calling the test method. - myTypeSource.Should().ContainSourceCode(""" - Body = static async testExecutionContext => - { - await using var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - """); - } - - [DataRow("class")] - [DataRow("struct")] - [DataRow("record")] - [DataRow("record struct")] - [DataRow("record class")] - [TestMethod] - [Ignore("Initialize is not supported yet.")] - public async Task When_TypeIsIAsyncDisposableAndIDisposable_GeneratedTestNodeIsAsExpected(string typeKind) - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public {{typeKind}} MyType : IAsyncDisposable, IDisposable - { - public Task TestMethod() - { - return Task.CompletedTask; - } - - public ValueTask DisposeAsync() - { - return ValueTask.CompletedTask; - } - - public void Dispose() - { - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(2); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - - // The body of the test node for the method should call only DisposeAsync after calling the test method. - myTypeSource.Should().ContainSourceCode(""" - Body = static async testExecutionContext => - { - await using var instance = new MyType(); - try - { - await instance.TestMethod(); - } - catch (global::System.Exception ex) - { - testExecutionContext.ReportException(ex, null); - } - }, - """); - } - - [TestMethod] - [Ignore("Initialize is not supported yet.")] - public async Task When_MethodIsNotAsyncButTypeUsesAsync_GeneratedTestNodeIsAsync() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class MyClass1 : IAsyncDisposable - { - public void TestMethod() - { - } - - public ValueTask DisposeAsync() - { - return ValueTask.CompletedTask; - } - } - - [TestClass] - public class MyClass2 : IAsyncInitializable - { - public void TestMethod() - { - } - - public Task InitializeAsync(InitializationContext context) - { - return Task.CompletedTask; - } - } - - [TestClass] - public class MyClass3 : IAsyncCleanable - { - public void TestMethod() - { - } - - public Task CleanupAsync(CleanupContext context) - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(4); - - SourceText myClass1Source = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myClass1Source.Should().ContainSourceCode(""" - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyClass1.TestMethod()", - """); - - SourceText myClass2Source = await generatorResult.RunResult.GeneratedTrees[1].GetTextAsync(TestContext.CancellationToken); - myClass2Source.Should().ContainSourceCode(""" - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyClass2.TestMethod()", - """); - - SourceText myClass3Source = await generatorResult.RunResult.GeneratedTrees[2].GetTextAsync(TestContext.CancellationToken); - myClass3Source.Should().ContainSourceCode(""" - new MSTF::InternalUnsafeAsyncActionTestNode - { - StableUid = "TestAssembly.MyNamespace.MyClass3.TestMethod()", - """); - } - - [TestMethod] - public async Task When_MethodIsObsolete_WrapMethodCallWithPragma() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - $$""" - using System; - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public class MyType - { - [Obsolete] - [TestMethod] - public Task TestMethod1() - { - return Task.CompletedTask; - } - - [Obsolete("This is obsolete with message")] - [TestMethod] - public Task TestMethod2() - { - return Task.CompletedTask; - } - - [Obsolete("This is obsolete with message", false)] - [TestMethod] - public Task TestMethod3() - { - return Task.CompletedTask; - } - - [Obsolete("This is obsolete with message", true)] - [TestMethod] - public Task TestMethod4() - { - return Task.CompletedTask; - } - } - } - """, CancellationToken.None); - - generatorResult.AssertFailedGeneration("*error CS0619: 'MyType.TestMethod4()' is obsolete*"); - generatorResult.GeneratedTrees.Should().HaveCount(3); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myTypeSource.Should().ContainSourceCode(""" - #pragma warning disable CS0612 // Type or member is obsolete - await instance.TestMethod1(); - #pragma warning restore CS0612 // Type or member is obsolete - """); - - myTypeSource.Should().ContainSourceCode(""" - #pragma warning disable CS0618 // Type or member is obsolete - await instance.TestMethod2(); - #pragma warning restore CS0618 // Type or member is obsolete - """); - - myTypeSource.Should().ContainSourceCode(""" - #pragma warning disable CS0618 // Type or member is obsolete - await instance.TestMethod3(); - #pragma warning restore CS0618 // Type or member is obsolete - """); - } - - [DataRow("1ab", "_1ab")] - [DataRow("a-b", "a_b")] - [DataRow("a.", "a_")] - [DataRow("!@#$%^&*()_+-=", "______________")] - [TestMethod] - - // Disabled for potential bug in templating (where escaping code is copied from) - // https://github.com/dotnet/templating/issues/7200 - // [DataRow("a..b", "a..b")] - // [DataRow("a...b", "a...b")] - public void GenerateValidNamespaceName_WithGivenAssemblyName_ReturnsExpectedNamespaceName(string assemblyName, string expectedNamespaceName) - => Assert.AreEqual(expectedNamespaceName, TestNodesGenerator.ToSafeNamespace(assemblyName)); - - [TestMethod] - public async Task When_APartialTypeIsMarkedWithTestClass_ItGeneratesAGraphWithAssemblyNamespaceTypeAndMethods() - { - GeneratorCompilationResult generatorResult = await GeneratorTester.TestGraph.CompileAndExecuteAsync( - [ - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - [TestClass] - public partial class MyType - { - public MyType(int a) { } - - [TestMethod] - public Task TestMethod1() - { - return Task.CompletedTask; - } - } - } - """, - $$""" - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - namespace MyNamespace - { - // Defining [TestClass] twice would fail - // the source gen with Duplicate source MyNamespace.MyType.g.cs - // but if we fix that problem it will subsequently fail with - // duplicate attribute [TestClass]. - public partial class MyType - { - public MyType() {} - - [TestMethod] - public Task TestMethod2() - { - return Task.CompletedTask; - } - } - } - """ - ], CancellationToken.None); - - generatorResult.AssertSuccessfulGeneration(); - generatorResult.GeneratedTrees.Should().HaveCount(3); - - SourceText myTypeSource = await generatorResult.RunResult.GeneratedTrees[0].GetTextAsync(TestContext.CancellationToken); - myTypeSource.Should().ContainSourceCode(""" - public static class MyNamespace_MyType - { - public static readonly MSTF::TestNode TestNode = new MSTF::TestNode - { - StableUid = "TestAssembly.MyNamespace.MyType", - DisplayName = "MyType", - Properties = new Msg::IProperty[2] - { - new Msg::TestFileLocationProperty(@"", new(new(6, -1), new(16, -1))), - new Msg::TestFileLocationProperty(@"", new(new(10, -1), new(19, -1))), - }, - Tests = new MSTF::TestNode[] - { - """); - - myTypeSource.Should().ContainSourceCode(""" - StableUid = "TestAssembly.MyNamespace.MyType.TestMethod1()", - """); - - myTypeSource.Should().ContainSourceCode(""" - StableUid = "TestAssembly.MyNamespace.MyType.TestMethod2()", - """); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/ConstantsTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/ConstantsTests.cs deleted file mode 100644 index 1944f5b664..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/ConstantsTests.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests; - -[TestClass] -public class ConstantsTests : TestBase -{ - [TestMethod] - public void NewLine_IsWindowsLineReturn() => Constants.NewLine.Should().Be("\r\n"); -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/GeneratorCompilationResultHelpers.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/GeneratorCompilationResultHelpers.cs deleted file mode 100644 index e9c4db7ff9..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/GeneratorCompilationResultHelpers.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; -using Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; - -internal static class GeneratorCompilationResultHelpers -{ - public static GeneratorCompilationResult AssertSuccessfulGeneration(this GeneratorCompilationResult result) - { - result.EmitResult.Success.Should().BeTrue("compilation should have been successful.\n" - + $"Diagnostics: {result.EmitResult.Diagnostics.Length}\n" - + $"Code:\n{result.FailingGeneratedCode}"); - result.TimingInfo.ElapsedTime.Should().BeGreaterThan(TimeSpan.Zero); - result.EmitResult.Diagnostics.Where(d => d.Severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Error).Should().BeEmpty(); - result.RunResult.Diagnostics.Should().BeEmpty(); - return result; - } - - public static GeneratorCompilationResult AssertFailedGeneration(this GeneratorCompilationResult result, params string[] diagnostics) - { - result.EmitResult.Success.Should().BeFalse(); - result.TimingInfo.ElapsedTime.Should().BeGreaterThan(TimeSpan.Zero); - result.RunResult.Diagnostics.Should().BeEmpty(); - result.EmitResult.Diagnostics.Should().HaveSameCount(diagnostics); - - for (int i = 0; i < diagnostics.Length; i++) - { - result.EmitResult.Diagnostics.Select(d => d.ToString()).Should().ContainMatch(diagnostics[i]); - } - - return result; - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/MinimalTestRunner.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/MinimalTestRunner.cs deleted file mode 100644 index 5537de1ad2..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/MinimalTestRunner.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -/// -/// Discovers and runs tests using the MSTest attributes, so we can run tests even when we completely break or delete the real MSTest engine. -/// -internal sealed class MinimalTestRunner -{ - public static async Task RunAllAsync(string? testNameContainsFilter = null) - { -#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - IEnumerable classes = Assembly.GetExecutingAssembly().GetTypes().Where(c => c.IsPublic); -#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - object[][] emptyRow = [[]]; - - int total = 0; - int failed = 0; - int passed = 0; - foreach (Type? c in classes) - { - IList attributes = c.GetCustomAttributesData(); - - if (!attributes.Any(a => a.AttributeType == typeof(TestClassAttribute))) - { - continue; - } - - if (attributes.Any(a => a.AttributeType == typeof(IgnoreAttribute))) - { - Console.WriteLine($"Class {c.Name} is ignored."); - continue; - } - -#pragma warning disable IL2075 // 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. - foreach (MethodInfo m in c.GetMethods()) - { - if (!string.IsNullOrWhiteSpace(testNameContainsFilter)) - { -#pragma warning disable CA1304 // Specify CultureInfo -#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons -#pragma warning disable CA1311 // Specify a culture or use an invariant version - if (!m.Name!.ToLower().Contains(testNameContainsFilter.ToLower())) - { - continue; - } -#pragma warning restore CA1311 // Specify a culture or use an invariant version -#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons -#pragma warning restore CA1304 // Specify CultureInfo - } - - IList methodAttributes = m.GetCustomAttributesData(); - if (!methodAttributes.Any(a => a.AttributeType == typeof(TestMethodAttribute))) - { - continue; - } - - if (methodAttributes.Any(a => a.AttributeType == typeof(IgnoreAttribute))) - { - Console.WriteLine($"Method {c.Name} is ignored."); - continue; - } - - object?[][]? rows = null; - if (methodAttributes.Any(a => a.AttributeType == typeof(DataRowAttribute))) - { - rows = [.. methodAttributes - .Where(a => a.AttributeType == typeof(DataRowAttribute)) - .SelectMany(a => a.ConstructorArguments.Select(arg => - { - // An object that represents the value of the argument or element, or a generic ReadOnlyCollection of CustomAttributeTypedArgument objects that represent the values of an array-type argument. - // https://learn.microsoft.com/en-us/dotnet/api/system.reflection.customattributetypedargument.value?view=net-8.0#property-value -#pragma warning disable IDE0046 // Convert to conditional expression - if (arg.Value is IReadOnlyCollection argumentCollection) - { - return argumentCollection.Select(argv => argv.Value).ToArray(); - } - else - { - return [arg.Value]; - } -#pragma warning restore IDE0046 // Convert to conditional expression - }))]; - } - - foreach (object?[]? row in rows ?? emptyRow) - { - ConsoleColor fg = Console.ForegroundColor; - try - { - total++; -#pragma warning disable IL2072 // Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. - object? classInstance = Activator.CreateInstance(c); -#pragma warning restore IL2072 // Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. - object? result = m.Invoke(classInstance, row); - if (result is Task task) - { - await task; - } - else if (result is ValueTask valueTask) - { - await valueTask; - } - - passed++; - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"Passed {c.Name}.{m.Name}"); - } - catch (TargetInvocationException ex) - { - failed++; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Failed {c.Name}.{m.Name}:\n{ex.InnerException}\n{ex.InnerException!.StackTrace}\n"); - } - catch (Exception ex) - { - failed++; - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Failed {c.Name}.{m.Name}:\n{ex}\n{ex.StackTrace}\n"); - } - finally - { - Console.ForegroundColor = fg; - } - } - } -#pragma warning restore IL2075 // 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. - } - - Console.WriteLine($"{(failed != 0 ? "failed" : "passed")}! - failed: {failed}, passed: {passed}, total: {total}"); - - return failed == 0 ? 0 : 1; - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SourceCodeAssertionExtensions.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SourceCodeAssertionExtensions.cs deleted file mode 100644 index e2c6014a6b..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SourceCodeAssertionExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions.Execution; - -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; - -internal static class SourceCodeAssertionExtensions -{ - public static SourceCodeAssertions Should(this SourceText sourceText) => new(sourceText.ToString(), AssertionChain.GetOrCreate()); -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SourceCodeAssertions.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SourceCodeAssertions.cs deleted file mode 100644 index 6d24195427..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SourceCodeAssertions.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; -using AwesomeAssertions.Execution; -using AwesomeAssertions.Primitives; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; - -internal sealed class SourceCodeAssertions : StringAssertions -{ - public SourceCodeAssertions(string value, AssertionChain assertionChain) - : base(value, assertionChain) - { - } - - public AndConstraint ContainSourceCode(string expectedSourceCode, string because = "", params object[] becauseArgs) - { - if (string.IsNullOrEmpty(expectedSourceCode)) - { - throw new ArgumentException("Cannot assert string containment against or Empty source code.", nameof(expectedSourceCode)); - } - - bool onlyDifferInWhitespace = false; - try - { - Subject.ShowReducedWhitespace().Should().Contain(expectedSourceCode.ShowReducedWhitespace()); - onlyDifferInWhitespace = true; - } - catch - { - } - - string subMessage = "Expected \n{context:string}\n{0}\n to contain\n{1}\n{reason}."; - string message = onlyDifferInWhitespace - ? $"WHITESPACE ONLY DIFFERENCE!\n\n{subMessage}" - : subMessage; - - string actual = Subject.ShowWhitespace(); - string expected = expectedSourceCode.ShowWhitespace(); - CurrentAssertionChain - .ForCondition(Contains(actual, expected, StringComparison.Ordinal)) - .BecauseOf(because, becauseArgs) - .FailWith(message, actual, expected); - - return new AndConstraint(this); - } - - public AndConstraint BeSourceCode(string expectedSourceCode, string because = "", params object[] becauseArgs) - { - if (string.IsNullOrEmpty(expectedSourceCode)) - { - throw new ArgumentException("Cannot assert string equality against or Empty source code.", nameof(expectedSourceCode)); - } - - bool onlyDifferInWhitespace = false; - try - { - Subject.ShowReducedWhitespace().Should().Be(expectedSourceCode.ShowReducedWhitespace()); - onlyDifferInWhitespace = true; - } - catch - { - } - - string subMessage = "Expected \n{context:string}\n{0}\n to match\n{1}\n{reason}."; - string message = onlyDifferInWhitespace - ? $"WHITESPACE ONLY DIFFERENCE!\n\n{subMessage}" - : subMessage; - - string actual = Subject.ShowWhitespace(); - string expected = expectedSourceCode.ShowWhitespace(); - - CurrentAssertionChain - .ForCondition(string.Equals(actual, expected, StringComparison.Ordinal)) - .BecauseOf(because, becauseArgs) - .FailWith(message, actual, expected); - - return new AndConstraint(this); - } - - private static bool Contains(string actual, string expected, StringComparison comparison) - => (actual ?? string.Empty).Contains(expected ?? string.Empty, comparison); -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SyntaxExtensions.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SyntaxExtensions.cs deleted file mode 100644 index 17133da1c3..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Helpers/SyntaxExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.Helpers; - -internal static class SyntaxExtensions -{ - public static string ShowWhitespace(this SourceText text) => text.ToString().ShowWhitespace(); - - /// - /// Show spaces and tabs as '·' and '→', replace "\r", but keep newlines, so the resulting text - /// is still formatted it would be in a file but the lines are not OS specific and the whitespace is easy to see. - /// - public static string ShowWhitespace(this string text) - { - if (text.Contains('·') || text.Contains('→')) - { - throw new ArgumentException("Provided text contains '·' or '→' characters, " + - $"which {nameof(ShowWhitespace)} uses to show whitespace. " + - "Did you copy paste it from the test result and forgot to remove those replacements?"); - } - - IDictionary map = new Dictionary - { - { "\r", string.Empty }, - - // Tabs output just 1 '→' on purpose, to break the code layout and be easier to spot. - // We don't want tabs in our code. - { "\t", "→" }, - { " ", "·" }, - }; - - var regex = new Regex(string.Join('|', map.Keys)); - return regex.Replace(text, m => map[m.Value]); - } - - /// - /// Remove every doubled space, which gives you text that still spans multiple lines - /// but the amount of whitespace is greatly reduced. This helps when you are not sure if your content - /// is incorrect or it is just whitespace that is incorrect. - /// - public static string ShowReducedWhitespace(this SourceText text) => ShowReducedWhitespace(text.ToString()); - - /// - /// Remove every doubled space, which gives you text that still spans multiple lines - /// but the amount of whitespace is greatly reduced. This helps when you are not sure if your content - /// is incorrect or it is just whitespace that is incorrect. - /// - public static string ShowReducedWhitespace(this string text) => Regex.Replace(text, " {2,}", string.Empty).ShowWhitespace(); -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/InheritedTestClassAttributeWithSourceGeneratorAnalyzerTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/InheritedTestClassAttributeWithSourceGeneratorAnalyzerTests.cs new file mode 100644 index 0000000000..766b053114 --- /dev/null +++ b/test/UnitTests/MSTest.SourceGeneration.UnitTests/InheritedTestClassAttributeWithSourceGeneratorAnalyzerTests.cs @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.Globalization; + +using AwesomeAssertions; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Analyzers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.UnitTests; + +[TestClass] +public sealed class InheritedTestClassAttributeWithSourceGeneratorAnalyzerTests +{ + private const string MSTestStub = """ + namespace Microsoft.VisualStudio.TestTools.UnitTesting + { + [System.AttributeUsage(System.AttributeTargets.Class, Inherited = true)] + public class TestClassAttribute : System.Attribute {} + } + """; + + [TestMethod] + public async Task NoDiagnostic_WhenTestClassIsAppliedDirectly() + { + const string source = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + namespace Sample + { + [TestClass] + public class MyTests {} + } + """; + + ImmutableArray diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + diagnostics.Should().BeEmpty(); + } + + [TestMethod] + public async Task NoDiagnostic_WhenNoTestClassAttributeAtAll() + { + const string source = """ + namespace Sample + { + public class BaseTests {} + public class DerivedTests : BaseTests {} + } + """; + + ImmutableArray diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + diagnostics.Should().BeEmpty(); + } + + [TestMethod] + public async Task Diagnostic_WhenTestClassIsInheritedFromDirectBase() + { + const string source = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + namespace Sample + { + [TestClass] + public class BaseTests {} + + public class DerivedTests : BaseTests {} + } + """; + + ImmutableArray diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + diagnostics.Should().ContainSingle(); + Diagnostic diagnostic = diagnostics[0]; + diagnostic.Id.Should().Be(InheritedTestClassAttributeWithSourceGeneratorAnalyzer.DiagnosticId); + diagnostic.Severity.Should().Be(DiagnosticSeverity.Warning); + diagnostic.GetMessage(CultureInfo.InvariantCulture).Should().Contain("DerivedTests").And.Contain("BaseTests"); + } + + [TestMethod] + public async Task Diagnostic_WhenTestClassIsInheritedFromGrandparent() + { + const string source = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + namespace Sample + { + [TestClass] + public class GrandparentTests {} + + public class ParentTests : GrandparentTests {} + + public class DerivedTests : ParentTests {} + } + """; + + ImmutableArray diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + // Both ParentTests and DerivedTests are missing the direct attribute. + diagnostics.Should().HaveCount(2); + diagnostics.Select(d => d.GetMessage(CultureInfo.InvariantCulture)).Should().AllSatisfy(m => m.Should().Contain("GrandparentTests")); + } + + [TestMethod] + public async Task NoDiagnostic_OnAbstractClass() + { + const string source = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + namespace Sample + { + [TestClass] + public abstract class BaseTests {} + + public abstract class AbstractDerived : BaseTests {} + } + """; + + ImmutableArray diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + diagnostics.Should().BeEmpty(); + } + + [TestMethod] + public async Task NoDiagnostic_OnStaticClass() + { + const string source = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + namespace Sample + { + [TestClass] + public class BaseTests {} + + // Static class can't actually derive from a normal class, but exercise the + // static filter via a self-attributed static type to ensure we don't flag it. + public static class StaticHelper {} + } + """; + + ImmutableArray diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + diagnostics.Should().BeEmpty(); + } + + [TestMethod] + public async Task Diagnostic_WhenSubclassOfTestClassAttributeIsInherited() + { + const string source = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + namespace Sample + { + public sealed class MyTestClassAttribute : TestClassAttribute {} + + [MyTestClass] + public class BaseTests {} + + public class DerivedTests : BaseTests {} + } + """; + + ImmutableArray diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + diagnostics.Should().ContainSingle(); + diagnostics[0].GetMessage(CultureInfo.InvariantCulture).Should().Contain("DerivedTests").And.Contain("BaseTests"); + } + + [TestMethod] + public async Task NoDiagnostic_OnOpenGenericClass() + { + // The source generator skips open generic types, so warning on them would be misleading + // — applying [TestClass] directly still wouldn't make them discoverable. + const string source = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + namespace Sample + { + [TestClass] + public class BaseTests {} + + public class DerivedTests : BaseTests {} + } + """; + + ImmutableArray diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + diagnostics.Should().BeEmpty(); + } + + [TestMethod] + public async Task NoDiagnostic_OnFileLocalClass() + { + // The source generator skips file-local types, so warning on them would be misleading. + const string source = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + namespace Sample + { + [TestClass] + public class BaseTests {} + + file class DerivedTests : BaseTests {} + } + """; + + ImmutableArray diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + diagnostics.Should().BeEmpty(); + } + + private static async Task> GetAnalyzerDiagnosticsAsync(string source) + { + SyntaxTree[] trees = + [ + CSharpSyntaxTree.ParseText(MSTestStub), + CSharpSyntaxTree.ParseText(source), + ]; + + MetadataReference[] references = + [ + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + ]; + + var compilation = CSharpCompilation.Create( + "TestSample", + trees, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var analyzer = new InheritedTestClassAttributeWithSourceGeneratorAnalyzer(); + CompilationWithAnalyzers withAnalyzers = compilation.WithAnalyzers([analyzer]); + ImmutableArray diagnostics = await withAnalyzers.GetAnalyzerDiagnosticsAsync(); + return diagnostics + .Where(d => d.Id == InheritedTestClassAttributeWithSourceGeneratorAnalyzer.DiagnosticId) + .ToImmutableArray(); + } +} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.csproj b/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.csproj index 014e756668..8cb7b76a77 100644 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.csproj +++ b/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.csproj @@ -1,35 +1,27 @@ - + net8.0 - Microsoft.Testing.Framework.SourceGeneration.UnitTests - $(NoWarn);NU1701 + Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.UnitTests + true true Exe - - - - - - - PreserveNewest - + - - - Analyzer - true - + + + + diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.launcher.config.json b/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.launcher.config.json deleted file mode 100644 index 0e1704afa1..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.launcher.config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "program": "MSTest.SourceGeneration.UnitTests.exe" -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.testingplatformconfig.json b/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.testingplatformconfig.json deleted file mode 100644 index 371fa66d5b..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration.UnitTests.testingplatformconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "testingplatform": { - "telemetry": { - "isDevelopmentRepository": true - }, - "exitProcessOnUnhandledException": true, - "testHostControllersManager": { - "singleConnectionNamedPipeServer": { - "waitConnectionTimeoutSeconds": 90 - }, - "namedPipeClient": { - "connectTimeoutSeconds": 90 - } - } - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration/Microsoft.Testing.Framework.SourceGeneration.TestNodesGenerator/Microsoft.Testing.Framework.Source b/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTest.SourceGeneration/Microsoft.Testing.Framework.SourceGeneration.TestNodesGenerator/Microsoft.Testing.Framework.Source deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/ObjectModels/InlineTestMethodArgumentsInfoTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/ObjectModels/InlineTestMethodArgumentsInfoTests.cs deleted file mode 100644 index 9c34e23745..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/ObjectModels/InlineTestMethodArgumentsInfoTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Testing.Framework.SourceGeneration.ObjectModels; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests; - -[TestClass] -public sealed class InlineTestMethodArgumentsInfoTests : TestBase -{ - [DataRow("a", "a")] - [DataRow("a, b", "a, b")] - [DataRow("\"ok\"", "\\\"ok\\\"")] - [DataRow("\"", "\\\"")] - [DataRow("\\", "\\")] - [DataRow("\\\\", "\\\\")] - [DataRow("\\\"", "\\\"")] - [TestMethod] - public void EscapeArgument_ProducesCorrectString(string value, string expectedEscapedValue) - { - // Arrange - StringBuilder stringBuilder = new(); - - // Act - DataRowTestMethodArgumentsInfo.EscapeArgument(value, stringBuilder); - - // Assert - Assert.AreEqual(expectedEscapedValue, stringBuilder.ToString()); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Program.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Program.cs index de3b386a21..9fbdc73473 100644 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Program.cs +++ b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Program.cs @@ -1,12 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Testing.Extensions; -using ExecutionScope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope; - -[assembly: Parallelize(Scope = ExecutionScope.MethodLevel, Workers = 0)] - ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); builder.AddMSTest(() => [Assembly.GetEntryAssembly()!]); diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Properties/launchSettings.json b/test/UnitTests/MSTest.SourceGeneration.UnitTests/Properties/launchSettings.json deleted file mode 100644 index e9745ed222..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/Properties/launchSettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "profiles": { - "MSTest.SourceGeneration.UnitTests": { - "commandName": "Project", - "commandLineArgs": "" - } - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/ReflectionMetadataGeneratorTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/ReflectionMetadataGeneratorTests.cs new file mode 100644 index 0000000000..91d3c79108 --- /dev/null +++ b/test/UnitTests/MSTest.SourceGeneration.UnitTests/ReflectionMetadataGeneratorTests.cs @@ -0,0 +1,1017 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Generators; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.UnitTests; + +[TestClass] +public sealed class ReflectionMetadataGeneratorTests +{ + private const string MinimalMSTestStub = """ + namespace Microsoft.VisualStudio.TestTools.UnitTesting + { + [System.AttributeUsage(System.AttributeTargets.Class)] + public sealed class TestClassAttribute : System.Attribute {} + + [System.AttributeUsage(System.AttributeTargets.Method)] + public class TestMethodAttribute : System.Attribute {} + } + """; + + private const string RuntimeHookStub = """ + namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration + { + public static class ReflectionMetadataHook + { + public static void Register(System.Reflection.Assembly assembly, System.Type[] types, System.Collections.Generic.IReadOnlyDictionary testMethods) { } + } + } + """; + + [TestMethod] + public void Generator_DiscoversTestClassesAndMethods_AndEmitsModuleInitializer() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class MyTests + { + [TestMethod] + public void Test1() {} + + [TestMethod] + public void Test2() {} + + public void NotATest() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + result.GeneratedSources.Should().HaveCount(1); + + // Full-snapshot match so any change to the emitter shape is caught by this test + // (rather than only the small set of substrings other tests probe for). + string generated = NormalizeNewlines(result.GeneratedSources[0].SourceText.ToString()); + const string expected = """ + // + // This file was generated by the MSTest source generator. + // Do not edit it manually; changes will be lost on the next build. + #nullable enable + + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Reflection; + using System.Runtime.CompilerServices; + + namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Generated + { + /// Source-generated MSTest reflection metadata hook for this test assembly. + internal static class MSTestSourceGeneratedReflectionMetadata + { + [ModuleInitializer] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(global::Sample.MyTests))] + internal static void Initialize() + { + var assembly = typeof(MSTestSourceGeneratedReflectionMetadata).Assembly; + var types = new Type[] + { + typeof(global::Sample.MyTests), + }; + var testMethods = new Dictionary + { + [typeof(global::Sample.MyTests)] = new MethodInfo[] + { + ResolveMethod(typeof(global::Sample.MyTests), "Test1", Type.EmptyTypes), + ResolveMethod(typeof(global::Sample.MyTests), "Test2", Type.EmptyTypes), + }, + }; + global::Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.ReflectionMetadataHook.Register(assembly, types, testMethods); + } + + private static MethodInfo ResolveMethod([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string name, Type[] parameterTypes) + { + const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; + foreach (MethodInfo candidate in type.GetMethods(flags)) + { + if (candidate.Name != name) + { + continue; + } + + ParameterInfo[] candidateParameters = candidate.GetParameters(); + if (candidateParameters.Length != parameterTypes.Length) + { + continue; + } + + bool match = true; + for (int i = 0; i < candidateParameters.Length; i++) + { + if (candidateParameters[i].ParameterType != parameterTypes[i]) + { + match = false; + break; + } + } + + if (match) + { + return candidate; + } + } + + throw new MissingMethodException(type.FullName, name); + } + } + } + + """; + + generated.Should().Be(NormalizeNewlines(expected)); + } + + [TestMethod] + public void Generator_GeneratedFile_HasExpectedHintName() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class MyTests + { + [TestMethod] + public void Test1() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + result.GeneratedSources.Should().HaveCount(1); + + // The hint name drives the on-disk path of the emitted file (when EmitCompilerGeneratedFiles + // is enabled) and is used by downstream tooling to identify the generator output. It must + // be deterministic: . + result.GeneratedSources[0].HintName.Should().Be("TestSample.MSTestReflectionMetadata.g.cs"); + } + + [TestMethod] + public void Generator_EmitsOverloadAwareMethodResolution() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class OverloadTests + { + [TestMethod] + public void Run() {} + + [TestMethod] + public void Run(int x) {} + + [TestMethod] + public void Run(string s, int x) {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = NormalizeNewlines(result.GeneratedSources[0].SourceText.ToString()); + + // Match the full testMethods block as a contiguous snippet so order, the new-array vs + // Type.EmptyTypes choice, and the FullName-qualified parameter types are all locked in. + const string expectedTypeMethodsBlock = """ + var testMethods = new Dictionary + { + [typeof(global::Sample.OverloadTests)] = new MethodInfo[] + { + ResolveMethod(typeof(global::Sample.OverloadTests), "Run", Type.EmptyTypes), + ResolveMethod(typeof(global::Sample.OverloadTests), "Run", new Type[] { typeof(global::System.Int32) }), + ResolveMethod(typeof(global::Sample.OverloadTests), "Run", new Type[] { typeof(global::System.String), typeof(global::System.Int32) }), + }, + }; + """; + + generated.Should().Contain(NormalizeNewlines(expectedTypeMethodsBlock)); + } + + [TestMethod] + public void Generator_EmittedSourceCompilesCleanly() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class MyTests + { + [TestMethod] + public void Test1() {} + + [TestMethod] + public void Test2(int x, string s) {} + + [TestMethod] + internal void NonPublicTest() {} + } + } + """; + + Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, RuntimeHookStub, userCode); + + Diagnostic[] errors = outputCompilation.GetDiagnostics() + .Where(d => d.Severity == DiagnosticSeverity.Error) + .ToArray(); + + errors.Should().BeEmpty("the generator output should compile without errors. Diagnostics: " + string.Join("\n", errors.Select(d => d.ToString()))); + } + + [TestMethod] + public void Generator_SkipsStaticAndAbstractTestClasses() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public static class StaticTests + { + [TestMethod] + public static void Test1() {} + } + + [TestClass] + public abstract class AbstractTests + { + [TestMethod] + public void Test1() {} + } + + [TestClass] + public class Concrete : AbstractTests + { + [TestMethod] + public void Test2() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // Neither the static nor the abstract test class is a runnable test class, so they must + // not be emitted into the `types[]` array or the `testMethods{}` dictionary. The + // discovery-side rules have not changed. + generated.Should().NotContain("typeof(global::Sample.StaticTests)"); + generated.Should().NotContain("[typeof(global::Sample.AbstractTests)]"); + + // The concrete derived class is the runnable test class — it must be emitted. + generated.Should().Contain("typeof(global::Sample.Concrete)"); + + // Concrete must surface BOTH its own Test2 AND the inherited Test1 from AbstractTests. + generated.Should().Contain("ResolveMethod(typeof(global::Sample.Concrete), \"Test1\", Type.EmptyTypes)"); + generated.Should().Contain("ResolveMethod(typeof(global::Sample.Concrete), \"Test2\", Type.EmptyTypes)"); + + // The abstract base class must still be referenced by a [DynamicDependency] on the + // generated module initializer so that members declared on it ([ClassInitialize], + // [ClassCleanup], [AssemblyInitialize], [AssemblyCleanup], TestContext setter) are + // preserved by the IL trimmer / Native AOT. Without this hint those members live only + // on the abstract base and are trimmed because [DynamicDependency(All, typeof(Concrete))] + // does not preserve base-type members. + generated.Should().Contain("[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(global::Sample.AbstractTests))]"); + } + + [TestMethod] + public void Generator_EmitsNestedTestClass_WithTypeofReference() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + public class Outer + { + [TestClass] + public class Nested + { + [TestMethod] + public void Test1() {} + } + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // For nested types we must emit `typeof(Outer.Nested)` (dot-qualified C# name) — never + // the runtime FullName form `Outer+Nested` which would not compile. The builder computes + // TypesByName at runtime from typeof(T).FullName, so the emitted source must only carry + // the C# typeof reference. + generated.Should().Contain("typeof(global::Sample.Outer.Nested)"); + generated.Should().NotContain("Sample.Outer+Nested"); + } + + [TestMethod] + public void Generator_HandlesEmptyAssembly() + { + const string userCode = """ + namespace Sample + { + public class NoTests {} + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + + // When no [TestClass] types are discovered, the generator must not emit any source + // (avoids polluting the compilation with an empty module initializer). + result.GeneratedSources.Should().BeEmpty(); + } + + [TestMethod] + public void Generator_SkipsGenericTestMethods() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class GenericMethodTests + { + [TestMethod] + public void GenericTest(T value) {} + + [TestMethod] + public void GenericTestNoParams() {} + + [TestMethod] + public void NormalTest(int x) {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // Generic test methods cannot be emitted by the non-generic module initializer: + // parameter types would include method-level type parameters (typeof(T)) which do not + // bind at module-initializer scope, breaking compilation. They are silently skipped so + // opting into the generator never introduces compile errors that reflection mode would + // tolerate. Plain test methods on the same class must still be emitted. + generated.Should().NotContain("\"GenericTest\""); + generated.Should().NotContain("\"GenericTestNoParams\""); + generated.Should().Contain("ResolveMethod(typeof(global::Sample.GenericMethodTests), \"NormalTest\", new Type[] { typeof(global::System.Int32) })"); + } + + [TestMethod] + public void Generator_SkipsOpenGenericTestClasses() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class Generic + { + [TestMethod] + public void Test1() {} + } + + [TestClass] + public class Concrete + { + [TestMethod] + public void Test2() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // Open generic test classes cannot be emitted as `typeof(Generic)` at module-initializer + // scope, so they are silently skipped. The non-generic sibling must still be emitted. + generated.Should().NotContain("typeof(global::Sample.Generic"); + generated.Should().Contain("typeof(global::Sample.Concrete)"); + } + + [TestMethod] + public void Generator_SkipsMethodsWithByRefParameters() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class ByRefTests + { + [TestMethod] + public void RefTest(ref int x) {} + + [TestMethod] + public void OutTest(out int x) { x = 0; } + + [TestMethod] + public void InTest(in int x) {} + + [TestMethod] + public void RefReadonlyTest(ref readonly int x) {} + + [TestMethod] + public void NormalTest(int x) {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // By-ref signatures (ref/out/in/ref readonly) cannot round-trip through ResolveMethod's + // typeof(T) == ParameterType check, so the generator omits these methods entirely. + // The plain-parameter overload must still be emitted. + generated.Should().NotContain("\"RefTest\""); + generated.Should().NotContain("\"OutTest\""); + generated.Should().NotContain("\"InTest\""); + generated.Should().NotContain("\"RefReadonlyTest\""); + generated.Should().Contain("ResolveMethod(typeof(global::Sample.ByRefTests), \"NormalTest\", new Type[] { typeof(global::System.Int32) })"); + } + + [TestMethod] + public void Generator_SkipsInaccessibleNestedTestClasses() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + public class Outer + { + [TestClass] + private class PrivateNested + { + [TestMethod] + public void Test1() {} + } + + [TestClass] + public class PublicNested + { + [TestMethod] + public void Test2() {} + } + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // A private (or otherwise inaccessible) nested [TestClass] cannot be referenced from + // the generated `internal` module initializer; it would compile as CS0122. The + // generator skips such types but still emits siblings that are reachable. + generated.Should().NotContain("PrivateNested"); + generated.Should().Contain("typeof(global::Sample.Outer.PublicNested)"); + } + + [TestMethod] + public void Generator_SkipsFileLocalTestClasses() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + file class FileLocalTests + { + [TestMethod] + public void HiddenTest() {} + } + + [TestClass] + public class VisibleTests + { + [TestMethod] + public void OpenTest() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // `file`-scoped types are only addressable within their own source file. The generated + // module initializer lives in a different file and would fail with CS9051. Skip them. + generated.Should().NotContain("FileLocalTests"); + generated.Should().NotContain("\"HiddenTest\""); + generated.Should().Contain("typeof(global::Sample.VisibleTests)"); + } + + [TestMethod] + public void Generator_DedupesOverriddenMethodsAcrossInheritance() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + public abstract class BaseTests + { + [TestMethod] + public virtual void TestA() {} + } + + [TestClass] + public class Derived : BaseTests + { + [TestMethod] + public override void TestA() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // The base + override must collapse to a single emit. Counting occurrences of the + // exact ResolveMethod call site catches double-emission regressions in the inheritance + // walk's dedupe-by-signature logic. + int count = CountOccurrences(generated, "ResolveMethod(typeof(global::Sample.Derived), \"TestA\", Type.EmptyTypes)"); + count.Should().Be(1); + } + + [TestMethod] + public void Generator_EmitsMultipleTestClasses_InOrder() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class AlphaTests + { + [TestMethod] + public void TestA() {} + } + + [TestClass] + public class BetaTests + { + [TestMethod] + public void TestB() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = NormalizeNewlines(result.GeneratedSources[0].SourceText.ToString()); + + // Two [TestClass]es must produce two entries in the types/testMethods locals and + // two [DynamicDependency] attributes on Initialize. We assert each block as a + // contiguous snippet so the per-class layout is locked in. + const string expectedDynamicDependencies = """ + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(global::Sample.AlphaTests))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(global::Sample.BetaTests))] + """; + generated.Should().Contain(NormalizeNewlines(expectedDynamicDependencies)); + + const string expectedTypes = """ + var types = new Type[] + { + typeof(global::Sample.AlphaTests), + typeof(global::Sample.BetaTests), + }; + """; + generated.Should().Contain(NormalizeNewlines(expectedTypes)); + + const string expectedTypeMethods = """ + var testMethods = new Dictionary + { + [typeof(global::Sample.AlphaTests)] = new MethodInfo[] + { + ResolveMethod(typeof(global::Sample.AlphaTests), "TestA", Type.EmptyTypes), + }, + [typeof(global::Sample.BetaTests)] = new MethodInfo[] + { + ResolveMethod(typeof(global::Sample.BetaTests), "TestB", Type.EmptyTypes), + }, + }; + """; + generated.Should().Contain(NormalizeNewlines(expectedTypeMethods)); + } + + [TestMethod] + public void Generator_EmitsTestClass_InGlobalNamespace() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class GlobalNamespaceTests + { + [TestMethod] + public void Test1() {} + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // A test class declared in the global namespace must be referenced with `global::TypeName` + // (no namespace segment between `global::` and the type), and still produce both the + // types-array entry and the testMethods-dictionary entry. + generated.Should().Contain("typeof(global::GlobalNamespaceTests)"); + generated.Should().Contain("ResolveMethod(typeof(global::GlobalNamespaceTests), \"Test1\", Type.EmptyTypes)"); + } + + [TestMethod] + public void Generator_DiscoversMethodsViaCustomTestMethodAttributeSubclass() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + public sealed class CustomTestMethodAttribute : TestMethodAttribute {} + + [TestClass] + public class CustomAttributeTests + { + [CustomTestMethod] + public void DerivedAttributeTest() {} + + [TestMethod] + public void PlainTest() {} + + public void NotATest() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // The generator's HasTestMethodAttribute walks the attribute class's base type chain, + // so any subclass of TestMethodAttribute (e.g. DataTestMethod, custom attributes) must + // be recognised the same way as TestMethod itself. + generated.Should().Contain("ResolveMethod(typeof(global::Sample.CustomAttributeTests), \"DerivedAttributeTest\", Type.EmptyTypes)"); + generated.Should().Contain("ResolveMethod(typeof(global::Sample.CustomAttributeTests), \"PlainTest\", Type.EmptyTypes)"); + generated.Should().NotContain("\"NotATest\""); + } + + [TestMethod] + public void Generator_EmitsInternalTestClasses() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + internal class InternalTests + { + [TestMethod] + public void Test1() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // Internal [TestClass]es are reachable from the generated `internal` module + // initializer (same assembly) and so must be emitted just like public ones. + generated.Should().Contain("typeof(global::Sample.InternalTests)"); + generated.Should().Contain("ResolveMethod(typeof(global::Sample.InternalTests), \"Test1\", Type.EmptyTypes)"); + } + + [TestMethod] + public void Generator_EmitsDynamicDependencyForFullBaseTypeChain() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + public abstract class GrandparentTests + { + [AssemblyInitialize] + public static void AssemblyInit(TestContext context) {} + } + + public abstract class ParentTests : GrandparentTests + { + [ClassInitialize] + public static void ClassInit(TestContext context) {} + } + + [TestClass] + public class Derived : ParentTests + { + [TestMethod] + public void Test1() {} + } + } + """; + + const string testContextStub = """ + namespace Microsoft.VisualStudio.TestTools.UnitTesting + { + public class TestContext {} + + [System.AttributeUsage(System.AttributeTargets.Method)] + public sealed class AssemblyInitializeAttribute : System.Attribute {} + + [System.AttributeUsage(System.AttributeTargets.Method)] + public sealed class ClassInitializeAttribute : System.Attribute {} + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, testContextStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // Every base type up the chain must be referenced by a [DynamicDependency] so the + // trimmer keeps members like [AssemblyInitialize] (declared on GrandparentTests) and + // [ClassInitialize] (declared on ParentTests). The concrete [TestClass] still owns the + // discovery entry, but the trimmer roots are emitted per type. + generated.Should().Contain("[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(global::Sample.Derived))]"); + generated.Should().Contain("[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(global::Sample.ParentTests))]"); + generated.Should().Contain("[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(global::Sample.GrandparentTests))]"); + + // Bases must NOT appear in the types[] array or the testMethods{} dictionary — they are + // not runnable test classes; discovery still goes through the concrete [TestClass]. + generated.Should().NotContain("typeof(global::Sample.GrandparentTests),"); + generated.Should().NotContain("[typeof(global::Sample.GrandparentTests)]"); + generated.Should().NotContain("typeof(global::Sample.ParentTests),"); + generated.Should().NotContain("[typeof(global::Sample.ParentTests)]"); + } + + [TestMethod] + public void Generator_DedupesDynamicDependencyForBaseShared() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + public abstract class SharedBase + { + [TestMethod] + public void Inherited() {} + } + + [TestClass] + public class DerivedA : SharedBase + { + [TestMethod] + public void TestA() {} + } + + [TestClass] + public class DerivedB : SharedBase + { + [TestMethod] + public void TestB() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // The shared abstract base produces only ONE [DynamicDependency] regardless of how + // many derived test classes reference it. Without deduping, every shared base would + // be emitted N times for N derived classes, bloating the generated source. + int sharedBaseCount = CountOccurrences( + generated, + "[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(global::Sample.SharedBase))]"); + sharedBaseCount.Should().Be(1); + } + + [TestMethod] + public void Generator_DoesNotEmitDynamicDependencyForObjectBase() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + [TestClass] + public class StandaloneTests + { + [TestMethod] + public void Test1() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // The base-type walk stops at System.Object — we never emit a [DynamicDependency] for + // it (would be useless noise and would increase the surface of types we root with the trimmer). + generated.Should().NotContain("typeof(global::System.Object)"); + generated.Should().NotContain("typeof(object)"); + } + + [TestMethod] + public void Generator_DoesNotEmitDynamicDependencyForInaccessibleBase() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + public class Outer + { + // A private nested base cannot be referenced from the generated `internal` + // module initializer (CS0122). Skip it from [DynamicDependency] emission to + // keep the generated source compilable. + private class PrivateBase + { + [TestMethod] + public void InheritedFromPrivate() {} + } + + [TestClass] + public class PublicDerived : PrivateBase + { + [TestMethod] + public void TestB() {} + } + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // The private base must not surface in any reference; the derived class still must. + generated.Should().NotContain("PrivateBase"); + generated.Should().Contain("typeof(global::Sample.Outer.PublicDerived)"); + } + + [TestMethod] + public void Generator_DoesNotEmitDynamicDependencyForGenericBase() + { + const string userCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Sample + { + public abstract class GenericBase + { + [TestMethod] + public void InheritedTest() {} + } + + [TestClass] + public class Derived : GenericBase + { + [TestMethod] + public void Test1() {} + } + } + """; + + GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode); + + result.Diagnostics.Should().BeEmpty(); + string generated = result.GeneratedSources[0].SourceText.ToString(); + + // For this conservative iteration we skip generic base types from [DynamicDependency] + // emission — emitting `typeof(GenericBase)` is technically valid but adds edge + // cases (open vs. closed, type-parameter capture) that the rest of the generator does + // not exercise. The derived class itself is still emitted, and the inherited test + // method is still surfaced through the base-walk in the methods collection. + generated.Should().NotContain("typeof(global::Sample.GenericBase"); + generated.Should().Contain("typeof(global::Sample.Derived)"); + generated.Should().Contain("ResolveMethod(typeof(global::Sample.Derived), \"InheritedTest\", Type.EmptyTypes)"); + } + + private static string NormalizeNewlines(string value) + => value.Replace("\r\n", "\n"); + + private static int CountOccurrences(string source, string needle) + { + int count = 0; + int index = 0; + while ((index = source.IndexOf(needle, index, StringComparison.Ordinal)) >= 0) + { + count++; + index += needle.Length; + } + + return count; + } + + private static GeneratorRunResult RunGenerator(params string[] sources) + { + IEnumerable trees = sources.Select(s => CSharpSyntaxTree.ParseText(s)); + var references = new MetadataReference[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.ModuleInitializerAttribute).Assembly.Location), + }; + + var compilation = CSharpCompilation.Create( + "TestSample", + trees, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + GeneratorDriver driver = CSharpGeneratorDriver.Create(new ReflectionMetadataGenerator()); + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _); + + return driver.GetRunResult().Results[0]; + } + + private static Compilation RunGeneratorAndGetCompilation(params string[] sources) + { + IEnumerable trees = sources.Select(s => CSharpSyntaxTree.ParseText(s)); + var references = new MetadataReference[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.ModuleInitializerAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Reflection.Assembly).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Collections.Generic.Dictionary<,>).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Reflection.MethodInfo).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Reflection.BindingFlags).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.MissingMethodException).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location), + }; + + var compilation = CSharpCompilation.Create( + "TestSample", + trees, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + GeneratorDriver driver = CSharpGeneratorDriver.Create(new ReflectionMetadataGenerator()); + driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out _); + + return outputCompilation; + } +} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestBase.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestBase.cs deleted file mode 100644 index 7a6d1c82ee..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestBase.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests; - -/// -/// Empty test base, because TestInfrastructure project depends on Testing.Framework, and we cannot use that. -/// -public abstract class TestBase; diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/CSharpCodeFixVerifier.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/CSharpCodeFixVerifier.cs deleted file mode 100644 index 7b2cfb8660..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/CSharpCodeFixVerifier.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Testing; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -internal static class CSharpCodeFixVerifier - where TAnalyzer : DiagnosticAnalyzer, new() - where TCodeFix : CodeFixProvider, new() -{ - public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) - { - var test = new Test { TestCode = source }; - test.ExpectedDiagnostics.AddRange(expected); - await test.RunAsync(); - } - - public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) - => CSharpCodeFixVerifier.Diagnostic(descriptor); - - public sealed class Test : CSharpCodeFixTest; -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/GeneratorCompilationResult.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/GeneratorCompilationResult.cs deleted file mode 100644 index 7fa121a363..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/GeneratorCompilationResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Emit; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -internal sealed class GeneratorCompilationResult(GeneratorDriverRunResult runResult, GeneratorDriverTimingInfo timingInfo, - EmitResult emitResult, string? failingGeneratedCode) -{ - public ImmutableArray GeneratedTrees => RunResult.GeneratedTrees; - - public GeneratorDriverRunResult RunResult { get; } = runResult; - - public GeneratorDriverTimingInfo TimingInfo { get; } = timingInfo; - - public EmitResult EmitResult { get; } = emitResult; - - public string? FailingGeneratedCode { get; } = failingGeneratedCode; -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/GeneratorTester.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/GeneratorTester.cs deleted file mode 100644 index 87f1705900..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/GeneratorTester.cs +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Immutable; - -using AwesomeAssertions; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Emit; -using Microsoft.CodeAnalysis.Testing; -using Microsoft.Testing.Extensions; -using Microsoft.Testing.Extensions.TrxReport.Abstractions; -using Microsoft.Testing.Platform.Extensions.Messages; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -internal sealed class GeneratorTester -{ - private readonly Func _incrementalGeneratorFactory; - private readonly string[] _additionalReferences; - private static readonly SemaphoreSlim Lock = new(1); - - public GeneratorTester(Func incrementalGeneratorFactory, string[] additionalReferences) - { - _incrementalGeneratorFactory = incrementalGeneratorFactory; - _additionalReferences = additionalReferences; - } - - public static GeneratorTester TestGraph { get; } = - new( - () => new TestNodesGenerator(), - [ - // Microsoft.Testing.Platform dll - Assembly.GetAssembly(typeof(IProperty))!.Location, - - // Microsoft.Testing.Framework dll - Assembly.GetAssembly(typeof(TestNode))!.Location, - - // Microsoft.Testing.Extensions dll - Assembly.GetAssembly(typeof(TrxReportExtensions))!.Location, - - // Microsoft.Testing.Extensions.TrxReport.Abstractions dll - Assembly.GetAssembly(typeof(TrxExceptionProperty))!.Location, - - // MSTest.TestFramework dll - Assembly.GetAssembly(typeof(TestClassAttribute))!.Location - ]); - - public static ImmutableArray? Net80MetadataReferences { get; set; } - - public async Task CompileAndExecuteAsync(string source, CancellationToken cancellationToken) - => await CompileAndExecuteAsync([source], cancellationToken); - - public async Task CompileAndExecuteAsync(string[] sources, CancellationToken cancellationToken) - { - // Cache the resolution in local and try to fire the finalizers - // In CI sometime we have a crash for http connection and the suspect is - // this call below that connects to nuget.org - if (Net80MetadataReferences is null) - { - await Lock.WaitAsync(cancellationToken); - try - { - if (Net80MetadataReferences is null) - { - string nuGetConfigFilePath = Path.Combine(RootFinder.Find(), "NuGet.config"); - - Net80MetadataReferences = - await ReferenceAssemblies.Net.Net80.WithNuGetConfigFilePath(nuGetConfigFilePath).ResolveAsync(LanguageNames.CSharp, cancellationToken); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - } - } - finally - { - Lock.Release(); - } - } - - MetadataReference[] metadataReferences = [.. Net80MetadataReferences.Value, .. _additionalReferences.Select(loc => MetadataReference.CreateFromFile(loc))]; - - var compilation = CSharpCompilation.Create( - "TestAssembly", - sources.Select(source => CSharpSyntaxTree.ParseText(source, cancellationToken: cancellationToken)), - metadataReferences, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - - ISourceGenerator generator = _incrementalGeneratorFactory().AsSourceGenerator(); - GeneratorDriver driver = CSharpGeneratorDriver.Create( - generators: [generator]); - - driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation? outputCompilation, - out ImmutableArray diagnostics, cancellationToken); - diagnostics.Should().BeEmpty(); - - using var ms = new MemoryStream(); - EmitResult result = outputCompilation.Emit(ms, cancellationToken: cancellationToken); - - GeneratorDriverRunResult runResult = driver.GetRunResult(); - GeneratorDriverTimingInfo timingInfo = driver.GetTimingInfo(); - - if (result.Success) - { - return new(runResult, timingInfo, result, null); - } - - var code = new StringBuilder(); - - // Append diagnostics that are not tied to any file. - foreach (Diagnostic? globalDiagnostic in result.Diagnostics.Where(d => d.Location.SourceTree == null || string.IsNullOrWhiteSpace(d.Location.SourceTree.FilePath))) - { - code.AppendLine(globalDiagnostic.ToString()); - } - - foreach (SyntaxTree output in outputCompilation.SyntaxTrees) - { - IEnumerable d = output.GetDiagnostics(cancellationToken); - - var diagnosticsByLine = new Dictionary>(); - result.Diagnostics - .Where(d => !string.IsNullOrEmpty(output.FilePath) && d.Location.SourceTree?.FilePath == output.FilePath) - .GroupBy(d => d.Location.GetLineSpan().StartLinePosition) - .ToList() - .ForEach(f => - { - if (diagnosticsByLine.TryGetValue(f.Key.Line, out List? list)) - { - list.AddRange(f); - } - else - { - var l = new List(); - l.AddRange(f); - diagnosticsByLine[f.Key.Line] = l; - } - }); - - if (diagnosticsByLine.Count == 0) - { - continue; - } - - code.Append("file '").Append(output.FilePath).AppendLine("':"); - string[] lines = output.ToString().Split('\n'); - int length = lines.Length; - int pad = length.ToString(CultureInfo.InvariantCulture).Length; - for (int i = 0; i < length; i++) - { - if (diagnosticsByLine.TryGetValue(i, out List? diagnosticsForLine)) - { - code.AppendLine(); - foreach (Diagnostic diagnostic in diagnosticsForLine) - { - code.Append(">>> ").AppendLine(diagnostic.ToString()); - } - } - - // Add line number (starting from 1) - code.Append((i + 1).ToString(CultureInfo.InvariantCulture).PadLeft(pad, '0')); - code.Append(' ').AppendLine(lines[i]); - } - - code.AppendLine(); - } - - return new(runResult, timingInfo, result, code.ToString()); - } -} diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/TestingFrameworkVerifier.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/TestingFrameworkVerifier.cs deleted file mode 100644 index d21e00b691..0000000000 --- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/TestUtilities/TestingFrameworkVerifier.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Testing; - -namespace Microsoft.Testing.Framework.SourceGeneration.UnitTests.TestUtilities; - -internal sealed class TestingFrameworkVerifier : IVerifier -{ - public TestingFrameworkVerifier() - : this([]) - { - } - - internal TestingFrameworkVerifier(ImmutableStack context) - => Context = context ?? throw new ArgumentNullException(nameof(context)); - - public ImmutableStack Context { get; } - - public void Empty(string collectionName, IEnumerable collection) => Assert.AreNotEqual(true, collection?.Any(), CreateMessage($"expected '{collectionName}' to be empty, contains '{collection?.Count()}' elements")); - - public void Equal(T expected, T actual, string? message = null) - { - if (message is null && Context.IsEmpty) - { - Assert.AreEqual(expected, actual); - } - else - { - Assert.AreEqual(expected, actual, CreateMessage(message)); - } - } - - [DoesNotReturn] - public void Fail(string? message = null) - { - if (message is null && Context.IsEmpty) - { - Assert.Fail(); - } - else - { - Assert.Fail(CreateMessage(message)); - } - - throw new InvalidOperationException("This program location is thought to be unreachable."); - } - - public void False([DoesNotReturnIf(true)] bool assert, string? message = null) - { - if (message is null && Context.IsEmpty) - { - Assert.IsFalse(assert); - } - else - { - Assert.IsFalse(assert, CreateMessage(message)); - } - } - - public void LanguageIsSupported(string language) => Assert.IsFalse(language is not LanguageNames.CSharp and not LanguageNames.VisualBasic, CreateMessage($"Unsupported Language: '{language}'")); - - public void NotEmpty(string collectionName, IEnumerable collection) => Assert.IsNotEmpty(collection, CreateMessage($"expected '{collectionName}' to be non-empty, contains")); - - public IVerifier PushContext(string context) - { - Assert.AreEqual(typeof(TestingFrameworkVerifier), GetType()); - return new TestingFrameworkVerifier(Context.Push(context)); - } - - public void SequenceEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer? equalityComparer = null, string? message = null) - { - var comparer = new SequenceEqualEnumerableEqualityComparer(equalityComparer); - bool areEqual = comparer.Equals(expected, actual); - if (!areEqual) - { - Assert.Fail(CreateMessage(message)); - } - } - - public void True([DoesNotReturnIf(false)] bool assert, string? message = null) - { - if (message is null && Context.IsEmpty) - { - Assert.IsTrue(assert); - } - else - { - Assert.IsTrue(assert, CreateMessage(message)); - } - } - - private string CreateMessage(string? message) - { - foreach (string frame in Context) - { - message = "Context: " + frame + Environment.NewLine + message; - } - - return message ?? string.Empty; - } - - private sealed class SequenceEqualEnumerableEqualityComparer : IEqualityComparer?> - { - private readonly IEqualityComparer _itemEqualityComparer; - - public SequenceEqualEnumerableEqualityComparer(IEqualityComparer? itemEqualityComparer) - => _itemEqualityComparer = itemEqualityComparer ?? EqualityComparer.Default; - - public bool Equals(IEnumerable? x, IEnumerable? y) - => ReferenceEquals(x, y) - || (x is not null && y is not null && x.SequenceEqual(y, _itemEqualityComparer)); - - public int GetHashCode(IEnumerable? obj) - { - if (obj is null) - { - return 0; - } - - // From System.Tuple - // - // The suppression is required due to an invalid contract in IEqualityComparer - // https://github.com/dotnet/runtime/issues/30998 - return obj - .Select(item => _itemEqualityComparer.GetHashCode(item!)) - .Aggregate( - 0, - (aggHash, nextHash) => ((aggHash << 5) + aggHash) ^ nextHash); - } - } -}