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 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