Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .github/sourcelink/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!--
Isolation stub. MSBuild imports only the NEAREST Directory.Build.props when
walking up, so this empty project stops the repo-root Directory.Build.props
(analyzers, BannedSymbols, TreatWarningsAsErrors, multi-TFM, PublicAPI, …)
from applying to the throwaway SourceLink step-into consumer. The consumer is
a debugging fixture, not shipped code, and must not inherit library policy.
-->
<Project>
</Project>
3 changes: 3 additions & 0 deletions .github/sourcelink/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!-- Isolation stub — see Directory.Build.props in this folder. -->
<Project>
</Project>
51 changes: 51 additions & 0 deletions .github/sourcelink/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# SourceLink step-into verification

Fixtures for [`sourcelink-stepinto.yaml`](../workflows/sourcelink-stepinto.yaml),
which proves the *debugger* half of SourceLink end-to-end: that pressing **F11**
in a consumer steps into the library's **real source** (resolved via the
SourceLink map in the PDB), not a decompiled placeholder.

This complements [`sourcelink.yaml`](../workflows/sourcelink.yaml) (PR #208), which
proves every document in the PDB resolves to real GitHub content via
`dotnet sourcelink test`. Together they cover the full chain F11 depends on and
satisfy #133: SourceLink map in the PDB → source-file resolution → GitHub raw-URL
fetch.

## How it works

1. Build [`consumer/`](consumer/) with `-c Debug -p:ContinuousIntegrationBuild=true`.
Its `ProjectReference` compiles the library with the **same SourceLink map a
released package ships** (deterministic `/_/…` source roots that map to GitHub
raw URLs), and Debug/non-optimized codegen so the step-into target isn't
reordered away.
2. Run [`verify_stepinto.py`](verify_stepinto.py) — it drives
[netcoredbg](https://github.com/Samsung/netcoredbg) over its MI interface to
break in the consumer, step into `TestExtractor`'s constructor, and assert
the resulting frame is the library's SourceLink-mapped source file with symbols
loaded. If SourceLink or the sequence points were broken the step would land
with no source and the script exits non-zero.

## Why a ProjectReference, not the packed NuGet

netcoredbg is the only CI-scriptable .NET debugger, and it does **not** reliably
pair a *package*-sourced assembly with its symbol-package (`.snupkg`) PDB — it
reports `symbols-loaded=0` and cannot step into it (verified during development).
A `ProjectReference` built with `ContinuousIntegrationBuild=true` produces a
**byte-identical SourceLink PDB**, so the SourceLink map under test is the same
one that ships; only the delivery path differs. The packed-package symbol/URL
resolution is covered by `sourcelink.yaml` (PR #208).

## Files

- `consumer/StepIntoConsumer.csproj` / `Program.cs` — the fixture consumer. The
break line is marked `STEP_INTO_TARGET`.
- `Directory.Build.props` / `.targets` — empty isolation stubs so the consumer
does **not** inherit the repo's analyzers / BannedSymbols / multi-TFM policy.
- `verify_stepinto.py` — the debugger driver (exit 0 = step-into resolved real
source).

## Scope

Scheduled + manual, not a PR gate — debugger automation is heavier and slightly
less deterministic than a unit test, and SourceLink's raw URLs resolve only
against a pushed commit.
14 changes: 14 additions & 0 deletions .github/sourcelink/consumer/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using Wolfgang.Etl.TestKit;

// End-to-end SourceLink "step into" fixture. The debugger sets a breakpoint on
// the line marked STEP_INTO_TARGET and issues a step-into (the F11 a consumer
// would press). If SourceLink is intact the debugger resolves the library's real
// source (from GitHub) at the constructor below, instead of a decompiled
// placeholder. TestExtractor's constructor is a plain, non-async method, which
// makes it a clean and stable step-into target.

IEnumerable<int> items = new[] { 1, 2, 3 };
var extractor = new TestExtractor<int>(items); // STEP_INTO_TARGET
Console.WriteLine(extractor.GetType().FullName);
29 changes: 29 additions & 0 deletions .github/sourcelink/consumer/StepIntoConsumer.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<!--
A minimal consumer that steps into the library. It references the library
PROJECT (built with ContinuousIntegrationBuild=true so the PDB carries the
same SourceLink map a released package ships). A ProjectReference is used
rather than a PackageReference because netcoredbg — the only CI-scriptable
.NET debugger — does not reliably pair a package-sourced assembly with its
symbol package's PDB, so it cannot step into it. The SourceLink PDB under
test is identical either way; the packed-package symbol/URL resolution is
covered by sourcelink.yaml (#133 / PR #208).
-->
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<!-- Non-optimized so the step-into target is not reordered/inlined away. -->
<Optimize>false</Optimize>
<DebugType>portable</DebugType>
<!-- Set by the workflow to the repo's src project path. -->
<TestKitProject Condition="'$(TestKitProject)' == ''">../../../src/Wolfgang.Etl.TestKit/Wolfgang.Etl.TestKit.csproj</TestKitProject>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="$(TestKitProject)" />
</ItemGroup>

</Project>
115 changes: 115 additions & 0 deletions .github/sourcelink/verify_stepinto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Drive netcoredbg to prove SourceLink "step into" (F11) works end-to-end.

Sets a breakpoint in the consumer, runs to it, issues a step-into, and inspects
the resulting stack frame. Succeeds (exit 0) only if the step landed in the
expected library source file with the library's symbols loaded — i.e. a debugger
resolves real library source, not a decompiled placeholder. If SourceLink or the
symbol package were broken, the step would land with no source mapping and this
fails.

Usage:
verify_stepinto.py <netcoredbg> <consumer.dll> <Program.cs:LINE> <expected-src.cs>
"""
import subprocess
import sys
import re
import threading
import queue
import time

if len(sys.argv) != 5:
print(__doc__)
sys.exit(2)

debugger, consumer_dll, break_spec, expected_src = sys.argv[1:5]

proc = subprocess.Popen(
[debugger, "--interpreter=mi", "--", "dotnet", consumer_dll],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)

events: "queue.Queue[str]" = queue.Queue()


def _reader():
for line in proc.stdout:
events.put(line.rstrip("\n"))
events.put(None)


threading.Thread(target=_reader, daemon=True).start()


def send(command):
print(">>>", command, flush=True)
proc.stdin.write(command + "\n")
proc.stdin.flush()


def wait_for(needles, timeout=60):
deadline = time.time() + timeout
while time.time() < deadline:
try:
line = events.get(timeout=max(0.1, deadline - time.time()))
except queue.Empty:
return None
if line is None:
return None
print("dbg>", line, flush=True)
if any(n in line for n in needles):
return line
return None


def fail(reason):
print("RESULT=FAIL reason=" + reason, flush=True)
try:
send("-gdb-exit")
except Exception:
pass
sys.exit(1)


send("-break-insert " + break_spec)
wait_for(["^done", "^error"], 15)
send("-exec-run")

# netcoredbg halts at the managed entry point first; continue to the breakpoint.
stop = wait_for(["*stopped"], 60)
if stop and 'reason="entry-point-hit"' in stop:
send("-exec-continue")
stop = wait_for(["*stopped"], 60)

if not stop or "breakpoint-hit" not in stop:
fail("breakpoint-not-hit")

# Step into (F11). Retry a couple of times in case the first step stays on the
# call line before descending into the callee.
resolved = None
for _ in range(4):
send("-exec-step")
stop = wait_for(["*stopped"], 30)
if not stop:
break
match = re.search(r'frame=\{[^}]*?file="([^"]+)"[^}]*?fullname="([^"]+)"', stop)
if match and match.group(1).endswith(expected_src):
resolved = (match.group(1), match.group(2), stop)
break

if not resolved:
fail("did-not-step-into-" + expected_src)

send("-gdb-exit")

source_file, fullname, stop = resolved
line_match = re.search(r'line="(\d+)"', stop)
print("RESOLVED_FILE=" + source_file, flush=True)
print("RESOLVED_FULLNAME=" + fullname, flush=True)
print("RESOLVED_LINE=" + (line_match.group(1) if line_match else "?"), flush=True)
print("RESULT=PASS", flush=True)
sys.exit(0)
99 changes: 99 additions & 0 deletions .github/workflows/sourcelink-stepinto.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: SourceLink Step-Into

# End-to-end "F11 into library source" verification, one step beyond sourcelink.yaml.
#
# sourcelink.yaml (PR #208) proves every document in the PDB resolves to real
# GitHub content via `dotnet sourcelink test`. This workflow proves the *debugger*
# half: it builds a scratch consumer against the library (compiled with the same
# ContinuousIntegrationBuild + SourceLink a release ships), then drives netcoredbg
# to set a breakpoint in the consumer and STEP INTO (the F11 a consumer would
# press) a library constructor — asserting the debugger lands in the library's
# real source (symbols loaded, SourceLink-mapped source file) rather than a
# decompiled placeholder. If the SourceLink map or sequence points were broken the
# step would land with no source and this fails.
#
# Why a project reference, not the packed NuGet: netcoredbg (the only
# CI-scriptable .NET debugger) does not reliably pair a *package*-sourced
# assembly with its symbol-package PDB, so it cannot step into it. The PDB under
# test is byte-identical either way; the packed-package symbol/URL resolution is
# covered by sourcelink.yaml (PR #208). See .github/sourcelink/README.md.
#
# Together these two workflows satisfy #133 (SourceLink debug-step-into
# verification). Scheduled + manual only (not a PR gate): debugger automation is
# heavier and slightly less deterministic than a unit test, and SourceLink's raw
# URLs resolve only against a pushed commit.

on:
schedule:
- cron: '30 7 * * 1' # Mondays 07:30 UTC
workflow_dispatch:

permissions:
contents: read

concurrency:
group: sourcelink-stepinto-${{ github.ref }}
cancel-in-progress: true

env:
NETCOREDBG_VERSION: 3.1.2-1054
# Reduce tiered-JIT reordering so the step-into target stays stable.
DOTNET_TieredCompilation: '0'

jobs:
step-into:
name: Debugger step-into (F11) resolves library source
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false

- name: Detect src project
id: detect
run: |
if git ls-files 'src/**/*.csproj' 'src/*.csproj' | grep -q .; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "No src/**/*.csproj found — skipping."
echo "found=false" >> "$GITHUB_OUTPUT"
fi

- name: Setup .NET
if: steps.detect.outputs.found == 'true'
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: '10.0.x'

# Build the fixture consumer. Its ProjectReference pulls in the library,
# compiled with ContinuousIntegrationBuild=true so the PDB carries the same
# SourceLink map a released package ships (Debug = non-optimized so the
# step-into target isn't reordered away).
- name: Build step-into consumer (SourceLink)
if: steps.detect.outputs.found == 'true'
run: |
dotnet build .github/sourcelink/consumer/StepIntoConsumer.csproj \
-c Debug \
-p:ContinuousIntegrationBuild=true

- name: Install netcoredbg
if: steps.detect.outputs.found == 'true'
run: |
url="https://github.com/Samsung/netcoredbg/releases/download/${NETCOREDBG_VERSION}/netcoredbg-linux-amd64.tar.gz"
curl -sSfL "$url" -o netcoredbg.tar.gz
tar -xzf netcoredbg.tar.gz
echo "NETCOREDBG=$PWD/netcoredbg/netcoredbg" >> "$GITHUB_ENV"

- name: Step into the library (F11) and assert real source resolves
if: steps.detect.outputs.found == 'true'
run: |
prog=".github/sourcelink/consumer/Program.cs"
line="$(grep -n 'STEP_INTO_TARGET' "$prog" | head -n1 | cut -d: -f1)"
echo "Breaking at Program.cs:$line"
python3 .github/sourcelink/verify_stepinto.py \
"$NETCOREDBG" \
".github/sourcelink/consumer/bin/Debug/net10.0/StepIntoConsumer.dll" \
"Program.cs:$line" \
"TestExtractor.cs"