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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1db1628bca035d2b5e5f402b39ceaabd5c78b5bf957cd7bcd7856e5819889334
4
- data.tar.gz: 477319961d6795bd6005c8d5819cc7d26ded26ba46704cc3bc6ab2c5e701227e
3
+ metadata.gz: 21f2ee14eda8fe0a048506c0ac7948c0f1c3cacb603ddb9c6490f1abef0e7fda
4
+ data.tar.gz: '0698d429e6feb3f2dfd358014ff71b7cca4415d2393cbab950f99776191ec43c'
5
5
  SHA512:
6
- metadata.gz: 30b473701dfd16d6337803a481caf8e358cf87c04877c076a66b2079da57bd68d0896d321c16c2cf1ecab1ce08b5179aba7f9845b0b28d58ecf9e67fb8d9f2b7
7
- data.tar.gz: f0ea756e2384a7d43f5d40a36d018b569fc5abdba092f5635ade2d11ec1bf24a33c1ba219d1d983fbc7a918fbf842bd2a85436a644cc65fa40d2ea0fb07a923d
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
- puts "\nInitializing RecordCollector for #{record.class.name}##{record.id}"
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 should_process_association?(record, association)
49
- association_key = "#{record.class.name}##{record.id}:#{association.name}"
50
- return false if @processed_associations.include?(association_key)
51
-
52
- puts " Checking if should process: #{association_key}"
53
- should_process = RealDataTests.configuration.should_process_association?(record, association.name)
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
- if should_process
57
- @processed_associations.add(association_key)
58
- if RealDataTests.configuration.prevent_reciprocal?(record.class, association.name)
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
- # Ensure stats structure is initialized
76
- @collection_stats[record.class.name] ||= { count: 0, associations: {}, polymorphic_types: {} }
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 ActiveRecord::RecordNotFound => e
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
- next unless should_process_association?(record, association)
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 process_association(record, association)
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
- related_records.each { |related_record| collect_record(related_record) }
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.limit(limit)
224
+ relation = relation[0...limit]
141
225
  end
142
226
 
143
- records = relation.to_a
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RealDataTests
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.2"
5
5
  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.0
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-13 00:00:00.000000000 Z
11
+ date: 2025-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails