expect_query 0.1.5
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 +7 -0
- data/.rspec +2 -0
- data/.tool-versions +1 -0
- data/.whitesource +14 -0
- data/README.md +128 -0
- data/Rakefile +13 -0
- data/expect_query.gemspec +43 -0
- data/lib/expect_query/cache_counter.rb +53 -0
- data/lib/expect_query/minitest.rb +74 -0
- data/lib/expect_query/rspec.rb +143 -0
- data/lib/expect_query/sql_counter.rb +31 -0
- data/lib/expect_query/store_patch.rb +12 -0
- data/lib/expect_query/version.rb +5 -0
- data/lib/expect_query.rb +12 -0
- metadata +155 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d3b8b4ed6d2b74521cc8ba8b8c3d916cccb41270c4c41823bc24ece67d51e2ba
|
|
4
|
+
data.tar.gz: 9dad000453c5308bde228f74313820d502efcc8ddab0ea4d64012d1c2cfb8a9d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 74308e0df458e741d4cb8af5b079b164858ab846bfca7d4253fb4c1b5018c8f5aa254b1085d23ec92f44a06fc019b75d20e75837f2df52af7e688d95f9955f5d
|
|
7
|
+
data.tar.gz: bb3009cbc2d35194b6dc4e30aa1807ed70b313d56c8767303eec51e44757224e767f88d171824ad244a2ffb859ec2d3f86c6617ec0d2ce2ca6b739384ec4a94b
|
data/.rspec
ADDED
data/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 4.0.1
|
data/.whitesource
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scanSettings": {
|
|
3
|
+
"baseBranches": []
|
|
4
|
+
},
|
|
5
|
+
"checkRunSettings": {
|
|
6
|
+
"vulnerableCheckRunConclusionLevel": "failure",
|
|
7
|
+
"displayMode": "diff",
|
|
8
|
+
"useMendCheckNames": true
|
|
9
|
+
},
|
|
10
|
+
"issueSettings": {
|
|
11
|
+
"minSeverityLevel": "LOW",
|
|
12
|
+
"issueType": "DEPENDENCY"
|
|
13
|
+
}
|
|
14
|
+
}
|
data/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# ExpectQuery
|
|
2
|
+
|
|
3
|
+
`expect_query` provides Rails 8.0+ compatible assertions and matchers for counting SQL queries and Cache operations. It supports both RSpec and Minitest.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'expect_query', group: :test
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### RSpec
|
|
16
|
+
|
|
17
|
+
Add this to your `spec_helper.rb`:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "expect_query/rspec"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
#### SQL Assertions
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
expect {
|
|
27
|
+
User.first
|
|
28
|
+
}.to make_sql_queries(1)
|
|
29
|
+
|
|
30
|
+
expect {
|
|
31
|
+
User.create(name: "Foo")
|
|
32
|
+
}.to make_sql_queries(1, matching: /INSERT/)
|
|
33
|
+
|
|
34
|
+
expect {
|
|
35
|
+
# ...
|
|
36
|
+
}.to make_sql_queries(at_most: 3)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### Cache Assertions
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
expect {
|
|
43
|
+
Rails.cache.read("foo")
|
|
44
|
+
Rails.cache.write("bar", 1)
|
|
45
|
+
}.to perform_cache_operations(read: 1, write: 1)
|
|
46
|
+
|
|
47
|
+
expect {
|
|
48
|
+
Rails.cache.fetch("foo") { 1 }
|
|
49
|
+
}.to perform_cache_operations(total: 2) # read + write (if miss)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Multiple Cache Stores
|
|
53
|
+
|
|
54
|
+
By default, assertions use `Rails.cache` (if available). You can specify a different store or multiple stores using `store` or `stores` options.
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# RSpec
|
|
58
|
+
expect {
|
|
59
|
+
my_cache.write("foo", 1)
|
|
60
|
+
}.to perform_cache_operations(write: 1, store: my_cache)
|
|
61
|
+
|
|
62
|
+
expect {
|
|
63
|
+
cache1.write("a", 1)
|
|
64
|
+
cache2.write("b", 1)
|
|
65
|
+
}.to perform_cache_operations(total: 2, stores: [cache1, cache2])
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# Minitest
|
|
70
|
+
assert_cache_operations(write: 1, store: my_cache) do
|
|
71
|
+
my_cache.write("foo", 1)
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
require "expect_query/minitest"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### SQL Assertions
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
assert_sql_queries(1) do
|
|
83
|
+
User.first
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### Cache Assertions
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
assert_cache_operations(read: 1, write: 1) do
|
|
91
|
+
Rails.cache.read("foo")
|
|
92
|
+
Rails.cache.write("bar", 1)
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Failure Messages
|
|
97
|
+
|
|
98
|
+
ExpectQuery provides detailed failure messages to help you debug.
|
|
99
|
+
|
|
100
|
+
### SQL Failure Example
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
expected to make 1 SQL queries, but made 2
|
|
104
|
+
Queries run:
|
|
105
|
+
1. SELECT * FROM users
|
|
106
|
+
2. UPDATE users SET name = 'foo'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Cache Failure Example
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
expected to perform cache operations: total 10,
|
|
113
|
+
but got: total 2
|
|
114
|
+
Cache operations performed:
|
|
115
|
+
1. read: foo
|
|
116
|
+
2. write: bar
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
To run tests:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
bundle exec rake
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This runs both RSpec and Minitest suites.
|
|
128
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
2
|
+
require "rspec/core/rake_task"
|
|
3
|
+
require "rake/testtask"
|
|
4
|
+
|
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
6
|
+
|
|
7
|
+
Rake::TestTask.new(:test) do |t|
|
|
8
|
+
t.libs << "test"
|
|
9
|
+
t.libs << "lib"
|
|
10
|
+
t.pattern = "test/**/*_test.rb"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
task default: %i[spec test]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/expect_query/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "expect_query"
|
|
7
|
+
spec.version = ExpectQuery::VERSION
|
|
8
|
+
spec.authors = ["Jade Ornelas"]
|
|
9
|
+
spec.email = ["jade@ornelas.io"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "A gem to assert SQL queries and Cache operations count."
|
|
12
|
+
spec.description = "Provides RSpec matchers and Minitest assertions to count SQL queries and Cache operations, supporting Rails 8.0+."
|
|
13
|
+
spec.homepage = "https://github.com/yknx4/expect_query"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/yknx4/expect_query"
|
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/yknx4/expect_query/blob/main/CHANGELOG.md"
|
|
20
|
+
|
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
25
|
+
(File.expand_path(f) == __FILE__) ||
|
|
26
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
spec.bindir = "exe"
|
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
31
|
+
spec.require_paths = ["lib"]
|
|
32
|
+
|
|
33
|
+
# Rails 8.0+ requirement
|
|
34
|
+
spec.add_dependency "activesupport", ">= 8.0"
|
|
35
|
+
spec.add_dependency "activerecord", ">= 8.0"
|
|
36
|
+
|
|
37
|
+
# Development dependencies
|
|
38
|
+
spec.add_development_dependency "bundler"
|
|
39
|
+
spec.add_development_dependency "rake"
|
|
40
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
41
|
+
spec.add_development_dependency "minitest"
|
|
42
|
+
spec.add_development_dependency "sqlite3"
|
|
43
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExpectQuery
|
|
4
|
+
class CacheCounter
|
|
5
|
+
attr_reader :counts, :log
|
|
6
|
+
|
|
7
|
+
VALID_EVENTS = %w[
|
|
8
|
+
cache_read.active_support
|
|
9
|
+
cache_write.active_support
|
|
10
|
+
cache_fetch_multi.active_support
|
|
11
|
+
cache_read_multi.active_support
|
|
12
|
+
cache_write_multi.active_support
|
|
13
|
+
cache_delete.active_support
|
|
14
|
+
cache_delete_multi.active_support
|
|
15
|
+
cache_exist?.active_support
|
|
16
|
+
cache_increment.active_support
|
|
17
|
+
cache_decrement.active_support
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(matching: nil, store_ids: nil)
|
|
21
|
+
@matching = matching
|
|
22
|
+
@store_ids = store_ids
|
|
23
|
+
@counts = Hash.new(0)
|
|
24
|
+
@log = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(name, _start, _finish, _message_id, payload)
|
|
28
|
+
if @store_ids
|
|
29
|
+
# If we are filtering by store, we must have a matching ID
|
|
30
|
+
return unless @store_ids.include?(payload[:expect_query_store_id])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# name is like "cache_read.active_support"
|
|
34
|
+
# We want to extract "read", "write", etc.
|
|
35
|
+
operation = name.split(".").first.sub(/^cache_/, "")
|
|
36
|
+
|
|
37
|
+
key = payload[:key]
|
|
38
|
+
|
|
39
|
+
if @matching
|
|
40
|
+
# Key can be a string or an array of strings (for multi operations)
|
|
41
|
+
if key.is_a?(Array)
|
|
42
|
+
return unless key.any? { |k| @matching.match?(k.to_s) }
|
|
43
|
+
else
|
|
44
|
+
return unless @matching.match?(key.to_s)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@counts[operation.to_sym] += 1
|
|
49
|
+
@counts[:total] += 1
|
|
50
|
+
@log << { operation: operation, key: key }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExpectQuery
|
|
4
|
+
module MinitestAssertions
|
|
5
|
+
def assert_sql_queries(count = nil, matching: nil, at_most: nil, &block)
|
|
6
|
+
counter = ExpectQuery::SqlCounter.new(matching: matching)
|
|
7
|
+
ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
|
|
8
|
+
block.call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
actual = counter.count
|
|
12
|
+
|
|
13
|
+
debug_info = ""
|
|
14
|
+
if counter.log.any?
|
|
15
|
+
debug_info = "\nQueries run:\n" + counter.log.map.with_index(1) { |sql, i| "#{i}. #{sql}" }.join("\n")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
if at_most
|
|
19
|
+
assert actual <= at_most, "Expected at most #{at_most} SQL queries, but made #{actual}#{debug_info}"
|
|
20
|
+
elsif count
|
|
21
|
+
assert_equal count, actual, "Expected #{count} SQL queries, but made #{actual}#{debug_info}"
|
|
22
|
+
else
|
|
23
|
+
assert actual > 0, "Expected some SQL queries, but made none#{debug_info}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def assert_cache_operations(count = nil, matching: nil, store: nil, stores: nil, **specific_counts, &block)
|
|
28
|
+
|
|
29
|
+
target_stores = Array(stores || store)
|
|
30
|
+
if target_stores.empty? && defined?(::Rails) && ::Rails.respond_to?(:cache)
|
|
31
|
+
target_stores = [::Rails.cache]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
store_ids = []
|
|
35
|
+
target_stores.each do |s|
|
|
36
|
+
unless s.singleton_class < ExpectQuery::StorePatch
|
|
37
|
+
s.singleton_class.prepend(ExpectQuery::StorePatch)
|
|
38
|
+
end
|
|
39
|
+
store_ids << s.object_id
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
pass_ids = store_ids.empty? ? nil : store_ids
|
|
43
|
+
|
|
44
|
+
counter = ExpectQuery::CacheCounter.new(matching: matching, store_ids: pass_ids)
|
|
45
|
+
|
|
46
|
+
subscriber = ActiveSupport::Notifications.subscribe(/cache_.*\.active_support/, counter)
|
|
47
|
+
begin
|
|
48
|
+
block.call
|
|
49
|
+
ensure
|
|
50
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
actual_counts = counter.counts
|
|
54
|
+
|
|
55
|
+
debug_info = ""
|
|
56
|
+
if counter.log.any?
|
|
57
|
+
debug_info = "\nCache operations performed:\n" + counter.log.map.with_index(1) { |entry, i| "#{i}. #{entry[:operation]}: #{entry[:key]}" }.join("\n")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if count
|
|
61
|
+
assert_equal count, actual_counts[:total].to_i, "Expected #{count} total cache operations, but got #{actual_counts[:total].to_i}#{debug_info}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
specific_counts.each do |op, expected|
|
|
65
|
+
actual = actual_counts[op].to_i
|
|
66
|
+
assert_equal expected, actual, "Expected #{expected} #{op} cache operations, but got #{actual}#{debug_info}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if defined?(Minitest::Test)
|
|
73
|
+
Minitest::Test.include ExpectQuery::MinitestAssertions
|
|
74
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec/expectations"
|
|
4
|
+
|
|
5
|
+
module ExpectQuery
|
|
6
|
+
module RSpecMatchers
|
|
7
|
+
def make_sql_queries(count = nil, matching: nil, at_most: nil)
|
|
8
|
+
MakeSqlQueries.new(count, matching: matching, at_most: at_most)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def perform_cache_operations(count = nil, matching: nil, store: nil, stores: nil, **counts)
|
|
12
|
+
PerformCacheOperations.new(count, matching: matching, store: store, stores: stores, **counts)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class MakeSqlQueries
|
|
16
|
+
include RSpec::Matchers::Composable
|
|
17
|
+
|
|
18
|
+
def initialize(expected, matching: nil, at_most: nil)
|
|
19
|
+
@expected = expected
|
|
20
|
+
@matching = matching
|
|
21
|
+
@at_most = at_most
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def supports_block_expectations?
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def matches?(block)
|
|
29
|
+
@counter = ExpectQuery::SqlCounter.new(matching: @matching)
|
|
30
|
+
ActiveSupport::Notifications.subscribed(@counter, "sql.active_record") do
|
|
31
|
+
block.call
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@actual = @counter.count
|
|
35
|
+
|
|
36
|
+
if @at_most
|
|
37
|
+
@actual <= @at_most
|
|
38
|
+
elsif @expected
|
|
39
|
+
@actual == @expected
|
|
40
|
+
else
|
|
41
|
+
@actual > 0 # generic "makes queries"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def failure_message
|
|
46
|
+
msg = "expected to make "
|
|
47
|
+
msg += "#{@expected} " if @expected
|
|
48
|
+
msg += "at most #{@at_most} " if @at_most
|
|
49
|
+
msg += "SQL queries"
|
|
50
|
+
msg += " matching #{@matching.inspect}" if @matching
|
|
51
|
+
msg += ", but made #{@actual}"
|
|
52
|
+
|
|
53
|
+
if @counter.log.any?
|
|
54
|
+
msg += "\nQueries run:\n"
|
|
55
|
+
msg += @counter.log.map.with_index(1) { |sql, i| "#{i}. #{sql}" }.join("\n")
|
|
56
|
+
end
|
|
57
|
+
msg
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class PerformCacheOperations
|
|
62
|
+
include RSpec::Matchers::Composable
|
|
63
|
+
|
|
64
|
+
def initialize(total = nil, matching: nil, store: nil, stores: nil, **specific_counts)
|
|
65
|
+
@total = total
|
|
66
|
+
@matching = matching
|
|
67
|
+
@specific_counts = specific_counts
|
|
68
|
+
@stores = Array(stores || store)
|
|
69
|
+
|
|
70
|
+
if @stores.empty? && defined?(::Rails) && ::Rails.respond_to?(:cache)
|
|
71
|
+
@stores = [::Rails.cache]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def supports_block_expectations?
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def matches?(block)
|
|
80
|
+
# Patch stores
|
|
81
|
+
store_ids = []
|
|
82
|
+
@stores.each do |store|
|
|
83
|
+
unless store.singleton_class < ExpectQuery::StorePatch
|
|
84
|
+
store.singleton_class.prepend(ExpectQuery::StorePatch)
|
|
85
|
+
end
|
|
86
|
+
store_ids << store.object_id
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# If no stores are defined (e.g. no Rails.cache and no store passed), we can't strict filter by ID.
|
|
90
|
+
# But we probably should pass empty array if we intended to track but found nothing?
|
|
91
|
+
# Or if store_ids is empty, maybe we track ALL?
|
|
92
|
+
# User said: "By default use Rails.cache". If Rails.cache is missing, maybe we should warn or track nothing?
|
|
93
|
+
# Let's pass nil if we want to track ALL, but if we defaulted to Rails.cache and it's there, we pass it.
|
|
94
|
+
# If user explicitly passed empty list?
|
|
95
|
+
pass_ids = store_ids.empty? ? nil : store_ids
|
|
96
|
+
|
|
97
|
+
@counter = ExpectQuery::CacheCounter.new(matching: @matching, store_ids: pass_ids)
|
|
98
|
+
|
|
99
|
+
subscriber = ActiveSupport::Notifications.subscribe(/cache_.*\.active_support/, @counter)
|
|
100
|
+
begin
|
|
101
|
+
block.call
|
|
102
|
+
ensure
|
|
103
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
@actual_counts = @counter.counts
|
|
107
|
+
|
|
108
|
+
result = true
|
|
109
|
+
|
|
110
|
+
if @total
|
|
111
|
+
result &&= (@actual_counts[:total].to_i == @total)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
@specific_counts.each do |op, count|
|
|
115
|
+
# op could be :read, :write
|
|
116
|
+
result &&= (@actual_counts[op].to_i == count)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
result
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def failure_message
|
|
123
|
+
msg = "expected to perform cache operations: "
|
|
124
|
+
msg += "total #{@total}, " if @total
|
|
125
|
+
@specific_counts.each { |k, v| msg += "#{k}: #{v}, " }
|
|
126
|
+
msg += "matching #{@matching.inspect}" if @matching
|
|
127
|
+
msg += "\nbut got: "
|
|
128
|
+
msg += "total #{@actual_counts[:total].to_i}, "
|
|
129
|
+
@specific_counts.each { |k, _| msg += "#{k}: #{@actual_counts[k].to_i}, " }
|
|
130
|
+
|
|
131
|
+
if @counter.log.any?
|
|
132
|
+
msg += "\nCache operations performed:\n"
|
|
133
|
+
msg += @counter.log.map.with_index(1) { |entry, i| "#{i}. #{entry[:operation]}: #{entry[:key]}" }.join("\n")
|
|
134
|
+
end
|
|
135
|
+
msg
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
RSpec.configure do |config|
|
|
142
|
+
config.include ExpectQuery::RSpecMatchers
|
|
143
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExpectQuery
|
|
4
|
+
class SqlCounter
|
|
5
|
+
attr_reader :count, :log
|
|
6
|
+
|
|
7
|
+
def initialize(matching: nil)
|
|
8
|
+
@matching = matching
|
|
9
|
+
@count = 0
|
|
10
|
+
@log = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(_name, _start, _finish, _message_id, payload)
|
|
14
|
+
return if payload[:cached]
|
|
15
|
+
|
|
16
|
+
sql = payload[:sql]
|
|
17
|
+
|
|
18
|
+
# Ignore schema queries, transactions, etc if needed.
|
|
19
|
+
# For now, we only care about user queries, but let's keep it simple and filter later if requested.
|
|
20
|
+
# Common ignores: SCHEMA, TRANSACTION, EXPLAIN
|
|
21
|
+
return if %w[SCHEMA TRANSACTION EXPLAIN].include?(payload[:name])
|
|
22
|
+
|
|
23
|
+
if @matching
|
|
24
|
+
return unless @matching.match?(sql)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@count += 1
|
|
28
|
+
@log << sql
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExpectQuery
|
|
4
|
+
module StorePatch
|
|
5
|
+
def instrument(name, key, options = nil, &block)
|
|
6
|
+
options ||= {}
|
|
7
|
+
options = options.dup unless options.frozen? # dup if possible to avoid mutation issues, though usually safe
|
|
8
|
+
options = options.merge(expect_query_store_id: object_id)
|
|
9
|
+
super(name, key, options, &block)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/expect_query.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "expect_query/version"
|
|
4
|
+
require_relative "expect_query/store_patch"
|
|
5
|
+
require_relative "expect_query/sql_counter"
|
|
6
|
+
require_relative "expect_query/cache_counter"
|
|
7
|
+
require "active_support"
|
|
8
|
+
require "active_support/notifications"
|
|
9
|
+
|
|
10
|
+
module ExpectQuery
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: expect_query
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.5
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jade Ornelas
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activerecord
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '8.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '8.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: bundler
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: minitest
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: sqlite3
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
description: Provides RSpec matchers and Minitest assertions to count SQL queries
|
|
111
|
+
and Cache operations, supporting Rails 8.0+.
|
|
112
|
+
email:
|
|
113
|
+
- jade@ornelas.io
|
|
114
|
+
executables: []
|
|
115
|
+
extensions: []
|
|
116
|
+
extra_rdoc_files: []
|
|
117
|
+
files:
|
|
118
|
+
- ".rspec"
|
|
119
|
+
- ".tool-versions"
|
|
120
|
+
- ".whitesource"
|
|
121
|
+
- README.md
|
|
122
|
+
- Rakefile
|
|
123
|
+
- expect_query.gemspec
|
|
124
|
+
- lib/expect_query.rb
|
|
125
|
+
- lib/expect_query/cache_counter.rb
|
|
126
|
+
- lib/expect_query/minitest.rb
|
|
127
|
+
- lib/expect_query/rspec.rb
|
|
128
|
+
- lib/expect_query/sql_counter.rb
|
|
129
|
+
- lib/expect_query/store_patch.rb
|
|
130
|
+
- lib/expect_query/version.rb
|
|
131
|
+
homepage: https://github.com/yknx4/expect_query
|
|
132
|
+
licenses:
|
|
133
|
+
- MIT
|
|
134
|
+
metadata:
|
|
135
|
+
homepage_uri: https://github.com/yknx4/expect_query
|
|
136
|
+
source_code_uri: https://github.com/yknx4/expect_query
|
|
137
|
+
changelog_uri: https://github.com/yknx4/expect_query/blob/main/CHANGELOG.md
|
|
138
|
+
rdoc_options: []
|
|
139
|
+
require_paths:
|
|
140
|
+
- lib
|
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: 3.2.0
|
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - ">="
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '0'
|
|
151
|
+
requirements: []
|
|
152
|
+
rubygems_version: 4.0.3
|
|
153
|
+
specification_version: 4
|
|
154
|
+
summary: A gem to assert SQL queries and Cache operations count.
|
|
155
|
+
test_files: []
|