fixture_kit 0.10.0 → 0.11.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/README.md +20 -0
- data/lib/fixture_kit/analyzer/ast_factory_detector.rb +64 -0
- data/lib/fixture_kit/analyzer/file_result.rb +28 -0
- data/lib/fixture_kit/analyzer/group_analyzer.rb +65 -0
- data/lib/fixture_kit/analyzer/let_definition.rb +32 -0
- data/lib/fixture_kit/analyzer/runner.rb +97 -0
- data/lib/fixture_kit/analyzer/text_formatter.rb +43 -0
- data/lib/fixture_kit/analyzer.rb +34 -0
- data/lib/fixture_kit/cache.rb +16 -42
- data/lib/fixture_kit/file_cache.rb +51 -0
- data/lib/fixture_kit/fixture.rb +8 -0
- data/lib/fixture_kit/memory_cache.rb +17 -0
- data/lib/fixture_kit/minitest.rb +2 -0
- data/lib/fixture_kit/rspec.rb +4 -0
- data/lib/fixture_kit/version.rb +1 -1
- data/lib/fixture_kit.rb +2 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ccc3496f3e23da0877bac37e98d2fd325daf737f4d7bc2988d80156d202c248
|
|
4
|
+
data.tar.gz: 332a5f7824ec4c449ef052bf67616c71b7bca07749f69dcb9c1369afbb95f1a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aaab87039e3f040b6b1f200a2e853e46263449f2bc900f664d167b009adffb2eccd48fc9080051b67dc6ad8c1e12f3aca4796db45ced24f63ad35f671cddb7f6
|
|
7
|
+
data.tar.gz: 55d185c158111d9a892ceb558af729f48a2708c4975e1365a411bdbbb6a6e26d776fd511bc9505dc818aa4e4747062acb5b51b514c3efe43351ff342e036a3e9
|
data/README.md
CHANGED
|
@@ -89,6 +89,26 @@ end
|
|
|
89
89
|
- ActiveRecord >= 8.0
|
|
90
90
|
- ActiveSupport >= 8.0
|
|
91
91
|
|
|
92
|
+
## Releasing
|
|
93
|
+
|
|
94
|
+
1. Bump the version in `lib/fixture_kit/version.rb`
|
|
95
|
+
2. Update lockfiles:
|
|
96
|
+
```sh
|
|
97
|
+
bundle install
|
|
98
|
+
cd spec/dummy && bundle install && cd ../..
|
|
99
|
+
bundle exec appraisal install
|
|
100
|
+
```
|
|
101
|
+
3. Commit and push to main:
|
|
102
|
+
```sh
|
|
103
|
+
git add lib/fixture_kit/version.rb spec/dummy/Gemfile.lock gemfiles/*.gemfile.lock
|
|
104
|
+
git commit -m "Release vX.Y.Z"
|
|
105
|
+
git push
|
|
106
|
+
```
|
|
107
|
+
4. Create a GitHub release:
|
|
108
|
+
```sh
|
|
109
|
+
gh release create vX.Y.Z --title "vX.Y.Z" --target main --generate-notes
|
|
110
|
+
```
|
|
111
|
+
|
|
92
112
|
## License
|
|
93
113
|
|
|
94
114
|
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module FixtureKit
|
|
7
|
+
module Analyzer
|
|
8
|
+
class AstFactoryDetector
|
|
9
|
+
FACTORY_METHODS = %w[create build build_stubbed create_list build_list].to_set.freeze
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@file_cache = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Returns array of factory name strings, e.g. ["company", "employee"]
|
|
16
|
+
def detect(mod, method_name)
|
|
17
|
+
meth = mod.instance_method(method_name)
|
|
18
|
+
file, line = meth.source_location
|
|
19
|
+
return [] unless file && File.exist?(file)
|
|
20
|
+
|
|
21
|
+
ast = parse_file(file)
|
|
22
|
+
block = find_block_at_line(ast, line)
|
|
23
|
+
return [] unless block
|
|
24
|
+
|
|
25
|
+
collect_factory_calls(block)
|
|
26
|
+
rescue
|
|
27
|
+
[]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def parse_file(path)
|
|
33
|
+
@file_cache[path] ||= Prism.parse(File.read(path)).value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Find the block node that starts at the target line.
|
|
37
|
+
def find_block_at_line(node, target_line)
|
|
38
|
+
if node.is_a?(Prism::BlockNode) && node.location.start_line == target_line
|
|
39
|
+
return node
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
node.child_nodes.compact.each do |child|
|
|
43
|
+
found = find_block_at_line(child, target_line)
|
|
44
|
+
return found if found
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Walk an AST subtree collecting factory call argument symbols.
|
|
51
|
+
def collect_factory_calls(node)
|
|
52
|
+
results = []
|
|
53
|
+
|
|
54
|
+
if node.is_a?(Prism::CallNode) && FACTORY_METHODS.include?(node.name.to_s)
|
|
55
|
+
first_arg = node.arguments&.arguments&.first
|
|
56
|
+
results << first_arg.value if first_arg.is_a?(Prism::SymbolNode)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
node.child_nodes.compact.each { |child| results.concat(collect_factory_calls(child)) }
|
|
60
|
+
results
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FixtureKit
|
|
4
|
+
module Analyzer
|
|
5
|
+
class FileResult
|
|
6
|
+
attr_reader :file, :total_examples, :lets
|
|
7
|
+
|
|
8
|
+
def initialize(file:, total_examples:, lets:)
|
|
9
|
+
@file = file
|
|
10
|
+
@total_examples = total_examples
|
|
11
|
+
@lets = lets.sort_by { |l| -l.example_count }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def max_reuse
|
|
15
|
+
lets.first&.example_count || 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
{
|
|
20
|
+
file: file,
|
|
21
|
+
total_examples: total_examples,
|
|
22
|
+
max_reuse: max_reuse,
|
|
23
|
+
lets: lets.map(&:to_h),
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FixtureKit
|
|
4
|
+
module Analyzer
|
|
5
|
+
class GroupAnalyzer
|
|
6
|
+
def initialize(detector: AstFactoryDetector.new)
|
|
7
|
+
@detector = detector
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Check if a group directly defines a let with this name
|
|
11
|
+
def group_defines_let?(group, let_name)
|
|
12
|
+
own_mod = group.const_get(:LetDefinitions, false)
|
|
13
|
+
own_mod.instance_methods(false).include?(let_name.to_sym)
|
|
14
|
+
rescue NameError
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Count examples that inherit a let defined at `group`, stopping at overrides
|
|
19
|
+
def examples_using_let(group, let_name)
|
|
20
|
+
count = group.examples.count
|
|
21
|
+
group.children.each do |child|
|
|
22
|
+
next if group_defines_let?(child, let_name)
|
|
23
|
+
count += examples_using_let(child, let_name)
|
|
24
|
+
end
|
|
25
|
+
count
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Total examples in group + all descendants
|
|
29
|
+
def total_examples(group)
|
|
30
|
+
group.examples.count + group.children.sum { |c| total_examples(c) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Walk tree, collect factory lets
|
|
34
|
+
def analyze(group, results = [])
|
|
35
|
+
own_mod = begin
|
|
36
|
+
group.const_get(:LetDefinitions, false)
|
|
37
|
+
rescue NameError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if own_mod
|
|
42
|
+
own_mod.instance_methods(false).each do |method_name|
|
|
43
|
+
factories = @detector.detect(own_mod, method_name)
|
|
44
|
+
next if factories.empty?
|
|
45
|
+
|
|
46
|
+
loc = own_mod.instance_method(method_name).source_location
|
|
47
|
+
example_count = examples_using_let(group, method_name.to_s)
|
|
48
|
+
|
|
49
|
+
results << LetDefinition.new(
|
|
50
|
+
name: method_name.to_s,
|
|
51
|
+
factories: factories,
|
|
52
|
+
example_count: example_count,
|
|
53
|
+
file: loc&.first,
|
|
54
|
+
line: loc&.last,
|
|
55
|
+
group_description: group.description.to_s[0, 80],
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
group.children.each { |child| analyze(child, results) }
|
|
61
|
+
results
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FixtureKit
|
|
4
|
+
module Analyzer
|
|
5
|
+
class LetDefinition
|
|
6
|
+
attr_reader :name, :factories, :example_count, :file, :line, :group_description
|
|
7
|
+
|
|
8
|
+
def initialize(name:, factories:, example_count:, file:, line:, group_description:)
|
|
9
|
+
@name = name
|
|
10
|
+
@factories = factories
|
|
11
|
+
@example_count = example_count
|
|
12
|
+
@file = file
|
|
13
|
+
@line = line
|
|
14
|
+
@group_description = group_description
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def defined_at
|
|
18
|
+
"#{file}:#{line}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
{
|
|
23
|
+
let_name: name,
|
|
24
|
+
factories: factories,
|
|
25
|
+
example_count: example_count,
|
|
26
|
+
defined_at: defined_at,
|
|
27
|
+
group: group_description,
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module FixtureKit
|
|
6
|
+
module Analyzer
|
|
7
|
+
class Runner
|
|
8
|
+
def initialize(
|
|
9
|
+
format: ENV.fetch("ANALYZER_FORMAT", "text"),
|
|
10
|
+
limit: ENV.fetch("ANALYZER_LIMIT", "50").to_i,
|
|
11
|
+
lets_per_file: ENV.fetch("ANALYZER_LETS_PER_FILE", "8").to_i,
|
|
12
|
+
min_reuse: ENV.fetch("ANALYZER_MIN_REUSE", "2").to_i,
|
|
13
|
+
output_path: ENV["ANALYZER_OUTPUT"]
|
|
14
|
+
)
|
|
15
|
+
@format = format
|
|
16
|
+
@limit = limit
|
|
17
|
+
@lets_per_file = lets_per_file
|
|
18
|
+
@min_reuse = min_reuse
|
|
19
|
+
@output_path = output_path
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run(example_groups)
|
|
23
|
+
return if example_groups.empty?
|
|
24
|
+
|
|
25
|
+
total = example_groups.length
|
|
26
|
+
$stderr.puts "[fixture_kit:analyzer] Analyzing #{total} top-level example groups..."
|
|
27
|
+
|
|
28
|
+
analyzer = GroupAnalyzer.new
|
|
29
|
+
|
|
30
|
+
by_file = Hash.new { |h, k| h[k] = {lets: [], total_examples: 0} }
|
|
31
|
+
|
|
32
|
+
example_groups.each_with_index do |group, idx|
|
|
33
|
+
file = group.metadata[:file_path] || group.metadata[:absolute_file_path] || "unknown"
|
|
34
|
+
if (idx + 1) % 500 == 0 || idx + 1 == total
|
|
35
|
+
$stderr.puts "[fixture_kit:analyzer] Processing group #{idx + 1}/#{total} (#{file})"
|
|
36
|
+
end
|
|
37
|
+
by_file[file][:total_examples] += analyzer.total_examples(group)
|
|
38
|
+
analyzer.analyze(group, by_file[file][:lets])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
results = by_file.map do |file, data|
|
|
42
|
+
FileResult.new(file: file, total_examples: data[:total_examples], lets: data[:lets])
|
|
43
|
+
end.sort_by { |r| -r.max_reuse }
|
|
44
|
+
|
|
45
|
+
output(results)
|
|
46
|
+
results
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Install the at_exit hook that runs the analyzer after rspec --dry-run
|
|
50
|
+
def self.install_rspec_hook!
|
|
51
|
+
load_start = Time.now
|
|
52
|
+
|
|
53
|
+
progress_thread = Thread.new do
|
|
54
|
+
last_count = 0
|
|
55
|
+
loop do
|
|
56
|
+
sleep 5
|
|
57
|
+
current = ::RSpec.world.example_groups.length rescue 0
|
|
58
|
+
if current != last_count
|
|
59
|
+
$stderr.puts "[fixture_kit:analyzer] Loading... #{current} top-level groups found (#{(Time.now - load_start).round(1)}s)"
|
|
60
|
+
last_count = current
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
progress_thread.abort_on_exception = false
|
|
65
|
+
|
|
66
|
+
at_exit do
|
|
67
|
+
progress_thread.kill
|
|
68
|
+
next unless defined?(::RSpec) && ::RSpec.world.example_groups.any?
|
|
69
|
+
|
|
70
|
+
$stderr.puts "[fixture_kit:analyzer] Spec loading complete: #{::RSpec.world.example_groups.length} groups in #{(Time.now - load_start).round(1)}s"
|
|
71
|
+
new.run(::RSpec.world.example_groups)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def output(results)
|
|
78
|
+
case @format
|
|
79
|
+
when "json"
|
|
80
|
+
json = JSON.pretty_generate(results.map(&:to_h))
|
|
81
|
+
$stdout.puts json
|
|
82
|
+
else
|
|
83
|
+
TextFormatter.new(
|
|
84
|
+
limit: @limit,
|
|
85
|
+
lets_per_file: @lets_per_file,
|
|
86
|
+
min_reuse: @min_reuse,
|
|
87
|
+
).render(results)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if @output_path
|
|
91
|
+
File.write(@output_path, JSON.pretty_generate(results.map(&:to_h)))
|
|
92
|
+
$stderr.puts "[fixture_kit:analyzer] Full JSON written to: #{@output_path}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FixtureKit
|
|
4
|
+
module Analyzer
|
|
5
|
+
class TextFormatter
|
|
6
|
+
def initialize(limit:, lets_per_file:, min_reuse:, io: $stdout)
|
|
7
|
+
@limit = limit
|
|
8
|
+
@lets_per_file = lets_per_file
|
|
9
|
+
@min_reuse = min_reuse
|
|
10
|
+
@io = io
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render(results)
|
|
14
|
+
filtered = results.select { |r| r.max_reuse >= @min_reuse }
|
|
15
|
+
|
|
16
|
+
@io.puts
|
|
17
|
+
@io.puts "=" * 110
|
|
18
|
+
@io.puts "FACTORY LET REUSE (ranked by highest single-let example count)"
|
|
19
|
+
@io.puts "=" * 110
|
|
20
|
+
@io.puts
|
|
21
|
+
@io.puts "#{results.length} spec files analyzed, #{filtered.length} with max reuse >= #{@min_reuse}"
|
|
22
|
+
|
|
23
|
+
filtered.first(@limit).each_with_index do |result, idx|
|
|
24
|
+
@io.puts
|
|
25
|
+
@io.puts "#{idx + 1}. #{result.file}"
|
|
26
|
+
@io.puts " examples: #{result.total_examples} | factory lets: #{result.lets.length} | max reuse: #{result.max_reuse}"
|
|
27
|
+
@io.puts " " + "-" * 95
|
|
28
|
+
|
|
29
|
+
result.lets.first(@lets_per_file).each do |l|
|
|
30
|
+
@io.puts Kernel.format(" let(:%s) examples: %d factories: %s",
|
|
31
|
+
l.name, l.example_count, l.factories.join(", "))
|
|
32
|
+
@io.puts " group: #{l.group_description}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
remaining = result.lets.length - @lets_per_file
|
|
36
|
+
@io.puts " ... +#{remaining} more" if remaining > 0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@io.puts
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# FixtureKit::Analyzer — Find high-ROI FixtureKit optimization targets.
|
|
4
|
+
#
|
|
5
|
+
# Analyzes RSpec specs via --dry-run to find factory_bot `let` blocks
|
|
6
|
+
# and rank them by how many examples each `let` is shared across.
|
|
7
|
+
# The more examples share a let, the bigger the speedup from caching
|
|
8
|
+
# it with FixtureKit.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# rspec --dry-run --require fixture_kit/analyzer [spec files or dirs...]
|
|
12
|
+
#
|
|
13
|
+
# Options (via env vars):
|
|
14
|
+
# ANALYZER_FORMAT=text|json Output format (default: text)
|
|
15
|
+
# ANALYZER_LIMIT=N Max files to show (default: 50)
|
|
16
|
+
# ANALYZER_LETS_PER_FILE=N Max lets to show per file (default: 8)
|
|
17
|
+
# ANALYZER_MIN_REUSE=N Only show files with max reuse >= N (default: 2)
|
|
18
|
+
# ANALYZER_OUTPUT=path Write JSON to file (default: none)
|
|
19
|
+
|
|
20
|
+
module FixtureKit
|
|
21
|
+
module Analyzer
|
|
22
|
+
autoload :AstFactoryDetector, File.expand_path("analyzer/ast_factory_detector", __dir__)
|
|
23
|
+
autoload :GroupAnalyzer, File.expand_path("analyzer/group_analyzer", __dir__)
|
|
24
|
+
autoload :LetDefinition, File.expand_path("analyzer/let_definition", __dir__)
|
|
25
|
+
autoload :FileResult, File.expand_path("analyzer/file_result", __dir__)
|
|
26
|
+
autoload :TextFormatter, File.expand_path("analyzer/text_formatter", __dir__)
|
|
27
|
+
autoload :Runner, File.expand_path("analyzer/runner", __dir__)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Auto-hook into RSpec when loaded via --require
|
|
32
|
+
if defined?(RSpec)
|
|
33
|
+
FixtureKit::Analyzer::Runner.install_rspec_hook!
|
|
34
|
+
end
|
data/lib/fixture_kit/cache.rb
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
require "fileutils"
|
|
5
3
|
require "active_support/core_ext/array/wrap"
|
|
6
|
-
require "active_support/inflector"
|
|
7
4
|
|
|
8
5
|
module FixtureKit
|
|
9
6
|
class Cache
|
|
10
7
|
ANONYMOUS_DIRECTORY = "_anonymous"
|
|
11
|
-
MemoryData = Data.define(:records, :exposed)
|
|
12
8
|
|
|
13
9
|
include ConfigurationHelper
|
|
14
10
|
|
|
@@ -19,7 +15,7 @@ module FixtureKit
|
|
|
19
15
|
end
|
|
20
16
|
|
|
21
17
|
def path
|
|
22
|
-
|
|
18
|
+
file_cache.path
|
|
23
19
|
end
|
|
24
20
|
|
|
25
21
|
def identifier
|
|
@@ -34,7 +30,11 @@ module FixtureKit
|
|
|
34
30
|
end
|
|
35
31
|
|
|
36
32
|
def exists?
|
|
37
|
-
data ||
|
|
33
|
+
data || file_cache.exists?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def clear_memory
|
|
37
|
+
@data = nil
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def load
|
|
@@ -42,7 +42,7 @@ module FixtureKit
|
|
|
42
42
|
raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{fixture.identifier}'"
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
@data ||=
|
|
45
|
+
@data ||= file_cache.read
|
|
46
46
|
statements_by_connection(data.records).each do |connection, statements|
|
|
47
47
|
connection.disable_referential_integrity do
|
|
48
48
|
# execute_batch is private in current supported Rails versions.
|
|
@@ -64,17 +64,23 @@ module FixtureKit
|
|
|
64
64
|
captured_models.concat(fixture.parent.cache.data.records.keys)
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
@data =
|
|
67
|
+
@data = MemoryCache.new(
|
|
68
68
|
records: generate_statements(captured_models),
|
|
69
|
-
exposed:
|
|
69
|
+
exposed: file_cache.serialize_exposed(fixture.definition.exposed)
|
|
70
70
|
)
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
file_cache.write(data)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
private
|
|
77
77
|
|
|
78
|
+
def file_cache
|
|
79
|
+
@file_cache ||= FileCache.new(
|
|
80
|
+
File.join(configuration.cache_path, "#{identifier}.json")
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
78
84
|
def generate_statements(models)
|
|
79
85
|
models.uniq.each_with_object({}) do |model, statements|
|
|
80
86
|
columns = model.column_names
|
|
@@ -104,16 +110,6 @@ module FixtureKit
|
|
|
104
110
|
"INSERT INTO #{quoted_table} (#{quoted_columns.join(", ")}) VALUES #{rows.join(", ")}"
|
|
105
111
|
end
|
|
106
112
|
|
|
107
|
-
def build_exposed_mapping(exposed)
|
|
108
|
-
exposed.each_with_object({}) do |(name, record), hash|
|
|
109
|
-
if record.is_a?(Array)
|
|
110
|
-
hash[name] = record.map { |record| { record.class => record.id } }
|
|
111
|
-
else
|
|
112
|
-
hash[name] = { record.class => record.id }
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
113
|
def statements_by_connection(records)
|
|
118
114
|
deleted_tables = Set.new
|
|
119
115
|
|
|
@@ -129,27 +125,5 @@ module FixtureKit
|
|
|
129
125
|
grouped[connection] << sql if sql
|
|
130
126
|
end
|
|
131
127
|
end
|
|
132
|
-
|
|
133
|
-
def load_memory_data
|
|
134
|
-
file_data = JSON.parse(File.read(path))
|
|
135
|
-
records = file_data.fetch("records").transform_keys do |model_name|
|
|
136
|
-
ActiveSupport::Inflector.constantize(model_name)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
exposed = file_data.fetch("exposed").each_with_object({}) do |(name, value), hash|
|
|
140
|
-
if value.is_a?(Array)
|
|
141
|
-
hash[name.to_sym] = value.map { |r| { ActiveSupport::Inflector.constantize(r.keys.first) => r.values.first } }
|
|
142
|
-
else
|
|
143
|
-
hash[name.to_sym] = { ActiveSupport::Inflector.constantize(value.keys.first) => value.values.first }
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
MemoryData.new(records: records, exposed: exposed)
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def save_file_data
|
|
151
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
152
|
-
File.write(path, JSON.pretty_generate(data.to_h))
|
|
153
|
-
end
|
|
154
128
|
end
|
|
155
129
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "active_support/inflector"
|
|
6
|
+
|
|
7
|
+
module FixtureKit
|
|
8
|
+
class FileCache
|
|
9
|
+
attr_reader :path
|
|
10
|
+
|
|
11
|
+
def initialize(path)
|
|
12
|
+
@path = path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def exists?
|
|
16
|
+
File.exist?(path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def read
|
|
20
|
+
file_data = JSON.parse(File.read(path))
|
|
21
|
+
records = file_data.fetch("records").transform_keys do |model_name|
|
|
22
|
+
ActiveSupport::Inflector.constantize(model_name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
exposed = file_data.fetch("exposed").each_with_object({}) do |(name, value), hash|
|
|
26
|
+
if value.is_a?(Array)
|
|
27
|
+
hash[name.to_sym] = value.map { |r| { ActiveSupport::Inflector.constantize(r.keys.first) => r.values.first } }
|
|
28
|
+
else
|
|
29
|
+
hash[name.to_sym] = { ActiveSupport::Inflector.constantize(value.keys.first) => value.values.first }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
MemoryCache.new(records: records, exposed: exposed)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def write(data)
|
|
37
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
38
|
+
File.write(path, JSON.pretty_generate(data.to_h))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def serialize_exposed(exposed)
|
|
42
|
+
exposed.each_with_object({}) do |(name, record), hash|
|
|
43
|
+
if record.is_a?(Array)
|
|
44
|
+
hash[name] = record.map { |record| { record.class => record.id } }
|
|
45
|
+
else
|
|
46
|
+
hash[name] = { record.class => record.id }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/fixture_kit/fixture.rb
CHANGED
|
@@ -36,8 +36,16 @@ module FixtureKit
|
|
|
36
36
|
emit(:cache_mounted) { @cache.load }
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def finish
|
|
40
|
+
@cache.clear_memory if anonymous?
|
|
41
|
+
end
|
|
42
|
+
|
|
39
43
|
private
|
|
40
44
|
|
|
45
|
+
def anonymous?
|
|
46
|
+
!identifier.is_a?(String)
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
def emit(event)
|
|
42
50
|
cache_identifier = @cache.identifier
|
|
43
51
|
unless block_given?
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FixtureKit
|
|
4
|
+
class MemoryCache
|
|
5
|
+
attr_reader :records, :exposed
|
|
6
|
+
|
|
7
|
+
def initialize(records:, exposed:)
|
|
8
|
+
@records = records
|
|
9
|
+
@exposed = exposed
|
|
10
|
+
freeze
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_h
|
|
14
|
+
{ records: records, exposed: exposed }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/fixture_kit/minitest.rb
CHANGED
data/lib/fixture_kit/rspec.rb
CHANGED
data/lib/fixture_kit/version.rb
CHANGED
data/lib/fixture_kit.rb
CHANGED
|
@@ -21,6 +21,8 @@ module FixtureKit
|
|
|
21
21
|
autoload :Repository, File.expand_path("fixture_kit/repository", __dir__)
|
|
22
22
|
autoload :SqlSubscriber, File.expand_path("fixture_kit/sql_subscriber", __dir__)
|
|
23
23
|
autoload :Cache, File.expand_path("fixture_kit/cache", __dir__)
|
|
24
|
+
autoload :FileCache, File.expand_path("fixture_kit/file_cache", __dir__)
|
|
25
|
+
autoload :MemoryCache, File.expand_path("fixture_kit/memory_cache", __dir__)
|
|
24
26
|
autoload :Runner, File.expand_path("fixture_kit/runner", __dir__)
|
|
25
27
|
autoload :Adapter, File.expand_path("fixture_kit/adapter", __dir__)
|
|
26
28
|
autoload :MinitestAdapter, File.expand_path("fixture_kit/adapters/minitest_adapter", __dir__)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fixture_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ngan Pham
|
|
@@ -151,12 +151,21 @@ files:
|
|
|
151
151
|
- lib/fixture_kit/adapter.rb
|
|
152
152
|
- lib/fixture_kit/adapters/minitest_adapter.rb
|
|
153
153
|
- lib/fixture_kit/adapters/rspec_adapter.rb
|
|
154
|
+
- lib/fixture_kit/analyzer.rb
|
|
155
|
+
- lib/fixture_kit/analyzer/ast_factory_detector.rb
|
|
156
|
+
- lib/fixture_kit/analyzer/file_result.rb
|
|
157
|
+
- lib/fixture_kit/analyzer/group_analyzer.rb
|
|
158
|
+
- lib/fixture_kit/analyzer/let_definition.rb
|
|
159
|
+
- lib/fixture_kit/analyzer/runner.rb
|
|
160
|
+
- lib/fixture_kit/analyzer/text_formatter.rb
|
|
154
161
|
- lib/fixture_kit/cache.rb
|
|
155
162
|
- lib/fixture_kit/callbacks.rb
|
|
156
163
|
- lib/fixture_kit/configuration.rb
|
|
157
164
|
- lib/fixture_kit/configuration_helper.rb
|
|
158
165
|
- lib/fixture_kit/definition.rb
|
|
166
|
+
- lib/fixture_kit/file_cache.rb
|
|
159
167
|
- lib/fixture_kit/fixture.rb
|
|
168
|
+
- lib/fixture_kit/memory_cache.rb
|
|
160
169
|
- lib/fixture_kit/minitest.rb
|
|
161
170
|
- lib/fixture_kit/registry.rb
|
|
162
171
|
- lib/fixture_kit/repository.rb
|