chore: Add unit tests for arm64 disassembler - #3245
Conversation
AI review
Accumulator bugs
Tests that can't fail
Minor
It looks like it found some possible bugs in the accumulator (I did not verify myself). Fine if you want to fix them here, or defer for out-of-scope. |
7aa8d2b to
52dc08c
Compare
This PR intended to add tests to verify existing arm64 disassembler behaviors.
|
|
Findings are all in the new test code; the production changes look good (the formatter padding is a real fix — a mnemonic reaching
Reviewed with Claude Code. |
84d7a22 to
a01d20d
Compare
a01d20d to
2fdfa41
Compare
2fdfa41 to
025c00a
Compare
timcassell
left a comment
There was a problem hiding this comment.
Follow-up on the updated head. The fixes from the last round all look right — the macOS-arm64 address, WithStrictOrdering, the duplicate MOVZ row, DmbIshLdInstr, and the Decode.cs comments. A few items are still open, plus one new one and one follow-up note.
Reviewed with Claude Code.
…rMethod is called
timcassell
left a comment
There was a problem hiding this comment.
Another pass on 7124b6c3a. AddFalsePointer and the absolute-address assertion in TryGetReferencedAddress_With_BranchRelative are both good additions. Remaining items below — the UnreachableException visibility one is the only one that affects shipped code.
Reviewed with Claude Code.
| /// <summary> | ||
| /// Exception thrown when the program executes an instruction that was thought to be unreachable. | ||
| /// </summary> | ||
| public sealed class UnreachableException : Exception |
There was a problem hiding this comment.
This polyfill is public, so BenchmarkDotNet's netstandard2.0 and net6.0 assets now export a public System.Diagnostics.UnreachableException. A consumer on those TFMs who references another library exporting the same polyfill gets CS0433 ("exists in both"), and one who declares their own gets CS0436 — purely from referencing BenchmarkDotNet.
Its only consumer is MockClrMethod in the test assembly, and Properties/AssemblyInfo.cs:9 already grants InternalsVisibleTo("BenchmarkDotNet.Tests"), so internal works. The sibling polyfills that declare types (AsyncEnumerable, FileExtensions) are both internal.
| @@ -0,0 +1,41 @@ | |||
| #if !NET8_0_OR_GREATER | |||
There was a problem hiding this comment.
System.Diagnostics.UnreachableException shipped in .NET 7, not .NET 8. This guard is correct today only because the TFM list (netstandard2.0;net6.0;net8.0;net9.0;net10.0) happens to skip net7.0 — add net7.0 and the polyfill duplicates the BCL type (CS0436), which TreatWarningsAsErrors in build/common.props makes fatal.
Should be #if !NET7_0_OR_GREATER.
| { | ||
| private Arm64RegisterValueAccumulator CreateValueAccumulator(Arm64RegisterX register, ushort initialValue) | ||
| { | ||
| using var clrRuntime = CreateMockClrRuntime(); |
There was a problem hiding this comment.
using disposes the runtime when this helper returns, but the accumulator it returns keeps it in _runtime (Init(clrRuntime) on the next line). Every caller therefore gets an accumulator holding a disposed IClrRuntime.
Harmless only while the BR/BLR paths never dereference it — the first MOVZ->LDR case added here reads through the disposed object, since Arm64RegisterValueAccumulator.Feed calls _runtime.DataTarget.DataReader.ReadPointer. Drop the using, or return the runtime alongside the accumulator so the caller owns its lifetime.
| { | ||
| if (JitHelperFunctionNames.TryGetValue(address, out var value)) | ||
| return value; | ||
| throw new ArgumentException($"Specified address(0x{address:X}) is not registered."); |
There was a problem hiding this comment.
These four lookups (also :41, :48, :55) throw for an unregistered address, where ClrMD returns null and the production code treats null as an ordinary outcome. So the null branches in TryTranslateAddressToName can't be reached through this mock, and a fixture that forgets one registration fails with an opaque exception from inside the mock instead of a readable assertion.
Arm64InstructionFormatterTests.FormatInstruction_IntroDisassembly_SumLocal only survives because every branch target happens to be pre-seeded into AddressToNameMapping; adding one more branch instruction to that fixture without registering its symbol would throw.
AddFalsePointer handled the equivalent problem on the MockMemory side nicely — same idea would fit here. (One gap remains there too: ClrMdDisassembler.FlushCachedDataIfNeeded's Read(...) <= 0 branch is still unreachable, since a 1-byte region returns 1 rather than 0.)
| @@ -0,0 +1,33 @@ | |||
| using AwesomeAssertions; | |||
There was a problem hiding this comment.
TFM guards are inconsistent across the new files: #if NET in seven, #if NET8_0_OR_GREATER in five, and none in eight — including this one, which calls the Capstone-backed Arm64TestInstructions.Movz. It survives on net472 only because the single test here is Skipped.
The two guards are equivalent for today's TFMs (net10.0 and net472), but DmbIshldOffset is defined under #if NET and consumed under #if NET8_0_OR_GREATER, so adding a net6.0/net7.0 test TFM — or un-skipping this test — breaks the build. Worth settling on one guard and applying it consistently.
|
|
||
| file static class ExtensionMethods | ||
| { | ||
| public static ReadBytesDelegate ToGetReadBytesDelegate(this uint[] rawInstructions) |
There was a problem hiding this comment.
ToGetReadBytesDelegate and ToTryReadPointerDelegate (:71) are no longer referenced anywhere — MockMemory replaced them, and the single-delegate MockDataReader constructors they fed were removed in 2304b834.
Worth deleting: they're the old address-ignoring helpers, so leaving them around invites reintroducing exactly the gap MockMemory closed.
|
|
||
| /// <summary> | ||
| /// Create MockClrRuntime with MockDataReader that returns following data. | ||
| /// Read: Throw InvalidOperationException. |
There was a problem hiding this comment.
Stale since 2304b834: MockDataReader(ulong) now sets _read = (_, _) => 0, so Read returns 0 rather than throwing. (Returning 0 is the right call — it matches the IDataReader.Read contract; it's just the doc that needs updating.)
|
|
||
| private readonly ulong BaseAddress; | ||
|
|
||
| public MockMemory(ulong baseAddress = 0) |
There was a problem hiding this comment.
The baseAddress parameter is never passed — every call site is new MockMemory() — and it's applied only in AddBytes, not in AddJitHelperFunctionName/AddMethodByHandle/AddMethodByInstructionPointer/AddTypeByMethodTable. If anyone does pass a base, instruction addresses would rebase while the lookup mappings silently would not. Either drop the parameter or apply it uniformly.
This PR contains following changes.
1. Cleanup arm64 disassembler related code to preparing to add unit tests
See following PR comment for details.
2. Add AsmArm64 package reference
AsmArm64 package to unit test project.
Currently it's used for test purpose.
It's expected existing arm64 disassembler is replaced to
AsmArm64based implementation. (#3246)3. Add arm64 disassembler related unit tests.
To ensure existing arm64 disassembler behavior.
Unit test codes are added for major code paths. (It can confirm code coverage results with
Analyze Code Coverageon VS)Note:
Almost of unit tests on .NET Framework are excluded by
#if NETdirective.UnsafeAccessor(It requires .NET 8 or later)Libraryproject)