fixtury 0.4.1 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35c4db20425a837221a7c979e51371b77e00790cded54b182b31e62f7f57d8c2
4
- data.tar.gz: cbdb024ba301b393b97604879594c1d9d53bfe1ffd4f24959824e8f41e1fe4e2
3
+ metadata.gz: 02d3acd3d4f3904ae6e00f53e81a5c29b3aa65b53a0bba96029625e99deb618e
4
+ data.tar.gz: 12a1fa78d029e6781ce240a3bed206b63b0fd02589773a84deeb80f18556585d
5
5
  SHA512:
6
- metadata.gz: 4748b476b6683ac9275a0c62ebca36f346f5d2bea208d47f55dfd836b521283c383aee7847961ffc90726292fd187389fc65b650dd508f9c67288fa32ae88c6c
7
- data.tar.gz: bc765697630a9780752ab2ab9512dbe6bd92cee4f612dfa1ba11437d1f59f3e0c58a7d5a107576704d0018507ce989bf2e8726c165f244bf8757676c301a4f4d
6
+ metadata.gz: 69b2c38ebddf785a673adb7084ae1f18acc9dce299381ca747af1db6d7a1a2e38772e09d8f4848b7f923d7ade9acac3d3780540c5b42c83b23e547897c3379f3
7
+ data.tar.gz: 9ef4c658ab06864de8192111596af75c44498ae26c9af3178c63c11e915bda8a6634fa5d1540eb85491085a90e4d58c00772599f4a85df0e286a825befb95b0d
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.5.1
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.1)
4
+ fixtury (1.0.0.beta1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- activesupport (6.0.3.1)
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
- i18n (>= 0.7, < 2)
12
- minitest (~> 5.1)
13
- tzinfo (~> 1.1)
14
- zeitwerk (~> 2.2, >= 2.2.2)
15
- ansi (1.5.0)
16
- autotest (5.0.0)
17
- minitest-autotest (~> 1.0)
18
- builder (3.2.3)
19
- byebug (11.0.1)
20
- concurrent-ruby (1.1.6)
21
- globalid (0.4.2)
22
- activesupport (>= 4.2.0)
23
- i18n (1.8.2)
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
- autotest
53
- bundler (~> 2.0)
56
+ activerecord
57
+ bundler
54
58
  byebug
55
59
  fixtury!
56
60
  globalid
57
- minitest (~> 5.0)
58
- minitest-reporters
61
+ m
62
+ minitest
59
63
  mocha
60
- rake (~> 13.0)
61
- sqlite
64
+ rake
65
+ sqlite3
62
66
 
63
67
  BUNDLED WITH
64
- 2.1.4
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
- include ::Fixtury::TestHooks
11
+ prepend ::Fixtury::TestHooks
12
12
 
13
- fixtury "users.fresh"
14
- let(:user) { fixtury("users.fresh") }
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.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.
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 "autotest"
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 "minitest", "~> 5.0"
35
- spec.add_development_dependency "minitest-reporters"
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", "~> 13.0"
38
- spec.add_development_dependency "sqlite"
37
+ spec.add_development_dependency "rake"
38
+ spec.add_development_dependency "sqlite3"
39
39
  end
@@ -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
- def info
33
- {
34
- name: name,
35
- loc: location_from_callable(callable),
36
- enhancements: enhancements.map { |e| location_from_callable(e) },
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
- def call(store: nil, execution_context: nil)
41
- executor = ::Fixtury::DefinitionExecutor.new(store: store, definition: self, execution_context: execution_context)
42
- executor.__call
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
- def location_from_callable(callable)
46
- return nil unless callable.respond_to?(:source_location)
45
+ # Delegates the `call` method to the `callable` object.
46
+ def_delegator :callable, :call
47
47
 
48
- callable.source_location.join(":")
49
- end
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, :execution_type, :definition, :store, :execution_context
7
+ attr_reader :value, :definition, :store
7
8
 
8
- def initialize(store: nil, execution_context: nil, definition:)
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 __call
17
- maybe_set_store_context do
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
- def run_callable(callable:, type:)
51
- @execution_type = type
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
- instance_exec(self, &callable)
29
+ deps = build_dependency_store
30
+ ::Fixtury.hooks.call(:execution, self) do
31
+ instance_exec(deps, &callable)
32
+ end
55
33
  else
56
- instance_eval(&callable)
57
- end
58
- end
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 provide_schema_hooks
69
- return yield unless definition.schema
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