diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 7222eef129a..0e4d70cf3f1 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -23,10 +23,8 @@
/tests/Aspire.Hosting.Maui.Tests @jfversluis
/tests/Aspire.Hosting.Testing.Tests @sebastienros
/tests/Aspire.Hosting.Tests @mitchdenny
-/tests/Aspire.Templates.Tests @radical @eerhardt
/tests/Shared @radical @eerhardt
/tests/helix @radical @eerhardt
-/tests/templates.proj @radical @eerhardt
# playground apps
/playground/deployers @mitchdenny
diff --git a/.github/actions/enumerate-tests/action.yml b/.github/actions/enumerate-tests/action.yml
index ed12d1c444e..22ca6cefcc0 100644
--- a/.github/actions/enumerate-tests/action.yml
+++ b/.github/actions/enumerate-tests/action.yml
@@ -5,7 +5,7 @@ inputs:
required: false
type: string
default: ''
- description: 'Additional MSBuild arguments passed to the test matrix generation step (e.g., /p:IncludeTemplateTests=true /p:OnlyDeploymentTests=true)'
+ description: 'Additional MSBuild arguments passed to the test matrix generation step (e.g., /p:IncludeCliE2ETests=true /p:OnlyDeploymentTests=true)'
# Output format: JSON with structure {"include": [{...}, ...]}
# Each entry contains:
@@ -19,7 +19,6 @@ inputs:
# - testSessionTimeout: Timeout for the test session (e.g., '20m')
# - testHangTimeout: Timeout for hung tests (e.g., '10m')
# - requiresNugets: Boolean indicating if NuGet packages are needed
-# - requiresTestSdk: Boolean indicating if test SDK is needed
# - extraTestArgs: Additional test arguments (e.g., '--filter-trait "Partition=P1"')
# - collection: (collection type only) Collection/partition name
# - classname: (class type only) Fully qualified test class name
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 92e53231665..4051ead265b 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -216,13 +216,6 @@ jobs:
Write-Host "NuGet package source '$source' does not exist — skipping copy"
}
- - name: Install sdk for nuget based testing
- if: ${{ fromJson(inputs.properties).requiresTestSdk == true }}
- run: >
- ${{ env.DOTNET_SCRIPT }} build ${{ github.workspace }}/tests/workloads.proj
- /p:SkipPackageCheckForTemplatesTesting=true
- /bl:${{ github.workspace }}/artifacts/log/Debug/InstallSdkForTesting.binlog
-
- name: Install Azure Functions Core Tools
if: runner.os == 'Linux' && (inputs.testShortName == 'Playground' || inputs.testShortName == 'Azure')
run: |
@@ -616,7 +609,6 @@ jobs:
path: |
**/*.binlog
testresults/**
- artifacts/bin/Aspire.Templates.Tests/Debug/net8.0/logs/**
artifacts/log/test-logs/**
- name: Copy CLI E2E recordings for upload
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 4061445999a..c898e4bf755 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -28,7 +28,7 @@ jobs:
- uses: ./.github/actions/enumerate-tests
id: generate_tests_matrix
with:
- buildArgs: '/p:IncludeTemplateTests=true /p:IncludeCliE2ETests=${{ github.event_name == ''pull_request'' }}'
+ buildArgs: '/p:IncludeCliE2ETests=true'
- name: Split matrix by dependency type
id: split_matrix
@@ -387,14 +387,10 @@ jobs:
# 'skipped' can be when a transitive dependency fails and the dependent job gets 'skipped'.
# For example, one of setup_* jobs failing and the dependent test jobs getting 'skipped'.
# Some jobs are optional and can have an empty matrix. In those cases we allow a 'skipped'
- # result. Specifically:
- # - tests_no_nugets_overflow: this overflow bucket is expected to be empty unless the
- # primary no-nugets matrix exceeds the overflow threshold.
- # - tests_requires_nugets_macos: some runs intentionally produce no macOS requires-nugets
- # tests, so an empty matrix and 'skipped' result are expected.
- # - cli_starter_validation_windows: this job only runs for pull requests and is expected to
- # be skipped for other workflow events.
- # All other jobs in this gate are required, and a 'skipped' result is treated as a failure.
+ # result. Any matrix-backed test job is only required when setup_for_tests produced at
+ # least one entry for that bucket. The CLI starter validation job only runs for pull
+ # requests and is expected to be skipped for other workflow events. All other jobs in
+ # this gate are required, and a 'skipped' result is treated as a failure.
if: >-
${{ always() &&
(contains(needs.*.result, 'failure') ||
@@ -403,21 +399,34 @@ jobs:
(needs.extension_tests_win.result == 'skipped' ||
needs.cli_starter_validation_windows.result == 'skipped' ||
needs.typescript_sdk_tests.result == 'skipped' ||
- needs.tests_no_nugets.result == 'skipped' ||
- needs.tests_requires_nugets_linux.result == 'skipped' ||
- needs.tests_requires_nugets_windows.result == 'skipped' ||
- (fromJson(needs.setup_for_tests.outputs.tests_matrix_requires_nugets_macos).include[0] != null &&
- needs.tests_requires_nugets_macos.result == 'skipped') ||
- needs.tests_requires_cli_archive.result == 'skipped' ||
+ (fromJson(needs.setup_for_tests.outputs.tests_matrix_no_nugets).include[0] != null &&
+ needs.tests_no_nugets.result == 'skipped') ||
+ (fromJson(needs.setup_for_tests.outputs.tests_matrix_no_nugets_overflow).include[0] != null &&
+ needs.tests_no_nugets_overflow.result == 'skipped') ||
+ (fromJson(needs.setup_for_tests.outputs.tests_matrix_requires_nugets_linux).include[0] != null &&
+ needs.tests_requires_nugets_linux.result == 'skipped') ||
+ (fromJson(needs.setup_for_tests.outputs.tests_matrix_requires_nugets_windows).include[0] != null &&
+ needs.tests_requires_nugets_windows.result == 'skipped') ||
+ (fromJson(needs.setup_for_tests.outputs.tests_matrix_requires_nugets_macos).include[0] != null &&
+ needs.tests_requires_nugets_macos.result == 'skipped') ||
+ (fromJson(needs.setup_for_tests.outputs.tests_matrix_requires_cli_archive).include[0] != null &&
+ needs.tests_requires_cli_archive.result == 'skipped') ||
needs.polyglot_validation.result == 'skipped')) ||
(github.event_name != 'pull_request' &&
(needs.extension_tests_win.result == 'skipped' ||
needs.typescript_sdk_tests.result == 'skipped' ||
- needs.tests_no_nugets.result == 'skipped' ||
- needs.tests_requires_nugets_linux.result == 'skipped' ||
- needs.tests_requires_nugets_windows.result == 'skipped' ||
- (fromJson(needs.setup_for_tests.outputs.tests_matrix_requires_nugets_macos).include[0] != null &&
- needs.tests_requires_nugets_macos.result == 'skipped') ||
+ (fromJson(needs.setup_for_tests.outputs.tests_matrix_no_nugets).include[0] != null &&
+ needs.tests_no_nugets.result == 'skipped') ||
+ (fromJson(needs.setup_for_tests.outputs.tests_matrix_no_nugets_overflow).include[0] != null &&
+ needs.tests_no_nugets_overflow.result == 'skipped') ||
+ (fromJson(needs.setup_for_tests.outputs.tests_matrix_requires_nugets_linux).include[0] != null &&
+ needs.tests_requires_nugets_linux.result == 'skipped') ||
+ (fromJson(needs.setup_for_tests.outputs.tests_matrix_requires_nugets_windows).include[0] != null &&
+ needs.tests_requires_nugets_windows.result == 'skipped') ||
+ (fromJson(needs.setup_for_tests.outputs.tests_matrix_requires_nugets_macos).include[0] != null &&
+ needs.tests_requires_nugets_macos.result == 'skipped') ||
+ (fromJson(needs.setup_for_tests.outputs.tests_matrix_requires_cli_archive).include[0] != null &&
+ needs.tests_requires_cli_archive.result == 'skipped') ||
needs.polyglot_validation.result == 'skipped'))) }}
run: |
echo "One or more dependent jobs failed."
diff --git a/Aspire.slnx b/Aspire.slnx
index 18fa49a110f..b5237b84342 100644
--- a/Aspire.slnx
+++ b/Aspire.slnx
@@ -416,7 +416,6 @@
-
diff --git a/docs/ci/TestingOnCI.md b/docs/ci/TestingOnCI.md
index 36f8ecf8757..13668d27f3a 100644
--- a/docs/ci/TestingOnCI.md
+++ b/docs/ci/TestingOnCI.md
@@ -58,7 +58,7 @@ This invokes `eng/TestEnumerationRunsheetBuilder/TestEnumerationRunsheetBuilder.
- Writes a `.tests-metadata.json` file to `artifacts/helix/` containing:
- `projectName`, `shortName`, `testProjectPath`
- `supportedOSes` array (e.g., `["windows", "linux", "macos"]`)
- - `properties` object with boolean flags (defined in `eng/testing/CITestsProperties.props`): `requiresNugets`, `requiresTestSdk`, `requiresCliArchive`, `requiresGitHubToken`, `enablePlaywrightInstall`
+ - `properties` object with boolean flags (defined in `eng/testing/CITestsProperties.props`): `requiresNugets`, `requiresCliArchive`, `requiresGitHubToken`, `enablePlaywrightInstall`
- `testSessionTimeout`, `testHangTimeout` values
- `uncollectedTestsSessionTimeout`, `uncollectedTestsHangTimeout` values
- `splitTests` flag
@@ -93,19 +93,18 @@ After all projects build, `eng/AfterSolutionBuild.targets` runs `eng/scripts/bui
{
"tests": [
{
- "name": "Templates-StarterTests",
- "shortname": "Templates-StarterTests",
- "testProjectPath": "tests/Aspire.Templates.Tests/...",
- "supportedOSes": ["windows", "linux", "macos"],
+ "name": "Cli E2E / SmokeTests",
+ "shortname": "SmokeTests",
+ "testProjectPath": "tests/Aspire.Cli.EndToEnd.Tests/Aspire.Cli.EndToEnd.Tests.csproj",
+ "supportedOSes": ["linux"],
"properties": {
"requiresNugets": true,
- "requiresTestSdk": true,
- "requiresCliArchive": false,
+ "requiresCliArchive": true,
"enablePlaywrightInstall": false
},
- "testSessionTimeout": "20m",
- "testHangTimeout": "10m",
- "extraTestArgs": "--filter-class \"...\""
+ "testSessionTimeout": "30m",
+ "testHangTimeout": "15m",
+ "extraTestArgs": "--filter-class \"Aspire.Cli.EndToEnd.Tests.SmokeTests\""
},
{
"name": "Hosting-Docker",
@@ -114,7 +113,6 @@ After all projects build, `eng/AfterSolutionBuild.targets` runs `eng/scripts/bui
"supportedOSes": ["linux"],
"properties": {
"requiresNugets": false,
- "requiresTestSdk": false,
"requiresCliArchive": false,
"enablePlaywrightInstall": false
},
@@ -253,8 +251,6 @@ For tests that need the built Aspire packages (e.g., template tests, end-to-end
```xml
true
-
- true
```
@@ -299,14 +295,13 @@ This flag is tracked in the test metadata and controls whether Playwright browse
## CI Test Property Registry
-All boolean test properties (such as `RequiresNugets`, `RequiresTestSdk`, `RequiresCliArchive`, `EnablePlaywrightInstall`) are defined in a single source of truth:
+All boolean test properties (such as `RequiresNugets`, `RequiresCliArchive`, `EnablePlaywrightInstall`) are defined in a single source of truth:
**`eng/testing/CITestsProperties.props`**
```xml
-
@@ -395,7 +390,7 @@ To run enumeration locally and inspect the generated matrix:
./build.sh -test \
/p:TestRunnerName=TestEnumerationRunsheetBuilder \
/p:TestMatrixOutputPath=artifacts/canonical-test-matrix.json \
- /p:IncludeTemplateTests=true \
+ /p:IncludeCliE2ETests=true \
/p:GenerateCIPartitions=true
```
diff --git a/docs/ci/azdo-public-pipeline.md b/docs/ci/azdo-public-pipeline.md
index ce172f77361..293c7d8839e 100644
--- a/docs/ci/azdo-public-pipeline.md
+++ b/docs/ci/azdo-public-pipeline.md
@@ -69,24 +69,22 @@ Which tests run here is controlled by the `RunOnAzdoCI` property (see [Test Rout
#### Helix Tests (`runHelixTests: true`)
-1. Installs SDKs for testing (`tests/workloads.proj`)
-2. Sends test work items to Helix via `send-to-helix.yml` → `send-to-helix-ci.proj`
-3. Downloads `.trx` result files from Helix after completion
+1. Sends test work items to Helix via `send-to-helix.yml` → `send-to-helix-ci.proj`
+2. Downloads `.trx` result files from Helix after completion
## Helix Test Infrastructure
### How Tests Are Sent to Helix
-The entry point is `tests/helix/send-to-helix-ci.proj`, which defines **four test categories**:
+The entry point is `tests/helix/send-to-helix-ci.proj`, which defines **three test categories**:
| Category | Targets file | Runs on Windows | Runs on Linux | Description |
|---------------------|-------------------------------------------|-----------------|---------------|-------------------------------------------------------------------|
| `basictests` | `send-to-helix-basictests.targets` | ✅ | ✅ | Standard unit/integration tests |
| `endtoendtests` | `send-to-helix-endtoendtests.targets` | ❌ | ✅ | End-to-end scenario tests (needs Docker) |
-| `templatestests` | `send-to-helix-templatestests.targets` | ✅ | ✅ | Template creation/run tests |
| `buildonhelixtests` | `send-to-helix-buildonhelixtests.targets` | ❌ | ✅ | Tests that `dotnet build` + `dotnet test` on Helix (needs Docker) |
-The `send-to-helix-ci.proj` first runs `PrepareDependencies` sequentially, then dispatches all categories in parallel via MSBuild.
+The `send-to-helix-ci.proj` first runs `PrepareDependencies` sequentially, then dispatches all categories in parallel via MSBuild. The removed template coverage is replaced by a directly-run AzDO subset of `Aspire.Cli.EndToEnd.Tests`, not by a Helix category.
Each category is handled by `send-to-helix-inner.proj` (the Helix SDK project), which imports the category-specific `.targets` file.
@@ -109,14 +107,6 @@ Each test category has its own strategy for splitting tests into Helix work item
- Each work item filters tests by `--filter-trait "scenario="`
- Tests run only on Linux (Docker required)
-#### `templatestests`
-
-- **One work item per test class** — test class names are extracted at build time
-- The `ExtractTestClassNames` target runs the test assembly with `--list-tests` to discover classes
-- Class names are written to `.tests.list`
-- Each class becomes a separate Helix work item with `--filter-class `
-- Correlation payloads include multiple SDK versions (`dotnet-8`, `dotnet-9`, `dotnet-10`)
-
#### `buildonhelixtests`
- **One work item per test project zip** — similar to `basictests`
@@ -135,7 +125,7 @@ Each test category has its own strategy for splitting tests into Helix work item
Each work item follows this lifecycle:
-1. **Pre-commands**: Clean up stale processes (dotnet-tests, dcp.exe), start Docker cleanup, set environment variables (DCP paths, SDK paths, dev certs, Docker BuildKit)
+1. **Pre-commands**: Clean up stale `dcp.exe` processes, start Docker cleanup, set environment variables (DCP paths, dev certs, Docker BuildKit)
2. **Command**: Run the test executable with MTP arguments, blame/crash dump collection, quarantine exclusion
3. **Post-commands**: List Docker state, rename `.trx` files for collection
@@ -146,7 +136,6 @@ These are shared across all work items in a Helix job:
- **DCP binary** — the orchestrator binary, set via `DcpPublisher__CliPath`
- **Dev cert scripts** — for HTTPS dev certificate setup on Linux
- **Docker CLI** — specific version installed on the agent
-- **SDKs for testing** — `dotnet-tests` directory with a configured .NET SDK
- **Built NuGet packages** — `artifacts/packages/Shipping/` for template tests
- **Playwright browser dependencies** — for UI tests
- **Azure Functions CLI** — for Functions integration tests
@@ -187,7 +176,6 @@ This is also the easiest way to inspect locally what payload Helix agents will r
2. **Archive directories** (defined in `tests/Directory.Build.props`):
- `artifacts/helix/tests/` — basic tests
- `artifacts/helix/e2e-tests/` — end-to-end tests
- - `artifacts/helix/templates-tests/` — template tests
- `artifacts/helix/build-on-helix-tests/` — build-on-helix tests
- `artifacts/helix/cli-e2e-tests/` — CLI E2E tests
- `artifacts/helix/deployment-e2e-tests/` — deployment E2E tests
@@ -198,11 +186,6 @@ This is also the easiest way to inspect locally what payload Helix agents will r
- `nuget.config` — configured to resolve built packages from artifacts
- Shared test utilities
-4. **Test class extraction** (for `templatestests`):
- - The `ExtractTestClassNames` target runs the test executable with `--list-tests`
- - Extracts unique class names matching a prefix regex
- - Writes them to `.tests.list` alongside the zip
-
## Helix xUnit Configuration
When `PrepareForHelix=true`, a special `xunit.runner.json` is used (`tests/helix/xunit.runner.json`):
@@ -275,7 +258,6 @@ Since AzDO tests don't run on PRs, changes can silently break the pipeline. The
**Real incidents**:
- `49b1fd3b`: Template tests ran `dotnet test --list-tests` which invoked the system dotnet (6.0) instead of the repo's dotnet (8.0+) → "You must install or update .NET" error. A prior PR removed `DOTNET_ROOT` environment variable override for GitHub Actions, breaking AzDO.
-- `258d2e95`: `dotnet-tests` SDK directory wasn't properly prepared for template and helix test runs
**What to watch for**: Changes to `DOTNET_ROOT`, `PATH`, or SDK version settings in `BuildAndTest.yml`, `tests/Directory.Build.targets`, or helix targets. If a change works by relying on the system dotnet or GitHub Actions' pre-installed SDK, it will likely break AzDO/Helix.
@@ -362,7 +344,6 @@ When reviewing PRs, flag these for manual AzDO validation (`/azp run aspire-test
- [ ] Tests using `AddProject()` in projects that run on Helix
- [ ] Changes to `AspireProjectOrPackageReference` items
- [ ] New or modified Verify snapshot tests
-- [ ] Changes to `tests/workloads.proj` or SDK setup
## How to Manually Trigger AzDO Tests
@@ -387,7 +368,6 @@ This comment in a PR will trigger the `aspire-tests` pipeline, which runs both p
| `tests/helix/send-to-helix-inner.proj` | Helix SDK project (work item builder) |
| `tests/helix/send-to-helix-basictests.targets` | Basic test work items |
| `tests/helix/send-to-helix-endtoendtests.targets` | E2E test work items (by scenario) |
-| `tests/helix/send-to-helix-templatestests.targets` | Template test work items (by class) |
| `tests/helix/send-to-helix-buildonhelixtests.targets` | Build-on-Helix test work items |
| `eng/Testing.props` | Default test runner properties |
| `eng/Testing.targets` | Test skip/run logic per runner context |
diff --git a/dogfood.sh b/dogfood.sh
index 79a1a325038..7d05991bed4 100755
--- a/dogfood.sh
+++ b/dogfood.sh
@@ -16,9 +16,10 @@ else
fi
REPO_ROOT=$(cd "${scriptroot}";pwd)
-SDK_PATH=$REPO_ROOT/artifacts/bin/dotnet-tests
+SDK_PATH=$REPO_ROOT/.dotnet
if [ ! -x "$SDK_PATH/dotnet" ]; then
echo "Error: Could not find dotnet at $SDK_PATH/dotnet"
+ echo "Run ./restore.sh first to install the repo-local SDK."
return
fi
diff --git a/eng/TestEnumerationRunsheetBuilder/TestEnumerationRunsheetBuilder.targets b/eng/TestEnumerationRunsheetBuilder/TestEnumerationRunsheetBuilder.targets
index 187ae06229f..8eff319d550 100644
--- a/eng/TestEnumerationRunsheetBuilder/TestEnumerationRunsheetBuilder.targets
+++ b/eng/TestEnumerationRunsheetBuilder/TestEnumerationRunsheetBuilder.targets
@@ -27,7 +27,6 @@
<_ShouldSkipProject Condition="'$(_ShouldSkipProject)' == '' and $(_NormalizedProjectDirectory.Contains('tests/testproject'))">true
<_ShouldSkipProject Condition="'$(_ShouldSkipProject)' == '' and $(_NormalizedProjectDirectory.Contains('tests/TestingAppHost1'))">true
- <_ShouldSkipProject Condition="'$(_ShouldSkipProject)' == '' and '$(IncludeTemplateTests)' != 'true' and '$(MSBuildProjectName)' == 'Aspire.Templates.Tests'">true
<_ShouldSkipProject Condition="'$(_ShouldSkipProject)' == '' and '$(IncludeCliE2ETests)' != 'true' and '$(MSBuildProjectName)' == 'Aspire.Cli.EndToEnd.Tests'">true
diff --git a/eng/Versions.props b/eng/Versions.props
index 99087265486..1c521532234 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -8,13 +8,11 @@
preview.1
net8.0
$(DefaultTargetFramework);net9.0;net10.0
-
+
8.0.21
9.0.10
-
- 8.0.415
-
- 9.0.306
+
+ 8.0.415
3.2.2
1.21.0
3.1.5
diff --git a/eng/pipelines/azure-pipelines-unofficial.yml b/eng/pipelines/azure-pipelines-unofficial.yml
index dd92bf3940e..7662f19fd97 100644
--- a/eng/pipelines/azure-pipelines-unofficial.yml
+++ b/eng/pipelines/azure-pipelines-unofficial.yml
@@ -236,6 +236,9 @@ extends:
repoLogPath: $(Build.Arcade.LogsPath)
repoTestResultsPath: $(Build.Arcade.TestResultsPath)
isWindows: true
+ targetRids:
+ - win-x64
+ - win-arm64
- pwsh: |
$ErrorActionPreference = 'Stop'
$shippingDir = "$(Build.SourcesDirectory)/artifacts/packages/$(_BuildConfig)/Shipping"
diff --git a/eng/pipelines/azure-pipelines.yml b/eng/pipelines/azure-pipelines.yml
index 848f70589dc..6833429ce53 100644
--- a/eng/pipelines/azure-pipelines.yml
+++ b/eng/pipelines/azure-pipelines.yml
@@ -327,6 +327,9 @@ extends:
publishVSCodeExtension: ${{ parameters.publishVSCodeExtension }}
vscePublishToken: $(VscePublishToken)
vscePublishPreRelease: ${{ parameters.vscePublishPreRelease }}
+ targetRids:
+ - win-x64
+ - win-arm64
# Extract the Aspire version from a generated nupkg filename so downstream
# stages can use it. Aspire.Hosting.AppHost has no RID in its filename and
diff --git a/eng/pipelines/templates/BuildAndTest.yml b/eng/pipelines/templates/BuildAndTest.yml
index 11cbde39bbf..8051882b9c3 100644
--- a/eng/pipelines/templates/BuildAndTest.yml
+++ b/eng/pipelines/templates/BuildAndTest.yml
@@ -34,6 +34,9 @@ parameters:
- name: vscePublishPreRelease
type: boolean
default: false
+ - name: targetRids
+ type: object
+ default: []
steps:
# Internal pipeline: Build with pack+sign
@@ -55,6 +58,27 @@ steps:
/p:BuildExtension=true
displayName: 🟣Build
+ # Verify the signed win-x64 CLI archive works (version check + starter/AppHost template smokes)
+ - pwsh: |
+ $ErrorActionPreference = 'Stop'
+ $archiveDir = "${{ parameters.repoArtifactsPath }}/signed-archives/${{ parameters.buildConfig }}"
+ $archive = Get-ChildItem -Path $archiveDir -Filter "aspire-cli-win-x64-*.zip" -Recurse | Select-Object -First 1
+ if (-not $archive) {
+ Write-Error "No win-x64 CLI archive found under '$archiveDir'"
+ exit 1
+ }
+ Write-Host "Found archive: $($archive.FullName)"
+ & "$(Build.SourcesDirectory)/eng/scripts/verify-cli-archive.ps1" -ArchivePath $archive.FullName
+ if ($LASTEXITCODE -ne 0) {
+ Write-Host "##[error]CLI archive verification failed"
+ exit 1
+ }
+ displayName: 🟣Verify CLI archive and template smokes (win-x64)
+ condition: succeeded()
+ env:
+ ASPIRE_CLI_TELEMETRY_OPTOUT: 'true'
+ DOTNET_CLI_TELEMETRY_OPTOUT: 'true'
+
# Log MicroBuild environment for debugging
# MicroBuildOutputFolderOverride is set by the MicroBuildSigningPlugin task in eng/common/templates-official/job/onelocbuild.yml
# which is installed via the Arcade SDK's install-microbuild.yml template that runs before our build steps.
@@ -192,29 +216,61 @@ steps:
PathtoPublish: '${{ parameters.repoArtifactsPath }}/packages/Release/vscode'
ArtifactName: aspire-vscode-extension
- - script: ${{ parameters.dotnetScript }}
- build
- tests/workloads.proj
- /p:SkipPackageCheckForTemplatesTesting=true
- displayName: 🟣Prepare sdks for templates testing
+ # Run a small, intentionally marked CLI E2E template-coverage subset directly on AzDO.
+ - ${{ if ne(parameters.isWindows, 'true') }}:
+ - script: |
+ set -euo pipefail
- - script: ${{ parameters.buildScript }}
- -build
- -restore
- -test
- -configuration ${{ parameters.buildConfig }}
- /bl:${{ parameters.repoLogPath }}/BuildTemplatesTests.binlog
- $(_OfficialBuildIdArgs)
- $(_InternalBuildArgs)
- /p:SkipTests=false
- -projects $(Build.SourcesDirectory)\tests\Aspire.Templates.Tests\Aspire.Templates.Tests.csproj
- env:
- RunOnlyBasicBuildTemplateTests: true
- # test root path for template test projects
- DEV_TEMP: $(Build.SourcesDirectory)\..
- DOTNET_ROOT: $(Build.SourcesDirectory)\.dotnet
- TEST_LOG_PATH: $(Build.SourcesDirectory)\artifacts\log\$(_BuildConfig)\Aspire.Templates.Tests
- displayName: 🟣Run Template tests
+ archive_dir="$(Build.StagingDirectory)/cli-e2e-archives"
+ mkdir -p "$archive_dir"
+
+ cp "${{ parameters.repoArtifactsPath }}/signed-archives/${{ parameters.buildConfig }}/aspire-cli-linux-x64-"*.tar.gz "$archive_dir/"
+ cp "${{ parameters.repoArtifactsPath }}/packages/${{ parameters.buildConfig }}/Shipping/"*.nupkg "$archive_dir/"
+
+ ASPIRE_E2E_CLI_ARCHIVE_DIR="$archive_dir" \
+ ${{ parameters.dotnetScript }} test \
+ --project "$(Build.SourcesDirectory)/tests/Aspire.Cli.EndToEnd.Tests/Aspire.Cli.EndToEnd.Tests.csproj" \
+ --no-launch-profile \
+ -- \
+ --filter-trait "azdo-template-coverage=true" \
+ --filter-not-trait "quarantined=true" \
+ --filter-not-trait "outerloop=true"
+ env:
+ DOCKER_BUILDKIT: 1
+ DOTNET_ROOT: $(Build.SourcesDirectory)/.dotnet
+ displayName: 🟣Run CLI E2E template coverage subset
+
+ # Publish Windows CLI archives as separate artifacts so the Prepare Installers
+ # stage can download them by name (matching the native_archives_ pattern
+ # used by build_sign_native for macOS/Linux).
+ # win-arm64 archives are staged for downstream consumers; runtime verification is
+ # intentionally skipped because these win-x64 build agents cannot execute them.
+ - ${{ if eq(parameters.isWindows, 'true') }}:
+ - ${{ each targetRid in parameters.targetRids }}:
+ - pwsh: |
+ $ErrorActionPreference = 'Stop'
+ $sourceDir = '${{ parameters.repoArtifactsPath }}/signed-archives/${{ parameters.buildConfig }}'
+ $pattern = 'aspire-cli-${{ targetRid }}-*'
+ $stagingDir = '$(Build.StagingDirectory)/native_archives_${{ replace(targetRid, '-', '_') }}'
+
+ New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null
+
+ $files = Get-ChildItem -Path $sourceDir -Recurse -Filter $pattern
+ if ($files.Count -eq 0) {
+ Write-Error "No CLI archives matching '$pattern' found under '$sourceDir'"
+ exit 1
+ }
+
+ foreach ($f in $files) {
+ Copy-Item $f.FullName $stagingDir
+ Write-Host "Staged: $($f.Name)"
+ }
+ displayName: 🟣Stage CLI archives (${{ targetRid }})
+ - task: 1ES.PublishBuildArtifacts@1
+ displayName: 🟣Publish native_archives_${{ replace(targetRid, '-', '_') }}
+ inputs:
+ PathtoPublish: $(Build.StagingDirectory)/native_archives_${{ replace(targetRid, '-', '_') }}
+ ArtifactName: native_archives_${{ replace(targetRid, '-', '_') }}
# Public pipeline - helix tests
- ${{ if eq(parameters.runAsPublic, 'true') }}:
@@ -279,13 +335,6 @@ steps:
# Helix tests are run only on the public pipeline
- ${{ if and(eq(parameters.runAsPublic, 'true'), eq(parameters.runHelixTests, 'true')) }}:
- - script: ${{ parameters.buildScript }}
- /p:Configuration=${{ parameters.buildConfig }}
- $(_OfficialBuildIdArgs)
- /bl:${{ parameters.repoLogPath }}/InstallSdksForTesting.binlog
- -projects $(Build.SourcesDirectory)/tests/workloads.proj
- displayName: Install sdk for testing
-
# Helix captures code coverage information and, once tests are complete, the code coverage information is
# downloaded to /artifacts/helixresults folder.
- template: /eng/pipelines/templates/send-to-helix.yml
diff --git a/eng/scripts/verify-cli-archive.ps1 b/eng/scripts/verify-cli-archive.ps1
index 98faaa73c18..6ddb7a83d69 100644
--- a/eng/scripts/verify-cli-archive.ps1
+++ b/eng/scripts/verify-cli-archive.ps1
@@ -7,8 +7,13 @@
1. Cleans ~/.aspire to ensure no stale state
2. Extracts the CLI archive to a temp location
3. Runs 'aspire --version' to validate the binary executes
- 4. Runs 'aspire new aspire-starter' to test bundle self-extraction + project creation
- 5. Cleans up temp directories
+ 4. Runs 'aspire new aspire-starter' to test bundle self-extraction + starter project creation
+ 5. Builds the generated starter AppHost project
+ 6. Enables hidden templates using a temp local config file and runs 'aspire new aspire-apphost' to validate empty AppHost creation
+ 7. Builds the generated empty AppHost project
+ 8. Builds representative template project-file scenarios that replaced Aspire.Templates.Tests coverage
+ 9. Builds the single-file AppHost template
+ 10. Cleans up temp directories
.PARAMETER ArchivePath
Path to the CLI archive (.zip or .tar.gz)
@@ -55,9 +60,126 @@ function Set-ExecutablePermission([string]$Path) {
}
}
+function Save-XmlDocument([xml]$Document, [string]$Path) {
+ $settings = [System.Xml.XmlWriterSettings]::new()
+ $settings.Indent = $true
+ $settings.OmitXmlDeclaration = $true
+
+ $writer = [System.Xml.XmlWriter]::Create($Path, $settings)
+ try {
+ $Document.Save($writer)
+ }
+ finally {
+ $writer.Dispose()
+ }
+}
+
+function Get-AppHostSdkVersion([string]$ProjectPath) {
+ [xml]$document = Get-Content -Raw $ProjectPath
+ $sdkValue = $document.Project.Sdk
+ if (-not $sdkValue) {
+ throw "Sdk attribute not found in '$ProjectPath'."
+ }
+
+ $prefix = "Aspire.AppHost.Sdk/"
+ if (-not $sdkValue.StartsWith($prefix, [System.StringComparison]::Ordinal)) {
+ throw "Unexpected SDK value '$sdkValue' in '$ProjectPath'."
+ }
+
+ return $sdkValue.Substring($prefix.Length)
+}
+
+function Add-PackageReference([string]$ProjectPath, [string]$PackageName, [string]$Version = $null) {
+ [xml]$document = Get-Content -Raw $ProjectPath
+ $project = $document.Project
+
+ $itemGroup = $document.CreateElement("ItemGroup")
+ $packageReference = $document.CreateElement("PackageReference")
+ $packageReference.SetAttribute("Include", $PackageName)
+ if ($Version) {
+ $packageReference.SetAttribute("Version", $Version)
+ }
+
+ $itemGroup.AppendChild($packageReference) | Out-Null
+ $project.AppendChild($itemGroup) | Out-Null
+ Save-XmlDocument $document $ProjectPath
+}
+
+function Rewrite-AsExplicitSdkReference([string]$ProjectPath, [bool]$IncludeAspireHostingAppHostPackageReference) {
+ [xml]$document = Get-Content -Raw $ProjectPath
+ $project = $document.Project
+ $version = Get-AppHostSdkVersion $ProjectPath
+
+ $project.SetAttribute("Sdk", "Microsoft.NET.Sdk")
+
+ $sdkElement = $document.CreateElement("Sdk")
+ $sdkElement.SetAttribute("Name", "Aspire.AppHost.Sdk")
+ $sdkElement.SetAttribute("Version", $version)
+
+ if ($project.FirstChild) {
+ $project.InsertBefore($sdkElement, $project.FirstChild) | Out-Null
+ }
+ else {
+ $project.AppendChild($sdkElement) | Out-Null
+ }
+
+ Save-XmlDocument $document $ProjectPath
+
+ if ($IncludeAspireHostingAppHostPackageReference) {
+ Add-PackageReference $ProjectPath "Aspire.Hosting.AppHost" $version
+ }
+}
+
+function Disable-PackageSourceMapping([string]$NuGetConfigPath) {
+ if (-not (Test-Path $NuGetConfigPath)) {
+ return
+ }
+
+ [xml]$document = Get-Content -Raw $NuGetConfigPath
+ $node = $document.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='packageSourceMapping']")
+ if ($node) {
+ $node.ParentNode.RemoveChild($node) | Out-Null
+ Save-XmlDocument $document $NuGetConfigPath
+ }
+}
+
+function Add-CentralPackageManagementForRedis([string]$ProjectPath, [string]$DirectoryPackagesPropsPath) {
+ $version = Get-AppHostSdkVersion $ProjectPath
+ Add-PackageReference $ProjectPath "Aspire.Hosting.Redis"
+
+ Set-Content -Path $DirectoryPackagesPropsPath -Value @"
+
+
+ true
+ NU1507;`$(NoWarn)
+
+
+
+
+
+"@
+}
+
$userHome = Get-UserHome
$verifyTmpDir = $null
$aspireBackup = $null
+$dotnetCmd = $null
+
+function Get-DotNetCommand {
+ $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
+ $repoDotNet = if ($IsWindows) {
+ Join-Path $repoRoot "dotnet.cmd"
+ }
+ else {
+ Join-Path $repoRoot "dotnet.sh"
+ }
+
+ if (Test-Path $repoDotNet) {
+ return $repoDotNet
+ }
+
+ return (Get-Command dotnet -ErrorAction Stop).Source
+}
function Invoke-Cleanup {
if ($verifyTmpDir -and (Test-Path $verifyTmpDir)) {
@@ -89,12 +211,14 @@ try {
$env:DOTNET_CLI_TELEMETRY_OPTOUT = "true"
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = "true"
$env:DOTNET_GENERATE_ASPNET_CERTIFICATE = "false"
+ $dotnetCmd = Get-DotNetCommand
Write-Host ""
Write-Host "=========================================="
Write-Host " Aspire CLI Archive Verification"
Write-Host "=========================================="
Write-Host " Archive: $ArchivePath"
+ Write-Host " dotnet: $dotnetCmd"
Write-Host "=========================================="
Write-Host ""
@@ -163,7 +287,7 @@ try {
Write-Host " Version: $versionOutput"
Write-Ok "'aspire --version' succeeded"
- # Step 4: Create a new project with aspire new
+ # Step 4: Create a new starter project with aspire new
# This exercises bundle self-extraction and aspire-managed (template search + download + scaffolding)
$projectDir = Join-Path $verifyTmpDir "VerifyApp"
New-Item -ItemType Directory -Path $projectDir -Force | Out-Null
@@ -184,6 +308,201 @@ try {
}
Write-Ok "'aspire new' created project successfully"
+ # Step 5: Build the generated starter AppHost project
+ $starterAppHostProject = Join-Path $appHostDir "VerifyApp.AppHost.csproj"
+ if (-not (Test-Path $starterAppHostProject)) {
+ Write-Err "Expected AppHost project '$starterAppHostProject' not found after 'aspire new aspire-starter'"
+ exit 1
+ }
+
+ Write-Step "Running 'dotnet build $starterAppHostProject'..."
+ & $dotnetCmd build $starterAppHostProject 2>&1 | Write-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "'dotnet build' failed for starter AppHost with exit code $LASTEXITCODE"
+ exit 1
+ }
+ Write-Ok "Starter AppHost build succeeded"
+
+ # Step 6: Enable hidden templates in a temp local config file so global developer config is untouched
+ $hiddenTemplateConfigDir = Join-Path $verifyTmpDir "hidden-template-config"
+ New-Item -ItemType Directory -Path $hiddenTemplateConfigDir -Force | Out-Null
+
+ Write-Step "Enabling hidden Aspire templates using temp local config..."
+ Push-Location $hiddenTemplateConfigDir
+ try {
+ & $aspireBin config set features:showAllTemplates true --non-interactive 2>&1 | Write-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "'aspire config set features:showAllTemplates' failed with exit code $LASTEXITCODE"
+ exit 1
+ }
+ }
+ finally {
+ Pop-Location
+ }
+ Write-Ok "Hidden Aspire templates enabled in temp local config"
+
+ # Step 7: Create an empty .NET AppHost project
+ $appHostTemplateDir = Join-Path $verifyTmpDir "VerifyAppHost"
+ New-Item -ItemType Directory -Path $appHostTemplateDir -Force | Out-Null
+
+ Push-Location $hiddenTemplateConfigDir
+ try {
+ Write-Step "Running 'aspire new aspire-apphost --name VerifyAppHost --output $appHostTemplateDir --non-interactive --nologo'..."
+ & $aspireBin new aspire-apphost --name VerifyAppHost --output $appHostTemplateDir --non-interactive --nologo 2>&1 | Write-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "'aspire new aspire-apphost' failed with exit code $LASTEXITCODE"
+ exit 1
+ }
+ }
+ finally {
+ Pop-Location
+ }
+
+ $emptyAppHostProject = Join-Path $appHostTemplateDir "VerifyAppHost.csproj"
+ if (-not (Test-Path $emptyAppHostProject)) {
+ Write-Err "Expected AppHost project '$emptyAppHostProject' not found after 'aspire new aspire-apphost'"
+ Get-ChildItem $appHostTemplateDir | Format-Table
+ exit 1
+ }
+ Write-Ok "'aspire new aspire-apphost' created project successfully"
+
+ # Step 8: Build the generated empty AppHost project
+ Write-Step "Running 'dotnet build $emptyAppHostProject'..."
+ & $dotnetCmd build $emptyAppHostProject 2>&1 | Write-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "'dotnet build' failed for empty AppHost with exit code $LASTEXITCODE"
+ exit 1
+ }
+ Write-Ok "Empty AppHost build succeeded"
+
+ # Step 9: Verify explicit SDK-reference AppHost project behavior on Windows
+ $explicitSdkDir = Join-Path $verifyTmpDir "VerifyExplicitSdkApp"
+ New-Item -ItemType Directory -Path $explicitSdkDir -Force | Out-Null
+
+ Push-Location $hiddenTemplateConfigDir
+ try {
+ Write-Step "Running 'aspire new aspire --name VerifyExplicitSdkApp --output $explicitSdkDir --non-interactive --nologo'..."
+ & $aspireBin new aspire --name VerifyExplicitSdkApp --output $explicitSdkDir --non-interactive --nologo 2>&1 | Write-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "'aspire new aspire' failed for explicit SDK scenario with exit code $LASTEXITCODE"
+ exit 1
+ }
+ }
+ finally {
+ Pop-Location
+ }
+
+ $explicitSdkAppHostProject = Join-Path $explicitSdkDir "VerifyExplicitSdkApp.AppHost/VerifyExplicitSdkApp.AppHost.csproj"
+ if (-not (Test-Path $explicitSdkAppHostProject)) {
+ Write-Err "Expected AppHost project '$explicitSdkAppHostProject' not found for explicit SDK scenario"
+ exit 1
+ }
+
+ Rewrite-AsExplicitSdkReference $explicitSdkAppHostProject $true
+ Write-Step "Running 'dotnet build $explicitSdkAppHostProject'..."
+ & $dotnetCmd build $explicitSdkAppHostProject 2>&1 | Write-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "'dotnet build' failed for explicit SDK AppHost with exit code $LASTEXITCODE"
+ exit 1
+ }
+ Write-Ok "Explicit SDK AppHost build succeeded"
+
+ # Step 10: Verify central package management AppHost project behavior on Windows
+ $cpmDir = Join-Path $verifyTmpDir "VerifyCpmApp"
+ New-Item -ItemType Directory -Path $cpmDir -Force | Out-Null
+
+ Push-Location $hiddenTemplateConfigDir
+ try {
+ Write-Step "Running 'aspire new aspire --name VerifyCpmApp --output $cpmDir --non-interactive --nologo'..."
+ & $aspireBin new aspire --name VerifyCpmApp --output $cpmDir --non-interactive --nologo 2>&1 | Write-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "'aspire new aspire' failed for CPM scenario with exit code $LASTEXITCODE"
+ exit 1
+ }
+ }
+ finally {
+ Pop-Location
+ }
+
+ $cpmAppHostProject = Join-Path $cpmDir "VerifyCpmApp.AppHost/VerifyCpmApp.AppHost.csproj"
+ if (-not (Test-Path $cpmAppHostProject)) {
+ Write-Err "Expected AppHost project '$cpmAppHostProject' not found for CPM scenario"
+ exit 1
+ }
+
+ Add-CentralPackageManagementForRedis $cpmAppHostProject (Join-Path $cpmDir "Directory.Packages.props")
+ Write-Step "Running 'dotnet build $cpmAppHostProject'..."
+ & $dotnetCmd build $cpmAppHostProject 2>&1 | Write-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "'dotnet build' failed for CPM AppHost with exit code $LASTEXITCODE"
+ exit 1
+ }
+ Write-Ok "Central package management AppHost build succeeded"
+
+ # Step 11: Verify AppHost package version override behavior on Windows
+ $versionedAppHostDir = Join-Path $verifyTmpDir "VerifyVersionedAppHost"
+ New-Item -ItemType Directory -Path $versionedAppHostDir -Force | Out-Null
+
+ Push-Location $hiddenTemplateConfigDir
+ try {
+ Write-Step "Running 'aspire new aspire-apphost --name VerifyVersionedAppHost --output $versionedAppHostDir --non-interactive --nologo'..."
+ & $aspireBin new aspire-apphost --name VerifyVersionedAppHost --output $versionedAppHostDir --non-interactive --nologo 2>&1 | Write-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "'aspire new aspire-apphost' failed for version override scenario with exit code $LASTEXITCODE"
+ exit 1
+ }
+ }
+ finally {
+ Pop-Location
+ }
+
+ $versionedAppHostProject = Join-Path $versionedAppHostDir "VerifyVersionedAppHost.csproj"
+ if (-not (Test-Path $versionedAppHostProject)) {
+ Write-Err "Expected AppHost project '$versionedAppHostProject' not found for version override scenario"
+ exit 1
+ }
+
+ Disable-PackageSourceMapping (Join-Path $versionedAppHostDir "nuget.config")
+ Add-PackageReference $versionedAppHostProject "Aspire.Hosting.AppHost" "8.1.0"
+ Write-Step "Running 'dotnet build $versionedAppHostProject'..."
+ & $dotnetCmd build $versionedAppHostProject 2>&1 | Write-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "'dotnet build' failed for version override AppHost with exit code $LASTEXITCODE"
+ exit 1
+ }
+ Write-Ok "Version override AppHost build succeeded"
+
+ # Step 12: Verify single-file AppHost template behavior on Windows
+ $singleFileAppHostDir = Join-Path $verifyTmpDir "VerifySingleFileAppHost"
+ New-Item -ItemType Directory -Path $singleFileAppHostDir -Force | Out-Null
+
+ Write-Step "Running 'dotnet new aspire-apphost-singlefile --name VerifySingleFileAppHost --output $singleFileAppHostDir'..."
+ & $dotnetCmd new aspire-apphost-singlefile --name VerifySingleFileAppHost --output $singleFileAppHostDir 2>&1 | Write-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "'dotnet new aspire-apphost-singlefile' failed with exit code $LASTEXITCODE"
+ exit 1
+ }
+
+ $singleFileAppHostPath = Join-Path $singleFileAppHostDir "apphost.cs"
+ if (-not (Test-Path $singleFileAppHostPath)) {
+ Write-Err "Expected single-file AppHost '$singleFileAppHostPath' not found"
+ exit 1
+ }
+
+ Push-Location $singleFileAppHostDir
+ try {
+ Write-Step "Running 'dotnet build apphost.cs'..."
+ & $dotnetCmd build apphost.cs 2>&1 | Write-Host
+ if ($LASTEXITCODE -ne 0) {
+ Write-Err "'dotnet build' failed for single-file AppHost with exit code $LASTEXITCODE"
+ exit 1
+ }
+ }
+ finally {
+ Pop-Location
+ }
+ Write-Ok "Single-file AppHost build succeeded"
+
Write-Host ""
Write-Host "=========================================="
Write-Host " All verification checks passed!" -ForegroundColor Green
diff --git a/eng/testing/CITestsProperties.props b/eng/testing/CITestsProperties.props
index b64698c5158..7bb53a8dcfa 100644
--- a/eng/testing/CITestsProperties.props
+++ b/eng/testing/CITestsProperties.props
@@ -15,7 +15,6 @@
-->
-
diff --git a/playground/AspireWithMaui/AspireWithMaui.slnx b/playground/AspireWithMaui/AspireWithMaui.slnx
index 8452badc2f2..e6da2e4e0b4 100644
--- a/playground/AspireWithMaui/AspireWithMaui.slnx
+++ b/playground/AspireWithMaui/AspireWithMaui.slnx
@@ -363,7 +363,6 @@
-
diff --git a/src/Aspire.ProjectTemplates/README.md b/src/Aspire.ProjectTemplates/README.md
index c8e1b381470..e32ebbb7133 100644
--- a/src/Aspire.ProjectTemplates/README.md
+++ b/src/Aspire.ProjectTemplates/README.md
@@ -24,4 +24,4 @@ dotnet pack ./src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj
### Updating tests
-Template tests can be run using the standard test commands. You can follow the directions in *[repo_root]/tests/Aspire.Templates.Tests/README.md* to run them locally if desired, or simply send a PR and observe the test output there.
+Template end-to-end coverage lives in *[repo_root]/tests/Aspire.Cli.EndToEnd.Tests/*. For local runs, follow the directions in *[repo_root]/tests/Aspire.Cli.EndToEnd.Tests/README.md*.
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Aspire.Cli.EndToEnd.Tests.csproj b/tests/Aspire.Cli.EndToEnd.Tests/Aspire.Cli.EndToEnd.Tests.csproj
index 0edc29f9342..58e851de8aa 100644
--- a/tests/Aspire.Cli.EndToEnd.Tests/Aspire.Cli.EndToEnd.Tests.csproj
+++ b/tests/Aspire.Cli.EndToEnd.Tests/Aspire.Cli.EndToEnd.Tests.csproj
@@ -8,8 +8,6 @@
true
true
Exe
- true
-
true
true
true
@@ -42,8 +40,9 @@
$(TestArchiveTestsDirForCliEndToEndTests)
- 30m
- 15m
+ 45m
+ 30m
+ $(_MtpDiagnosticFlags) --hangdump-timeout $(TestHangTimeout) --timeout $(TestSessionTimeout)
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/BundleSmokeTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/BundleSmokeTests.cs
deleted file mode 100644
index 5a72dd22291..00000000000
--- a/tests/Aspire.Cli.EndToEnd.Tests/BundleSmokeTests.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.Cli.EndToEnd.Tests.Helpers;
-using Aspire.Cli.Tests.Utils;
-using Hex1b.Automation;
-using Xunit;
-
-namespace Aspire.Cli.EndToEnd.Tests;
-
-///
-/// End-to-end tests for running .NET csproj AppHost projects using the Aspire bundle.
-/// Validates that the bundle correctly provides DCP and Dashboard paths to the hosting
-/// infrastructure when running SDK-based app hosts (not just polyglot/guest app hosts).
-///
-public sealed class BundleSmokeTests(ITestOutputHelper output)
-{
- [CaptureWorkspaceOnFailure]
- [Fact]
- public async Task CreateAndRunAspireStarterProjectWithBundle()
- {
- var repoRoot = CliE2ETestHelpers.GetRepoRoot();
- var strategy = CliInstallStrategy.Detect(output.WriteLine);
-
- var workspace = TemporaryWorkspace.Create(output);
-
- using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
-
- var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
-
- var counter = new SequenceCounter();
- var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
-
- await auto.PrepareDockerEnvironmentAsync(counter, workspace);
- await auto.InstallAspireCliAsync(strategy, counter);
-
- await auto.AspireNewAsync("BundleStarterApp", counter);
-
- await auto.AspireStartAsync(counter);
- await auto.AspireStopAsync(counter);
-
- await auto.TypeAsync("exit");
- await auto.EnterAsync();
-
- await pendingRun;
- }
-}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/CSharpInitTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/CSharpInitTests.cs
index 702ef4c1fe2..d64ea83356b 100644
--- a/tests/Aspire.Cli.EndToEnd.Tests/CSharpInitTests.cs
+++ b/tests/Aspire.Cli.EndToEnd.Tests/CSharpInitTests.cs
@@ -26,7 +26,7 @@ public sealed class CSharpInitTests(ITestOutputHelper output)
public async Task InteractiveCSharpInitCreatesExpectedFiles()
{
var repoRoot = CliE2ETestHelpers.GetRepoRoot();
- var strategy = CliInstallStrategy.Detect();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
var workspace = TemporaryWorkspace.Create(output);
using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace);
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/ConfigHealingTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/ConfigHealingTests.cs
index fa617337cb9..5a2c867a482 100644
--- a/tests/Aspire.Cli.EndToEnd.Tests/ConfigHealingTests.cs
+++ b/tests/Aspire.Cli.EndToEnd.Tests/ConfigHealingTests.cs
@@ -70,7 +70,7 @@ public async Task InvalidAppHostPathWithComments_IsHealedOnRun()
await auto.WaitForSuccessPromptAsync(counter);
await auto.TypeAsync("aspire run");
await auto.EnterAsync();
- await auto.WaitUntilTextAsync("Press CTRL+C to stop the AppHost and exit.", timeout: TimeSpan.FromMinutes(3));
+ await auto.WaitUntilTextAsync("Press CTRL+C to stop the", timeout: TimeSpan.FromMinutes(3));
await auto.Ctrl().KeyAsync(Hex1bKey.C);
await auto.WaitForSuccessPromptAsync(counter);
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs
index e4eaf3e0a2f..2d45dbbce8c 100644
--- a/tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs
+++ b/tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Aspire.Cli.EndToEnd.Tests.Helpers;
-using Aspire.Cli.Resources;
using Aspire.Cli.Tests.Utils;
using Hex1b.Automation;
using Xunit;
@@ -43,10 +42,7 @@ public async Task DescribeCommandShowsRunningResources()
await auto.WaitForSuccessPromptAsync(counter);
// Start the AppHost in the background using aspire start
- await auto.TypeAsync("aspire start");
- await auto.EnterAsync();
- await auto.WaitUntilTextAsync(RunCommandStrings.AppHostStartedSuccessfully, timeout: TimeSpan.FromMinutes(3));
- await auto.WaitForSuccessPromptAsync(counter);
+ await auto.AspireStartAsync(counter);
// Wait a bit for resources to stabilize
await auto.TypeAsync("sleep 5");
@@ -73,10 +69,7 @@ public async Task DescribeCommandShowsRunningResources()
await auto.WaitForSuccessPromptAsync(counter);
// Stop the AppHost using aspire stop
- await auto.TypeAsync("aspire stop");
- await auto.EnterAsync();
- await auto.WaitUntilTextAsync(StopCommandStrings.AppHostStoppedSuccessfully, timeout: TimeSpan.FromMinutes(1));
- await auto.WaitForSuccessPromptAsync(counter);
+ await auto.AspireStopAsync(counter);
// Exit the shell
await auto.TypeAsync("exit");
@@ -147,10 +140,7 @@ public async Task DescribeCommandResolvesReplicaNames()
output.WriteLine("Modified AppHost.cs to add .WithReplicas(2) to apiservice and clear fixed endpoint target ports");
// Start the AppHost in the background using aspire start
- await auto.TypeAsync("aspire start");
- await auto.EnterAsync();
- await auto.WaitUntilTextAsync(RunCommandStrings.AppHostStartedSuccessfully, timeout: TimeSpan.FromMinutes(3));
- await auto.WaitForSuccessPromptAsync(counter);
+ await auto.AspireStartAsync(counter);
// Wait for resources to stabilize
await auto.TypeAsync("sleep 10");
@@ -198,10 +188,7 @@ public async Task DescribeCommandResolvesReplicaNames()
await auto.WaitForSuccessPromptAsync(counter);
// Stop the AppHost using aspire stop
- await auto.TypeAsync("aspire stop");
- await auto.EnterAsync();
- await auto.WaitUntilTextAsync(StopCommandStrings.AppHostStoppedSuccessfully, timeout: TimeSpan.FromMinutes(1));
- await auto.WaitForSuccessPromptAsync(counter);
+ await auto.AspireStopAsync(counter);
// Exit the shell
await auto.TypeAsync("exit");
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DotNetTemplateBehaviorTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DotNetTemplateBehaviorTests.cs
new file mode 100644
index 00000000000..55e977f28a4
--- /dev/null
+++ b/tests/Aspire.Cli.EndToEnd.Tests/DotNetTemplateBehaviorTests.cs
@@ -0,0 +1,646 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+using Aspire.Cli.EndToEnd.Tests.Helpers;
+using Aspire.Cli.Tests.Utils;
+using Hex1b.Automation;
+using Xunit;
+
+namespace Aspire.Cli.EndToEnd.Tests;
+
+public sealed class DotNetTemplateLocalhostTldTests(ITestOutputHelper output)
+{
+ public static TheoryData LocalhostTldHostnameTestData()
+ {
+ return new()
+ {
+ { "aspire", "my.namespace.app", "my-namespace-app", true },
+ { "aspire", ".StartWithDot", "startwithdot", true },
+ { "aspire", "EndWithDot.", "endwithdot", true },
+ { "aspire", "My..Test__Project", "my-test-project", true },
+ { "aspire", "Project123.Test456", "project123-test456", true },
+ { "aspire-apphost", "my.service.name", "my-service-name", true },
+ { "aspire-apphost-singlefile", "-my.service..name-", "my-service-name", true },
+ { "aspire-starter", "Test_App.1", "test-app-1", false },
+ { "aspire-ts-cs-starter", "My-App.Test", "my-app-test", false }
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(LocalhostTldHostnameTestData))]
+ [CaptureWorkspaceOnFailure]
+ public async Task LocalhostTldGeneratesDnsCompliantHostnames(string templateName, string projectName, string expectedHostname, bool useBootstrapInstall)
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ string projectRoot;
+ if (useBootstrapInstall)
+ {
+ await DotNetTemplateBehaviorTestHelpers.CreateTemplateBootstrapAsync(auto, counter);
+ projectRoot = await DotNetTemplateBehaviorTestHelpers.CreateDotNetTemplateInBootstrapAsync(auto, counter, workspace, templateName, projectName, ["--localhost-tld"]);
+ }
+ else
+ {
+ projectRoot = await DotNetTemplateBehaviorTestHelpers.CreateCliTemplateAsync(auto, counter, workspace, templateName, projectName, useLocalhostTld: true);
+ }
+
+ DotNetTemplateBehaviorTestHelpers.AssertDevLocalhostHostname(projectRoot, templateName, projectName, expectedHostname);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+}
+
+public sealed class DotNetSingleFileAppHostTemplateTests(ITestOutputHelper output)
+{
+ [Fact]
+ [Trait("azdo-template-coverage", "true")]
+ [CaptureWorkspaceOnFailure]
+ public async Task SingleFileAppHostTemplateBuildsAndStarts()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ await DotNetTemplateBehaviorTestHelpers.CreateTemplateBootstrapAsync(auto, counter);
+ await DotNetTemplateBehaviorTestHelpers.CreateDotNetTemplateInBootstrapAsync(auto, counter, workspace, "aspire-apphost-singlefile", "SingleFileAppHost", []);
+
+ await auto.TypeAsync("cd SingleFileAppHost");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.TypeAsync("dotnet build apphost.cs");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(4));
+
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+}
+
+public sealed class DotNetTemplateTransportTests(ITestOutputHelper output)
+{
+ [Fact]
+ [CaptureWorkspaceOnFailure]
+ public async Task NoHttpsTemplateRequiresAllowUnsecuredTransport()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ await DotNetTemplateBehaviorTestHelpers.CreateTemplateBootstrapAsync(auto, counter);
+ var projectRoot = await DotNetTemplateBehaviorTestHelpers.CreateDotNetTemplateInBootstrapAsync(auto, counter, workspace, "aspire", "NoHttpsApp", ["--no-https"]);
+ var appHostDirectory = Path.Combine(projectRoot, "NoHttpsApp.AppHost");
+
+ await auto.TypeAsync($"cd {CliE2ETestHelpers.ToContainerPath(appHostDirectory, workspace)}");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.TypeAsync("aspire start");
+ await auto.EnterAsync();
+ await auto.WaitForAnyPromptAsync(counter, TimeSpan.FromMinutes(1));
+
+ var detachLogPath = await auto.CaptureLatestAspireLogAsync(
+ "~/.aspire/logs/cli_*_detach-child_*.log",
+ workspace,
+ counter,
+ "_aspire-detach.log");
+ Assert.Contains("must be an https address unless the 'ASPIRE_ALLOW_UNSECURED_TRANSPORT' environment variable is set to true", File.ReadAllText(detachLogPath), StringComparison.Ordinal);
+
+ await auto.ClearScreenAsync(counter);
+
+ await auto.TypeAsync("export ASPIRE_ALLOW_UNSECURED_TRANSPORT=true");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+}
+
+public sealed class DotNetTemplateTargetFrameworkTests(ITestOutputHelper output)
+{
+ [Fact]
+ [CaptureWorkspaceOnFailure]
+ public async Task DotNetTemplateCreatesSupportedTargetFrameworksAndOlderSdkRejectsNewerTarget()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ await DotNetTemplateBehaviorTestHelpers.CreateTemplateBootstrapAsync(auto, counter);
+
+ var projectRoots = new Dictionary(StringComparer.Ordinal)
+ {
+ ["net8.0"] = await DotNetTemplateBehaviorTestHelpers.CreateDotNetTemplateInBootstrapAsync(auto, counter, workspace, "aspire", "EmptyNet8", ["-f", "net8.0"]),
+ ["net10.0"] = await DotNetTemplateBehaviorTestHelpers.CreateDotNetTemplateInBootstrapAsync(auto, counter, workspace, "aspire", "EmptyNet10", ["-f", "net10.0"])
+ };
+
+ foreach (var (targetFramework, projectRoot) in projectRoots)
+ {
+ DotNetTemplateBehaviorTestHelpers.AssertGeneratedProjectsTargetFramework(projectRoot, targetFramework);
+
+ await auto.CaptureCommandOutputAsync(
+ $"dotnet build {AspireCliShellCommandHelpers.QuoteBashArg(CliE2ETestHelpers.ToContainerPath(projectRoot, workspace))}",
+ workspace,
+ counter,
+ $"dotnet-build-{targetFramework}.log",
+ TimeSpan.FromMinutes(4));
+ }
+
+ await auto.CaptureCommandOutputAsync(
+ "if [ ! -x /tmp/dotnet8/dotnet ]; then " +
+ "curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh && " +
+ "bash /tmp/dotnet-install.sh --channel 8.0 --install-dir /tmp/dotnet8 --no-path; " +
+ "fi",
+ workspace,
+ counter,
+ "dotnet8-install.log",
+ TimeSpan.FromMinutes(5));
+
+ var olderSdkBuildLogPath = await auto.CaptureFailingCommandOutputAsync(
+ $"DOTNET_MULTILEVEL_LOOKUP=0 /tmp/dotnet8/dotnet build {AspireCliShellCommandHelpers.QuoteBashArg(CliE2ETestHelpers.ToContainerPath(projectRoots["net10.0"], workspace))}",
+ workspace,
+ counter,
+ "dotnet8-net10-build.log",
+ TimeSpan.FromMinutes(2));
+
+ Assert.Contains("The current .NET SDK does not support targeting .NET 10.0", File.ReadAllText(olderSdkBuildLogPath), StringComparison.Ordinal);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+}
+
+public sealed class DotNetTemplateProjectFileBehaviorTests(ITestOutputHelper output)
+{
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ [Trait("azdo-template-coverage", "true")]
+ [CaptureWorkspaceOnFailure]
+ public async Task DotNetTemplateWithExplicitSdkReferenceBuildsAndStarts(bool includeAspireHostingAppHostPackageReference)
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+ await auto.EnableShowAllTemplatesAsync(counter);
+
+ await auto.AspireNewSubcommandAsync("aspire", "ExplicitSdkApp", counter);
+
+ var appHostProjectPath = Path.Combine(workspace.WorkspaceRoot.FullName, "ExplicitSdkApp", "ExplicitSdkApp.AppHost", "ExplicitSdkApp.AppHost.csproj");
+ DotNetTemplateBehaviorTestHelpers.RewriteAsExplicitSdkReference(appHostProjectPath, includeAspireHostingAppHostPackageReference);
+
+ var appHostDirectory = Path.GetDirectoryName(appHostProjectPath)!;
+ await auto.TypeAsync($"cd {CliE2ETestHelpers.ToContainerPath(appHostDirectory, workspace)}");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.TypeAsync("dotnet build");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2));
+
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ [Fact]
+ [Trait("azdo-template-coverage", "true")]
+ [CaptureWorkspaceOnFailure]
+ public Task DotNetAppHostTemplateBuildsWithAppHostPackageVersionOverride_8_1_0()
+ => DotNetAppHostTemplateBuildsWithAppHostPackageVersionOverride("8.1.0");
+
+ [Theory]
+ [InlineData("9.*-*")]
+ [InlineData("[9.0.0]")]
+ [CaptureWorkspaceOnFailure]
+ public Task DotNetAppHostTemplateBuildsWithAppHostPackageVersionOverride(string version)
+ => DotNetAppHostTemplateBuildsWithAppHostPackageVersionOverrideCore(version);
+
+ private async Task DotNetAppHostTemplateBuildsWithAppHostPackageVersionOverrideCore(string version)
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+ await auto.EnableShowAllTemplatesAsync(counter);
+
+ await auto.AspireNewSubcommandAsync("aspire-apphost", "VersionedAppHost", counter);
+
+ var appHostRoot = Path.Combine(workspace.WorkspaceRoot.FullName, "VersionedAppHost");
+ var appHostProjectPath = Path.Combine(appHostRoot, "VersionedAppHost.csproj");
+ var nugetConfigPath = Path.Combine(appHostRoot, "nuget.config");
+ if (File.Exists(nugetConfigPath))
+ {
+ DotNetTemplateBehaviorTestHelpers.DisableAspirePackageSourceMapping(nugetConfigPath);
+ }
+
+ DotNetTemplateBehaviorTestHelpers.AddAspireHostingAppHostPackageReference(appHostProjectPath, version);
+
+ await auto.TypeAsync("cd VersionedAppHost");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.TypeAsync("dotnet build");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(4));
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ [Fact]
+ [Trait("azdo-template-coverage", "true")]
+ [CaptureWorkspaceOnFailure]
+ public async Task DotNetTemplateWithCentralPackageManagementBuildsAndStarts()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+ await auto.EnableShowAllTemplatesAsync(counter);
+
+ await auto.AspireNewSubcommandAsync("aspire", "CpmApp", counter);
+
+ var projectRoot = Path.Combine(workspace.WorkspaceRoot.FullName, "CpmApp");
+ var appHostProjectPath = Path.Combine(projectRoot, "CpmApp.AppHost", "CpmApp.AppHost.csproj");
+ DotNetTemplateBehaviorTestHelpers.AddCentralPackageManagementForRedis(appHostProjectPath, Path.Combine(projectRoot, "Directory.Packages.props"));
+
+ await auto.TypeAsync("cd CpmApp/CpmApp.AppHost");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.TypeAsync("dotnet build");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2));
+
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+}
+
+internal static partial class DotNetTemplateBehaviorTestHelpers
+{
+ internal static async Task CreateTemplateBootstrapAsync(Hex1bTerminalAutomator auto, SequenceCounter counter)
+ {
+ await auto.AspireNewSubcommandAsync("aspire-starter", "TemplateBootstrap", counter, "--use-redis-cache", "false", "--test-framework", "None");
+ await auto.ClearScreenAsync(counter);
+ await auto.TypeAsync("cd TemplateBootstrap");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+ }
+
+ internal static async Task CreateDotNetTemplateInBootstrapAsync(
+ Hex1bTerminalAutomator auto,
+ SequenceCounter counter,
+ TemporaryWorkspace workspace,
+ string templateName,
+ string projectName,
+ IReadOnlyList extraArgs)
+ {
+ var commandParts = new List
+ {
+ "dotnet",
+ "new",
+ templateName,
+ "-n",
+ Quote(projectName),
+ "-o",
+ Quote($"./{projectName}")
+ };
+
+ commandParts.AddRange(extraArgs);
+
+ await auto.TypeAsync(string.Join(" ", commandParts));
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2));
+
+ var markerFileName = templateName switch
+ {
+ "aspire" or "aspire-starter" or "aspire-ts-cs-starter" => "*.sln",
+ "aspire-apphost-singlefile" => "apphost.run.json",
+ _ => null
+ };
+
+ return markerFileName is null
+ ? Path.Combine(GetTemplateBootstrapRoot(workspace), projectName)
+ : ResolveGeneratedTemplateDirectory(workspace, projectName, markerFileName);
+ }
+
+ internal static string ResolveGeneratedTemplateDirectory(TemporaryWorkspace workspace, string projectName, string markerFileName)
+ {
+ var bootstrapRoot = GetTemplateBootstrapRoot(workspace);
+ var expectedProjectRoot = Path.Combine(bootstrapRoot, projectName);
+ if (Directory.Exists(expectedProjectRoot))
+ {
+ return expectedProjectRoot;
+ }
+
+ var candidates = Directory.EnumerateFiles(bootstrapRoot, markerFileName, SearchOption.AllDirectories)
+ .Select(path => Path.GetDirectoryName(path)!)
+ .Where(directory =>
+ {
+ var relativePath = Path.GetRelativePath(bootstrapRoot, directory);
+
+ return relativePath != "." &&
+ !relativePath.StartsWith("TemplateBootstrap", StringComparison.Ordinal);
+ })
+ .Distinct(StringComparer.Ordinal)
+ .ToArray();
+
+ return Assert.Single(candidates);
+ }
+
+ private static string GetTemplateBootstrapRoot(TemporaryWorkspace workspace)
+ => Path.Combine(workspace.WorkspaceRoot.FullName, "TemplateBootstrap");
+
+ internal static async Task CreateCliTemplateAsync(
+ Hex1bTerminalAutomator auto,
+ SequenceCounter counter,
+ TemporaryWorkspace workspace,
+ string templateName,
+ string projectName,
+ bool useLocalhostTld)
+ {
+ var extraArgs = templateName switch
+ {
+ "aspire-starter" => useLocalhostTld
+ ? new[] { "--localhost-tld", "--use-redis-cache", "false", "--test-framework", "None" }
+ : new[] { "--use-redis-cache", "false", "--test-framework", "None" },
+ "aspire-ts-cs-starter" => useLocalhostTld
+ ? new[] { "--localhost-tld", "--use-redis-cache", "false" }
+ : new[] { "--use-redis-cache", "false" },
+ _ => useLocalhostTld ? ["--localhost-tld"] : []
+ };
+
+ await auto.AspireNewSubcommandAsync(templateName, projectName, counter, extraArgs);
+ return Path.Combine(workspace.WorkspaceRoot.FullName, projectName);
+ }
+
+ internal static void AssertDevLocalhostHostname(string projectRoot, string templateName, string projectName, string expectedHostname)
+ {
+ var settingsPath = templateName switch
+ {
+ "aspire" or "aspire-starter" or "aspire-ts-cs-starter" => Path.Combine(projectRoot, $"{projectName}.AppHost", "Properties", "launchSettings.json"),
+ "aspire-apphost" => Path.Combine(projectRoot, "Properties", "launchSettings.json"),
+ "aspire-apphost-singlefile" => Path.Combine(projectRoot, "apphost.run.json"),
+ _ => throw new ArgumentOutOfRangeException(nameof(templateName), templateName, "Unknown template name.")
+ };
+
+ if (!File.Exists(settingsPath))
+ {
+ settingsPath = FindGeneratedAppHostSettingsPath(projectRoot, templateName);
+ }
+
+ Assert.True(File.Exists(settingsPath), $"Expected launch settings at {settingsPath}.");
+
+ using var document = JsonDocument.Parse(File.ReadAllText(settingsPath));
+ var profiles = document.RootElement.GetProperty("profiles");
+
+ var foundDevLocalhost = false;
+ foreach (var profile in profiles.EnumerateObject())
+ {
+ if (!profile.Value.TryGetProperty("applicationUrl", out var applicationUrl))
+ {
+ continue;
+ }
+
+ var urls = applicationUrl.GetString();
+ if (string.IsNullOrEmpty(urls) || !urls.Contains(".dev.localhost:", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ foundDevLocalhost = true;
+ Assert.Contains($"{expectedHostname}.dev.localhost:", urls, StringComparison.Ordinal);
+
+ foreach (Match match in DevLocalhostHostnameRegex().Matches(urls))
+ {
+ var hostname = match.Groups[1].Value;
+ Assert.DoesNotContain("_", hostname, StringComparison.Ordinal);
+ Assert.DoesNotContain(".", hostname, StringComparison.Ordinal);
+ Assert.False(hostname.StartsWith("-", StringComparison.Ordinal), $"Hostname '{hostname}' should not start with hyphen.");
+ Assert.False(hostname.EndsWith("-", StringComparison.Ordinal), $"Hostname '{hostname}' should not end with hyphen.");
+ }
+ }
+
+ Assert.True(foundDevLocalhost, $"Expected a .dev.localhost URL in {settingsPath}.");
+ }
+
+ internal static void AssertGeneratedProjectsTargetFramework(string projectRoot, string expectedTargetFramework)
+ {
+ Assert.True(Directory.Exists(projectRoot), $"Expected project root at {projectRoot}.");
+
+ var projectFiles = Directory.EnumerateFiles(projectRoot, "*.csproj", SearchOption.AllDirectories)
+ .OrderBy(static path => path, StringComparer.Ordinal)
+ .ToArray();
+
+ Assert.NotEmpty(projectFiles);
+
+ foreach (var projectFile in projectFiles)
+ {
+ var document = XDocument.Load(projectFile);
+ var targetFrameworks = document.Descendants()
+ .Where(static element => element.Name.LocalName is "TargetFramework" or "TargetFrameworks")
+ .SelectMany(static element => element.Value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
+ .ToArray();
+
+ Assert.Contains(expectedTargetFramework, targetFrameworks);
+ }
+ }
+
+ private static string FindGeneratedAppHostSettingsPath(string projectRoot, string templateName)
+ {
+ Assert.True(Directory.Exists(projectRoot), $"Expected project root at {projectRoot}.");
+
+ if (templateName is "aspire-apphost-singlefile")
+ {
+ return Assert.Single(Directory.EnumerateFiles(projectRoot, "apphost.run.json", SearchOption.AllDirectories));
+ }
+
+ var candidates = Directory.EnumerateFiles(projectRoot, "launchSettings.json", SearchOption.AllDirectories)
+ .Where(path =>
+ {
+ var propertiesDirectory = Directory.GetParent(path);
+ var appHostDirectory = propertiesDirectory?.Parent;
+
+ return string.Equals(propertiesDirectory?.Name, "Properties", StringComparison.Ordinal) &&
+ appHostDirectory?.Name.EndsWith(".AppHost", StringComparison.Ordinal) is true;
+ })
+ .ToArray();
+
+ return Assert.Single(candidates);
+ }
+
+ internal static void RewriteAsExplicitSdkReference(string appHostProjectPath, bool includeAspireHostingAppHostPackageReference)
+ {
+ var document = XDocument.Load(appHostProjectPath);
+ var project = document.Root ?? throw new InvalidOperationException($"Project root not found in {appHostProjectPath}.");
+ var sdkAttribute = project.Attribute("Sdk") ?? throw new InvalidOperationException($"Sdk attribute not found in {appHostProjectPath}.");
+ var version = ExtractSdkVersion(sdkAttribute.Value);
+
+ project.SetAttributeValue("Sdk", "Microsoft.NET.Sdk");
+ project.Elements("Sdk").Remove();
+ project.AddFirst(new XElement("Sdk",
+ new XAttribute("Name", "Aspire.AppHost.Sdk"),
+ new XAttribute("Version", version)));
+
+ if (includeAspireHostingAppHostPackageReference)
+ {
+ project.Add(new XElement("ItemGroup",
+ new XElement("PackageReference",
+ new XAttribute("Include", "Aspire.Hosting.AppHost"),
+ new XAttribute("Version", version))));
+ }
+
+ document.Save(appHostProjectPath);
+ }
+
+ internal static void AddAspireHostingAppHostPackageReference(string appHostProjectPath, string version)
+ {
+ var document = XDocument.Load(appHostProjectPath);
+ var project = document.Root ?? throw new InvalidOperationException($"Project root not found in {appHostProjectPath}.");
+ project.Add(new XElement("ItemGroup",
+ new XElement("PackageReference",
+ new XAttribute("Include", "Aspire.Hosting.AppHost"),
+ new XAttribute("Version", version))));
+ document.Save(appHostProjectPath);
+ }
+
+ internal static void DisableAspirePackageSourceMapping(string nugetConfigPath)
+ {
+ var document = XDocument.Load(nugetConfigPath);
+ document.Root?.Element("packageSourceMapping")?.Remove();
+ document.Save(nugetConfigPath);
+ }
+
+ internal static void AddCentralPackageManagementForRedis(string appHostProjectPath, string directoryPackagesPropsPath)
+ {
+ var document = XDocument.Load(appHostProjectPath);
+ var project = document.Root ?? throw new InvalidOperationException($"Project root not found in {appHostProjectPath}.");
+ var sdkAttribute = project.Attribute("Sdk") ?? throw new InvalidOperationException($"Sdk attribute not found in {appHostProjectPath}.");
+ var version = ExtractSdkVersion(sdkAttribute.Value);
+
+ project.Add(new XElement("ItemGroup",
+ new XElement("PackageReference",
+ new XAttribute("Include", "Aspire.Hosting.Redis"))));
+ document.Save(appHostProjectPath);
+
+ File.WriteAllText(directoryPackagesPropsPath, $$"""
+
+
+ true
+ NU1507;$(NoWarn)
+
+
+
+
+
+ """);
+ }
+
+ private static string ExtractSdkVersion(string sdkValue)
+ {
+ const string prefix = "Aspire.AppHost.Sdk/";
+ return sdkValue.StartsWith(prefix, StringComparison.Ordinal)
+ ? sdkValue[prefix.Length..]
+ : throw new InvalidOperationException($"Unexpected SDK value '{sdkValue}'.");
+ }
+
+ internal static string Quote(string value) => $"\"{value}\"";
+
+ [GeneratedRegex(@"://([^:]+)\.dev\.localhost:")]
+ private static partial Regex DevLocalhostHostnameRegex();
+}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/EmptyAppHostTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/EmptyAppHostTemplateTests.cs
deleted file mode 100644
index 5c8703ffbe8..00000000000
--- a/tests/Aspire.Cli.EndToEnd.Tests/EmptyAppHostTemplateTests.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.Cli.EndToEnd.Tests.Helpers;
-using Aspire.Cli.Tests.Utils;
-using Hex1b.Automation;
-using Xunit;
-
-namespace Aspire.Cli.EndToEnd.Tests;
-
-///
-/// End-to-end tests for Aspire CLI with Empty AppHost template.
-/// Each test class runs as a separate CI job for parallelization.
-///
-public sealed class EmptyAppHostTemplateTests(ITestOutputHelper output)
-{
- [CaptureWorkspaceOnFailure]
- [Fact]
- public async Task CreateAndRunEmptyAppHostProject()
- {
- var repoRoot = CliE2ETestHelpers.GetRepoRoot();
- var strategy = CliInstallStrategy.Detect(output.WriteLine);
- var workspace = TemporaryWorkspace.Create(output);
-
- using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
-
- var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
-
- var counter = new SequenceCounter();
- var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
-
- await auto.PrepareDockerEnvironmentAsync(counter, workspace);
- await auto.InstallAspireCliAsync(strategy, counter);
-
- await auto.AspireNewAsync("AspireEmptyApp", counter, template: AspireTemplate.EmptyAppHost);
-
- // Start the empty AppHost to verify the scaffolded project works
- await auto.TypeAsync("cd AspireEmptyApp");
- await auto.EnterAsync();
- await auto.WaitForSuccessPromptAsync(counter);
-
- await auto.AspireStartAsync(counter);
- await auto.AspireStopAsync(counter);
-
- await auto.TypeAsync("exit");
- await auto.EnterAsync();
-
- await pendingRun;
- }
-}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs
index 9f20237a613..3d08ef50d16 100644
--- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs
+++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs
@@ -215,7 +215,7 @@ private static string BuildAspireNewEmptyAppHostCommand(
$"aspire new {AspireCliShellCommandHelpers.QuoteBashArg(templateName)} " +
$"--name {AspireCliShellCommandHelpers.QuoteBashArg(projectName)} " +
$"--output {AspireCliShellCommandHelpers.QuoteBashArg(output)}" +
- $"{channelArgument} --localhost-tld {localhostTldValue}";
+ $"{channelArgument} --localhost-tld {localhostTldValue} --suppress-agent-init";
}
private static async Task WaitForAspireNewEmptyAppHostCompletionAsync(
@@ -672,6 +672,89 @@ internal static async Task EnableExperimentalJavaSupportAsync(
await auto.WaitForSuccessPromptAsync(counter);
}
+ ///
+ /// Enables the hidden dotnet-template entries for aspire new.
+ ///
+ internal static async Task EnableShowAllTemplatesAsync(
+ this Hex1bTerminalAutomator auto,
+ SequenceCounter counter)
+ {
+ await auto.TypeAsync("aspire config set features:showAllTemplates true --global --non-interactive");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+ }
+
+ ///
+ /// Runs aspire new <template> non-interactively for a specific template subcommand.
+ ///
+ internal static async Task AspireNewSubcommandAsync(
+ this Hex1bTerminalAutomator auto,
+ string templateName,
+ string projectName,
+ SequenceCounter counter,
+ params string[] extraArgs)
+ {
+ var commandParts = new List
+ {
+ "aspire",
+ "new",
+ templateName,
+ "--name",
+ AspireCliShellCommandHelpers.QuoteBashArg(projectName),
+ "--output",
+ AspireCliShellCommandHelpers.QuoteBashArg($"./{projectName}"),
+ "--suppress-agent-init",
+ };
+
+ foreach (var arg in extraArgs)
+ {
+ commandParts.Add(arg);
+ }
+
+ await auto.TypeAsync(string.Join(" ", commandParts));
+ await auto.EnterAsync();
+ await auto.CompleteAspireNewSubcommandPromptsAsync(counter);
+ }
+
+ private static async Task CompleteAspireNewSubcommandPromptsAsync(
+ this Hex1bTerminalAutomator auto,
+ SequenceCounter counter,
+ TimeSpan? timeout = null)
+ {
+ var effectiveTimeout = timeout ?? TimeSpan.FromSeconds(500);
+ var devLocalhostPrompt = new CellPatternSearcher()
+ .Find("Use *.dev.localhost URLs");
+ var agentInitPrompt = new CellPatternSearcher()
+ .Find("configure AI agent environments");
+
+ var devLocalhostPromptFound = false;
+ await auto.WaitUntilAsync(snapshot =>
+ {
+ if (devLocalhostPrompt.Search(snapshot).Count > 0)
+ {
+ devLocalhostPromptFound = true;
+ return true;
+ }
+
+ if (agentInitPrompt.Search(snapshot).Count > 0)
+ {
+ return true;
+ }
+
+ var successSearcher = new CellPatternSearcher()
+ .FindPattern(counter.Value.ToString())
+ .RightText(" OK] $ ");
+ return successSearcher.Search(snapshot).Count > 0;
+ }, timeout: effectiveTimeout, description: $"dev localhost prompt, agent init prompt, or success prompt [{counter.Value} OK] $");
+
+ if (devLocalhostPromptFound)
+ {
+ await auto.EnterAsync();
+ }
+
+ await auto.DeclineAgentInitPromptAsync(counter, effectiveTimeout);
+ }
+
///
/// Installs a specific GA version of the Aspire CLI using the install script.
///
@@ -686,6 +769,228 @@ internal static async Task InstallAspireCliVersionAsync(
await auto.RunCommandFailFastAsync(command, counter, TimeSpan.FromSeconds(300));
}
+ ///
+ /// Runs aspire run, captures its transcript in the workspace, and waits for the AppHost to be ready.
+ ///
+ internal static async Task AspireRunUntilReadyAsync(
+ this Hex1bTerminalAutomator auto,
+ TemporaryWorkspace workspace,
+ TimeSpan? timeout = null)
+ {
+ var effectiveTimeout = timeout ?? TimeSpan.FromMinutes(5);
+ var hostOutputPath = CliE2ETestHelpers.GetWorkspaceFilePath(workspace, "_aspire-run.log");
+ var containerOutputPath = CliE2ETestHelpers.ToContainerPath(hostOutputPath, workspace);
+
+ CliE2ETestHelpers.RegisterCaptureFile("_aspire-run.log", hostOutputPath);
+
+ await auto.TypeAsync(
+ "(set -o pipefail; aspire run 2>&1 | tee " +
+ AspireCliShellCommandHelpers.QuoteBashArg(containerOutputPath) +
+ "; s=${PIPESTATUS[0]}; if [ \"$s\" -eq 130 ]; then exit 0; fi; exit \"$s\")");
+ await auto.EnterAsync();
+
+ await auto.WaitUntilAsync(s =>
+ {
+ if (s.ContainsText("Select an AppHost to use:"))
+ {
+ throw new InvalidOperationException(
+ "Unexpected apphost selection prompt detected! " +
+ "This indicates multiple apphosts were incorrectly detected.");
+ }
+
+ return s.ContainsText("Press CTRL+C to stop the AppHost and exit.")
+ || s.ContainsText("Press CTRL+C to stop the apphost and exit.");
+ }, timeout: effectiveTimeout, description: "Press CTRL+C message (aspire run started)");
+ }
+
+ ///
+ /// Copies the latest aspire start --format json transcript into the mounted workspace.
+ ///
+ internal static async Task PersistAspireStartJsonAsync(
+ this Hex1bTerminalAutomator auto,
+ TemporaryWorkspace workspace,
+ SequenceCounter counter)
+ {
+ var hostOutputPath = CliE2ETestHelpers.GetWorkspaceFilePath(workspace, "_aspire-start.json");
+ var containerOutputPath = CliE2ETestHelpers.ToContainerPath(hostOutputPath, workspace);
+
+ CliE2ETestHelpers.RegisterCaptureFile("_aspire-start.json", hostOutputPath);
+
+ await auto.RunCommandAsync(
+ $"if [ -f {AspireCliShellCommandHelpers.QuoteBashArg(AspireStartJsonFile)} ]; then cp {AspireCliShellCommandHelpers.QuoteBashArg(AspireStartJsonFile)} {AspireCliShellCommandHelpers.QuoteBashArg(containerOutputPath)}; fi",
+ counter);
+ }
+
+ ///
+ /// Runs a JSON-producing CLI command and writes stdout to a workspace file for host-side assertions.
+ ///
+ internal static async Task CaptureJsonOutputAsync(
+ this Hex1bTerminalAutomator auto,
+ string command,
+ TemporaryWorkspace workspace,
+ SequenceCounter counter,
+ string outputFileName)
+ {
+ var (hostOutputPath, containerOutputPath) = RegisterWorkspaceCaptureFile(workspace, outputFileName);
+
+ await auto.RunCommandAsync($"{command} > {AspireCliShellCommandHelpers.QuoteBashArg(containerOutputPath)}", counter);
+ return hostOutputPath;
+ }
+
+ ///
+ /// Runs a shell command and captures stdout and stderr to a workspace file.
+ ///
+ internal static async Task CaptureCommandOutputAsync(
+ this Hex1bTerminalAutomator auto,
+ string command,
+ TemporaryWorkspace workspace,
+ SequenceCounter counter,
+ string outputFileName,
+ TimeSpan? timeout = null)
+ {
+ var (hostOutputPath, containerOutputPath) = RegisterWorkspaceCaptureFile(workspace, outputFileName);
+
+ await auto.RunCommandFailFastAsync(
+ $"{command} > {AspireCliShellCommandHelpers.QuoteBashArg(containerOutputPath)} 2>&1",
+ counter,
+ timeout ?? TimeSpan.FromSeconds(500));
+
+ return hostOutputPath;
+ }
+
+ ///
+ /// Runs a shell command expected to fail and captures stdout and stderr to a workspace file.
+ ///
+ internal static async Task CaptureFailingCommandOutputAsync(
+ this Hex1bTerminalAutomator auto,
+ string command,
+ TemporaryWorkspace workspace,
+ SequenceCounter counter,
+ string outputFileName,
+ TimeSpan? timeout = null)
+ {
+ var (hostOutputPath, containerOutputPath) = RegisterWorkspaceCaptureFile(workspace, outputFileName);
+ var expectedCounter = counter.Value;
+ var succeeded = false;
+
+ await auto.TypeAsync($"{command} > {AspireCliShellCommandHelpers.QuoteBashArg(containerOutputPath)} 2>&1");
+ await auto.EnterAsync();
+ await auto.WaitUntilAsync(snapshot =>
+ {
+ var successSearcher = new CellPatternSearcher()
+ .FindPattern(expectedCounter.ToString())
+ .RightText(" OK] $ ");
+ if (successSearcher.Search(snapshot).Count > 0)
+ {
+ succeeded = true;
+ return true;
+ }
+
+ var errorSearcher = new CellPatternSearcher()
+ .FindPattern(expectedCounter.ToString())
+ .RightText(" ERR:");
+
+ return errorSearcher.Search(snapshot).Count > 0;
+ }, timeout: timeout ?? TimeSpan.FromSeconds(500), description: $"expected failing command [{expectedCounter} ERR]");
+
+ counter.Increment();
+
+ if (succeeded)
+ {
+ throw new InvalidOperationException($"Expected command to fail, but it succeeded. Output captured at {hostOutputPath}. Command: {command}");
+ }
+
+ return hostOutputPath;
+ }
+
+ ///
+ /// Copies the latest Aspire CLI log matching a glob from the container into the mounted workspace.
+ ///
+ internal static async Task CaptureLatestAspireLogAsync(
+ this Hex1bTerminalAutomator auto,
+ string logGlob,
+ TemporaryWorkspace workspace,
+ SequenceCounter counter,
+ string outputFileName)
+ {
+ var (hostOutputPath, containerOutputPath) = RegisterWorkspaceCaptureFile(workspace, outputFileName);
+
+ await auto.RunCommandAsync(
+ $"LOG=$(ls -t {logGlob} 2>/dev/null | head -1) && test -n \"$LOG\" && cp \"$LOG\" {AspireCliShellCommandHelpers.QuoteBashArg(containerOutputPath)}",
+ counter);
+
+ return hostOutputPath;
+ }
+
+ ///
+ /// Verifies a URL responds with HTTP 200 from inside the CLI test environment.
+ ///
+ internal static async Task AssertUrlRespondsAsync(
+ this Hex1bTerminalAutomator auto,
+ string url,
+ string label,
+ SequenceCounter counter,
+ TimeSpan? timeout = null)
+ {
+ var failurePrefix = $"{label}-http-status=";
+
+ await auto.TypeAsync(
+ $"status=$(curl -ksSL -o /dev/null -w '%{{http_code}}' {AspireCliShellCommandHelpers.QuoteBashArg(url)}) && " +
+ $"{{ [ \"$status\" = \"200\" ] || {{ printf '%s%s\\n' {AspireCliShellCommandHelpers.QuoteBashArg(failurePrefix)} \"$status\"; exit 1; }}; }}");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptFailFastAsync(counter, timeout ?? TimeSpan.FromSeconds(30));
+ }
+
+ private static async Task ExtractLocalHiveArchiveAsync(
+ this Hex1bTerminalAutomator auto,
+ string archivePath,
+ SequenceCounter counter)
+ {
+ await auto.RunCommandAsync($"mkdir -p ~/.aspire && tar -xzf {archivePath} -C ~/.aspire 2>/dev/null", counter, TimeSpan.FromSeconds(30));
+ }
+
+ private static async Task ConfigureLocalHiveAsync(
+ this Hex1bTerminalAutomator auto,
+ SequenceCounter counter)
+ {
+ await auto.RunCommandAsync("aspire config set channel local -g", counter);
+ await auto.RunCommandAsync("SDK_VER=$(ls ~/.aspire/hives/local/packages/Aspire.Hosting.*.nupkg 2>/dev/null | head -1 | sed 's/.*Aspire\\.Hosting\\.//;s/\\.nupkg//') && aspire config set sdk.version \"$SDK_VER\" -g", counter);
+ }
+
+ private static (string HostOutputPath, string ContainerOutputPath) RegisterWorkspaceCaptureFile(
+ TemporaryWorkspace workspace,
+ string outputFileName)
+ {
+ var hostOutputPath = CliE2ETestHelpers.GetWorkspaceFilePath(workspace, outputFileName);
+ var containerOutputPath = CliE2ETestHelpers.ToContainerPath(hostOutputPath, workspace);
+
+ CliE2ETestHelpers.RegisterCaptureFile(outputFileName, hostOutputPath);
+
+ return (hostOutputPath, containerOutputPath);
+ }
+
+ private static async Task RunCommandAsync(
+ this Hex1bTerminalAutomator auto,
+ string command,
+ SequenceCounter counter,
+ TimeSpan? timeout = null)
+ {
+ await auto.TypeAsync(command);
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter, timeout);
+ }
+
+ private static async Task RunCommandFailFastAsync(
+ this Hex1bTerminalAutomator auto,
+ string command,
+ SequenceCounter counter,
+ TimeSpan timeout)
+ {
+ await auto.TypeAsync(command);
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptFailFastAsync(counter, timeout);
+ }
+
///
/// Starts an Aspire AppHost with aspire start --format json, extracts the dashboard URL,
/// and verifies the dashboard is reachable. Caller is responsible for calling
@@ -854,13 +1159,16 @@ await auto.TypeAsync(
}
///
- /// Stops a running Aspire AppHost with aspire stop.
+ /// Stops running Aspire AppHosts with aspire stop --all.
+ /// In the isolated Docker E2E environment each test owns its own container, so stopping all
+ /// avoids interactive selection prompts when the CLI cannot correlate the current directory to
+ /// the running AppHost path.
///
internal static async Task AspireStopAsync(
this Hex1bTerminalAutomator auto,
SequenceCounter counter)
{
- await auto.TypeAsync("aspire stop");
+ await auto.TypeAsync("aspire stop --all");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);
}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs
index b92a2b4d373..08af24d412b 100644
--- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs
+++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
+using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Xml.Linq;
@@ -497,6 +498,197 @@ internal static string ToContainerPath(string hostPath, TemporaryWorkspace works
return $"/workspace/{workspace.WorkspaceRoot.Name}/" + relativePath.Replace('\\', '/');
}
+ ///
+ /// Gets a file path rooted under the mounted workspace.
+ ///
+ internal static string GetWorkspaceFilePath(TemporaryWorkspace workspace, string relativePath)
+ {
+ return Path.Combine(workspace.WorkspaceRoot.FullName, relativePath.Replace('/', Path.DirectorySeparatorChar));
+ }
+
+ ///
+ /// Verifies the persisted aspire start --format json transcript contains the expected contract fields.
+ ///
+ internal static void AssertAspireStartJsonContract(TemporaryWorkspace workspace)
+ {
+ var jsonPath = GetWorkspaceFilePath(workspace, "_aspire-start.json");
+ WithJsonFailureDiagnostics("aspire start JSON", jsonPath, content =>
+ {
+ using var document = JsonDocument.Parse(content);
+ var root = document.RootElement;
+
+ Assert.True(root.ValueKind is JsonValueKind.Object, $"Expected JSON object in {jsonPath}");
+ Assert.True(root.TryGetProperty("appHostPath", out var appHostPath) &&
+ appHostPath.ValueKind is JsonValueKind.String &&
+ !string.IsNullOrWhiteSpace(appHostPath.GetString()),
+ $"Expected non-empty appHostPath in {jsonPath}");
+ Assert.True(root.TryGetProperty("appHostPid", out var appHostPid) &&
+ HasNonEmptyStringOrNumber(appHostPid),
+ $"Expected non-empty appHostPid in {jsonPath}");
+ Assert.True(root.TryGetProperty("dashboardUrl", out var dashboardUrl) &&
+ dashboardUrl.ValueKind is JsonValueKind.String &&
+ !string.IsNullOrWhiteSpace(dashboardUrl.GetString()),
+ $"Expected non-empty dashboardUrl in {jsonPath}");
+
+ return true;
+ });
+ }
+
+ ///
+ /// Verifies the aspire ps --format json output contains at least one AppHost entry with the expected keys.
+ ///
+ internal static void AssertPsJsonContract(string jsonPath)
+ {
+ WithJsonFailureDiagnostics("aspire ps JSON", jsonPath, content =>
+ {
+ using var document = JsonDocument.Parse(content);
+ var root = document.RootElement;
+
+ Assert.True(root.ValueKind is JsonValueKind.Array, $"Expected JSON array in {jsonPath}");
+
+ var sawItem = false;
+ foreach (var item in root.EnumerateArray())
+ {
+ sawItem = true;
+
+ Assert.True(item.TryGetProperty("appHostPath", out var appHostPath) &&
+ appHostPath.ValueKind is JsonValueKind.String &&
+ !string.IsNullOrWhiteSpace(appHostPath.GetString()),
+ $"Expected non-empty appHostPath in {jsonPath}");
+ Assert.True(item.TryGetProperty("appHostPid", out var appHostPid) &&
+ HasNonEmptyStringOrNumber(appHostPid),
+ $"Expected non-empty appHostPid in {jsonPath}");
+ }
+
+ Assert.True(sawItem, $"Expected at least one AppHost entry in {jsonPath}");
+ return true;
+ });
+ }
+
+ ///
+ /// Extracts the first localhost resource endpoint URL from a JSON file.
+ ///
+ internal static string GetFirstLocalhostUrlFromJsonFile(string jsonPath)
+ {
+ return WithJsonFailureDiagnostics("resource JSON", jsonPath, content =>
+ {
+ using var document = JsonDocument.Parse(content);
+ foreach (var resource in EnumerateResources(document.RootElement))
+ {
+ if (!resource.TryGetProperty("urls", out var urls) ||
+ urls.ValueKind is not JsonValueKind.Array)
+ {
+ continue;
+ }
+
+ foreach (var url in urls.EnumerateArray())
+ {
+ var urlString = GetResourceUrlString(url);
+ if (urlString is not null &&
+ Uri.TryCreate(urlString, UriKind.Absolute, out var uri) &&
+ (uri.Scheme is "http" or "https") &&
+ string.Equals(uri.Host, "localhost", StringComparison.OrdinalIgnoreCase))
+ {
+ return urlString;
+ }
+ }
+ }
+
+ throw new InvalidOperationException($"Expected a localhost resource endpoint URL in {jsonPath}");
+ });
+ }
+
+ private static IEnumerable EnumerateResources(JsonElement root)
+ {
+ if (root.ValueKind is JsonValueKind.Object &&
+ root.TryGetProperty("resources", out var resources) &&
+ resources.ValueKind is JsonValueKind.Array)
+ {
+ foreach (var resource in resources.EnumerateArray())
+ {
+ yield return resource;
+ }
+
+ yield break;
+ }
+
+ if (root.ValueKind is JsonValueKind.Array)
+ {
+ foreach (var resource in root.EnumerateArray())
+ {
+ yield return resource;
+ }
+
+ yield break;
+ }
+
+ if (root.ValueKind is JsonValueKind.Object)
+ {
+ yield return root;
+ }
+ }
+
+ private static string? GetResourceUrlString(JsonElement url)
+ {
+ if (url.ValueKind is JsonValueKind.String)
+ {
+ return url.GetString();
+ }
+
+ return url.ValueKind is JsonValueKind.Object &&
+ url.TryGetProperty("url", out var value) &&
+ value.ValueKind is JsonValueKind.String
+ ? value.GetString()
+ : null;
+ }
+
+ private static bool HasNonEmptyStringOrNumber(JsonElement value)
+ {
+ return value.ValueKind switch
+ {
+ JsonValueKind.Number => true,
+ JsonValueKind.String => !string.IsNullOrWhiteSpace(value.GetString()),
+ _ => false
+ };
+ }
+
+ private static T WithJsonFailureDiagnostics(string label, string jsonPath, Func assertion)
+ {
+ string? content = null;
+
+ try
+ {
+ Assert.True(File.Exists(jsonPath), $"Expected JSON file at {jsonPath}");
+ content = File.ReadAllText(jsonPath);
+ return assertion(content);
+ }
+ catch
+ {
+ WriteJsonDiagnostic(label, jsonPath, content);
+ throw;
+ }
+ }
+
+ private static void WriteJsonDiagnostic(string label, string jsonPath, string? content)
+ {
+ var testContext = TestContext.Current;
+ var header = $"=== {label}: {jsonPath} ===";
+ var body = content ?? "";
+ var footer = $"=== END {label} ===";
+
+ if (testContext.TestOutputHelper is { } outputHelper)
+ {
+ outputHelper.WriteLine(header);
+ outputHelper.WriteLine(body);
+ outputHelper.WriteLine(footer);
+ return;
+ }
+
+ testContext.SendDiagnosticMessage(header);
+ testContext.SendDiagnosticMessage("{0}", body);
+ testContext.SendDiagnosticMessage(footer);
+ }
+
///
/// Reads the VersionPrefix (e.g., "13.3.0") from eng/Versions.props by parsing
/// the MajorVersion, MinorVersion, and PatchVersion MSBuild properties.
@@ -544,7 +736,7 @@ internal static bool IsStabilizedBuild()
return string.Equals(stabilize, "true", StringComparison.OrdinalIgnoreCase);
}
- private static void RegisterCaptureFile(string fileName, string path)
+ internal static void RegisterCaptureFile(string fileName, string path)
{
if (TestContext.Current is null)
{
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/JavaEmptyAppHostTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/JavaEmptyAppHostTemplateTests.cs
deleted file mode 100644
index 9c32d1a5355..00000000000
--- a/tests/Aspire.Cli.EndToEnd.Tests/JavaEmptyAppHostTemplateTests.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.Cli.EndToEnd.Tests.Helpers;
-using Aspire.Cli.Tests.Utils;
-using Hex1b.Automation;
-using Xunit;
-
-namespace Aspire.Cli.EndToEnd.Tests;
-
-///
-/// End-to-end tests for the Java empty AppHost template (aspire-java-empty).
-/// Validates that aspire new creates a working Java AppHost project
-/// and that aspire start runs it successfully.
-///
-public sealed class JavaEmptyAppHostTemplateTests(ITestOutputHelper output)
-{
- [Fact]
- [CaptureWorkspaceOnFailure]
- public async Task CreateAndRunJavaEmptyAppHostProject()
- {
- var repoRoot = CliE2ETestHelpers.GetRepoRoot();
- var strategy = CliInstallStrategy.Detect(output.WriteLine);
- var workspace = TemporaryWorkspace.Create(output);
-
- using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.PolyglotJava, mountDockerSocket: true, workspace: workspace);
-
- var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
-
- var counter = new SequenceCounter();
- var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
-
- await auto.PrepareDockerEnvironmentAsync(counter, workspace);
- await auto.InstallAspireCliAsync(strategy, counter);
- await auto.EnableExperimentalJavaSupportAsync(counter);
-
- await auto.AspireNewAsync("JavaEmptyApp", counter, template: AspireTemplate.JavaEmptyAppHost);
-
- GitIgnoreAssertions.AssertContainsEntry(
- Path.Combine(workspace.WorkspaceRoot.FullName, "JavaEmptyApp"),
- ".aspire/");
-
- await auto.TypeAsync("cd JavaEmptyApp");
- await auto.EnterAsync();
- await auto.WaitForSuccessPromptAsync(counter);
-
- await auto.AspireStartAsync(counter);
- await auto.AspireStopAsync(counter);
-
- await auto.TypeAsync("exit");
- await auto.EnterAsync();
-
- await pendingRun;
- }
-}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/JsReactTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/JsReactTemplateTests.cs
deleted file mode 100644
index d092a928b88..00000000000
--- a/tests/Aspire.Cli.EndToEnd.Tests/JsReactTemplateTests.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.Cli.EndToEnd.Tests.Helpers;
-using Aspire.Cli.Tests.Utils;
-using Hex1b.Automation;
-using Hex1b.Input;
-using Xunit;
-
-namespace Aspire.Cli.EndToEnd.Tests;
-
-///
-/// End-to-end tests for Aspire CLI with ASP.NET Core/React (TypeScript/C#) template.
-/// Each test class runs as a separate CI job for parallelization.
-///
-public sealed class JsReactTemplateTests(ITestOutputHelper output)
-{
- [Fact]
- [CaptureWorkspaceOnFailure]
- public async Task CreateAndRunJsReactProject()
- {
- var repoRoot = CliE2ETestHelpers.GetRepoRoot();
- var strategy = CliInstallStrategy.Detect(output.WriteLine);
- var workspace = TemporaryWorkspace.Create(output);
-
- using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
-
- var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
-
- var counter = new SequenceCounter();
- var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
-
- await auto.PrepareDockerEnvironmentAsync(counter, workspace);
- await auto.InstallAspireCliAsync(strategy, counter);
-
- await auto.AspireNewAsync("AspireJsReactApp", counter, template: AspireTemplate.JsReact, useRedisCache: false);
-
- // Run the project with aspire run
- await auto.TypeAsync("aspire run");
- await auto.EnterAsync();
-
- // Regression test for https://github.com/microsoft/aspire/issues/13971
- await auto.WaitUntilAsync(s =>
- {
- if (s.ContainsText("Select an AppHost to use:"))
- {
- throw new InvalidOperationException(
- "Unexpected apphost selection prompt detected! " +
- "This indicates multiple apphosts were incorrectly detected.");
- }
- return s.ContainsText("Press CTRL+C to stop the AppHost and exit.");
- }, timeout: TimeSpan.FromMinutes(2), description: "Press CTRL+C message (aspire run started)");
-
- await auto.Ctrl().KeyAsync(Hex1bKey.C);
- await auto.WaitForSuccessPromptAsync(counter);
-
- await auto.TypeAsync("exit");
- await auto.EnterAsync();
-
- await pendingRun;
- }
-}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs
deleted file mode 100644
index 9d16f783c7b..00000000000
--- a/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.Cli.EndToEnd.Tests.Helpers;
-using Aspire.Cli.Tests.Utils;
-using Hex1b.Automation;
-using Xunit;
-
-namespace Aspire.Cli.EndToEnd.Tests;
-
-///
-/// End-to-end tests for Aspire CLI with Python/React (FastAPI/Vite) template.
-/// Each test class runs as a separate CI job for parallelization.
-///
-public sealed class PythonReactTemplateTests(ITestOutputHelper output)
-{
- [Fact]
- [CaptureWorkspaceOnFailure]
- public async Task CreateAndRunPythonReactProject()
- {
- var repoRoot = CliE2ETestHelpers.GetRepoRoot();
- var strategy = CliInstallStrategy.Detect(output.WriteLine);
- var workspace = TemporaryWorkspace.Create(output);
-
- using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
-
- var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
-
- var counter = new SequenceCounter();
- var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
-
- await auto.PrepareDockerEnvironmentAsync(counter, workspace);
- await auto.InstallAspireCliAsync(strategy, counter);
-
- // Step 1: Create project using aspire new, selecting the FastAPI/React template
- await auto.AspireNewAsync("AspirePyReactApp", counter, template: AspireTemplate.PythonReact, useRedisCache: false);
-
- GitIgnoreAssertions.AssertContainsEntry(
- Path.Combine(workspace.WorkspaceRoot.FullName, "AspirePyReactApp"),
- ".aspire/");
-
- // Step 2: Navigate into the project directory so config resolution finds the
- // project-level aspire.config.json (which has the packages section).
- // See https://github.com/microsoft/aspire/issues/15623
- await auto.TypeAsync("cd AspirePyReactApp");
- await auto.EnterAsync();
- await auto.WaitForSuccessPromptAsync(counter);
-
- // Step 3: Verify the generated TypeScript AppHost builds successfully.
- await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2));
-
- // Step 4: Start and stop the project
- await auto.AspireStartAsync(counter);
- await auto.AspireStopAsync(counter);
-
- await auto.TypeAsync("exit");
- await auto.EnterAsync();
-
- await pendingRun;
- }
-}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs
index 1a9a812d25b..21707833134 100644
--- a/tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs
+++ b/tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs
@@ -12,14 +12,43 @@
namespace Aspire.Cli.EndToEnd.Tests;
///
-/// End-to-end tests for Aspire CLI run command (creating and launching projects).
-/// Each test class runs as a separate CI job for parallelization.
+/// End-to-end smoke tests for the core Aspire CLI starter scenarios and JSON contracts.
///
public sealed class SmokeTests(ITestOutputHelper output)
{
[CaptureWorkspaceOnFailure]
[Fact]
- public async Task CreateAndRunAspireStarterProject()
+ public async Task CreateAndRunAspireStarterProjectWithBundle()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ await auto.AspireNewAsync("BundleStarterApp", counter);
+
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ [CaptureWorkspaceOnFailure]
+ [Fact]
+ [Trait("azdo-template-coverage", "true")]
+ public async Task CreateAndRunDefaultAspireStarterProject()
{
var repoRoot = CliE2ETestHelpers.GetRepoRoot();
var strategy = CliInstallStrategy.Detect(output.WriteLine);
@@ -39,26 +68,11 @@ public async Task CreateAndRunAspireStarterProject()
// Install the Aspire CLI
await auto.InstallAspireCliAsync(strategy, counter);
- // Create a new project using aspire new
+ // Create a new project using aspire new with the default starter options.
await auto.AspireNewAsync("AspireStarterApp", counter);
- // Run the project with aspire run
- await auto.TypeAsync("aspire run");
- await auto.EnterAsync();
-
- // Regression test for https://github.com/microsoft/aspire/issues/13971
- // If the apphost selection prompt appears, it means multiple apphosts were
- // incorrectly detected (e.g., AppHost.cs was incorrectly treated as a single-file apphost)
- await auto.WaitUntilAsync(s =>
- {
- if (s.ContainsText("Select an AppHost to use:"))
- {
- throw new InvalidOperationException(
- "Unexpected apphost selection prompt detected! " +
- "This indicates multiple apphosts were incorrectly detected.");
- }
- return s.ContainsText("Press CTRL+C to stop the AppHost and exit.");
- }, timeout: TimeSpan.FromMinutes(2), description: "Press CTRL+C message (aspire run started)");
+ // Run the project with aspire run and persist the transcript for failed-run debugging.
+ await auto.AspireRunUntilReadyAsync(workspace);
// Stop the running apphost with Ctrl+C
await auto.Ctrl().KeyAsync(Hex1bKey.C);
@@ -155,6 +169,172 @@ public async Task LatestCliCanStartStableChannelTypeScriptAppHost()
await pendingRun;
}
+ [CaptureWorkspaceOnFailure]
+ [Fact]
+ public async Task CreateAndRunEmptyAppHostProject()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ await auto.AspireNewAsync("AspireEmptyApp", counter, template: AspireTemplate.EmptyAppHost);
+
+ await auto.TypeAsync("cd AspireEmptyApp");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ [CaptureWorkspaceOnFailure]
+ [Fact]
+ [Trait("azdo-template-coverage", "true")]
+ public async Task CreateAndRunDotNetEmptyTemplateProject()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+ await auto.EnableShowAllTemplatesAsync(counter);
+
+ await auto.AspireNewSubcommandAsync("aspire", "DotNetEmptyApp", counter, "--localhost-tld", "false");
+
+ await auto.TypeAsync("cd DotNetEmptyApp");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ [CaptureWorkspaceOnFailure]
+ [Fact]
+ [Trait("azdo-template-coverage", "true")]
+ public async Task CreateAndRunDotNetAppHostTemplateProject()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+ await auto.EnableShowAllTemplatesAsync(counter);
+
+ await auto.AspireNewSubcommandAsync("aspire-apphost", "DotNetAppHost", counter, "--localhost-tld", "false");
+
+ await auto.TypeAsync("cd DotNetAppHost");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ ///
+ /// Creates a starter project, starts it with aspire start --format json,
+ /// validates machine-readable JSON contracts, and verifies the web frontend endpoint
+ /// responds with HTTP 200.
+ ///
+ [CaptureWorkspaceOnFailure]
+ [Fact]
+ public async Task StarterJsonContractsAndEndpointsRespond()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ await auto.AspireNewAsync("EndpointTest", counter, useRedisCache: false);
+
+ await auto.TypeAsync("cd EndpointTest");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.AspireStartAsync(counter);
+ await auto.PersistAspireStartJsonAsync(workspace, counter);
+ CliE2ETestHelpers.AssertAspireStartJsonContract(workspace);
+
+ await auto.TypeAsync("aspire wait webfrontend --status up --timeout 300");
+ await auto.EnterAsync();
+ await auto.WaitUntilTextAsync("is up (running).", timeout: TimeSpan.FromMinutes(6));
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ var psJsonPath = await auto.CaptureJsonOutputAsync(
+ "aspire ps --format json",
+ workspace,
+ counter,
+ "ps.json");
+ CliE2ETestHelpers.AssertPsJsonContract(psJsonPath);
+
+ await auto.AssertResourcesExistAsync(counter, "webfrontend", "apiservice");
+
+ var webfrontendJsonPath = await auto.CaptureJsonOutputAsync(
+ "aspire describe webfrontend --format json",
+ workspace,
+ counter,
+ "webfrontend.json");
+ var webUrl = CliE2ETestHelpers.GetFirstLocalhostUrlFromJsonFile(webfrontendJsonPath);
+ await auto.AssertUrlRespondsAsync(webUrl, "webfrontend", counter);
+
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
private static string GetAppHostSdkVersion(string appHostPath)
{
if (!File.Exists(appHostPath))
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/StarterTemplateBehaviorTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/StarterTemplateBehaviorTests.cs
new file mode 100644
index 00000000000..fd82cce8783
--- /dev/null
+++ b/tests/Aspire.Cli.EndToEnd.Tests/StarterTemplateBehaviorTests.cs
@@ -0,0 +1,360 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+using Aspire.Cli.EndToEnd.Tests.Helpers;
+using Aspire.Cli.Tests.Utils;
+using Hex1b.Automation;
+using Xunit;
+
+namespace Aspire.Cli.EndToEnd.Tests;
+
+///
+/// End-to-end regression tests for starter-template behaviors that were previously covered by the template harness.
+///
+public sealed class StarterTemplateBehaviorTests(ITestOutputHelper output)
+{
+ [Theory]
+ [InlineData("StarterXunitV2", "xUnit.net", "v2")]
+ [InlineData("Starter & With.1", "xUnit.net", "v3mtp")]
+ [InlineData("StarterNUnit", "NUnit", null)]
+ [InlineData("StarterMSTest", "MSTest", null)]
+ [CaptureWorkspaceOnFailure]
+ public async Task StarterTemplateCanGenerateAndRunBuiltInTests(string projectName, string testFramework, string? xunitVersion)
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ var args = new List
+ {
+ "--use-redis-cache", "false",
+ "--test-framework", testFramework
+ };
+
+ if (xunitVersion is not null)
+ {
+ args.Add("--xunit-version");
+ args.Add(xunitVersion);
+ }
+
+ await auto.AspireNewSubcommandAsync("aspire-starter", projectName, counter, [.. args]);
+
+ var projectRoot = Path.Combine(workspace.WorkspaceRoot.FullName, projectName);
+ var testsProjectPath = ResolveGeneratedTestsProjectPath(projectRoot);
+ var testsDirectory = Path.GetDirectoryName(testsProjectPath)
+ ?? throw new InvalidOperationException($"Could not determine generated tests directory for {testsProjectPath}.");
+
+ Assert.True(Directory.Exists(testsDirectory), $"Expected generated tests directory at {testsDirectory}.");
+ Assert.True(File.Exists(testsProjectPath), $"Expected generated tests project at {testsProjectPath}.");
+
+ await auto.TypeAsync($"cd {AspireCliShellCommandHelpers.QuoteBashArg(CliE2ETestHelpers.ToContainerPath(testsDirectory, workspace))}");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.TypeAsync($"dotnet test {Quote(Path.GetFileName(testsProjectPath))}");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(4));
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ [Fact]
+ [CaptureWorkspaceOnFailure]
+ public async Task StarterTemplateWithRedisHasCacheResourceAndApiResponds()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ await auto.AspireNewSubcommandAsync("aspire-starter", "StarterRedisApp", counter, "--use-redis-cache", "true", "--test-framework", "None");
+
+ await auto.TypeAsync("cd StarterRedisApp");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.AspireStartAsync(counter);
+
+ await auto.AssertResourcesExistAsync(counter, "webfrontend", "apiservice", "cache");
+
+ var resourceCommandNamePath = CliE2ETestHelpers.GetWorkspaceFilePath(workspace, "resource-command-name.txt");
+ var quotedResourceCommandNamePath = AspireCliShellCommandHelpers.QuoteBashArg(CliE2ETestHelpers.ToContainerPath(resourceCommandNamePath, workspace));
+ CliE2ETestHelpers.RegisterCaptureFile("resource-command-name.txt", resourceCommandNamePath);
+
+ await auto.TypeAsync($"if aspire resource --help >/dev/null 2>&1; then echo resource > {quotedResourceCommandNamePath}; elif aspire command --help >/dev/null 2>&1; then echo command > {quotedResourceCommandNamePath}; else echo none > {quotedResourceCommandNamePath}; fi");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ var resourceCommandName = File.ReadAllText(resourceCommandNamePath).Trim();
+ if (resourceCommandName is "resource" or "command")
+ {
+ await auto.TypeAsync($"aspire {resourceCommandName} webfrontend start");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptFailFastAsync(counter, TimeSpan.FromMinutes(2));
+
+ var webfrontendJsonPath = CliE2ETestHelpers.GetWorkspaceFilePath(workspace, "webfrontend.json");
+ var containerWebfrontendJsonPath = CliE2ETestHelpers.ToContainerPath(webfrontendJsonPath, workspace);
+ var quotedContainerWebfrontendJsonPath = AspireCliShellCommandHelpers.QuoteBashArg(containerWebfrontendJsonPath);
+ CliE2ETestHelpers.RegisterCaptureFile("webfrontend.json", webfrontendJsonPath);
+
+ await auto.TypeAsync(
+ $"for i in $(seq 1 60); do " +
+ $"aspire describe webfrontend --format json > {quotedContainerWebfrontendJsonPath}; " +
+ $"if grep -q '\"url\"[[:space:]]*:' {quotedContainerWebfrontendJsonPath}; then break; fi; " +
+ $"sleep 2; " +
+ $"done; if ! grep -q '\"url\"[[:space:]]*:' {quotedContainerWebfrontendJsonPath}; then cat {quotedContainerWebfrontendJsonPath}; false; fi");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptFailFastAsync(counter, TimeSpan.FromMinutes(3));
+
+ var webUrl = CliE2ETestHelpers.GetFirstLocalhostUrlFromJsonFile(webfrontendJsonPath);
+ await auto.AssertUrlRespondsAsync(webUrl, "redis-webfrontend", counter);
+ }
+ else
+ {
+ Assert.Fail($"Expected 'resource' or 'command' subcommand support; the installed Aspire CLI reported: '{resourceCommandName}'.");
+ }
+
+ var apiJsonPath = await auto.CaptureJsonOutputAsync(
+ "aspire describe apiservice --format json",
+ workspace,
+ counter,
+ "apiservice.json");
+ var apiUrl = GetFirstApiServiceUrl(apiJsonPath);
+
+ var hostWeatherPath = CliE2ETestHelpers.GetWorkspaceFilePath(workspace, "weather.json");
+ var containerWeatherPath = CliE2ETestHelpers.ToContainerPath(hostWeatherPath, workspace);
+ CliE2ETestHelpers.RegisterCaptureFile("weather.json", hostWeatherPath);
+
+ await auto.TypeAsync("aspire wait apiservice --status up --timeout 300");
+ await auto.EnterAsync();
+ await auto.WaitUntilTextAsync("is up (running).", timeout: TimeSpan.FromMinutes(6));
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.TypeAsync($"curl -ksSL {Quote($"{apiUrl.TrimEnd('/')}/weatherforecast")} > {Quote(containerWeatherPath)}");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30));
+
+ using var document = JsonDocument.Parse(File.ReadAllText(hostWeatherPath));
+ Assert.True(document.RootElement.ValueKind is JsonValueKind.Array, $"Expected JSON array in {hostWeatherPath}.");
+ Assert.Equal(5, document.RootElement.GetArrayLength());
+
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ private static string GetFirstApiServiceUrl(string jsonPath)
+ {
+ using var document = JsonDocument.Parse(File.ReadAllText(jsonPath));
+ var resources = document.RootElement.GetProperty("resources");
+ foreach (var resource in resources.EnumerateArray())
+ {
+ if (!resource.TryGetProperty("displayName", out var displayName) ||
+ !string.Equals(displayName.GetString(), "apiservice", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ if (!resource.TryGetProperty("urls", out var urls))
+ {
+ continue;
+ }
+
+ foreach (var url in urls.EnumerateArray())
+ {
+ if (!url.TryGetProperty("url", out var value))
+ {
+ continue;
+ }
+
+ var urlString = value.GetString();
+ if (!string.IsNullOrWhiteSpace(urlString))
+ {
+ return urlString;
+ }
+ }
+ }
+
+ throw new InvalidOperationException($"Expected an api service URL in {jsonPath}.");
+ }
+
+ private static string ResolveGeneratedTestsProjectPath(string projectRoot)
+ {
+ Assert.True(Directory.Exists(projectRoot), $"Expected generated project directory at {projectRoot}.");
+
+ var candidates = Directory.EnumerateFiles(projectRoot, "*.Tests.csproj", SearchOption.AllDirectories)
+ .OrderBy(static path => path, StringComparer.Ordinal)
+ .ToArray();
+
+ return Assert.Single(candidates);
+ }
+
+ private static string Quote(string value) => $"\"{value}\"";
+}
+
+public sealed class SupportProjectTemplateBehaviorTests(ITestOutputHelper output)
+{
+ [Theory]
+ [InlineData("aspire-xunit", "SupportTemplate.Xunit", null)]
+ [InlineData("aspire-xunit", "SupportTemplate.XunitV3", "v3")]
+ [InlineData("aspire-nunit", "SupportTemplate.NUnit", null)]
+ [InlineData("aspire-mstest", "SupportTemplate.MSTest", null)]
+ [CaptureWorkspaceOnFailure]
+ public async Task SupportProjectTemplatesRunAgainstGeneratedAppHost(string templateName, string projectName, string? xunitVersion)
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+ await DotNetTemplateBehaviorTestHelpers.CreateTemplateBootstrapAsync(auto, counter);
+
+ string[] args = xunitVersion is null ? [] : ["--xunit-version", xunitVersion];
+ var testProjectDirectory = await CreateSupportTemplateInBootstrapAsync(auto, counter, workspace, templateName, projectName, args);
+
+ const string appHostProjectName = "TemplateBootstrap.AppHost";
+ var testProjectPath = Path.Combine(testProjectDirectory, $"{projectName}.csproj");
+
+ Assert.True(Directory.Exists(testProjectDirectory), $"Expected generated tests directory at {testProjectDirectory}.");
+ Assert.True(File.Exists(testProjectPath), $"Expected generated tests project at {testProjectPath}.");
+
+ PrepareSupportProject(testProjectPath, appHostProjectName);
+ PrepareSupportTestSource(testProjectDirectory, templateName, appHostProjectName);
+
+ await auto.TypeAsync($"cd {CliE2ETestHelpers.ToContainerPath(testProjectDirectory, workspace)}");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.TypeAsync("dotnet test");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(6));
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ private static async Task CreateSupportTemplateInBootstrapAsync(
+ Hex1bTerminalAutomator auto,
+ SequenceCounter counter,
+ TemporaryWorkspace workspace,
+ string templateName,
+ string projectName,
+ IReadOnlyList extraArgs)
+ {
+ var commandParts = new List
+ {
+ "dotnet",
+ "new",
+ templateName,
+ "-n",
+ Quote(projectName),
+ "-o",
+ Quote($"./{projectName}")
+ };
+
+ commandParts.AddRange(extraArgs);
+
+ await auto.TypeAsync(string.Join(" ", commandParts));
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2));
+
+ return DotNetTemplateBehaviorTestHelpers.ResolveGeneratedTemplateDirectory(workspace, projectName, "IntegrationTest1.cs");
+ }
+
+ private static void PrepareSupportProject(string testProjectPath, string appHostProjectName)
+ {
+ var document = XDocument.Load(testProjectPath);
+ var project = document.Root ?? throw new InvalidOperationException($"Project root not found in {testProjectPath}.");
+
+ project.Add(new XElement("ItemGroup",
+ new XElement("ProjectReference",
+ new XAttribute("Include", Path.Combine("..", appHostProjectName, $"{appHostProjectName}.csproj")))));
+
+ document.Save(testProjectPath);
+ }
+
+ private static void PrepareSupportTestSource(string testProjectDirectory, string templateName, string appHostProjectName)
+ {
+ var testSourcePath = Path.Combine(testProjectDirectory, "IntegrationTest1.cs");
+ Assert.True(File.Exists(testSourcePath), $"Expected generated test source at {testSourcePath}.");
+
+ var marker = templateName switch
+ {
+ "aspire-nunit" => "[Test]",
+ "aspire-mstest" => "[TestMethod]",
+ "aspire-xunit" => "[Fact]",
+ _ => throw new ArgumentOutOfRangeException(nameof(templateName), templateName, "Unknown support template name.")
+ };
+
+ var uncomment = false;
+ var lines = File.ReadAllLines(testSourcePath);
+ for (var i = 0; i < lines.Length; i++)
+ {
+ if (!uncomment && lines[i].Contains(marker, StringComparison.Ordinal))
+ {
+ uncomment = true;
+ }
+
+ if (uncomment)
+ {
+ lines[i] = UncommentLine(lines[i]);
+ }
+ }
+
+ var generatedAppHostClassName = Regex.Replace(appHostProjectName, "[^A-Za-z0-9_]", "_");
+ var updatedSource = string.Join(Environment.NewLine, lines)
+ .Replace("Projects.MyAspireApp_AppHost", $"Projects.{generatedAppHostClassName}", StringComparison.Ordinal);
+
+ File.WriteAllText(testSourcePath, updatedSource + Environment.NewLine);
+ }
+
+ private static string UncommentLine(string line)
+ {
+ var trimmed = line.TrimStart();
+ if (!trimmed.StartsWith("//", StringComparison.Ordinal))
+ {
+ return line;
+ }
+
+ var indentation = line[..(line.Length - trimmed.Length)];
+ var uncommented = trimmed.Length > 2 && trimmed[2] == ' ' ? trimmed[3..] : trimmed[2..];
+ return indentation + uncommented;
+ }
+
+ private static string Quote(string value) => $"\"{value}\"";
+}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TemplateVariantSmokeTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TemplateVariantSmokeTests.cs
new file mode 100644
index 00000000000..9d6f864e8b0
--- /dev/null
+++ b/tests/Aspire.Cli.EndToEnd.Tests/TemplateVariantSmokeTests.cs
@@ -0,0 +1,219 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.EndToEnd.Tests.Helpers;
+using Aspire.Cli.Tests.Utils;
+using Hex1b.Automation;
+using Hex1b.Input;
+using Xunit;
+
+namespace Aspire.Cli.EndToEnd.Tests;
+
+///
+/// End-to-end smoke tests for non-default Aspire CLI template variants.
+///
+public sealed class TemplateVariantSmokeTests(ITestOutputHelper output)
+{
+ [Fact]
+ [CaptureWorkspaceOnFailure]
+ public async Task CreateAndRunJavaEmptyAppHostProject()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(
+ repoRoot,
+ strategy,
+ output,
+ variant: CliE2ETestHelpers.DockerfileVariant.PolyglotJava,
+ mountDockerSocket: true,
+ workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+ await auto.EnableExperimentalJavaSupportAsync(counter);
+
+ await auto.AspireNewAsync("JavaEmptyApp", counter, template: AspireTemplate.JavaEmptyAppHost);
+
+ GitIgnoreAssertions.AssertContainsEntry(
+ Path.Combine(workspace.WorkspaceRoot.FullName, "JavaEmptyApp"),
+ ".aspire/");
+
+ await auto.TypeAsync("cd JavaEmptyApp");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ [Fact]
+ [CaptureWorkspaceOnFailure]
+ public async Task CreateAndRunJsReactProject()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ await auto.AspireNewAsync("AspireJsReactApp", counter, template: AspireTemplate.JsReact, useRedisCache: false);
+
+ await auto.AspireRunUntilReadyAsync(workspace);
+
+ await auto.Ctrl().KeyAsync(Hex1bKey.C);
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ [Fact]
+ [CaptureWorkspaceOnFailure]
+ public async Task CreateAndRunPythonReactProject()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ await auto.AspireNewAsync("AspirePyReactApp", counter, template: AspireTemplate.PythonReact, useRedisCache: false);
+
+ GitIgnoreAssertions.AssertContainsEntry(
+ Path.Combine(workspace.WorkspaceRoot.FullName, "AspirePyReactApp"),
+ ".aspire/");
+
+ await auto.TypeAsync("cd AspirePyReactApp");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2));
+
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ [Fact]
+ [CaptureWorkspaceOnFailure]
+ public async Task CreateAndRunTypeScriptEmptyAppHostProject()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ await auto.AspireNewAsync("TsEmptyApp", counter, template: AspireTemplate.TypeScriptEmptyAppHost);
+
+ GitIgnoreAssertions.AssertContainsEntry(
+ Path.Combine(workspace.WorkspaceRoot.FullName, "TsEmptyApp"),
+ ".aspire/");
+
+ await auto.TypeAsync("cd TsEmptyApp");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2));
+
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+
+ [Fact]
+ [CaptureWorkspaceOnFailure]
+ public async Task CreateAndRunTypeScriptStarterProject()
+ {
+ var repoRoot = CliE2ETestHelpers.GetRepoRoot();
+ var strategy = CliInstallStrategy.Detect(output.WriteLine);
+ var workspace = TemporaryWorkspace.Create(output);
+
+ using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
+
+ var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
+
+ var counter = new SequenceCounter();
+ var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
+
+ await auto.PrepareDockerEnvironmentAsync(counter, workspace);
+ await auto.InstallAspireCliAsync(strategy, counter);
+
+ await auto.AspireNewAsync("TsStarterApp", counter, template: AspireTemplate.ExpressReact);
+
+ var projectRoot = Path.Combine(workspace.WorkspaceRoot.FullName, "TsStarterApp");
+ GitIgnoreAssertions.AssertContainsEntry(projectRoot, ".aspire/");
+ var modulesDir = Path.Combine(projectRoot, ".modules");
+
+ if (!Directory.Exists(modulesDir))
+ {
+ Assert.Fail($".modules directory was not created at {modulesDir}");
+ }
+
+ var aspireModulePath = Path.Combine(modulesDir, "aspire.ts");
+ if (!File.Exists(aspireModulePath))
+ {
+ Assert.Fail($"Expected generated file not found: {aspireModulePath}");
+ }
+
+ await auto.TypeAsync("cd TsStarterApp");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2));
+
+ await auto.AspireStartAsync(counter);
+ await auto.AspireStopAsync(counter);
+
+ await auto.TypeAsync("exit");
+ await auto.EnterAsync();
+
+ await pendingRun;
+ }
+}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptEmptyAppHostTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptEmptyAppHostTemplateTests.cs
deleted file mode 100644
index d76991579d5..00000000000
--- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptEmptyAppHostTemplateTests.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.Cli.EndToEnd.Tests.Helpers;
-using Aspire.Cli.Tests.Utils;
-using Hex1b.Automation;
-using Xunit;
-
-namespace Aspire.Cli.EndToEnd.Tests;
-
-///
-/// End-to-end tests for the TypeScript Empty AppHost template (aspire-ts-empty).
-/// Validates that aspire new creates a working TypeScript AppHost project
-/// and that aspire start runs it successfully.
-///
-public sealed class TypeScriptEmptyAppHostTemplateTests(ITestOutputHelper output)
-{
- [Fact]
- [CaptureWorkspaceOnFailure]
- public async Task CreateAndRunTypeScriptEmptyAppHostProject()
- {
- var repoRoot = CliE2ETestHelpers.GetRepoRoot();
- var strategy = CliInstallStrategy.Detect(output.WriteLine);
- var workspace = TemporaryWorkspace.Create(output);
-
- using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
-
- var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
-
- var counter = new SequenceCounter();
- var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
-
- await auto.PrepareDockerEnvironmentAsync(counter, workspace);
- await auto.InstallAspireCliAsync(strategy, counter);
-
- await auto.AspireNewAsync("TsEmptyApp", counter, template: AspireTemplate.TypeScriptEmptyAppHost);
-
- GitIgnoreAssertions.AssertContainsEntry(
- Path.Combine(workspace.WorkspaceRoot.FullName, "TsEmptyApp"),
- ".aspire/");
-
- // Start the empty TypeScript AppHost to verify the scaffolded project works
- await auto.TypeAsync("cd TsEmptyApp");
- await auto.EnterAsync();
- await auto.WaitForSuccessPromptAsync(counter);
-
- await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2));
-
- await auto.AspireStartAsync(counter);
- await auto.AspireStopAsync(counter);
-
- await auto.TypeAsync("exit");
- await auto.EnterAsync();
-
- await pendingRun;
- }
-}
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptReusablePackageTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptReusablePackageTests.cs
index 2da46bd52aa..0934a4e8db2 100644
--- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptReusablePackageTests.cs
+++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptReusablePackageTests.cs
@@ -64,31 +64,17 @@ public async Task RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes()
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);
- await auto.TypeAsync("aspire restore");
- await auto.EnterAsync();
- await auto.WaitUntilTextAsync("SDK code restored successfully", timeout: TimeSpan.FromMinutes(3));
- await auto.WaitForSuccessPromptAsync(counter);
+ await auto.AspireRestoreAndTypeCheckTypeScriptAsync(counter, typeCheckCommand: "npx --no-install tsc --noEmit");
var helperModulesDirectory = Path.Combine(helperDirectory.FullName, ".modules");
Assert.True(Directory.Exists(helperModulesDirectory), $".modules directory was not created for helper package at {helperModulesDirectory}");
Assert.Contains("addRedis", File.ReadAllText(Path.Combine(helperModulesDirectory, "aspire.ts")));
- await auto.TypeAsync("npx tsc --noEmit");
- await auto.EnterAsync();
- await auto.WaitForSuccessPromptFailFastAsync(counter, TimeSpan.FromMinutes(2));
-
await auto.TypeAsync($"cd {CliE2ETestHelpers.ToContainerPath(appDirectory.FullName, workspace)}");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);
- await auto.TypeAsync("aspire restore");
- await auto.EnterAsync();
- await auto.WaitUntilTextAsync("SDK code restored successfully", timeout: TimeSpan.FromMinutes(3));
- await auto.WaitForSuccessPromptAsync(counter);
-
- await auto.TypeAsync("npx tsc --noEmit");
- await auto.EnterAsync();
- await auto.WaitForSuccessPromptFailFastAsync(counter, TimeSpan.FromMinutes(2));
+ await auto.AspireRestoreAndTypeCheckTypeScriptAsync(counter, typeCheckCommand: "npx --no-install tsc --noEmit");
await auto.TypeAsync("exit");
await auto.EnterAsync();
diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs
deleted file mode 100644
index c0ae40c8d8f..00000000000
--- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.Cli.EndToEnd.Tests.Helpers;
-using Aspire.Cli.Tests.Utils;
-using Hex1b.Automation;
-using Xunit;
-
-namespace Aspire.Cli.EndToEnd.Tests;
-
-///
-/// End-to-end tests for the TypeScript Express/React starter template (aspire-ts-starter).
-/// Validates that aspire new creates a working Express API + React frontend project
-/// and that aspire run starts it successfully.
-///
-public sealed class TypeScriptStarterTemplateTests(ITestOutputHelper output)
-{
- [CaptureWorkspaceOnFailure]
- [Fact]
- public async Task CreateAndRunTypeScriptStarterProject()
- {
- var repoRoot = CliE2ETestHelpers.GetRepoRoot();
- var strategy = CliInstallStrategy.Detect(output.WriteLine);
- var workspace = TemporaryWorkspace.Create(output);
-
- using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace);
-
- var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
-
- var counter = new SequenceCounter();
- var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
-
- await auto.PrepareDockerEnvironmentAsync(counter, workspace);
- await auto.InstallAspireCliAsync(strategy, counter);
-
- // Step 1: Create project using aspire new, selecting the Express/React template
- await auto.AspireNewAsync("TsStarterApp", counter, template: AspireTemplate.ExpressReact);
-
- // Step 1.5: Verify starter creation also restored the generated TypeScript SDK.
- var projectRoot = Path.Combine(workspace.WorkspaceRoot.FullName, "TsStarterApp");
- GitIgnoreAssertions.AssertContainsEntry(projectRoot, ".aspire/");
- var modulesDir = Path.Combine(projectRoot, ".modules");
-
- if (!Directory.Exists(modulesDir))
- {
- throw new InvalidOperationException($".modules directory was not created at {modulesDir}");
- }
-
- var aspireModulePath = Path.Combine(modulesDir, "aspire.ts");
- if (!File.Exists(aspireModulePath))
- {
- throw new InvalidOperationException($"Expected generated file not found: {aspireModulePath}");
- }
-
- // Step 2: Navigate into the project and start it
- await auto.TypeAsync("cd TsStarterApp");
- await auto.EnterAsync();
- await auto.WaitForSuccessPromptAsync(counter);
-
- await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2));
-
- await auto.AspireStartAsync(counter);
- await auto.AspireStopAsync(counter);
-
- await auto.TypeAsync("exit");
- await auto.EnterAsync();
-
- await pendingRun;
- }
-}
diff --git a/tests/Aspire.Cli.Tests/DotNet/ProcessExecutionTests.cs b/tests/Aspire.Cli.Tests/DotNet/ProcessExecutionTests.cs
index e2641fb04e3..0232719ff60 100644
--- a/tests/Aspire.Cli.Tests/DotNet/ProcessExecutionTests.cs
+++ b/tests/Aspire.Cli.Tests/DotNet/ProcessExecutionTests.cs
@@ -131,7 +131,11 @@ public async Task WaitForExitAsync_AllowsBufferedTailOutputAfterLongIdlePeriod()
Assert.True(execution.Start());
- var exitCode = await execution.WaitForExitAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(30));
+ var waitForExitTimeout = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? TimeSpan.FromSeconds(60)
+ : TimeSpan.FromSeconds(30);
+
+ var exitCode = await execution.WaitForExitAsync(CancellationToken.None).WaitAsync(waitForExitTimeout);
await releaseTask.WaitAsync(TimeSpan.FromSeconds(1));
Assert.Equal(0, exitCode);
diff --git a/tests/Aspire.EndToEnd.Tests/Directory.Build.props b/tests/Aspire.EndToEnd.Tests/Directory.Build.props
index 0f87452ee44..d42fd1eb754 100644
--- a/tests/Aspire.EndToEnd.Tests/Directory.Build.props
+++ b/tests/Aspire.EndToEnd.Tests/Directory.Build.props
@@ -1,11 +1,6 @@
-
-
- true
-
-
- $([MSBuild]::NormalizeDirectory($([System.IO.Path]::GetTempPath()), $([System.IO.Path]::GetRandomFileName())))
+ $([MSBuild]::NormalizeDirectory($([System.IO.Path]::GetTempPath()), $([System.IO.Path]::GetRandomFileName())))
diff --git a/tests/Aspire.Templates.Tests/AppHostTemplateTests.cs b/tests/Aspire.Templates.Tests/AppHostTemplateTests.cs
deleted file mode 100644
index dab50938b6a..00000000000
--- a/tests/Aspire.Templates.Tests/AppHostTemplateTests.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Xunit;
-using System.Text.RegularExpressions;
-
-namespace Aspire.Templates.Tests;
-
-public partial class AppHostTemplateTests : TemplateTestsBase
-{
- public AppHostTemplateTests(ITestOutputHelper testOutput)
- : base(testOutput)
- {
- }
-
- [Fact]
- public async Task EnsureProjectsReferencing8_1_0AppHostWithNewerWorkloadCanBuild()
- {
- string projectId = "aspire-can-reference-8.1.0";
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(
- projectId,
- "aspire-apphost",
- _testOutput,
- BuildEnvironment.ForDefaultFramework,
- addEndpointsHook: false);
-
- var projectPath = Path.Combine(project.RootDir, $"{projectId}.csproj");
-
- // Replace the reference to Aspire.Hosting.AppHost with version 8.1.0
- var newContents = AppHostPackageReferenceRegex().Replace(File.ReadAllText(projectPath), @"$1""8.1.0""");
-
- File.WriteAllText(projectPath, newContents);
-
- // Ensure project builds successfully
- await project.BuildAsync(workingDirectory: project.RootDir);
- }
-
- [GeneratedRegex(@"(PackageReference\s.*""Aspire\.Hosting\.AppHost.*Version=)""[^""]+""")]
- private static partial Regex AppHostPackageReferenceRegex();
-}
diff --git a/tests/Aspire.Templates.Tests/Aspire.Templates.Tests.csproj b/tests/Aspire.Templates.Tests/Aspire.Templates.Tests.csproj
deleted file mode 100644
index 567c2635495..00000000000
--- a/tests/Aspire.Templates.Tests/Aspire.Templates.Tests.csproj
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
- $(DefaultTargetFramework)
-
- true
- true
-
- xunit.runner.json
- $(TestArchiveTestsDirForTemplateTests)
-
- true
- true
- true
-
- true
- Aspire.Templates.Tests
-
-
- true
- true
- true
-
-
- false
- false
-
-
- true
-
-
- 20m
- 15m
-
-
- $(NoWarn);xUnit1051
-
-
- true
- $(TestRunnerAdditionalArguments) --filter-trait category=basic-build
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tests/Aspire.Templates.Tests/BuildAndRunStarterTemplateBuiltInTest.cs b/tests/Aspire.Templates.Tests/BuildAndRunStarterTemplateBuiltInTest.cs
deleted file mode 100644
index b5cb174babf..00000000000
--- a/tests/Aspire.Templates.Tests/BuildAndRunStarterTemplateBuiltInTest.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.TestUtilities;
-using Xunit;
-
-namespace Aspire.Templates.Tests;
-
-public class BuildAndRunStarterTemplateBuiltInTest : TemplateTestsBase
-{
- public BuildAndRunStarterTemplateBuiltInTest(ITestOutputHelper testOutput)
- : base(testOutput)
- {}
-
- public static TheoryData TestFrameworkTypeWithConfig()
- {
- var data = new TheoryData();
- foreach (var testType in TemplateTestsBase.TestFrameworkTypes)
- {
- data.Add("Debug", testType);
- if (!PlatformDetection.IsRunningPRValidation)
- {
- data.Add("Release", testType);
- }
- }
- return data;
- }
-
- [Theory]
- [MemberData(nameof(TestFrameworkTypeWithConfig))]
- [RequiresFeature(TestFeature.Docker | TestFeature.SSLCertificate)]
- [Trait("category", "basic-build")]
- public async Task BuildAndRunStarterTemplateBuiltInTest_Test(string config, string testType)
- {
- string id = TemplateTestsBase.GetNewProjectId(prefix: $"starter test.{config}-{testType.Replace(".", "_")}");
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(
- id,
- "aspire-starter",
- _testOutput,
- buildEnvironment: BuildEnvironment.ForDefaultFramework,
- extraArgs: $"-t {testType}");
-
- await TemplateTestsBase.AssertTestProjectRunAsync(project.TestsProjectDirectory, testType, _testOutput, config);
- }
-}
diff --git a/tests/Aspire.Templates.Tests/BuildAndRunTemplateTests.cs b/tests/Aspire.Templates.Tests/BuildAndRunTemplateTests.cs
deleted file mode 100644
index 7c4fc4ce163..00000000000
--- a/tests/Aspire.Templates.Tests/BuildAndRunTemplateTests.cs
+++ /dev/null
@@ -1,250 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Text.RegularExpressions;
-using Aspire.Hosting;
-using Aspire.TestUtilities;
-using Xunit;
-
-namespace Aspire.Templates.Tests;
-
-// This class has tests that start projects on their own
-public partial class BuildAndRunTemplateTests : TemplateTestsBase
-{
- public BuildAndRunTemplateTests(ITestOutputHelper testOutput)
- : base(testOutput)
- {}
-
- public static TheoryData BuildConfigurationsForTestData()
- {
- var data = new TheoryData() { "Debug" };
- if (!PlatformDetection.IsRunningPRValidation)
- {
- data.Add("Release");
- }
-
- return data;
- }
-
- [Theory]
- [MemberData(nameof(BuildConfigurationsForTestData))]
- [RequiresFeature(TestFeature.SSLCertificate), RequiresFeature(TestFeature.Playwright)]
- [Trait("category", "basic-build")]
- [OuterloopTest("playwright test")]
- public async Task BuildAndRunAspireTemplate(string config)
- {
- string id = GetNewProjectId(prefix: $"aspire_{config}");
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(id, "aspire", _testOutput, buildEnvironment: BuildEnvironment.ForDefaultFramework);
-
- await project.BuildAsync(extraBuildArgs: [$"-c {config}"]);
- await project.StartAppHostAsync(extraArgs: [$"-c {config}"]);
-
- await using var context = await CreateNewBrowserContextAsync();
- var page = await project.OpenDashboardPageAsync(context);
- await CheckDashboardHasResourcesAsync(page, [], logPath: project.LogPath);
- }
-
- [Fact]
- public async Task BuildAndRunAspireTemplateWithCentralPackageManagement()
- {
- string id = GetNewProjectId(prefix: "aspire_CPM");
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(
- id,
- "aspire",
- _testOutput,
- buildEnvironment: BuildEnvironment.ForDefaultFramework);
-
- string version = ExtractVersionFromSdkAndAddRedisPackageReference(project);
-
- CreateCPMFile(project, version);
-
- await project.BuildAsync();
- await project.StartAppHostAsync();
- await project.StopAppHostAsync();
-
- static string ExtractVersionFromSdkAndAddRedisPackageReference(AspireProject project)
- {
- var projectName = Directory.GetFiles(project.AppHostProjectDirectory, "*.csproj").FirstOrDefault();
- Assert.False(string.IsNullOrEmpty(projectName));
-
- var projectContents = File.ReadAllText(projectName);
-
- var match = ProjectSdkVersionRegex().Match(projectContents);
-
- File.WriteAllText(
- projectName,
- ProjectClosingTagRegex().Replace(projectContents,
- """
-
-
-
-
- """)
- );
-
- var version = match.Groups[1].Value;
- Assert.NotNull(version);
-
- return version;
- }
-
- static void CreateCPMFile(AspireProject project, string version)
- {
- var cpmFilePath = Path.Combine(project.RootDir, "Directory.Packages.props");
- var cpmContent = $"""
-
-
- true
-
- NU1507;$(NoWarn)
-
-
-
-
-
- """;
-
- File.WriteAllText(cpmFilePath, cpmContent);
- }
- }
-
- [Theory]
- [InlineData(true)]
- [InlineData(false)]
- public async Task BuildAndRunAspireTemplateWithExplicitSdkReference(bool includeAspireHostingAppHostPackageReference)
- {
- string id = GetNewProjectId(prefix: "aspire_explicit_SDK");
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(
- id,
- "aspire",
- _testOutput,
- buildEnvironment: BuildEnvironment.ForDefaultFramework);
-
- UpdateSdkReferencesAndAddAppHostPackageReference(project, includeAspireHostingAppHostPackageReference);
-
- await project.BuildAsync();
- await project.StartAppHostAsync();
- await project.StopAppHostAsync();
-
- static void UpdateSdkReferencesAndAddAppHostPackageReference(AspireProject project, bool addPackageRef)
- {
- var projectName = Directory.GetFiles(project.AppHostProjectDirectory, "*.csproj").FirstOrDefault();
- Assert.False(string.IsNullOrEmpty(projectName));
-
- var projectContents = File.ReadAllText(projectName);
-
- var match = ProjectSdkVersionRegex().Match(projectContents);
- var version = match.Groups[1].Value;
- Assert.NotNull(version);
-
- File.WriteAllText(
- projectName,
- ProjectSdkVersionRegex().Replace(projectContents,
- $"""
-
-
- """)
- );
-
- if (addPackageRef)
- {
- File.WriteAllText(
- projectName,
- ProjectClosingTagRegex().Replace(projectContents,
- $"""
-
-
-
-
- """));
- }
- }
- }
-
- [Theory]
- [MemberData(nameof(BuildConfigurationsForTestData))]
- [RequiresFeature(TestFeature.SSLCertificate), RequiresFeature(TestFeature.Playwright)]
- [Trait("category", "basic-build")]
- [OuterloopTest("playwright test")]
- public async Task StarterTemplateNewAndRunWithoutExplicitBuild(string config)
- {
- var id = GetNewProjectId(prefix: $"aspire_starter_run_{config}");
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(
- id,
- "aspire-starter",
- _testOutput,
- buildEnvironment: BuildEnvironment.ForDefaultFramework);
-
- await using var context = await CreateNewBrowserContextAsync();
- await AssertStarterTemplateRunAsync(context, project, config, _testOutput);
- }
-
- [Fact]
- [RequiresFeature(TestFeature.Playwright)]
- [OuterloopTest("playwright test")]
- [ActiveIssue("https://github.com/microsoft/aspire/issues/9155", typeof(PlatformDetection), nameof(PlatformDetection.IsMacOS))]
- public async Task ProjectWithNoHTTPSRequiresExplicitOverrideWithEnvironmentVariable()
- {
- string id = GetNewProjectId(prefix: "aspire");
- // Using a copy so envvars can be modified without affecting other tests
- var testSpecificBuildEnvironment = new BuildEnvironment(BuildEnvironment.ForDefaultFramework);
-
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(
- id,
- "aspire",
- _testOutput,
- buildEnvironment: testSpecificBuildEnvironment,
- extraArgs: "--no-https");
-
- await project.BuildAsync();
- using var buildCmd = new DotNetCommand(_testOutput, buildEnv: testSpecificBuildEnvironment, label: "first-run")
- .WithWorkingDirectory(project.AppHostProjectDirectory);
-
- var res = await buildCmd.ExecuteAsync("run");
- Assert.True(res.ExitCode != 0, $"Expected the app run to fail");
- Assert.Contains($"setting must be an https address unless the '{KnownConfigNames.AllowUnsecuredTransport}'", res.Output);
-
- // Run with the environment variable set
- testSpecificBuildEnvironment.EnvVars[KnownConfigNames.AllowUnsecuredTransport] = "true";
- await project.StartAppHostAsync();
-
- await using var context = await CreateNewBrowserContextAsync();
- var page = await project.OpenDashboardPageAsync(context);
- await CheckDashboardHasResourcesAsync(page, [], logPath: project.LogPath);
- }
-
- [Theory]
- [InlineData("9.*-*")]
- [InlineData("[9.0.0]")]
- [Trait("category", "basic-build")]
- public async Task CreateAndModifyAspireAppHostTemplate(string version)
- {
- string id = GetNewProjectId(prefix: $"aspire_apphost_{version.Replace("*", "wildcard").Replace("[", "").Replace("]", "")}");
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(id, "aspire-apphost", _testOutput, buildEnvironment: BuildEnvironment.ForDefaultFramework, addEndpointsHook: false);
-
- ModifyProjectFile(project, version);
-
- await project.BuildAsync(workingDirectory: project.RootDir);
-
- static void ModifyProjectFile(AspireProject project, string version)
- {
- var projectName = Directory.GetFiles(project.RootDir, "*.csproj").FirstOrDefault();
- Assert.False(string.IsNullOrEmpty(projectName));
-
- var projectContents = File.ReadAllText(projectName);
-
- var modifiedContents = AppHostVersionRegex().Replace(projectContents, $@"");
-
- File.WriteAllText(projectName, modifiedContents);
- }
- }
-
- [GeneratedRegex(@"")]
- private static partial Regex AppHostVersionRegex();
-
- [GeneratedRegex(@"")]
- private static partial Regex ProjectClosingTagRegex();
-
- [GeneratedRegex(@"")]
- private static partial Regex ProjectSdkVersionRegex();
-}
diff --git a/tests/Aspire.Templates.Tests/Directory.Build.props b/tests/Aspire.Templates.Tests/Directory.Build.props
deleted file mode 100644
index 853e73e2ac5..00000000000
--- a/tests/Aspire.Templates.Tests/Directory.Build.props
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
- true
-
-
-
-
diff --git a/tests/Aspire.Templates.Tests/EmptyTemplateRunFixture.cs b/tests/Aspire.Templates.Tests/EmptyTemplateRunFixture.cs
deleted file mode 100644
index a11f2399a6e..00000000000
--- a/tests/Aspire.Templates.Tests/EmptyTemplateRunFixture.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Xunit.Sdk;
-
-namespace Aspire.Templates.Tests;
-
-///
-/// This fixture ensures the TestProject.AppHost application is started before a test is executed.
-///
-/// Represents the the IntegrationServiceA project in the test application used to send HTTP requests
-/// to the project's endpoints.
-///
-public sealed class EmptyTemplateRunFixture : TemplateAppFixture
-{
- public EmptyTemplateRunFixture(IMessageSink diagnosticMessageSink)
- : base(diagnosticMessageSink, "aspire")
- {
- }
-}
diff --git a/tests/Aspire.Templates.Tests/EmptyTemplateRunTests.cs b/tests/Aspire.Templates.Tests/EmptyTemplateRunTests.cs
deleted file mode 100644
index 1397691e086..00000000000
--- a/tests/Aspire.Templates.Tests/EmptyTemplateRunTests.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.TestUtilities;
-using Xunit;
-
-namespace Aspire.Templates.Tests;
-
-public class EmptyTemplateRunTests : TemplateTestsBase, IClassFixture
-{
- private readonly EmptyTemplateRunFixture _testFixture;
-
- public EmptyTemplateRunTests(EmptyTemplateRunFixture fixture, ITestOutputHelper testOutput)
- : base(testOutput)
- {
- _testFixture = fixture;
- }
-
- [Fact]
- [RequiresFeature(TestFeature.Playwright)]
- [RequiresFeature(TestFeature.SSLCertificate)]
- [OuterloopTest("Resource-intensive Playwright browser test")]
- public async Task ResourcesShowUpOnDashboad()
- {
- await using var context = await CreateNewBrowserContextAsync();
- await CheckDashboardHasResourcesAsync(
- await _testFixture.Project!.OpenDashboardPageAsync(context),
- [],
- timeoutSecs: 1_000,
- logPath: _testFixture.Project.LogPath);
- }
-}
diff --git a/tests/Aspire.Templates.Tests/LocalhostTldHostnameTests.cs b/tests/Aspire.Templates.Tests/LocalhostTldHostnameTests.cs
deleted file mode 100644
index 4ed489c2461..00000000000
--- a/tests/Aspire.Templates.Tests/LocalhostTldHostnameTests.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Text.Json;
-using System.Text.RegularExpressions;
-using Xunit;
-
-namespace Aspire.Templates.Tests;
-
-public partial class LocalhostTldHostnameTests(ITestOutputHelper testOutput) : TemplateTestsBase(testOutput)
-{
- [GeneratedRegex(@"://([^:]+)\.dev\.localhost:")]
- private static partial Regex HostnamePattern();
-
- public static TheoryData LocalhostTldHostname_TestData() => new()
- {
- // templateName, projectName, expectedHostname
- { "aspire", "my.namespace.app", "my-namespace-app" },
- { "aspire", ".StartWithDot", "startwithdot" },
- { "aspire", "EndWithDot.", "endwithdot" },
- { "aspire", "My..Test__Project", "my-test-project" },
- { "aspire", "Project123.Test456", "project123-test456" },
- { "aspire-apphost", "my.service.name", "my-service-name" },
- { "aspire-apphost-singlefile", "-my.service..name-", "my-service-name" },
- { "aspire-starter", "Test_App.1", "test-app-1" },
- { "aspire-ts-cs-starter", "My-App.Test", "my-app-test" }
- };
-
- [Theory]
- [MemberData(nameof(LocalhostTldHostname_TestData))]
- [Trait("category", "basic-build")]
- public async Task LocalhostTld_GeneratesDnsCompliantHostnames(string templateName, string projectName, string expectedHostname)
- {
- var id = GetNewProjectId(prefix: $"localhost_tld_{templateName}");
-
- var targetFramework = templateName switch
- {
- "aspire-apphost-singlefile" => TestTargetFramework.None, // These templates do not support -f argument
- _ => TestTargetFramework.Next // LocalhostTld only available on net10.0
- };
-
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(
- id,
- templateName,
- _testOutput,
- buildEnvironment: BuildEnvironment.ForNextSdkOnly, // Need Next SDK for net10.0
- extraArgs: $"--localhost-tld -n \"{projectName}\"",
- targetFramework,
- addEndpointsHook: false); // Don't add endpoint hook since we're just checking file generation
-
- // When using -n, the template still outputs to the -o directory (id),
- // but the project names inside use the -n value
- // Find the launchSettings.json file - it will be in a directory named after the project
- var launchSettingsPath = templateName switch
- {
- "aspire-ts-cs-starter" or "aspire-starter" => Path.Combine(project.RootDir, $"{projectName}.AppHost", "Properties", "launchSettings.json"),
- "aspire" => Path.Combine(project.RootDir, $"{projectName}.AppHost", "Properties", "launchSettings.json"),
- "aspire-apphost" => Path.Combine(project.RootDir, "Properties", "launchSettings.json"),
- "aspire-apphost-singlefile" => Path.Combine(project.RootDir, "apphost.run.json"),
- _ => throw new ArgumentException($"Unknown template: {templateName}")
- };
-
- Assert.True(File.Exists(launchSettingsPath), $"launchSettings.json/apphost.run.json file not found at {launchSettingsPath}");
-
- var launchSettingsContent = await File.ReadAllTextAsync(launchSettingsPath);
- using var launchSettings = JsonDocument.Parse(launchSettingsContent);
-
- var profiles = launchSettings.RootElement.GetProperty("profiles");
-
- var foundDevLocalhost = false;
- foreach (var profile in profiles.EnumerateObject())
- {
- if (profile.Value.TryGetProperty("applicationUrl", out var applicationUrl))
- {
- var urls = applicationUrl.GetString();
- if (!string.IsNullOrEmpty(urls) && urls.Contains(".dev.localhost:"))
- {
- foundDevLocalhost = true;
-
- // Verify the hostname in the URL matches expected DNS-compliant format
- Assert.Contains($"{expectedHostname}.dev.localhost:", urls);
-
- // Verify no underscores in hostname (RFC 952/1123 compliance)
- var matches = HostnamePattern().Matches(urls);
- foreach (Match match in matches)
- {
- var hostname = match.Groups[1].Value;
- Assert.DoesNotContain("_", hostname, StringComparison.Ordinal);
- Assert.DoesNotContain(".", hostname, StringComparison.Ordinal);
- Assert.False(hostname.StartsWith("-", StringComparison.Ordinal),
- $"Hostname '{hostname}' should not start with hyphen (RFC 952/1123 violation)");
- Assert.False(hostname.EndsWith("-", StringComparison.Ordinal),
- $"Hostname '{hostname}' should not end with hyphen (RFC 952/1123 violation)");
- }
- }
- }
- }
-
- Assert.True(foundDevLocalhost, "No .dev.localhost URLs found in launchSettings.json");
- }
-}
diff --git a/tests/Aspire.Templates.Tests/NewUpAndBuildStandaloneTemplateTests.cs b/tests/Aspire.Templates.Tests/NewUpAndBuildStandaloneTemplateTests.cs
deleted file mode 100644
index b0e1678bbbe..00000000000
--- a/tests/Aspire.Templates.Tests/NewUpAndBuildStandaloneTemplateTests.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Xunit;
-
-namespace Aspire.Templates.Tests;
-
-public class NewUpAndBuildStandaloneTemplateTests(ITestOutputHelper testOutput) : TemplateTestsBase(testOutput)
-{
- [Theory]
- [MemberData(nameof(TestDataForNewAndBuildTemplateTests), arguments: ["aspire", ""])]
- [MemberData(nameof(TestDataForNewAndBuildTemplateTests), arguments: ["aspire-starter", ""])]
- [MemberData(nameof(TestDataForNewAndBuildTemplateTests), arguments: ["aspire-ts-cs-starter", ""])]
- [Trait("category", "basic-build")]
- public async Task CanNewAndBuild(string templateName, string extraArgs, TestSdk sdk, TestTargetFramework tfm, string? error)
- {
- var id = GetNewProjectId(prefix: $"new_build_{templateName}_{tfm.ToTFMString()}");
-
- var buildEnvToUse = sdk switch
- {
- TestSdk.Current => BuildEnvironment.ForCurrentSdkOnly,
- TestSdk.Previous => BuildEnvironment.ForPreviousSdkOnly,
- TestSdk.Next => BuildEnvironment.ForNextSdkOnly,
- TestSdk.NextSdkWithCurrentAndPreviousRuntime => BuildEnvironment.ForNextSdkWithCurrentAndPreviousRuntimes,
- _ => throw new ArgumentOutOfRangeException(nameof(sdk))
- };
-
- try
- {
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(
- id,
- templateName,
- _testOutput,
- buildEnvironment: buildEnvToUse,
- extraArgs: extraArgs,
- targetFramework: tfm);
-
- Assert.True(error is null, $"Expected to throw an exception with message: {error}");
-
- await project.BuildAsync(extraBuildArgs: [$"-c Debug"]);
- }
- catch (ToolCommandException tce) when (error is not null)
- {
- Assert.NotNull(tce.Result);
- Assert.Contains(error, tce.Result.Value.Output);
- }
- }
-}
diff --git a/tests/Aspire.Templates.Tests/NewUpAndBuildSupportProjectTemplatesTests.cs b/tests/Aspire.Templates.Tests/NewUpAndBuildSupportProjectTemplatesTests.cs
deleted file mode 100644
index 96e1a4eb3ac..00000000000
--- a/tests/Aspire.Templates.Tests/NewUpAndBuildSupportProjectTemplatesTests.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Xunit;
-
-namespace Aspire.Templates.Tests;
-
-public abstract class NewUpAndBuildSupportProjectTemplatesBase(ITestOutputHelper testOutput) : TemplateTestsBase(testOutput)
-{
- [Trait("category", "basic-build")]
- protected async Task CanNewAndBuildActual(string templateName, string extraTestCreationArgs, TestSdk sdk, TestTargetFramework tfm, string? error)
- {
- var id = GetNewProjectId(prefix: $"new_build_{FixupSymbolName(templateName)}");
- var topLevelDir = Path.Combine(BuildEnvironment.TestRootPath, id + "_root");
- string config = "Debug";
-
- var buildEnvToUse = sdk switch
- {
- TestSdk.Current => BuildEnvironment.ForCurrentSdkOnly,
- TestSdk.Previous => BuildEnvironment.ForPreviousSdkOnly,
- TestSdk.Next => BuildEnvironment.ForNextSdkOnly,
- TestSdk.NextSdkWithCurrentAndPreviousRuntime => BuildEnvironment.ForNextSdkWithCurrentAndPreviousRuntimes,
- _ => throw new ArgumentOutOfRangeException(nameof(sdk))
- };
-
- if (Directory.Exists(topLevelDir))
- {
- Directory.Delete(topLevelDir, recursive: true);
- }
- Directory.CreateDirectory(topLevelDir);
-
- try
- {
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(
- id: id + ".AppHost",
- template: "aspire-apphost",
- testOutput: _testOutput,
- buildEnvironment: buildEnvToUse,
- targetFramework: tfm,
- addEndpointsHook: false,
- overrideRootDir: topLevelDir);
- project.AppHostProjectDirectory = Path.Combine(topLevelDir, id + ".AppHost");
-
- var testProjectDir = await CreateAndAddTestTemplateProjectAsync(
- id: id,
- testTemplateName: templateName,
- project: project,
- tfm: tfm,
- buildEnvironment: buildEnvToUse,
- extraArgs: extraTestCreationArgs,
- overrideRootDir: topLevelDir);
-
- await project.BuildAsync(extraBuildArgs: [$"-c {config}"], workingDirectory: testProjectDir);
- }
- catch (ToolCommandException tce) when (error is not null)
- {
- Assert.NotNull(tce.Result);
- Assert.Contains(error, tce.Result.Value.Output);
- }
- }
-}
-
-public class NUnit_NewUpAndBuildSupportProjectTemplatesTests(ITestOutputHelper testOutput) : NewUpAndBuildSupportProjectTemplatesBase(testOutput)
-{
- [Theory]
- [MemberData(nameof(TestDataForNewAndBuildTemplateTests), arguments: ["aspire-nunit", ""])]
- public Task CanNewAndBuild(string templateName, string extraTestCreationArgs, TestSdk sdk, TestTargetFramework tfm, string? error)
- {
- return CanNewAndBuildActual(templateName, extraTestCreationArgs, sdk, tfm, error);
- }
-}
-
-public class XUnit_Default_NewUpAndBuildSupportProjectTemplatesTests(ITestOutputHelper testOutput) : NewUpAndBuildSupportProjectTemplatesBase(testOutput)
-{
- [Theory]
- [MemberData(nameof(TestDataForNewAndBuildTemplateTests), arguments: ["aspire-xunit", ""])]
- public Task CanNewAndBuild(string templateName, string extraTestCreationArgs, TestSdk sdk, TestTargetFramework tfm, string? error)
- {
- return CanNewAndBuildActual(templateName, extraTestCreationArgs, sdk, tfm, error);
- }
-}
-
-public class XUnit_V2_NewUpAndBuildSupportProjectTemplatesTests(ITestOutputHelper testOutput) : NewUpAndBuildSupportProjectTemplatesBase(testOutput)
-{
- [Theory]
- [MemberData(nameof(TestDataForNewAndBuildTemplateTests), arguments: ["aspire-xunit", "--xunit-version v2"])]
- public Task CanNewAndBuild(string templateName, string extraTestCreationArgs, TestSdk sdk, TestTargetFramework tfm, string? error)
- {
- return CanNewAndBuildActual(templateName, extraTestCreationArgs, sdk, tfm, error);
- }
-}
-
-public class XUnit_V3_NewUpAndBuildSupportProjectTemplatesTests(ITestOutputHelper testOutput) : NewUpAndBuildSupportProjectTemplatesBase(testOutput)
-{
- [Theory]
- [MemberData(nameof(TestDataForNewAndBuildTemplateTests), arguments: ["aspire-xunit", "--xunit-version v3"])]
- public Task CanNewAndBuild(string templateName, string extraTestCreationArgs, TestSdk sdk, TestTargetFramework tfm, string? error)
- {
- return CanNewAndBuildActual(templateName, extraTestCreationArgs, sdk, tfm, error);
- }
-}
-
-public class XUnit_V3MTP_NewUpAndBuildSupportProjectTemplatesTests(ITestOutputHelper testOutput) : NewUpAndBuildSupportProjectTemplatesBase(testOutput)
-{
- [Theory]
- [MemberData(nameof(TestDataForNewAndBuildTemplateTests), arguments: ["aspire-xunit", "--xunit-version v3mtp"])]
- public Task CanNewAndBuild(string templateName, string extraTestCreationArgs, TestSdk sdk, TestTargetFramework tfm, string? error)
- {
- return CanNewAndBuildActual(templateName, extraTestCreationArgs, sdk, tfm, error);
- }
-}
-
-public class XUnit_NewUpAndBuildSupportProjectTemplatesTests(ITestOutputHelper testOutput) : NewUpAndBuildSupportProjectTemplatesBase(testOutput)
-{
- [Theory]
- [MemberData(nameof(TestDataForNewAndBuildTemplateTests), arguments: ["aspire-xunit", ""])]
- public Task CanNewAndBuild(string templateName, string extraTestCreationArgs, TestSdk sdk, TestTargetFramework tfm, string? error)
- {
- return CanNewAndBuildActual(templateName, extraTestCreationArgs, sdk, tfm, error);
- }
-}
-
-public class MSTest_NewUpAndBuildSupportProjectTemplatesTests(ITestOutputHelper testOutput) : NewUpAndBuildSupportProjectTemplatesBase(testOutput)
-{
- [Theory]
- [MemberData(nameof(TestDataForNewAndBuildTemplateTests), arguments: ["aspire-mstest", ""])]
- public Task CanNewAndBuild(string templateName, string extraTestCreationArgs, TestSdk sdk, TestTargetFramework tfm, string? error)
- {
- return CanNewAndBuildActual(templateName, extraTestCreationArgs, sdk, tfm, error);
- }
-}
diff --git a/tests/Aspire.Templates.Tests/PerTestFrameworkTemplatesTests.cs b/tests/Aspire.Templates.Tests/PerTestFrameworkTemplatesTests.cs
deleted file mode 100644
index f5104a4136c..00000000000
--- a/tests/Aspire.Templates.Tests/PerTestFrameworkTemplatesTests.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.TestUtilities;
-using Microsoft.Playwright;
-using Xunit;
-
-namespace Aspire.Templates.Tests;
-
-public abstract partial class PerTestFrameworkTemplatesTests : TemplateTestsBase
-{
- private readonly string _testTemplateName;
-
- public PerTestFrameworkTemplatesTests(string testType, ITestOutputHelper testOutput) : base(testOutput)
- {
- _testTemplateName = testType;
- }
-
- public static TheoryData ProjectNames_TestData() => new(GetProjectNamesForTest());
-
- [Theory]
- [MemberData(nameof(ProjectNames_TestData))]
- [RequiresFeature(TestFeature.Playwright)]
- [Trait("category", "basic-build")]
- [OuterloopTest("playwright test")]
- [ActiveIssue("https://github.com/microsoft/aspire/issues/8011")]
- public async Task TemplatesForIndividualTestFrameworks(string prefix)
- {
- var id = $"{prefix}-{_testTemplateName}";
- var config = "Debug";
-
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(
- id,
- "aspire",
- _testOutput,
- buildEnvironment: BuildEnvironment.ForDefaultFramework);
-
- await project.BuildAsync(extraBuildArgs: [$"-c {config}"]);
- if (RequiresFeatureAttribute.IsFeatureSupported(TestFeature.SSLCertificate))
- {
- await using (var context = await CreateNewBrowserContextAsync())
- {
- await AssertBasicTemplateAsync(context);
- }
- }
-
- var testProjectDir = await CreateAndAddTestTemplateProjectAsync(id, _testTemplateName, project);
-
- using var cmd = new DotNetCommand(_testOutput, label: $"test-{_testTemplateName}")
- .WithWorkingDirectory(testProjectDir)
- .WithTimeout(TimeSpan.FromMinutes(3));
-
- var res = await cmd.ExecuteAsync($"test -c {config}");
-
- Assert.True(res.ExitCode != 0, $"Expected the tests project run to fail");
- Assert.Matches("Failed! * - Failed: *1, Passed: *0, Skipped: *0, Total: *1", res.Output);
-
- async Task AssertBasicTemplateAsync(IBrowserContext context)
- {
- await project.StartAppHostAsync(extraArgs: [$"-c {config}"]);
-
- try
- {
- var page = await project.OpenDashboardPageAsync(context);
- await CheckDashboardHasResourcesAsync(page, [], logPath: project.LogPath);
- }
- finally
- {
- await project.StopAppHostAsync();
- }
- }
- }
-}
-
-// Individual class for each test framework so the tests can run in separate helix jobs
-public class MSTest_PerTestFrameworkTemplatesTests : PerTestFrameworkTemplatesTests
-{
- public MSTest_PerTestFrameworkTemplatesTests(ITestOutputHelper testOutput) : base("aspire-mstest", testOutput)
- {
- }
-}
-
-public class Xunit_PerTestFrameworkTemplatesTests : PerTestFrameworkTemplatesTests
-{
- public Xunit_PerTestFrameworkTemplatesTests(ITestOutputHelper testOutput) : base("aspire-xunit", testOutput)
- {
- }
-}
-
-public class Nunit_PerTestFrameworkTemplatesTests : PerTestFrameworkTemplatesTests
-{
- public Nunit_PerTestFrameworkTemplatesTests(ITestOutputHelper testOutput) : base("aspire-nunit", testOutput)
- {
- }
-}
diff --git a/tests/Aspire.Templates.Tests/README.md b/tests/Aspire.Templates.Tests/README.md
deleted file mode 100644
index 4cbb8d238bd..00000000000
--- a/tests/Aspire.Templates.Tests/README.md
+++ /dev/null
@@ -1,83 +0,0 @@
-# Aspire.Template.Tests
-
-The purpose of the `Aspire.Template.Tests` project is to run end-to-end tests against pre-built NuGet packages (nupkgs). These tests validate the ability to create projects from templates just like a user would, and then build, run, and interact with Aspire projects to ensure compatibility with CI pipelines and local development environments.
-
-For pull-requests in CI the tests are run via GitHub actions defined in `tests-templates.yml`.
-
-## TL;DR or How do I use this?
-
-1. [Install the SDK](#install-the-sdk)
-2. Run/debug the tests normally now, and they will be using the SDK
-
-## (details) What are *template* tests?
-
-The individual tests need to create projects from templates just like a user would, and then run, and validate them. For this we need:
- - a SDK installation with the necessary components installed
- - the components should use packages from the locally built NuGet packages
- - and the NuGet packages should be used from the locally built ones
-
-### Solution (SDK):
-
-- SDK is installed with `$(SdkVersionForTemplateTesting)` set to the version in `global.json` by default.
-- The necessary Aspire components are installed using NuGet packages from `artifacts/packages/*/Shipping`
-- Then, with a custom `nuget.config` which points to the built NuGet packages in `artifacts`, the SDK is configured to use local packages
- - which installs the components using the NuGet packages from the `artifacts` into `artifacts/bin/dotnet-tests`
-- This simulates the SDK being installed on a user's machine, and being independent of the aspire repo.
-- At this point the SDK is usable from outside the repo by using `source /path-to-aspire-repo/dogfood.sh`
-- The nuget versions for the locally built packages are like `8.0.0-dev` or `8.0.0-ci`.
-
-### Helix
-
-- The SDK is sent to helix, and used by all the tests for creating/running projects.
-
-## Install the SDK
-
-1. `.\build.cmd -pack` - to build all the NuGet packages (or `./build.sh -pack`)
-2. `dotnet build tests/workloads.proj /p:Configuration=`
- - this will install the SDK, and the necessary components using the NuGet packages from `artifacts/packages/*/Shipping` into `artifacts/bin/dotnet-tests`
- - note: `artifacts/bin/dotnet-none` contains the SDK with component manifests but NO components
-
-The SDK in `artifacts/bin/dotnet-tests` is usable outside the repo at this point.
-
-## Using the SDK outside the repo
-
-- Follow the steps to [install the SDK](#install-the-sdk).
-
-- The environment needs to be set up to use this. It can be done manually with:
- - Add `/path-to-aspire-repo/artifacts/bin/dotnet-tests` to `PATH`.
- - Add `artifacts/packages/$(Configuration)/Shipping` as a NuGet source for your projects, so the locally built packages can be picked up.
- - `tests/Shared/TemplateTesting/data/nuget8.config` can be used as a template for this, or you can add it manually to your `nuget.config`
- - This `nuget8.config` uses the environment variable `BUILT_NUGETS_PATH`, so set `BUILT_NUGETS_PATH=/path-to-aspire/artifacts/packages/$(Configuration)/Shipping`
-
-- An alternative way is to use `source /path-to-aspire-repo/dogfood.sh` on Linux/macOS.
- - Copy `tests/Shared/TemplateTesting/data/nuget8.config nuget.config`
- - and set `BUILT_NUGETS_PATH=/path-to-aspire/artifacts/packages/$(Configuration)/Shipping`
-
-## Inner loop tips
-
-- The sdk+workload is never updated automatically. In other words, once installed the workload packs don't get overwritten even when the source binaries changes in `artifacts`. This may change in future.
-
-There are three categories of NuGet packages used by the workload:
-
-1. `Aspire.Dashboard.Sdk.osx-arm64`, `Aspire.Hosting.Orchestration.osx-arm64`, and `Aspire.AppHost.Sdk`
- - these are installed in `artifacts/bin/dotnet-tests/packs/`
- - Once the workload is installed, these are never updated automatically, so any changes made locally won't show up in the tests
-
-2. All the other `Aspire` NuGet packages
- - These are not part of the workload itself, but can be referenced from the user projects.
- - When tests build a project referencing such a NuGet package, it gets resolved from the `artifacts`.
- - And a local tests-specific cache is used for this like `/path-to-aspire-repo/artifacts/bin/Aspire.Template.Tests/Release/net8.0/nuget-cache-Net80`
- - this is printed at the start of the test suite run
-
-3. Project templates installed in `artifacts/bin/dotnet-tests/template-packs`.
-
-### Testing with local changes
-
-- If there are changes to bits that would be part of the workload packs, then those would need to be updated (copied over) manually
-- The project templates don't follow the above model though. The NuGet packages for those need to be built, and then copied to `artifacts/bin/dotnet-tests/template-packs/aspire.projecttemplates.*.nupkg` for the changes to be usable in the tests
-
-- For changes related to other NuGet packages, there are a couple of options:
- 1. rebuild the NuGet package; delete the unpacked NuGet package from the cache
- 2. Since the cache is never automatically deleted, copy over any changed files directly in the NuGet cache.
-
- Subsequent test runs would pick up the changes.
diff --git a/tests/Aspire.Templates.Tests/StarterTemplateFixture.cs b/tests/Aspire.Templates.Tests/StarterTemplateFixture.cs
deleted file mode 100644
index efccc99fcb8..00000000000
--- a/tests/Aspire.Templates.Tests/StarterTemplateFixture.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Xunit.Sdk;
-
-namespace Aspire.Templates.Tests;
-
-///
-/// This fixture creates a new project using the `aspire-starter` template, and runs that
-///
-public sealed class StarterTemplateFixture : TemplateAppFixture
-{
- public StarterTemplateFixture(IMessageSink diagnosticMessageSink)
- : base(diagnosticMessageSink, "aspire-starter")
- {
- }
-}
diff --git a/tests/Aspire.Templates.Tests/StarterTemplateFixture_PreviousTFM.cs b/tests/Aspire.Templates.Tests/StarterTemplateFixture_PreviousTFM.cs
deleted file mode 100644
index f313d85f9ca..00000000000
--- a/tests/Aspire.Templates.Tests/StarterTemplateFixture_PreviousTFM.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Xunit.Sdk;
-
-namespace Aspire.Templates.Tests;
-
-public sealed class StarterTemplateFixture_PreviousTFM : TemplateAppFixture
-{
- public StarterTemplateFixture_PreviousTFM(IMessageSink diagnosticMessageSink)
- : base(diagnosticMessageSink, "aspire-starter", tfm: TestTargetFramework.Previous)
- {
- }
-}
diff --git a/tests/Aspire.Templates.Tests/StarterTemplateProjectNamesTests.cs b/tests/Aspire.Templates.Tests/StarterTemplateProjectNamesTests.cs
deleted file mode 100644
index 47a0e5ce840..00000000000
--- a/tests/Aspire.Templates.Tests/StarterTemplateProjectNamesTests.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.TestUtilities;
-using Xunit;
-
-namespace Aspire.Templates.Tests;
-
-public abstract class StarterTemplateProjectNamesTests : TemplateTestsBase
-{
- private readonly string _testType;
- public StarterTemplateProjectNamesTests(string testType, ITestOutputHelper testOutput)
- : base(testOutput)
- {
- _testType = testType;
- }
-
- public static TheoryData ProjectNames_TestData()
- => new(GetProjectNamesForTest());
-
- [Theory]
- [MemberData(nameof(ProjectNames_TestData))]
- [RequiresFeature(TestFeature.SSLCertificate)]
- [RequiresFeature(TestFeature.Playwright)]
- [OuterloopTest("playwright test")]
- public async Task StarterTemplateWithTest_ProjectNames(string prefix)
- {
- string id = $"{prefix}-{_testType}";
- string config = "Debug";
-
- await using var project = await AspireProject.CreateNewTemplateProjectAsync(
- id,
- "aspire-starter",
- _testOutput,
- BuildEnvironment.ForDefaultFramework,
- $"-t {_testType}");
-
- await using var context = await CreateNewBrowserContextAsync();
- _testOutput.WriteLine($"Checking the starter template project");
- await AssertStarterTemplateRunAsync(context, project, config, _testOutput);
-
- _testOutput.WriteLine($"Checking the starter template project tests");
- await AssertTestProjectRunAsync(project.TestsProjectDirectory, _testType, _testOutput, config);
- }
-}
-
-// Individual class for each test framework so the tests can run in separate helix jobs
-public class None_StarterTemplateProjectNamesTests : StarterTemplateProjectNamesTests
-{
- public None_StarterTemplateProjectNamesTests(ITestOutputHelper testOutput) : base("none", testOutput)
- {
- }
-}
-
-public class MSTest_StarterTemplateProjectNamesTests : StarterTemplateProjectNamesTests
-{
- public MSTest_StarterTemplateProjectNamesTests(ITestOutputHelper testOutput) : base("mstest", testOutput)
- {
- }
-}
-
-public class Xunit_StarterTemplateProjectNamesTests : StarterTemplateProjectNamesTests
-{
- public Xunit_StarterTemplateProjectNamesTests(ITestOutputHelper testOutput) : base("xunit.net", testOutput)
- {
- }
-}
-
-public class Nunit_StarterTemplateProjectNamesTests : StarterTemplateProjectNamesTests
-{
- public Nunit_StarterTemplateProjectNamesTests(ITestOutputHelper testOutput) : base("nunit", testOutput)
- {
- }
-}
diff --git a/tests/Aspire.Templates.Tests/StarterTemplateRunTests.cs b/tests/Aspire.Templates.Tests/StarterTemplateRunTests.cs
deleted file mode 100644
index b618998acc2..00000000000
--- a/tests/Aspire.Templates.Tests/StarterTemplateRunTests.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.TestUtilities;
-using Xunit;
-
-namespace Aspire.Templates.Tests;
-
-[RequiresFeature(TestFeature.SSLCertificate)]
-public class StarterTemplateRunTests : StarterTemplateRunTestsBase
-{
- public StarterTemplateRunTests(StarterTemplateFixture fixture, ITestOutputHelper testOutput)
- : base(fixture, testOutput)
- {
- }
-}
diff --git a/tests/Aspire.Templates.Tests/StarterTemplateRunTestsBase.cs b/tests/Aspire.Templates.Tests/StarterTemplateRunTestsBase.cs
deleted file mode 100644
index f58790c30ea..00000000000
--- a/tests/Aspire.Templates.Tests/StarterTemplateRunTestsBase.cs
+++ /dev/null
@@ -1,184 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Microsoft.Playwright;
-using static Microsoft.Playwright.Assertions;
-using Xunit;
-using Aspire.Hosting.Redis;
-using System.Net.Http.Json;
-using Aspire.TestUtilities;
-
-namespace Aspire.Templates.Tests;
-
-public abstract class StarterTemplateRunTestsBase : TemplateTestsBase, IClassFixture where T : TemplateAppFixture
-{
- protected readonly T _testFixture;
- protected bool HasRedisCache;
- protected virtual int DashboardResourcesWaitTimeoutSecs => 120;
-
- public StarterTemplateRunTestsBase(T fixture, ITestOutputHelper testOutput)
- : base(testOutput)
- {
- _testFixture = fixture;
- }
-
- [Fact]
- [RequiresFeature(TestFeature.Playwright)]
- [OuterloopTest("Resource-intensive Playwright browser test")]
- public async Task ResourcesShowUpOnDashboard()
- {
- await using var context = await CreateNewBrowserContextAsync();
- await CheckDashboardHasResourcesAsync(
- await _testFixture.Project!.OpenDashboardPageAsync(context),
- GetExpectedResources(_testFixture.Project!, hasRedisCache: HasRedisCache),
- timeoutSecs: DashboardResourcesWaitTimeoutSecs,
- logPath: _testFixture.Project.LogPath);
- }
-
- [Theory]
- [InlineData("http://")]
- [InlineData("https://")]
- [RequiresFeature(TestFeature.Playwright)]
- [OuterloopTest("Resource-intensive Playwright browser test")]
- public async Task WebFrontendWorks(string urlPrefix)
- {
- await using var context = await CreateNewBrowserContextAsync();
- var resourceRows = await CheckDashboardHasResourcesAsync(
- await _testFixture.Project!.OpenDashboardPageAsync(context),
- GetExpectedResources(_testFixture.Project!, hasRedisCache: HasRedisCache),
- timeoutSecs: DashboardResourcesWaitTimeoutSecs,
- logPath: _testFixture.Project.LogPath);
-
- string url = _testFixture.Project.InfoTable["webfrontend"].Endpoints
- .First(e => e.Uri.StartsWith(urlPrefix))
- .Uri;
- await CheckWebFrontendWorksAsync(context, url, _testOutput, _testFixture.Project.LogPath, hasRedisCache: HasRedisCache);
- }
-
- [Theory]
- [InlineData("http://")]
- [InlineData("https://")]
- [RequiresFeature(TestFeature.Playwright)]
- [OuterloopTest("Resource-intensive Playwright browser test")]
- [Trait("category", "basic-build")]
- public async Task ApiServiceWorks(string urlPrefix)
- {
- await using var context = await CreateNewBrowserContextAsync();
- var resourceRows = await CheckDashboardHasResourcesAsync(
- await _testFixture.Project!.OpenDashboardPageAsync(context),
- GetExpectedResources(_testFixture.Project!, hasRedisCache: HasRedisCache),
- timeoutSecs: DashboardResourcesWaitTimeoutSecs,
- logPath: _testFixture.Project.LogPath);
-
- string url = _testFixture.Project.InfoTable["apiservice"].Endpoints
- .First(e => e.Uri.StartsWith(urlPrefix))
- .Uri;
- await CheckApiServiceWorksAsync(url, _testOutput, _testFixture.Project.LogPath);
- }
-
- public static async Task CheckApiServiceWorksAsync(string url, ITestOutputHelper testOutput, string logPath)
- {
- var uri = new UriBuilder(url) { Path = "weatherforecast" }.Uri;
-
- using var httpClient = new HttpClient();
- var response = await httpClient.GetFromJsonAsync(uri);
-
- Assert.NotNull(response);
- Assert.Equal(5, response.Length);
- }
-
- public static async Task CheckWebFrontendWorksAsync(IBrowserContext context, string url, ITestOutputHelper testOutput, string logPath, bool hasRedisCache = false)
- {
- var pageWrapper = await context.NewPageWithLoggingAsync(testOutput);
-
- try
- {
- // Enabling routing disables the http cache
- await pageWrapper.Page.RouteAsync("**", async route => await route.ContinueAsync());
- await pageWrapper.Page.GotoAsync(url);
-
- await pageWrapper.Page.GetByRole(AriaRole.Link, new PageGetByRoleOptions { Name = "Weather" }).ClickAsync();
-
- var tableLoc = pageWrapper.Page.Locator("//table[//thead/tr/th/text()='Date']");
- await Expect(tableLoc).ToBeVisibleAsync();
-
- if (hasRedisCache)
- {
- // Compare weather data after refreshes
- var firstLoadText = string.Join(',', (await GetAndValidateCellTexts(tableLoc)).SelectMany(r => r));
- await Task.Delay(10_000);
-
- await pageWrapper.Page.ReloadAsync(new PageReloadOptions { WaitUntil = WaitUntilState.Load });
-
- var secondLoadText = string.Join(',', (await GetAndValidateCellTexts(tableLoc)).SelectMany(r => r));
- Assert.NotEqual(firstLoadText, secondLoadText);
- }
- }
- catch (Exception ex)
- {
- testOutput.WriteLine($"Error: {ex}");
- string screenshotPath = Path.Combine(logPath, "webfrontend-fail.png");
- await pageWrapper.Page.ScreenshotAsync(new PageScreenshotOptions { Path = screenshotPath });
- throw;
- }
-
- static async Task> GetAndValidateCellTexts(ILocator tableLoc)
- {
- List cellTexts = [];
- var rowsLoc = tableLoc.Locator("//tbody/tr");
- foreach (var row in await rowsLoc.AllAsync())
- {
- var texts = (await row.Locator("//td").AllAsync())
- .Select(cell => cell.InnerHTMLAsync())
- .Select(t => t.Result)
- .ToArray();
- cellTexts.Add(texts);
- }
-
- foreach (var row in cellTexts)
- {
- Assert.Collection(row,
- r => Assert.True(DateTime.TryParse(r, out _)),
- r => Assert.True(int.TryParse(r, out var actualTempC) && actualTempC >= -20 && actualTempC <= 55),
- r => Assert.True(int.TryParse(r, out var actualTempF) && actualTempF >= -5 && actualTempF <= 133),
- r => Assert.Contains(r, new HashSet { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }));
- }
-
- return cellTexts;
- }
- }
-
- public static List GetExpectedResources(AspireProject project, bool hasRedisCache)
- {
- var expectedResources = new List
- {
- new(Type: "Project",
- Name: "apiservice",
- State: "Running",
- SourceContains: $"{project.Id}.ApiService.csproj"),
-
- new(Type: "Project",
- Name: "webfrontend",
- State: "Running",
- SourceContains: $"{project.Id}.Web.csproj")
- };
-
- if (hasRedisCache)
- {
- expectedResources.Add(
- new ResourceRow(Type: "Container",
- Name: "cache",
- State: "Running",
- SourceContains: $"{RedisContainerImageTags.Registry}/{RedisContainerImageTags.Image}:{RedisContainerImageTags.Tag}"));
- }
-
- return expectedResources;
- }
-}
-
-public sealed record ResourceRow(string Type, string Name, string State, string SourceContains);
-
-public sealed record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
-{
- public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
-}
diff --git a/tests/Aspire.Templates.Tests/StarterTemplateRunTests_PreviousTFM.cs b/tests/Aspire.Templates.Tests/StarterTemplateRunTests_PreviousTFM.cs
deleted file mode 100644
index 8536b15ff8e..00000000000
--- a/tests/Aspire.Templates.Tests/StarterTemplateRunTests_PreviousTFM.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.TestUtilities;
-using Xunit;
-
-namespace Aspire.Templates.Tests;
-
-[RequiresFeature(TestFeature.SSLCertificate)]
-public class StarterTemplateRunTests_PreviousTFM : StarterTemplateRunTestsBase
-{
- public StarterTemplateRunTests_PreviousTFM(StarterTemplateFixture_PreviousTFM fixture, ITestOutputHelper testOutput)
- : base(fixture, testOutput)
- {
- }
-}
diff --git a/tests/Aspire.Templates.Tests/StarterTemplateWithRedisCacheFixture.cs b/tests/Aspire.Templates.Tests/StarterTemplateWithRedisCacheFixture.cs
deleted file mode 100644
index e079b43ec01..00000000000
--- a/tests/Aspire.Templates.Tests/StarterTemplateWithRedisCacheFixture.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Xunit.Sdk;
-
-namespace Aspire.Templates.Tests;
-
-///
-/// This fixture runs a aspire-starter template created with --use-redis-cache
-///
-public sealed class StarterTemplateWithRedisCacheFixture : TemplateAppFixture
-{
- public StarterTemplateWithRedisCacheFixture(IMessageSink diagnosticMessageSink)
- : base(diagnosticMessageSink, "aspire-starter", "--use-redis-cache")
- {
- }
-}
diff --git a/tests/Aspire.Templates.Tests/StarterTemplateWithRedisCacheFixture_PreviousTFM.cs b/tests/Aspire.Templates.Tests/StarterTemplateWithRedisCacheFixture_PreviousTFM.cs
deleted file mode 100644
index 7d24bcaa6a2..00000000000
--- a/tests/Aspire.Templates.Tests/StarterTemplateWithRedisCacheFixture_PreviousTFM.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Xunit.Sdk;
-
-namespace Aspire.Templates.Tests;
-
-public sealed class StarterTemplateWithRedisCacheFixture_PreviousTFM : TemplateAppFixture
-{
- public StarterTemplateWithRedisCacheFixture_PreviousTFM(IMessageSink diagnosticMessageSink)
- : base(diagnosticMessageSink, "aspire-starter", "--use-redis-cache", tfm: TestTargetFramework.Previous)
- {
- }
-}
diff --git a/tests/Aspire.Templates.Tests/StarterTemplateWithRedisCacheTests.cs b/tests/Aspire.Templates.Tests/StarterTemplateWithRedisCacheTests.cs
deleted file mode 100644
index c1c25a06ac1..00000000000
--- a/tests/Aspire.Templates.Tests/StarterTemplateWithRedisCacheTests.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.TestUtilities;
-using Xunit;
-
-namespace Aspire.Templates.Tests;
-
-[RequiresFeature(TestFeature.Docker)]
-[RequiresFeature(TestFeature.SSLCertificate)]
-public class StarterTemplateWithRedisCacheTests : StarterTemplateRunTestsBase
-{
- protected override int DashboardResourcesWaitTimeoutSecs => 300;
-
- public StarterTemplateWithRedisCacheTests(StarterTemplateWithRedisCacheFixture fixture, ITestOutputHelper testOutput)
- : base(fixture, testOutput)
- {
- HasRedisCache = true;
- }
-}
diff --git a/tests/Aspire.Templates.Tests/StarterTemplateWithRedisCacheTests_PreviousTFM.cs b/tests/Aspire.Templates.Tests/StarterTemplateWithRedisCacheTests_PreviousTFM.cs
deleted file mode 100644
index 30feb8c5218..00000000000
--- a/tests/Aspire.Templates.Tests/StarterTemplateWithRedisCacheTests_PreviousTFM.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Aspire.TestUtilities;
-using Xunit;
-
-namespace Aspire.Templates.Tests;
-
-[RequiresFeature(TestFeature.Docker)]
-[RequiresFeature(TestFeature.SSLCertificate)]
-public class StarterTemplateWithRedisCacheTests_PreviousTFM : StarterTemplateRunTestsBase
-{
- protected override int DashboardResourcesWaitTimeoutSecs => 300;
-
- public StarterTemplateWithRedisCacheTests_PreviousTFM(StarterTemplateWithRedisCacheFixture_PreviousTFM fixture, ITestOutputHelper testOutput)
- : base(fixture, testOutput)
- {
- HasRedisCache = true;
- }
-}
diff --git a/tests/Aspire.Templates.Tests/TemplateAppFixture.cs b/tests/Aspire.Templates.Tests/TemplateAppFixture.cs
deleted file mode 100644
index 62fd10ec859..00000000000
--- a/tests/Aspire.Templates.Tests/TemplateAppFixture.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Xunit;
-using Xunit.Sdk;
-
-namespace Aspire.Templates.Tests;
-
-///
-/// This fixture runs a project created from a given template
-///
-public class TemplateAppFixture : IAsyncLifetime
-{
- private readonly IMessageSink _diagnosticMessageSink;
- private readonly TestOutputWrapper _testOutput;
- private readonly string _dotnetNewArgs;
- private readonly string _config;
- private readonly TestTargetFramework? _tfm;
-
- public AspireProject? Project { get; private set; }
-
- public string Id { get; init; }
- public string TemplateName { get; set; }
-
- public TemplateAppFixture(IMessageSink diagnosticMessageSink, string templateName, string? dotnetNewArgs = null, string config = "Debug", TestTargetFramework tfm = TestTargetFramework.Current)
- {
- _diagnosticMessageSink = diagnosticMessageSink;
- _testOutput = new TestOutputWrapper(messageSink: _diagnosticMessageSink);
- _dotnetNewArgs = dotnetNewArgs ?? string.Empty;
- _config = config;
- Id = TemplateTestsBase.GetNewProjectId(prefix: $"{templateName}_{tfm.ToTFMString()}");
- TemplateName = templateName;
- _tfm = tfm;
- }
-
- public async ValueTask InitializeAsync()
- {
- Project = await AspireProject.CreateNewTemplateProjectAsync(
- Id,
- TemplateName,
- _testOutput,
- extraArgs: _dotnetNewArgs,
- buildEnvironment: BuildEnvironment.ForDefaultFramework,
- targetFramework: _tfm);
-
- await Project.BuildAsync(extraBuildArgs: [$"-c {_config}"]);
- await Project.StartAppHostAsync(extraArgs: [$"-c {_config}"]);
- }
-
- public async ValueTask DisposeAsync()
- {
- if (Project is not null)
- {
- await Project.DisposeAsync();
- }
- }
-
- public void EnsureAppHostRunning()
- {
- if (Project!.AppHostProcess is null || Project.AppHostProcess.HasExited || Project.AppExited?.Task.IsCompleted == true)
- {
- throw new InvalidOperationException($"The app host process is not running. {Project.AppHostProcess?.HasExited}, {Project.AppExited?.Task.IsCompleted}");
- }
- }
-}
diff --git a/tests/Aspire.Templates.Tests/TemplateTestsBase.cs b/tests/Aspire.Templates.Tests/TemplateTestsBase.cs
deleted file mode 100644
index fa0454a3186..00000000000
--- a/tests/Aspire.Templates.Tests/TemplateTestsBase.cs
+++ /dev/null
@@ -1,402 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Xml;
-using Aspire.TestUtilities;
-using Microsoft.Playwright;
-using Xunit;
-using static Aspire.Templates.Tests.TestExtensions;
-
-namespace Aspire.Templates.Tests;
-
-public partial class TemplateTestsBase
-{
- [GeneratedRegex(@"^\s*//")]
- private static partial Regex CommentLineRegex();
-
- // Regex is from src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets - _GeneratedClassNameFixupRegex
- [GeneratedRegex(@"(((?<=\.)|^)(?=\d)|\W)")]
- private static partial Regex GeneratedClassNameFixupRegex();
- private static Lazy Browser => new(CreateBrowser);
- private static readonly XmlWriterSettings s_xmlWriterSettings = new() { ConformanceLevel = ConformanceLevel.Fragment };
- protected readonly TestOutputWrapper _testOutput;
-
- public static readonly string[] TestFrameworkTypes = ["none", "mstest", "nunit", "xunit.net"];
-
- public TemplateTestsBase(ITestOutputHelper testOutput)
- {
- _testOutput = new TestOutputWrapper(testOutput);
- }
-
- private static IBrowser CreateBrowser()
- {
- var t = Task.Run(async () => await PlaywrightProvider.CreateBrowserAsync());
-
- // default timeout for playwright.Chromium.LaunchAsync is 30secs,
- // so using a timeout here as a fallback
- if (!t.Wait(45 * 1000))
- {
- throw new TimeoutException("Browser creation timed out");
- }
-
- return t.Result;
- }
-
- public async Task CreateAndAddTestTemplateProjectAsync(
- string id,
- string testTemplateName,
- AspireProject project,
- TestTargetFramework? tfm = null,
- BuildEnvironment? buildEnvironment = null,
- string? extraArgs = null,
- Func? onBuildAspireProject = null,
- string? overrideRootDir = null)
- {
- buildEnvironment ??= BuildEnvironment.ForDefaultFramework;
- var tmfArg = tfm is not null ? $"-f {tfm.Value.ToTFMString()}" : "";
-
- string rootDirToUse = overrideRootDir ?? project.RootDir;
- // Add test project
- var testProjectName = $"{id}.{FixupSymbolName(testTemplateName)}Tests";
- using var newTestCmd = new DotNetNewCommand(
- _testOutput,
- label: $"new-test-{testTemplateName}",
- buildEnv: buildEnvironment)
- .WithWorkingDirectory(rootDirToUse);
- var res = await newTestCmd.ExecuteAsync($"{testTemplateName} {tmfArg} -o \"{testProjectName}\" {extraArgs}");
- res.EnsureSuccessful();
-
- var testProjectDir = Path.Combine(rootDirToUse, testProjectName);
- Assert.True(Directory.Exists(testProjectDir), $"Expected tests project at {testProjectDir}");
-
- var testProjectPath = Path.Combine(testProjectDir, testProjectName + ".csproj");
- Assert.True(File.Exists(testProjectPath), $"Expected tests project file at {testProjectPath}");
-
- var appHostProjectName = Path.GetFileName(project.AppHostProjectDirectory)!;
- PrepareTestCsFile(
- id: project.Id,
- projectDir: testProjectDir,
- appHostProjectName: appHostProjectName,
- testTemplateName: testTemplateName);
- PrepareTestProject(
- project: project,
- projectPath: testProjectPath,
- appHostProjectName: appHostProjectName);
-
- return testProjectDir;
-
- static void PrepareTestProject(AspireProject project, string projectPath, string appHostProjectName)
- {
- // Insert in the project file
-
- // taken from https://raw.githubusercontent.com/dotnet/templating/a325ffa18edd1590f9b340cf83d51d8eb567ebdc/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueForms/XmlEncodeValueFormFactory.cs
- StringBuilder output = new();
- using (var w = XmlWriter.Create(output, s_xmlWriterSettings))
- {
- w.WriteString(appHostProjectName);
- }
- var xmlEncodedId = output.ToString();
-
- var projectReference = $@"";
-
- var newContents = File.ReadAllText(projectPath)
- .Replace("
", $"{projectReference}\n");
- File.WriteAllText(projectPath, newContents);
- }
-
- static void PrepareTestCsFile(string id, string projectDir, string appHostProjectName, string testTemplateName)
- {
- var testCsPath = Path.Combine(projectDir, "IntegrationTest1.cs");
- var sb = new StringBuilder();
-
- // Uncomment everything after the marker line
- var inTest = false;
- var marker = testTemplateName switch
- {
- "aspire-nunit" or "aspire-nunit-9" => "// [Test]",
- "aspire-mstest" or "aspire-mstest-9" => "// [TestMethod]",
- "aspire-xunit" or "aspire-xunit-9" => "// [Fact]",
- _ => throw new NotImplementedException($"Unknown test template: {testTemplateName}")
- };
-
- foreach (var line in File.ReadAllLines(testCsPath))
- {
- if (!inTest && line.Contains(marker))
- {
- inTest = true;
- }
-
- if (inTest && CommentLineRegex().IsMatch(line))
- {
- sb.AppendLine(CommentLineRegex().Replace(line, " "));
- continue;
- }
-
- sb.AppendLine(line);
- }
-
- var classNameFromId = GeneratedClassNameFixupRegex().Replace(appHostProjectName, "_");
- sb.Replace("Projects.MyAspireApp_AppHost", $"Projects.{classNameFromId}");
- File.WriteAllText(testCsPath, sb.ToString());
- }
- }
-
- public static Task CreateNewBrowserContextAsync()
- => PlaywrightProvider.HasPlaywrightSupport
- ? Browser.Value.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true })
- : throw new InvalidOperationException("Playwright is not available");
-
- protected Task CheckDashboardHasResourcesAsync(WrapperForIPage dashboardPageWrapper, IEnumerable expectedResources, string logPath, int timeoutSecs = 120)
- => CheckDashboardHasResourcesAsync(dashboardPageWrapper, expectedResources, _testOutput, logPath, timeoutSecs);
-
- protected static async Task CheckDashboardHasResourcesAsync(WrapperForIPage dashboardPageWrapper,
- IEnumerable expectedResources,
- ITestOutputHelper testOutput,
- string logPath,
- int timeoutSecs = 120)
- {
- try
- {
- return await CheckDashboardHasResourcesActualAsync(dashboardPageWrapper, expectedResources, testOutput, timeoutSecs);
- }
- catch
- {
- string screenshotPath = Path.Combine(logPath, $"dashboard-fail-{Guid.NewGuid().ToString()[..8]}.png");
- await dashboardPageWrapper.Page.ScreenshotAsync(new PageScreenshotOptions { Path = screenshotPath });
- testOutput.WriteLine($"Dashboard screenshot saved to {screenshotPath}");
- throw;
- }
- }
-
- private static async Task CheckDashboardHasResourcesActualAsync(WrapperForIPage dashboardPageWrapper, IEnumerable expectedResources, ITestOutputHelper testOutput, int timeoutSecs = 120)
- {
- // FIXME: check the page has 'Resources' label
- // fluent-toolbar/h1 resources
-
- var numAttempts = 0;
- testOutput.WriteLine($"Waiting for resources to appear on the dashboard");
- await Task.Delay(500);
-
- var expectedRowsTable = expectedResources.ToDictionary(r => r.Name);
- HashSet foundNames = [];
- List foundRows = [];
-
- var cts = new CancellationTokenSource();
- cts.CancelAfter(TimeSpan.FromSeconds(timeoutSecs));
-
- while (foundNames.Count < expectedRowsTable.Count && !cts.IsCancellationRequested)
- {
- if (dashboardPageWrapper.HasErrors)
- {
- if (numAttempts >= 3)
- {
- throw new InvalidOperationException($"Failed to load dashboard page after {numAttempts} attempts");
- }
-
- testOutput.WriteLine($"----- Reloading dashboard page");
- await dashboardPageWrapper.ReloadAsync(new PageReloadOptions { WaitUntil = WaitUntilState.Load });
- numAttempts++;
- }
-
- await Task.Delay(500);
-
- // _testOutput.WriteLine($"Checking for rows again");
- var rowsLocator = dashboardPageWrapper.Page.Locator("//tr[@class='fluent-data-grid-row hover resource-row']");
- var allRows = await rowsLocator.AllAsync();
- // _testOutput.WriteLine($"found rows#: {allRows.Count}");
- if (allRows.Count == 0)
- {
- // Console.WriteLine ($"** no rows found ** elapsed: {sw.Elapsed.TotalSeconds} secs");
- continue;
- }
-
- foreach (var rowLoc in allRows)
- {
- // get the cells
- var cellLocs = await rowLoc.Locator("//td[@role='gridcell']").AllAsync();
-
- // is the resource name expected?
- var resourceNameCell = cellLocs[0];
- var resourceName = await resourceNameCell.InnerTextAsync();
- resourceName = resourceName.Trim();
- if (!expectedRowsTable.TryGetValue(resourceName, out var expectedRow))
- {
- Assert.Fail($"Row with unknown name found: '{resourceName}'. Expected values: {string.Join(", ", expectedRowsTable.Keys.Select(k => $"'{k}'"))}");
- }
- if (foundNames.Contains(resourceName))
- {
- continue;
- }
-
- AssertEqual(expectedRow.Name, resourceName, $"Name for '{resourceName}'");
-
- var stateCell = cellLocs[1];
- var actualState = await stateCell.InnerTextAsync().ConfigureAwait(false);
- actualState = actualState.Trim();
- if (expectedRow.State != actualState && actualState != "Finished" && !actualState.Contains("failed", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
- AssertEqual(expectedRow.State, actualState, $"State for {resourceName}");
-
- // Check 'Source' column
- var sourceCell = cellLocs[3];
- // Since this will be the entire command, we can just confirm that the path of the executable contains
- // the expected source (executable/project)
- Assert.Contains(expectedRow.SourceContains, await sourceCell.InnerTextAsync());
-
- foundRows.Add(expectedRow);
- foundNames.Add(resourceName);
- }
- }
-
- if (foundNames.Count != expectedRowsTable.Count)
- {
- Assert.Fail($"Expected rows not found: {string.Join(", ", expectedRowsTable.Keys.Except(foundNames))}");
- }
-
- return foundRows.ToArray();
- }
-
- // Don't fixup the prefix so it can have characters meant for testing, like spaces
- public static string GetNewProjectId(string? prefix = null)
- => (prefix is null ? "" : $"{prefix}_") + FixupSymbolName(Path.GetRandomFileName());
-
- public static IEnumerable GetProjectNamesForTest()
- {
- if (!PlatformDetection.IsRunningPRValidation && !EnvironmentVariables.RunOnlyBasicBuildTemplatesTests)
- {
- // Avoid running these cases on PR validation
-
- yield return "aspire_龦唉丂荳_㐁ᠭ_ᠤསྲིདخەلꌠ_1ᥕ项目1"; // sln should have UTF-8 byte order mark
- yield return "aspire_starter.1period then.34letters";
- yield return "aspire-starter & with.1";
-
- // ActiveIssue: https://github.com/dotnet/aspnetcore/issues/56277
- // yield return "aspire_😀";
- }
-
- // basic case
- yield return "aspire";
- }
-
- public static async Task AssertStarterTemplateRunAsync(IBrowserContext? context, AspireProject project, string config, ITestOutputHelper _testOutput)
- {
- await project.StartAppHostAsync(extraArgs: [$"-c {config}"], noBuild: false);
-
- if (context is not null)
- {
- var page = await project.OpenDashboardPageAsync(context);
- await CheckDashboardHasResourcesAsync(
- page,
- StarterTemplateRunTestsBase.GetExpectedResources(project, hasRedisCache: false),
- _testOutput,
- project.LogPath).ConfigureAwait(false);
-
- string apiServiceUrl = project.InfoTable["apiservice"].Endpoints[0].Uri;
- await StarterTemplateRunTestsBase.CheckApiServiceWorksAsync(apiServiceUrl, _testOutput, project.LogPath);
-
- string webFrontEnd = project.InfoTable["webfrontend"].Endpoints[0].Uri;
- await StarterTemplateRunTestsBase.CheckWebFrontendWorksAsync(context, webFrontEnd, _testOutput, project.LogPath);
- }
- else
- {
- _testOutput.WriteLine($"Skipping playwright part of the test");
- }
-
- await project.StopAppHostAsync();
- }
-
- public static async Task AssertTestProjectRunAsync(string testProjectDirectory, string testType, ITestOutputHelper testOutput, string config = "Debug", int testRunTimeoutSecs = 3 * 60)
- {
- if (testType == "none")
- {
- Assert.False(Directory.Exists(testProjectDirectory), "Expected no tests project to be created");
- return null;
- }
- else
- {
- Assert.True(Directory.Exists(testProjectDirectory), $"Expected tests project at {testProjectDirectory}");
-
- // Build first, because `dotnet test` does not show test results if all the tests pass
- using var buildCmd = new DotNetCommand(testOutput, label: $"test-{testType}")
- .WithWorkingDirectory(testProjectDirectory)
- .WithTimeout(TimeSpan.FromSeconds(testRunTimeoutSecs));
-
- (await buildCmd.ExecuteAsync($"test -c {config}")).EnsureSuccessful();
-
- // .. then test with --no-build
- using var testCmd = new DotNetCommand(testOutput, label: $"test-{testType}")
- .WithWorkingDirectory(testProjectDirectory)
- .WithTimeout(TimeSpan.FromSeconds(testRunTimeoutSecs));
-
- var testRes = (await testCmd.ExecuteAsync($"test -c {config} --no-build"))
- .EnsureSuccessful();
-
- Assert.Matches("Passed! * - Failed: *0, Passed: *1, Skipped: *0, Total: *1", testRes.Output);
- return testRes;
- }
- }
-
- public static TheoryData TestDataForNewAndBuildTemplateTests(string templateName, string extraArgs) => new()
- {
- // Previous Sdk, Previous TFM
- { templateName, extraArgs, TestSdk.Previous, TestTargetFramework.Previous, null },
- // Previous Sdk - Current TFM
- { templateName, extraArgs, TestSdk.Previous, TestTargetFramework.Current, "The current .NET SDK does not support targeting .NET 9.0" },
-
- // Current SDK, Previous TFM
- { templateName, extraArgs, TestSdk.Current, TestTargetFramework.Previous, null },
- // Current SDK, Current TFM
- { templateName, extraArgs, TestSdk.Current, TestTargetFramework.Current, null },
- // Current SDK, Next TFM
- { templateName, extraArgs, TestSdk.Current, TestTargetFramework.Next, "The current .NET SDK does not support targeting .NET 10.0" },
-
- // Next SDK, Previous TFM
- { templateName, extraArgs, TestSdk.Next, TestTargetFramework.Previous, null },
- // Next SDK, Current TFM
- { templateName, extraArgs, TestSdk.Next, TestTargetFramework.Current, null },
- // Next SDK, Next TFM
- { templateName, extraArgs, TestSdk.Next, TestTargetFramework.Next, null },
-
- // Current SDK + previous runtime, Previous TFM
- { templateName, extraArgs, TestSdk.NextSdkWithCurrentAndPreviousRuntime, TestTargetFramework.Previous, null },
- // Current SDK + previous runtime, Current TFM
- { templateName, extraArgs, TestSdk.NextSdkWithCurrentAndPreviousRuntime, TestTargetFramework.Current, null },
- // Next SDK + current runtime, Next TFM
- { templateName, extraArgs, TestSdk.NextSdkWithCurrentAndPreviousRuntime, TestTargetFramework.Next, null },
- };
-
- // Taken from dotnet/runtime src/tasks/Common/Utils.cs
- private static readonly char[] s_charsToReplace = ['.', '-', '+', '<', '>'];
- public static string FixupSymbolName(string name)
- {
- UTF8Encoding utf8 = new();
- byte[] bytes = utf8.GetBytes(name);
- StringBuilder sb = new();
-
- foreach (byte b in bytes)
- {
- if ((b >= (byte)'0' && b <= (byte)'9') ||
- (b >= (byte)'a' && b <= (byte)'z') ||
- (b >= (byte)'A' && b <= (byte)'Z') ||
- (b == (byte)'_'))
- {
- sb.Append((char)b);
- }
- else if (s_charsToReplace.Contains((char)b))
- {
- sb.Append('_');
- }
- else
- {
- sb.Append($"_{b:X}_");
- }
- }
-
- return sb.ToString();
- }
-
-}
diff --git a/tests/Aspire.Templates.Tests/TestSdk.cs b/tests/Aspire.Templates.Tests/TestSdk.cs
deleted file mode 100644
index 874fdac76ec..00000000000
--- a/tests/Aspire.Templates.Tests/TestSdk.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Aspire.Templates.Tests;
-
-public enum TestSdk
-{
- Previous,
- Current,
- Next,
- NextSdkWithCurrentAndPreviousRuntime
-}
diff --git a/tests/Aspire.Templates.Tests/xunit.runner.json b/tests/Aspire.Templates.Tests/xunit.runner.json
deleted file mode 100644
index 6a881a7a913..00000000000
--- a/tests/Aspire.Templates.Tests/xunit.runner.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
- "diagnosticMessages": true,
- "longRunningTestSeconds": 120,
- "parallelizeAssembly": false,
- "parallelizeTestCollections": false
-}
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
index efb884f8ee9..a961aa89b8d 100644
--- a/tests/Directory.Build.props
+++ b/tests/Directory.Build.props
@@ -6,7 +6,6 @@
$([MSBuild]::NormalizeDirectory($(ArtifactsDir), 'helix', 'tests'))
- $([MSBuild]::NormalizeDirectory($(ArtifactsDir), 'helix', 'templates-tests'))
$([MSBuild]::NormalizeDirectory($(ArtifactsDir), 'helix', 'e2e-tests'))
$([MSBuild]::NormalizeDirectory($(ArtifactsDir), 'helix', 'build-on-helix-tests'))
$([MSBuild]::NormalizeDirectory($(ArtifactsDir), 'helix', 'cli-e2e-tests'))
@@ -22,12 +21,6 @@
false
-
-
-
- $(ArtifactsShippingPackagesDir)
-
-
diff --git a/tests/Infrastructure.Tests/PowerShellScripts/BuildTestMatrixTests.cs b/tests/Infrastructure.Tests/PowerShellScripts/BuildTestMatrixTests.cs
index f7abe073564..a522dd675d7 100644
--- a/tests/Infrastructure.Tests/PowerShellScripts/BuildTestMatrixTests.cs
+++ b/tests/Infrastructure.Tests/PowerShellScripts/BuildTestMatrixTests.cs
@@ -342,33 +342,6 @@ public async Task UsesUncollectedMtpBaseArgsForUncollectedEntry()
Assert.Equal("--hangdump-timeout 20m --timeout 45m", uncollectedEntry.MtpBaseArgs);
}
- [Fact]
- [RequiresTools(["pwsh"])]
- public async Task PassesRequiresTestSdkProperty()
- {
- // Arrange
- var artifactsDir = Path.Combine(_tempDir.Path, "artifacts");
- Directory.CreateDirectory(artifactsDir);
-
- TestDataBuilder.CreateTestsMetadataJson(
- Path.Combine(artifactsDir, "SdkProject.tests-metadata.json"),
- projectName: "SdkProject",
- testProjectPath: "tests/SdkProject/SdkProject.csproj",
- requiresTestSdk: true);
-
- var outputFile = Path.Combine(_tempDir.Path, "matrix.json");
-
- // Act
- var result = await RunScript(artifactsDir, outputFile);
-
- // Assert
- result.EnsureSuccessful();
-
- var matrix = ParseCanonicalMatrix(outputFile);
- var entry = Assert.Single(matrix.Tests);
- Assert.True(entry.Properties.GetValueOrDefault("requiresTestSdk"));
- }
-
[Fact]
[RequiresTools(["pwsh"])]
public async Task PreservesSupportedOSes()
diff --git a/tests/Infrastructure.Tests/Shared/TestDataBuilder.cs b/tests/Infrastructure.Tests/Shared/TestDataBuilder.cs
index 1b58f4bd335..9e7705ccad0 100644
--- a/tests/Infrastructure.Tests/Shared/TestDataBuilder.cs
+++ b/tests/Infrastructure.Tests/Shared/TestDataBuilder.cs
@@ -27,7 +27,6 @@ public static string CreateTestsMetadataJson(
string? shortName = null,
string? mtpBaseArgs = null,
bool requiresNugets = false,
- bool requiresTestSdk = false,
bool requiresCliArchive = false,
bool enablePlaywrightInstall = false,
string? extraTestArgs = null,
@@ -44,7 +43,6 @@ public static string CreateTestsMetadataJson(
Properties = new Dictionary
{
["requiresNugets"] = requiresNugets,
- ["requiresTestSdk"] = requiresTestSdk,
["requiresCliArchive"] = requiresCliArchive,
["enablePlaywrightInstall"] = enablePlaywrightInstall
},
@@ -74,7 +72,6 @@ public static string CreateSplitTestsMetadataJson(
string? mtpBaseArgs = null,
string? uncollectedMtpBaseArgs = null,
bool requiresNugets = false,
- bool requiresTestSdk = false,
bool requiresCliArchive = false,
bool enablePlaywrightInstall = false,
string[]? supportedOSes = null,
@@ -91,7 +88,6 @@ public static string CreateSplitTestsMetadataJson(
Properties = new Dictionary
{
["requiresNugets"] = requiresNugets,
- ["requiresTestSdk"] = requiresTestSdk,
["requiresCliArchive"] = requiresCliArchive,
["enablePlaywrightInstall"] = enablePlaywrightInstall
},
@@ -190,7 +186,6 @@ public static CanonicalMatrixEntry CreateMatrixEntry(
string? extraTestArgs = null,
string mtpBaseArgs = "",
bool requiresNugets = false,
- bool requiresTestSdk = false,
bool requiresCliArchive = false,
bool enablePlaywrightInstall = false,
string[]? supportedOSes = null,
@@ -211,7 +206,6 @@ public static CanonicalMatrixEntry CreateMatrixEntry(
Properties = new Dictionary
{
["requiresNugets"] = requiresNugets,
- ["requiresTestSdk"] = requiresTestSdk,
["requiresCliArchive"] = requiresCliArchive,
["enablePlaywrightInstall"] = enablePlaywrightInstall
},
diff --git a/tests/Infrastructure.Tests/WorkflowScripts/AutoRerunTransientCiFailuresTests.cs b/tests/Infrastructure.Tests/WorkflowScripts/AutoRerunTransientCiFailuresTests.cs
index 8ddbefc4b7b..f43730217a6 100644
--- a/tests/Infrastructure.Tests/WorkflowScripts/AutoRerunTransientCiFailuresTests.cs
+++ b/tests/Infrastructure.Tests/WorkflowScripts/AutoRerunTransientCiFailuresTests.cs
@@ -735,7 +735,6 @@ public async Task RepresentativeWorkflowFixturesStayAlignedWithCurrentWorkflowDe
[
"- name: Checkout code",
"- name: Set up .NET Core",
- "- name: Install sdk for nuget based testing",
"- name: Build test project",
"- name: Run tests (Windows)",
"- name: Upload logs, and test results",
diff --git a/tests/README.md b/tests/README.md
index f6b3c93e1a1..ca76486e056 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -3,14 +3,7 @@
The Helix CI job builds `tests/helix/send-to-helix-ci.proj`, which in turns builds the `Test` target on `tests/helix/send-to-helix-inner.proj`. This inner project uses the Helix SDK to construct `@(HelixWorkItem)`s, and send them to Helix to run.
- `tests/helix/send-to-helix-basictests.targets` - this prepares all the tests that don't need special preparation
-- `tests/helix/send-to-helix-endtoend-tests.targets` - this is for tests that require a SDK installed
-
-## Install SDK from artifacts
-
-1. `.\build.cmd -pack`
-2. `dotnet build tests\workloads.proj`
-
-.. which results in `artifacts\bin\dotnet-tests` which has a SDK (version from `global.json`) with the necessary components installed using packs from `artifacts/packages`.
+- `tests/helix/send-to-helix-endtoendtests.targets` - this is for end-to-end scenario tests that require Docker
## Controlling test runs on CI
@@ -29,6 +22,6 @@ Individual test projects can be opted-out by setting appropriate MSBuild propert
- Use `--filter-method`, `--filter-class`, or `--filter-namespace` (after `--`) to run specific tests.
- Set `TestCaptureOutput=false` as an environment variable to see the output on the command line.
-- Use `-tl:false` to disable msbuild's terminal logger so live output can be seen.
+- Set `MSBUILDTERMINALLOGGER=false` to disable MSBuild's terminal logger so live output can be seen.
-Example: `dotnet test --project tests/Aspire.Templates.Tests/Aspire.Templates.Tests.csproj --no-launch-profile -tl:false -- --filter-class "*.NewUpAndBuildStandaloneTemplateTests"`
+Example: `MSBUILDTERMINALLOGGER=false dotnet test --project tests/Aspire.Cli.EndToEnd.Tests/Aspire.Cli.EndToEnd.Tests.csproj --no-launch-profile -- --filter-class "*.TemplateVariantSmokeTests"`
diff --git a/tests/Shared/Aspire.Templates.Testing.props b/tests/Shared/Aspire.Templates.Testing.props
deleted file mode 100644
index 81eff702fb6..00000000000
--- a/tests/Shared/Aspire.Templates.Testing.props
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/tests/Shared/Aspire.Templates.Testing.targets b/tests/Shared/Aspire.Templates.Testing.targets
deleted file mode 100644
index 906d4a3b609..00000000000
--- a/tests/Shared/Aspire.Templates.Testing.targets
+++ /dev/null
@@ -1,146 +0,0 @@
-
-
-
- $(DotNetSdkCurrentVersionForTesting)
- $(DotNetSdkPreviousVersionForTesting)
-
-
- <_GlobalJsonContent>$([System.IO.File]::ReadAllText('$(RepoRoot)global.json'))
- <_DotNetCliVersionFromGlobalJson>$([System.Text.RegularExpressions.Regex]::Match($(_GlobalJsonContent), '(%3F<="dotnet": ").*(%3F=")'))
- $(_DotNetCliVersionFromGlobalJson)
-
- $(ArtifactsBinDir)dotnet-9\
- $(SdkDirForCurrentTFM).version-$(SdkVersionForCurrentTFM).stamp
-
- $(ArtifactsBinDir)dotnet-tests\
- $(SdkDirForCurrentAndPreviousTFM).version-$(SdkVersionForCurrentTFM)-$(SdkVersionForPreviousTFM).stamp
-
- $(ArtifactsBinDir)dotnet-8\
- $(SdkDirForPreviousTFM).version-$(SdkVersionForPreviousTFM).stamp
-
- $(ArtifactsBinDir)dotnet-10\
- $(SdkDirForNextTFM).version-$(SdkVersionForNextTFM).stamp
-
- $(ArtifactsBinDir)dotnet-tests\
- $(SdkDirForNextWithCurrentAndPreviousRuntimes).version-$(SdkVersionForNextTFM)-$(SdkVersionForCurrentTFM).stamp
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_SdkDirToInstallTo>$(SdkDirForNextWithCurrentAndPreviousRuntimes)
-
-
-
-
-
-
-
-
- <_SdkNextFile Include="$(SdkDirForNextTFM)\**\*" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_SrcProjects Include="$(RepoRoot)src\**\*.csproj"
- Exclude="$(RepoRoot)src\Aspire.ProjectTemplates\templates\**\*.csproj" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tests/Shared/Docker/Dockerfile.e2e b/tests/Shared/Docker/Dockerfile.e2e
index ee66407869e..0aec0a87e2e 100644
--- a/tests/Shared/Docker/Dockerfile.e2e
+++ b/tests/Shared/Docker/Dockerfile.e2e
@@ -22,12 +22,14 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG SKIP_SOURCE_BUILD=false
ARG UBUNTU_APT_MIRROR=
+COPY tests/Shared/Docker/apt-retry.sh /usr/local/bin/apt-retry
+RUN chmod +x /usr/local/bin/apt-retry
+
# Install native AOT build toolchain.
COPY tests/Shared/Docker/configure-ubuntu-apt-mirror.sh /usr/local/bin/
RUN sh /usr/local/bin/configure-ubuntu-apt-mirror.sh "$UBUNTU_APT_MIRROR"
RUN if [ "$SKIP_SOURCE_BUILD" != "true" ]; then \
- apt-get update -qq && \
- apt-get install -y --no-install-recommends clang zlib1g-dev && \
+ apt-retry install clang zlib1g-dev && \
rm -rf /var/lib/apt/lists/*; \
fi
@@ -54,39 +56,48 @@ ARG UBUNTU_APT_MIRROR=
# --- Common tooling (shared between dotnet and polyglot variants) ---
+COPY tests/Shared/Docker/apt-retry.sh /usr/local/bin/apt-retry
+RUN chmod +x /usr/local/bin/apt-retry
+
# Install gh CLI (needed by get-aspire-cli-pr.sh to download PR artifacts).
# Install Docker CLI plus the buildx/compose plugins used by publish and deploy flows.
COPY tests/Shared/Docker/configure-ubuntu-apt-mirror.sh /usr/local/bin/
RUN sh /usr/local/bin/configure-ubuntu-apt-mirror.sh "$UBUNTU_APT_MIRROR"
-RUN apt-get update -qq && \
- apt-get install -y --no-install-recommends gpg docker.io docker-buildx docker-compose-v2 unzip && \
+RUN apt-retry install gpg docker.io docker-buildx docker-compose-v2 unzip && \
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list && \
- apt-get update -qq && \
- apt-get install -y --no-install-recommends gh && \
+ apt-retry install gh && \
rm -rf /var/lib/apt/lists/*
# Install Node.js plus supported TypeScript AppHost toolchains.
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
- apt-get install -y --no-install-recommends nodejs && \
+ apt-retry install nodejs && \
npm install -g yarn@1.22.22 pnpm@10 && \
- curl -fsSL https://bun.sh/install | bash && \
+ for attempt in 1 2 3 4 5; do \
+ if curl -fsSL https://bun.sh/install -o /tmp/bun-install.sh && \
+ bash /tmp/bun-install.sh && \
+ [ -x /root/.bun/bin/bun ]; then \
+ rm -f /tmp/bun-install.sh; \
+ break; \
+ fi; \
+ rm -f /tmp/bun-install.sh; \
+ if [ "$attempt" = "5" ]; then exit 1; fi; \
+ sleep 10; \
+ done && \
ln -s /root/.bun/bin/bun /usr/local/bin/bun && \
ln -s /root/.bun/bin/bunx /usr/local/bin/bunx && \
rm -rf /var/lib/apt/lists/*
# Install Python and uv (needed for Python templates).
-RUN apt-get update -qq && \
- apt-get install -y --no-install-recommends python3 python3-pip python3-venv && \
+RUN apt-retry install python3 python3-pip python3-venv && \
rm -rf /var/lib/apt/lists/*
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:${PATH}"
# Install Java (needed for Java AppHost tests).
-RUN apt-get update -qq && \
- apt-get install -y --no-install-recommends openjdk-21-jdk && \
+RUN apt-retry install openjdk-21-jdk && \
rm -rf /var/lib/apt/lists/*
# --- Aspire CLI setup ---
diff --git a/tests/Shared/Docker/Dockerfile.e2e-podman b/tests/Shared/Docker/Dockerfile.e2e-podman
index 72e619f1dcd..919d49665c4 100644
--- a/tests/Shared/Docker/Dockerfile.e2e-podman
+++ b/tests/Shared/Docker/Dockerfile.e2e-podman
@@ -7,9 +7,11 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0
ARG UBUNTU_APT_MIRROR=
COPY tests/Shared/Docker/configure-ubuntu-apt-mirror.sh /usr/local/bin/
+COPY tests/Shared/Docker/apt-retry.sh /usr/local/bin/apt-retry
+RUN chmod +x /usr/local/bin/apt-retry
RUN sh /usr/local/bin/configure-ubuntu-apt-mirror.sh "$UBUNTU_APT_MIRROR"
-RUN apt-get update -qq && \
- apt-get install -y --no-install-recommends \
+
+RUN apt-retry install \
curl \
ca-certificates \
gpg \
@@ -23,8 +25,7 @@ RUN apt-get update -qq && \
| gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list && \
- apt-get update -qq && \
- apt-get install -y --no-install-recommends gh && \
+ apt-retry install gh && \
rm -rf /var/lib/apt/lists/*
# Rootful Podman-in-container is reliable here when it uses a dedicated vfs-backed storage root.
diff --git a/tests/Shared/Docker/Dockerfile.e2e-polyglot-base b/tests/Shared/Docker/Dockerfile.e2e-polyglot-base
index a4a6e095aa3..6bc9ddb130c 100644
--- a/tests/Shared/Docker/Dockerfile.e2e-polyglot-base
+++ b/tests/Shared/Docker/Dockerfile.e2e-polyglot-base
@@ -23,12 +23,14 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG SKIP_SOURCE_BUILD=false
ARG UBUNTU_APT_MIRROR=
+COPY tests/Shared/Docker/apt-retry.sh /usr/local/bin/apt-retry
+RUN chmod +x /usr/local/bin/apt-retry
+
# Install native AOT build toolchain.
COPY tests/Shared/Docker/configure-ubuntu-apt-mirror.sh /usr/local/bin/
RUN sh /usr/local/bin/configure-ubuntu-apt-mirror.sh "$UBUNTU_APT_MIRROR"
RUN if [ "$SKIP_SOURCE_BUILD" != "true" ]; then \
- apt-get update -qq && \
- apt-get install -y --no-install-recommends clang zlib1g-dev && \
+ apt-retry install clang zlib1g-dev && \
rm -rf /var/lib/apt/lists/*; \
fi
@@ -53,11 +55,13 @@ FROM ubuntu:24.04 AS runtime
ARG UBUNTU_APT_MIRROR=
-# Install base tools and Docker CLI.
COPY tests/Shared/Docker/configure-ubuntu-apt-mirror.sh /usr/local/bin/
+COPY tests/Shared/Docker/apt-retry.sh /usr/local/bin/apt-retry
+RUN chmod +x /usr/local/bin/apt-retry
RUN sh /usr/local/bin/configure-ubuntu-apt-mirror.sh "$UBUNTU_APT_MIRROR"
-RUN apt-get update -qq && \
- apt-get install -y --no-install-recommends \
+
+# Install base tools and Docker CLI.
+RUN apt-retry install \
ca-certificates curl gpg docker.io git libicu-dev unzip && \
rm -rf /var/lib/apt/lists/*
@@ -66,15 +70,24 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list && \
- apt-get update -qq && \
- apt-get install -y --no-install-recommends gh && \
+ apt-retry install gh && \
rm -rf /var/lib/apt/lists/*
# Install Node.js plus supported TypeScript AppHost toolchains.
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
- apt-get install -y --no-install-recommends nodejs && \
+ apt-retry install nodejs && \
npm install -g yarn@1.22.22 pnpm@10 && \
- curl -fsSL https://bun.sh/install | bash && \
+ for attempt in 1 2 3 4 5; do \
+ if curl -fsSL https://bun.sh/install -o /tmp/bun-install.sh && \
+ bash /tmp/bun-install.sh && \
+ [ -x /root/.bun/bin/bun ]; then \
+ rm -f /tmp/bun-install.sh; \
+ break; \
+ fi; \
+ rm -f /tmp/bun-install.sh; \
+ if [ "$attempt" = "5" ]; then exit 1; fi; \
+ sleep 10; \
+ done && \
ln -s /root/.bun/bin/bun /usr/local/bin/bun && \
ln -s /root/.bun/bin/bunx /usr/local/bin/bunx && \
rm -rf /var/lib/apt/lists/*
diff --git a/tests/Shared/Docker/Dockerfile.e2e-polyglot-java b/tests/Shared/Docker/Dockerfile.e2e-polyglot-java
index eeb3edd7dc1..7c80f4fc33a 100644
--- a/tests/Shared/Docker/Dockerfile.e2e-polyglot-java
+++ b/tests/Shared/Docker/Dockerfile.e2e-polyglot-java
@@ -6,7 +6,9 @@ FROM aspire-e2e-polyglot-base
ARG UBUNTU_APT_MIRROR=
COPY tests/Shared/Docker/configure-ubuntu-apt-mirror.sh /usr/local/bin/
+COPY tests/Shared/Docker/apt-retry.sh /usr/local/bin/apt-retry
+RUN chmod +x /usr/local/bin/apt-retry
RUN sh /usr/local/bin/configure-ubuntu-apt-mirror.sh "$UBUNTU_APT_MIRROR"
-RUN apt-get update -qq && \
- apt-get install -y --no-install-recommends openjdk-25-jdk && \
+
+RUN apt-retry install openjdk-25-jdk && \
rm -rf /var/lib/apt/lists/*
diff --git a/tests/Shared/Docker/apt-retry.sh b/tests/Shared/Docker/apt-retry.sh
new file mode 100644
index 00000000000..f25946721d8
--- /dev/null
+++ b/tests/Shared/Docker/apt-retry.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+set -eu
+
+if [ "$#" -lt 2 ] || [ "$1" != "install" ]; then
+ echo "Usage: apt-retry install " >&2
+ exit 64
+fi
+
+shift
+
+ubuntu_mirror=${APT_RETRY_UBUNTU_MIRROR-http://azure.archive.ubuntu.com/ubuntu}
+if [ -n "$ubuntu_mirror" ]; then
+ ubuntu_mirror=${ubuntu_mirror%/}
+ ubuntu_sources_configured=false
+
+ for source_file in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
+ [ -f "$source_file" ] || continue
+
+ if grep -Eq 'http://(azure\.)?archive\.ubuntu\.com/ubuntu|http://security\.ubuntu\.com/ubuntu' "$source_file"; then
+ ubuntu_sources_configured=true
+ fi
+
+ sed -i \
+ -e "s#http://azure.archive.ubuntu.com/ubuntu#$ubuntu_mirror#g" \
+ -e "s#http://archive.ubuntu.com/ubuntu#$ubuntu_mirror#g" \
+ -e "s#http://security.ubuntu.com/ubuntu#$ubuntu_mirror#g" \
+ "$source_file"
+ done
+
+ if [ "$ubuntu_sources_configured" = "true" ]; then
+ echo "using Ubuntu apt mirror: $ubuntu_mirror" >&2
+ fi
+fi
+
+for attempt in 1 2 3; do
+ echo "apt install attempt $attempt/3: $*" >&2
+ rm -rf /var/lib/apt/lists/*
+
+ apt-get \
+ -o Acquire::Retries=2 \
+ -o Acquire::http::Timeout=20 \
+ -o Acquire::https::Timeout=20 \
+ -o APT::Update::Error-Mode=any \
+ update -qq &&
+ apt-get \
+ -o Acquire::Retries=2 \
+ -o Acquire::http::Timeout=20 \
+ -o Acquire::https::Timeout=20 \
+ -o Dpkg::Use-Pty=0 \
+ install -y --no-install-recommends "$@" &&
+ exit 0
+ status=$?
+
+ if [ "$attempt" = "3" ]; then
+ exit "$status"
+ fi
+
+ echo "apt install failed on attempt $attempt/3; retrying..." >&2
+ sleep 5
+done
diff --git a/tests/Shared/Hex1bAutomatorTestHelpers.cs b/tests/Shared/Hex1bAutomatorTestHelpers.cs
index cb735929ccc..7ac876981cc 100644
--- a/tests/Shared/Hex1bAutomatorTestHelpers.cs
+++ b/tests/Shared/Hex1bAutomatorTestHelpers.cs
@@ -457,12 +457,19 @@ internal static async Task DeclineAgentInitPromptAsync(
var agentInitPrompt = new CellPatternSearcher()
.Find("configure AI agent environments");
+ var agentInitWorkflowPromptFound = false;
var agentInitFound = false;
var errorPromptFound = false;
// Wait for either the agent init prompt (new CLI) or the success prompt (old CLI).
await auto.WaitUntilAsync(s =>
{
+ if (IsAgentInitWorkflowPromptVisible(s))
+ {
+ agentInitWorkflowPromptFound = true;
+ return true;
+ }
+
if (agentInitPrompt.Search(s).Count > 0)
{
agentInitFound = true;
@@ -488,6 +495,13 @@ await auto.WaitUntilAsync(s =>
throw new InvalidOperationException($"Command failed with error prompt [{counter.Value} ERR:*] while waiting for the agent init prompt or success prompt.");
}
+ if (agentInitWorkflowPromptFound)
+ {
+ await auto.Ctrl().KeyAsync(Hex1bKey.C);
+ await auto.WaitForAnyPromptAsync(counter, effectiveTimeout);
+ return;
+ }
+
if (!agentInitFound)
{
counter.Increment();
@@ -497,13 +511,65 @@ await auto.WaitUntilAsync(s =>
await auto.WaitAsync(500);
await auto.TypeAsync("n");
- // Do not send Enter after typing "n" — the Spectre Console [Y/n] confirmation
- // prompt accepts a single character. Sending Enter risks a race: if aspire init
- // exits after reading "n" but before the Enter is delivered, bash receives the
- // Enter and executes a phantom blank command, advancing CMDCOUNT and desyncing
- // the test counter from the shell counter.
+ // Some prompts accept a single "n" immediately, while others require Enter.
+ // Only send Enter if the command did not already finish to avoid a phantom
+ // blank shell command that advances CMDCOUNT and desyncs prompt tracking.
+ var successPromptFound = false;
+ try
+ {
+ await auto.WaitUntilAsync(s =>
+ {
+ var successSearcher = new CellPatternSearcher()
+ .FindPattern(counter.Value.ToString())
+ .RightText(" OK] $ ");
+ successPromptFound = successSearcher.Search(s).Count > 0;
+
+ return successPromptFound;
+ }, timeout: TimeSpan.FromSeconds(2), description: $"success prompt [{counter.Value} OK] $ after agent init response");
+ }
+ catch (Hex1bAutomationException)
+ {
+ }
+
+ if (successPromptFound)
+ {
+ counter.Increment();
+ return;
+ }
+ var agentInitWorkflowStarted = false;
+ try
+ {
+ await auto.WaitUntilAsync(s =>
+ {
+ agentInitWorkflowStarted = IsAgentInitWorkflowPromptVisible(s);
+ return agentInitWorkflowStarted;
+ }, timeout: TimeSpan.FromMilliseconds(500), description: "agent init workflow prompt after agent init response");
+ }
+ catch (Hex1bAutomationException)
+ {
+ }
+
+ if (agentInitWorkflowStarted)
+ {
+ await auto.Ctrl().KeyAsync(Hex1bKey.C);
+ await auto.WaitForAnyPromptAsync(counter, effectiveTimeout);
+ return;
+ }
+
+ await auto.EnterAsync();
await auto.WaitForSuccessPromptFailFastAsync(counter, effectiveTimeout);
+
+ static bool IsAgentInitWorkflowPromptVisible(Hex1bTerminalSnapshot snapshot)
+ {
+ var skillLocationPrompt = new CellPatternSearcher()
+ .Find("Where should skill files be installed?");
+ var skillsPrompt = new CellPatternSearcher()
+ .Find("Which skills should be installed?");
+
+ return skillLocationPrompt.Search(snapshot).Count > 0
+ || skillsPrompt.Search(snapshot).Count > 0;
+ }
}
///
@@ -536,7 +602,7 @@ await auto.WaitUntilAsync(
case AspireTemplate.JsReact:
await auto.DownAsync();
await auto.WaitUntilAsync(
- s => new CellPatternSearcher().Find("> Starter App (ASP.NET Core/React, C# AppHost)").Search(s).Count > 0,
+ s => new CellPatternSearcher().Find("> Starter App (ASP.NET Core/React").Search(s).Count > 0,
timeout: TimeSpan.FromSeconds(5),
description: "JS React template selected");
await auto.EnterAsync();
@@ -546,7 +612,7 @@ await auto.WaitUntilAsync(
await auto.DownAsync();
await auto.DownAsync();
await auto.WaitUntilAsync(
- s => new CellPatternSearcher().Find("> Starter App (Express/React, TypeScript AppHost)").Search(s).Count > 0,
+ s => new CellPatternSearcher().Find("> Starter App (Express/React").Search(s).Count > 0,
timeout: TimeSpan.FromSeconds(5),
description: "Express React template selected");
await auto.EnterAsync();
@@ -557,7 +623,7 @@ await auto.WaitUntilAsync(
await auto.DownAsync();
await auto.DownAsync();
await auto.WaitUntilAsync(
- s => new CellPatternSearcher().Find("> Starter App (FastAPI/React, TypeScript AppHost)").Search(s).Count > 0,
+ s => new CellPatternSearcher().Find("> Starter App (FastAPI/React").Search(s).Count > 0,
timeout: TimeSpan.FromSeconds(5),
description: "Python React template selected");
await auto.EnterAsync();
@@ -636,6 +702,7 @@ await auto.WaitUntilAsync(
if (!useRedisCache)
{
await auto.TypeAsync("n");
+ await auto.EnterAsync();
}
else
{
@@ -657,6 +724,35 @@ await auto.WaitUntilAsync(
await auto.DeclineAgentInitPromptAsync(counter);
}
+ ///
+ /// Installs dependencies, runs aspire restore, and then type-checks the generated TypeScript project.
+ ///
+ internal static async Task AspireRestoreAndTypeCheckTypeScriptAsync(
+ this Hex1bTerminalAutomator auto,
+ SequenceCounter counter,
+ string typeCheckCommand = "npx --no-install tsc --noEmit --project tsconfig.apphost.json",
+ TimeSpan? installTimeout = null,
+ TimeSpan? restoreTimeout = null,
+ TimeSpan? typeCheckTimeout = null)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(typeCheckCommand);
+
+ await auto.TypeAsync("npm install");
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptFailFastAsync(counter, installTimeout ?? TimeSpan.FromMinutes(5));
+
+ await auto.TypeAsync("aspire restore");
+ await auto.EnterAsync();
+ await auto.WaitUntilTextAsync(
+ "SDK code restored successfully",
+ timeout: restoreTimeout ?? TimeSpan.FromMinutes(3));
+ await auto.WaitForSuccessPromptAsync(counter);
+
+ await auto.TypeAsync(typeCheckCommand);
+ await auto.EnterAsync();
+ await auto.WaitForSuccessPromptFailFastAsync(counter, typeCheckTimeout ?? TimeSpan.FromMinutes(2));
+ }
+
///
/// Runs aspire init --language csharp and handles the NuGet.config, URLs, and agent init prompts.
///
diff --git a/tests/Shared/InstallSdk.props b/tests/Shared/InstallSdk.props
deleted file mode 100644
index 168af4a8953..00000000000
--- a/tests/Shared/InstallSdk.props
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
- <_DotNetInstallScriptName Condition="!$([MSBuild]::IsOSPlatform('windows'))">dotnet-install.sh
- <_DotNetInstallScriptName Condition=" $([MSBuild]::IsOSPlatform('windows'))">dotnet-install.ps1
-
- <_DotNetInstallScriptPath>$(ArtifactsObjDir)$(_DotNetInstallScriptName)
-
-
diff --git a/tests/Shared/InstallSdk.targets b/tests/Shared/InstallSdk.targets
deleted file mode 100644
index 5d91da67242..00000000000
--- a/tests/Shared/InstallSdk.targets
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_DotNetInstallCommand Condition="!$([MSBuild]::IsOSPlatform('windows'))"
- >$(_DotNetInstallScriptPath) -i $(SdkTargetDir) -v $(SdkVersionToInstall)
- <_DotNetInstallCommand Condition="$([MSBuild]::IsOSPlatform('windows'))"
- >$(_DotNetInstallScriptPath) -InstallDir $(SdkTargetDir) -Version $(SdkVersionToInstall)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tests/Shared/TemplatesTesting/BuildEnvironment.cs b/tests/Shared/TemplatesTesting/BuildEnvironment.cs
index 424372f92bd..789a7997181 100644
--- a/tests/Shared/TemplatesTesting/BuildEnvironment.cs
+++ b/tests/Shared/TemplatesTesting/BuildEnvironment.cs
@@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
-using Microsoft.Extensions.Diagnostics.Latency;
-using Xunit.Sdk;
namespace Aspire.Templates.Tests;
@@ -28,8 +26,8 @@ public class BuildEnvironment
: Path.GetTempPath();
public static readonly TestTargetFramework DefaultTargetFramework = ComputeDefaultTargetFramework();
- public static readonly string TestAssetsPath = Path.Combine(AppContext.BaseDirectory, "testassets");
- public static readonly string TestRootPath = Path.Combine(TempDir, "templates-testroot");
+ public static readonly string TestAssetsPath = Path.Combine(AppContext.BaseDirectory, "testassets");
+ public static readonly string TestRootPath = Path.Combine(TempDir, "templates-testroot");
public static bool IsRunningOnHelix => Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT") is not null;
public static bool IsRunningOnCIBuildMachine => Environment.GetEnvironmentVariable("BUILD_BUILDID") is not null;
@@ -37,73 +35,27 @@ public class BuildEnvironment
public static bool IsRunningOnCI => IsRunningOnHelix || IsRunningOnCIBuildMachine || IsRunningOnGithubActions;
public static bool ShouldRunPlaywrightTests => PlaywrightProvider.HasPlaywrightSupport && !EnvironmentVariables.RunOnlyBasicBuildTemplatesTests;
- private static readonly Lazy s_instance_80 = new(() =>
- new BuildEnvironment(sdkDirName: "dotnet-8"));
+ private static readonly Lazy s_instance = new(() => new BuildEnvironment());
- private static readonly Lazy s_instance_90 = new(() =>
- new BuildEnvironment(sdkDirName: "dotnet-9"));
+ public static BuildEnvironment ForDefaultFramework => s_instance.Value;
- private static readonly Lazy s_instance_100 = new(() =>
- new BuildEnvironment(sdkDirName: "dotnet-10"));
-
- private static readonly Lazy s_instance_100_90_80 = new(() =>
- new BuildEnvironment(sdkDirName: "dotnet-tests"));
-
- public static BuildEnvironment ForPreviousSdkOnly => s_instance_80.Value;
- public static BuildEnvironment ForCurrentSdkOnly => s_instance_90.Value;
- public static BuildEnvironment ForNextSdkOnly => s_instance_100.Value;
- public static BuildEnvironment ForNextSdkWithCurrentAndPreviousRuntimes => s_instance_100_90_80.Value;
-
- public static BuildEnvironment ForDefaultFramework =>
- DefaultTargetFramework switch
- {
- TestTargetFramework.Previous => ForPreviousSdkOnly,
-
- // Use current+previous to allow running tests on helix built with 9.0 sdk
- // but targeting 8.0 tfm
- TestTargetFramework.Current => ForNextSdkWithCurrentAndPreviousRuntimes,
-
- _ => throw new ArgumentOutOfRangeException(nameof(DefaultTargetFramework))
- };
-
- public BuildEnvironment(bool useSystemDotNet = false, string sdkDirName = "dotnet-tests")
+ public BuildEnvironment(bool useSystemDotNet = false)
{
- UsesCustomDotNet = !useSystemDotNet;
RepoRoot = TestUtils.FindRepoRoot();
string sdkForTemplatePath;
if (RepoRoot is not null)
{
- // Local run
- if (!useSystemDotNet)
+ var repoDotNetPath = GetRepoDotNetPath(RepoRoot);
+ if (!useSystemDotNet && File.Exists(repoDotNetPath))
{
- var sdkFromArtifactsPath = Path.Combine(RepoRoot!.FullName, "artifacts", "bin", sdkDirName);
- if (Directory.Exists(sdkFromArtifactsPath))
- {
- sdkForTemplatePath = Path.GetFullPath(sdkFromArtifactsPath);
- }
- else
- {
- string buildCmd = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".\\build.cmd" : "./build.sh";
- string workloadsProjString = Path.Combine("tests", "workloads.proj");
- throw new XunitException(
- $"Could not find a SDK with the necessary components installed at {sdkFromArtifactsPath} computed from {nameof(RepoRoot)}={RepoRoot}." +
- $" Build all the packages with '{buildCmd} -pack'." +
- $" Then install the SDK with 'dotnet build {workloadsProjString}'." +
- " See https://github.com/microsoft/aspire/tree/main/tests/Aspire.Templates.Tests#readme for more details.");
- }
+ sdkForTemplatePath = Path.GetDirectoryName(repoDotNetPath)!;
+ UsesCustomDotNet = true;
}
else
{
- string? dotnetPath = Environment.GetEnvironmentVariable("PATH")!
- .Split(Path.PathSeparator)
- .Select(path => Path.Combine(path, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"))
- .FirstOrDefault(File.Exists);
- if (dotnetPath is null)
- {
- throw new ArgumentException($"Could not find dotnet.exe in PATH={Environment.GetEnvironmentVariable("PATH")}");
- }
- sdkForTemplatePath = Path.GetDirectoryName(dotnetPath)!;
+ sdkForTemplatePath = FindDotNetDirectory();
+ UsesCustomDotNet = false;
}
#if RELEASE
@@ -116,38 +68,12 @@ public BuildEnvironment(bool useSystemDotNet = false, string sdkDirName = "dotne
}
else
{
- if (useSystemDotNet)
- {
- string? dotnetPath = Environment.GetEnvironmentVariable("PATH")!
- .Split(Path.PathSeparator)
- .Select(path => Path.Combine(path, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"))
- .FirstOrDefault(File.Exists);
- if (dotnetPath is null)
- {
- throw new ArgumentException($"Could not find dotnet.exe in PATH={Environment.GetEnvironmentVariable("PATH")}");
- }
- sdkForTemplatePath = Path.GetDirectoryName(dotnetPath)!;
- }
- else
- {
- // CI - helix
- if (string.IsNullOrEmpty(EnvironmentVariables.SdkForTemplateTestingPath))
- {
- throw new ArgumentException($"Environment variable SDK_FOR_TEMPLATES_TESTING_PATH is unset");
- }
-
- string? baseDir = Path.GetDirectoryName(EnvironmentVariables.SdkForTemplateTestingPath);
- if (baseDir is null)
- {
- throw new ArgumentException($"Cannot find base directory for SDK_FOR_TEMPLATES_TESTING_PATH - {baseDir}");
- }
-
- sdkForTemplatePath = Path.Combine(baseDir, sdkDirName);
- }
+ sdkForTemplatePath = FindDotNetDirectory();
+ UsesCustomDotNet = false;
if (string.IsNullOrEmpty(EnvironmentVariables.BuiltNuGetsPath) || !Directory.Exists(EnvironmentVariables.BuiltNuGetsPath))
{
- throw new ArgumentException($"Cannot find 'BUILT_NUGETS_PATH={EnvironmentVariables.BuiltNuGetsPath}' or {BuiltNuGetsPath}");
+ throw new ArgumentException($"Cannot find 'BUILT_NUGETS_PATH={EnvironmentVariables.BuiltNuGetsPath}'");
}
BuiltNuGetsPath = EnvironmentVariables.BuiltNuGetsPath;
}
@@ -169,7 +95,10 @@ public BuildEnvironment(bool useSystemDotNet = false, string sdkDirName = "dotne
EnvVars["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1";
EnvVars["PATH"] = $"{sdkForTemplatePath}{Path.PathSeparator}{Environment.GetEnvironmentVariable("PATH")}";
}
- EnvVars["NUGET_PACKAGES"] = NuGetPackagesPath!;
+ if (NuGetPackagesPath is not null)
+ {
+ EnvVars["NUGET_PACKAGES"] = NuGetPackagesPath;
+ }
EnvVars["BUILT_NUGETS_PATH"] = BuiltNuGetsPath;
EnvVars["TreatWarningsAsErrors"] = "true";
// Set DEBUG_SESSION_PORT='' to avoid the app from the tests connecting
@@ -187,11 +116,7 @@ public BuildEnvironment(bool useSystemDotNet = false, string sdkDirName = "dotne
EnvVars["ASPIRE_DEVELOPER_CERTIFICATE_DEFAULT_HTTPS_TERMINATION"] = "false";
}
- DotNet = Path.Combine(sdkForTemplatePath!, "dotnet");
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- DotNet += ".exe";
- }
+ DotNet = Path.Combine(sdkForTemplatePath, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet");
if (!string.IsNullOrEmpty(EnvironmentVariables.TestLogPath))
{
@@ -208,7 +133,7 @@ public BuildEnvironment(bool useSystemDotNet = false, string sdkDirName = "dotne
Directory.CreateDirectory(TestRootPath);
Console.WriteLine($"*** Using Sdk path: {sdkForTemplatePath}");
- if (UsesCustomDotNet)
+ if (NuGetPackagesPath is not null)
{
if (EnvironmentVariables.IsRunningOnCI)
{
@@ -220,7 +145,7 @@ public BuildEnvironment(bool useSystemDotNet = false, string sdkDirName = "dotne
}
else
{
- if (NuGetPackagesPath is not null && Directory.Exists(NuGetPackagesPath))
+ if (Directory.Exists(NuGetPackagesPath))
{
foreach (var dir in Directory.GetDirectories(NuGetPackagesPath, "aspire*"))
{
@@ -284,6 +209,22 @@ public BuildEnvironment(BuildEnvironment otherBuildEnvironment)
TemplatesCustomHive = otherBuildEnvironment.TemplatesCustomHive;
}
+ private static string GetRepoDotNetPath(DirectoryInfo repoRoot)
+ => Path.Combine(repoRoot.FullName, ".dotnet", RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet");
+
+ private static string FindDotNetDirectory()
+ {
+ var dotnetFileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet";
+ var dotnetPath = Environment.GetEnvironmentVariable("PATH")!
+ .Split(Path.PathSeparator)
+ .Select(path => Path.Combine(path, dotnetFileName))
+ .FirstOrDefault(File.Exists);
+
+ return dotnetPath is not null
+ ? Path.GetDirectoryName(dotnetPath)!
+ : throw new ArgumentException($"Could not find {dotnetFileName} in PATH={Environment.GetEnvironmentVariable("PATH")}");
+ }
+
private static TestTargetFramework ComputeDefaultTargetFramework()
=> EnvironmentVariables.DefaultTFMForTesting?.ToLowerInvariant() switch
{
@@ -292,7 +233,6 @@ private static TestTargetFramework ComputeDefaultTargetFramework()
"net10.0" => TestTargetFramework.Next,
_ => throw new ArgumentOutOfRangeException(nameof(EnvironmentVariables.DefaultTFMForTesting), EnvironmentVariables.DefaultTFMForTesting, "Invalid value")
};
-
}
public enum TestTargetFramework
diff --git a/tests/Shared/TemplatesTesting/EnvironmentVariables.cs b/tests/Shared/TemplatesTesting/EnvironmentVariables.cs
index ba8513aece0..68abd4e7bbc 100644
--- a/tests/Shared/TemplatesTesting/EnvironmentVariables.cs
+++ b/tests/Shared/TemplatesTesting/EnvironmentVariables.cs
@@ -7,7 +7,6 @@ namespace Aspire.Templates.Tests;
public static class EnvironmentVariables
{
- public static readonly string? SdkForTemplateTestingPath = Environment.GetEnvironmentVariable("SDK_FOR_TEMPLATES_TESTING_PATH");
public static readonly string? TestLogPath = Environment.GetEnvironmentVariable("TEST_LOG_PATH");
public static readonly string? SkipProjectCleanup = Environment.GetEnvironmentVariable("SKIP_PROJECT_CLEANUP");
public static readonly string? BuiltNuGetsPath = Environment.GetEnvironmentVariable("BUILT_NUGETS_PATH");
diff --git a/tests/helix/send-to-helix-basictests.targets b/tests/helix/send-to-helix-basictests.targets
index 944f064920c..7d5e80ce4c7 100644
--- a/tests/helix/send-to-helix-basictests.targets
+++ b/tests/helix/send-to-helix-basictests.targets
@@ -4,8 +4,6 @@
$(TestArchiveTestsDir)**/*.zip
$(BuildHelixWorkItemsDependsOn);BuildHelixWorkItemsForDefaultTests
true
-
- true
false
^(.*?)(-net[\d.]+)?$
diff --git a/tests/helix/send-to-helix-buildonhelixtests.targets b/tests/helix/send-to-helix-buildonhelixtests.targets
index 2cbeed03d0b..1fcf678222a 100644
--- a/tests/helix/send-to-helix-buildonhelixtests.targets
+++ b/tests/helix/send-to-helix-buildonhelixtests.targets
@@ -2,7 +2,6 @@
$(BuildHelixWorkItemsDependsOn);BuildHelixWorkItemsForBuildOnHelixTests
- true
true
true
@@ -48,8 +47,6 @@
<_TestRunCommand>dotnet build -bl:$(_HelixLogsPath)/build.binlog /p:TreatWarningsAsErrors=true $(_ShellCommandSeparator) $(_TestRunCommand)
<_TestRunCommand Condition="'$(HelixPerWorkItemPreCommand)' != ''">$(HelixPerWorkItemPreCommand) $(_ShellCommandSeparator) $(_TestRunCommand)
- <_SetPathEnvVar Condition="'$(OS)' != 'Windows_NT'">PATH=${SDK_FOR_TEMPLATES_TESTING_PATH}:$PATH
- <_SetPathEnvVar Condition="'$(OS)' == 'Windows_NT'">PATH=%SDK_FOR_TEMPLATES_TESTING_PATH%%3B%PATH%
@@ -58,7 +55,7 @@
%(Identity)
- $(_EnvVarSetKeyword) "TEST_NAME=%(FileName)" $(_ShellCommandSeparator) $(_EnvVarSetKeyword) "$(_SetPathEnvVar)"
+ $(_EnvVarSetKeyword) "TEST_NAME=%(FileName)"
cd tests/%(FileName) %3B $(_TestRunCommand)
00:15:00
diff --git a/tests/helix/send-to-helix-ci.proj b/tests/helix/send-to-helix-ci.proj
index cb898b769d2..ce67655f3d4 100644
--- a/tests/helix/send-to-helix-ci.proj
+++ b/tests/helix/send-to-helix-ci.proj
@@ -4,7 +4,6 @@
-
diff --git a/tests/helix/send-to-helix-endtoendtests.targets b/tests/helix/send-to-helix-endtoendtests.targets
index 1e9b2d70f33..38d1b65f661 100644
--- a/tests/helix/send-to-helix-endtoendtests.targets
+++ b/tests/helix/send-to-helix-endtoendtests.targets
@@ -2,7 +2,6 @@
$(BuildHelixWorkItemsDependsOn);BuildHelixWorkItemsForEnd2EndTests
- true
diff --git a/tests/helix/send-to-helix-inner.proj b/tests/helix/send-to-helix-inner.proj
index 92481d23d8a..a7fc7b9d232 100644
--- a/tests/helix/send-to-helix-inner.proj
+++ b/tests/helix/send-to-helix-inner.proj
@@ -13,7 +13,7 @@
true
- $(DotNetSdkPreviousVersionForTesting)
+ $(DotNetSdkVersionForHelixWorkItems)
<_DotNetToolJsonPath>$(RepoRoot).config/dotnet-tools.json
@@ -22,7 +22,6 @@
<_AzureFunctionsCliUrl Condition="'$(OS)' == 'Windows_NT'">https://github.com/Azure/azure-functions-core-tools/releases/download/4.0.7512/Azure.Functions.Cli.min.win-x64.4.0.7512.zip
<_AzureFunctionsCliUrl Condition="'$(OS)' != 'Windows_NT'">https://github.com/Azure/azure-functions-core-tools/releases/download/4.0.7512/Azure.Functions.Cli.linux-x64.4.0.7512.zip
- <_DefaultSdkDirNameForTests>dotnet-tests
@@ -50,8 +49,8 @@
<_DisableRyukForTestcontainersWorkItemCommand Condition="'$(OS)' != 'Windows_NT'">if $(_UsesTestcontainersWorkItemCommand)%3B then echo "Disabling Testcontainers Ryuk for this Helix work item." $(_ShellCommandSeparator) export TESTCONTAINERS_RYUK_DISABLED=true%3B fi
<_CleanupDockerForTestcontainersWorkItemCommand Condition="'$(OS)' != 'Windows_NT'">if $(_UsesTestcontainersWorkItemCommand)%3B then echo "Cleaning up Docker resources for this Testcontainers Helix work item." $(_ShellCommandSeparator) $(_ShutdownDockerContainersCommand) $(_ShellCommandSeparator) $(_DeleteDockerVolumesCommand) $(_ShellCommandSeparator) docker network prune -f%3B fi
- <_CleanupProcessesCommand Condition="'$(OS)' == 'Windows_NT'">powershell -ExecutionPolicy ByPass -NoProfile -command "& get-ciminstance win32_process | where-object ExecutablePath -Match 'dotnet-tests|dcp.exe' | foreach-object { echo $_.ProcessId $_.ExecutablePath %3B stop-process -id $_.ProcessId -force -ErrorAction SilentlyContinue }"
- <_CleanupProcessesCommand Condition="'$(OS)' != 'Windows_NT'">pgrep -lf "dotnet-tests|dcp.exe" | awk '{print %3B system("kill -9 "$1)}'
+ <_CleanupProcessesCommand Condition="'$(OS)' == 'Windows_NT'">powershell -ExecutionPolicy ByPass -NoProfile -command "& get-ciminstance win32_process | where-object ExecutablePath -Match 'dcp.exe' | foreach-object { echo $_.ProcessId $_.ExecutablePath %3B stop-process -id $_.ProcessId -force -ErrorAction SilentlyContinue }"
+ <_CleanupProcessesCommand Condition="'$(OS)' != 'Windows_NT'">pgrep -lf "dcp.exe" | awk '{print %3B system("kill -9 "$1)}'
<_WaitForDcpCommand Condition="'$(OS)' != 'Windows_NT'">echo "Waiting for dcp process to exit..."; start_time=$(date +%s); echo "Start time: $start_time"; timeout=120; while pgrep -lf "dcp.exe" > /dev/null && [ $timeout -gt 0 ]; do sleep 1; timeout=$((timeout - 1)); done; end_time=$(date +%s); echo "End time: $end_time"; if pgrep -lf "dcp.exe" > /dev/null; then echo "dcp process did not exit within the timeout period."; exit 1; fi
@@ -92,17 +91,7 @@
-
-
-
-
-
-
-
-
-
-
-
+
@@ -160,14 +149,8 @@
-
-
- set PATH=%HELIX_CORRELATION_PAYLOAD%\$(_DefaultSdkDirNameForTests)%3B%PATH%
- export PATH=$HELIX_CORRELATION_PAYLOAD/$(_DefaultSdkDirNameForTests):$PATH
-
- $(HelixPerWorkItemPreCommand) & set DOTNET_ROOT=%HELIX_CORRELATION_PAYLOAD%\$(_DefaultSdkDirNameForTests)
- $(HelixPerWorkItemPreCommand) && export DOTNET_ROOT=$HELIX_CORRELATION_PAYLOAD/$(_DefaultSdkDirNameForTests)
- $(HelixPerWorkItemPreCommand) && $(_DisableRyukForTestcontainersWorkItemCommand)
+
+ $(_DisableRyukForTestcontainersWorkItemCommand)
@@ -194,8 +177,7 @@
-
-
+
diff --git a/tests/helix/send-to-helix-templatestests.targets b/tests/helix/send-to-helix-templatestests.targets
deleted file mode 100644
index 7f5a55cb605..00000000000
--- a/tests/helix/send-to-helix-templatestests.targets
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
- $(BuildHelixWorkItemsDependsOn);BuildHelixWorkItemsForTemplateTests
- true
- true
-
- Aspire.Templates.Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <_TestRunCommandArguments Condition="'$(OS)' != 'Windows_NT'" Include="${TEST_ARGS}" />
- <_TestRunCommandArguments Condition="'$(OS)' == 'Windows_NT'" Include="%TEST_ARGS%" />
-
-
-
- <_TestRunCommand Condition="'$(RunWithCodeCoverage)' == 'true'">@(_TestCoverageCommand, ' ') "@(_TestRunCommandArguments, ' ')"
- <_TestRunCommand Condition="'$(RunWithCodeCoverage)' != 'true'">@(_TestRunCommandArguments, ' ')
- <_TestRunCommand Condition="'$(HelixPerWorkItemPreCommand)' != ''">$(HelixPerWorkItemPreCommand) $(_ShellCommandSeparator) $(_TestRunCommand)
-
-
-
-
-
-
-
-
- <_TemplateTestsClassNames Include="@(_TemplateTestsClassNamesRaw->'%(Identity)'->Replace('class:', ''))" />
-
-
-
-
-
-
-
-
-
-
-
- <_TemplateTestsClassNames TestNameSuffix="$([System.String]::Copy('%(Identity)').Replace('Aspire.Template.Tests.', ''))" PreCommands="" />
-
-
- $(TestArchiveTestsDirForTemplateTests)\$(TestProjectName).zip
-
- $(_EnvVarSetKeyword) "TEST_NAME=%(FileName)"
- %(PreCommands) $(_ShellCommandSeparator) $(_EnvVarSetKeyword) "CODE_COV_FILE_SUFFIX=-%(TestNameSuffix)"
- %(PreCommands) $(_ShellCommandSeparator) $(_EnvVarSetKeyword) "TEST_NAME_SUFFIX=%(TestNameSuffix)"
-
-
- %(PreCommands) $(_ShellCommandSeparator) set "TEST_ARGS=--filter-not-trait quarantined=true --filter-not-trait category=failing --filter-class %(Identity)"
- %(PreCommands) $(_ShellCommandSeparator) export "TEST_ARGS=--filter-not-trait quarantined=true --filter-not-trait category=failing --filter-class %(Identity)"
-
- $(_TestRunCommand)
- $(_workItemTimeout)
-
- logs/$(TestProjectName)-%(TestNameSuffix).trx
-
-
-
-
diff --git a/tests/workloads.proj b/tests/workloads.proj
deleted file mode 100644
index cea633c14d3..00000000000
--- a/tests/workloads.proj
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
- Debug
-
-
-
-
-