foundries 0.1.3 → 0.1.4
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/CHANGELOG.md +8 -11
- data/README.md +45 -0
- data/lib/foundries/recording/aggregator.rb +119 -0
- data/lib/foundries/recording/collector.rb +90 -0
- data/lib/foundries/recording/node.rb +69 -0
- data/lib/foundries/recording/rake_task.rb +43 -0
- data/lib/foundries/recording/reporter.rb +114 -0
- data/lib/foundries/recording.rb +71 -0
- data/lib/foundries/version.rb +2 -2
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee65151ab0ae7d4ef338d8dd8dec76fbf9f0746be24d99d814abdee8716c500a
|
|
4
|
+
data.tar.gz: 9a298a3ca012affe264f0467240f7a44b46decb2227f88f9eb2ca27c28f8bfad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b0eef54e2302dbda0dcc4ee7a9224c75e31f90b42498dabcdcac4a8e00318bc79bd34f343367cd2f03ab1cb969f72b01d4336c49ae4329eac5d7ebd136b64c1
|
|
7
|
+
data.tar.gz: 60f6748284f995381cc16f26a816f7f1ac9fac581948bbd0bc553c4839caf4cd9df6634982930d9017f896bdbb8d5f3edd3072aca7be19f5b9ba1620479c3a27
|
data/CHANGELOG.md
CHANGED
|
@@ -5,22 +5,19 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
7
7
|
|
|
8
|
-
## [0.1.
|
|
8
|
+
## [0.1.4] - 2026-03-16
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- Recording module to analyze FactoryBot usage and suggest preset candidates (a294324)
|
|
13
|
+
- parallel_tests support for Recording with per-worker output and merge rake task (8e59deb)
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
### Changed
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
- README now covers Recording feature and parallel_tests support (69fa45b)
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
- lookup_order DSL on Blueprint for ancestor traversal (377632f)
|
|
20
|
-
- find_or_create pattern on Blueprint (377632f)
|
|
21
|
-
- Tests for inherited registries, parent-scoped find, collection_find_by kwargs, find_or_create, ascending_find, and parent_present? (2d92945)
|
|
19
|
+
## [0.1.3] - 2026-03-13
|
|
22
20
|
|
|
23
|
-
###
|
|
21
|
+
### Added
|
|
24
22
|
|
|
25
|
-
-
|
|
26
|
-
- Fix find parent scoping and collection_find_by kwargs handling (3ebfc3e)
|
|
23
|
+
- ancestor DSL and ancestors_for for path-based hierarchy building (7027e06)
|
data/README.md
CHANGED
|
@@ -224,6 +224,51 @@ When two presets share identical structure or one is structurally contained with
|
|
|
224
224
|
|
|
225
225
|
Each unique pair is warned once per process. The detection normalizes trees by deduplicating sibling nodes (keeping the richest subtree), collapsing pass-through chains, and sorting alphabetically. This means presets that build the same *shape* of data are detected regardless of the specific names or attribute values used.
|
|
226
226
|
|
|
227
|
+
## Factory Usage Recording
|
|
228
|
+
|
|
229
|
+
If you're migrating an existing test suite to Foundries, the recording feature can help you discover which factory call patterns appear most often — and would make good preset candidates.
|
|
230
|
+
|
|
231
|
+
Add to your `spec_helper.rb`:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
require "foundries/recording"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Then run your suite with the environment variable:
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
FOUNDRIES_RECORD=1 bundle exec rspec
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
After the suite finishes, a summary is printed to stdout:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
[Foundries] Recording complete. 482 tests, 3841 factory creates.
|
|
247
|
+
[Foundries] Top preset candidates:
|
|
248
|
+
1. team > [project > [task], user] (34 tests, score: 102)
|
|
249
|
+
2. team > [user] (28 tests, score: 56)
|
|
250
|
+
3. team > [project > [task > [comment]], user] (12 tests, score: 60)
|
|
251
|
+
[Foundries] Full report: tmp/foundries/recording.json
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The full JSON report at `tmp/foundries/recording.json` includes per-test breakdowns and all candidates with the test names that use each pattern.
|
|
255
|
+
|
|
256
|
+
### Parallel tests
|
|
257
|
+
|
|
258
|
+
Recording works with [parallel_tests](https://github.com/grosser/parallel_tests). Each worker writes its own file, then a rake task merges the results:
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
FOUNDRIES_RECORD=1 bundle exec parallel_rspec
|
|
262
|
+
rake foundries:recording:merge
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
To use the rake task, add to your `Rakefile`:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
require "foundries/recording/rake_task"
|
|
269
|
+
Foundries::Recording::RakeTask.install
|
|
270
|
+
```
|
|
271
|
+
|
|
227
272
|
## Requirements
|
|
228
273
|
|
|
229
274
|
- Ruby >= 4.0
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "node"
|
|
4
|
+
|
|
5
|
+
module Foundries
|
|
6
|
+
module Recording
|
|
7
|
+
class Aggregator
|
|
8
|
+
def initialize(results)
|
|
9
|
+
@results = results
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def candidates
|
|
13
|
+
return [] if @results.empty?
|
|
14
|
+
|
|
15
|
+
normalized = normalize_results
|
|
16
|
+
full_tree_candidates = group_by_full_tree(normalized)
|
|
17
|
+
subtree_candidates = find_common_subtrees(normalized)
|
|
18
|
+
merged = merge_candidates(full_tree_candidates, subtree_candidates)
|
|
19
|
+
filtered = merged.reject { |c| c[:tree_size] <= 1 }
|
|
20
|
+
filtered.sort_by { |c| -c[:score] }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def normalize_results
|
|
26
|
+
@results.transform_values(&:normalize)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def group_by_full_tree(normalized)
|
|
30
|
+
groups = Hash.new { |h, k| h[k] = [] }
|
|
31
|
+
|
|
32
|
+
normalized.each do |test_id, root|
|
|
33
|
+
structure = root_structure(root)
|
|
34
|
+
groups[structure] << test_id
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
groups.map do |structure, tests|
|
|
38
|
+
sample_root = normalized[tests.first]
|
|
39
|
+
tree_size = sample_root.children.sum(&:tree_size)
|
|
40
|
+
{
|
|
41
|
+
structure: structure,
|
|
42
|
+
frequency: tests.size,
|
|
43
|
+
tree_size: tree_size,
|
|
44
|
+
score: tests.size * tree_size,
|
|
45
|
+
tests: tests.map(&:to_s)
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def root_structure(root)
|
|
51
|
+
root.children.sort_by(&:signature).map(&:signature).join(", ")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def find_common_subtrees(normalized)
|
|
55
|
+
subtree_tests = Hash.new { |h, k| h[k] = Set.new }
|
|
56
|
+
subtree_nodes = {}
|
|
57
|
+
|
|
58
|
+
normalized.each do |test_id, root|
|
|
59
|
+
root.children.each do |child|
|
|
60
|
+
extract_nontrivial_subtrees(child).each do |subtree|
|
|
61
|
+
sig = subtree.signature
|
|
62
|
+
subtree_tests[sig] << test_id
|
|
63
|
+
subtree_nodes[sig] ||= subtree
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
subtree_tests.map do |sig, tests|
|
|
69
|
+
subtree = subtree_nodes[sig]
|
|
70
|
+
{
|
|
71
|
+
structure: sig,
|
|
72
|
+
frequency: tests.size,
|
|
73
|
+
tree_size: subtree.tree_size,
|
|
74
|
+
score: tests.size * subtree.tree_size,
|
|
75
|
+
tests: tests.map(&:to_s)
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def extract_nontrivial_subtrees(node)
|
|
81
|
+
subtrees = []
|
|
82
|
+
# A non-trivial subtree is a node that has children
|
|
83
|
+
subtrees << node unless node.children.empty?
|
|
84
|
+
node.children.each do |child|
|
|
85
|
+
subtrees.concat(extract_nontrivial_subtrees(child))
|
|
86
|
+
end
|
|
87
|
+
subtrees
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def merge_candidates(full_tree, subtree)
|
|
91
|
+
by_structure = {}
|
|
92
|
+
|
|
93
|
+
full_tree.each do |candidate|
|
|
94
|
+
by_structure[candidate[:structure]] = candidate
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
subtree.each do |candidate|
|
|
98
|
+
if by_structure.key?(candidate[:structure])
|
|
99
|
+
existing = by_structure[candidate[:structure]]
|
|
100
|
+
merged_tests = (existing[:tests] + candidate[:tests]).uniq
|
|
101
|
+
frequency = merged_tests.size
|
|
102
|
+
tree_size = candidate[:tree_size]
|
|
103
|
+
by_structure[candidate[:structure]] = {
|
|
104
|
+
structure: candidate[:structure],
|
|
105
|
+
frequency: frequency,
|
|
106
|
+
tree_size: tree_size,
|
|
107
|
+
score: frequency * tree_size,
|
|
108
|
+
tests: merged_tests
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
by_structure[candidate[:structure]] = candidate
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
by_structure.values
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
require_relative "node"
|
|
5
|
+
|
|
6
|
+
module Foundries
|
|
7
|
+
module Recording
|
|
8
|
+
class Collector
|
|
9
|
+
def initialize
|
|
10
|
+
@results = {}
|
|
11
|
+
@total_creates = 0
|
|
12
|
+
@subscriber = nil
|
|
13
|
+
@stack = nil
|
|
14
|
+
@current_root = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def start_test(test_id)
|
|
18
|
+
@current_root = MutableNode.new(:__root__, [])
|
|
19
|
+
@stack = [@current_root]
|
|
20
|
+
|
|
21
|
+
@subscriber = ActiveSupport::Notifications.subscribe(
|
|
22
|
+
"factory_bot.run_factory",
|
|
23
|
+
Subscriber.new(self)
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def stop_test(test_id)
|
|
28
|
+
ActiveSupport::Notifications.unsubscribe(@subscriber)
|
|
29
|
+
@subscriber = nil
|
|
30
|
+
|
|
31
|
+
@results[test_id] = freeze_tree(@current_root)
|
|
32
|
+
@stack = nil
|
|
33
|
+
@current_root = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :results
|
|
37
|
+
|
|
38
|
+
attr_reader :total_creates
|
|
39
|
+
|
|
40
|
+
# @api private — called by the Subscriber
|
|
41
|
+
def handle_start(payload)
|
|
42
|
+
return unless payload[:strategy] == :create
|
|
43
|
+
|
|
44
|
+
traits = payload[:traits] || []
|
|
45
|
+
mutable = MutableNode.new(payload[:name], traits)
|
|
46
|
+
@stack.last.children << mutable
|
|
47
|
+
@stack.push(mutable)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @api private — called by the Subscriber
|
|
51
|
+
def handle_finish(payload)
|
|
52
|
+
return unless payload[:strategy] == :create
|
|
53
|
+
|
|
54
|
+
@stack.pop
|
|
55
|
+
@total_creates += 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def freeze_tree(mutable)
|
|
61
|
+
frozen_children = mutable.children.map { |child| freeze_tree(child) }
|
|
62
|
+
Node.new(
|
|
63
|
+
factory: mutable.factory,
|
|
64
|
+
traits: mutable.traits,
|
|
65
|
+
children: frozen_children
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
MutableNode = Struct.new(:factory, :traits) do
|
|
70
|
+
def children
|
|
71
|
+
@children ||= []
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class Subscriber
|
|
76
|
+
def initialize(collector)
|
|
77
|
+
@collector = collector
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def start(_name, _id, payload)
|
|
81
|
+
@collector.handle_start(payload)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def finish(_name, _id, payload)
|
|
85
|
+
@collector.handle_finish(payload)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foundries
|
|
4
|
+
module Recording
|
|
5
|
+
class Node
|
|
6
|
+
attr_reader :factory, :traits, :children
|
|
7
|
+
|
|
8
|
+
def initialize(factory:, traits: [], children: [])
|
|
9
|
+
@factory = factory.to_sym
|
|
10
|
+
@traits = traits.map(&:to_sym).sort.freeze
|
|
11
|
+
@children = children.freeze
|
|
12
|
+
@self_signature = compute_self_signature
|
|
13
|
+
@signature = compute_signature
|
|
14
|
+
@tree_size = 1 + children.sum(&:tree_size)
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :signature, :self_signature, :tree_size
|
|
19
|
+
|
|
20
|
+
def normalize
|
|
21
|
+
normalized_children = children.map(&:normalize)
|
|
22
|
+
deduped = normalized_children
|
|
23
|
+
.group_by(&:self_signature)
|
|
24
|
+
.map { |_sig, group| group.max_by(&:tree_size) }
|
|
25
|
+
.sort_by(&:signature)
|
|
26
|
+
self.class.new(factory: factory, traits: traits, children: deduped)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{
|
|
31
|
+
factory: factory.to_s,
|
|
32
|
+
traits: traits.map(&:to_s),
|
|
33
|
+
children: children.map(&:to_h)
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def ==(other)
|
|
38
|
+
other.is_a?(self.class) && signature == other.signature
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
alias_method :eql?, :==
|
|
42
|
+
|
|
43
|
+
def hash
|
|
44
|
+
signature.hash
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_s
|
|
48
|
+
signature
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def compute_self_signature
|
|
54
|
+
base = factory.to_s
|
|
55
|
+
base = "#{base}[#{traits.map { |t| ":#{t}" }.join(", ")}]" unless traits.empty?
|
|
56
|
+
base
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def compute_signature
|
|
60
|
+
base = @self_signature
|
|
61
|
+
unless children.empty?
|
|
62
|
+
sorted_children = children.sort_by(&:signature)
|
|
63
|
+
base = "#{base} > [#{sorted_children.map(&:signature).join(", ")}]"
|
|
64
|
+
end
|
|
65
|
+
base
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rake"
|
|
4
|
+
require_relative "../recording"
|
|
5
|
+
|
|
6
|
+
module Foundries
|
|
7
|
+
module Recording
|
|
8
|
+
class RakeTask
|
|
9
|
+
include Rake::DSL
|
|
10
|
+
|
|
11
|
+
def self.install(output_path: Recording::DEFAULT_OUTPUT_PATH)
|
|
12
|
+
new.install_tasks(output_path: output_path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def install_tasks(output_path:)
|
|
16
|
+
namespace :foundries do
|
|
17
|
+
namespace :recording do
|
|
18
|
+
desc "Merge per-worker recording files into a single report"
|
|
19
|
+
task :merge do
|
|
20
|
+
dir = File.dirname(output_path)
|
|
21
|
+
base = File.basename(output_path, File.extname(output_path))
|
|
22
|
+
ext = File.extname(output_path)
|
|
23
|
+
pattern = File.join(dir, "#{base}-*#{ext}")
|
|
24
|
+
files = Dir.glob(pattern).sort
|
|
25
|
+
|
|
26
|
+
if files.empty?
|
|
27
|
+
$stdout.puts "[Foundries] No worker recording files found matching #{pattern}"
|
|
28
|
+
next
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Foundries::Recording::RakeTask.merge(files, output_path: output_path)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.merge(files, output_path:)
|
|
38
|
+
summary = Reporter.merge_files(files, output_path: output_path)
|
|
39
|
+
$stdout.puts summary
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require_relative "aggregator"
|
|
7
|
+
require_relative "node"
|
|
8
|
+
|
|
9
|
+
module Foundries
|
|
10
|
+
module Recording
|
|
11
|
+
class Reporter
|
|
12
|
+
MAX_STDOUT_CANDIDATES = 10
|
|
13
|
+
|
|
14
|
+
def self.merge_files(paths, output_path:)
|
|
15
|
+
combined_per_test = {}
|
|
16
|
+
total_creates = 0
|
|
17
|
+
|
|
18
|
+
paths.each do |path|
|
|
19
|
+
data = JSON.parse(File.read(path))
|
|
20
|
+
total_creates += data["total_factory_creates"]
|
|
21
|
+
data["per_test"].each do |test_id, test_data|
|
|
22
|
+
combined_per_test[test_id] = test_data
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
total_tests = combined_per_test.size
|
|
27
|
+
|
|
28
|
+
results = combined_per_test.transform_values do |test_data|
|
|
29
|
+
children = (test_data["tree"] || []).map { |h| node_from_hash(h) }
|
|
30
|
+
Node.new(factory: :__root__, children: children)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
reporter = new(results: results, total_creates: total_creates, total_tests: total_tests)
|
|
34
|
+
reporter.write_json(output_path)
|
|
35
|
+
reporter.summary(json_path: output_path)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.node_from_hash(hash)
|
|
39
|
+
children = (hash["children"] || []).map { |c| node_from_hash(c) }
|
|
40
|
+
Node.new(
|
|
41
|
+
factory: hash["factory"].to_sym,
|
|
42
|
+
traits: (hash["traits"] || []).map(&:to_sym),
|
|
43
|
+
children: children
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private_class_method :node_from_hash
|
|
48
|
+
|
|
49
|
+
def initialize(results:, total_creates:, total_tests:)
|
|
50
|
+
@results = results
|
|
51
|
+
@total_creates = total_creates
|
|
52
|
+
@total_tests = total_tests
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def candidates
|
|
56
|
+
@candidates ||= Aggregator.new(@results).candidates
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def write_json(path)
|
|
60
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
61
|
+
File.write(path, JSON.pretty_generate(json_data))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def summary(json_path: nil)
|
|
65
|
+
lines = []
|
|
66
|
+
lines << "[Foundries] Recording complete. #{@total_tests} tests, #{@total_creates} factory creates."
|
|
67
|
+
|
|
68
|
+
if candidates.empty?
|
|
69
|
+
lines << "[Foundries] No preset candidates found."
|
|
70
|
+
else
|
|
71
|
+
lines << "[Foundries] Top preset candidates:"
|
|
72
|
+
candidates.first(MAX_STDOUT_CANDIDATES).each_with_index do |candidate, index|
|
|
73
|
+
lines << " #{index + 1}. #{candidate[:structure]} (#{candidate[:frequency]} tests, score: #{candidate[:score]})"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
lines << "[Foundries] Full report: #{json_path}" if json_path
|
|
78
|
+
|
|
79
|
+
lines.join("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def json_data
|
|
85
|
+
{
|
|
86
|
+
recorded_at: Time.now.utc.iso8601,
|
|
87
|
+
total_tests: @total_tests,
|
|
88
|
+
total_factory_creates: @total_creates,
|
|
89
|
+
candidates: candidates.map { |c| stringify_candidate(c) },
|
|
90
|
+
per_test: build_per_test
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def stringify_candidate(candidate)
|
|
95
|
+
{
|
|
96
|
+
structure: candidate[:structure],
|
|
97
|
+
frequency: candidate[:frequency],
|
|
98
|
+
tree_size: candidate[:tree_size],
|
|
99
|
+
score: candidate[:score],
|
|
100
|
+
tests: candidate[:tests]
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_per_test
|
|
105
|
+
@results.each_with_object({}) do |(test_id, root), hash|
|
|
106
|
+
hash[test_id.to_s] = {
|
|
107
|
+
factories_created: root.children.size,
|
|
108
|
+
tree: root.children.map(&:to_h)
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "recording/collector"
|
|
4
|
+
require_relative "recording/reporter"
|
|
5
|
+
|
|
6
|
+
module Foundries
|
|
7
|
+
module Recording
|
|
8
|
+
DEFAULT_OUTPUT_PATH = "tmp/foundries/recording.json"
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
attr_writer :enabled, :output_path
|
|
12
|
+
|
|
13
|
+
def enabled?
|
|
14
|
+
return @enabled unless @enabled.nil?
|
|
15
|
+
ENV["FOUNDRIES_RECORD"] == "1"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def collector
|
|
19
|
+
@collector ||= Collector.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def output_path
|
|
23
|
+
@output_path || DEFAULT_OUTPUT_PATH
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def worker_output_path
|
|
27
|
+
worker_id = ENV["TEST_ENV_NUMBER"]
|
|
28
|
+
return output_path if worker_id.nil?
|
|
29
|
+
|
|
30
|
+
suffix = worker_id.empty? ? Process.pid.to_s : worker_id
|
|
31
|
+
base = output_path
|
|
32
|
+
ext = File.extname(base)
|
|
33
|
+
"#{base.chomp(ext)}-#{suffix}#{ext}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset!
|
|
37
|
+
@collector = nil
|
|
38
|
+
@enabled = nil
|
|
39
|
+
@output_path = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def report!
|
|
43
|
+
total_tests = collector.results.size
|
|
44
|
+
reporter = Reporter.new(
|
|
45
|
+
results: collector.results,
|
|
46
|
+
total_creates: collector.total_creates,
|
|
47
|
+
total_tests: total_tests
|
|
48
|
+
)
|
|
49
|
+
path = worker_output_path
|
|
50
|
+
reporter.write_json(path)
|
|
51
|
+
$stdout.puts reporter.summary(json_path: path)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if Foundries::Recording.enabled? && defined?(RSpec)
|
|
58
|
+
RSpec.configure do |config|
|
|
59
|
+
config.before(:each) do |example|
|
|
60
|
+
Foundries::Recording.collector.start_test(example.full_description)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
config.after(:each) do |example|
|
|
64
|
+
Foundries::Recording.collector.stop_test(example.full_description)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
config.after(:suite) do
|
|
68
|
+
Foundries::Recording.report!
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/foundries/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foundries
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- John Dowd
|
|
@@ -54,6 +54,12 @@ files:
|
|
|
54
54
|
- lib/foundries.rb
|
|
55
55
|
- lib/foundries/base.rb
|
|
56
56
|
- lib/foundries/blueprint.rb
|
|
57
|
+
- lib/foundries/recording.rb
|
|
58
|
+
- lib/foundries/recording/aggregator.rb
|
|
59
|
+
- lib/foundries/recording/collector.rb
|
|
60
|
+
- lib/foundries/recording/node.rb
|
|
61
|
+
- lib/foundries/recording/rake_task.rb
|
|
62
|
+
- lib/foundries/recording/reporter.rb
|
|
57
63
|
- lib/foundries/similarity.rb
|
|
58
64
|
- lib/foundries/similarity/comparator.rb
|
|
59
65
|
- lib/foundries/similarity/recorder.rb
|