Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 215 additions & 0 deletions managed/.github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
name: CI

on:
push:
branches: ["**"]
pull_request:

jobs:
# Linux: build everything and run the unit tests. The net472 / net462
# targets build here against the Microsoft.NETFramework.ReferenceAssemblies
# package (compile-only).
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Build
run: dotnet build AppMap.sln -c Release
- name: Test
run: dotnet test test/AppMap.Agent.Tests/AppMap.Agent.Tests.csproj -c Release --no-build

# Windows: real .NET Framework reference assemblies, and — the point of
# this job — actual validation of the classic (native) Windows PDB
# fallback. HelloAppMap is built with <DebugType>full</DebugType>, which
# produces a non-portable PDB, forcing SourceLocator down the
# WindowsPdbReader/diasymreader path. We then assert the recorded AppMap
# carries source locations, which only happens if that path works.
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Build solution
run: dotnet build AppMap.sln -c Release
- name: Test
run: dotnet test test/AppMap.Agent.Tests/AppMap.Agent.Tests.csproj -c Release --no-build

- name: Record HelloAppMap with a native (full) PDB
shell: pwsh
working-directory: examples/HelloAppMap
env:
APPMAP_RECORD_PROCESS: "true"
APPMAP_DEBUG: "true"
run: |
dotnet build -c Release -p:DebugType=full
$hook = "$PWD/../../src/AppMap.StartupHook/bin/Release/net8.0/AppMap.StartupHook.dll"
$env:DOTNET_STARTUP_HOOKS = (Resolve-Path $hook)
dotnet run -c Release --no-build

- name: Assert source locations resolved from the Windows PDB
shell: pwsh
working-directory: examples/HelloAppMap
run: |
$map = Get-ChildItem -Recurse tmp/appmap/process_recording/*.appmap.json |
Select-Object -First 1
if (-not $map) { throw "no AppMap was produced" }
$json = Get-Content $map.FullName -Raw | ConvertFrom-Json
$locations = @()
function Walk($node) {
if ($node.location) { $script:locations += $node.location }
foreach ($child in $node.children) { Walk $child }
}
foreach ($root in $json.classMap) { Walk $root }
Write-Host "resolved $($locations.Count) source location(s):"
$locations | ForEach-Object { Write-Host " $_" }
if ($locations.Count -eq 0) {
throw "Windows PDB fallback produced no source locations"
}
# The path must be repo-relative with forward slashes, not the
# build machine's C:\...; this is what lets a Windows recording be
# queried on Linux (see the cross-platform-query job).
$abs = $locations | Where-Object { $_ -match '\\' -or $_ -match '^[A-Za-z]:[\\/]' }
if ($abs) { throw "absolute/backslash path(s) leaked: $abs" }

- name: Upload the Windows-recorded AppMap
uses: actions/upload-artifact@v4
with:
name: windows-appmaps
path: examples/HelloAppMap/tmp/appmap

# R4 cross-platform: a map RECORDED ON WINDOWS (above) must INDEX and QUERY
# ON LINUX, with its source paths resolving against a Linux checkout. This
# is the agent's headline claim and is only possible because SourceLocator
# emits repo-relative paths.
cross-platform-query:
needs: windows
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install AppMap CLI
run: |
npm install -g @appland/appmap --ignore-scripts
# index/query use better-sqlite3's native binding, skipped by --ignore-scripts.
( cd "$(npm root -g)/@appland/appmap" && npm rebuild better-sqlite3 )
- name: Download the Windows-recorded AppMap
uses: actions/download-artifact@v4
with:
name: windows-appmaps
path: windows-maps
- name: Index and query the Windows map on Linux
run: |
appmap index --appmap-dir windows-maps
map=$(find windows-maps -name '*.appmap.json' | head -1)
test -n "$map" || { echo "no map in artifact"; exit 1; }
appmap sequence-diagram -f json "$map" > /dev/null
echo "queried $map on Linux"
- name: Assert the Windows paths resolve against the Linux checkout
run: |
python3 - <<'PY'
import json, glob, os
maps = glob.glob('windows-maps/**/*.appmap.json', recursive=True)
assert maps, "no maps in artifact"
doc = json.load(open(maps[0]))
locs = set()
def walk(n):
if n.get('location'): locs.add(n['location'].split(':')[0])
for c in n.get('children', []): walk(c)
for r in doc.get('classMap', []): walk(r)
assert locs, "Windows map carried no source locations"
missing = [p for p in locs if not os.path.isfile(p)]
print("locations:", sorted(locs))
assert not missing, f"paths do not resolve on Linux: {missing}"
print(f"OK: {len(locs)} Windows-recorded path(s) resolve against the Linux checkout")
PY

