real_data_tests 0.3.0 → 0.3.1

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: 1c5ba3c2d2b4bb4f9ec9a6c539ddd809b6840d3eb7e6cbfb33d02f524177b3ff
4
+ data.tar.gz: e64f18ddd2a0bab9c0f3aad9ab628fb7bd86097644807c5d99596aa3c3af55c4
5
5
  SHA512:
6
- metadata.gz: 30b473701dfd16d6337803a481caf8e358cf87c04877c076a66b2079da57bd68d0896d321c16c2cf1ecab1ce08b5179aba7f9845b0b28d58ecf9e67fb8d9f2b7
7
- data.tar.gz: f0ea756e2384a7d43f5d40a36d018b569fc5abdba092f5635ade2d11ec1bf24a33c1ba219d1d983fbc7a918fbf842bd2a85436a644cc65fa40d2ea0fb07a923d
6
+ metadata.gz: 14a8d12cb5fd01582b2bf22d47d7fa518f2f0be2258e15c477f42c29c1c95b9d362fc20f486fe583f655eeac5b15ceba48c888eb49e9bc47444ab3ffcf79feb6
7
+ data.tar.gz: 1c2a99d52fa1b740c07152bf12d6802711bba8af8af38d94924d66ddda06d2eab76e6f459b532af3c29e2a0767bfd4ee7977aa23293b3a616225a5b340010a89
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.1] - 2025-01-14
4
+ ### Fixed
5
+ - Fixed circular dependency handling in RecordCollector to correctly limit record collection
6
+ - Moved prevention logic earlier in the collection process to stop circular dependencies before record collection
7
+ - Improved tracking of visited associations for more accurate prevention
8
+ - Added better logging for dependency prevention decisions
9
+ - Fixed test case for circular dependency prevention in nested associations
10
+
3
11
  ## [0.3.0] - 2025-01-13
4
12
  ### Added
5
13
  - **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
65
68
 
66
69
  def initialize
67
70
  @association_filter_mode = nil
@@ -70,6 +73,26 @@ 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
+ end
79
+
80
+ def prevent_circular_dependency(klass, association_name)
81
+ key = if klass.is_a?(String)
82
+ "#{klass}:#{association_name}"
83
+ else
84
+ "#{klass.name}:#{association_name}"
85
+ end
86
+ @prevented_reciprocals << key
87
+ end
88
+
89
+ def has_circular_dependency?(klass, association_name)
90
+ key = if klass.is_a?(String)
91
+ "#{klass}:#{association_name}"
92
+ else
93
+ "#{klass.name}:#{association_name}"
94
+ end
95
+ @prevented_reciprocals.include?(key)
73
96
  end
74
97
 
75
98
  def include_associations(*associations)
@@ -109,7 +132,7 @@ module RealDataTests
109
132
 
110
133
  def prevent_reciprocal?(record_class, association_name)
111
134
  path = "#{record_class.name}.#{association_name}"
112
- @prevent_reciprocal_loading[path]
135
+ @prevent_reciprocal_loading[path] || has_circular_dependency?(record_class, association_name)
113
136
  end
114
137
 
115
138
  def prevent_reciprocal(path)
@@ -6,6 +6,10 @@ module RealDataTests
6
6
  @record = record
7
7
  @collected_records = Set.new
8
8
  @collection_stats = {}
9
+ @processed_associations = Set.new
10
+ @association_path = []
11
+ @current_depth = 0
12
+ @visited_associations = {}
9
13
 
10
14
  # Initialize stats for the record's class
11
15
  @collection_stats[record.class.name] = {
@@ -20,9 +24,6 @@ module RealDataTests
20
24
  end
21
25
  end
22
26
 
23
- @processed_associations = Set.new
24
- @association_path = []
25
-
26
27
  puts "\nInitializing RecordCollector for #{record.class.name}##{record.id}"
27
28
  record.class.reflect_on_all_associations(:belongs_to).each do |assoc|
28
29
  if assoc.polymorphic?
@@ -35,45 +36,30 @@ module RealDataTests
35
36
 
36
37
  def collect
37
38
  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
39
+ filter_mode = RealDataTests.configuration.current_preset.association_filter_mode
40
+ filter_list = RealDataTests.configuration.current_preset.association_filter_list
40
41
  puts "Using #{filter_mode || 'no'} filter with #{filter_list.any? ? filter_list.join(', ') : 'no associations'}"
41
- collect_record(@record)
42
+ collect_record(@record, 0)
42
43
  print_collection_stats
43
44
  @collected_records.to_a
44
45
  end
45
46
 
46
47
  private
47
48
 
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}"
55
-
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
61
- end
62
- true
63
- else
64
- false
65
- end
66
- end
67
-
68
- def collect_record(record)
49
+ def collect_record(record, depth)
69
50
  return if @collected_records.include?(record)
70
51
  return unless record # Guard against nil records
52
+ return if depth > RealDataTests.configuration.current_preset.max_depth
71
53
 
72
54
  puts "\nCollecting record: #{record.class.name}##{record.id}"
73
55
  @collected_records.add(record)
74
56
 
75
- # Ensure stats structure is initialized
76
- @collection_stats[record.class.name] ||= { count: 0, associations: {}, polymorphic_types: {} }
57
+ # Initialize stats structure
58
+ @collection_stats[record.class.name] ||= {
59
+ count: 0,
60
+ associations: {},
61
+ polymorphic_types: {}
62
+ }
77
63
  @collection_stats[record.class.name][:count] += 1
