fixtury 0.4.1 → 1.0.0.beta1
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 +4 -4
- data/.ruby-version +1 -1
- data/Gemfile.lock +48 -44
- data/README.md +4 -4
- data/fixtury.gemspec +6 -6
- data/lib/fixtury/definition.rb +39 -36
- data/lib/fixtury/definition_executor.rb +23 -52
- data/lib/fixtury/dependency.rb +52 -0
- data/lib/fixtury/dependency_store.rb +45 -0
- data/lib/fixtury/errors.rb +81 -0
- data/lib/fixtury/hooks.rb +90 -0
- data/lib/fixtury/locator.rb +38 -27
- data/lib/fixtury/locator_backend/common.rb +17 -19
- data/lib/fixtury/locator_backend/globalid.rb +8 -8
- data/lib/fixtury/locator_backend/memory.rb +7 -7
- data/lib/fixtury/mutation_observer.rb +140 -0
- data/lib/fixtury/path_resolver.rb +45 -0
- data/lib/fixtury/railtie.rb +4 -0
- data/lib/fixtury/reference.rb +12 -10
- data/lib/fixtury/schema.rb +21 -225
- data/lib/fixtury/schema_node.rb +113 -0
- data/lib/fixtury/store.rb +113 -68
- data/lib/fixtury/test_hooks.rb +60 -89
- data/lib/fixtury/version.rb +1 -1
- data/lib/fixtury.rb +44 -5
- metadata +33 -33
- data/lib/fixtury/errors/already_defined_error.rb +0 -13
- data/lib/fixtury/errors/circular_dependency_error.rb +0 -13
- data/lib/fixtury/errors/fixture_not_defined_error.rb +0 -13
- data/lib/fixtury/errors/option_collision_error.rb +0 -13
- data/lib/fixtury/errors/schema_frozen_error.rb +0 -13
- data/lib/fixtury/errors/unrecognizable_locator_error.rb +0 -11
- data/lib/fixtury/path.rb +0 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 02d3acd3d4f3904ae6e00f53e81a5c29b3aa65b53a0bba96029625e99deb618e
|
4
|
+
data.tar.gz: 12a1fa78d029e6781ce240a3bed206b63b0fd02589773a84deeb80f18556585d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 69b2c38ebddf785a673adb7084ae1f18acc9dce299381ca747af1db6d7a1a2e38772e09d8f4848b7f923d7ade9acac3d3780540c5b42c83b23e547897c3379f3
|
7
|
+
data.tar.gz: 9ef4c658ab06864de8192111596af75c44498ae26c9af3178c63c11e915bda8a6634fa5d1540eb85491085a90e4d58c00772599f4a85df0e286a825befb95b0d
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
3.2.2
|
data/Gemfile.lock
CHANGED
@@ -1,64 +1,68 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
fixtury (0.
|
4
|
+
fixtury (1.0.0.beta1)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
|
9
|
+
activemodel (7.1.3.2)
|
10
|
+
activesupport (= 7.1.3.2)
|
11
|
+
activerecord (7.1.3.2)
|
12
|
+
activemodel (= 7.1.3.2)
|
13
|
+
activesupport (= 7.1.3.2)
|
14
|
+
timeout (>= 0.4.0)
|
15
|
+
activesupport (7.1.3.2)
|
16
|
+
base64
|
17
|
+
bigdecimal
|
10
18
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
byebug (11.
|
20
|
-
concurrent-ruby (1.
|
21
|
-
|
22
|
-
|
23
|
-
|
19
|
+
connection_pool (>= 2.2.5)
|
20
|
+
drb
|
21
|
+
i18n (>= 1.6, < 2)
|
22
|
+
minitest (>= 5.1)
|
23
|
+
mutex_m
|
24
|
+
tzinfo (~> 2.0)
|
25
|
+
base64 (0.2.0)
|
26
|
+
bigdecimal (3.1.6)
|
27
|
+
byebug (11.1.3)
|
28
|
+
concurrent-ruby (1.2.3)
|
29
|
+
connection_pool (2.4.1)
|
30
|
+
drb (2.2.1)
|
31
|
+
globalid (1.2.1)
|
32
|
+
activesupport (>= 6.1)
|
33
|
+
i18n (1.14.4)
|
34
|
+
concurrent-ruby (~> 1.0)
|
35
|
+
m (1.6.2)
|
36
|
+
method_source (>= 0.6.7)
|
37
|
+
rake (>= 0.9.2.2)
|
38
|
+
method_source (1.0.0)
|
39
|
+
mini_portile2 (2.8.5)
|
40
|
+
minitest (5.22.2)
|
41
|
+
mocha (2.1.0)
|
42
|
+
ruby2_keywords (>= 0.0.5)
|
43
|
+
mutex_m (0.2.0)
|
44
|
+
rake (13.1.0)
|
45
|
+
ruby2_keywords (0.0.5)
|
46
|
+
sqlite3 (1.7.2)
|
47
|
+
mini_portile2 (~> 2.8.0)
|
48
|
+
timeout (0.4.1)
|
49
|
+
tzinfo (2.0.6)
|
24
50
|
concurrent-ruby (~> 1.0)
|
25
|
-
metaclass (0.0.4)
|
26
|
-
minitest (5.13.0)
|
27
|
-
minitest-autotest (1.1.1)
|
28
|
-
minitest-server (~> 1.0)
|
29
|
-
path_expander (~> 1.0)
|
30
|
-
minitest-reporters (1.4.2)
|
31
|
-
ansi
|
32
|
-
builder
|
33
|
-
minitest (>= 5.0)
|
34
|
-
ruby-progressbar
|
35
|
-
minitest-server (1.0.5)
|
36
|
-
minitest (~> 5.0)
|
37
|
-
mocha (1.8.0)
|
38
|
-
metaclass (~> 0.0.1)
|
39
|
-
path_expander (1.1.0)
|
40
|
-
rake (13.0.1)
|
41
|
-
ruby-progressbar (1.10.1)
|
42
|
-
sqlite (1.0.2)
|
43
|
-
thread_safe (0.3.6)
|
44
|
-
tzinfo (1.2.7)
|
45
|
-
thread_safe (~> 0.1)
|
46
|
-
zeitwerk (2.3.0)
|
47
51
|
|
48
52
|
PLATFORMS
|
49
53
|
ruby
|
50
54
|
|
51
55
|
DEPENDENCIES
|
52
|
-
|
53
|
-
bundler
|
56
|
+
activerecord
|
57
|
+
bundler
|
54
58
|
byebug
|
55
59
|
fixtury!
|
56
60
|
globalid
|
57
|
-
|
58
|
-
minitest
|
61
|
+
m
|
62
|
+
minitest
|
59
63
|
mocha
|
60
|
-
rake
|
61
|
-
|
64
|
+
rake
|
65
|
+
sqlite3
|
62
66
|
|
63
67
|
BUNDLED WITH
|
64
|
-
2.
|
68
|
+
2.5.6
|
data/README.md
CHANGED
@@ -8,10 +8,10 @@ For example, if a developer is running a test locally in their development envir
|
|
8
8
|
|
9
9
|
```ruby
|
10
10
|
class MyTest < ::ActiveSupport::TestCase
|
11
|
-
|
11
|
+
prepend ::Fixtury::TestHooks
|
12
12
|
|
13
|
-
fixtury "users
|
14
|
-
let(:user) { fixtury("users
|
13
|
+
fixtury "users/fresh"
|
14
|
+
let(:user) { fixtury("users/fresh") }
|
15
15
|
|
16
16
|
def test_whatever
|
17
17
|
assert_eq "Doug", user.first_name
|
@@ -20,6 +20,6 @@ class MyTest < ::ActiveSupport::TestCase
|
|
20
20
|
end
|
21
21
|
```
|
22
22
|
|
23
|
-
Loading this file would ensure `users
|
23
|
+
Loading this file would ensure `users/fresh` is loaded into the fixture set before the suite is run. In the context of ActiveSupport::TestCase, the Fixtury::Hooks file will ensure the database records are present prior to your suite running. Setting `use_transactional_fixtures` ensures all records are rolled back prior to running another test.
|
24
24
|
|
25
25
|
In a CI environment, we'd likely want to preload all fixtures. This can be done by requiring all the test files, then telling the fixtury store to load all definitions.
|
data/fixtury.gemspec
CHANGED
@@ -27,13 +27,13 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
|
30
|
-
spec.add_development_dependency "
|
31
|
-
spec.add_development_dependency "bundler", "~> 2.0"
|
30
|
+
spec.add_development_dependency "bundler"
|
32
31
|
spec.add_development_dependency "byebug"
|
33
32
|
spec.add_development_dependency "globalid"
|
34
|
-
spec.add_development_dependency "
|
35
|
-
spec.add_development_dependency "
|
33
|
+
spec.add_development_dependency "activerecord"
|
34
|
+
spec.add_development_dependency "m"
|
35
|
+
spec.add_development_dependency "minitest"
|
36
36
|
spec.add_development_dependency "mocha"
|
37
|
-
spec.add_development_dependency "rake"
|
38
|
-
spec.add_development_dependency "
|
37
|
+
spec.add_development_dependency "rake"
|
38
|
+
spec.add_development_dependency "sqlite3"
|
39
39
|
end
|
data/lib/fixtury/definition.rb
CHANGED
@@ -1,52 +1,55 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "fixtury/definition_executor"
|
4
|
-
|
5
3
|
module Fixtury
|
4
|
+
# A class that contains the definition of a fixture. It also maintains a list of it's
|
5
|
+
# dependencies to allow for analysis of the fixture graph.
|
6
6
|
class Definition
|
7
|
+
include ::Fixtury::SchemaNode
|
8
|
+
extend ::Forwardable
|
9
|
+
|
10
|
+
# Initializes a new Definition object.
|
11
|
+
#
|
12
|
+
# @param deps [Array] An array of dependencies.
|
13
|
+
# @param opts [Hash] Additional options for the Definition.
|
14
|
+
# @param block [Proc] A block of code to be executed.
|
15
|
+
def initialize(deps: [], **opts, &block)
|
16
|
+
super(**opts)
|
17
|
+
|
18
|
+
@dependencies = Array(deps).each_with_object({}) do |d, deps|
|
19
|
+
parsed_deps = Dependency.from(parent, d)
|
20
|
+
parsed_deps.each do |dep|
|
21
|
+
existing = deps[dep.accessor]
|
22
|
+
raise ArgumentError, "Accessor #{dep.accessor} is already declared by #{existing.search}" if existing
|
23
|
+
|
24
|
+
deps[dep.accessor] = dep
|
25
|
+
end
|
26
|
+
end
|
7
27
|
|
8
|
-
attr_reader :name
|
9
|
-
attr_reader :schema
|
10
|
-
alias parent schema
|
11
|
-
attr_reader :options
|
12
|
-
|
13
|
-
attr_reader :callable
|
14
|
-
attr_reader :enhancements
|
15
|
-
|
16
|
-
def initialize(schema: nil, name:, options: {}, &block)
|
17
|
-
@name = name
|
18
|
-
@schema = schema
|
19
28
|
@callable = block
|
20
|
-
@options = options
|
21
|
-
@enhancements = []
|
22
|
-
end
|
23
|
-
|
24
|
-
def enhance(&block)
|
25
|
-
@enhancements << block
|
26
|
-
end
|
27
|
-
|
28
|
-
def enhanced?
|
29
|
-
@enhancements.any?
|
30
29
|
end
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
}
|
31
|
+
# Returns the type of the schema node.
|
32
|
+
#
|
33
|
+
# @return [String] The schema node type.
|
34
|
+
def schema_node_type
|
35
|
+
"dfn"
|
38
36
|
end
|
39
37
|
|
40
|
-
|
41
|
-
|
42
|
-
|
38
|
+
# Indicates whether the Definition acts like a Fixtury definition.
|
39
|
+
#
|
40
|
+
# @return [Boolean] `true` if it acts like a Fixtury definition, `false` otherwise.
|
41
|
+
def acts_like_fixtury_definition?
|
42
|
+
true
|
43
43
|
end
|
44
44
|
|
45
|
-
|
46
|
-
|
45
|
+
# Delegates the `call` method to the `callable` object.
|
46
|
+
def_delegator :callable, :call
|
47
47
|
|
48
|
-
|
49
|
-
|
48
|
+
# Returns the parent schema of the Definition.
|
49
|
+
#
|
50
|
+
# @return [Object] The parent schema.
|
51
|
+
alias schema parent
|
50
52
|
|
53
|
+
attr_reader :callable, :dependencies
|
51
54
|
end
|
52
55
|
end
|
@@ -1,77 +1,48 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Fixtury
|
4
|
+
# A container that manages the execution of a definition in the context of a store.
|
4
5
|
class DefinitionExecutor
|
5
6
|
|
6
|
-
attr_reader :value, :
|
7
|
+
attr_reader :value, :definition, :store
|
7
8
|
|
8
|
-
def initialize(store: nil,
|
9
|
+
def initialize(store: nil, definition:)
|
9
10
|
@store = store
|
10
11
|
@definition = definition
|
11
|
-
@execution_context = execution_context
|
12
|
-
@execution_type = nil
|
13
12
|
@value = nil
|
14
13
|
end
|
15
14
|
|
16
|
-
def
|
17
|
-
|
18
|
-
provide_schema_hooks do
|
19
|
-
run_callable(callable: definition.callable, type: :definition)
|
20
|
-
definition.enhancements.each do |e|
|
21
|
-
run_callable(callable: e, type: :enhancement)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
15
|
+
def call
|
16
|
+
run_definition
|
26
17
|
value
|
27
18
|
end
|
28
19
|
|
29
|
-
def get(name)
|
30
|
-
raise ArgumentError, "A store is required for #{definition.name}" unless store
|
31
|
-
|
32
|
-
store.get(name, execution_context: execution_context)
|
33
|
-
end
|
34
|
-
alias [] get
|
35
|
-
|
36
|
-
def method_missing(method_name, *args, &block)
|
37
|
-
return super unless execution_context
|
38
|
-
|
39
|
-
execution_context.send(method_name, *args, &block)
|
40
|
-
end
|
41
|
-
|
42
|
-
def respond_to_missing?(method_name)
|
43
|
-
return super unless execution_context
|
44
|
-
|
45
|
-
execution_context.respond_to?(method_name, true)
|
46
|
-
end
|
47
|
-
|
48
20
|
private
|
49
21
|
|
50
|
-
|
51
|
-
|
22
|
+
# If the callable has a positive arity we generate a DependencyStore
|
23
|
+
# and yield it to the callable. Otherwise we just instance_eval the callable.
|
24
|
+
# We wrap the actual execution of the definition with a hook for observation.
|
25
|
+
def run_definition
|
26
|
+
callable = definition.callable
|
52
27
|
|
53
28
|
@value = if callable.arity.positive?
|
54
|
-
|
29
|
+
deps = build_dependency_store
|
30
|
+
::Fixtury.hooks.call(:execution, self) do
|
31
|
+
instance_exec(deps, &callable)
|
32
|
+
end
|
55
33
|
else
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
def maybe_set_store_context
|
61
|
-
return yield unless store
|
62
|
-
|
63
|
-
store.with_relative_schema(definition.schema) do
|
64
|
-
yield
|
34
|
+
::Fixtury.hooks.call(:execution, self) do
|
35
|
+
instance_eval(&callable)
|
36
|
+
end
|
65
37
|
end
|
38
|
+
rescue Errors::Base
|
39
|
+
raise
|
40
|
+
rescue => e
|
41
|
+
raise Errors::DefinitionExecutionError.new(definition.pathname, e)
|
66
42
|
end
|
67
43
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
@value = definition.schema.around_fixture_hook(self) do
|
72
|
-
yield
|
73
|
-
value
|
74
|
-
end
|
44
|
+
def build_dependency_store
|
45
|
+
DependencyStore.new(definition: definition, store: store)
|
75
46
|
end
|
76
47
|
|
77
48
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Fixtury
|
2
|
+
class Dependency
|
3
|
+
|
4
|
+
# Resolve a Dependency from a multitude of input types
|
5
|
+
# @param parent [Fixtury::Definition] the parent definition
|
6
|
+
# @param thing [Fixtury::Dependency, Hash, Array, String, Symbol] the thing to resolve
|
7
|
+
# @option thing [Fixtury::Dependency] a dependency will be cloned.
|
8
|
+
# @option thing [Hash] a hash with exactly one key will be resolved as { accessor => search }.
|
9
|
+
# @option thing [Array] an array with two elements will be resolved as [ accessor, search ].
|
10
|
+
# @option thing [String, Symbol] a string or symbol will be resolved as both the accessor and the search.
|
11
|
+
# @return [Array<Fixtury::Dependency>] the resolved dependency
|
12
|
+
def self.from(parent, thing)
|
13
|
+
out = case thing
|
14
|
+
when self
|
15
|
+
Dependency.new(parent: parent, search: thing.search, accessor: thing.accessor)
|
16
|
+
when Hash
|
17
|
+
thing.each_with_object([]) do |(k, v), arr|
|
18
|
+
arr << Dependency.new(parent: parent, search: v, accessor: k)
|
19
|
+
end
|
20
|
+
when Array
|
21
|
+
raise ArgumentError, "Array must have an even number of elements" unless thing.size % 2 == 0
|
22
|
+
|
23
|
+
thing.each_slice(2).map do |pair|
|
24
|
+
Dependency.new(parent: parent, search: pair[1], accessor: pair[0])
|
25
|
+
end
|
26
|
+
when String, Symbol
|
27
|
+
Dependency.new(parent: parent, search: thing, accessor: thing)
|
28
|
+
else
|
29
|
+
raise ArgumentError, "Unknown dependency type: #{thing.inspect}"
|
30
|
+
end
|
31
|
+
|
32
|
+
Array(out)
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :parent, :search, :accessor
|
36
|
+
|
37
|
+
def initialize(parent:, search:, accessor:)
|
38
|
+
@parent = parent
|
39
|
+
@search = search.to_s
|
40
|
+
@accessor = accessor.to_s.split("/").last
|
41
|
+
end
|
42
|
+
|
43
|
+
def definition
|
44
|
+
@definition ||= parent&.get!(search)
|
45
|
+
end
|
46
|
+
|
47
|
+
def inspect
|
48
|
+
"#{self.class}(accessor: #{accessor.inspect}, search: #{search.inspect}, parent: #{parent.name.inspect})"
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Fixtury
|
2
|
+
# An object which allows access to a specific subset of fixtures
|
3
|
+
# in the context of a definition's dependencies.
|
4
|
+
class DependencyStore
|
5
|
+
|
6
|
+
attr_reader :definition, :store
|
7
|
+
|
8
|
+
def initialize(definition:, store:)
|
9
|
+
@definition = definition
|
10
|
+
@store = store
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
"#{self.class}(definition: #{definition.pathname.inspect}, dependencies: #{definition.dependencies.keys.inspect})"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the value of the dependency with the given key
|
18
|
+
#
|
19
|
+
# @param key [String, Symbol] the accessor of the dependency
|
20
|
+
# @return [Object] the value of the dependency
|
21
|
+
# @raise [Fixtury::Errors::UnknownDependencyError] if the definition does not contain the provided dependency
|
22
|
+
def get(key)
|
23
|
+
dep = definition.dependencies.fetch(key.to_s) do
|
24
|
+
raise Errors::UnknownDependencyError.new(definition, key)
|
25
|
+
end
|
26
|
+
store.get(dep.definition.pathname)
|
27
|
+
end
|
28
|
+
alias [] get
|
29
|
+
|
30
|
+
# If an accessor is used and we recognize the accessor as a dependency
|
31
|
+
# of our definition, we return the value of the dependency.
|
32
|
+
def method_missing(method, *args, &block)
|
33
|
+
if definition.dependencies.key?(method.to_s)
|
34
|
+
get(method)
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def respond_to_missing?(method, include_private = false)
|
41
|
+
definition.dependencies.key?(method.to_s) || super
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fixtury
|
4
|
+
module Errors
|
5
|
+
|
6
|
+
class Base < StandardError
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
class AlreadyDefinedError < Base
|
11
|
+
|
12
|
+
def initialize(name)
|
13
|
+
super("An element identified by #{name.inspect} already exists.")
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
class CircularDependencyError < Base
|
19
|
+
|
20
|
+
def initialize(name)
|
21
|
+
super("One of the dependencies of #{name.inspect} is dependent on #{name.inspect}.")
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
class DefinitionExecutionError < Base
|
27
|
+
|
28
|
+
def initialize(pathname, error)
|
29
|
+
super("Error while building #{pathname.inspect}: #{error}")
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
class SchemaNodeNotDefinedError < Base
|
35
|
+
|
36
|
+
def initialize(pathname, search)
|
37
|
+
super("A schema node identified by #{search.inspect} could not be found from #{pathname.inspect}.")
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
class SchemaNodeNameInvalidError < Base
|
43
|
+
def initialize(parent_name, child_name)
|
44
|
+
super("The schema node name #{child_name.inspect} must start with #{parent_name.inspect} to be added to it.")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class OptionCollisionError < Base
|
49
|
+
|
50
|
+
def initialize(schema_name, option_key, old_value, new_value)
|
51
|
+
super("The #{schema_name.inspect} schema #{option_key.inspect} option value of #{old_value.inspect} conflicts with the new value #{new_value.inspect}.")
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
class UnrecognizableLocatorError < Base
|
57
|
+
|
58
|
+
def initialize(action, thing)
|
59
|
+
super("Locator did not recognize #{thing} during #{action}")
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
class IsolatedMutationError < Base
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
class UnknownTestDependencyError < Base
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
class UnknownDependencyError < Base
|
73
|
+
|
74
|
+
def initialize(defn, key)
|
75
|
+
super("#{defn.pathname} does not contain the provided dependency: #{key}")
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fixtury
|
4
|
+
# Provides a mechanism for observing Fixtury lifecycle events.
|
5
|
+
class Hooks
|
6
|
+
|
7
|
+
attr_reader :hooks
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@hooks = Hash.new { |h, k| h[k] = { before: [], after: [], around: [], on: [] } }
|
11
|
+
end
|
12
|
+
|
13
|
+
# Register a hook to be called around the execution of a trigger.
|
14
|
+
# The around hook should ensure the return value is preserved.
|
15
|
+
# This also means that the hook itself could modify the return value.
|
16
|
+
#
|
17
|
+
# @param trigger_type [Symbol] the type of trigger to hook into
|
18
|
+
# @param hook [Proc] the hook to be called
|
19
|
+
def around(trigger_type, &hook)
|
20
|
+
register_hook(trigger_type, :around, hook)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Register a hook to be called before the execution of a trigger.
|
24
|
+
# (see #register_hook)
|
25
|
+
def before(trigger_type, &hook)
|
26
|
+
register_hook(trigger_type, :before, hook)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Register a hook to be called after the execution of a trigger.
|
30
|
+
# The return value will be provided as the first argument to the hook.
|
31
|
+
# (see #register_hook)
|
32
|
+
def after(trigger_type, &hook)
|
33
|
+
register_hook(trigger_type, :after, hook)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Similar to after, but the return value is not injected.
|
37
|
+
# (see #register_hook)
|
38
|
+
def on(trigger_type, &hook)
|
39
|
+
register_hook(trigger_type, :on, hook)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Trigger the hooks registered for a specific trigger type.
|
43
|
+
# :before hooks will be triggered first, followed by :around hooks,
|
44
|
+
# :on hooks, and finally :after hooks.
|
45
|
+
#
|
46
|
+
# @param trigger_type [Symbol] the type of trigger to initiate
|
47
|
+
# @param args [Array] arguments to be passed to the hooks
|
48
|
+
# @param block [Proc] a block of code to be executed
|
49
|
+
# @return [Object] the return value of the block
|
50
|
+
def call(trigger_type, *args, &block)
|
51
|
+
hook_lists = hooks[trigger_type.to_sym]
|
52
|
+
|
53
|
+
call_inline_hooks(hook_lists[:before], *args)
|
54
|
+
return_value = call_around_hooks(hook_lists[:around], 0, block, *args)
|
55
|
+
call_inline_hooks(hook_lists[:on], *args)
|
56
|
+
call_inline_hooks(hook_lists[:after], return_value, *args)
|
57
|
+
|
58
|
+
return_value
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Register a hook to be called for a specific trigger type and hook type.
|
64
|
+
#
|
65
|
+
# @param trigger_type [Symbol] the type of trigger to hook into
|
66
|
+
# @param hook_type [Symbol] the point in the trigger to hook into
|
67
|
+
# @param hook [Proc] the hook to be called
|
68
|
+
# @return [Fixtury::Hooks] the current instance
|
69
|
+
def register_hook(trigger_type, hook_type, hook)
|
70
|
+
hooks[trigger_type.to_sym][hook_type.to_sym] << hook
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def call_around_hooks(hook_list, idx, block, *args)
|
75
|
+
if idx >= hook_list.length
|
76
|
+
block.call
|
77
|
+
else
|
78
|
+
hook_list[idx].call(*args) do
|
79
|
+
call_around_hooks(hook_list, idx + 1, block, *args)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def call_inline_hooks(hook_list, *args)
|
85
|
+
hook_list.each do |hook|
|
86
|
+
hook.call(*args)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|