familia 2.0.0.pre6 → 2.0.0.pre7
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/.github/workflows/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -13
- data/Gemfile +2 -2
- data/Gemfile.lock +2 -2
- data/docs/wiki/Feature-System-Guide.md +36 -5
- data/docs/wiki/Home.md +30 -20
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type.rb +7 -4
- data/lib/familia/features/encrypted_fields/concealed_string.rb +21 -23
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +1 -1
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +3 -1
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encryption_fields/concealed_string_core_try.rb +3 -0
- data/try/features/encryption_fields/context_isolation_try.rb +1 -0
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/helpers/test_helpers.rb +1 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +2 -0
- data/try/horreum/relations_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +32 -4
- data/docs/wiki/RelatableObjects-Guide.md +0 -563
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,324 @@
|
|
1
|
+
# Validation testing for performance analysis and optimization detection
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
require_relative '../../lib/familia/validation'
|
5
|
+
|
6
|
+
extend Familia::Validation::TestHelpers
|
7
|
+
|
8
|
+
# Initialize validation framework
|
9
|
+
setup_validation_test
|
10
|
+
|
11
|
+
# Test models for performance validation
|
12
|
+
class PerformanceTestUser < Familia::Horreum
|
13
|
+
identifier_field :user_id
|
14
|
+
field :user_id
|
15
|
+
field :name
|
16
|
+
field :email
|
17
|
+
field :status
|
18
|
+
list :activities
|
19
|
+
set :tags
|
20
|
+
zset :scores
|
21
|
+
end
|
22
|
+
|
23
|
+
class PerformanceTestPost < Familia::Horreum
|
24
|
+
identifier_field :post_id
|
25
|
+
field :post_id
|
26
|
+
field :title
|
27
|
+
field :content
|
28
|
+
field :author_id
|
29
|
+
|
30
|
+
def load_with_author
|
31
|
+
# Inefficient - loads author separately (N+1 pattern)
|
32
|
+
author = PerformanceTestUser.new(user_id: author_id)
|
33
|
+
author.refresh!
|
34
|
+
{ post: self, author: author }
|
35
|
+
end
|
36
|
+
|
37
|
+
def efficient_load_with_author
|
38
|
+
# More efficient - could batch this operation
|
39
|
+
author_data = Familia.dbclient.hgetall("performancetestuser:#{author_id}:object")
|
40
|
+
author = PerformanceTestUser.new(user_id: author_id)
|
41
|
+
author_data.each { |k, v| author.send("#{k}=", v) if author.respond_to?("#{k}=") }
|
42
|
+
{ post: self, author: author }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
extend Familia::Validation::TestHelpers
|
47
|
+
setup_validation_test
|
48
|
+
|
49
|
+
## Clean up existing test data
|
50
|
+
cleanup_keys = Familia.dbclient.keys("performancetest*")
|
51
|
+
Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
|
52
|
+
true
|
53
|
+
#=> true
|
54
|
+
|
55
|
+
## Performance analysis captures timing information
|
56
|
+
validator = Familia::Validation::Validator.new(performance_tracking: true)
|
57
|
+
commands = validator.capture_redis_commands do
|
58
|
+
user = PerformanceTestUser.new(user_id: "perf1", name: "Performance Test", email: "perf@test.com")
|
59
|
+
user.save
|
60
|
+
user.activities.unshift("activity1", "activity2", "activity3")
|
61
|
+
user.tags.add("tag1", "tag2")
|
62
|
+
user.scores.add(100, "score1")
|
63
|
+
end
|
64
|
+
|
65
|
+
analysis = validator.analyze_performance(commands)
|
66
|
+
analysis[:total_commands] > 0 && analysis[:total_duration_ms] >= 0
|
67
|
+
#=> true
|
68
|
+
|
69
|
+
## Performance analysis detects command type breakdown
|
70
|
+
validator = Familia::Validation::Validator.new(performance_tracking: true)
|
71
|
+
commands = validator.capture_redis_commands do
|
72
|
+
user = PerformanceTestUser.new(user_id: "perf2", name: "Test User")
|
73
|
+
user.save # Hash operations
|
74
|
+
user.activities.unshift("item") # List operation
|
75
|
+
user.tags.add("tag") # Set operation
|
76
|
+
user.scores.add(50, "score") # Sorted set operation
|
77
|
+
end
|
78
|
+
|
79
|
+
analysis = validator.analyze_performance(commands)
|
80
|
+
breakdown = analysis[:command_type_breakdown]
|
81
|
+
breakdown.keys.length > 1 # Should have multiple command types
|
82
|
+
#=> true
|
83
|
+
|
84
|
+
## assert_performance_within helper works
|
85
|
+
result = assert_performance_within(1000) do # 1 second limit
|
86
|
+
user = PerformanceTestUser.new(user_id: "perf3", name: "Speed Test")
|
87
|
+
user.save
|
88
|
+
end
|
89
|
+
result
|
90
|
+
#=> true
|
91
|
+
|
92
|
+
## assert_efficient_commands detects N+1 patterns
|
93
|
+
posts_data = (1..5).map do |i|
|
94
|
+
PerformanceTestPost.new(post_id: "post#{i}", title: "Post #{i}",
|
95
|
+
content: "Content #{i}", author_id: "user#{i}")
|
96
|
+
end
|
97
|
+
|
98
|
+
# Create users first
|
99
|
+
(1..5).each do |i|
|
100
|
+
user = PerformanceTestUser.new(user_id: "user#{i}", name: "User #{i}")
|
101
|
+
user.save
|
102
|
+
end
|
103
|
+
|
104
|
+
# Save posts
|
105
|
+
posts_data.each(&:save)
|
106
|
+
|
107
|
+
# This should trigger N+1 detection (multiple separate HGETALL calls)
|
108
|
+
begin
|
109
|
+
assert_efficient_commands do
|
110
|
+
posts_data.each do |post|
|
111
|
+
post.load_with_author # Separate query for each author
|
112
|
+
end
|
113
|
+
end
|
114
|
+
false # Should not reach here
|
115
|
+
rescue Familia::Validation::ValidationError
|
116
|
+
true # Expected - N+1 pattern detected
|
117
|
+
end
|
118
|
+
#=> true
|
119
|
+
|
120
|
+
## Efficient commands pass validation
|
121
|
+
assert_efficient_commands do
|
122
|
+
# Single user operation - no N+1 pattern
|
123
|
+
user = PerformanceTestUser.new(user_id: "efficient1", name: "Efficient User")
|
124
|
+
user.save
|
125
|
+
user.activities.unshift("single_activity")
|
126
|
+
end
|
127
|
+
#=> true
|
128
|
+
|
129
|
+
## Performance metrics include slowest commands
|
130
|
+
validator = Familia::Validation::Validator.new(performance_tracking: true)
|
131
|
+
commands = validator.capture_redis_commands do
|
132
|
+
# Create operations of varying complexity
|
133
|
+
user = PerformanceTestUser.new(user_id: "slow_test", name: "Slow Test")
|
134
|
+
user.save
|
135
|
+
|
136
|
+
# Multiple list operations (potentially slower)
|
137
|
+
(1..10).each { |i| user.activities.unshift("item#{i}") }
|
138
|
+
end
|
139
|
+
|
140
|
+
analysis = validator.analyze_performance(commands)
|
141
|
+
analysis[:slowest_commands].length > 0
|
142
|
+
#=> true
|
143
|
+
|
144
|
+
## Transaction efficiency is calculated correctly
|
145
|
+
validator = Familia::Validation::Validator.new(performance_tracking: true)
|
146
|
+
commands = validator.capture_redis_commands do
|
147
|
+
user = PerformanceTestUser.new(user_id: "tx_test", name: "Transaction Test")
|
148
|
+
user.save
|
149
|
+
|
150
|
+
# Use transaction for batch operations
|
151
|
+
Familia.transaction do |conn|
|
152
|
+
conn.hset(user.dbkey, "status", "active")
|
153
|
+
conn.hset(user.dbkey, "email", "tx@test.com")
|
154
|
+
conn.lpush("#{user.dbkey.sub(':object', ':activities')}", "tx_activity")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
analysis = validator.analyze_performance(commands)
|
159
|
+
efficiency = analysis[:transaction_efficiency]
|
160
|
+
efficiency[:score] > 0 && efficiency[:details].include?("transaction")
|
161
|
+
#=> true
|
162
|
+
|
163
|
+
## Empty command sequence analysis handles gracefully
|
164
|
+
empty_commands = Familia::Validation::CommandRecorder::CommandSequence.new
|
165
|
+
analysis = Familia::Validation::PerformanceAnalyzer.new(empty_commands).analyze
|
166
|
+
analysis[:total_commands] == 0 && analysis[:efficiency_score] >= 0
|
167
|
+
#=> true
|
168
|
+
|
169
|
+
## Performance analysis identifies potential batching opportunities
|
170
|
+
validator = Familia::Validation::Validator.new(performance_tracking: true)
|
171
|
+
commands = validator.capture_redis_commands do
|
172
|
+
# Multiple similar operations that could be batched
|
173
|
+
user = PerformanceTestUser.new(user_id: "batch_test")
|
174
|
+
user.save
|
175
|
+
|
176
|
+
# Multiple separate HSET operations - could be HMSET
|
177
|
+
(1..5).each do |i|
|
178
|
+
Familia.dbclient.hset(user.dbkey, "field#{i}", "value#{i}")
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
analysis = validator.analyze_performance(commands)
|
183
|
+
n_plus_one_patterns = analysis[:potential_n_plus_one]
|
184
|
+
n_plus_one_patterns.any? { |pattern| pattern[:command] == "HSET" }
|
185
|
+
#=> true
|
186
|
+
|
187
|
+
## Command sequence metadata is accurate
|
188
|
+
validator = Familia::Validation::Validator.new
|
189
|
+
commands = validator.capture_redis_commands do
|
190
|
+
user = PerformanceTestUser.new(user_id: "meta_test", name: "Metadata Test")
|
191
|
+
user.save
|
192
|
+
user.activities.unshift("test_activity")
|
193
|
+
|
194
|
+
Familia.transaction do |conn|
|
195
|
+
conn.hset(user.dbkey, "status", "verified")
|
196
|
+
conn.incr("meta_counter")
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
[
|
201
|
+
commands&.command_count.to_i > 0,
|
202
|
+
commands&.transaction_count.to_i == 1,
|
203
|
+
commands&.transaction_blocks&.first&.command_count.to_i == 2
|
204
|
+
]
|
205
|
+
#=> [true, true, true]
|
206
|
+
|
207
|
+
## Recorded commands contain proper context information
|
208
|
+
validator = Familia::Validation::Validator.new
|
209
|
+
commands = validator.capture_redis_commands do
|
210
|
+
user = PerformanceTestUser.new(user_id: "context_test")
|
211
|
+
user.save
|
212
|
+
|
213
|
+
Familia.transaction do |conn|
|
214
|
+
conn.hset(user.dbkey, "test_field", "test_value")
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
transaction_commands = commands.commands.select(&:atomic_command?)
|
219
|
+
non_transaction_commands = commands.commands.reject(&:atomic_command?)
|
220
|
+
|
221
|
+
transaction_commands.any? && non_transaction_commands.any?
|
222
|
+
#=> true
|
223
|
+
|
224
|
+
## Performance thresholds can be customized
|
225
|
+
begin
|
226
|
+
assert_performance_within(1) do # Very tight 1ms limit
|
227
|
+
# This will likely exceed 1ms
|
228
|
+
user = PerformanceTestUser.new(user_id: "timeout_test", name: "Timeout Test")
|
229
|
+
user.save
|
230
|
+
(1..10).each { |i| user.activities.unshift("item#{i}") }
|
231
|
+
end
|
232
|
+
false # Should not reach here
|
233
|
+
rescue Familia::Validation::ValidationError => e
|
234
|
+
e.message.include?("took") && e.message.include?("expected less than")
|
235
|
+
end
|
236
|
+
#=> true
|
237
|
+
|
238
|
+
## Efficiency score calculation includes multiple factors
|
239
|
+
validator = Familia::Validation::Validator.new(performance_tracking: true)
|
240
|
+
|
241
|
+
# Inefficient pattern - lots of individual operations
|
242
|
+
commands_inefficient = validator.capture_redis_commands do
|
243
|
+
user = PerformanceTestUser.new(user_id: "inefficient", name: "Inefficient")
|
244
|
+
user.save
|
245
|
+
|
246
|
+
# Many individual operations instead of batching
|
247
|
+
(1..20).each do |i|
|
248
|
+
Familia.dbclient.hset("test:key#{i}", "field", "value#{i}")
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Efficient pattern - using transactions
|
253
|
+
commands_efficient = validator.capture_redis_commands do
|
254
|
+
user = PerformanceTestUser.new(user_id: "efficient", name: "Efficient")
|
255
|
+
user.save
|
256
|
+
|
257
|
+
Familia.transaction do |conn|
|
258
|
+
(1..5).each do |i|
|
259
|
+
conn.hset(user.dbkey, "field#{i}", "value#{i}")
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
analysis_inefficient = validator.analyze_performance(commands_inefficient)
|
265
|
+
analysis_efficient = validator.analyze_performance(commands_efficient)
|
266
|
+
|
267
|
+
analysis_efficient[:efficiency_score] > analysis_inefficient[:efficiency_score]
|
268
|
+
#=> true
|
269
|
+
|
270
|
+
## Command duration tracking works
|
271
|
+
validator = Familia::Validation::Validator.new
|
272
|
+
commands = validator.capture_redis_commands do
|
273
|
+
user = PerformanceTestUser.new(user_id: "duration_test", name: "Duration Test")
|
274
|
+
user.save
|
275
|
+
end
|
276
|
+
|
277
|
+
first_command = commands&.commands&.first
|
278
|
+
first_command&.duration_us.to_i >= 0 && first_command&.timestamp.is_a?(Time)
|
279
|
+
#=> true
|
280
|
+
|
281
|
+
## Transaction boundaries are properly detected in complex scenarios
|
282
|
+
validator = Familia::Validation::Validator.new
|
283
|
+
commands = validator.capture_redis_commands do
|
284
|
+
user1 = PerformanceTestUser.new(user_id: "complex1", name: "Complex 1")
|
285
|
+
user2 = PerformanceTestUser.new(user_id: "complex2", name: "Complex 2")
|
286
|
+
|
287
|
+
# Mixed atomic and non-atomic operations
|
288
|
+
user1.save # Non-atomic
|
289
|
+
user2.save # Non-atomic
|
290
|
+
|
291
|
+
# Atomic batch update
|
292
|
+
Familia.transaction do |conn|
|
293
|
+
conn.hset(user1.dbkey, "status", "active")
|
294
|
+
conn.hset(user2.dbkey, "status", "active")
|
295
|
+
conn.incr("active_user_count")
|
296
|
+
end
|
297
|
+
|
298
|
+
# More non-atomic operations
|
299
|
+
user1.activities.unshift("logged_in")
|
300
|
+
user2.activities.unshift("logged_in")
|
301
|
+
end
|
302
|
+
|
303
|
+
atomic_commands = commands.commands.select(&:atomic_command?)
|
304
|
+
non_atomic_commands = commands.commands.reject(&:atomic_command?)
|
305
|
+
|
306
|
+
[
|
307
|
+
commands&.transaction_count.to_i == 1,
|
308
|
+
commands&.transaction_blocks&.first&.command_count.to_i == 3,
|
309
|
+
atomic_commands&.length.to_i == 3,
|
310
|
+
non_atomic_commands&.length.to_i > 3
|
311
|
+
]
|
312
|
+
#=> [true, true, true, true]
|
313
|
+
|
314
|
+
## Cleanup test environment
|
315
|
+
teardown_validation_test
|
316
|
+
|
317
|
+
## Clean up test data
|
318
|
+
cleanup_keys = Familia.dbclient.keys("performancetest*")
|
319
|
+
cleanup_keys.concat(Familia.dbclient.keys("test:key*"))
|
320
|
+
cleanup_keys.concat(Familia.dbclient.keys("meta_counter"))
|
321
|
+
cleanup_keys.concat(Familia.dbclient.keys("active_user_count"))
|
322
|
+
Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
|
323
|
+
true
|
324
|
+
#=> true
|
@@ -0,0 +1,390 @@
|
|
1
|
+
# Real-world validation scenarios testing complex application workflows
|
2
|
+
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
require_relative '../../lib/familia/validation'
|
5
|
+
|
6
|
+
# Real-world test models that demonstrate complex validation scenarios
|
7
|
+
class User < Familia::Horreum
|
8
|
+
identifier_field :user_id
|
9
|
+
field :user_id
|
10
|
+
field :username
|
11
|
+
field :email
|
12
|
+
field :created_at
|
13
|
+
field :last_login
|
14
|
+
field :status
|
15
|
+
|
16
|
+
list :activity_log
|
17
|
+
set :permissions
|
18
|
+
zset :scores
|
19
|
+
hashkey :preferences
|
20
|
+
end
|
21
|
+
|
22
|
+
class BlogPost < Familia::Horreum
|
23
|
+
identifier_field :post_id
|
24
|
+
field :post_id
|
25
|
+
field :title
|
26
|
+
field :content
|
27
|
+
field :author_id
|
28
|
+
field :published_at
|
29
|
+
field :status
|
30
|
+
|
31
|
+
list :comments
|
32
|
+
set :tags
|
33
|
+
zset :ratings
|
34
|
+
end
|
35
|
+
|
36
|
+
class Session < Familia::Horreum
|
37
|
+
identifier_field :session_id
|
38
|
+
field :session_id
|
39
|
+
field :user_id
|
40
|
+
field :created_at
|
41
|
+
field :expires_at
|
42
|
+
field :ip_address
|
43
|
+
end
|
44
|
+
|
45
|
+
# Service classes that perform complex operations
|
46
|
+
class UserService
|
47
|
+
def self.create_user_with_profile(user_data)
|
48
|
+
# Should be atomic - user creation + initial setup
|
49
|
+
user = User.new(user_data)
|
50
|
+
|
51
|
+
Familia.transaction do |conn|
|
52
|
+
# Save user data
|
53
|
+
user_data.each do |field, value|
|
54
|
+
conn.hset(user.dbkey, field.to_s, value.to_s)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Set default permissions
|
58
|
+
conn.sadd("#{user.dbkey.sub(':object', ':permissions')}", "read")
|
59
|
+
|
60
|
+
# Initialize empty activity log
|
61
|
+
conn.lpush("#{user.dbkey.sub(':object', ':activity_log')}", "account_created")
|
62
|
+
|
63
|
+
# Set default score
|
64
|
+
conn.zadd("#{user.dbkey.sub(':object', ':scores')}", 0, "reputation")
|
65
|
+
end
|
66
|
+
|
67
|
+
user
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.login_user(user_id, session_data)
|
71
|
+
user = User.new(user_id: user_id)
|
72
|
+
session = Session.new(session_data)
|
73
|
+
|
74
|
+
# Should be atomic - update last login + create session
|
75
|
+
Familia.transaction do |conn|
|
76
|
+
# Update user last login
|
77
|
+
conn.hset(user.dbkey, "last_login", Time.now.to_i.to_s)
|
78
|
+
|
79
|
+
# Create session
|
80
|
+
session_data.each do |field, value|
|
81
|
+
conn.hset(session.dbkey, field.to_s, value.to_s)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Log activity
|
85
|
+
conn.lpush("#{user.dbkey.sub(':object', ':activity_log')}", "logged_in:#{Time.now.to_i}")
|
86
|
+
end
|
87
|
+
|
88
|
+
{ user: user, session: session }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class BlogService
|
93
|
+
def self.publish_post(post_data, author_id)
|
94
|
+
post = BlogPost.new(post_data.merge(author_id: author_id))
|
95
|
+
|
96
|
+
# Should be atomic - create post + update author stats
|
97
|
+
user = User.new(user_id: author_id)
|
98
|
+
|
99
|
+
Familia.transaction do |conn|
|
100
|
+
# Save post
|
101
|
+
post_data.each do |field, value|
|
102
|
+
conn.hset(post.dbkey, field.to_s, value.to_s)
|
103
|
+
end
|
104
|
+
conn.hset(post.dbkey, "author_id", author_id)
|
105
|
+
conn.hset(post.dbkey, "published_at", Time.now.to_i.to_s)
|
106
|
+
conn.hset(post.dbkey, "status", "published")
|
107
|
+
|
108
|
+
# Update author score
|
109
|
+
conn.zincrby("#{user.dbkey.sub(':object', ':scores')}", 10, "posts_published")
|
110
|
+
|
111
|
+
# Log activity
|
112
|
+
conn.lpush("#{user.dbkey.sub(':object', ':activity_log')}", "published_post:#{post.post_id}")
|
113
|
+
end
|
114
|
+
|
115
|
+
post
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
extend Familia::Validation::TestHelpers
|
120
|
+
setup_validation_test
|
121
|
+
|
122
|
+
## Clean up existing test data
|
123
|
+
cleanup_keys = Familia.dbclient.keys("user:*")
|
124
|
+
cleanup_keys.concat(Familia.dbclient.keys("blogpost:*"))
|
125
|
+
cleanup_keys.concat(Familia.dbclient.keys("session:*"))
|
126
|
+
Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
|
127
|
+
true
|
128
|
+
#=> true
|
129
|
+
|
130
|
+
## Real-world user creation scenario validates correctly
|
131
|
+
assert_atomic_operation do |expect|
|
132
|
+
expect.transaction do |tx|
|
133
|
+
tx.hset("user:john123:object", "user_id", "john123")
|
134
|
+
.hset("user:john123:object", "username", "john_doe")
|
135
|
+
.hset("user:john123:object", "email", "john@example.com")
|
136
|
+
.sadd("user:john123:permissions", "read")
|
137
|
+
.lpush("user:john123:activity_log", "account_created")
|
138
|
+
.zadd("user:john123:scores", "0", "reputation")
|
139
|
+
end
|
140
|
+
|
141
|
+
UserService.create_user_with_profile({
|
142
|
+
user_id: "john123",
|
143
|
+
username: "john_doe",
|
144
|
+
email: "john@example.com"
|
145
|
+
})
|
146
|
+
end
|
147
|
+
#=> true
|
148
|
+
|
149
|
+
## User login scenario with session creation validates atomically
|
150
|
+
user_data = { user_id: "alice456", username: "alice", email: "alice@example.com" }
|
151
|
+
existing_user = UserService.create_user_with_profile(user_data)
|
152
|
+
|
153
|
+
assert_atomic_operation do |expect|
|
154
|
+
expect.transaction do |tx|
|
155
|
+
tx.hset("user:alice456:object", "last_login", any_string)
|
156
|
+
.hset("session:sess789:object", "session_id", "sess789")
|
157
|
+
.hset("session:sess789:object", "user_id", "alice456")
|
158
|
+
.hset("session:sess789:object", "ip_address", "192.168.1.1")
|
159
|
+
.lpush("user:alice456:activity_log", match_regex(/logged_in:/))
|
160
|
+
end
|
161
|
+
|
162
|
+
UserService.login_user("alice456", {
|
163
|
+
session_id: "sess789",
|
164
|
+
user_id: "alice456",
|
165
|
+
ip_address: "192.168.1.1"
|
166
|
+
})
|
167
|
+
end
|
168
|
+
#=> true
|
169
|
+
|
170
|
+
## Blog post publishing with author score update validates correctly
|
171
|
+
# Create author first
|
172
|
+
author = UserService.create_user_with_profile({
|
173
|
+
user_id: "writer1",
|
174
|
+
username: "writer",
|
175
|
+
email: "writer@blog.com"
|
176
|
+
})
|
177
|
+
|
178
|
+
assert_atomic_operation do |expect|
|
179
|
+
expect.transaction do |tx|
|
180
|
+
tx.hset("blogpost:post001:object", "post_id", "post001")
|
181
|
+
.hset("blogpost:post001:object", "title", "My First Post")
|
182
|
+
.hset("blogpost:post001:object", "content", "Hello world!")
|
183
|
+
.hset("blogpost:post001:object", "author_id", "writer1")
|
184
|
+
.hset("blogpost:post001:object", "published_at", any_string)
|
185
|
+
.hset("blogpost:post001:object", "status", "published")
|
186
|
+
.zincrby("user:writer1:scores", "10", "posts_published")
|
187
|
+
.lpush("user:writer1:activity_log", "published_post:post001")
|
188
|
+
end
|
189
|
+
|
190
|
+
BlogService.publish_post({
|
191
|
+
post_id: "post001",
|
192
|
+
title: "My First Post",
|
193
|
+
content: "Hello world!"
|
194
|
+
}, "writer1")
|
195
|
+
end
|
196
|
+
#=> true
|
197
|
+
|
198
|
+
## Complex multi-object operation validates with proper expectations
|
199
|
+
assert_redis_commands do |expect|
|
200
|
+
expect.strict_order(false) # Allow flexible order for complex operations
|
201
|
+
.transaction do |tx|
|
202
|
+
tx.hset("user:batch1:object", "user_id", "batch1")
|
203
|
+
.hset("user:batch1:object", "username", "batcher")
|
204
|
+
.sadd("user:batch1:permissions", "read")
|
205
|
+
.lpush("user:batch1:activity_log", "account_created")
|
206
|
+
.zadd("user:batch1:scores", "0", "reputation")
|
207
|
+
end
|
208
|
+
.transaction do |tx|
|
209
|
+
tx.hset("blogpost:batch_post:object", "post_id", "batch_post")
|
210
|
+
.hset("blogpost:batch_post:object", "title", "Batch Post")
|
211
|
+
.hset("blogpost:batch_post:object", "author_id", "batch1")
|
212
|
+
.hset("blogpost:batch_post:object", "published_at", any_string)
|
213
|
+
.hset("blogpost:batch_post:object", "status", "published")
|
214
|
+
.zincrby("user:batch1:scores", "10", "posts_published")
|
215
|
+
.lpush("user:batch1:activity_log", "published_post:batch_post")
|
216
|
+
end
|
217
|
+
|
218
|
+
# Execute complex workflow
|
219
|
+
user = UserService.create_user_with_profile({
|
220
|
+
user_id: "batch1",
|
221
|
+
username: "batcher"
|
222
|
+
})
|
223
|
+
|
224
|
+
BlogService.publish_post({
|
225
|
+
post_id: "batch_post",
|
226
|
+
title: "Batch Post"
|
227
|
+
}, "batch1")
|
228
|
+
end
|
229
|
+
#=> true
|
230
|
+
|
231
|
+
## Performance analysis on real-world operations shows efficiency
|
232
|
+
validator = Familia::Validation::Validator.new(performance_tracking: true)
|
233
|
+
commands = validator.capture_redis_commands do
|
234
|
+
# Simulate real application load
|
235
|
+
users = []
|
236
|
+
posts = []
|
237
|
+
|
238
|
+
# Create multiple users efficiently
|
239
|
+
(1..3).each do |i|
|
240
|
+
users << UserService.create_user_with_profile({
|
241
|
+
user_id: "perf_user#{i}",
|
242
|
+
username: "perfuser#{i}",
|
243
|
+
email: "perf#{i}@example.com"
|
244
|
+
})
|
245
|
+
end
|
246
|
+
|
247
|
+
# Each user publishes a post
|
248
|
+
users.each_with_index do |user, i|
|
249
|
+
posts << BlogService.publish_post({
|
250
|
+
post_id: "perf_post#{i + 1}",
|
251
|
+
title: "Performance Post #{i + 1}",
|
252
|
+
content: "Content for performance testing"
|
253
|
+
}, user.user_id)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
analysis = validator.analyze_performance(commands)
|
258
|
+
[
|
259
|
+
analysis[:total_commands] > 10, # Should have many commands
|
260
|
+
analysis[:transaction_efficiency][:score] > 0.5, # Good transaction usage
|
261
|
+
analysis[:efficiency_score] > 60 # Reasonable efficiency
|
262
|
+
]
|
263
|
+
#=> [true, true, true]
|
264
|
+
|
265
|
+
## Familia test helpers work with real-world objects
|
266
|
+
user = User.new(user_id: "helper_test", username: "helper", email: "helper@test.com")
|
267
|
+
|
268
|
+
assert_familia_save(user, {
|
269
|
+
user_id: "helper_test",
|
270
|
+
username: "helper",
|
271
|
+
email: "helper@test.com"
|
272
|
+
})
|
273
|
+
#=> true
|
274
|
+
|
275
|
+
## Data type operations validate correctly with helpers
|
276
|
+
user = User.new(user_id: "data_test", username: "datatester")
|
277
|
+
user.save
|
278
|
+
|
279
|
+
assert_list_operation(user, :activity_log, :lpush, "test_activity") do
|
280
|
+
# This should execute: LPUSH user:data_test:activity_log test_activity
|
281
|
+
end
|
282
|
+
#=> true
|
283
|
+
|
284
|
+
## Test set operation validation
|
285
|
+
assert_set_operation(user, :permissions, :sadd, "admin") do
|
286
|
+
# This should execute: SADD user:data_test:permissions admin
|
287
|
+
end
|
288
|
+
#=> true
|
289
|
+
|
290
|
+
## Test sorted set operation validation
|
291
|
+
assert_sorted_set_operation(user, :scores, :zadd, 100, "test_score") do
|
292
|
+
# This should execute: ZADD user:data_test:scores 100 test_score
|
293
|
+
end
|
294
|
+
#=> true
|
295
|
+
|
296
|
+
## Mixed atomic and non-atomic operations are properly categorized
|
297
|
+
validator = Familia::Validation::Validator.new(strict_atomicity: true)
|
298
|
+
commands = validator.capture_redis_commands do
|
299
|
+
user = User.new(user_id: "mixed_test", username: "mixed")
|
300
|
+
|
301
|
+
# Non-atomic operation
|
302
|
+
user.save
|
303
|
+
|
304
|
+
# Atomic operation
|
305
|
+
user.batch_update(status: "active", last_login: Time.now.to_i.to_s)
|
306
|
+
|
307
|
+
# Non-atomic operation
|
308
|
+
user.activity_log.unshift("status_updated")
|
309
|
+
end
|
310
|
+
|
311
|
+
atomic_count = commands.commands.count(&:atomic_command?)
|
312
|
+
non_atomic_count = commands.commands.count { |cmd| !cmd.atomic_command? }
|
313
|
+
|
314
|
+
[atomic_count > 0, non_atomic_count > 0]
|
315
|
+
#=> [true, true]
|
316
|
+
|
317
|
+
## Edge case: Empty transaction is detected but doesn't fail validation
|
318
|
+
validator = Familia::Validation::Validator.new
|
319
|
+
result = validator.validate do |expect|
|
320
|
+
expect.transaction do |tx|
|
321
|
+
# Empty transaction expectation
|
322
|
+
end
|
323
|
+
|
324
|
+
# Execute empty transaction
|
325
|
+
Familia.transaction do |conn|
|
326
|
+
# No operations inside transaction
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
result.valid? && result.warning_messages.any?
|
331
|
+
#=> true
|
332
|
+
|
333
|
+
## Error scenarios are properly handled and reported
|
334
|
+
validator = Familia::Validation::Validator.new
|
335
|
+
result = validator.validate do |expect|
|
336
|
+
expect.hset("wrong:key", "field", "value") # Wrong expectation
|
337
|
+
|
338
|
+
# Execute different operation
|
339
|
+
user = User.new(user_id: "error_test", username: "error")
|
340
|
+
user.save
|
341
|
+
end
|
342
|
+
|
343
|
+
[
|
344
|
+
!result.valid?,
|
345
|
+
result.error_messages.any?,
|
346
|
+
result.detailed_report.include?("FAIL")
|
347
|
+
]
|
348
|
+
#=> [true, true, true]
|
349
|
+
|
350
|
+
## Flexible matching works for dynamic scenarios
|
351
|
+
validator = Familia::Validation::Validator.new
|
352
|
+
result = validator.validate do |expect|
|
353
|
+
expect.strict_order(false)
|
354
|
+
.allow_extra_commands(true)
|
355
|
+
.match_pattern(/HSET user:dynamic\d+:object/)
|
356
|
+
.match_pattern(/SADD user:dynamic\d+:permissions/)
|
357
|
+
|
358
|
+
# Execute with dynamic ID
|
359
|
+
dynamic_id = "dynamic#{Time.now.to_i}"
|
360
|
+
user = UserService.create_user_with_profile({
|
361
|
+
user_id: dynamic_id,
|
362
|
+
username: "dynamic_user"
|
363
|
+
})
|
364
|
+
end
|
365
|
+
|
366
|
+
result.valid?
|
367
|
+
#=> true
|
368
|
+
|
369
|
+
## Debug helpers provide useful output for troubleshooting
|
370
|
+
commands = capture_redis_commands do
|
371
|
+
user = User.new(user_id: "debug_test", username: "debugger")
|
372
|
+
user.save
|
373
|
+
user.permissions.add("debug")
|
374
|
+
end
|
375
|
+
|
376
|
+
# These should not raise errors and should provide useful output
|
377
|
+
debug_commands = commands.commands.map(&:to_s)
|
378
|
+
debug_commands.length > 0 && debug_commands.all? { |cmd| cmd.is_a?(String) }
|
379
|
+
#=> true
|
380
|
+
|
381
|
+
## Cleanup test environment
|
382
|
+
teardown_validation_test
|
383
|
+
|
384
|
+
## Clean up test data
|
385
|
+
cleanup_keys = Familia.dbclient.keys("user:*")
|
386
|
+
cleanup_keys.concat(Familia.dbclient.keys("blogpost:*"))
|
387
|
+
cleanup_keys.concat(Familia.dbclient.keys("session:*"))
|
388
|
+
Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
|
389
|
+
true
|
390
|
+
#=> true
|