diff --git a/tests/steps/contextutil.nim b/tests/steps/contextutil.nim new file mode 100644 index 0000000..6187876 --- /dev/null +++ b/tests/steps/contextutil.nim @@ -0,0 +1,58 @@ +# package: cucumber_nim +# module tests/steps/contextutil + +##[ + + Some utilities for manipulating step contexts in dynamically loaded modules. + + Used to check step and hook definitions. + +]## + +import tables +import dynlib +import "../../cucumber/types" +import "../../cucumber/parameter" +import "../../cucumber/step" +import "./dynmodule" + + +type + SetIntContext* = (proc(context: string, param: string, value: int): void {.nimcall.}) + GetIntContext* = (proc(context: string, param: string): int {.nimcall.}) + SetStringContext* = (proc(context: string, param: string, value: string): void {.nimcall.}) + + QualName* = tuple + context: string + name: string + ContextValues* = TableRef[QualName, int] + +DeclareRefParamType(ContextValues) + +proc fillContext*( + lib: LibModule, contextValues: ContextValues + ): void = + + if contextValues != nil: + let setIntContext = bindInLib[SetIntContext]( + lib, "setIntContext") + for qname, value in contextValues: + setIntContext(qname.context, qname.name, value) + +When " context parameter is $", ( + context: string, param: string, value: int, + scenario.contextValues: var ContextValues): + + let qname : QualName = (context, param) + if contextValues == nil: + contextValues = newTable[QualName, int]() + contextValues[qname] = value + +Then r""" context parameter is """, ( + scenario.defMod: LibModule, + context: string, param: string, value: int): + let getIntContext = bindInLib[GetIntContext]( + defMod, "getIntContext") + let actualValue = getIntContext(context, param) + assert actualValue == value + diff --git a/tests/steps/dynmodHooks.nim b/tests/steps/dynmodHooks.nim new file mode 100644 index 0000000..c46a12c --- /dev/null +++ b/tests/steps/dynmodHooks.nim @@ -0,0 +1,23 @@ +# package cucumber_nim +# module tests/steps/dynmodHooks + +import sets +import "../../cucumber/types" +import "../../cucumber/parameter" +import "../../cucumber/step" +import "../../cucumber/hook" +import "./dynmodule" + +##[ + Tag @defMod for scenarios that define steps or hooks themselves. + Tag @featureDefMod for features in which background defines + all steps and features. +]## + +BeforeScenario @defMod, ( + scenario.defMod: var LibModule + ): + defMod = LibModule(lib: nil, fn: nil) + +AfterScenario @defMod, (scenario.defMod: LibModule): + cleanupModule(defMod) diff --git a/tests/steps/dynmodule.nim b/tests/steps/dynmodule.nim new file mode 100644 index 0000000..0f23b69 --- /dev/null +++ b/tests/steps/dynmodule.nim @@ -0,0 +1,183 @@ +# package: cucumber_nim +# tests/steps/dynmodule.nim +##[ + Creates module containing step and hook definitions written + by steps. + + SECURITY WARNING: unsafe execution of code in /tmp. An attacker + with right permissions could overwrite. + + A single module is created so that hooks and steps share + the same globals. As tests are clearer when definitions are + specified in separate steps, the source of the module is created + incrementally. The first attempt to access the module causes it + to be written, compiled into a dynamic library and loaded. + +]## + +import os +import tables +import osproc +import dynlib +import strutils +import sequtils +import tempfile +import "../../cucumber/parameter" + +export LibHandle + +type + InitProc* = (proc() {.nimcall.}) + LibModule* = ref object + source*: string + fn*: string + lib*: LibHandle + +DeclareRefParamType(LibModule) + +proc buildModule(libMod: LibModule) + +proc indentCode*(text: string): string = + var lines = text.split("\n") + lines = lines.mapIt " " & it + result = lines.join("\n") + +proc built(libMod: LibModule): bool = libMod.fn != nil + +proc bindInLib*[T](libMod: LibModule, name: string, alt: string = nil) : T = + if not libMod.built: + libMod.buildModule + result = cast[T](checkedSymAddr(libMod.lib, name)) + if result == nil and alt != nil: + result = cast[T](checkedSymAddr(libMod.lib, alt)) + if result == nil: + raise newException( + ValueError, "Couldn't find $1 in $2" % [name, libFN(libMod.fn)]) + +proc libFN*(sourceFN: string) : string = + let (dir, base, ext) = splitFile(sourceFN) + discard ext + result = joinPath(dir, (DynlibFormat % base)) + +proc getFN*(libMod: LibModule) : string = + if libMod == nil or libMod.source == nil: + return "" + if libMod.fn == nil: + buildModule(libMod) + return libMod.fn + +let wrapper = """ +import macros +import strutils +import sets +import nre +import "$1/cucumber/types" +import "$1/cucumber/parameter" +import "$1/cucumber/step" +import "$1/cucumber/hook" +import "$1/cucumber/macroutil" + +macro defStep(step: typed) : untyped = + result = step + +type SR = ref object + items: seq[string] + +var stepReprs : SR = SR(items: newSeq[string]()) + +macro saveTree(step: typed) : untyped = + result = newCall( + newDot(newDot("stepReprs", "items"), "add"), step.treeRepr.newLit) + +macro defHook(hook: typed) : untyped = + result = hook + +type HR = ref object + items: seq[string] + +var hookReprs : HR = HR(items: newSeq[string]()) + +macro saveHookTree(hook: typed) : untyped = + result = newCall( + newDot(newDot("hookReprs", "items"), "add"), hook.treeRepr.newLit) + + +{.push exportc.} + +proc getStepDefns() : StepDefinitions = + return step.stepDefinitions + +proc getStepReprs() : SR = + stepReprs + +proc getHookDefns() : HookDefinitions = + return hook.hookDefinitions + +proc getHookReprs() : HR = + hookReprs + +proc setIntContext(context: string, param: string, value: int): void = + let ctype = contextTypeFor(context) + paramTypeIntSetter(ctype, param, value) + +proc setStringContext(context: string, param: string, value: string): void = + let ctype = contextTypeFor(context) + paramTypeStringSetter(ctype, param, value) + +proc getIntContext(context: string, param: string) : int = + let ctype = contextTypeFor(context) + return paramTypeIntGetter(ctype, param) + +{.pop.} +""" + +proc loadSource*(source: string): LibModule = + let module = LibModule(source: source) + return module + +proc addSource*( + libMod: var LibModule, source: string) = + if libMod.source == nil: + let baseDir = getCurrentDir() + let source = wrapper % baseDir & "\n" & source + libMod.source = source + else: + if libMod.fn != nil: + raise newException(Exception, "Module already built: $1" % libMod.fn) + libMod.source &= "\n" & source + +proc buildModule(libMod: LibModule) = + + let (file, stepFN) = mkstemp(mode = fmWrite, suffix = ".nim") + file.write(libMod.source) + file.close() + + let libFN = libFN(stepFN) + let output = execProcess( + "nim c --verbosity:0 --app:lib $1" % stepFN, + options = {poStdErrToStdOut, poUsePath, poEvalCommand}) + if not fileExists(libFN): + echo "COULDNT GENERATE STEP WRAPPER:" + echo output + raise newException( + ValueError, "Couldn't generate step wrapper (source in $1)." % stepFN) + + libMod.fn = stepFN + libMod.lib = loadLib(libFN) + let init = bindInLib[InitProc](libMod, "NimMain") + init() + +proc cleanupModule*(libMod: LibModule) : void = + assert libMod != nil + # TODO: unload causes hang. Pehaps memory from library left dangling? + # unloadLib(libMod.lib) + if fileExists(libMod.fn): + removeFile(libMod.fn) + let (dir, name, ext) = libMod.fn.splitFile + discard ext + let libFN = joinPath(dir, (DynLibFormat % name)) + if fileExists(libFN): + removeFile(libFN) + + + diff --git a/tests/steps/featureSteps.nim b/tests/steps/featureSteps.nim new file mode 100644 index 0000000..2ea86b4 --- /dev/null +++ b/tests/steps/featureSteps.nim @@ -0,0 +1,149 @@ +# package: cucumber +# module: tests/steps + +import tables +import sets +import streams +import strutils +import "../../cucumber" +import "../../cucumber/parameter" +import "../../cucumber/feature" +import macros + + +DeclareRefParamType(Stream) +DeclareRefParamType(Feature) + + + +Given "a feature file:", ( + quote.data: string, scenario.featureStream: var Stream): + featureStream = newStringStream(data) + +When "I read the feature file:", ( + quote.data: string, scenario.feature: var Feature): + + var featureStream = newStringStream(data) + feature = readFeature(featureStream) + +Then "reading the feature file causes an error:", ( + scenario.featureStream: Stream, quote.message: string): + try: + discard readFeature(featureStream) + except: + let exc = getCurrentException() + let amsg = exc.msg.strip() + let emsg = message.strip() + assert amsg == emsg, "$1 != $2" % [amsg, emsg] + +Then "the feature description is \"(.*)\"", ( + scenario.feature: Feature, description: string): + assert feature.description == description + +Then r"""the feature explanation is \"([^"]*)\"""", ( + scenario.feature: Feature, explanation: string): + assert feature.explanation.strip() == explanation.strip() + +Then r"the feature contains (\d+) scenarios", ( + scenario.feature: Feature, nscenarios: int): + assert feature.scenarios.len == nscenarios + +Then r"the feature has no background block", ( + scenario.feature: Feature): + assert feature.background == nil + +Then r"""the feature has tags? \"\"""", ( + scenario.feature: Feature, tags: string): + let taglist = tags.split.toSet + assert feature.tags == taglist + +Then r"scenario contains steps", ( + scenario.feature: Feature, iscenario: int, nsteps: int): + let scenario = feature.scenarios[iscenario] + assert scenario.steps.len == nsteps + +Then r"""scenario has tags? \"\"""", ( + scenario.feature: Feature, iscenario: int, tags: string): + let scenario = feature.scenarios[iscenario] + let taglist = tags.split.toSet + assert scenario.tags == taglist + +proc checkStepType(step: Step, typeName: string): void = + case typeName + of "Given": assert step.stepType == stGiven + of "When": assert step.stepType == stWhen + of "Then": assert step.stepType == stThen + else: + raise newException(AssertionError, "unknown step type " & typeName) + +Then r"""step (\d+) of scenario (\d+) is of type \"(\w+)\"""", ( + scenario.feature: Feature, istep: int, iscenario: int, typeName: string): + let step = feature.scenarios[iscenario].steps[istep] + checkStepType(step, typeName) + +Then r"""step (\d+) of the background is of type \"(\w+)\"""", ( + scenario.feature: Feature, istep: int, typeName: string): + let step = feature.background.steps[istep] + checkStepType(step, typeName) + +Then r"""step (\d+) of scenario (\d+) has text \"(.*)\"""", ( + scenario.feature: Feature, istep: int, iscenario: int, text: string): + let step = feature.scenarios[iscenario].steps[istep] + assert step.text == text + +Then r"""step (\d+) of the background has text \"(.*)\"""", ( + scenario.feature: Feature, istep: int, text: string): + let step = feature.background.steps[istep] + assert step.text == text + +Then r"""step (\d+) of scenario (\d+) has no block parameter""", ( + scenario.feature: Feature, istep: int, iscenario: int): + let step = feature.scenarios[iscenario].steps[istep] + assert step.blockParam == nil + +Then r"""step (\d+) of the background has no block parameter""", ( + scenario.feature: Feature, istep: int): + let step = feature.background.steps[istep] + assert step.blockParam == nil + +Then r"step (\d+) of scenario (\d+) has block parameter:", ( + scenario.feature: Feature, istep: int, iscenario: int, + quote.blockParam: string): + let step = feature.scenarios[iscenario].steps[istep] + assert step.blockParam.strip() == blockParam.strip() + +Then r"the feature has a background block", ( + scenario.feature: Feature): + assert feature.background != nil + +Then r"the background contains (\d+) steps", ( + scenario.feature: Feature, nsteps: int): + let background = feature.background + assert background.steps.len == nsteps + +Then r"scenario (\d+) contains (\d+) examples?", ( + scenario.feature: Feature, iscenario: int, nexamples: int): + let scenario = feature.scenarios[iscenario] + assert scenario.examples.len == nexamples + +Then r"example (\d+) of scenario (\d+) has (\d+) column", ( + scenario.feature: Feature, iexample: int, iscenario: int, ncolumns int): + let example = feature.scenarios[iscenario].examples[iexample] + assert example.columns.len == ncolumns + +Then r"""column (\d+) of example (\d+), scenario (\d+) is named \"([^\"]*)\"""", ( + scenario.feature: Feature, icolumn: int, iexample: int, iscenario: int, columnName: string): + let example = feature.scenarios[iscenario].examples[iexample] + let column = example.columns[icolumn] + assert column == columnName + +Then r"step of scenario has table with rows and columns:", ( + scenario.feature: Feature, + istep: int, iscenario: int, irows: int, column.name: seq[string]): + let step = feature.scenarios[iscenario].steps[istep] + let table = step.table + assert table != nil + assert name.len == table.columns.len + assert irows == table.values.len + for i, n in table.columns: + assert n == name[i] diff --git a/tests/steps/hookDefinitionSteps.nim b/tests/steps/hookDefinitionSteps.nim new file mode 100644 index 0000000..9722d30 --- /dev/null +++ b/tests/steps/hookDefinitionSteps.nim @@ -0,0 +1,245 @@ +# package cucumber_nim +# module tests/steps/hookDefinitionSteps + +import sets +import tables +import os +import strutils +import sequtils +import nre +import "../../cucumber" +import "../../cucumber/parameter" +import "../../cucumber/feature" +import "../../cucumber/hook" +import "./dynmodule" +import "./contextutil" +import "./ntree" + +type + HR = ref object + items: seq[string] + + GetDefns = (proc(): HookDefinitions {.nimcall.}) + GetReprs = (proc(): HR {.nimcall.}) + + HookTrees = ref object + items: array[HookType, seq[NTree]] + + HookDefInstrument* = ref object + module: LibModule + definitionsP: HookDefinitions + treesP: HookTrees + +proc definitions*(hookDefs: HookDefInstrument) : HookDefinitions +proc `[]`*( + hooks: HookDefInstrument, hookType: HookType, ihook: int + ): HookDefinition = + assert ihook < hooks.definitions[hookType].len + hooks.definitions[hookType][ihook] + +proc newHookType(): HookType = htBeforeAll +DeclareParamType( + "HookType", HookType, hookTypeFor, newHookType, r"(\w*)" ) + +proc newStringSet(): StringSet = initSet[string]() +proc parseStringSet(s : string): StringSet = + (s.split(',').mapIt it.strip).toSet +DeclareParamType( + "StringSet", StringSet, parseStringSet, newStringSet, r"\{(.*)\}") +DeclareRefParamType(HookDefInstrument) + +proc `[]`*(ht: var HookTrees, hookType: HookType): var seq[NTree] = + ht.items[hookType] +proc `[]`*(ht: HookTrees, hookType: HookType, istep: int) : NTree = + ht.items[hookType][istep] + +proc newHookTrees(): HookTrees = + HookTrees(items: [ + newSeq[NTree](), newSeq[NTree](), newSeq[NTree](), newSeq[NTree](), + newSeq[NTree](), newSeq[NTree](), newSeq[NTree](), newSeq[NTree](), + ]) + +proc addToTree*(hookTrees: var HookTrees, treeRepr: string) : void = + var active: seq[NTree] = @[] + for line in treeRepr.split("\n"): + let nindent = ((line.match re"^\s*").get.match.len) div 2 + let node = NTree(content: line.strip, children: newSeq[NTree]()) + if nindent >= active.len: + active.add(node) + else: + active[nindent] = node + setLen(active, nindent + 1) + if nindent > 0: + active[nindent - 1].children.add(node) + if active.len > 0: + # retrieve hook type from call to add hook to definitions + let last = active[0][^1] + let hkNode = last[1][0][2] + let stype = hookTypeFor(hkNode.content.split()[1][3..^2]) + hookTrees[stype].add(active[0]) + +let hookSectionTemplate = """ +defHook: +$1 + +saveHookTree: +$1 +""" + +let hookStartRE = ( + re"""(?m)^(?=(?:Before|After)(?:All|Feature|Scenario|Step))""" ) +proc splitHooks(data: string) : seq[string] = + data.split(hookStartRE) + +proc newHookDefInstrument( + libMod: var LibModule, data: string): HookDefInstrument = + + var strDefs = data.substr.splitHooks + var defSects = strDefs.map (proc (s:string) : string = + let text = indentCode(s) + result = hookSectionTemplate % text) + let sectionsText = defSects.join("\n") + addSource(libMod, sectionsText) + result = HookDefInstrument(module: libMod) + + +proc loadHookDefinitions(hookDefs: HookDefInstrument) = + let getDefns = bindInLib[GetDefns](hookDefs.module, "getHookDefns") + var hookDefinitions = getDefns() + let getHookReprs = bindInLib[GetReprs](hookDefs.module, "getHookReprs") + let hookReprs = getHookReprs() + var hookTrees = newHookTrees() + for hrepr in hookReprs.items: + hookTrees.addToTree(hrepr) + hookDefs.definitionsP = hookDefinitions + hookDefs.treesP = hookTrees + +proc definitions*(hookDefs: HookDefInstrument) : HookDefinitions = + if hookDefs.definitionsP == nil: + loadHookDefinitions(hookdefs) + return hookDefs.definitionsP + +proc trees*(hookDefs: HookDefInstrument) : HookTrees = + if hookDefs.treesP == nil: + loadHookDefinitions(hookdefs) + return hookDefs.treesP + +# --------------------------------------------------------------------- + + +Given "a hook definition:", ( + quote.data: string, hookType: HookType, + scenario.hooks: var HookDefInstrument, + scenario.defMod: var LibModule + ): + discard hookType + hooks = newHookDefInstrument(defMod, data) + +Given "(?:a )?hook definitions?:", ( + quote.data: string, + scenario.hooks: var HookDefInstrument, + scenario.defMod: var LibModule + ): + hooks = newHookDefInstrument(defMod, data) + +Then r"""I have hook definitions?""", ( + scenario.hooks: HookDefInstrument, + nhooks: int, hookType: HookType): + assert hooks.definitions[hookType].len == nhooks + +Then r"""hook takes arguments from context.""", ( + scenario.hooks: HookDefInstrument, + hookType: HookType, ihook: int, + nargs: int): + let hookTree = hooks.trees[hookType, ihook] + let args = getArgsFromNTree(hookTree) + let cargs = args.filterIt(it.atype in {ctGlobal, ctFeature, ctScenario}) + assert cargs.len == nargs + +Then r"""hook has no tags""", ( + scenario.hooks: HookDefInstrument, + hookType: HookType, ihook: int, + ): + let hookTree = hooks.trees[hookType, ihook] + let tagExpr = hookTree[0][^2][1] + assert tagExpr.content.split()[1] == "1" + +proc fillHookArgs(hookType: HookType) : (Feature, Scenario, Step) = + var feature = Feature() + var scenario = Scenario(parent: feature) + var step = Step(parent: scenario) + case hookType + of htBeforeAll, htAfterAll: + feature = nil + scenario = nil + step = nil + of htBeforeFeature, htAfterFeature: + scenario = nil + step = nil + of htBeforeScenario, htAfterScenario: + step = nil + else: + discard + +proc checkRun( + hook: HookDefinition, + feature: Feature, scenario: Scenario, step: Step, + excMessage: string = nil + ): void = + var exc: ref Exception + try: + hook.defn(feature, scenario, step) + except: + if excMessage == "*": + return + exc = getCurrentException() + echo "EXC " & exc.msg + if excMessage == nil or excMessage.strip != exc.msg.strip: + echo "UNEXPECTED EXCEPTION RUNNING SAMPLE HOOK: " & exc.msg + echo exc.getStackTrace + assert exc.msg == nil + if excMessage != nil and exc == nil: + raise newException(AssertionError, "expecting exception: " & excMessage) + +Then "running hook succeeds.", ( + scenario.hooks: HookDefInstrument, + scenario.defMod: LibModule, + hookType: HookType, ihook: int, + scenario.contextValues: ContextValues + ): + let hook = hooks[hookType, ihook] + let (feature, scenario, step) = fillHookArgs(hookType) + fillContext(defMod, contextValues) + checkRun(hook, feature, scenario, step) + +Then "running hook fails.", ( + scenario.hooks: HookDefInstrument, + scenario.defMod: LibModule, + hookType: HookType, ihook: int, + scenario.contextValues: ContextValues + ): + let hook = hooks[hookType, ihook] + let (feature, scenario, step) = fillHookArgs(hookType) + fillContext(defMod, contextValues) + checkRun(hook, feature, scenario, step, "*") + +Then "running hook fails with message:", ( + quote.message: string, + scenario.defMod: LibModule, + scenario.hooks: HookDefInstrument, + hookType: HookType, ihook: int, + scenario.contextValues: ContextValues + ): + let hook = hooks[hookType, ihook] + let (feature, scenario, step) = fillHookArgs(hookType) + fillContext(defMod, contextValues) + checkRun(hook, feature, scenario, step, message) + +Then "hook tag filter (matches|doesn't match) ", ( + scenario.hooks: HookDefInstrument, + hookType: HookType, ihook: int, + matches: string, tagSet: StringSet): + + let matches = matches == "matches" + let hook = hooks[hookType, ihook] + assert hook.tagFilter(tagSet) == matches diff --git a/tests/steps/ntree.nim b/tests/steps/ntree.nim new file mode 100644 index 0000000..dddaae7 --- /dev/null +++ b/tests/steps/ntree.nim @@ -0,0 +1,63 @@ +# package: cucumber_nim +# module ntree.nim +# +##[ + NTrees are simplified stand-ins for NimNodes, derived + from "dumpTree". They owe their existence to the fact that I + could not get ahold of the actual NimNodes from a dynamically + loaded module, but could get a hold of the string from "treeRepr". +]## + +import sequtils +import strutils +import nre +import options +import "../../cucumber/types" + +type + NTree* = ref NTreeObj + NTreeObj* = object + content*: string + children*: seq[NTree] + +proc `[]`*(nt: NTree, i: int) : NTree = nt.children[i] + +proc len*(nt: NTree) : int = nt.children.len + +proc `$`*(nt: NTree, indent: int = 0) : string = + result = repeat(" ", indent) & nt.content & "\n" + for child in nt.children: + result = result & `$`(child, indent + 2) + +let symRE = re"""\"(.*)\"""" +proc getSym*(nt: NTree): string = + try: + return nt.content.find(symRE).get.captures[0] + except Exception: + raise newException(ValueError, "Couldn't get symbol from " & nt.content) + +type + ArgDesc* = tuple + name: string + atype: ContextType + +proc getArgsFromNTree*(nt : NTree) : seq[ArgDesc] = + var procStart: NTree + if nt[1][^2].content != "Empty": + procStart = nt[1][^2][1][1] + else: + procStart = nt[1][^1] + let children = procStart[0].children + result = @[] + for child in children: + let name = child[0].getSym + var ncall = child[2] + if ncall.content == "HiddenDeref": + ncall = ncall[0] + let fct = ncall[0].getSym + var ctype: ContextType + if fct == "paramTypeIntGetter": + ctype = ctGlobal + else: + ctype = ctNotContext + result.add((name, ctype)) diff --git a/tests/steps/runnerSteps.nim b/tests/steps/runnerSteps.nim new file mode 100644 index 0000000..f057f60 --- /dev/null +++ b/tests/steps/runnerSteps.nim @@ -0,0 +1,163 @@ +# tests/steps/runnerSteps.nim + +import future +import os +import tables +import sets +import sequtils +import strutils +import "../../cucumber" +import "../../cucumber/step" +import "../../cucumber/hook" +import "../../cucumber/runner" +import "../../cucumber/parameter" +import "../../cucumber/report" +import "./featureSteps" +import "./stepDefinitionSteps" +import "./hookDefinitionSteps" +import "./dynmodule" + +type + ScenarioResults* = ref object + items: seq[ScenarioResult] + RunFeaturesProc* = ( + proc(data: string, options: CucumberOptions): ScenarioResults {.nimcall.}) + +DeclareRefParamType(ScenarioResults) + +let runnerModuleTemplate = """ + +import streams +import "$1/cucumber/types" +import "$1/cucumber/feature" +import "$1/cucumber/runner" +import "$1/cucumber/hook" + +const defModulePresent = $2 +when defModulePresent: + import "$3" + +type + ScenarioResults* = ref object + items: seq[ScenarioResult] + + +{.push exportc.} + +# NB -- when defined in "runFeatures" have problem with garbage collection +# "grow". OK like this for the moment just if "runFeatures" is called once +var features: seq[Feature] = @[] +var sresults: seq[ScenarioResult] = newSeq[ScenarioResult]() + +proc runFeatures(data: string, options: CucumberOptions): ScenarioResults = + var featureStream = newStringStream(data) + let feature = readFeature(featureStream) + features.add(feature) + let itr = runner(features, options) + for i, sresult in itr(): + sresults.add(sresult) + + result = ScenarioResults(items: sresults) + +{.pop.} +""" + +proc len(a: ScenarioResults) : int = a.items.len + + +# --------------------------------------------------------------------- + +AfterScenario @runnerMod, (scenario.runnerMod: var LibModule): + cleanupModule(runnerMod) + +proc runFeature( + data: string, results: var ScenarioResults, defMod: LibModule, + runnerMod: var LibModule, defineTags: StringSet = initSet[string]() + ) = + + let baseDir = getCurrentDir() + let defModulePresent = defMod.getFN != "" + let runnerSource = runnerModuleTemplate % [ + baseDir, $defModulePresent, defMod.getFN + ] + runnerMod = loadSource(runnerSource) + let runFeatures = bindInLib[RunFeaturesProc](runnerMod, "runFeatures") + let options = CucumberOptions( + verbosity: -2, bail: false, + tagFilter: (s: StringSet)=> not("@skip" in s), + defineTags: defineTags ) + results = runFeatures(data, options) + +When "I run the feature:", ( + quote.data: string, + scenario.results: var ScenarioResults, + scenario.defMod: LibModule, + scenario.runnerMod: var LibModule + ): + runFeature(data, results, defMod, runnerMod) + +When "I run the feature with \"\" defined:", ( + quote.data: string, + scenario.results: var ScenarioResults, + scenario.defMod: LibModule, + scenario.runnerMod: var LibModule, + tagsToDefine: string): + + var defineTags = initSet[string]() + if tagsToDefine.len > 0: + for s in tagsToDefine.split(","): + defineTags.incl(s) + runFeature(data, results, defMod, runnerMod, defineTags) + +Then "there are scenario results", ( + nresults: int, + scenario.results: ScenarioResults): + assert results.len == nresults + +type + ResultsSummary = array[StepResultValue, int] + +proc summary(results: ScenarioResults) : ResultsSummary = + var sum : ResultsSummary = [0, 0, 0, 0] + for sresult in results.items: + inc sum[sresult.stepResult.value] + result = sum + +Then r"there (?:(?:is)|(?:are)) successful scenarios?", ( + nsucc: int, + scenario.results: ScenarioResults): + let summary = summary(results) + assert summary[srSuccess] == nsucc + + +Then r"""scenario results are distributed: \[\].""", ( + scenario.results: ScenarioResults, expected: string): + let expected = expected.split(',').mapIt parseInt(it.strip) + let summary = summary(results) + var isErr = false + for i in countUp(0, 3): + if summary[StepResultValue(i)] != expected[i]: + isErr = true + if isErr: + var msg = """ + results: $1 != $2 + """ % [ + (summary.mapIt($it)).join(", "), + (expected.mapIt($it)).join(", ")] + if summary[srFail] > expected[1]: + msg &= "\nFailures detail: ---------------\n\n" + for sresult in results.items: + let resultValue = sresult.stepResult.value + if resultValue != srSuccess: + msg &= "$1: $2\n" % [ + sresult.scenario.description, resultDesc[resultValue]] + msg &= " Step: $1\n" % sresult.step.description + let exc = sresult.stepResult.exception + if exc == nil or (exc of NoDefinitionForStep): + continue + msg &= "\nDetail: \n" & sresult.stepResult.exception.msg + msg &= sresult.stepResult.exception.getStackTrace() + msg &= "------------------- (end detail) ----\n" + + raise newException(ValueError, msg) + \ No newline at end of file diff --git a/tests/steps/stepDefinitionSteps.nim b/tests/steps/stepDefinitionSteps.nim new file mode 100644 index 0000000..f915550 --- /dev/null +++ b/tests/steps/stepDefinitionSteps.nim @@ -0,0 +1,255 @@ +# tests/steps/stepDefinitionSteps +# +# Steps for testing step definition + +import future +import macros +import tables +import sets +import strutils +import os +import nre +import sequtils +import "../../cucumber" +import "../../cucumber/step" +import "../../cucumber/hook" +import "../../cucumber/parameter" +import "../../cucumber/macroutil" +import "./dynmodule" +import "./ntree" +import "./contextutil" + +type + SR = ref object + items: seq[string] + + GetDefns = (proc(): StepDefinitions {.nimcall.}) + GetReprs = (proc(): SR {.nimcall.}) + + StepTrees = ref object + items: array[StepType, seq[NTree]] + + StepDefInstrument* = ref object + module: LibModule + definitionsP: StepDefinitions + treesP: StepTrees + +proc `[]`*(st: StepTrees, stepType: StepType): var seq[NTree] = + st.items[stepType] + +proc `[]`*(st: StepTrees, stepType: StepType, istep: int): NTree = + st.items[stepType][istep] + +proc newStepTrees(): StepTrees = + StepTrees(items: [newSeq[NTree](), newSeq[NTree](), newSeq[NTree]()]) + +proc addToTree*(stepTrees: var StepTrees, treeRepr: string) : void = + var active: seq[NTree] = @[] + for line in treeRepr.split("\n"): + let nindent = ((line.match re"^\s*").get.match.len) div 2 + let node = NTree(content: line.strip, children: newSeq[NTree]()) + if nindent >= active.len: + active.add(node) + else: + active[nindent] = node + setLen(active, nindent + 1) + if nindent > 0: + active[nindent - 1].children.add(node) + if active.len > 0: + let last = active[0][^1] + let stNode = last[1][0][2] + let stype = stepTypeFor(stNode.content.split()[1][3..^2]) + stepTrees[stype].add(active[0]) + +proc loadStepDefinitions(stepDefs: StepDefInstrument) = + let getDefns = bindInLib[GetDefns](stepDefs.module, "getStepDefns") + var stepDefinitions = getDefns() + let getStepReprs = bindInLib[GetReprs](stepDefs.module, "getStepReprs") + let stepReprs = getStepReprs() + var stepTrees = newStepTrees() + for srepr in stepReprs.items: + stepTrees.addToTree(srepr) + stepDefs.definitionsP = stepDefinitions + stepDefs.treesP = stepTrees + +proc definitions*(stepDefs: StepDefInstrument): StepDefinitions = + if stepDefs.definitionsP == nil: + loadStepDefinitions(stepDefs) + return stepDefs.definitionsP + +proc trees*(stepDefs: StepDefInstrument): var StepTrees = + if stepDefs.treesP == nil: + loadStepDefinitions(stepDefs) + return stepDefs.treesP + +DeclareRefParamType(StepDefInstrument) +proc newStepType(): StepType = stGiven +DeclareParamType( + "StepType", StepType, stepTypeFor, newStepType, r"(\w*)" ) + + +proc `[]`*( + steps: StepDefInstrument, stepType: StepType, istep: int + ): StepDefinition = + assert istep < steps.definitions[stepType].len + steps.definitions[stepType][istep] + +let stepStart = re"""(?m)^(?=Given|When|Then)""" +proc splitSteps(data: string) : seq[string] = + data.split(stepStart) + +let stepSectionTemplate = """ +defStep: +$1 + +saveTree: +$1 +""" +## Template for nim module which defines steps + + +proc newStepDefInstrument( + libMod: var LibModule, data: string) : StepDefInstrument = + + var strDefs = data.substr.splitSteps + var defSects = strDefs.map (proc (s:string) : string = + let codeText = indentCode(s) + result = stepSectionTemplate % codeText + ) + let sectionsText = defSects.join("\n") + addSource(libMod, sectionsText) + result = StepDefInstrument(module: libMod) + +# --------------------------------------------------------------------- + +Given "(?:a )?step definitions?:", ( + quote.data: string, + scenario.steps: var StepDefInstrument, + scenario.defMod: var LibModule + ): + steps = newStepDefInstrument(defMod, data) + +Given "a step definition:", ( + quote.data: string, + stepType: StepType, + scenario.steps: var StepDefInstrument, + scenario.defMod: var LibModule + ): + discard stepType + steps = newStepDefInstrument(defMod, data) + +Then r"""I have step definitions?""", ( + scenario.steps: StepDefInstrument, + nsteps: int, stepType: StepType): + assert steps.definitions[stepType].len == nsteps + +Then r"""step (\d+) has pattern \"([^\"]*)\"""", ( + scenario.steps: StepDefInstrument, + stepType: StepType, istep: int, + pattern: string): + let step = steps[stepType, istep] + assert step.stepRE.pattern == pattern + +Then r"""step (\d+) takes (\d+) arguments from step text.""", ( + scenario.steps: StepDefInstrument, + stepType: StepType, istep: int, + nargs: int): + let trees = steps.trees + let stepTree = trees[stepType, istep] + let args = getArgsFromNTree(stepTree) + let targs = args.filterIt(it.atype == ctNotContext) + assert targs.len == nargs + +Then r"""step (\d+) takes (\d+) arguments from context.""", ( + scenario.steps: StepDefInstrument, + stepType: StepType, istep: int, + nargs: int): + let stepTree = steps.trees[stepType][istep] + let args = getArgsFromNTree(stepTree) + let cargs = args.filterIt(it.atype in {ctGlobal, ctFeature, ctScenario}) + assert cargs.len == nargs + +Then r"""step (\d+) expects block.""", ( + scenario.steps: StepDefInstrument, + stepType: StepType, istep: int, + expectsBlock: bool): + let step = steps[stepType, istep] + assert bool(step.blockParamName != nil) == expectsBlock + let stepTree = steps.trees[stepType][istep] + let args = getArgsFromNTree(stepTree) + let qargs = args.filterIt(it.name == step.blockParamName) + assert qargs.len == int(expectsBlock) + + +proc checkSucceedsOrFails(value: StepResultValue, succeedsOrFails: string) = + case succeedsOrFails.strip(chars = {'.'}) + of "succeeds": + assert value == srSuccess + of "fails": + assert value == srFail + else: + raise newException( + Exception, "unexpected result type: " & succeedsOrFails) + +proc checkRun(step: StepDefinition, args: StepArgs): StepResult = + try: + return step.defn(args) + except: + let exc = getCurrentException() + echo "UNEXPECTED EXCEPTION RUNNING SAMPLE STEP: " & exc.msg + echo exc.getStackTrace + assert exc.msg == nil + +Then r"""running step \.$""", ( + scenario.steps: StepDefInstrument, + scenario.defMod: var LibModule, + stepType: StepType, istep: int, + succeedsOrFails: string, + scenario.contextValues: ContextValues + ): + let step = steps[stepType, istep] + let args = StepArgs(stepText: "a step definition:") + fillContext(defMod, contextValues) + let value = checkRun(step, args).value #step.defn(args).value + checkSucceedsOrFails(value, succeedsOrFails) + +Then r"""running step with text \"([^\"]*)\"""", ( + scenario.steps: StepDefInstrument, + scenario.defMod: var LibModule, + stepType: StepType, istep: int, + succeedsOrFails: string, + stepText: string, + scenario.contextValues: ContextValues): + + let step = steps[stepType, istep] + let args = StepArgs(stepText: stepText) + fillContext(defMod, contextValues) + let value = checkRun(step, args).value + checkSucceedsOrFails(value, succeedsOrFails) + +Then r"""step with block .""", ( + scenario.steps: StepDefInstrument, + scenario.defMod: var LibModule, + stepType: StepType, istep: int, + succeedsOrFails: string, param: string + ): + let step = steps[stepType, istep] + let formal = getArgsFromNTree(steps.trees[stepType][istep]) + let name = formal[0].name + let args = StepArgs(stepText: "a step definition:", blockParam: param) + let setStringContext = bindInLib[SetStringContext]( + defMod, "setStringContext") + setStringContext("quote", name, param) + let value = checkRun(step, args).value + checkSucceedsOrFails(value, succeedsOrFails) + +Then r"""running step fails with error:""", ( + scenario.steps: StepDefInstrument, + stepType: StepType, istep: int, + quote.excText: string): + let step = steps[stepType, istep] + let args = StepArgs(stepText: "a failing step definition") + let stepResult = step.defn(args) + assert stepResult.value == srFail + assert stepResult.exception.msg.strip == excText.strip +