fixtury 0.4.1 → 1.0.0.beta2

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