# R1: validate the classic-ASP.NET (System.Web) IHttpModule actually records
# on a real IIS host (IIS Express) — the legacy .NET Framework attach path,
# which the unit/PDB jobs don't exercise. The app references no AppMap
# source; the agent attaches only by being registered in web.config.
system-web-iis:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Build the System.Web module (net472)
run: dotnet build src/AppMap.SystemWeb/AppMap.SystemWeb.csproj -c Release
- name: Record a request through IIS Express
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$site = Join-Path $env:RUNNER_TEMP "site"
New-Item -ItemType Directory -Force (Join-Path $site "bin") | Out-Null
Copy-Item harness/fixtures/SystemWebApp/* $site -Recurse -Force
# Drop the agent (module + Agent + Harmony + deps) into the app's bin.
Copy-Item src/AppMap.SystemWeb/bin/Release/net472/*.dll (Join-Path $site "bin") -Force

# The agent is netstandard2.0; under .NET Framework its transitive
# System.* package assemblies need binding redirects or they throw
# FileLoadException at runtime (and the module silently records
# nothing). Generate redirects from the actual bin assemblies.
$deps = ""
Get-ChildItem (Join-Path $site "bin\*.dll") | ForEach-Object {
try {
$an = [System.Reflection.AssemblyName]::GetAssemblyName($_.FullName)
$tokenBytes = $an.GetPublicKeyToken()
if ($tokenBytes -and $tokenBytes.Length -gt 0) {
$token = ($tokenBytes | ForEach-Object { $_.ToString("x2") }) -join ""
$deps += "<dependentAssembly><assemblyIdentity name='$($an.Name)' publicKeyToken='$token' culture='neutral' /><bindingRedirect oldVersion='0.0.0.0-$($an.Version)' newVersion='$($an.Version)' /></dependentAssembly>"
}
} catch {}
}
$runtime = "<runtime><assemblyBinding xmlns='urn:schemas-microsoft-com:asm.v1'>$deps</assemblyBinding></runtime>"
$cfg = Join-Path $site "web.config"
(Get-Content $cfg -Raw) -replace '</configuration>', "$runtime</configuration>" | Set-Content $cfg
Write-Host "injected $((($deps -split '</dependentAssembly>').Count) - 1) binding redirect(s)"

$env:APPMAP_CONFIG_FILE = Join-Path $site "appmap.yml"
$env:APPMAP_OUTPUT_DIRECTORY = Join-Path $site "tmp\appmap"
$env:APPMAP_DEBUG = "true"

$exe = "C:\Program Files\IIS Express\iisexpress.exe"
if (-not (Test-Path $exe)) { $exe = "C:\Program Files (x86)\IIS Express\iisexpress.exe" }
if (-not (Test-Path $exe)) { throw "IIS Express not found on the runner" }

# Capture the host's output (agent debug + any FileLoadException).
$out = Join-Path $site "iisexpress.out.log"
$err = Join-Path $site "iisexpress.err.log"
$proc = Start-Process $exe -ArgumentList "/path:$site","/port:8088" -PassThru `
-RedirectStandardOutput $out -RedirectStandardError $err
try {
$served = $false
for ($i = 0; $i -lt 30; $i++) {
try {
(Invoke-WebRequest "http://localhost:8088/Default.aspx" -UseBasicParsing).Content | Out-Null
$served = $true; break
} catch { Start-Sleep -Seconds 1 }
}
Start-Sleep -Seconds 1 # let the end-request hook flush the map
} finally {
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
}
Write-Host "--- iisexpress stdout ---"; Get-Content $out -ErrorAction SilentlyContinue
Write-Host "--- iisexpress stderr ---"; Get-Content $err -ErrorAction SilentlyContinue
Write-Host "--- appmap tree ---"; Get-ChildItem -Recurse (Join-Path $site "tmp") -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
if (-not $served) { throw "IIS Express never served the app" }

- name: Assert the module produced a request AppMap
shell: pwsh
run: |
$dir = Join-Path $env:RUNNER_TEMP "site\tmp\appmap"
$maps = Get-ChildItem -Recurse $dir -Filter *.appmap.json -ErrorAction SilentlyContinue
if (-not $maps) { throw "no AppMap produced by the System.Web module" }
$json = Get-Content $maps[0].FullName -Raw | ConvertFrom-Json
$http = $json.events | Where-Object { $_.http_server_request } | Select-Object -First 1
if (-not $http) { throw "map has no http_server_request event" }
Write-Host "System.Web module recorded $($http.http_server_request.request_method) $($http.http_server_request.path_info)"
84 changes: 84 additions & 0 deletions managed/.github/workflows/harness.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Agent harness (eShopOnWeb)

# Records Microsoft's unmodified eShopOnWeb reference app under the agent and
# validates every AppMap with the official CLI plus coverage thresholds.
# Heavier than the unit CI (clones + builds an external repo), so it runs on
# demand and weekly, and on changes to the agent or the harness.
on:
workflow_dispatch:
schedule:
- cron: "0 6 * * 1"
push:
paths:
- "src/**"
- "harness/**"
- ".github/workflows/harness.yml"

jobs:
# Method + label coverage: record Microsoft's unmodified eShopOnWeb test
# suite under the agent.
eshoponweb:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install AppMap CLI
# --ignore-scripts skips fsevents' (macOS-only) native build.
run: npm install -g @appland/appmap --ignore-scripts
- name: Run harness against eShopOnWeb
run: python3 harness/run.py harness/targets/eshoponweb.json

# HTTP + SQL coverage: launch a web app that references no AppMap package
# and attach the agent purely through environment variables (the zero-touch
# HostingStartup), then assert per-request maps carry HTTP and SQL events.
zero-touch-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install AppMap CLI
run: npm install -g @appland/appmap --ignore-scripts
- name: Run harness against the zero-touch web fixture
# --determinism records twice: the first pass is the coverage check,
# the second asserts the maps are structurally identical (R5).
run: python3 harness/run.py harness/targets/zerotouch-web.json --determinism

# SQL capture against SQL Server (Microsoft.Data.SqlClient) — the provider
# whose partial type-load on Linux exposed the SqlHooks bug that SQLite
# never would. Regression-guards that fix end to end.
sql-server-web:
runs-on: ubuntu-latest
services:
mssql:
image: mcr.microsoft.com/mssql/server:2022-latest
env:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: "Appmap!Harness2026"
ports:
- 1433:1433
env:
# The fixture reads this; the app retries until the container accepts
# connections, so no service health check is needed.
ConnectionStrings__Default: "Server=localhost,1433;Database=AppMapHarness;User Id=sa;Password=Appmap!Harness2026;TrustServerCertificate=True;Encrypt=False"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install AppMap CLI
run: npm install -g @appland/appmap --ignore-scripts
- name: Run harness against the SQL Server web fixture
run: python3 harness/run.py harness/targets/sqlserver-web.json
13 changes: 13 additions & 0 deletions managed/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
bin/
obj/
*.user
**/tmp/appmap/

