|
| 1 | +--- |
| 2 | +name: apex-migration |
| 3 | +description: >- |
| 4 | + Migrate NuGet PowerShell E2E tests to C# Apex tests. Use this skill whenever the user asks to |
| 5 | + migrate, convert, or port a PowerShell end-to-end test from test/EndToEnd/tests/ to an Apex test |
| 6 | + in test/NuGet.Tests.Apex/. Also trigger when the user mentions "Apex test", "migrate PS test", |
| 7 | + "E2E test migration", "PMC test", or references any PowerShell test function like |
| 8 | + Install-PackageTest or Update-PackageTest and wants it rewritten in C#. Even if the user just |
| 9 | + says "migrate this test" while looking at a PS E2E file, use this skill. |
| 10 | +--- |
| 11 | + |
| 12 | +# Migrating PowerShell E2E Tests to Apex Tests |
| 13 | + |
| 14 | +PowerShell E2E tests live in `test/EndToEnd/tests/`. Apex tests live in |
| 15 | +`test/NuGet.Tests.Apex/NuGet.Tests.Apex/NuGetEndToEndTests/`. The goal is to migrate PS tests |
| 16 | +to C# Apex tests that run the **exact same scenario**, then remove the PS test function. |
| 17 | + |
| 18 | +## Workflow |
| 19 | + |
| 20 | +1. **Read the PS test** — understand what it does: which project type, which PMC commands, what assertions. |
| 21 | +2. **Pick the right Apex file** — match the scenario to an existing test class (see File Placement below). |
| 22 | +3. **Translate** — use the mappings in this skill to convert each PS construct to its Apex equivalent. |
| 23 | +4. **Verify** — run `get_errors` or build the Apex project to confirm it compiles cleanly. |
| 24 | +5. **Remove the PS function** — delete the migrated function from the PS test file. |
| 25 | +6. **If already covered** — if an existing Apex test already covers the same scenario, just delete the PS test. No new Apex test needed. |
| 26 | +7. **Update this skill** — if you discovered new mappings, gotchas, or corrections, add them to the appropriate section of this file (e.g., new rows in the mapping tables, new bullets in Common gotchas). |
| 27 | + |
| 28 | +## File placement |
| 29 | + |
| 30 | +Choose the target file by **interaction surface**, not project type: |
| 31 | + |
| 32 | +| Interaction surface | Apex file | |
| 33 | +|---|---| |
| 34 | +| PMC commands (Install-Package, Update-Package, etc.) | `NuGetConsoleTestCase.cs` | |
| 35 | +| NuGet UI / Package Manager dialog | `NuGetUITestCase.cs` | |
| 36 | +| IVsPackageInstaller / IVsServices API | `IVsServicesTestCase.cs` | |
| 37 | +| Sync/binding redirect scenarios | `SyncPackageTestCase.cs` | |
| 38 | +| Audit / vulnerability scenarios | `NuGetAuditTests.cs` | |
| 39 | +| .NET Core project-creation / restore / source-mapping | `NetCoreProjectTestCase.cs` | |
| 40 | + |
| 41 | +PMC tests for PackageReference projects still go in `NuGetConsoleTestCase.cs` — the deciding |
| 42 | +factor is whether the test exercises the PMC console, not the project's package management style. |
| 43 | + |
| 44 | +## Template mapping |
| 45 | + |
| 46 | +| PowerShell function | Apex `ProjectTemplate` | Package management | Verified | |
| 47 | +|---|---|---|---| |
| 48 | +| `New-ConsoleApplication` | `ProjectTemplate.ConsoleApplication` | packages.config | ✅ | |
| 49 | +| `New-ClassLibrary` | `ProjectTemplate.ClassLibrary` | packages.config | ✅ | |
| 50 | +| `New-WebSite` | `ProjectTemplate.WebSiteEmpty` | packages.config | ✅ | |
| 51 | +| `New-WebApplication` | `ProjectTemplate.WebApplicationEmpty` | packages.config | ❌ | |
| 52 | +| `New-WPFApplication` | `ProjectTemplate.WPFApplication` | packages.config | ❌ | |
| 53 | +| `New-MvcApplication` | `ProjectTemplate.WebApplicationEmptyMvc` | packages.config | ❌ | |
| 54 | +| `New-FSharpLibrary` | `ProjectTemplate.FSharpLibrary` | PackageReference | ❌ | |
| 55 | +| `New-NetCoreConsoleApp` | `ProjectTemplate.NetCoreConsoleApp` | PackageReference | ✅ | |
| 56 | +| `New-NetStandardClassLib` | `ProjectTemplate.NetStandardClassLib` | PackageReference | ✅ | |
| 57 | + |
| 58 | +| PowerShell function | Apex equivalent | |
| 59 | +|---|---| |
| 60 | +| `New-SolutionFolder 'Name'` | `testContext.SolutionService.AddSolutionFolder("Name")` | |
| 61 | + |
| 62 | +The package management style determines which assertion methods to use — packages.config projects |
| 63 | +use `AssertPackageInPackagesConfig`, while PackageReference projects use `AssertPackageInAssetsFile`. |
| 64 | + |
| 65 | +> **Note:** This table covers the most common PS project factories. Some PS tests use specialized |
| 66 | +> factories like `New-ClassLibraryNET46`, `New-BuildIntegratedProj`, `New-UwpPackageRefClassLibrary`, |
| 67 | +> or `New-NetCoreConsoleMultipleTargetFrameworksApp`. These don't have a 1:1 `ProjectTemplate` enum |
| 68 | +> value — check the Apex `ProjectTemplate` enum and existing tests for the closest match, or create |
| 69 | +> a standard template and modify the csproj afterward (e.g., for multi-targeting). |
| 70 | +
|
| 71 | +## Command execution |
| 72 | + |
| 73 | +| Scenario | Apex API | |
| 74 | +|---|---| |
| 75 | +| Standard install with `-Version` | `nugetConsole.InstallPackageFromPMC(packageName, packageVersion)` | |
| 76 | +| Install with extra flags (`-Source`, `-WhatIf`, `-IgnoreDependencies`) | `nugetConsole.Execute($"Install-Package {packageName} -ProjectName {project.Name} -Source {source}")` | |
| 77 | +| Standard uninstall | `nugetConsole.UninstallPackageFromPMC(packageName)` | |
| 78 | +| Standard update with `-Version` | `nugetConsole.UpdatePackageFromPMC(packageName, packageVersion)` | |
| 79 | +| Update with `-Safe`, `-Reinstall`, etc. | `nugetConsole.Execute($"Update-Package {packageName} -Safe")` | |
| 80 | +| Any raw PMC command | `nugetConsole.Execute(command)` | |
| 81 | + |
| 82 | +**Key rule:** Both `InstallPackageFromPMC()` and `UpdatePackageFromPMC()` always inject `-Version`. |
| 83 | +If the original PS test does **not** use `-Version`, use `Execute()` with the raw command string |
| 84 | +instead — using the helper changes the semantics. |
| 85 | + |
| 86 | +**PowerShell session state is accessible.** `nugetConsole.Execute()` runs in a live PMC PowerShell |
| 87 | +session. It can execute **any** PowerShell command, not just NuGet commands. This means PS session |
| 88 | +state — global variables (`$global:InstallVar`), registered functions |
| 89 | +(`Test-Path function:\Get-World`), environment checks — can all be queried and asserted via |
| 90 | +`Execute()` + `IsMessageFoundInPMC()`. Do not skip tests just because they assert PS session state. |
| 91 | + |
| 92 | +## Assertion mapping |
| 93 | + |
| 94 | +| PowerShell assertion | Apex equivalent | |
| 95 | +|---|---| |
| 96 | +| `Assert-Package $p PackageName Version` (packages.config) | `CommonUtility.AssertPackageInPackagesConfig(VisualStudio, testContext.Project, packageName, version, Logger)` | |
| 97 | +| `Assert-Package $p PackageName` (no version, packages.config) | `CommonUtility.AssertPackageInPackagesConfig(VisualStudio, testContext.Project, packageName, Logger)` | |
| 98 | +| `Assert-Package $p PackageName Version` (PackageReference) | `CommonUtility.AssertPackageInAssetsFile(VisualStudio, testContext.Project, packageName, version, Logger)` | |
| 99 | +| `Assert-Throws { ... } $expectedMessage` | `nugetConsole.IsMessageFoundInPMC(expectedMessage)` — PMC errors appear as text, not C# exceptions | |
| 100 | +| `Assert-Null (Get-ProjectPackage ...)` / not installed | `CommonUtility.AssertPackageNotInPackagesConfig(VisualStudio, testContext.Project, packageName, Logger)` | |
| 101 | +| `Assert-NoPackage $p PackageName Version` (PackageReference) | `CommonUtility.AssertPackageNotInAssetsFile(VisualStudio, testContext.Project, packageName, version, Logger)` | |
| 102 | +| `Assert-PackageReference $p PackageName Version` | `CommonUtility.AssertPackageReferenceExists(VisualStudio, testContext.Project, packageName, version, Logger)` | |
| 103 | +| `Assert-NoPackageReference $p PackageName` | `CommonUtility.AssertPackageReferenceDoesNotExist(VisualStudio, testContext.Project, packageName, Logger)` | |
| 104 | + |
| 105 | +## Package sources |
| 106 | + |
| 107 | +| PowerShell source | Apex equivalent | |
| 108 | +|---|---| |
| 109 | +| `$context.RepositoryRoot` / `$context.RepositoryPath` | `testContext.PackageSource` — create packages with `CommonUtility.CreatePackageInSourceAsync()` | |
| 110 | +| No `-Source` (uses nuget.org) | Create a local package with `CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, ...)` — never depend on nuget.org | |
| 111 | +| Hardcoded invalid sources (`http://example.com`, `ftp://...`) | Use the same hardcoded strings directly | |
| 112 | + |
| 113 | +### Creating test packages |
| 114 | + |
| 115 | +For simple packages: |
| 116 | +```csharp |
| 117 | +await CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, packageName, packageVersion); |
| 118 | +``` |
| 119 | + |
| 120 | +For packages with dependencies: |
| 121 | +```csharp |
| 122 | +await CommonUtility.CreateDependenciesPackageInSourceAsync( |
| 123 | + testContext.PackageSource, packageName, packageVersion, dependencyName, dependencyVersion); |
| 124 | +``` |
| 125 | + |
| 126 | +For .NET Framework-specific packages: |
| 127 | +```csharp |
| 128 | +await CommonUtility.CreateNetFrameworkPackageInSourceAsync( |
| 129 | + testContext.PackageSource, packageName, packageVersion); |
| 130 | +``` |
| 131 | + |
| 132 | +## NuGet.Config manipulation |
| 133 | + |
| 134 | +PS tests that use `Get-VSComponentModel` + `ISettings` to modify NuGet config at runtime can be |
| 135 | +migrated by pre-configuring `SimpleTestPathContext` before passing it to `ApexTestContext`. |
| 136 | + |
| 137 | +**Via Settings API** (preferred): |
| 138 | +```csharp |
| 139 | +using var simpleTestPathContext = new SimpleTestPathContext(); |
| 140 | +simpleTestPathContext.Settings.AddSource("PrivateRepo", privatePath); |
| 141 | + |
| 142 | +using var testContext = new ApexTestContext(VisualStudio, projectTemplate, Logger, |
| 143 | + simpleTestPathContext: simpleTestPathContext); |
| 144 | +``` |
| 145 | + |
| 146 | +**Via raw config file** (for settings not covered by the API like `dependencyVersion` or `bindingRedirects`): |
| 147 | +```csharp |
| 148 | +using var simpleTestPathContext = new SimpleTestPathContext(); |
| 149 | +File.WriteAllText(simpleTestPathContext.NuGetConfig, |
| 150 | + $@"<?xml version=""1.0"" encoding=""utf-8""?> |
| 151 | +<configuration> |
| 152 | + <config> |
| 153 | + <add key=""dependencyVersion"" value=""HighestPatch"" /> |
| 154 | + </config> |
| 155 | + <packageSources> |
| 156 | + <clear /> |
| 157 | + <add key=""source"" value=""{simpleTestPathContext.PackageSource}"" /> |
| 158 | + </packageSources> |
| 159 | +</configuration>"); |
| 160 | + |
| 161 | +using var testContext = new ApexTestContext(VisualStudio, projectTemplate, Logger, |
| 162 | + simpleTestPathContext: simpleTestPathContext); |
| 163 | +``` |
| 164 | + |
| 165 | +## Test structure patterns |
| 166 | + |
| 167 | +### Error-path tests (no package creation needed) — synchronous |
| 168 | + |
| 169 | +```csharp |
| 170 | +[TestMethod] |
| 171 | +[Timeout(DefaultTimeout)] |
| 172 | +public void DescriptiveTestName_Fails() |
| 173 | +{ |
| 174 | + using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.ConsoleApplication, Logger); |
| 175 | + |
| 176 | + var packageName = "Rules"; |
| 177 | + var source = @"c:\temp\data"; |
| 178 | + var expectedMessage = $"Unable to find package '{packageName}' at source '{source}'. Source not found."; |
| 179 | + |
| 180 | + var nugetConsole = GetConsole(testContext.Project); |
| 181 | + nugetConsole.Execute($"Install-Package {packageName} -ProjectName {testContext.Project.Name} -Source {source}"); |
| 182 | + |
| 183 | + Assert.IsTrue( |
| 184 | + nugetConsole.IsMessageFoundInPMC(expectedMessage), |
| 185 | + $"Expected error message was not found in PMC output. Actual output: {nugetConsole.GetText()}"); |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +### Success-path tests (need package creation) — async |
| 190 | + |
| 191 | +```csharp |
| 192 | +[TestMethod] |
| 193 | +[Timeout(DefaultTimeout)] |
| 194 | +public async Task DescriptiveTestNameAsync() |
| 195 | +{ |
| 196 | + using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.ConsoleApplication, Logger); |
| 197 | + |
| 198 | + var packageName = "TestPackage"; |
| 199 | + var packageVersion = "1.0.0"; |
| 200 | + await CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, packageName, packageVersion); |
| 201 | + |
| 202 | + var nugetConsole = GetConsole(testContext.Project); |
| 203 | + nugetConsole.InstallPackageFromPMC(packageName, packageVersion); |
| 204 | + |
| 205 | + CommonUtility.AssertPackageInPackagesConfig(VisualStudio, testContext.Project, packageName, packageVersion, Logger); |
| 206 | +} |
| 207 | +``` |
| 208 | + |
| 209 | +### Data-driven tests (multiple project templates) |
| 210 | + |
| 211 | +When the same scenario applies to multiple project types, use `[DataTestMethod]`: |
| 212 | +```csharp |
| 213 | +[DataTestMethod] |
| 214 | +[DataRow(ProjectTemplate.NetCoreConsoleApp)] |
| 215 | +[DataRow(ProjectTemplate.NetStandardClassLib)] |
| 216 | +[Timeout(DefaultTimeout)] |
| 217 | +public async Task InstallPackageForMultipleProjectTypesAsync(ProjectTemplate projectTemplate) |
| 218 | +{ |
| 219 | + using var testContext = new ApexTestContext(VisualStudio, projectTemplate, Logger); |
| 220 | + // ... test body |
| 221 | +} |
| 222 | +``` |
| 223 | + |
| 224 | +### Multi-targeted project tests |
| 225 | + |
| 226 | +To create a multi-targeted project, modify the csproj after project creation: |
| 227 | +```csharp |
| 228 | +using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.NetCoreConsoleApp, Logger); |
| 229 | +// Modify csproj to multi-target via XDocument: |
| 230 | +// change <TargetFramework> to <TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks> |
| 231 | +``` |
| 232 | + |
| 233 | +## Style rules |
| 234 | + |
| 235 | +- Use `using var` (inline using declaration), not `using (var ...) { }`. |
| 236 | +- Place migrated tests before the static helper methods (`GetNetCoreTemplates`, etc.) in the file. |
| 237 | +- Method names: `{Action}FromPMC{Scenario}[_Fails|Async]`. Suffix with `_Fails` for error tests, |
| 238 | + `Async` for async tests. |
| 239 | +- Always include `[Timeout(DefaultTimeout)]`. |
| 240 | +- Always include `nugetConsole.GetText()` in assertion failure messages for diagnostics. |
| 241 | +- Use `var` for local variables except value tuples (use decomposed names). |
| 242 | +- The test class inherits `SharedVisualStudioHostTestClass` which provides `VisualStudio` and `Logger`. |
| 243 | +- Get PMC console via `GetConsole(testContext.Project)` helper method in the test class. |
| 244 | + |
| 245 | +## Tests that should NOT be migrated |
| 246 | + |
| 247 | +Skip PS tests that: |
| 248 | +- Use `Assert-BindingRedirect` — binding redirect tests are already `[SkipTest]` in PS and not |
| 249 | + worth migrating. |
| 250 | +- Depend on **DTE project hierarchy semantics** (e.g., `Get-ProjectItem` to check tree structure, |
| 251 | + parent/child relationships). However, if the PS test only uses `Get-ProjectItem` / |
| 252 | + `Get-ProjectItemPath` to verify a **file exists on disk**, migrate it using filesystem assertions |
| 253 | + instead: `File.Exists(path)`, XML reads on the project file, or |
| 254 | + `CommonUtility.WaitForFileExists()`. |
| 255 | + |
| 256 | +## After migration checklist |
| 257 | + |
| 258 | +1. ✅ Remove the migrated function from the PS test file. |
| 259 | +2. ✅ If a PS test is already covered by an existing Apex test (duplicate), just delete the PS |
| 260 | + test — no new Apex test needed. |
| 261 | +3. ✅ Build the Apex project or run `get_errors` to verify it compiles cleanly. |
| 262 | +4. ✅ Verify assertion methods match the project's package management style |
| 263 | + (packages.config vs PackageReference). |
| 264 | + |
| 265 | +## Common gotchas |
| 266 | + |
| 267 | +- **Console width**: PMC output assertions are text-sensitive. The Apex infrastructure forces |
| 268 | + console width to 1024 to avoid wrapping issues. |
| 269 | +- **Restore timing**: After install/update operations, the Apex infrastructure handles waiting for |
| 270 | + restore completion. You generally don't need explicit waits. |
| 271 | +- **nuget.org dependency**: PS tests that don't specify `-Source` implicitly use nuget.org. Always |
| 272 | + replace this with local package creation via `CreatePackageInSourceAsync` — tests must not depend |
| 273 | + on external feeds. |
| 274 | +- **`NuGetApexTestService` limitations**: It does NOT expose `ISolutionManager` or VS DTE project |
| 275 | + item inspection. Only `IVsPackageInstaller`, `IVsSolutionRestoreStatusProvider`, |
| 276 | + `IVsPackageUninstaller`, `IVsPathContextProvider2`, and `IVsUIShell` are available. |
| 277 | +- **IVs error-path tests**: `NuGetApexTestService.InstallPackage()` swallows |
| 278 | + `InvalidOperationException` and logs it — it does NOT rethrow. For error-path IVs tests, assert |
| 279 | + that the package was NOT installed (`AssertPackageNotInPackagesConfig`) rather than trying to |
| 280 | + catch exceptions. |
| 281 | +- **Feature renaming**: Some PS tests use older feature names (e.g., "PackageNameSpace"). When |
| 282 | + migrating, use the current feature name (e.g., "PackageSourceMapping") in test method names and |
| 283 | + comments. |
| 284 | +- **IVs tests use `EnvDTE.Project`**: IVs API methods like `InstallPackage()` take |
| 285 | + `project.UniqueName` (from `EnvDTE.Project`), not a `ProjectTestExtension`. Get it via |
| 286 | + `VisualStudio.Dte.Solution.Projects.Item(1)`. |
| 287 | +- **`_pathContext` vs `testContext`**: `IVsServicesTestCase` uses a class-level |
| 288 | + `SimpleTestPathContext _pathContext` (initialized in constructor), not per-test `ApexTestContext`. |
| 289 | + PMC tests in `NuGetConsoleTestCase` use per-test `ApexTestContext`. |
| 290 | + |
0 commit comments