Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #45: Improve execution time in large assemblies #69

Merged
merged 7 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 3 additions & 6 deletions TickSpec/ScenarioGen.fs
Original file line number Diff line number Diff line change
Expand Up @@ -481,10 +481,7 @@ let defineStepMethod
let defineRunMethod
(scenarioBuilder:TypeBuilder)
(providerField:FieldBuilder)
(beforeScenarioEvents:MethodInfo list,
afterScenarioEvents:MethodInfo list,
beforeStepEvents:MethodInfo list,
afterStepEvents:MethodInfo list)
(beforeScenarioEvents,afterScenarioEvents,beforeStepEvents,afterStepEvents)
(stepMethods:MethodBuilder seq) =
/// Run method to execute all scenario steps
let runMethod =
Expand All @@ -496,8 +493,8 @@ let defineRunMethod
let gen = runMethod.GetILGenerator()

// Emit event methods
let emitEvents (ms:MethodInfo seq) =
ms |> Seq.iter (fun mi ->
let emitEvents =
Seq.iter (fun (mi:MethodInfo) ->
if mi.IsStatic then
gen.EmitCall(OpCodes.Call, mi, null)
else
Expand Down
167 changes: 107 additions & 60 deletions TickSpec/TickSpec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ open System.Text.RegularExpressions
open TickSpec.FeatureParser
open TickSpec.ScenarioRun

type internal MethodWithScope =
// tags * features * scenarios * method
string list * string list * string list * MethodInfo

type internal MethodScope =
// tags * features * scenarios
string list * string list * string list

type internal CategorizedMethods =
Dictionary<Type, (MethodWithScope * obj) list>

/// Encapsulates step definitions for execution against features
type StepDefinitions (givens,whens,thens,events,valueParsers) =
let instanceProviderFactory = ref (fun () -> new InstanceProvider() :> IInstanceProvider)
Expand Down Expand Up @@ -44,10 +55,11 @@ type StepDefinitions (givens,whens,thens,events,valueParsers) =
let r = Regex.Match(text,pattern)
if r.Success then Some r else None
definitions
|> List.filter (fun (_,m) -> m |> isMethodInScope feature scenario)
|> List.choose (fun (pattern:string,(_,_,_,m):string list * string list * string list * MethodInfo) ->
|> Seq.filter (fun (_,m) -> m |> isMethodInScope feature scenario)
|> Seq.choose (fun (pattern:string,(_,_,_,m):MethodWithScope) ->
chooseDefinition pattern |> Option.map (fun r -> r,m)
)
|> Seq.toList
/// Extract arguments from specified match
static let extractArgs (r:Match) =
let args = List<string>()
Expand All @@ -56,9 +68,9 @@ type StepDefinitions (givens,whens,thens,events,valueParsers) =
args.ToArray()
/// Gets description as scenario lines
static let getDescription steps =
steps
|> Seq.map (fun (_,line) -> line.Text)
|> String.concat "\r\n"
steps
|> Seq.map (fun (_,line) -> line.Text)
|> String.concat "\r\n"
/// Chooses definitions for specified step and text
let matchStep feature scenario = function
| GivenStep text -> chooseDefinitions feature scenario text givens
Expand All @@ -85,10 +97,11 @@ type StepDefinitions (givens,whens,thens,events,valueParsers) =
/// Chooses in scope events
let chooseInScopeEvents feature (scenario:ScenarioSource) =
let choose xs =
xs
|> Seq.filter (fun m -> m |> isMethodInScope feature scenario)
|> Seq.map (fun (_,_,_,e) -> e)
|> Seq.toList
[|
for _,_,_,e as x in xs do
if isMethodInScope feature scenario x then
yield e
|]
events
|> fun (ea,eb,ec,ed) -> choose ea, choose eb, choose ec, choose ed
new () =
Expand All @@ -98,69 +111,103 @@ type StepDefinitions (givens,whens,thens,events,valueParsers) =
StepDefinitions(assembly.GetTypes())
/// Constructs instance by reflecting against specified types
new (types:Type[]) =
let getScope attributes =
attributes
|> Seq.cast
|> Seq.fold (fun (tags,features,scenarios) (x:StepScopeAttribute) ->
x.Tag::tags, x.Feature::features, x.Scenario::scenarios
) ([],[],[])
let methods =
types
|> Seq.collect (fun t ->
let attributes = t.GetCustomAttributes(typeof<StepScopeAttribute>,true)
let tags, features, scenarios = getScope attributes
t.GetMethods()
|> Seq.map (fun m ->
let attributes = m.GetCustomAttributes(typeof<StepScopeAttribute>,true)
let tags', features', scenarios' = getScope attributes
tags@tags' |> List.filter (not << String.IsNullOrEmpty),
features@features' |> List.filter (not << String.IsNullOrEmpty),
scenarios@scenarios' |> List.filter (not << String.IsNullOrEmpty),
m
|> Seq.collect (fun t -> t.GetMethods())
StepDefinitions(methods)
internal new (methods:MethodInfo seq) =
let categorizedMethods =
let getScope attributes =
mchaloupka marked this conversation as resolved.
Show resolved Hide resolved
attributes
|> Seq.cast
|> Seq.fold (fun (tags,features,scenarios) (x:StepScopeAttribute) ->
x.Tag::tags, x.Feature::features, x.Scenario::scenarios
) ([],[],[])

let attributeMap = Dictionary<Type, (MethodWithScope * obj) list>()
let parentScope = Dictionary<Type, MethodScope>()
let methodScope = Dictionary<MethodInfo, MethodScope>()

let attributes = [|
typeof<GivenAttribute>; typeof<WhenAttribute>; typeof<ThenAttribute>
typeof<BeforeScenarioAttribute>; typeof<AfterScenarioAttribute>
typeof<BeforeStepAttribute>; typeof<AfterStepAttribute>
typeof<ParserAttribute>
|]

// Initialize the attribute map
attributes |> Array.iter (fun attrType -> attributeMap.[attrType] <- List.empty)

// Iterate through all methods
methods |> Seq.iter (fun mi ->
mchaloupka marked this conversation as resolved.
Show resolved Hide resolved
// Get all attributes of the method
mi.GetCustomAttributes(true)
|> Array.iter (fun attr ->
let usedType = attr.GetType()
let correspondingAttrType =
attributes
|> Array.tryFind (fun x -> x.IsAssignableFrom(usedType))

match correspondingAttrType with
// In case it is one of the ones we care about, we add it to the map
| Some attrType ->
// We use caching to not repeatedly get the scope for methods or its declaring types
if methodScope.ContainsKey mi |> not then
let parentType = mi.DeclaringType
if parentScope.ContainsKey parentType |> not then
let parentScopeAttribute = parentType.GetCustomAttributes(typeof<StepScopeAttribute>,true)
parentScope.[parentType] <- getScope parentScopeAttribute
let tags', features', scenarios' = parentScope.[parentType]
let methodScopeAttribute = mi.GetCustomAttributes(typeof<StepScopeAttribute>,true)
let tags, features, scenarios = getScope methodScopeAttribute
methodScope.[mi] <- (
tags@tags' |> List.filter (not << String.IsNullOrEmpty),
mchaloupka marked this conversation as resolved.
Show resolved Hide resolved
features@features' |> List.filter (not << String.IsNullOrEmpty),
scenarios@scenarios' |> List.filter (not << String.IsNullOrEmpty)
)

let tags, features, scenarios = methodScope.[mi]

let existingPairs = attributeMap.[attrType]
attributeMap.[attrType] <- ((tags, features, scenarios, mi), attr)::existingPairs
| None -> ()
)
)
StepDefinitions(methods)
internal new (methods:(string list * string list * string list * MethodInfo) seq) =

attributeMap

/// Step methods
let givens, whens, thens =
methods
|> Seq.map (fun ((_,_,_,m) as sm) -> sm, getStepAttributes m)
|> Seq.filter (fun (m,ca) -> ca.Length > 0)
|> Seq.collect (fun ((_,_,_,m) as sm,ca) ->
ca
|> Array.map (fun a ->
let p =
match (a :?> StepAttribute).Step with
| null -> m.Name
| step -> step
p,a,sm
)
let extractStepAttribute stepAttribute =
categorizedMethods.[stepAttribute]
mchaloupka marked this conversation as resolved.
Show resolved Hide resolved
|> Seq.map (fun ((_,_,_,m) as method, attr) ->
let p =
match (attr :?> StepAttribute).Step with
| null -> m.Name
| step -> step
p,method
)
|> Seq.fold (fun (gs,ws,ts) (p,a,m) ->
match a with
| :? GivenAttribute -> ((p,m)::gs,ws,ts)
| :? WhenAttribute -> (gs,(p,m)::ws,ts)
| :? ThenAttribute -> (gs,ws,(p,m)::ts)
| _ -> invalidOp "Unhandled StepAttribute"
) ([],[],[])
|> Array.ofSeq

let filter (t:Type) (elements:(string list * string list * string list * MethodInfo) seq) =
elements
|> Seq.filter (fun (_,_,_,m) -> null <> Attribute.GetCustomAttribute(m,t))
|> Seq.toList
let givens = typeof<GivenAttribute> |> extractStepAttribute
let whens = typeof<WhenAttribute> |> extractStepAttribute
let thens = typeof<ThenAttribute> |> extractStepAttribute

/// Step events
let events = methods |> filter typeof<EventAttribute>
let beforeScenario = events |> filter typeof<BeforeScenarioAttribute>
let afterScenario = events |> filter typeof<AfterScenarioAttribute>
let beforeStep = events |> filter typeof<BeforeStepAttribute>
let afterStep = events |> filter typeof<AfterStepAttribute>
let filterEvents eventAttribute =
categorizedMethods.[eventAttribute]
|> Seq.map fst
|> Array.ofSeq

let beforeScenario = typeof<BeforeScenarioAttribute> |> filterEvents
let afterScenario = typeof<AfterScenarioAttribute> |> filterEvents
let beforeStep = typeof<BeforeStepAttribute> |> filterEvents
let afterStep = typeof<AfterStepAttribute> |> filterEvents
let events = beforeScenario, afterScenario, beforeStep, afterStep

/// Parser methods
let valueParsers =
methods
|> filter typeof<ParserAttribute>
|> Seq.map (fun (_,_,_,m) -> m.ReturnType, m)
categorizedMethods.[typeof<ParserAttribute>]
|> Seq.map (fun ((_,_,_,m),_) -> m.ReturnType, m)
|> Dict.ofSeq
StepDefinitions(givens,whens,thens,events,valueParsers)

Expand Down
Loading