fixture_kit 0.18.0 → 0.20.0
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/lib/fixture_kit/adapters/minitest_adapter.rb +11 -5
- data/lib/fixture_kit/adapters/rspec_adapter.rb +10 -5
- data/lib/fixture_kit/cache.rb +26 -2
- data/lib/fixture_kit/definition.rb +31 -1
- data/lib/fixture_kit/file_cache.rb +0 -10
- data/lib/fixture_kit/fixture.rb +2 -0
- data/lib/fixture_kit/registry.rb +18 -0
- data/lib/fixture_kit/version.rb +1 -1
- data/lib/fixture_kit.rb +2 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90cf1c283943f0ec2cf0c82b5f5b6b65de810a1e4e5ed8d15fd12c4b2ba64879
|
|
4
|
+
data.tar.gz: 2eb935b512b373c399220ed60a6250b3d32349aa9dc5e4c31801d3db45a94b94
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a4f49bf2e6422be97e8a316acfd623c9e27c861bc63c29465dc6d8fb331ddbff85a84aeede77d654e4e544096061b62c8aa9daa3bfac23ec5ae7d6dfc8db957
|
|
7
|
+
data.tar.gz: a799c0b665d6396837fb82ee50d6e193e3adde8114f0edb63d3f3ed5540205b486587a5ff1065ea5811c2b61fa0ed9cb10f0c43fba18da9b15eb0a913b76f660
|
|
@@ -7,15 +7,18 @@ require "active_support/inflector"
|
|
|
7
7
|
module FixtureKit
|
|
8
8
|
class MinitestAdapter < FixtureKit::Adapter
|
|
9
9
|
TEST_NAME = "fixture kit cache pregeneration"
|
|
10
|
+
TEST_METHOD = :"test_#{TEST_NAME.tr(" ", "_")}"
|
|
10
11
|
|
|
11
12
|
def execute(&block)
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
test = test_class.new(TEST_METHOD)
|
|
14
|
+
# Minitest runs a test by sending its name to the instance, so defining it
|
|
15
|
+
# here leaves the reused class untouched and the block dies with the test.
|
|
16
|
+
test.define_singleton_method(TEST_METHOD) do
|
|
14
17
|
block.call(self)
|
|
15
18
|
pass
|
|
16
19
|
end
|
|
17
20
|
|
|
18
|
-
result =
|
|
21
|
+
result = test.run
|
|
19
22
|
return if result.passed?
|
|
20
23
|
|
|
21
24
|
raise result.failures.first.error
|
|
@@ -27,8 +30,11 @@ module FixtureKit
|
|
|
27
30
|
|
|
28
31
|
private
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
# Reused rather than built per generation: including
|
|
34
|
+
# ActiveRecord::TestFixtures appends the class to ActiveSupport's
|
|
35
|
+
# :active_record_fixtures load hooks, and that never shrinks.
|
|
36
|
+
def test_class
|
|
37
|
+
@test_class ||= Class.new(ActiveSupport::TestCase) do
|
|
32
38
|
::Minitest::Runnable.runnables.delete(self)
|
|
33
39
|
include(::ActiveRecord::TestFixtures)
|
|
34
40
|
end
|
|
@@ -7,12 +7,14 @@ module FixtureKit
|
|
|
7
7
|
def execute(&block)
|
|
8
8
|
previous_example = ::RSpec.current_example
|
|
9
9
|
previous_scope = ::RSpec.current_scope
|
|
10
|
-
|
|
11
|
-
example =
|
|
10
|
+
group = example_group
|
|
11
|
+
example = group.example { block.call(self) }
|
|
12
12
|
succeeded =
|
|
13
13
|
begin
|
|
14
|
-
example.run(
|
|
14
|
+
example.run(group.new, ::RSpec::Core::NullReporter)
|
|
15
15
|
ensure
|
|
16
|
+
# The group is reused, and the example it holds retains this block.
|
|
17
|
+
group.examples.clear
|
|
16
18
|
::RSpec.current_example = previous_example
|
|
17
19
|
::RSpec.current_scope = previous_scope
|
|
18
20
|
end
|
|
@@ -27,8 +29,11 @@ module FixtureKit
|
|
|
27
29
|
|
|
28
30
|
private
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
# Reused rather than built per generation: rspec-rails includes
|
|
33
|
+
# ActiveRecord::TestFixtures into every group, which appends it to
|
|
34
|
+
# ActiveSupport's :active_record_fixtures load hooks, and that never shrinks.
|
|
35
|
+
def example_group
|
|
36
|
+
@example_group ||= ::RSpec::Core::ExampleGroup.subclass(
|
|
32
37
|
::RSpec::Core::ExampleGroup,
|
|
33
38
|
"FixtureKit",
|
|
34
39
|
[],
|
data/lib/fixture_kit/cache.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
3
5
|
module FixtureKit
|
|
4
6
|
class Cache
|
|
5
7
|
ANONYMOUS_DIRECTORY = "_anonymous"
|
|
8
|
+
DIGEST_LENGTH = 12
|
|
6
9
|
|
|
7
10
|
include ConfigurationHelper
|
|
8
11
|
|
|
@@ -22,7 +25,8 @@ module FixtureKit
|
|
|
22
25
|
if raw_identifier.is_a?(String)
|
|
23
26
|
raw_identifier
|
|
24
27
|
else
|
|
25
|
-
|
|
28
|
+
normalized_scope = FixtureKit.runner.adapter.identifier_for(raw_identifier)
|
|
29
|
+
File.join(ANONYMOUS_DIRECTORY, "#{normalized_scope}.#{definition_digest}")
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
end
|
|
@@ -53,7 +57,7 @@ module FixtureKit
|
|
|
53
57
|
FixtureKit.runner.adapter.execute do |context|
|
|
54
58
|
@content = MemoryCache.new(
|
|
55
59
|
data: evaluate(FixtureKit.runner.coders, context),
|
|
56
|
-
exposed:
|
|
60
|
+
exposed: fixture.definition.exposed
|
|
57
61
|
)
|
|
58
62
|
end
|
|
59
63
|
|
|
@@ -62,6 +66,26 @@ module FixtureKit
|
|
|
62
66
|
|
|
63
67
|
private
|
|
64
68
|
|
|
69
|
+
# The scope name alone does not identify a declaration site. Test
|
|
70
|
+
# frameworks derive it from the group description, which is lossy -- RSpec
|
|
71
|
+
# strips every non-alphanumeric character, so `Foo::Bar` and `"Foo Bar"`
|
|
72
|
+
# both become `FooBar` -- and, worse, not even unique within a process:
|
|
73
|
+
# test runners that load specs in batches call RSpec::Core::World#reset
|
|
74
|
+
# between them, which drops the constants RSpec disambiguates against, so
|
|
75
|
+
# the first group of a given description in *every* batch takes the
|
|
76
|
+
# unsuffixed name. The cache directory is cleared once per process, so
|
|
77
|
+
# without the digest two spec files sharing a top-level description share
|
|
78
|
+
# this entry, and the second fixture to generate silently mounts the
|
|
79
|
+
# first's records.
|
|
80
|
+
#
|
|
81
|
+
# Joined to the scope with a dot rather than an underscore: scope names are
|
|
82
|
+
# snake_case, so an underscore leaves no boundary between the two, while a
|
|
83
|
+
# dot cannot appear in one -- RSpec strips non-alphanumerics from the group
|
|
84
|
+
# description before it is underscored.
|
|
85
|
+
def definition_digest
|
|
86
|
+
Digest::SHA256.hexdigest(fixture.definition.fingerprint)[0, DIGEST_LENGTH]
|
|
87
|
+
end
|
|
88
|
+
|
|
65
89
|
def evaluate(coders, context, data = {}, &block)
|
|
66
90
|
if coders.empty?
|
|
67
91
|
fixture.definition.evaluate(context, parent: fixture.parent&.mount)
|
|
@@ -14,6 +14,17 @@ module FixtureKit
|
|
|
14
14
|
@definition.source_location.first
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
def location
|
|
18
|
+
file, line = @definition.source_location
|
|
19
|
+
"#{file}:#{line}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Two definitions sharing a fingerprint evaluate the same block against the
|
|
23
|
+
# same parent, so they are interchangeable and may share a cache entry.
|
|
24
|
+
def fingerprint
|
|
25
|
+
"#{location}:#{extends}"
|
|
26
|
+
end
|
|
27
|
+
|
|
17
28
|
def evaluate(context, parent: nil)
|
|
18
29
|
context.singleton_class.prepend(mixin(parent))
|
|
19
30
|
context.instance_exec(&@definition)
|
|
@@ -25,12 +36,31 @@ module FixtureKit
|
|
|
25
36
|
raise FixtureKit::DuplicateNameError, "Name #{name} already exposed"
|
|
26
37
|
end
|
|
27
38
|
|
|
28
|
-
@exposed[name] = record
|
|
39
|
+
@exposed[name] = serialize(name, record)
|
|
29
40
|
end
|
|
30
41
|
end
|
|
31
42
|
|
|
32
43
|
private
|
|
33
44
|
|
|
45
|
+
def serialize(name, record)
|
|
46
|
+
if record.is_a?(Array)
|
|
47
|
+
record.map { |item| reference(name, item) }
|
|
48
|
+
else
|
|
49
|
+
reference(name, record)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def reference(name, record)
|
|
54
|
+
unless record.persisted?
|
|
55
|
+
raise FixtureKit::UnpersistedRecordError,
|
|
56
|
+
"cannot expose #{name.inspect}: the #{record.class} is not persisted. " \
|
|
57
|
+
"Exposed records are captured as class/id pairs when `expose` is called, " \
|
|
58
|
+
"so save the record before exposing it."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
{ record.class => record.id }
|
|
62
|
+
end
|
|
63
|
+
|
|
34
64
|
def mixin(parent)
|
|
35
65
|
definition = self
|
|
36
66
|
|
|
@@ -48,16 +48,6 @@ module FixtureKit
|
|
|
48
48
|
File.write(path, JSON.pretty_generate(content))
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
def serialize_exposed(exposed)
|
|
52
|
-
exposed.each_with_object({}) do |(name, record), hash|
|
|
53
|
-
if record.is_a?(Array)
|
|
54
|
-
hash[name] = record.map { |record| { record.class => record.id } }
|
|
55
|
-
else
|
|
56
|
-
hash[name] = { record.class => record.id }
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
51
|
private
|
|
62
52
|
|
|
63
53
|
def coder_for(class_name)
|
data/lib/fixture_kit/fixture.rb
CHANGED
data/lib/fixture_kit/registry.rb
CHANGED
|
@@ -8,6 +8,7 @@ module FixtureKit
|
|
|
8
8
|
@declarations = {}
|
|
9
9
|
@fixtures = {}
|
|
10
10
|
@resolving = []
|
|
11
|
+
@cache_owners = {}
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def add(name_or_definition, scope = nil)
|
|
@@ -34,6 +35,23 @@ module FixtureKit
|
|
|
34
35
|
@fixtures.values
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
# Fixtures sharing a cache identifier share a cache file: the second one to
|
|
39
|
+
# generate finds the file already written, skips generation, and mounts the
|
|
40
|
+
# other's records. That is only safe when both declarations are the same, so
|
|
41
|
+
# anything else has to fail here -- otherwise it surfaces as a confusing
|
|
42
|
+
# NoMethodError on Repository, or not at all when both expose the same names.
|
|
43
|
+
# Identifiers are derived from the declaration site, so this is a backstop
|
|
44
|
+
# against a digest collision or a regression in that derivation.
|
|
45
|
+
def claim_cache_identifier(fixture)
|
|
46
|
+
owner = (@cache_owners[fixture.cache.identifier] ||= fixture)
|
|
47
|
+
return if owner.equal?(fixture)
|
|
48
|
+
return if owner.definition.fingerprint == fixture.definition.fingerprint
|
|
49
|
+
|
|
50
|
+
raise FixtureKit::CacheIdentifierCollision,
|
|
51
|
+
"fixtures declared at #{owner.definition.location} and #{fixture.definition.location} " \
|
|
52
|
+
"both resolve to the cache identifier '#{fixture.cache.identifier}'"
|
|
53
|
+
end
|
|
54
|
+
|
|
37
55
|
private
|
|
38
56
|
|
|
39
57
|
def fetch_named_fixture(name)
|
data/lib/fixture_kit/version.rb
CHANGED
data/lib/fixture_kit.rb
CHANGED
|
@@ -6,9 +6,11 @@ module FixtureKit
|
|
|
6
6
|
class InvalidFixtureDeclaration < Error; end
|
|
7
7
|
class MultipleFixtures < Error; end
|
|
8
8
|
class CacheMissingError < Error; end
|
|
9
|
+
class CacheIdentifierCollision < Error; end
|
|
9
10
|
class FixtureDefinitionNotFound < Error; end
|
|
10
11
|
class RunnerAlreadyStartedError < Error; end
|
|
11
12
|
class CircularFixtureInheritance < Error; end
|
|
13
|
+
class UnpersistedRecordError < Error; end
|
|
12
14
|
|
|
13
15
|
autoload :VERSION, File.expand_path("fixture_kit/version", __dir__)
|
|
14
16
|
autoload :Configuration, File.expand_path("fixture_kit/configuration", __dir__)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fixture_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.20.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ngan Pham
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|