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 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