fixtury 0.4.1 → 1.0.0.beta2

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: cd14efe323fc1de1a2ec63805508e7fb816db30f028998e951c78e95cbe14dfe
4
+ data.tar.gz: e0a1055bf1ebfff930569a5b678b1b433c21b48e2a4305197bdeb7938e22332a
5
5
  SHA512:
6
- metadata.gz: 4748b476b6683ac9275a0c62ebca36f346f5d2bea208d47f55dfd836b521283c383aee7847961ffc90726292fd187389fc65b650dd508f9c67288fa32ae88c6c
7
- data.tar.gz: bc765697630a9780752ab2ab9512dbe6bd92cee4f612dfa1ba11437d1f59f3e0c58a7d5a107576704d0018507ce989bf2e8726c165f244bf8757676c301a4f4d
6
+ metadata.gz: d819ff4d3096eee7a2ac3c4617f89463b28b7826f3046efbf391408b595b8758f52ac556c8e337eae396109839a148a033750b2ae5ac10ebddce27a041344be3
7
+ data.tar.gz: febce1127eceb8f3ef6e148b7212591e657dab0d62f4b57d0ab6e8b6f8ba8d9c210180f6713fe78d5de6ce35356a5c448cfd9903d60a406efe5bc835c9cbbe11
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.beta2)
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
@@ -0,0 +1,117 @@
1
+ require "digest"
2
+
3
+ module Fixtury
4
+ # Provides an interface for managing settings and dependencies related to fixture
5
+ # generation.
6
+ class Configuration
7
+
8
+ attr_reader :filepath, :fixture_files, :dependency_files
9
+
10
+ def initialize
11
+ @filepath = nil
12
+ @fixture_files = Set.new
13
+ @dependency_files = Set.new
14
+ end
15
+
16
+ def log_level
17
+ return @log_level if @log_level
18
+
19
+ @log_level = ENV["FIXTURY_LOG_LEVEL"]
20
+ @log_level ||= DEFAULT_LOG_LEVEL
21
+ @log_level = @log_level.to_s.to_sym
22
+ @log_level
23
+ end
24
+
25
+ # Delete the storage file if it exists.
26
+ def reset
27
+ File.delete(filepath) if filepath && File.file?(filepath)
28
+ end
29
+
30
+ # Set the location of the storage file. The storage file will maintain
31
+ # checksums of all tracked files and serialized references to fixtures.
32
+ #
33
+ # @param path [String] The path to the storage file.
34
+ def filepath=(path)
35
+ @filepath = path.to_s
36
+ end
37
+
38
+ # Add a file or glob pattern to the list of fixture files.
39
+ #
40
+ # @param path_or_globs [String, Array<String>] The file or glob pattern(s) to add.
41
+ def add_fixture_path(*path_or_globs)
42
+ @fixture_files = fixture_files | Dir[*path_or_globs]
43
+ end
44
+ alias add_fixture_paths add_fixture_path
45
+
46
+ # Add a file or glob pattern to the list of dependency files.
47
+ #
48
+ # @param path_or_globs [String, Array<String>] The file or glob pattern(s) to add.
49
+ def add_dependency_path(*path_or_globs)
50
+ @dependency_files = dependency_files | Dir[*path_or_globs]
51
+ end
52
+ alias add_dependency_paths add_dependency_path
53
+
54
+ # The references stored in the dependency file. When stores are initialized
55
+ # these will be used to bootstrap the references.
56
+ #
57
+ # @return [Hash] The references stored in the dependency file.
58
+ def stored_references
59
+ return {} if stored_data.nil?
60
+
61
+ stored_data[:references] || {}
62
+ end
63
+
64
+ # Dump the current state of the dependency manager to the storage file.
65
+ def dump_file
66
+ return unless filepath
67
+
68
+ FileUtils.mkdir_p(File.dirname(filepath))
69
+ File.binwrite(filepath, file_data.to_yaml)
70
+ end
71
+
72
+ private
73
+
74
+ def file_data
75
+ checksums = {}
76
+ calculate_checksums do |filepath, checksum|
77
+ checksums[filepath] = checksum
78
+ end
79
+
80
+ {
81
+ dependencies: checksums,
82
+ references: ::Fixtury.store.references,
83
+ }
84
+ end
85
+
86
+ def stored_data
87
+ return nil unless filepath
88
+ return nil unless File.file?(filepath)
89
+
90
+ YAML.unsafe_load_file(filepath)
91
+ end
92
+
93
+ def files_changed?
94
+ return true if stored_data.nil?
95
+
96
+ stored_checksums = (stored_data[:dependencies] || {})
97
+ seen_filepaths = []
98
+ calculate_checksums do |filepath, checksum|
99
+ # Early return if the checksums don't match
100
+ return true unless stored_checksums[filepath] == checksum
101
+
102
+ seen_filepaths << filepath
103
+ end
104
+
105
+ # If we have a new file or a file has been removed, we need to report a change.
106
+ seen_filepaths.sort != stored_checksums.keys.sort
107
+ end
108
+
109
+ def calculate_checksums(&block)
110
+ (fixture_files.to_a | dependency_files.to_a).sort.each do |filepath|
111
+ yield filepath, Digest::MD5.file(filepath).hexdigest
112
+ end
113
+ end
114
+
115
+
116
+ end
117
+ end
@@ -1,52 +1,48 @@
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
29
  end
27
30
 
28
- def enhanced?
29
- @enhancements.any?
31
+ # Indicates whether the Definition acts like a Fixtury definition.
32
+ #
33
+ # @return [Boolean] `true` if it acts like a Fixtury definition, `false` otherwise.
34
+ def acts_like_fixtury_definition?
35
+ true
30
36
  end
31
37
 
32
- def info
33
- {
34
- name: name,
35
- loc: location_from_callable(callable),
36
- enhancements: enhancements.map { |e| location_from_callable(e) },
37
- }
38
- end
39
-
40
- def call(store: nil, execution_context: nil)
41
- executor = ::Fixtury::DefinitionExecutor.new(store: store, definition: self, execution_context: execution_context)
42
- executor.__call
43
- end
38
+ # Delegates the `call` method to the `callable` object.
39
+ def_delegator :callable, :call
44
40
 
45
- def location_from_callable(callable)
46
- return nil unless callable.respond_to?(:source_location)
47
-
48
- callable.source_location.join(":")
49
- end
41
+ # Returns the parent schema of the Definition.
42
+ #
43
+ # @return [Object] The parent schema.
44
+ alias schema parent
50
45
 
46
+ attr_reader :callable, :dependencies
51
47
  end
52
48
  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