78
64
 
79
65
  # Track types for polymorphic belongs_to associations
@@ -91,43 +77,106 @@ module RealDataTests
91
77
  else
92
78
  puts " Skipping polymorphic type for #{assoc.name} due to missing associated record"
93
79
  end
94
- rescue ActiveRecord::RecordNotFound => e
80
+ rescue StandardError => e
95
81
  puts " Error loading polymorphic association #{assoc.name}: #{e.message}"
96
82
  end
97
83
  end
98
84
 
99
- collect_associations(record)
85
+ collect_associations(record, depth)
100
86
  end
101
87
 
102
- def collect_associations(record)
88
+ def collect_associations(record, depth)
103
89
  return unless record.class.respond_to?(:reflect_on_all_associations)
90
+ return if depth >= RealDataTests.configuration.current_preset.max_depth
104
91
 
105
92
  associations = record.class.reflect_on_all_associations
106
93
  puts "\nProcessing associations for: #{record.class.name}##{record.id}"
107
94
  puts "Found #{associations.length} associations"
108
95
 
109
96
  associations.each do |association|
110
- next unless should_process_association?(record, association)
97
+ association_key = "#{record.class.name}##{record.id}:#{association.name}"
98
+ puts " Checking if should process: #{association_key}"
99
+
100
+ if RealDataTests.configuration.current_preset.prevent_reciprocal?(record.class, association.name)
101
+ track_key = "#{record.class.name}:#{association.name}"
102
+ @visited_associations[track_key] ||= Set.new
103
+
104
+ # Skip if we've already processed this association type for this class
105
+ if @visited_associations[track_key].any?
106
+ puts " Skipping prevented reciprocal association: #{track_key}"
107
+ next
108
+ end
109
+ @visited_associations[track_key].add(record.id)
110
+ end
111
+
112
+ next unless should_process_association?(record, association, depth)
111
113
 
112
114
  puts " Processing #{association.macro} #{association.polymorphic? ? 'polymorphic ' : ''}association: #{association.name}"
113
- process_association(record, association)
115
+ process_association(record, association, depth)
116
+ end
117
+ end
118
+
119
+ def should_process_association?(record, association, depth = 0)
120
+ return false if depth >= RealDataTests.configuration.current_preset.max_depth
121
+
122
+ association_key = "#{record.class.name}##{record.id}:#{association.name}"
123
+ return false if @processed_associations.include?(association_key)
124
+
125
+ # Check if the association is allowed by configuration
126
+ should_process = RealDataTests.configuration.current_preset.should_process_association?(record, association.name)
127
+ puts " Configuration says: #{should_process}"
128
+
129
+ if should_process
130
+ @processed_associations.add(association_key)
131
+ true
132
+ else
133
+ false
114
134
  end
115
135
  end
116
136
 
117
- def process_association(record, association)
137
+ def process_association(record, association, depth)
138
+ @association_path.push(association.name)
139
+
118
140
  begin
141
+ if detect_circular_dependency?(record, association)
142
+ puts " Skipping circular dependency for #{association.name} on #{record.class.name}##{record.id}"
143
+ return
144
+ end
145
+
119
146
  related_records = fetch_related_records(record, association)
120
147
  count = related_records.length
121
148
  puts " Found #{count} related #{association.name} records"
149
+
122
150
  @collection_stats[record.class.name][:associations][association.name.to_s] ||= 0
123
151
  @collection_stats[record.class.name][:associations][association.name.to_s] += count
124
152
 
125
- related_records.each { |related_record| collect_record(related_record) }
153
+ related_records.each { |related_record| collect_record(related_record, depth + 1) }
126
154
  rescue => e
127
155
  puts " Error processing association #{association.name}: #{e.message}"
156
+ ensure
157
+ @association_path.pop
128
158
  end
129
159
  end
130
160
 
161
+ def self_referential_association?(klass, association)
162
+ return false unless association.options[:class_name]
163
+ return false if association.polymorphic?
164
+ association.options[:class_name] == klass.name
165
+ end
166
+
167
+ def detect_circular_dependency?(record, association)
168
+ return false unless association.belongs_to?
169
+ return false if association.polymorphic?
170
+
171
+ target_class = association.klass
172
+ return false unless target_class
173
+
174
+ path_key = "#{target_class.name}:#{association.name}"
175
+ visited_count = @association_path.count { |assoc| "#{target_class.name}:#{assoc}" == path_key }
176
+
177
+ visited_count > 1
178
+ end
179
+
131
180
  def fetch_related_records(record, association)
132
181
  case association.macro
133
182
  when :belongs_to, :has_one
@@ -135,14 +184,12 @@ module RealDataTests
135
184
  when :has_many, :has_and_belongs_to_many
136
185
  relation = record.public_send(association.name)
137
186
 
138
- if limit = RealDataTests.configuration.get_association_limit(record.class, association.name)
187
+ if limit = RealDataTests.configuration.current_preset.get_association_limit(record.class, association.name)
139
188
  puts " Applying configured limit of #{limit} records for #{record.class.name}.#{association.name}"
140
- relation = relation.limit(limit)
189
+ relation = relation[0...limit]
141
190
  end
142
191
 
143
- records = relation.to_a
144
- records = records[0...limit] if limit # Ensure in-memory limit as well
145
- records
192
+ relation
146
193
  else
147
194
  []
148
195
  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.1"
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.1
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