yard_example_runner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.commitlintrc.yml +38 -0
- data/.github/workflows/continuous-integration.yml +47 -0
- data/.github/workflows/enforce_conventional_commits.yml +27 -0
- data/.github/workflows/release.yml +52 -0
- data/.gitignore +28 -0
- data/.husky/commit-msg +1 -0
- data/.markdownlint.yml +25 -0
- data/.release-please-config.json +35 -0
- data/.release-please-manifest.json +3 -0
- data/.rubocop.yml +27 -0
- data/AI_POLICY.md +26 -0
- data/CHANGELOG.md +21 -0
- data/CODE_OF_CONDUCT.md +25 -0
- data/CONTRIBUTING.md +237 -0
- data/GOVERNANCE.md +103 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +23 -0
- data/MAINTAINERS.md +16 -0
- data/README.md +516 -0
- data/Rakefile +59 -0
- data/bin/setup +13 -0
- data/lib/yard/cli/run_examples.rb +297 -0
- data/lib/yard_example_runner/example/comparison.rb +250 -0
- data/lib/yard_example_runner/example/constant_sandbox.rb +123 -0
- data/lib/yard_example_runner/example/evaluator.rb +145 -0
- data/lib/yard_example_runner/example.rb +360 -0
- data/lib/yard_example_runner/expectation.rb +23 -0
- data/lib/yard_example_runner/rake.rb +92 -0
- data/lib/yard_example_runner/version.rb +7 -0
- data/lib/yard_example_runner.rb +134 -0
- data/package.json +12 -0
- data/rake_tasks/cucumber.rake +9 -0
- data/rake_tasks/gem_tasks.rake +12 -0
- data/rake_tasks/markdownlint.rake +6 -0
- data/rake_tasks/rubocop.rake +5 -0
- data/rake_tasks/yard.rake +39 -0
- data/yard_example_runner.gemspec +43 -0
- metadata +251 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardExampleRunner
|
|
4
|
+
class Example < ::Minitest::Spec
|
|
5
|
+
# Isolates constant definitions introduced during example evaluation
|
|
6
|
+
#
|
|
7
|
+
# Resolves the top-level class or module for a YARD definition path,
|
|
8
|
+
# snapshots the constants on both +Object+ and that scope, yields control
|
|
9
|
+
# to the caller, then removes any constants that were added during the
|
|
10
|
+
# block. This prevents one example's constant definitions from leaking
|
|
11
|
+
# into subsequent examples.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# ConstantSandbox.new('MyClass#foo').isolate do |scope|
|
|
15
|
+
# # evaluate code that may define constants…
|
|
16
|
+
# end
|
|
17
|
+
# # any constants defined inside the block are now removed
|
|
18
|
+
#
|
|
19
|
+
class ConstantSandbox
|
|
20
|
+
# Creates a sandbox for the given YARD definition path
|
|
21
|
+
#
|
|
22
|
+
# @param definition [String] a YARD path such as +"MyClass#method"+ or
|
|
23
|
+
# +"MyClass.method"+
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# ConstantSandbox.new('MyClass#foo')
|
|
27
|
+
#
|
|
28
|
+
# @api private
|
|
29
|
+
#
|
|
30
|
+
def initialize(definition)
|
|
31
|
+
@scope = resolve_scope(definition)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Snapshots constants, yields the resolved scope, then cleans up
|
|
35
|
+
#
|
|
36
|
+
# Any constants added to +Object+ or the resolved scope during the
|
|
37
|
+
# block are removed when the block returns (or raises), with one
|
|
38
|
+
# exception: constants added to +Object+ whose source file was newly
|
|
39
|
+
# loaded via +require+ during the block are preserved. This prevents
|
|
40
|
+
# one example's constant definitions from leaking into subsequent
|
|
41
|
+
# examples while still allowing +require+ calls to have their normal
|
|
42
|
+
# lasting effect (re-requiring a cached file would be a no-op, so
|
|
43
|
+
# stripping those constants would cause +NameError+ in later examples).
|
|
44
|
+
#
|
|
45
|
+
# The resolved scope is yielded so that callers can use it as the
|
|
46
|
+
# evaluation binding.
|
|
47
|
+
#
|
|
48
|
+
# @yield [scope] gives the resolved class/module (or +nil+) to the block
|
|
49
|
+
# @yieldparam scope [Class, Module, nil] the resolved scope constant
|
|
50
|
+
#
|
|
51
|
+
# @return [void]
|
|
52
|
+
#
|
|
53
|
+
# @example
|
|
54
|
+
# sandbox.isolate { |scope| scope.class_eval(code) }
|
|
55
|
+
#
|
|
56
|
+
# @api private
|
|
57
|
+
#
|
|
58
|
+
def isolate
|
|
59
|
+
global_before = Object.constants
|
|
60
|
+
scope_before = @scope.respond_to?(:constants) ? @scope.constants : nil
|
|
61
|
+
loaded_before = $LOADED_FEATURES.dup
|
|
62
|
+
|
|
63
|
+
yield @scope
|
|
64
|
+
ensure
|
|
65
|
+
loaded_during = $LOADED_FEATURES - loaded_before
|
|
66
|
+
clear_extra_constants(Object, global_before, skip_if_loaded_by: loaded_during)
|
|
67
|
+
clear_extra_constants(@scope, scope_before) if scope_before
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Resolves the top-level class or module constant for a YARD definition path
|
|
73
|
+
#
|
|
74
|
+
# Extracts the leading constant name from +definition+ (the portion before
|
|
75
|
+
# the first +#+ or +.+ separator), then returns the corresponding constant
|
|
76
|
+
# from +Object+ if it exists. Returns +nil+ if the definition does not start
|
|
77
|
+
# with a constant name or if the constant is not currently defined.
|
|
78
|
+
#
|
|
79
|
+
# @param definition [String] a YARD path such as +"MyClass#method"+
|
|
80
|
+
#
|
|
81
|
+
# @return [Class, Module, nil] the resolved constant, or +nil+
|
|
82
|
+
#
|
|
83
|
+
# @example
|
|
84
|
+
# resolve_scope('MyClass#foo') # => MyClass
|
|
85
|
+
#
|
|
86
|
+
# @api private
|
|
87
|
+
#
|
|
88
|
+
def resolve_scope(definition)
|
|
89
|
+
name = definition.split(/#|\./).first
|
|
90
|
+
Object.const_get(name) if name&.match?(/\A[A-Z]/) && Object.const_defined?(name)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Removes constants from +scope+ that were not present in +before+
|
|
94
|
+
#
|
|
95
|
+
# When +skip_if_loaded_by+ is non-empty, any constant on +scope+ whose
|
|
96
|
+
# source file (as reported by +Module#const_source_location+) appears in
|
|
97
|
+
# +skip_if_loaded_by+ is preserved rather than removed. This is used to
|
|
98
|
+
# retain constants introduced by +require+ calls during example evaluation.
|
|
99
|
+
#
|
|
100
|
+
# @param scope [Module] the scope to clean up
|
|
101
|
+
# @param before [Array<Symbol>] the constant names present before evaluation
|
|
102
|
+
# @param skip_if_loaded_by [Array<String>] absolute paths of files newly
|
|
103
|
+
# loaded during evaluation; constants defined in these files are kept
|
|
104
|
+
#
|
|
105
|
+
# @return [void]
|
|
106
|
+
#
|
|
107
|
+
# @example
|
|
108
|
+
# clear_extra_constants(Object, before_constants)
|
|
109
|
+
#
|
|
110
|
+
# @api private
|
|
111
|
+
#
|
|
112
|
+
def clear_extra_constants(scope, before, skip_if_loaded_by: [])
|
|
113
|
+
(scope.constants - before).each do |constant|
|
|
114
|
+
if skip_if_loaded_by.any?
|
|
115
|
+
source_file, = scope.const_source_location(constant.to_s)
|
|
116
|
+
next if source_file && skip_if_loaded_by.include?(source_file)
|
|
117
|
+
end
|
|
118
|
+
scope.__send__(:remove_const, constant)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardExampleRunner
|
|
4
|
+
class Example < ::Minitest::Spec
|
|
5
|
+
# Manages binding creation and code evaluation for example expressions
|
|
6
|
+
#
|
|
7
|
+
# Each {Evaluator} is constructed with a fallback binding (whose +self+ is
|
|
8
|
+
# the +Minitest::Spec+ instance, providing access to any methods included
|
|
9
|
+
# on {Example} such as +RSpec::Matchers+) and a snapshot of instance
|
|
10
|
+
# variables from the spec instance (set by +before+ hooks). These are used
|
|
11
|
+
# to build per-scope evaluation contexts that mirror the documented class's
|
|
12
|
+
# namespace.
|
|
13
|
+
#
|
|
14
|
+
# @see Example
|
|
15
|
+
#
|
|
16
|
+
class Evaluator
|
|
17
|
+
# Creates a new evaluator
|
|
18
|
+
#
|
|
19
|
+
# @param fallback_binding [Binding] a binding whose +self+ is the spec
|
|
20
|
+
# instance, used when the expression is not scoped to a specific class
|
|
21
|
+
#
|
|
22
|
+
# @param instance_variables [Hash{Symbol => Object}] a snapshot of instance
|
|
23
|
+
# variable names to values from the spec instance, transplanted into
|
|
24
|
+
# class-scoped bindings so that hook-set state is accessible
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# Evaluator.new(fallback_binding: binding, instance_variables: {})
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
#
|
|
31
|
+
def initialize(fallback_binding:, instance_variables:)
|
|
32
|
+
@fallback_binding = fallback_binding
|
|
33
|
+
@instance_variables = instance_variables
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Evaluates a Ruby code string in the given scope
|
|
37
|
+
#
|
|
38
|
+
# @param code [String] the Ruby expression to evaluate
|
|
39
|
+
# @param bind [Class, nil] the class scope to evaluate in, or +nil+ for
|
|
40
|
+
# the default (fallback) binding
|
|
41
|
+
#
|
|
42
|
+
# @return [Object] the result of evaluating +code+
|
|
43
|
+
#
|
|
44
|
+
# @raise [StandardError] any error raised during evaluation propagates
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# evaluator.evaluate('1 + 1', nil) # => 2
|
|
48
|
+
#
|
|
49
|
+
# @api private
|
|
50
|
+
#
|
|
51
|
+
def evaluate(code, bind)
|
|
52
|
+
context(bind).eval(code)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Evaluates a Ruby code string, capturing any +StandardError+ as a value
|
|
56
|
+
#
|
|
57
|
+
# If evaluation raises a +StandardError+, the error itself is returned
|
|
58
|
+
# instead of propagating. This allows callers to compare raised errors
|
|
59
|
+
# against expected error values.
|
|
60
|
+
#
|
|
61
|
+
# @param code [String] the Ruby expression to evaluate
|
|
62
|
+
# @param bind [Class, nil] the class scope to evaluate in
|
|
63
|
+
#
|
|
64
|
+
# @return [Object, StandardError] the result of evaluation, or the error
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# evaluator.evaluate_with_assertion('raise "oops"', nil) # => RuntimeError
|
|
68
|
+
#
|
|
69
|
+
# @api private
|
|
70
|
+
#
|
|
71
|
+
def evaluate_with_assertion(code, bind)
|
|
72
|
+
evaluate(code, bind)
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
e
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Returns or creates the cached evaluation context for the given scope
|
|
80
|
+
#
|
|
81
|
+
# @param bind [Class, nil] the class scope
|
|
82
|
+
#
|
|
83
|
+
# @return [Binding] a binding suitable for evaluating code in +bind+
|
|
84
|
+
#
|
|
85
|
+
# @example
|
|
86
|
+
# context(MyClass) # => Binding
|
|
87
|
+
#
|
|
88
|
+
# @api private
|
|
89
|
+
#
|
|
90
|
+
def context(bind)
|
|
91
|
+
@contexts ||= {}.compare_by_identity
|
|
92
|
+
@contexts[bind] ||= build_context(bind)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Builds an evaluation context for the given scope
|
|
96
|
+
#
|
|
97
|
+
# When +bind+ responds to +class_eval+, a new binding is opened inside
|
|
98
|
+
# that class and instance variables are transplanted into it. Otherwise
|
|
99
|
+
# the fallback binding (whose +self+ is the spec instance) is returned.
|
|
100
|
+
#
|
|
101
|
+
# @param bind [Class, nil] the class scope
|
|
102
|
+
#
|
|
103
|
+
# @return [Binding]
|
|
104
|
+
#
|
|
105
|
+
# @example
|
|
106
|
+
# build_context(MyClass) # => Binding
|
|
107
|
+
#
|
|
108
|
+
# @api private
|
|
109
|
+
#
|
|
110
|
+
def build_context(bind)
|
|
111
|
+
if bind.respond_to?(:class_eval)
|
|
112
|
+
ctx = bind.class_eval('binding', __FILE__, __LINE__)
|
|
113
|
+
transplant_instance_variables(ctx)
|
|
114
|
+
ctx
|
|
115
|
+
else
|
|
116
|
+
@fallback_binding
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Copies instance variables into an evaluation binding
|
|
121
|
+
#
|
|
122
|
+
# Sets each instance variable from the snapshot as a local, then
|
|
123
|
+
# assigns it to the corresponding instance variable name via +eval+.
|
|
124
|
+
# This makes hook-set state (e.g. +@flag+) available in class-scoped
|
|
125
|
+
# bindings.
|
|
126
|
+
#
|
|
127
|
+
# @param ctx [Binding] the target binding
|
|
128
|
+
#
|
|
129
|
+
# @return [void]
|
|
130
|
+
#
|
|
131
|
+
# @example
|
|
132
|
+
# transplant_instance_variables(ctx)
|
|
133
|
+
#
|
|
134
|
+
# @api private
|
|
135
|
+
#
|
|
136
|
+
def transplant_instance_variables(ctx)
|
|
137
|
+
@instance_variables.each do |ivar, value|
|
|
138
|
+
local = "__yard_example_runner__#{ivar.to_s.delete('@')}"
|
|
139
|
+
ctx.local_variable_set(local, value)
|
|
140
|
+
ctx.eval("#{ivar} = #{local}")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'example/constant_sandbox'
|
|
4
|
+
require_relative 'example/evaluator'
|
|
5
|
+
require_relative 'example/comparison'
|
|
6
|
+
|
|
7
|
+
module YardExampleRunner
|
|
8
|
+
# Represents a YARD +@example+ tag as a runnable +Minitest::Spec+
|
|
9
|
+
#
|
|
10
|
+
# Each instance is populated from a single +@example+ tag by
|
|
11
|
+
# {YARD::CLI::RunExamples#build_spec} and holds everything needed to
|
|
12
|
+
# generate and execute a test:
|
|
13
|
+
#
|
|
14
|
+
# - {#definition} — the YARD path of the documented object
|
|
15
|
+
# (e.g. +"MyClass#my_method"+), used as the spec description
|
|
16
|
+
#
|
|
17
|
+
# - {#filepath} — the source location of the documented object
|
|
18
|
+
# (e.g. +"lib/my_class.rb:10"+), prepended to failure backtraces
|
|
19
|
+
#
|
|
20
|
+
# - {#expectations} — the list of {Expectation} objects parsed from the
|
|
21
|
+
# example body, each pairing a Ruby expression to evaluate with an
|
|
22
|
+
# optional expected return value
|
|
23
|
+
#
|
|
24
|
+
# Calling {#generate} dynamically defines and registers an anonymous
|
|
25
|
+
# +Minitest::Spec+ subclass that wraps the expectations in a single +it+
|
|
26
|
+
# block. The registered spec is then picked up by +Minitest.autorun+ when
|
|
27
|
+
# the process exits.
|
|
28
|
+
#
|
|
29
|
+
# Each expectation is evaluated inside a binding scoped to the owning
|
|
30
|
+
# object's class (if one can be resolved), so instance methods and
|
|
31
|
+
# constants are available without qualification, mirroring how the code
|
|
32
|
+
# appears in the source documentation.
|
|
33
|
+
#
|
|
34
|
+
# Evaluation is delegated to an {Evaluator} (binding management and
|
|
35
|
+
# +eval+), constant isolation is handled by {ConstantSandbox}, and
|
|
36
|
+
# assertion / matcher logic lives in the {Comparison} module.
|
|
37
|
+
#
|
|
38
|
+
# @see YARD::CLI::RunExamples
|
|
39
|
+
#
|
|
40
|
+
# @see Expectation
|
|
41
|
+
#
|
|
42
|
+
# @see Evaluator
|
|
43
|
+
#
|
|
44
|
+
# @see ConstantSandbox
|
|
45
|
+
#
|
|
46
|
+
# @see Comparison
|
|
47
|
+
#
|
|
48
|
+
# @api public
|
|
49
|
+
#
|
|
50
|
+
class Example < ::Minitest::Spec
|
|
51
|
+
include Comparison
|
|
52
|
+
|
|
53
|
+
# The YARD namespace path of the documented object (e.g. +Foo#bar+)
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# example.definition #=> 'Foo#bar'
|
|
57
|
+
#
|
|
58
|
+
# @return [String] namespace path of example
|
|
59
|
+
#
|
|
60
|
+
# @api public
|
|
61
|
+
attr_accessor :definition
|
|
62
|
+
|
|
63
|
+
# The source location of the documented object (e.g. +app/app.rb:10+)
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# example.filepath #=> 'app/app.rb:10'
|
|
67
|
+
#
|
|
68
|
+
# @return [String] filepath to definition
|
|
69
|
+
#
|
|
70
|
+
# @api public
|
|
71
|
+
attr_accessor :filepath
|
|
72
|
+
|
|
73
|
+
# The list of expectations parsed from the example body
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# example.expectations #=> []
|
|
77
|
+
#
|
|
78
|
+
# @return [Array<YardExampleRunner::Expectation>] expectations to be verified
|
|
79
|
+
#
|
|
80
|
+
# @api public
|
|
81
|
+
attr_accessor :expectations
|
|
82
|
+
|
|
83
|
+
# Dynamically defines and registers a +Minitest::Spec+ for this example
|
|
84
|
+
#
|
|
85
|
+
# Creates an anonymous subclass of this class and evaluates a +describe+/+it+
|
|
86
|
+
# block inside it. The steps are:
|
|
87
|
+
#
|
|
88
|
+
# 1. Calls +load_helpers+ to require any +example_runner_helper+ files found in
|
|
89
|
+
# +.+, +support/+, +spec/+, or +test/+.
|
|
90
|
+
# 2. Skips silently if {YardExampleRunner.skips} contains a substring that
|
|
91
|
+
# matches {#definition}.
|
|
92
|
+
# 3. Opens a +describe+ block keyed on {#definition}, which becomes the spec
|
|
93
|
+
# group name reported by Minitest.
|
|
94
|
+
# 4. Registers any matching +before+/+after+ hooks via +register_hooks+. These
|
|
95
|
+
# are registered by the user with {YardExampleRunner.before} and
|
|
96
|
+
# {YardExampleRunner.after}.
|
|
97
|
+
# 5. Opens an +it+ block keyed on +name+ (the +@example+ tag title) that calls
|
|
98
|
+
# +run_expectations+ to evaluate every {Expectation} in {#expectations}.
|
|
99
|
+
#
|
|
100
|
+
# The anonymous class and its specs are registered with Minitest's internal list
|
|
101
|
+
# by the +describe+ call. They will be executed when +Minitest.autorun+'s
|
|
102
|
+
# +at_exit+ hook fires.
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# example.generate
|
|
106
|
+
#
|
|
107
|
+
# @return [void]
|
|
108
|
+
#
|
|
109
|
+
def generate
|
|
110
|
+
self.class.send(:load_helpers)
|
|
111
|
+
return if skipped?
|
|
112
|
+
|
|
113
|
+
this = self
|
|
114
|
+
Class.new(this.class).class_eval do
|
|
115
|
+
describe this.definition do
|
|
116
|
+
register_hooks(example_name_for(this), YardExampleRunner.hooks, this)
|
|
117
|
+
it(this.name) { run_expectations(this) }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
protected
|
|
123
|
+
|
|
124
|
+
# Returns +true+ if this example's {#definition} matches any skip pattern
|
|
125
|
+
#
|
|
126
|
+
# Iterates over {YardExampleRunner.skips} and returns +true+ as soon as a
|
|
127
|
+
# pattern is found that is a substring of {#definition}. Used by {#generate}
|
|
128
|
+
# to bail out before registering any +Minitest::Spec+ subclass.
|
|
129
|
+
#
|
|
130
|
+
# @example
|
|
131
|
+
# example.skipped? #=> false
|
|
132
|
+
#
|
|
133
|
+
# @return [Boolean] +true+ if the example should be skipped, +false+ otherwise
|
|
134
|
+
#
|
|
135
|
+
# @api private
|
|
136
|
+
#
|
|
137
|
+
def skipped?
|
|
138
|
+
YardExampleRunner.skips.any? { |skip| definition.include?(skip) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Evaluates every {Expectation} in the given example
|
|
142
|
+
#
|
|
143
|
+
# Delegates constant isolation to {ConstantSandbox}, which snapshots
|
|
144
|
+
# the current constants on +Object+ and the resolved scope, yields the
|
|
145
|
+
# scope for evaluation, then removes any constants that were introduced
|
|
146
|
+
# during evaluation.
|
|
147
|
+
#
|
|
148
|
+
# @param example [Example] the example whose {Example#expectations} are to be run
|
|
149
|
+
#
|
|
150
|
+
# @example
|
|
151
|
+
# run_expectations(example)
|
|
152
|
+
#
|
|
153
|
+
# @return [void]
|
|
154
|
+
#
|
|
155
|
+
# @api private
|
|
156
|
+
#
|
|
157
|
+
def run_expectations(example)
|
|
158
|
+
ConstantSandbox.new(example.definition).isolate do |scope|
|
|
159
|
+
example.expectations.each { |expectation| run_expectation(example, expectation, scope) }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Evaluates a single {Expectation} within the given scope
|
|
164
|
+
#
|
|
165
|
+
# If the expectation has no expected value ({Expectation#expected} is +nil+),
|
|
166
|
+
# the actual expression is evaluated for its side-effects only via
|
|
167
|
+
# {#evaluate_actual}. Otherwise the actual and expected expressions are both
|
|
168
|
+
# evaluated and compared via {Comparison#verify_actual}.
|
|
169
|
+
#
|
|
170
|
+
# @param example [Example] the owning example, used for backtrace decoration
|
|
171
|
+
#
|
|
172
|
+
# @param expectation [Expectation] the expectation to evaluate
|
|
173
|
+
#
|
|
174
|
+
# @param scope [Class, nil] the class scope to evaluate expressions in, or
|
|
175
|
+
# +nil+ to evaluate in the default binding
|
|
176
|
+
#
|
|
177
|
+
# @example
|
|
178
|
+
# run_expectation(example, expectation, MyClass)
|
|
179
|
+
#
|
|
180
|
+
# @return [void]
|
|
181
|
+
#
|
|
182
|
+
# @api private
|
|
183
|
+
#
|
|
184
|
+
def run_expectation(example, expectation, scope)
|
|
185
|
+
if expectation.expected.nil?
|
|
186
|
+
evaluate_actual(example, expectation.actual, scope)
|
|
187
|
+
else
|
|
188
|
+
verify_actual(example, expectation.expected, expectation.actual, scope)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Evaluates the actual expression for side-effects only
|
|
193
|
+
#
|
|
194
|
+
# Delegates to {Evaluator#evaluate} and re-raises any +StandardError+ after
|
|
195
|
+
# prepending the example's source location to the backtrace via
|
|
196
|
+
# {#add_filepath_to_backtrace}, so that failure output points to the
|
|
197
|
+
# documented source rather than this file.
|
|
198
|
+
#
|
|
199
|
+
# @param example [Example] the owning example, used to decorate error backtraces
|
|
200
|
+
#
|
|
201
|
+
# @param actual [String] the Ruby expression to evaluate
|
|
202
|
+
#
|
|
203
|
+
# @param bind [Class, nil] the class scope to evaluate the expression in, or
|
|
204
|
+
# +nil+ to use the default binding
|
|
205
|
+
#
|
|
206
|
+
# @example
|
|
207
|
+
# evaluate_actual(example, 'foo(1)', MyClass)
|
|
208
|
+
#
|
|
209
|
+
# @return [void]
|
|
210
|
+
#
|
|
211
|
+
# @raise [StandardError] re-raises any error raised during evaluation, with the
|
|
212
|
+
# example's filepath prepended to the backtrace
|
|
213
|
+
#
|
|
214
|
+
# @api private
|
|
215
|
+
#
|
|
216
|
+
def evaluate_actual(example, actual, bind)
|
|
217
|
+
evaluator.evaluate(actual, bind)
|
|
218
|
+
rescue StandardError => e
|
|
219
|
+
add_filepath_to_backtrace(e, example.filepath)
|
|
220
|
+
raise e
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Returns the lazily-initialized {Evaluator} for this spec instance
|
|
224
|
+
#
|
|
225
|
+
# The evaluator is created with a fallback binding (whose +self+ is this
|
|
226
|
+
# spec instance, so that methods included on {Example} — such as
|
|
227
|
+
# +RSpec::Matchers+ — are accessible in evaluated code) and a snapshot of
|
|
228
|
+
# the spec instance's instance variables (set by +before+ hooks).
|
|
229
|
+
#
|
|
230
|
+
# @example
|
|
231
|
+
# evaluator.evaluate('1 + 1', nil)
|
|
232
|
+
#
|
|
233
|
+
# @return [Evaluator]
|
|
234
|
+
#
|
|
235
|
+
# @api private
|
|
236
|
+
#
|
|
237
|
+
def evaluator
|
|
238
|
+
@evaluator ||= Evaluator.new(
|
|
239
|
+
fallback_binding: create_fallback_binding,
|
|
240
|
+
instance_variables: instance_variable_hash
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Prepends the example's filepath to an exception's backtrace
|
|
245
|
+
#
|
|
246
|
+
# @param exception [Exception] the exception to decorate
|
|
247
|
+
#
|
|
248
|
+
# @param filepath [String] the source location to prepend
|
|
249
|
+
#
|
|
250
|
+
# @example
|
|
251
|
+
# add_filepath_to_backtrace(exception, 'app/app.rb:10')
|
|
252
|
+
#
|
|
253
|
+
# @return [void]
|
|
254
|
+
#
|
|
255
|
+
# @api private
|
|
256
|
+
#
|
|
257
|
+
def add_filepath_to_backtrace(exception, filepath)
|
|
258
|
+
exception.set_backtrace([filepath] + exception.backtrace)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
private
|
|
262
|
+
|
|
263
|
+
# Returns a binding whose +self+ is this spec instance
|
|
264
|
+
#
|
|
265
|
+
# Because {Example} includes any modules the user adds (e.g.
|
|
266
|
+
# +RSpec::Matchers+), the returned binding automatically exposes those
|
|
267
|
+
# methods to code evaluated via the {Evaluator}'s fallback path.
|
|
268
|
+
#
|
|
269
|
+
# @example
|
|
270
|
+
# create_fallback_binding
|
|
271
|
+
#
|
|
272
|
+
# @return [Binding]
|
|
273
|
+
#
|
|
274
|
+
# @api private
|
|
275
|
+
#
|
|
276
|
+
def create_fallback_binding
|
|
277
|
+
binding
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Snapshots instance variables as a +Hash+
|
|
281
|
+
#
|
|
282
|
+
# Returns a hash mapping instance variable names to their current values
|
|
283
|
+
# on this spec instance. The {Evaluator} uses this snapshot to transplant
|
|
284
|
+
# hook-set state into class-scoped bindings.
|
|
285
|
+
#
|
|
286
|
+
# @example
|
|
287
|
+
# instance_variable_hash #=> { :@foo => 1 }
|
|
288
|
+
#
|
|
289
|
+
# @return [Hash{Symbol => Object}]
|
|
290
|
+
#
|
|
291
|
+
# @api private
|
|
292
|
+
#
|
|
293
|
+
def instance_variable_hash
|
|
294
|
+
instance_variables.to_h do |ivar|
|
|
295
|
+
[ivar, instance_variable_get(ivar)]
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
class << self
|
|
300
|
+
protected
|
|
301
|
+
|
|
302
|
+
# Requires any +example_runner_helper+ files found in known directories
|
|
303
|
+
#
|
|
304
|
+
# @example
|
|
305
|
+
# load_helpers
|
|
306
|
+
#
|
|
307
|
+
# @return [void]
|
|
308
|
+
#
|
|
309
|
+
# @api private
|
|
310
|
+
#
|
|
311
|
+
def load_helpers
|
|
312
|
+
%w[. support spec test].each do |dir|
|
|
313
|
+
require "#{dir}/example_runner_helper" if File.exist?("#{dir}/example_runner_helper.rb")
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Returns the full example name including an optional title suffix
|
|
318
|
+
#
|
|
319
|
+
# @param example [Example] the example to build a name for
|
|
320
|
+
#
|
|
321
|
+
# @example
|
|
322
|
+
# example_name_for(example) #=> 'Foo#bar'
|
|
323
|
+
#
|
|
324
|
+
# @return [String] the example name
|
|
325
|
+
#
|
|
326
|
+
# @api private
|
|
327
|
+
#
|
|
328
|
+
def example_name_for(example)
|
|
329
|
+
return example.definition if example.name.empty?
|
|
330
|
+
|
|
331
|
+
"#{example.definition}@#{example.name}"
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Registers matching before/after hooks on the current spec context
|
|
335
|
+
#
|
|
336
|
+
# @param example_name [String] the name of the example
|
|
337
|
+
#
|
|
338
|
+
# @param all_hooks [Hash{Symbol => Array<Hash>}] hooks grouped by type
|
|
339
|
+
#
|
|
340
|
+
# @param example [Example] the example being registered
|
|
341
|
+
#
|
|
342
|
+
# @example
|
|
343
|
+
# register_hooks('Foo#bar', YardExampleRunner.hooks, example)
|
|
344
|
+
#
|
|
345
|
+
# @return [void]
|
|
346
|
+
#
|
|
347
|
+
# @api private
|
|
348
|
+
#
|
|
349
|
+
def register_hooks(example_name, all_hooks, example)
|
|
350
|
+
all_hooks.each do |type, hooks|
|
|
351
|
+
global_hooks = hooks.reject { |hook| hook[:test] }
|
|
352
|
+
test_hooks = hooks.select { |hook| hook[:test] && example_name.include?(hook[:test]) }
|
|
353
|
+
__send__(type) do
|
|
354
|
+
(global_hooks + test_hooks).each { |hook| instance_exec(example, &hook[:block]) }
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardExampleRunner
|
|
4
|
+
# @!parse
|
|
5
|
+
# # Represents a single expected outcome parsed from a YARD +@example+ tag
|
|
6
|
+
# #
|
|
7
|
+
# # Each instance holds the Ruby expression to evaluate (+actual+) and the
|
|
8
|
+
# # string representation of the value it should return (+expected+). When
|
|
9
|
+
# # +expected+ is +nil+, the expression is evaluated for side-effects only and
|
|
10
|
+
# # no assertion is made against its return value.
|
|
11
|
+
# #
|
|
12
|
+
# # @!attribute actual [r]
|
|
13
|
+
# # @return [String] the Ruby expression to evaluate
|
|
14
|
+
# #
|
|
15
|
+
# # @!attribute expected [r]
|
|
16
|
+
# # @return [String, nil] the expected return value, or +nil+ if no
|
|
17
|
+
# # assertion should be made
|
|
18
|
+
# #
|
|
19
|
+
# # @api public
|
|
20
|
+
# #
|
|
21
|
+
# class Expectation < Data
|
|
22
|
+
Expectation = Data.define(:actual, :expected)
|
|
23
|
+
end
|