# SQLite runtime databases (PetClinic example)
*.db
*.db-shm
*.db-wal

# Python bytecode (harness tooling)
__pycache__/
*.pyc
84 changes: 84 additions & 0 deletions managed/AppMap.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppMap.Agent", "src\AppMap.Agent\AppMap.Agent.csproj", "{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A11}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppMap.StartupHook", "src\AppMap.StartupHook\AppMap.StartupHook.csproj", "{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A12}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppMap.AspNetCore", "src\AppMap.AspNetCore\AppMap.AspNetCore.csproj", "{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A13}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppMap.Testing.Xunit", "src\AppMap.Testing.Xunit\AppMap.Testing.Xunit.csproj", "{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A14}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppMap.Testing.NUnit", "src\AppMap.Testing.NUnit\AppMap.Testing.NUnit.csproj", "{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A15}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppMap.Agent.Tests", "test\AppMap.Agent.Tests\AppMap.Agent.Tests.csproj", "{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A16}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloAppMap", "examples\HelloAppMap\HelloAppMap.csproj", "{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A17}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{24212A48-1590-42F2-8A12-7A972CF0644B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PetClinic", "examples\PetClinic\PetClinic.csproj", "{95076417-54BC-4F31-A7FF-2F5A5380F5A9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3B997D78-7003-4D00-8627-2200251B6C40}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppMap.Attributes", "src\AppMap.Attributes\AppMap.Attributes.csproj", "{2F012F7B-B729-4256-AECB-4AA7441113C8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppMap.SystemWeb", "src\AppMap.SystemWeb\AppMap.SystemWeb.csproj", "{3E63E126-93C9-4E74-B227-64AA8EE2C50F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A11}.Release|Any CPU.Build.0 = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A12}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A12}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A12}.Release|Any CPU.Build.0 = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A13}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A13}.Release|Any CPU.Build.0 = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A14}.Release|Any CPU.Build.0 = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A15}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A15}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A15}.Release|Any CPU.Build.0 = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A16}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A16}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A16}.Release|Any CPU.Build.0 = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A17}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A17}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B1F4E2A-9C3D-4A75-8E10-2F5B7D9C0A17}.Release|Any CPU.Build.0 = Release|Any CPU
{95076417-54BC-4F31-A7FF-2F5A5380F5A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{95076417-54BC-4F31-A7FF-2F5A5380F5A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{95076417-54BC-4F31-A7FF-2F5A5380F5A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{95076417-54BC-4F31-A7FF-2F5A5380F5A9}.Release|Any CPU.Build.0 = Release|Any CPU
{2F012F7B-B729-4256-AECB-4AA7441113C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F012F7B-B729-4256-AECB-4AA7441113C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F012F7B-B729-4256-AECB-4AA7441113C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2F012F7B-B729-4256-AECB-4AA7441113C8}.Release|Any CPU.Build.0 = Release|Any CPU
{3E63E126-93C9-4E74-B227-64AA8EE2C50F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3E63E126-93C9-4E74-B227-64AA8EE2C50F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3E63E126-93C9-4E74-B227-64AA8EE2C50F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E63E126-93C9-4E74-B227-64AA8EE2C50F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{95076417-54BC-4F31-A7FF-2F5A5380F5A9} = {24212A48-1590-42F2-8A12-7A972CF0644B}
{2F012F7B-B729-4256-AECB-4AA7441113C8} = {3B997D78-7003-4D00-8627-2200251B6C40}
{3E63E126-93C9-4E74-B227-64AA8EE2C50F} = {3B997D78-7003-4D00-8627-2200251B6C40}
EndGlobalSection
EndGlobal
Loading
Loading