Skip to content

Commit

Permalink
For issue #135 -- pre-resolve local scopes, allowing for some types o…
Browse files Browse the repository at this point in the history
…f defective input data
  • Loading branch information
SteveGilham committed Dec 8, 2021
1 parent 657a648 commit e2c10f7
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 13 deletions.
104 changes: 92 additions & 12 deletions AltCover.Engine/CecilEx.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,102 @@ open Mono.Cecil.Cil

[<AutoOpen>]
module internal CecilExtension =
let internal scopesSeen =
System.Collections.Generic.HashSet<ScopeDebugInformation>()

let internal safeOffset (point: InstructionOffset) =
if point.IsEndOfMethod then
None
else
Some point.Offset

// workround for old MCS + Cecil 0.11.4
let pruneLocalScopes (m: MethodDefinition) =
scopesSeen.Clear()

let rec pruneScope (scope: ScopeDebugInformation) =
let novel = scopesSeen.Add scope

if novel then
let scopes = scope.Scopes // non-null by construction

scopes
|> Seq.filter
(fun subScope ->
let repeat =
subScope
|> Option.ofObj
|> Option.map pruneScope
|> Option.defaultValue true

repeat || subScope.Start.IsEndOfMethod)
|> Seq.toList
|> List.iter (scopes.Remove >> ignore)

not novel

m.DebugInformation.Scope
|> Option.ofObj
|> Option.map pruneScope
|> ignore

// address issue 135
let internal isResolvedProp =
typeof<InstructionOffset>.GetProperty
("IsResolved",
System.Reflection.BindingFlags.Instance
||| System.Reflection.BindingFlags.NonPublic)

let internal offsetTable =
System.Collections.Generic.SortedDictionary<int, Instruction>()

let unresolved (point: InstructionOffset) =
isResolvedProp.GetValue(point) :?> bool |> not

let prepareLocalScopes (m: MethodDefinition) =
offsetTable.Clear()
scopesSeen.Clear()

let size =
m.Body.Instructions
|> Seq.fold
(fun _ i ->
offsetTable.Add(i.Offset, i)
i.Offset + i.GetSize())
0

let resolvePoint (point: InstructionOffset) =
point
|> safeOffset
|> Option.map
(fun offset ->
let o = Math.Max(offset, 0)
let ok, i = offsetTable.TryGetValue(o)

if ok then
InstructionOffset(i)
else
offsetTable.Keys
|> Seq.filter (fun kk -> o < size && kk <= o)
|> Seq.tryLast
|> Option.map (fun k -> InstructionOffset(offsetTable.[k]))
|> Option.defaultValue (InstructionOffset()))
|> Option.defaultValue (InstructionOffset())

let rec resolveScope (scope: ScopeDebugInformation) =
if scope.IsNotNull then
let scopes = scope.Scopes

if scopes.IsNotNull then
scopes
|> Seq.filter
(fun subScope ->
pruneScope subScope
subScope.Start.IsEndOfMethod)
|> Seq.toList
|> List.iter (scopes.Remove >> ignore)

m.DebugInformation.Scope |> pruneScope
if scopesSeen.Add scope then
scope.Scopes // non-null by construction
|> Seq.iter resolveScope

if unresolved scope.Start then
scope.Start <- resolvePoint scope.Start

if unresolved scope.End then
scope.End <- resolvePoint scope.End

m.DebugInformation.Scope |> resolveScope
pruneLocalScopes m

// Adjust the IL for exception handling
// param name="handler">The exception handler</param>
Expand Down
4 changes: 3 additions & 1 deletion AltCover.Engine/Instrument.fs
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,9 @@ module internal Instrument =
let private visitMethod (state: InstrumentContext) m =
match m.Inspection.IsInstrumented with
| true ->
let body = m.Method.Body
let mt = m.Method
if mt.HasBody then prepareLocalScopes mt
let body = mt.Body

{ state with
MethodBody = body
Expand Down
64 changes: 64 additions & 0 deletions AltCover.Tests/Tests2.fs
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,70 @@ module AltCoverTests2 =
|> Seq.isEmpty,
"pruned subscope.Start.IsEndOfMethod"
)
(* was
IL_0000: ldarg.0
IL_0001: ldfld bool Sample31.Class3/Class4::'<Defer>k__BackingField'
IL_0006: stloc.0
IL_0007: br IL_000c
IL_000c: ldloc.0
IL_000d: ret
has been prefixed with Ldc_I4_1 (1 byte)
*)

let start =
pathGetterDef.Body.Instructions |> Seq.head

let finish =
pathGetterDef.Body.Instructions |> Seq.last

let rescope = ScopeDebugInformation(start, null)
let size = finish.Offset + finish.GetSize()

pathGetterDef.DebugInformation.Scope <- rescope

[ -1
4
finish.Offset
size - 1
size
Int32.MaxValue ]
|> Seq.iter
(fun i ->
let s = ScopeDebugInformation(start, null)
s.Start <- InstructionOffset(i)
s.End <- InstructionOffset(size)
rescope.Scopes.Add s)

rescope.Scopes.Add rescope

prepareLocalScopes pathGetterDef

Assert.True(
InstructionOffset() |> safeOffset |> Option.isNone,
"End should go to none"
)

// prune 1 recursion and 2 at end
Assert.That(rescope.Scopes |> Seq.length, Is.EqualTo 4)

test <@ start.Offset = 0 @>
test <@ start.Next.Offset = 1 @>
test <@ start.Next.Next.Offset = 2 @>
test <@ start.Next.Next.Next.Offset = 7 @>

let expected =
[ Some start.Offset
Some start.Next.Next.Offset
Some finish.Offset
Some finish.Offset ]

let result =
rescope.Scopes
|> Seq.map (fun s -> safeOffset s.Start)
|> Seq.toList

test <@ result = expected @>

[<Test>]
let ShouldWriteMonoAssemblyOK () =
Expand Down

0 comments on commit e2c10f7

Please sign in to comment.