cache_stache 0.1.0

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +231 -0
  3. data/app/assets/stylesheets/cache_stache/application.css +5 -0
  4. data/app/assets/stylesheets/cache_stache/pico.css +4 -0
  5. data/app/controllers/cache_stache/application_controller.rb +11 -0
  6. data/app/controllers/cache_stache/dashboard_controller.rb +32 -0
  7. data/app/helpers/cache_stache/application_helper.rb +37 -0
  8. data/app/views/cache_stache/dashboard/index.html.erb +154 -0
  9. data/app/views/cache_stache/dashboard/keyspace.html.erb +83 -0
  10. data/app/views/layouts/cache_stache/application.html.erb +14 -0
  11. data/config/routes.rb +6 -0
  12. data/lib/cache_stache/cache_client.rb +202 -0
  13. data/lib/cache_stache/configuration.rb +87 -0
  14. data/lib/cache_stache/engine.rb +17 -0
  15. data/lib/cache_stache/instrumentation.rb +142 -0
  16. data/lib/cache_stache/keyspace.rb +28 -0
  17. data/lib/cache_stache/rack_after_reply_middleware.rb +22 -0
  18. data/lib/cache_stache/railtie.rb +30 -0
  19. data/lib/cache_stache/stats_query.rb +89 -0
  20. data/lib/cache_stache/version.rb +5 -0
  21. data/lib/cache_stache/web.rb +69 -0
  22. data/lib/cache_stache/window_options.rb +34 -0
  23. data/lib/cache_stache.rb +37 -0
  24. data/lib/generators/cache_stache/install_generator.rb +21 -0
  25. data/lib/generators/cache_stache/templates/README +35 -0
  26. data/lib/generators/cache_stache/templates/cache_stache.rb +43 -0
  27. data/spec/cache_stache_helper.rb +148 -0
  28. data/spec/dummy_app/Rakefile +5 -0
  29. data/spec/dummy_app/app/assets/config/manifest.js +1 -0
  30. data/spec/dummy_app/config/application.rb +31 -0
  31. data/spec/dummy_app/config/boot.rb +3 -0
  32. data/spec/dummy_app/config/environment.rb +5 -0
  33. data/spec/dummy_app/config/routes.rb +7 -0
  34. data/spec/integration/dashboard_controller_spec.rb +94 -0
  35. data/spec/integration/full_cache_flow_spec.rb +202 -0
  36. data/spec/integration/instrumentation_spec.rb +259 -0
  37. data/spec/integration/rack_after_reply_spec.rb +47 -0
  38. data/spec/integration/rake_tasks_spec.rb +17 -0
  39. data/spec/spec_helper.rb +64 -0
  40. data/spec/unit/cache_client_spec.rb +278 -0
  41. data/spec/unit/configuration_spec.rb +209 -0
  42. data/spec/unit/keyspace_spec.rb +93 -0
  43. data/spec/unit/stats_query_spec.rb +367 -0
  44. data/tasks/cache_stache.rake +74 -0
  45. metadata +226 -0
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../cache_stache_helper"
4
+
5
+ RSpec.describe "Full Cache Flow" do
6
+ include ActiveSupport::Testing::TimeHelpers
7
+
8
+ include_context "with instrumentation and search"
9
+
10
+ after do
11
+ travel_back if respond_to?(:travel_back)
12
+ end
13
+
14
+ describe "complete workflow" do
15
+ it "tracks cache operations end-to-end" do
16
+ # Generate cache misses (first fetch)
17
+ 10.times { |i| Rails.cache.fetch("views/product_#{i}") { "Product #{i}" } }
18
+
19
+ # Generate cache hits (repeat fetch)
20
+ 5.times { Rails.cache.fetch("views/product_1") { "Product 1" } }
21
+
22
+ # Generate model cache operations
23
+ 3.times { |i| Rails.cache.fetch("community/#{i}") { "Community #{i}" } }
24
+
25
+ # Generate operations matching multiple keyspaces
26
+ Rails.cache.fetch("views/community_search") { "Search results" }
27
+
28
+ sleep 0.2 # Allow instrumentation to process
29
+
30
+ # Query statistics
31
+ query = CacheStache::StatsQuery.new(window: 5.minutes)
32
+ results = query.execute
33
+
34
+ # Verify overall stats
35
+ expect(results[:overall][:total_operations]).to be >= 18 # 10 + 5 + 3
36
+ expect(results[:overall][:hits]).to be >= 5
37
+ expect(results[:overall][:misses]).to be >= 13
38
+
39
+ # Verify view keyspace stats
40
+ expect(results[:keyspaces][:views][:total_operations]).to be >= 16 # 10 + 5 + 1
41
+ expect(results[:keyspaces][:views][:hits]).to be >= 5
42
+
43
+ # Verify model keyspace stats
44
+ expect(results[:keyspaces][:models][:total_operations]).to be >= 4 # 3 + 1
45
+
46
+ # Verify search keyspace stats
47
+ expect(results[:keyspaces][:search][:total_operations]).to be >= 1
48
+ end
49
+
50
+ it "calculates accurate hit rates" do
51
+ # Generate known pattern: 10 misses, 5 hits
52
+ 10.times { |i| Rails.cache.fetch("views/item_#{i}") { "Item #{i}" } }
53
+ 5.times { Rails.cache.fetch("views/item_1") { "Item 1" } }
54
+
55
+ sleep 0.2
56
+
57
+ query = CacheStache::StatsQuery.new(window: 5.minutes)
58
+ results = query.execute
59
+
60
+ # Total operations = 15, hits = 5, misses = 10
61
+ # Hit rate should be ~33%
62
+ expect(results[:overall][:total_operations]).to eq(15)
63
+ expect(results[:overall][:hits]).to eq(5)
64
+ expect(results[:overall][:misses]).to eq(10)
65
+ expect(results[:overall][:hit_rate_percent]).to be_within(0.1).of(33.33)
66
+ end
67
+
68
+ it "handles mixed cache operations" do
69
+ # Mix of reads, writes, and fetches
70
+ Rails.cache.write("test_1", "value")
71
+ Rails.cache.read("test_1") # hit
72
+ Rails.cache.read("nonexistent") # miss
73
+ Rails.cache.fetch("test_2") { "value" } # miss (write + read)
74
+ Rails.cache.fetch("test_2") { "value" } # hit
75
+
76
+ sleep 0.2
77
+
78
+ query = CacheStache::StatsQuery.new(window: 5.minutes)
79
+ results = query.execute
80
+
81
+ # Only reads are tracked
82
+ expect(results[:overall][:total_operations]).to be >= 4
83
+ end
84
+
85
+ it "handles keyspaces with overlapping patterns" do
86
+ # Key matches both views and search keyspaces
87
+ Rails.cache.fetch("views/search_page") { "content" }
88
+
89
+ sleep 0.2
90
+
91
+ query = CacheStache::StatsQuery.new(window: 5.minutes)
92
+ results = query.execute
93
+
94
+ # Both keyspaces should record the operation
95
+ expect(results[:keyspaces][:views][:total_operations]).to eq(1)
96
+ expect(results[:keyspaces][:search][:total_operations]).to eq(1)
97
+
98
+ # Overall should still count it once
99
+ expect(results[:overall][:total_operations]).to eq(1)
100
+ end
101
+ end
102
+
103
+ describe "edge cases" do
104
+ it "handles cache keys with special characters" do
105
+ special_keys = [
106
+ "views/product:123",
107
+ "views/category/sub-category",
108
+ "views/item.json",
109
+ "views/search?query=test"
110
+ ]
111
+
112
+ special_keys.each { |key| Rails.cache.fetch(key) { "value" } }
113
+ sleep 0.2
114
+
115
+ query = CacheStache::StatsQuery.new(window: 5.minutes)
116
+ results = query.execute
117
+
118
+ expect(results[:keyspaces][:views][:total_operations]).to eq(special_keys.size)
119
+ end
120
+
121
+ it "handles very high operation volumes" do
122
+ 100.times { |i| Rails.cache.fetch("views/item_#{i}") { "value" } }
123
+ sleep 0.3
124
+
125
+ query = CacheStache::StatsQuery.new(window: 5.minutes)
126
+ results = query.execute
127
+
128
+ expect(results[:overall][:total_operations]).to eq(100)
129
+ end
130
+
131
+ it "handles cache operations with nil values" do
132
+ Rails.cache.write("nil_value", nil)
133
+ Rails.cache.read("nil_value") # This is a hit
134
+
135
+ sleep 0.2
136
+
137
+ query = CacheStache::StatsQuery.new(window: 5.minutes)
138
+ results = query.execute
139
+
140
+ expect(results[:overall][:hits]).to be >= 1
141
+ end
142
+ end
143
+
144
+ describe "realistic usage scenarios" do
145
+ it "simulates a typical request pattern" do
146
+ # Simulate multiple requests hitting various cache keys
147
+
148
+ # Request 1: Homepage (all hits after first)
149
+ Rails.cache.fetch("views/homepage") { "content" }
150
+ 3.times { Rails.cache.fetch("views/homepage") { "content" } }
151
+
152
+ # Request 2: Product listings (some hits, some misses)
153
+ Rails.cache.fetch("views/products/list") { "list" }
154
+ Rails.cache.fetch("views/products/list") { "list" }
155
+
156
+ # Request 3: Community data (misses)
157
+ 5.times { |i| Rails.cache.fetch("community/#{i}/stats") { "stats" } }
158
+
159
+ # Request 4: Search results (misses)
160
+ Rails.cache.fetch("search/results?q=test") { "results" }
161
+
162
+ sleep 0.2
163
+
164
+ query = CacheStache::StatsQuery.new(window: 5.minutes)
165
+ results = query.execute
166
+
167
+ # Verify realistic metrics
168
+ expect(results[:overall][:total_operations]).to be >= 12
169
+ expect(results[:overall][:hit_rate_percent]).to be > 0
170
+ expect(results[:overall][:hit_rate_percent]).to be < 100
171
+
172
+ # Verify keyspace breakdown
173
+ expect(results[:keyspaces][:views][:total_operations]).to be >= 6
174
+ expect(results[:keyspaces][:models][:total_operations]).to be >= 5
175
+ expect(results[:keyspaces][:search][:total_operations]).to be >= 1
176
+ end
177
+
178
+ it "simulates gradual cache warming" do
179
+ # First pass: all misses (cold cache)
180
+ 10.times { |i| Rails.cache.fetch("warming_item_#{i}") { "value" } }
181
+ sleep 0.2
182
+
183
+ query = CacheStache::StatsQuery.new(window: 5.minutes)
184
+ results_after_cold = query.execute
185
+ cold_misses = results_after_cold[:overall][:misses]
186
+ expect(cold_misses).to eq(10)
187
+
188
+ # Second pass: all hits (warm cache)
189
+ 10.times { |i| Rails.cache.fetch("warming_item_#{i}") { "value" } }
190
+ sleep 0.2
191
+
192
+ query = CacheStache::StatsQuery.new(window: 5.minutes)
193
+ results_after_warm = query.execute
194
+
195
+ # Should now have 10 misses + 10 hits = 20 total, 50% hit rate
196
+ expect(results_after_warm[:overall][:total_operations]).to eq(20)
197
+ expect(results_after_warm[:overall][:hits]).to eq(10)
198
+ expect(results_after_warm[:overall][:misses]).to eq(10)
199
+ expect(results_after_warm[:overall][:hit_rate_percent]).to eq(50.0)
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../cache_stache_helper"
4
+
5
+ RSpec.describe CacheStache::Instrumentation do
6
+ let(:config) do
7
+ build_test_config(
8
+ keyspaces: {
9
+ views: {label: "View Fragments", match: /^views\//},
10
+ models: {label: "Model Cache", match: /community/}
11
+ }
12
+ )
13
+ end
14
+
15
+ before do
16
+ allow(CacheStache).to receive(:configuration).and_return(config)
17
+ end
18
+
19
+ describe ".install!" do
20
+ it "installs instrumentation when enabled" do
21
+ expect(Rails.logger).to receive(:info).with(/Instrumentation installed/)
22
+ described_class.install!
23
+
24
+ expect(described_class.instance_variable_get(:@installed)).to be(true)
25
+ end
26
+
27
+ it "does not install when disabled" do
28
+ config.enabled = false
29
+ described_class.install!
30
+
31
+ expect(described_class.instance_variable_get(:@installed)).to be_falsey
32
+ end
33
+
34
+ it "does not install twice" do
35
+ described_class.install!
36
+ expect(Rails.logger).not_to receive(:info)
37
+ described_class.install!
38
+ end
39
+
40
+ it "subscribes to cache_read.active_support" do
41
+ described_class.install!
42
+
43
+ listeners = ActiveSupport::Notifications.notifier.listeners_for("cache_read.active_support")
44
+ expect(listeners).not_to be_empty
45
+ end
46
+
47
+ it "creates a cache client" do
48
+ described_class.install!
49
+ cache_client = described_class.instance_variable_get(:@cache_client)
50
+
51
+ expect(cache_client).to be_a(CacheStache::CacheClient)
52
+ end
53
+
54
+ it "stores config metadata" do
55
+ described_class.install!
56
+
57
+ raw_metadata = cache_stache_redis.get("cache_stache:v1:test:config")
58
+ metadata = raw_metadata ? JSON.parse(raw_metadata) : nil
59
+ expect(metadata).not_to be_nil
60
+ expect(metadata["bucket_seconds"]).to eq(300)
61
+ end
62
+
63
+ it "captures the monitored store class name" do
64
+ described_class.install!
65
+
66
+ expect(described_class.monitored_store_class).to eq(Rails.cache.class.name)
67
+ end
68
+ end
69
+
70
+ describe ".call" do
71
+ before do
72
+ described_class.install!
73
+ end
74
+
75
+ it "records cache hits" do
76
+ Rails.cache.write("test_key", "value")
77
+ Rails.cache.read("test_key")
78
+
79
+ sleep 0.1
80
+
81
+ expect(current_bucket_stats["overall:hits"].to_f).to be >= 1
82
+ end
83
+
84
+ it "records cache misses" do
85
+ Rails.cache.read("nonexistent_key")
86
+
87
+ sleep 0.1
88
+
89
+ expect(current_bucket_stats["overall:misses"].to_f).to be >= 1
90
+ end
91
+
92
+ it "records keyspace hits when key matches" do
93
+ Rails.cache.write("views/product_123", "value")
94
+ Rails.cache.read("views/product_123")
95
+
96
+ sleep 0.1
97
+
98
+ expect(current_bucket_stats["views:hits"].to_f).to be >= 1
99
+ end
100
+
101
+ it "records keyspace misses when key matches" do
102
+ Rails.cache.read("views/nonexistent")
103
+
104
+ sleep 0.1
105
+
106
+ expect(current_bucket_stats["views:misses"].to_f).to be >= 1
107
+ end
108
+
109
+ it "records multiple keyspaces when key matches multiple patterns" do
110
+ Rails.cache.write("views/community_123", "value")
111
+ Rails.cache.read("views/community_123")
112
+
113
+ sleep 0.1
114
+
115
+ stats = current_bucket_stats
116
+ expect(stats["views:hits"].to_f).to be >= 1
117
+ expect(stats["models:hits"].to_f).to be >= 1
118
+ end
119
+
120
+ it "ignores cache_stache keys to prevent recursion" do
121
+ Rails.cache.read("cache_stache:v1:test:12345")
122
+
123
+ sleep 0.1
124
+
125
+ stats = Rails.cache.read(current_bucket_key)
126
+
127
+ # Should not record the cache_stache key read
128
+ expect(stats).to be_nil
129
+ end
130
+
131
+ it "handles payloads with :key field" do
132
+ payload = {key: "test_key", hit: true, store: Rails.cache.class.name}
133
+ described_class.call(nil, nil, nil, nil, payload)
134
+
135
+ sleep 0.1
136
+
137
+ expect(current_bucket_stats["overall:hits"].to_f).to be >= 1
138
+ end
139
+
140
+ it "handles payloads with :name field" do
141
+ payload = {name: "test_key", hit: false, store: Rails.cache.class.name}
142
+ described_class.call(nil, nil, nil, nil, payload)
143
+
144
+ sleep 0.1
145
+
146
+ expect(current_bucket_stats["overall:misses"].to_f).to be >= 1
147
+ end
148
+
149
+ it "ignores payloads without key or name" do
150
+ payload = {hit: true, store: Rails.cache.class.name}
151
+ expect { described_class.call(nil, nil, nil, nil, payload) }.not_to raise_error
152
+
153
+ stats = Rails.cache.read(current_bucket_key)
154
+
155
+ expect(stats).to be_nil
156
+ end
157
+
158
+ it "ignores payloads from different store classes" do
159
+ payload = {key: "test_key", hit: true, store: "ActiveSupport::Cache::MemoryStore"}
160
+ # Only ignore if Rails.cache is not a MemoryStore
161
+ if Rails.cache.class.name != "ActiveSupport::Cache::MemoryStore"
162
+ described_class.call(nil, nil, nil, nil, payload)
163
+
164
+ sleep 0.1
165
+
166
+ stats = Rails.cache.read(current_bucket_key)
167
+
168
+ expect(stats).to be_nil
169
+ end
170
+ end
171
+
172
+ it "handles errors gracefully" do
173
+ allow_any_instance_of(CacheStache::CacheClient).to receive(:increment_stats).and_raise(StandardError, "Test error")
174
+ allow(Rails.logger).to receive(:error)
175
+
176
+ payload = {key: "test_key", hit: true, store: Rails.cache.class.name}
177
+ expect { described_class.call(nil, nil, nil, nil, payload) }.not_to raise_error
178
+ expect(Rails.logger).to have_received(:error).with(/instrumentation error/)
179
+ end
180
+
181
+ it "logs error backtraces" do
182
+ allow_any_instance_of(CacheStache::CacheClient).to receive(:increment_stats).and_raise(StandardError, "Test error")
183
+ allow(Rails.logger).to receive(:error)
184
+
185
+ payload = {key: "test_key", hit: true, store: Rails.cache.class.name}
186
+ described_class.call(nil, nil, nil, nil, payload)
187
+
188
+ # Should log both the error message and the backtrace
189
+ expect(Rails.logger).to have_received(:error).twice
190
+ end
191
+ end
192
+
193
+ describe "bucket alignment" do
194
+ before do
195
+ described_class.install!
196
+ end
197
+
198
+ it "assigns operations to correct time buckets" do
199
+ Rails.cache.read("test_bucket_alignment") # miss
200
+
201
+ expect(current_bucket_stats["overall:misses"].to_f).to be >= 1
202
+ end
203
+
204
+ it "groups operations within the same bucket" do
205
+ Rails.cache.read("test_bucket_1") # miss
206
+ Rails.cache.read("test_bucket_2") # miss
207
+ Rails.cache.read("test_bucket_3") # miss
208
+
209
+ expect(current_bucket_stats["overall:misses"].to_f).to be >= 3
210
+ end
211
+ end
212
+
213
+ describe "sample_rate" do
214
+ it "reduces event volume when sample_rate is lower than 1.0" do
215
+ config.sample_rate = 0.5
216
+ described_class.install!
217
+
218
+ 200.times { |i| Rails.cache.read("sample_test_#{i}") }
219
+
220
+ sleep 0.1
221
+
222
+ recorded_count = (current_bucket_stats["overall:misses"] || 0).to_f
223
+
224
+ expect(recorded_count).to be >= 80
225
+ expect(recorded_count).to be <= 120
226
+ expect(recorded_count).to be < 200
227
+ end
228
+
229
+ it "records all events when sample_rate is 1.0" do
230
+ config.sample_rate = 1.0
231
+ described_class.install!
232
+
233
+ 50.times { |i| Rails.cache.read("full_sample_test_#{i}") }
234
+
235
+ sleep 0.1
236
+
237
+ recorded_count = (current_bucket_stats["overall:misses"] || 0).to_f
238
+
239
+ expect(recorded_count).to eq(50)
240
+ end
241
+
242
+ it "applies sampling consistently across keyspaces" do
243
+ config.sample_rate = 0.5
244
+ described_class.install!
245
+
246
+ 200.times { |i| Rails.cache.read("views/sample_test_#{i}") }
247
+
248
+ sleep 0.1
249
+
250
+ stats = current_bucket_stats
251
+ overall_count = (stats["overall:misses"] || 0).to_f
252
+ keyspace_count = (stats["views:misses"] || 0).to_f
253
+
254
+ expect(overall_count).to be >= 80
255
+ expect(overall_count).to be <= 120
256
+ expect(keyspace_count).to eq(overall_count)
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/mock"
4
+ require "active_support/testing/time_helpers"
5
+ require_relative "../cache_stache_helper"
6
+
7
+ RSpec.describe "CacheStache rack.after_reply" do
8
+ include ActiveSupport::Testing::TimeHelpers
9
+
10
+ let(:config) { build_test_config(use_rack_after_reply: true) }
11
+
12
+ before do
13
+ allow(CacheStache).to receive(:configuration).and_return(config)
14
+ CacheStache::Instrumentation.install!
15
+ end
16
+
17
+ after do
18
+ travel_back if respond_to?(:travel_back)
19
+ end
20
+
21
+ it "records cache stats via a single after_reply proc" do
22
+ travel_to Time.utc(2020, 1, 1, 0, 0, 0) do
23
+ inner = lambda do |_env|
24
+ Rails.cache.write("test_key", "value")
25
+ Rails.cache.read("test_key") # hit
26
+ Rails.cache.read("missing_key") # miss
27
+ [200, {"Content-Type" => "text/plain"}, ["ok"]]
28
+ end
29
+
30
+ app = CacheStache::RackAfterReplyMiddleware.new(inner)
31
+
32
+ env = Rack::MockRequest.env_for("/", "rack.after_reply" => [])
33
+ status, = app.call(env)
34
+ expect(status).to eq(200)
35
+
36
+ expect(env["rack.after_reply"].size).to eq(1)
37
+
38
+ expect(cache_stache_redis.hgetall(current_bucket_key)).to be_empty
39
+
40
+ env["rack.after_reply"].each(&:call)
41
+
42
+ stats = current_bucket_stats
43
+ expect(stats["overall:hits"].to_f).to be >= 1
44
+ expect(stats["overall:misses"].to_f).to be >= 1
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../spec_helper"
4
+
5
+ RSpec.describe "CacheStache rake tasks" do
6
+ it "loads without error and registers tasks" do
7
+ # Run `rake -P` from the dummy app to verify the railtie loads tasks correctly.
8
+ # If the railtie's rake task path is wrong, this will fail with LoadError.
9
+ dummy_app_path = File.expand_path("../dummy_app", __dir__)
10
+ output = `cd #{dummy_app_path} && bundle exec rake -P 2>&1`
11
+
12
+ expect($?.success?).to be(true), "rake -P failed:\n#{output}"
13
+ expect(output).to include("cache_stache:config")
14
+ expect(output).to include("cache_stache:prune")
15
+ expect(output).to include("cache_stache:stats")
16
+ end
17
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Only run SimpleCov when running CacheStache specs in isolation
4
+ # (not when loaded as part of the main app's test suite)
5
+ if ENV["CACHE_STACHE_COVERAGE"] || !defined?(Rails)
6
+ require "simplecov"
7
+
8
+ SimpleCov.start do
9
+ enable_coverage :branch
10
+ command_name "CacheStache"
11
+
12
+ # The project root for CacheStache is the lib/cache_stache directory
13
+ cache_stache_root = File.expand_path("..", __dir__)
14
+ root cache_stache_root
15
+ coverage_dir File.join(cache_stache_root, "coverage")
16
+
17
+ # Only track files within lib/cache_stache
18
+ add_filter do |source_file|
19
+ !source_file.filename.start_with?(cache_stache_root)
20
+ end
21
+
22
+ add_filter "/spec/"
23
+ add_filter "/bin/"
24
+ add_filter "/tasks/"
25
+ add_filter "/generators/"
26
+
27
+ add_group "Core", ["cache_stache.rb", "configuration.rb", "keyspace.rb"]
28
+ add_group "Storage", ["cache_client.rb"]
29
+ add_group "Instrumentation", ["instrumentation.rb"]
30
+ add_group "Query", ["stats_query.rb"]
31
+ add_group "Web", ["engine.rb", "railtie.rb", "web.rb", "app/"]
32
+
33
+ # Coverage thresholds disabled for now - just tracking coverage
34
+ # minimum_coverage line: 80, branch: 60
35
+ # minimum_coverage_by_file line: 50, branch: 30
36
+ end
37
+ end
38
+
39
+ require "bundler/setup"
40
+
41
+ # Set up Rails environment and load the dummy app (which loads CacheStache)
42
+ ENV["RAILS_ENV"] ||= "test"
43
+ require_relative "dummy_app/config/application"
44
+
45
+ RSpec.configure do |config|
46
+ # Enable flags like --only-failures and --next-failure
47
+ config.example_status_persistence_file_path = ".rspec_status"
48
+
49
+ # Disable RSpec exposing methods globally on `Module` and `main`
50
+ config.disable_monkey_patching!
51
+
52
+ config.expect_with :rspec do |c|
53
+ c.syntax = :expect
54
+ end
55
+
56
+ # Use consistent ordering for tests
57
+ config.order = :random
58
+ Kernel.srand config.seed
59
+
60
+ # Clear configuration before each test
61
+ config.before do
62
+ CacheStache.instance_variable_set(:@configuration, nil)
63
+ end
64
+ end