mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
The PdbGen tests compared the reconstructed PDB to the C# compiler's byte-for-byte, so any non-trivial method failed on reconstructed IL ranges, hidden sequence points and local scopes - none of which the decompiler can reproduce exactly. That left four of seven fixtures [Ignore]d and the suite with almost no coverage. Compare only what a debugging user actually feels: the visible (non-hidden) breakpoint map, parsed straight from the sequence-point blobs and keyed by method-definition row (shared between the PDB and the PE it describes). IL offsets, hidden points, local scopes and the embedded source are dropped. The compiler's own PDB is the oracle, so the tests assert correct debugging behavior rather than the decompiler's past output. Methods where the decompiler legitimately diverges pin an auto-derived residual snapshot, the same accept-the-diff workflow as the pretty tests; a separate oracle-free check rejects duplicate or overlapping sequence points. Un-ignores ForLoopTests, LambdaCapturing and Members (its source is regenerated to match the decompiler's per-type output, collapsing ~50 lines of indentation-induced coordinate noise to two genuine differences). Assisted-by: Claude:claude-opus-4-8:Claude Codepull/3823/head
10 changed files with 356 additions and 229 deletions
@ -0,0 +1,198 @@
@@ -0,0 +1,198 @@
|
||||
// Copyright (c) 2026 Siegfried Pammer
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Reflection.Metadata; |
||||
using System.Reflection.Metadata.Ecma335; |
||||
using System.Reflection.PortableExecutable; |
||||
using System.Text; |
||||
|
||||
namespace ICSharpCode.Decompiler.Tests.Helpers |
||||
{ |
||||
/// <summary>
|
||||
/// Reads the sequence points out of a portable PDB and compares two PDBs (typically the one
|
||||
/// the original C# compiler produced versus the one <c>PortablePdbWriter</c> reconstructs)
|
||||
/// at the granularity that actually matters to a debugging user: the breakpoint map.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The decompiler reconstructs IL ranges, hidden sequence points and local scopes from the
|
||||
/// ILAst, so those never match the original compiler byte-for-byte. What it can and should
|
||||
/// get right is <em>where the visible (non-hidden) breakpoints land in the source</em>.
|
||||
/// The comparison therefore projects each PDB onto the ordered list of visible sequence-point
|
||||
/// source locations per method (keyed by method-definition row, which is shared between the
|
||||
/// PDB and the PE it describes) and reports the residual difference.
|
||||
/// </remarks>
|
||||
static class PdbSequencePoints |
||||
{ |
||||
internal readonly record struct RawSequencePoint(int Offset, bool IsHidden, |
||||
int StartLine, int StartColumn, int EndLine, int EndColumn) |
||||
{ |
||||
/// <summary>Visible source location, formatted with or without columns.</summary>
|
||||
public string Format(bool includeColumns) |
||||
{ |
||||
return includeColumns |
||||
? $"({StartLine},{StartColumn})-({EndLine},{EndColumn})" |
||||
: $"L{StartLine}-{EndLine}"; |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Reads every method's sequence points (hidden ones included, in document order) from a
|
||||
/// portable PDB, keyed by the row number of the owning method definition.
|
||||
/// </summary>
|
||||
public static Dictionary<int, List<RawSequencePoint>> Read(Stream portablePdb) |
||||
{ |
||||
portablePdb.Position = 0; |
||||
using var provider = MetadataReaderProvider.FromPortablePdbStream(portablePdb, MetadataStreamOptions.LeaveOpen); |
||||
var reader = provider.GetMetadataReader(); |
||||
var result = new Dictionary<int, List<RawSequencePoint>>(); |
||||
foreach (var handle in reader.MethodDebugInformation) |
||||
{ |
||||
var info = reader.GetMethodDebugInformation(handle); |
||||
if (info.SequencePointsBlob.IsNil) |
||||
continue; |
||||
var list = new List<RawSequencePoint>(); |
||||
foreach (var sp in info.GetSequencePoints()) |
||||
{ |
||||
list.Add(new RawSequencePoint(sp.Offset, sp.IsHidden, |
||||
sp.StartLine, sp.StartColumn, sp.EndLine, sp.EndColumn)); |
||||
} |
||||
if (list.Count > 0) |
||||
{ |
||||
// A MethodDebugInformation row is parallel to its MethodDefinition row.
|
||||
result[MetadataTokens.GetRowNumber(handle)] = list; |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Resolves a readable "Namespace.Type.Method" label for each method-definition row,
|
||||
/// for use in comparison failure messages.
|
||||
/// </summary>
|
||||
public static Dictionary<int, string> ReadMethodNames(string peFileName) |
||||
{ |
||||
using var peStream = File.OpenRead(peFileName); |
||||
using var peReader = new PEReader(peStream); |
||||
var md = peReader.GetMetadataReader(); |
||||
var names = new Dictionary<int, string>(); |
||||
foreach (var handle in md.MethodDefinitions) |
||||
{ |
||||
var method = md.GetMethodDefinition(handle); |
||||
var type = md.GetTypeDefinition(method.GetDeclaringType()); |
||||
string typeName = md.GetString(type.Name); |
||||
if (!type.Namespace.IsNil && md.GetString(type.Namespace) is { Length: > 0 } ns) |
||||
typeName = ns + "." + typeName; |
||||
names[MetadataTokens.GetRowNumber(handle)] = typeName + "." + md.GetString(method.Name); |
||||
} |
||||
return names; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Compares the visible breakpoint maps of two PDBs and returns the residual difference as
|
||||
/// a deterministic, human-readable report. An empty string means the maps are identical.
|
||||
/// </summary>
|
||||
/// <param name="expected">PDB used as the oracle (the original compiler's PDB).</param>
|
||||
/// <param name="actual">PDB under test (the decompiler's reconstruction).</param>
|
||||
/// <param name="methodNames">Row-to-label map from <see cref="ReadMethodNames"/>.</param>
|
||||
/// <param name="includeColumns">
|
||||
/// When true, breakpoints must match on line <em>and</em> column; when false, only the
|
||||
/// line span is compared (use for methods where the decompiler's column placement within a
|
||||
/// statement legitimately differs).
|
||||
/// </param>
|
||||
public static string CompareBreakpointMaps( |
||||
Dictionary<int, List<RawSequencePoint>> expected, |
||||
Dictionary<int, List<RawSequencePoint>> actual, |
||||
Dictionary<int, string> methodNames, |
||||
bool includeColumns) |
||||
{ |
||||
var report = new StringBuilder(); |
||||
foreach (int row in expected.Keys.Union(actual.Keys).OrderBy(r => r)) |
||||
{ |
||||
var expectedPoints = Visible(expected, row, includeColumns); |
||||
var actualPoints = Visible(actual, row, includeColumns); |
||||
|
||||
var expectedOnly = MultisetDifference(expectedPoints, actualPoints); |
||||
var actualOnly = MultisetDifference(actualPoints, expectedPoints); |
||||
if (expectedOnly.Count == 0 && actualOnly.Count == 0) |
||||
continue; |
||||
|
||||
methodNames.TryGetValue(row, out var name); |
||||
report.Append($"0x{0x06000000 | row:x8} {name}").Append('\n'); |
||||
foreach (var p in expectedOnly.OrderBy(p => p)) |
||||
report.Append(" - compiler only: ").Append(p).Append('\n'); |
||||
foreach (var p in actualOnly.OrderBy(p => p)) |
||||
report.Append(" + decompiler only: ").Append(p).Append('\n'); |
||||
} |
||||
return report.ToString(); |
||||
} |
||||
|
||||
static List<string> Visible(Dictionary<int, List<RawSequencePoint>> map, int row, bool includeColumns) |
||||
{ |
||||
if (!map.TryGetValue(row, out var list)) |
||||
return new List<string>(); |
||||
return list.Where(sp => !sp.IsHidden).Select(sp => sp.Format(includeColumns)).ToList(); |
||||
} |
||||
|
||||
static List<string> MultisetDifference(List<string> a, List<string> b) |
||||
{ |
||||
var counts = new Dictionary<string, int>(); |
||||
foreach (var x in b) |
||||
counts[x] = counts.GetValueOrDefault(x) + 1; |
||||
var result = new List<string>(); |
||||
foreach (var x in a) |
||||
{ |
||||
if (counts.TryGetValue(x, out int c) && c > 0) |
||||
counts[x] = c - 1; |
||||
else |
||||
result.Add(x); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Checks that the decompiler's own PDB is structurally well-formed, independent of any
|
||||
/// oracle: within each method the sequence points' IL offsets must be strictly increasing,
|
||||
/// i.e. no duplicate or overlapping sequence points (a duplicate is exactly the failure
|
||||
/// that aborts <c>PortablePdbWriter</c> in debug builds). Returns the offending methods, or
|
||||
/// an empty string when well-formed.
|
||||
/// </summary>
|
||||
public static string CheckWellFormed( |
||||
Dictionary<int, List<RawSequencePoint>> pdb, |
||||
Dictionary<int, string> methodNames) |
||||
{ |
||||
var report = new StringBuilder(); |
||||
foreach (int row in pdb.Keys.OrderBy(r => r)) |
||||
{ |
||||
var list = pdb[row]; |
||||
for (int i = 1; i < list.Count; i++) |
||||
{ |
||||
if (list[i].Offset <= list[i - 1].Offset) |
||||
{ |
||||
methodNames.TryGetValue(row, out var name); |
||||
report.Append($"0x{0x06000000 | row:x8} {name}: non-increasing IL offset 0x{list[i].Offset:x} after 0x{list[i - 1].Offset:x}").Append('\n'); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
return report.ToString(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
0x06000001 ICSharpCode.Decompiler.Tests.TestCases.PdbGen.LambdaCapturing.Main |
||||
+ decompiler only: (8,2)-(8,3) |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
0x0600000b C..ctor |
||||
- compiler only: (50,2)-(50,12) |
||||
+ decompiler only: (51,2)-(51,3) |
||||
0x0600000c C.Finalize |
||||
- compiler only: (56,2)-(56,3) |
||||
- compiler only: (57,2)-(57,3) |
||||
Loading…
Reference in new issue