real_data_tests 0.3.0 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/lib/real_data_tests/configuration.rb +26 -2
- data/lib/real_data_tests/record_collector.rb +140 -58
- data/lib/real_data_tests/version.rb +1 -1
- 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: 21f2ee14eda8fe0a048506c0ac7948c0f1c3cacb603ddb9c6490f1abef0e7fda
|
4
|
+
data.tar.gz: '0698d429e6feb3f2dfd358014ff71b7cca4415d2393cbab950f99776191ec43c'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7839506386159938f4d5764bc0871876d7bb02ebac18ed522f23fc822e137757ea5a8a8dda7c36c4c43a91e48fa44cc0b8d69dfdca6fde402a12fe743c606fc9
|
7
|
+
data.tar.gz: bc74ab0ab3c19d1766714ebe35b25c310360a7b2d0154b675b223b46e0ee8610dd37ada15de1620867895fb922cba7edaf63a3d252e2d86dcaeb070bb9716b5b
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.3.2] - 2025-01-14
|
4
|
+
### Fixed
|
5
|
+
- Enhanced association statistics tracking in RecordCollector
|
6
|
+
- Added separate statistics tracking method to ensure accurate counts
|
7
|
+
- Stats are now tracked before circular dependency checks
|
8
|
+
- Fixed parent-child relationship counting in recursive associations
|
9
|
+
- Improved initialization of statistics structures for better reliability
|
10
|
+
|
11
|
+
## [0.3.1] - 2025-01-14
|
12
|
+
### Fixed
|
13
|
+
- Fixed circular dependency handling in RecordCollector to correctly limit record collection
|
14
|
+
- Moved prevention logic earlier in the collection process to stop circular dependencies before record collection
|
15
|
+
- Improved tracking of visited associations for more accurate prevention
|
16
|
+
- Added better logging for dependency prevention decisions
|
17
|
+
- Fixed test case for circular dependency prevention in nested associations
|
18
|
+
|
3
19
|
## [0.3.0] - 2025-01-13
|
4
20
|
### Added
|
5
21
|
- **Polymorphic Association Support**:
|
@@ -61,7 +61,10 @@ module RealDataTests
|
|
61
61
|
class PresetConfig
|
62
62
|
attr_reader :association_filter_mode, :association_filter_list,
|
63
63
|
:model_specific_associations, :association_limits,
|
64
|
-
:prevent_reciprocal_loading, :anonymization_rules
|
64
|
+
:prevent_reciprocal_loading, :anonymization_rules,
|
65
|
+
:prevented_reciprocals
|
66
|
+
|
67
|
+
attr_accessor :max_depth, :max_self_ref_depth
|
65
68
|
|
66
69
|
def initialize
|
67
70
|
@association_filter_mode = nil
|
@@ -70,6 +73,27 @@ module RealDataTests
|
|
70
73
|
@association_limits = {}
|
71
74
|
@prevent_reciprocal_loading = {}
|
72
75
|
@anonymization_rules = {}
|
76
|
+
@prevented_reciprocals = Set.new
|
77
|
+
@max_depth = 10
|
78
|
+
@max_self_ref_depth = 2
|
79
|
+
end
|
80
|
+
|
81
|
+
def prevent_circular_dependency(klass, association_name)
|
82
|
+
key = if klass.is_a?(String)
|
83
|
+
"#{klass}:#{association_name}"
|
84
|
+
else
|
85
|
+
"#{klass.name}:#{association_name}"
|
86
|
+
end
|
87
|
+
@prevented_reciprocals << key
|
88
|
+
end
|
89
|
+
|
90
|
+
def has_circular_dependency?(klass, association_name)
|
91
|
+
key = if klass.is_a?(String)
|
92
|
+
"#{klass}:#{association_name}"
|
93
|
+
else
|
94
|
+
"#{klass.name}:#{association_name}"
|
95
|
+
end
|
96
|
+
@prevented_reciprocals.include?(key)
|
73
97
|
end
|
74
98
|
|
75
99
|
def include_associations(*associations)
|
@@ -109,7 +133,7 @@ module RealDataTests
|
|
109
133
|
|
110
134
|
def prevent_reciprocal?(record_class, association_name)
|
111
135
|
path = "#{record_class.name}.#{association_name}"
|
112
|
-
@prevent_reciprocal_loading[path]
|
136
|
+
@prevent_reciprocal_loading[path] || has_circular_dependency?(record_class, association_name)
|
113
137
|
end
|
114
138
|
|
115
139
|
def prevent_reciprocal(path)
|
@@ -6,74 +6,55 @@ module RealDataTests
|
|
6
6
|
@record = record
|
7
7
|
@collected_records = Set.new
|
8
8
|
@collection_stats = {}
|
9
|
-
|
10
|
-
# Initialize stats for the record's class
|
11
|
-
@collection_stats[record.class.name] = {
|
12
|
-
count: 0,
|
13
|
-
associations: Hash.new(0),
|
14
|
-
polymorphic_types: {}
|
15
|
-
}
|
16
|
-
|
17
|
-
record.class.reflect_on_all_associations(:belongs_to).each do |assoc|
|
18
|
-
if assoc.polymorphic?
|
19
|
-
@collection_stats[record.class.name][:polymorphic_types][assoc.name.to_s] ||= Set.new
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
9
|
@processed_associations = Set.new
|
24
10
|
@association_path = []
|
11
|
+
@current_depth = 0
|
12
|
+
@visited_associations = {}
|
13
|
+
@processed_self_refs = Hash.new { |h, k| h[k] = Set.new }
|
25
14
|
|
26
|
-
|
27
|
-
record.class.reflect_on_all_associations(:belongs_to).each do |assoc|
|
28
|
-
if assoc.polymorphic?
|
29
|
-
type = record.public_send("#{assoc.name}_type")
|
30
|
-
id = record.public_send("#{assoc.name}_id")
|
31
|
-
puts "Found polymorphic belongs_to '#{assoc.name}' with type: #{type}, id: #{id}"
|
32
|
-
end
|
33
|
-
end
|
15
|
+
init_collection_stats(record)
|
34
16
|
end
|
35
17
|
|
36
18
|
def collect
|
37
19
|
puts "\nStarting record collection from: #{@record.class.name}##{@record.id}"
|
38
|
-
filter_mode = RealDataTests.configuration.association_filter_mode
|
39
|
-
filter_list = RealDataTests.configuration.association_filter_list
|
20
|
+
filter_mode = RealDataTests.configuration.current_preset.association_filter_mode
|
21
|
+
filter_list = RealDataTests.configuration.current_preset.association_filter_list
|
40
22
|
puts "Using #{filter_mode || 'no'} filter with #{filter_list.any? ? filter_list.join(', ') : 'no associations'}"
|
41
|
-
collect_record(@record)
|
23
|
+
collect_record(@record, 0)
|
42
24
|
print_collection_stats
|
43
25
|
@collected_records.to_a
|
44
26
|
end
|
45
27
|
|
46
28
|
private
|
47
29
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
puts " Configuration says: #{should_process}"
|
30
|
+
def init_collection_stats(record)
|
31
|
+
@collection_stats[record.class.name] = {
|
32
|
+
count: 0,
|
33
|
+
associations: Hash.new(0),
|
34
|
+
polymorphic_types: {}
|
35
|
+
}
|
55
36
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
puts " Skipping prevented reciprocal association: #{association.name} on #{record.class.name}"
|
60
|
-
return false
|
37
|
+
record.class.reflect_on_all_associations(:belongs_to).each do |assoc|
|
38
|
+
if assoc.polymorphic?
|
39
|
+
@collection_stats[record.class.name][:polymorphic_types][assoc.name.to_s] ||= Set.new
|
61
40
|
end
|
62
|
-
true
|
63
|
-
else
|
64
|
-
false
|
65
41
|
end
|
66
42
|
end
|
67
43
|
|
68
|
-
def collect_record(record)
|
44
|
+
def collect_record(record, depth)
|
69
45
|
return if @collected_records.include?(record)
|
70
46
|
return unless record # Guard against nil records
|
47
|
+
return if depth > RealDataTests.configuration.current_preset.max_depth
|
71
48
|
|
72
49
|
puts "\nCollecting record: #{record.class.name}##{record.id}"
|
73
50
|
@collected_records.add(record)
|
74
51
|
|
75
|
-
#
|
76
|
-
@collection_stats[record.class.name] ||= {
|
52
|
+
# Initialize stats structure
|
53
|
+
@collection_stats[record.class.name] ||= {
|
54
|
+
count: 0,
|
55
|
+
associations: {},
|
56
|
+
polymorphic_types: {}
|
57
|
+
}
|
77
58
|
@collection_stats[record.class.name][:count] += 1
|
78
59
|
|
79
60
|
# Track types for polymorphic belongs_to associations
|
@@ -91,43 +72,146 @@ module RealDataTests
|
|
91
72
|
else
|
92
73
|
puts " Skipping polymorphic type for #{assoc.name} due to missing associated record"
|
93
74
|
end
|
94
|
-
rescue
|
75
|
+
rescue StandardError => e
|
95
76
|
puts " Error loading polymorphic association #{assoc.name}: #{e.message}"
|
96
77
|
end
|
97
78
|
end
|
98
79
|
|
99
|
-
collect_associations(record)
|
80
|
+
collect_associations(record, depth)
|
100
81
|
end
|
101
82
|
|
102
|
-
def collect_associations(record)
|
83
|
+
def collect_associations(record, depth)
|
103
84
|
return unless record.class.respond_to?(:reflect_on_all_associations)
|
85
|
+
return if depth >= RealDataTests.configuration.current_preset.max_depth
|
104
86
|
|
105
87
|
associations = record.class.reflect_on_all_associations
|
106
88
|
puts "\nProcessing associations for: #{record.class.name}##{record.id}"
|
107
89
|
puts "Found #{associations.length} associations"
|
108
90
|
|
109
91
|
associations.each do |association|
|
110
|
-
|
92
|
+
association_key = "#{record.class.name}##{record.id}:#{association.name}"
|
93
|
+
puts " Checking if should process: #{association_key}"
|
94
|
+
|
95
|
+
if RealDataTests.configuration.current_preset.prevent_reciprocal?(record.class, association.name)
|
96
|
+
track_key = "#{record.class.name}:#{association.name}"
|
97
|
+
@visited_associations[track_key] ||= Set.new
|
98
|
+
|
99
|
+
# Skip if we've already processed this association type for this class
|
100
|
+
if @visited_associations[track_key].any?
|
101
|
+
puts " Skipping prevented reciprocal association: #{track_key}"
|
102
|
+
next
|
103
|
+
end
|
104
|
+
@visited_associations[track_key].add(record.id)
|
105
|
+
end
|
106
|
+
|
107
|
+
next unless should_process_association?(record, association, depth)
|
111
108
|
|
112
109
|
puts " Processing #{association.macro} #{association.polymorphic? ? 'polymorphic ' : ''}association: #{association.name}"
|
113
|
-
process_association(record, association)
|
110
|
+
process_association(record, association, depth)
|
114
111
|
end
|
115
112
|
end
|
116
113
|
|
117
|
-
def
|
114
|
+
def should_process_association?(record, association, depth = 0)
|
115
|
+
return false if depth >= RealDataTests.configuration.current_preset.max_depth
|
116
|
+
|
117
|
+
# Handle self-referential associations
|
118
|
+
if self_referential_association?(record.class, association)
|
119
|
+
track_key = "#{record.class.name}:#{association.name}"
|
120
|
+
return false if @processed_self_refs[track_key].include?(record.id)
|
121
|
+
@processed_self_refs[track_key].add(record.id)
|
122
|
+
end
|
123
|
+
|
124
|
+
association_key = "#{record.class.name}##{record.id}:#{association.name}"
|
125
|
+
return false if @processed_associations.include?(association_key)
|
126
|
+
|
127
|
+
# Check if the association is allowed by configuration
|
128
|
+
should_process = RealDataTests.configuration.current_preset.should_process_association?(record, association.name)
|
129
|
+
|
130
|
+
if should_process
|
131
|
+
@processed_associations.add(association_key)
|
132
|
+
true
|
133
|
+
else
|
134
|
+
false
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def process_association(record, association, depth)
|
139
|
+
@association_path.push(association.name)
|
140
|
+
|
118
141
|
begin
|
119
142
|
related_records = fetch_related_records(record, association)
|
120
143
|
count = related_records.length
|
121
|
-
puts " Found #{count} related #{association.name} records"
|
122
|
-
@collection_stats[record.class.name][:associations][association.name.to_s] ||= 0
|
123
|
-
@collection_stats[record.class.name][:associations][association.name.to_s] += count
|
124
144
|
|
125
|
-
|
145
|
+
# Track statistics even if we're going to skip processing
|
146
|
+
track_association_stats(record.class.name, association.name, count)
|
147
|
+
|
148
|
+
# Check for circular dependency after getting the related records
|
149
|
+
if detect_circular_dependency?(record, association)
|
150
|
+
puts " Skipping circular dependency for #{association.name} on #{record.class.name}##{record.id}"
|
151
|
+
return
|
152
|
+
end
|
153
|
+
|
154
|
+
# For self-referential associations, check depth
|
155
|
+
if self_referential_association?(record.class, association)
|
156
|
+
max_self_ref_depth = 2 # Default max depth for self-referential associations
|
157
|
+
if depth >= max_self_ref_depth
|
158
|
+
puts " Reached max self-referential depth for #{association.name}"
|
159
|
+
return
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
related_records.each { |related_record| collect_record(related_record, depth + 1) }
|
126
164
|
rescue => e
|
127
165
|
puts " Error processing association #{association.name}: #{e.message}"
|
166
|
+
ensure
|
167
|
+
@association_path.pop
|
128
168
|
end
|
129
169
|
end
|
130
170
|
|
171
|
+
def track_association_stats(class_name, association_name, count)
|
172
|
+
# Initialize stats for this class if not already done
|
173
|
+
@collection_stats[class_name] ||= {
|
174
|
+
count: 0,
|
175
|
+
associations: Hash.new(0),
|
176
|
+
polymorphic_types: {}
|
177
|
+
}
|
178
|
+
|
179
|
+
# Update the association count
|
180
|
+
@collection_stats[class_name][:associations][association_name.to_s] ||= 0
|
181
|
+
@collection_stats[class_name][:associations][association_name.to_s] += count
|
182
|
+
end
|
183
|
+
|
184
|
+
def self_referential_association?(klass, association)
|
185
|
+
return false unless association.options[:class_name]
|
186
|
+
return false if association.polymorphic?
|
187
|
+
|
188
|
+
target_class_name = if association.options[:class_name].is_a?(String)
|
189
|
+
association.options[:class_name]
|
190
|
+
else
|
191
|
+
association.options[:class_name].name
|
192
|
+
end
|
193
|
+
|
194
|
+
klass.name == target_class_name
|
195
|
+
end
|
196
|
+
|
197
|
+
def detect_circular_dependency?(record, association)
|
198
|
+
return false unless association.belongs_to?
|
199
|
+
return false if association.polymorphic?
|
200
|
+
|
201
|
+
target_class = association.klass
|
202
|
+
return false unless target_class
|
203
|
+
|
204
|
+
if self_referential_association?(record.class, association)
|
205
|
+
track_key = "#{target_class.name}:#{association.name}"
|
206
|
+
return @processed_self_refs[track_key].include?(record.id)
|
207
|
+
end
|
208
|
+
|
209
|
+
path_key = "#{target_class.name}:#{association.name}"
|
210
|
+
visited_count = @association_path.count { |assoc| "#{target_class.name}:#{assoc}" == path_key }
|
211
|
+
|
212
|
+
visited_count > 1
|
213
|
+
end
|
214
|
+
|
131
215
|
def fetch_related_records(record, association)
|
132
216
|
case association.macro
|
133
217
|
when :belongs_to, :has_one
|
@@ -135,14 +219,12 @@ module RealDataTests
|
|
135
219
|
when :has_many, :has_and_belongs_to_many
|
136
220
|
relation = record.public_send(association.name)
|
137
221
|
|
138
|
-
if limit = RealDataTests.configuration.get_association_limit(record.class, association.name)
|
222
|
+
if limit = RealDataTests.configuration.current_preset.get_association_limit(record.class, association.name)
|
139
223
|
puts " Applying configured limit of #{limit} records for #{record.class.name}.#{association.name}"
|
140
|
-
relation = relation
|
224
|
+
relation = relation[0...limit]
|
141
225
|
end
|
142
226
|
|
143
|
-
|
144
|
-
records = records[0...limit] if limit # Ensure in-memory limit as well
|
145
|
-
records
|
227
|
+
relation
|
146
228
|
else
|
147
229
|
[]
|
148
230
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: real_data_tests
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Dias
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-01-
|
11
|
+
date: 2025-01-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|