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
50 changes: 50 additions & 0 deletions .github/sourcelink/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 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), 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:
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 `Report`'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`.

## 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.
3 changes: 3 additions & 0 deletions .github/sourcelink/consumer/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project>
<!-- Isolate the SourceLink step-into fixture from the repo analyzers/props. -->
</Project>
3 changes: 3 additions & 0 deletions .github/sourcelink/consumer/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project>
<!-- Isolate the SourceLink step-into fixture from the repo targets. -->
</Project>
12 changes: 12 additions & 0 deletions .github/sourcelink/consumer/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using Wolfgang.Etl.Abstractions;

// End-to-end SourceLink "step into" fixture. The debugger sets a breakpoint on
// the marked line below 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. Report's constructor is a plain, non-async method with a guard
// clause, which makes it a clean and stable step-into target.

var report = new Report(42); // STEP_INTO_TARGET
Console.WriteLine(report.CurrentItemCount);
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.
-->
<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. -->
<AbstractionsProject Condition="'$(AbstractionsProject)' == ''">../../../src/Wolfgang.Etl.Abstractions/Wolfgang.Etl.Abstractions.csproj</AbstractionsProject>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="$(AbstractionsProject)" />
</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)
64 changes: 64 additions & 0 deletions .github/workflows/sourcelink-stepinto.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: SourceLink Step-Into

# Proves the DEBUGGER half of SourceLink end-to-end (#214): 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. sourcelink.yaml already proves every PDB
# document resolves to real GitHub content; this drives an actual debugger step-into.
#
# See .github/sourcelink/README.md. Scheduled + manual, NOT a PR gate — debugger
# automation is heavier/less deterministic than a unit test, and SourceLink's raw
# URLs resolve only against a pushed commit.

on:
schedule:
- cron: '0 4 * * 2' # weekly Tuesday 04:00 UTC
workflow_dispatch:

permissions:
contents: read

env:
NETCOREDBG_VERSION: '3.1.2-1054'
CONSUMER: .github/sourcelink/consumer
EXPECTED_SRC: Report.cs

jobs:
step-into:
name: F11 step-into resolves library source
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: '10.0.x'

- name: Install netcoredbg
run: |
set -euo pipefail
url="https://github.com/Samsung/netcoredbg/releases/download/${NETCOREDBG_VERSION}/netcoredbg-linux-amd64.tar.gz"
curl -sSfL -o netcoredbg.tar.gz "$url"
tar -xzf netcoredbg.tar.gz
echo "NETCOREDBG=$PWD/netcoredbg/netcoredbg" >> "$GITHUB_ENV"

- name: Build the step-into fixture (Debug, deterministic SourceLink PDB)
# Debug + ContinuousIntegrationBuild=true: the referenced library compiles
# with the same SourceLink map a released package ships, non-optimized so the
# step-into target is not reordered/inlined.
run: >
dotnet build "$CONSUMER/StepIntoConsumer.csproj"
-c Debug -p:ContinuousIntegrationBuild=true

- name: Drive netcoredbg step-into
run: |
set -euo pipefail
bpline=$(grep -n STEP_INTO_TARGET "$CONSUMER/Program.cs" | head -1 | cut -d: -f1)
dll="$CONSUMER/bin/Debug/net10.0/StepIntoConsumer.dll"
echo "Breakpoint at Program.cs:$bpline — expecting step-into to land in $EXPECTED_SRC"
python3 -u .github/sourcelink/verify_stepinto.py \
"$NETCOREDBG" "$dll" "Program.cs:$bpline" "$EXPECTED_SRC"
Loading