real_data_tests 0.3.0 → 0.3.2
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 +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
|