familia 2.0.0.pre5 → 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 -10
- data/Gemfile +2 -2
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +631 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -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/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +9 -6
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- 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/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- 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/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- 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 +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- 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/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_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 +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,443 @@
|
|
1
|
+
# lib/familia/validation/test_helpers.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Validation
|
5
|
+
# Test helper methods for integrating Redis command validation with
|
6
|
+
# the tryouts testing framework. Provides easy-to-use assertion methods
|
7
|
+
# and automatic setup/cleanup for command validation tests.
|
8
|
+
#
|
9
|
+
# @example Basic usage in a try file
|
10
|
+
# require_relative '../validation/test_helpers'
|
11
|
+
# extend Familia::Validation::TestHelpers
|
12
|
+
#
|
13
|
+
# ## User save should execute expected Redis commands
|
14
|
+
# user = TestUser.new(id: "123", name: "John")
|
15
|
+
#
|
16
|
+
# assert_redis_commands do |expect|
|
17
|
+
# expect.hset("testuser:123:object", "name", "John")
|
18
|
+
# .hset("testuser:123:object", "id", "123")
|
19
|
+
#
|
20
|
+
# user.save
|
21
|
+
# end
|
22
|
+
# #=> true
|
23
|
+
#
|
24
|
+
# @example Transaction validation
|
25
|
+
# assert_atomic_operation do |expect|
|
26
|
+
# expect.transaction do |tx|
|
27
|
+
# tx.hset("account:123", "balance", "1000")
|
28
|
+
# .hset("account:456", "balance", "2000")
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# transfer_funds(from: "123", to: "456", amount: 500)
|
32
|
+
# end
|
33
|
+
# #=> true
|
34
|
+
#
|
35
|
+
module TestHelpers
|
36
|
+
# Assert that a block executes the expected Redis commands
|
37
|
+
def assert_redis_commands(message = nil, &block)
|
38
|
+
validator = Validator.new
|
39
|
+
result = validator.validate(&block)
|
40
|
+
|
41
|
+
unless result.valid?
|
42
|
+
error_msg = message || "Redis command validation failed"
|
43
|
+
error_msg += "\n" + result.detailed_report
|
44
|
+
raise ValidationError, error_msg
|
45
|
+
end
|
46
|
+
|
47
|
+
result.valid?
|
48
|
+
end
|
49
|
+
|
50
|
+
# Assert that a block executes Redis commands atomically
|
51
|
+
def assert_atomic_operation(message = nil, &block)
|
52
|
+
validator = Validator.new(strict_atomicity: true)
|
53
|
+
|
54
|
+
if block.arity == 1
|
55
|
+
# Block expects expectations parameter
|
56
|
+
result = validator.validate(&block)
|
57
|
+
else
|
58
|
+
# Block is just execution code - validate atomicity only
|
59
|
+
result = validator.validate_atomicity(&block)
|
60
|
+
end
|
61
|
+
|
62
|
+
unless result.valid?
|
63
|
+
error_msg = message || "Atomic operation validation failed"
|
64
|
+
error_msg += "\n" + result.detailed_report
|
65
|
+
raise ValidationError, error_msg
|
66
|
+
end
|
67
|
+
|
68
|
+
result.valid?
|
69
|
+
end
|
70
|
+
|
71
|
+
# Assert that specific commands were executed (flexible order)
|
72
|
+
def assert_commands_executed(*expected_commands)
|
73
|
+
validator = Validator.new
|
74
|
+
|
75
|
+
CommandRecorder.start_recording
|
76
|
+
yield if block_given?
|
77
|
+
actual_commands = CommandRecorder.stop_recording
|
78
|
+
|
79
|
+
result = validator.assert_commands_executed(expected_commands, actual_commands)
|
80
|
+
|
81
|
+
unless result.valid?
|
82
|
+
error_msg = "Expected commands were not executed as specified"
|
83
|
+
error_msg += "\n" + result.detailed_report
|
84
|
+
raise ValidationError, error_msg
|
85
|
+
end
|
86
|
+
|
87
|
+
result.valid?
|
88
|
+
end
|
89
|
+
|
90
|
+
# Assert that no Redis commands were executed
|
91
|
+
def assert_no_redis_commands(&block)
|
92
|
+
CommandRecorder.start_recording
|
93
|
+
block.call if block_given?
|
94
|
+
commands = CommandRecorder.stop_recording
|
95
|
+
|
96
|
+
unless commands.command_count == 0
|
97
|
+
error_msg = "Expected no Redis commands, but #{commands.command_count} were executed:"
|
98
|
+
commands.commands.each { |cmd| error_msg += "\n #{cmd}" }
|
99
|
+
raise ValidationError, error_msg
|
100
|
+
end
|
101
|
+
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
# Assert that a specific number of commands were executed
|
106
|
+
def assert_command_count(expected_count, &block)
|
107
|
+
CommandRecorder.start_recording
|
108
|
+
block.call if block_given?
|
109
|
+
commands = CommandRecorder.stop_recording
|
110
|
+
|
111
|
+
actual_count = commands.command_count
|
112
|
+
unless actual_count == expected_count
|
113
|
+
error_msg = "Expected #{expected_count} Redis commands, but #{actual_count} were executed"
|
114
|
+
raise ValidationError, error_msg
|
115
|
+
end
|
116
|
+
|
117
|
+
true
|
118
|
+
end
|
119
|
+
|
120
|
+
# Assert that commands were executed within a transaction
|
121
|
+
def assert_transaction_used(&block)
|
122
|
+
CommandRecorder.start_recording
|
123
|
+
block.call if block_given?
|
124
|
+
commands = CommandRecorder.stop_recording
|
125
|
+
|
126
|
+
unless commands.transaction_count > 0
|
127
|
+
error_msg = "Expected operations to use transactions, but none were found"
|
128
|
+
raise ValidationError, error_msg
|
129
|
+
end
|
130
|
+
|
131
|
+
true
|
132
|
+
end
|
133
|
+
|
134
|
+
# Assert that commands were NOT executed within a transaction
|
135
|
+
def assert_no_transaction_used(&block)
|
136
|
+
CommandRecorder.start_recording
|
137
|
+
block.call if block_given?
|
138
|
+
commands = CommandRecorder.stop_recording
|
139
|
+
|
140
|
+
unless commands.transaction_count == 0
|
141
|
+
error_msg = "Expected operations to NOT use transactions, but #{commands.transaction_count} were found"
|
142
|
+
raise ValidationError, error_msg
|
143
|
+
end
|
144
|
+
|
145
|
+
true
|
146
|
+
end
|
147
|
+
|
148
|
+
# Capture and return Redis commands without validation
|
149
|
+
def capture_redis_commands(&block)
|
150
|
+
CommandRecorder.start_recording
|
151
|
+
block.call if block_given?
|
152
|
+
CommandRecorder.stop_recording
|
153
|
+
end
|
154
|
+
|
155
|
+
# Performance assertion - assert operations complete within time limit
|
156
|
+
def assert_performance_within(max_duration_ms, &block)
|
157
|
+
start_time = Time.now
|
158
|
+
CommandRecorder.start_recording
|
159
|
+
|
160
|
+
result = block.call if block_given?
|
161
|
+
|
162
|
+
commands = CommandRecorder.stop_recording
|
163
|
+
actual_duration_ms = (Time.now - start_time) * 1000
|
164
|
+
|
165
|
+
if actual_duration_ms > max_duration_ms
|
166
|
+
error_msg = "Operation took #{actual_duration_ms.round(2)}ms, " \
|
167
|
+
"expected less than #{max_duration_ms}ms"
|
168
|
+
raise ValidationError, error_msg
|
169
|
+
end
|
170
|
+
|
171
|
+
result
|
172
|
+
end
|
173
|
+
|
174
|
+
# Assert efficient command usage (no N+1 patterns)
|
175
|
+
def assert_efficient_commands(&block)
|
176
|
+
validator = Validator.new(performance_tracking: true)
|
177
|
+
commands = capture_redis_commands(&block)
|
178
|
+
|
179
|
+
analysis = validator.analyze_performance(commands)
|
180
|
+
|
181
|
+
if analysis[:efficiency_score] < 70 # Threshold for acceptable efficiency
|
182
|
+
error_msg = "Inefficient Redis command usage detected (score: #{analysis[:efficiency_score]})"
|
183
|
+
|
184
|
+
if analysis[:potential_n_plus_one].any?
|
185
|
+
error_msg += "\nPotential N+1 patterns:"
|
186
|
+
analysis[:potential_n_plus_one].each do |pattern|
|
187
|
+
error_msg += "\n #{pattern[:command]}: #{pattern[:count]} calls - #{pattern[:suggestion]}"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
raise ValidationError, error_msg
|
192
|
+
end
|
193
|
+
|
194
|
+
true
|
195
|
+
end
|
196
|
+
|
197
|
+
# Setup and teardown helpers for validation tests
|
198
|
+
def setup_validation_test
|
199
|
+
# Ensure middleware is registered
|
200
|
+
@original_middleware_state = CommandRecorder.recording?
|
201
|
+
|
202
|
+
# Clear any existing state
|
203
|
+
CommandRecorder.clear if CommandRecorder.recording?
|
204
|
+
|
205
|
+
# Enable database logging for better debugging
|
206
|
+
@original_logging_state = Familia.enable_database_logging
|
207
|
+
Familia.enable_database_logging = true
|
208
|
+
|
209
|
+
# Enable command counting
|
210
|
+
@original_counter_state = Familia.enable_database_counter
|
211
|
+
Familia.enable_database_counter = true
|
212
|
+
|
213
|
+
DatabaseCommandCounter.reset
|
214
|
+
end
|
215
|
+
|
216
|
+
def teardown_validation_test
|
217
|
+
# Stop recording if active
|
218
|
+
CommandRecorder.stop_recording if CommandRecorder.recording?
|
219
|
+
|
220
|
+
# Restore original states
|
221
|
+
Familia.enable_database_logging = @original_logging_state if @original_logging_state
|
222
|
+
Familia.enable_database_counter = @original_counter_state if @original_counter_state
|
223
|
+
|
224
|
+
# Reset counters
|
225
|
+
DatabaseCommandCounter.reset
|
226
|
+
end
|
227
|
+
|
228
|
+
# Wrapper for validation tests with automatic setup/teardown
|
229
|
+
def with_validation_test(&block)
|
230
|
+
setup_validation_test
|
231
|
+
begin
|
232
|
+
block.call
|
233
|
+
ensure
|
234
|
+
teardown_validation_test
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Helper to create expectation builders for common patterns
|
239
|
+
def expect_horreum_save(class_name, identifier, fields = {})
|
240
|
+
dbkey = "#{class_name.to_s.downcase}:#{identifier}:object"
|
241
|
+
|
242
|
+
expectations = CommandExpectations.new
|
243
|
+
fields.each do |field, value|
|
244
|
+
expectations.hset(dbkey, field.to_s, value.to_s)
|
245
|
+
end
|
246
|
+
|
247
|
+
expectations
|
248
|
+
end
|
249
|
+
|
250
|
+
def expect_horreum_load(class_name, identifier, fields = [])
|
251
|
+
dbkey = "#{class_name.to_s.downcase}:#{identifier}:object"
|
252
|
+
|
253
|
+
expectations = CommandExpectations.new
|
254
|
+
if fields.empty?
|
255
|
+
expectations.hgetall(dbkey)
|
256
|
+
else
|
257
|
+
fields.each do |field|
|
258
|
+
expectations.hget(dbkey, field.to_s)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
expectations
|
263
|
+
end
|
264
|
+
|
265
|
+
def expect_data_type_operation(class_name, identifier, type_name, operation, *args)
|
266
|
+
dbkey = "#{class_name.to_s.downcase}:#{identifier}:#{type_name}"
|
267
|
+
|
268
|
+
expectations = CommandExpectations.new
|
269
|
+
expectations.command(operation, dbkey, *args)
|
270
|
+
end
|
271
|
+
|
272
|
+
# Debugging helpers
|
273
|
+
def debug_print_commands(command_sequence = nil)
|
274
|
+
commands = command_sequence || capture_redis_commands { yield if block_given? }
|
275
|
+
|
276
|
+
puts "Redis Commands Executed (#{commands.command_count} total):"
|
277
|
+
puts "=" * 50
|
278
|
+
|
279
|
+
commands.commands.each_with_index do |cmd, i|
|
280
|
+
prefix = cmd.atomic_command? ? "[TX]" : " "
|
281
|
+
puts "#{prefix} #{i + 1}. #{cmd} (#{cmd.duration_us}µs)"
|
282
|
+
end
|
283
|
+
|
284
|
+
if commands.transaction_count > 0
|
285
|
+
puts "\nTransactions (#{commands.transaction_count} total):"
|
286
|
+
commands.transaction_blocks.each_with_index do |tx, i|
|
287
|
+
puts " #{i + 1}. #{tx.command_count} commands"
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
puts "=" * 50
|
292
|
+
end
|
293
|
+
|
294
|
+
def debug_print_performance(command_sequence = nil)
|
295
|
+
commands = command_sequence || CommandRecorder.current_sequence
|
296
|
+
validator = Validator.new(performance_tracking: true)
|
297
|
+
analysis = validator.analyze_performance(commands)
|
298
|
+
|
299
|
+
puts "Performance Analysis:"
|
300
|
+
puts "=" * 30
|
301
|
+
puts "Total Commands: #{analysis[:total_commands]}"
|
302
|
+
puts "Total Duration: #{analysis[:total_duration_ms].round(2)}ms"
|
303
|
+
puts "Average Command Time: #{analysis[:average_command_time_us].round(2)}µs"
|
304
|
+
puts "Efficiency Score: #{analysis[:efficiency_score].round(1)}/100"
|
305
|
+
|
306
|
+
if analysis[:slowest_commands].any?
|
307
|
+
puts "\nSlowest Commands:"
|
308
|
+
analysis[:slowest_commands].each do |cmd|
|
309
|
+
puts " #{cmd[:command]} (#{cmd[:duration_us]}µs)"
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
if analysis[:potential_n_plus_one].any?
|
314
|
+
puts "\nPotential N+1 Patterns:"
|
315
|
+
analysis[:potential_n_plus_one].each do |pattern|
|
316
|
+
puts " #{pattern[:command]}: #{pattern[:count]} calls"
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
puts "=" * 30
|
321
|
+
end
|
322
|
+
|
323
|
+
# Matcher helpers for more readable tests
|
324
|
+
def match_command(cmd, *args)
|
325
|
+
if args.empty?
|
326
|
+
->(recorded) { recorded.command == cmd.to_s.upcase }
|
327
|
+
else
|
328
|
+
->(recorded) { recorded.command == cmd.to_s.upcase && recorded.args == args.map(&:to_s) }
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
def match_pattern(pattern)
|
333
|
+
case pattern
|
334
|
+
when Regexp
|
335
|
+
->(recorded) { pattern.match?(recorded.to_s) }
|
336
|
+
when String
|
337
|
+
->(recorded) { recorded.to_s.include?(pattern) }
|
338
|
+
else
|
339
|
+
pattern
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def any_string
|
344
|
+
ArgumentMatcher.new(:any_string)
|
345
|
+
end
|
346
|
+
|
347
|
+
def any_number
|
348
|
+
ArgumentMatcher.new(:any_number)
|
349
|
+
end
|
350
|
+
|
351
|
+
def any_value
|
352
|
+
ArgumentMatcher.new(:any_value)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# Extended test helpers specifically for Familia data types
|
357
|
+
module FamiliaTestHelpers
|
358
|
+
include TestHelpers
|
359
|
+
|
360
|
+
# Assert Familia object operations
|
361
|
+
def assert_familia_save(obj, expected_fields = nil, &block)
|
362
|
+
class_name = obj.class.name.split('::').last.downcase
|
363
|
+
identifier = obj.identifier
|
364
|
+
|
365
|
+
assert_redis_commands do |expect|
|
366
|
+
if expected_fields
|
367
|
+
expected_fields.each do |field, value|
|
368
|
+
expect.hset("#{class_name}:#{identifier}:object", field.to_s, value.to_s)
|
369
|
+
end
|
370
|
+
else
|
371
|
+
expect.match_pattern(/^HSET #{class_name}:#{identifier}:object/)
|
372
|
+
end
|
373
|
+
|
374
|
+
block.call if block
|
375
|
+
obj.save
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def assert_familia_load(obj_class, identifier, &block)
|
380
|
+
class_name = obj_class.name.split('::').last.downcase
|
381
|
+
|
382
|
+
assert_redis_commands do |expect|
|
383
|
+
expect.hgetall("#{class_name}:#{identifier}:object")
|
384
|
+
|
385
|
+
block.call if block
|
386
|
+
obj_class.new(id: identifier).refresh!
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def assert_familia_destroy(obj, &block)
|
391
|
+
class_name = obj.class.name.split('::').last.downcase
|
392
|
+
identifier = obj.identifier
|
393
|
+
|
394
|
+
assert_redis_commands do |expect|
|
395
|
+
expect.del("#{class_name}:#{identifier}:object")
|
396
|
+
|
397
|
+
block.call if block
|
398
|
+
obj.destroy!
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# Assert data type operations
|
403
|
+
def assert_list_operation(obj, list_name, operation, *args, &block)
|
404
|
+
class_name = obj.class.name.split('::').last.downcase
|
405
|
+
identifier = obj.identifier
|
406
|
+
dbkey = "#{class_name}:#{identifier}:#{list_name}"
|
407
|
+
|
408
|
+
assert_redis_commands do |expect|
|
409
|
+
expect.command(operation.to_s.upcase, dbkey, *args)
|
410
|
+
|
411
|
+
block.call if block
|
412
|
+
obj.send(list_name).send(operation, *args)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
def assert_set_operation(obj, set_name, operation, *args, &block)
|
417
|
+
class_name = obj.class.name.split('::').last.downcase
|
418
|
+
identifier = obj.identifier
|
419
|
+
dbkey = "#{class_name}:#{identifier}:#{set_name}"
|
420
|
+
|
421
|
+
assert_redis_commands do |expect|
|
422
|
+
expect.command(operation.to_s.upcase, dbkey, *args)
|
423
|
+
|
424
|
+
block.call if block
|
425
|
+
obj.send(set_name).send(operation, *args)
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
def assert_sorted_set_operation(obj, zset_name, operation, *args, &block)
|
430
|
+
class_name = obj.class.name.split('::').last.downcase
|
431
|
+
identifier = obj.identifier
|
432
|
+
dbkey = "#{class_name}:#{identifier}:#{zset_name}"
|
433
|
+
|
434
|
+
assert_redis_commands do |expect|
|
435
|
+
expect.command(operation.to_s.upcase, dbkey, *args)
|
436
|
+
|
437
|
+
block.call if block
|
438
|
+
obj.send(zset_name).send(operation, *args)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|