Skip to content

Commit 8a1031c

Browse files
committed
Migrate Package Source Mapping ests
1 parent 5f97df1 commit 8a1031c

5 files changed

Lines changed: 531 additions & 619 deletions

File tree

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
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

Comments
 (0)