6.9 KiB
ICSharpCode.Decompiler.Tests guide
How the decompiler test suite is structured, what each test kind does, and how to add tests.
The matrix-testing model
Most fixtures run one logical test against a whole matrix of compilers and options: an NUnit
[Test] method takes a CompilerOptions parameter fed by [ValueSource] from static config
arrays declared per runner (defaultOptions including mcs, roslynOnlyOptions,
roslyn2OrNewerOptions, roslyn3OrNewerOptions, roslyn4OrNewerOptions, each also in a
...WithNet40Options variant). One test method therefore becomes 4-26 test cases.
CompilerOptions flags (see Helpers/Tester.cs) select:
- the compiler: legacy csc (
None),UseRoslyn1_3_2,UseRoslyn2_10_0,UseRoslyn3_11_0,UseRoslyn4_14_0,UseRoslynLatest(version comes fromRoslynVersioninDirectory.Packages.props), orUseMcs2_6_4/UseMcs5_23 - the target:
TargetNet40(compiles against .NET Framework reference assemblies from theILSpy-testssubmodule) vs. .NET Core reference packs (net5.0 for Roslyn 3, current preview for Roslyn 4/latest) - codegen options:
Optimize(-o+, definesOPT),UseDebug,Force32Bit,Library,GeneratePdb,NullableEnable,CheckForOverflowUnderflow, ...
Compiled artifacts are named <TestName><suffix>.exe|dll where the suffix encodes the config
(Tester.GetSuffix, e.g. .opt.roslyn3.net40). By default they land next to the test case
sources; set TestsAssemblyTempPath in DecompilerTests.config.json to redirect them
(Helpers/TestsAssemblyOutput.cs).
First test run on a machine: the [SetUpFixture] in TestTraceListener.cs calls
Tester.Initialize(), which downloads the Roslyn toolsets, vswhere, and the reference-assembly
packs from NuGet (network required; cached under the test output directory afterwards) and
builds the self-contained ICSharpCode.Decompiler.TestRunner. Package downloads check the
ILSpy-tests/nuget folder first, so the ILSpy-tests submodule must be initialized (see the
root CLAUDE.md section on the submodule).
Test kinds
| Kind | Runner / fixture dir (TestCases/...) |
Pipeline | Compared against |
|---|---|---|---|
| Pretty | PrettyTestRunner / Pretty/*.cs |
compile -> decompile | the test source itself |
| Correctness | CorrectnessTestRunner / Correctness/*.{cs,vb,il} |
compile -> decompile -> recompile -> execute both | runtime output (stdout/stderr/exit code) of original vs. re-compiled |
| ILPretty | ILPrettyTestRunner / ILPretty/*.il |
ilasm -> decompile | sibling .cs file |
| Ugly | UglyTestRunner / Ugly/*.cs |
compile -> decompile with sugar settings disabled | sibling .Expected.cs file |
| Disassembler | DisassemblerPrettyTestRunner / Disassembler/Pretty/*.il |
ilasm -> disassemble with our ReflectionDisassembler |
the .il source (or .expected.il, e.g. SortedOutput) |
| VBPretty | VBPrettyTestRunner / VBPretty/*.vb |
vbc -> decompile to C# | sibling .cs file |
| PdbGen | PdbGenerationTestRunner / PdbGen/*.xml |
in-proc Roslyn compile, generate portable PDB with PortablePdbWriter, dump both PDBs with PdbToXmlConverter |
.expected.xml |
| Roundtrip | RoundtripAssembly (inputs from ILSpy-tests/) |
whole-project decompile -> MSBuild rebuild -> run original NUnit tests against the rebuilt assembly | test-run success |
| Unit tests | TypeSystem/, Semantics/, Output/, Util/, DataFlowTest, Metadata/, ProjectDecompiler/ |
plain in-process NUnit | assertions |
How to add a test
Common to the file-based kinds: every file in the fixture directory must have a matching test
method - each runner has an AllFilesHaveTests test that fails otherwise. The method name must
equal the file name (minus extension); it usually just calls the runner's Run/RunForLibrary
helper, which picks the file via [CallerMemberName].
- Pretty (decompiler produces nice code): add
TestCases/Pretty/MyTest.csplus a test method choosing the narrowest sensible config group (e.g. C# 8 features needroslyn3OrNewerOptions). The file is simultaneously input and expected output, so write it exactly as ILSpy pretty-prints (tabs,switch {on the same line, trailing commas, ...). Iterate by running the fixture and adjusting the file to the diff. - Correctness (decompiled code behaves identically): add
TestCases/Correctness/MyTest.cswith aMainthat prints observable state; the harness compiles it, decompiles, re-compiles the decompiled output, executes both, and diffs the output streams. Roslyn-non-net40 configs execute throughICSharpCode.Decompiler.TestRunner(anAssemblyLoadContexthost); other configs run the exe directly. - Ugly (output with decompiler features switched off still compiles/behaves): add
MyTest.csplus the expected decompilation asMyTest.Expected.cs. - ILPretty / Disassembler: add a
.ilfile (assembled with the NuGet ilasm) and the expected.cs(ILPretty) or rely on round-tripping the.ilitself (Disassembler). - VBPretty: add
MyTest.vband the expected C# decompilationMyTest.cs. - PdbGen: add
MyTest.xmlcontaining the source files inside<file name="...">elements; the expected PDB dump lives inMyTest.expected.xml.
Conditional expectations (#if) and comparison rules
Different configs legitimately produce different decompilations. Pretty-style comparisons parse
both sides with Roslyn using the config's preprocessor symbols and delete inactive #if
regions (Helpers/CodeAssert.cs), so test sources can branch on:
OPT(optimized build),EXPECTED_OUTPUT(defined only while comparing, never while compiling - use it for "what ILSpy prints" vs. "equivalent compilable input" differences)- compiler family/version:
LEGACY_CSC,LEGACY_VBC,MCS,MCS2,MCS5,ROSLYN,ROSLYN2,ROSLYN3,ROSLYN4 - language version:
CS60...CS130,VB11...VB16 - target framework:
NET40,NETCORE,NET50...NET100
Normalization before diffing: lines are trimmed, // comments stripped, lines starting with
# ignored (so #pragma/#region in test sources are harmless), blank lines ignored.
Everything else must match exactly.
Probing compiler codegen
To learn how every supported compiler/option combination lowers a construct, add a test case exercising it and run the fixture: the harness automatically compiles it with all configured compiler versions and settings, and each failing config's diff shows you what the decompiler produced for that compiler's IL. This beats hand-running csc versions.
Running
dotnet test --solution ILSpy.sln --report-trx --filter FullyQualifiedName~PrettyTestRunner.SwitchExpressions
(Microsoft.Testing.Platform syntax; see root CLAUDE.md "Test discipline".) A failing
comparison prints an aligned diff with +/- markers. On failure the decompiled output
file is left on disk for inspection (Correctness failures print its path; output diffs are
also written to %TEMP%/<test>.original.out / .decompiled.out).