rspec-notifications 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +137 -0
- data/LICENSE.txt +21 -0
- data/README.md +106 -0
- data/lib/rspec/notifications/matcher.rb +140 -0
- data/lib/rspec/notifications/payload_matcher.rb +27 -0
- data/lib/rspec/notifications/subscriber.rb +63 -0
- data/lib/rspec/notifications/version.rb +5 -0
- data/lib/rspec/notifications.rb +27 -0
- data/lib/rspec-notifications.rb +1 -0
- data/rspec-notifications.gemspec +21 -0
- metadata +120 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2a6a99147391a7c47e824a7d9b7aa3517feb8725926193f28bfda6a329fea44f
|
|
4
|
+
data.tar.gz: b45d825be7f67e08b68973745c48a2993f468f733563c6e368c7df95d6d557db
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: eb2e953a251fc0e7da9b5b8218efcb033cb78573f9b24dcd888def44a6131624c200f4555364d4e450d5524b4d9e311ee104a01be16b9e005ef85a8ce5c2cefd
|
|
7
|
+
data.tar.gz: 0f593443c3cb6123d2de099fd965d4c51207b351cb5ffb3d583a1a41842e6df4aa3334c9490e104797e118fd0d2894e7c77098a6fae402a91b690d4a4e559303
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## 0.1.0
|
|
2
|
+
- initial release
|
|
3
|
+
- `emit_notification` matcher for `ActiveSupport::Notifications`
|
|
4
|
+
- payload matching via `.with` (partial, nested, embedded matchers)
|
|
5
|
+
- count assertions: `.once`, `.twice`, `.exactly`, `.at_least`, `.at_most`
|
|
6
|
+
- wildcard and regexp notification names
|
|
7
|
+
- `instrument_notification` alias
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
rspec-notifications (0.1.0)
|
|
5
|
+
activesupport (>= 7)
|
|
6
|
+
rspec-expectations (>= 3)
|
|
7
|
+
|
|
8
|
+
GEM
|
|
9
|
+
remote: https://rubygems.org/
|
|
10
|
+
specs:
|
|
11
|
+
activesupport (8.1.3)
|
|
12
|
+
base64
|
|
13
|
+
bigdecimal
|
|
14
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
15
|
+
connection_pool (>= 2.2.5)
|
|
16
|
+
drb
|
|
17
|
+
i18n (>= 1.6, < 2)
|
|
18
|
+
json
|
|
19
|
+
logger (>= 1.4.2)
|
|
20
|
+
minitest (>= 5.1)
|
|
21
|
+
securerandom (>= 0.3)
|
|
22
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
|
23
|
+
uri (>= 0.13.1)
|
|
24
|
+
base64 (0.3.0)
|
|
25
|
+
bigdecimal (4.1.2)
|
|
26
|
+
concurrent-ruby (1.3.6)
|
|
27
|
+
connection_pool (3.0.2)
|
|
28
|
+
date (3.5.1)
|
|
29
|
+
debug (1.11.1)
|
|
30
|
+
irb (~> 1.10)
|
|
31
|
+
reline (>= 0.3.8)
|
|
32
|
+
diff-lcs (1.6.2)
|
|
33
|
+
docile (1.4.1)
|
|
34
|
+
drb (2.2.3)
|
|
35
|
+
erb (6.0.4)
|
|
36
|
+
i18n (1.14.8)
|
|
37
|
+
concurrent-ruby (~> 1.0)
|
|
38
|
+
io-console (0.8.2)
|
|
39
|
+
irb (1.17.0)
|
|
40
|
+
pp (>= 0.6.0)
|
|
41
|
+
prism (>= 1.3.0)
|
|
42
|
+
rdoc (>= 4.0.0)
|
|
43
|
+
reline (>= 0.4.2)
|
|
44
|
+
json (2.19.8)
|
|
45
|
+
logger (1.7.0)
|
|
46
|
+
minitest (6.0.6)
|
|
47
|
+
drb (~> 2.0)
|
|
48
|
+
prism (~> 1.5)
|
|
49
|
+
pp (0.6.3)
|
|
50
|
+
prettyprint
|
|
51
|
+
prettyprint (0.2.0)
|
|
52
|
+
prism (1.9.0)
|
|
53
|
+
psych (5.3.1)
|
|
54
|
+
date
|
|
55
|
+
stringio
|
|
56
|
+
rdoc (7.2.0)
|
|
57
|
+
erb
|
|
58
|
+
psych (>= 4.0.0)
|
|
59
|
+
tsort
|
|
60
|
+
reline (0.6.3)
|
|
61
|
+
io-console (~> 0.5)
|
|
62
|
+
rspec (3.13.2)
|
|
63
|
+
rspec-core (~> 3.13.0)
|
|
64
|
+
rspec-expectations (~> 3.13.0)
|
|
65
|
+
rspec-mocks (~> 3.13.0)
|
|
66
|
+
rspec-core (3.13.6)
|
|
67
|
+
rspec-support (~> 3.13.0)
|
|
68
|
+
rspec-expectations (3.13.5)
|
|
69
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
70
|
+
rspec-support (~> 3.13.0)
|
|
71
|
+
rspec-mocks (3.13.8)
|
|
72
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
73
|
+
rspec-support (~> 3.13.0)
|
|
74
|
+
rspec-support (3.13.7)
|
|
75
|
+
securerandom (0.4.1)
|
|
76
|
+
simplecov (0.22.0)
|
|
77
|
+
docile (~> 1.1)
|
|
78
|
+
simplecov-html (~> 0.11)
|
|
79
|
+
simplecov_json_formatter (~> 0.1)
|
|
80
|
+
simplecov-html (0.13.2)
|
|
81
|
+
simplecov_json_formatter (0.1.4)
|
|
82
|
+
stringio (3.2.0)
|
|
83
|
+
tsort (0.2.0)
|
|
84
|
+
tzinfo (2.0.6)
|
|
85
|
+
concurrent-ruby (~> 1.0)
|
|
86
|
+
uri (1.1.1)
|
|
87
|
+
|
|
88
|
+
PLATFORMS
|
|
89
|
+
ruby
|
|
90
|
+
|
|
91
|
+
DEPENDENCIES
|
|
92
|
+
debug
|
|
93
|
+
rspec
|
|
94
|
+
rspec-notifications!
|
|
95
|
+
simplecov
|
|
96
|
+
|
|
97
|
+
CHECKSUMS
|
|
98
|
+
activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e
|
|
99
|
+
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
100
|
+
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
|
|
101
|
+
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
|
|
102
|
+
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
|
103
|
+
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
|
|
104
|
+
debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
|
|
105
|
+
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
|
|
106
|
+
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
|
|
107
|
+
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
|
108
|
+
erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
|
|
109
|
+
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
|
|
110
|
+
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
|
|
111
|
+
irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae
|
|
112
|
+
json (2.19.8) sha256=6354310fd76ef69b87d5bd1f38b40d730613baf90b6803d2d0a48f618d32dfaa
|
|
113
|
+
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
114
|
+
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
|
|
115
|
+
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
|
|
116
|
+
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
|
|
117
|
+
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
118
|
+
psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
|
|
119
|
+
rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
|
|
120
|
+
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
|
121
|
+
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
122
|
+
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
|
123
|
+
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
|
124
|
+
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
|
|
125
|
+
rspec-notifications (0.1.0)
|
|
126
|
+
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
|
|
127
|
+
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
|
|
128
|
+
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
|
|
129
|
+
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
|
|
130
|
+
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
|
|
131
|
+
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
|
|
132
|
+
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
|
133
|
+
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
|
|
134
|
+
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
|
|
135
|
+
|
|
136
|
+
BUNDLED WITH
|
|
137
|
+
4.0.9
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Pepper
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
rspec-notifications
|
|
2
|
+
======
|
|
3
|
+

|
|
4
|
+
[](https://codecov.io/gh/dpep/rspec-notifications)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
RSpec matchers for testing `ActiveSupport::Notifications`.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "rspec/notifications"
|
|
11
|
+
|
|
12
|
+
expect {
|
|
13
|
+
service.call
|
|
14
|
+
}.to emit_notification("user.created")
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The matcher subscribes to `ActiveSupport::Notifications` before running the
|
|
18
|
+
block and unsubscribes afterward, so there is nothing to set up or tear down.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Payloads
|
|
22
|
+
|
|
23
|
+
Match the payload with `.with`. Matching is partial — only the keys you name
|
|
24
|
+
are checked, so extra payload keys are ignored.
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
expect {
|
|
28
|
+
service.call
|
|
29
|
+
}.to emit_notification("user.created").with(user_id: user.id)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Values support embedded RSpec matchers, nested hashes, and any composable
|
|
33
|
+
matcher:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
expect {
|
|
37
|
+
service.call
|
|
38
|
+
}.to emit_notification("user.created").with(user_id: kind_of(Integer))
|
|
39
|
+
|
|
40
|
+
# nested hashes match exactly; use a_hash_including for a nested subset
|
|
41
|
+
expect {
|
|
42
|
+
service.call
|
|
43
|
+
}.to emit_notification("user.created").with(user: a_hash_including(role: "admin"))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Counts
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
expect { service.call }.to emit_notification("user.created").once
|
|
51
|
+
expect { service.call }.to emit_notification("user.created").twice
|
|
52
|
+
expect { service.call }.to emit_notification("user.created").exactly(3).times
|
|
53
|
+
expect { service.call }.to emit_notification("user.created").at_least(2).times
|
|
54
|
+
expect { service.call }.to emit_notification("user.created").at_most(2).times
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Without a count, the matcher passes when the notification is emitted at least
|
|
58
|
+
once. Counts apply to events matching both the name and the payload.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## Names
|
|
62
|
+
|
|
63
|
+
Names can be matched exactly, with a `*` wildcard, or with a `Regexp`:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
emit_notification("user.created") # exact
|
|
67
|
+
emit_notification("user.*") # wildcard
|
|
68
|
+
emit_notification(/user\./) # regexp
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## Negation
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
expect { service.call }.not_to emit_notification("user.created")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
## Aliases
|
|
80
|
+
|
|
81
|
+
`instrument_notification` is an alias for `emit_notification`.
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
## Edge cases
|
|
85
|
+
|
|
86
|
+
- **Nested notifications** — each instrumented event is captured independently,
|
|
87
|
+
including notifications instrumented within the block of another.
|
|
88
|
+
- **Exceptions** — if the block raises, the exception propagates (the matcher
|
|
89
|
+
still unsubscribes). When `ActiveSupport::Notifications.instrument` re-raises,
|
|
90
|
+
it adds `:exception` / `:exception_object` to the payload, which you can match
|
|
91
|
+
on with `.with`.
|
|
92
|
+
- **Concurrency** — notifications delivered from other threads during the block
|
|
93
|
+
are captured too; appends are guarded by a mutex.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
----
|
|
97
|
+
## Contributing
|
|
98
|
+
|
|
99
|
+
Yes please :)
|
|
100
|
+
|
|
101
|
+
1. Fork it
|
|
102
|
+
1. Create your feature branch (`git checkout -b my-feature`)
|
|
103
|
+
1. Ensure the tests pass (`bundle exec rspec`)
|
|
104
|
+
1. Commit your changes (`git commit -am 'awesome new feature'`)
|
|
105
|
+
1. Push your branch (`git push origin my-feature`)
|
|
106
|
+
1. Create a Pull Request
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
module RSpec
|
|
2
|
+
module Notifications
|
|
3
|
+
# RSpec matcher asserting that a block emits an ActiveSupport::Notifications
|
|
4
|
+
# event. See RSpec::Matchers#emit_notification for the public entry point.
|
|
5
|
+
class Matcher
|
|
6
|
+
include RSpec::Matchers::Composable
|
|
7
|
+
|
|
8
|
+
def initialize(pattern)
|
|
9
|
+
@pattern = pattern
|
|
10
|
+
@expected_payload = nil
|
|
11
|
+
@count_type = nil
|
|
12
|
+
@count = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# --- chained expectations ------------------------------------------
|
|
16
|
+
|
|
17
|
+
def with(payload)
|
|
18
|
+
@expected_payload = payload
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def once
|
|
23
|
+
exactly(1)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def twice
|
|
27
|
+
exactly(2)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def exactly(count)
|
|
31
|
+
set_count(:exactly, count)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def at_least(count)
|
|
35
|
+
set_count(:at_least, count)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def at_most(count)
|
|
39
|
+
set_count(:at_most, count)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# syntactic sugar: exactly(n).times
|
|
43
|
+
def times
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# --- matcher protocol ----------------------------------------------
|
|
48
|
+
|
|
49
|
+
def matches?(block)
|
|
50
|
+
@events = Subscriber.capture(subscribe_pattern, &block)
|
|
51
|
+
@matching_events = @events.select do |event|
|
|
52
|
+
PayloadMatcher.matches?(@expected_payload, event.payload)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
count_satisfied?(@matching_events.size)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def does_not_match?(block)
|
|
59
|
+
!matches?(block)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def supports_block_expectations?
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def description
|
|
67
|
+
desc = "emit #{@pattern.inspect} notification"
|
|
68
|
+
desc += " with payload #{description_of(@expected_payload)}" if @expected_payload
|
|
69
|
+
desc += " #{count_description}" if @count_type
|
|
70
|
+
desc
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def failure_message
|
|
74
|
+
"expected block to #{description}, but #{observed_summary}#{emitted_breakdown}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def failure_message_when_negated
|
|
78
|
+
"expected block not to #{description}, but #{observed_summary}#{emitted_breakdown}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def set_count(type, count)
|
|
84
|
+
@count_type = type
|
|
85
|
+
@count = count
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# ActiveSupport::Notifications.subscribe natively matches a String exactly
|
|
90
|
+
# or a Regexp via ===. A String containing "*" is treated as a wildcard
|
|
91
|
+
# and converted to an anchored Regexp.
|
|
92
|
+
def subscribe_pattern
|
|
93
|
+
return @pattern unless @pattern.is_a?(String) && @pattern.include?("*")
|
|
94
|
+
|
|
95
|
+
segments = @pattern.split("*", -1).map { |segment| Regexp.escape(segment) }
|
|
96
|
+
Regexp.new("\\A#{segments.join(".*")}\\z")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def count_satisfied?(count)
|
|
100
|
+
case @count_type
|
|
101
|
+
when :exactly then count == @count
|
|
102
|
+
when :at_least then count >= @count
|
|
103
|
+
when :at_most then count <= @count
|
|
104
|
+
else count >= 1
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def count_description
|
|
109
|
+
"#{@count_type.to_s.tr("_", " ")} #{pluralize(@count, "time")}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def observed_summary
|
|
113
|
+
matched = @matching_events.size
|
|
114
|
+
|
|
115
|
+
if @expected_payload && @events.any?
|
|
116
|
+
"#{pluralize(matched, "matching notification")} were emitted " \
|
|
117
|
+
"(#{pluralize(@events.size, "notification")} matched the name)"
|
|
118
|
+
elsif matched.zero?
|
|
119
|
+
"no matching notifications were emitted"
|
|
120
|
+
else
|
|
121
|
+
"it was emitted #{pluralize(matched, "time")}"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def emitted_breakdown
|
|
126
|
+
return "" if @events.empty?
|
|
127
|
+
|
|
128
|
+
lines = @events.map do |event|
|
|
129
|
+
" - #{event.name.inspect} #{event.payload.inspect}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
"\nemitted notifications:\n#{lines.join("\n")}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def pluralize(count, noun)
|
|
136
|
+
"#{count} #{noun}#{"s" unless count == 1}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require "rspec/support/fuzzy_matcher"
|
|
2
|
+
|
|
3
|
+
module RSpec
|
|
4
|
+
module Notifications
|
|
5
|
+
# Matches an expected payload against an actual notification payload.
|
|
6
|
+
#
|
|
7
|
+
# Matching is partial at the top level: only the keys named in +expected+
|
|
8
|
+
# are checked, so unrelated payload keys are ignored. Values are compared
|
|
9
|
+
# with RSpec's fuzzy matching, which supports embedded matchers (e.g.
|
|
10
|
+
# +kind_of(Integer)+), regexps, ranges, and nested structures. Nested
|
|
11
|
+
# hashes are compared exactly -- use +a_hash_including(...)+ for a nested
|
|
12
|
+
# partial match.
|
|
13
|
+
module PayloadMatcher
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def matches?(expected, actual)
|
|
17
|
+
return true if expected.nil?
|
|
18
|
+
return false unless actual.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
expected.all? do |key, value|
|
|
21
|
+
actual.key?(key) &&
|
|
22
|
+
RSpec::Support::FuzzyMatcher.values_match?(value, actual.fetch(key))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module RSpec
|
|
2
|
+
module Notifications
|
|
3
|
+
# Subscribes to ActiveSupport::Notifications for the duration of a block,
|
|
4
|
+
# capturing every matching event, then unsubscribes.
|
|
5
|
+
#
|
|
6
|
+
# Thread-safe: notifications may be delivered from threads other than the
|
|
7
|
+
# one running the block, so appends are guarded by a mutex.
|
|
8
|
+
class Subscriber
|
|
9
|
+
Event = Struct.new(
|
|
10
|
+
:name,
|
|
11
|
+
:payload,
|
|
12
|
+
:started,
|
|
13
|
+
:finished,
|
|
14
|
+
:transaction_id,
|
|
15
|
+
keyword_init: true,
|
|
16
|
+
) do
|
|
17
|
+
# Duration in milliseconds, when both timestamps are available.
|
|
18
|
+
def duration
|
|
19
|
+
return unless started && finished
|
|
20
|
+
|
|
21
|
+
(finished - started) * 1_000.0
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Subscribe to +pattern+, run +block+, and return the captured events.
|
|
26
|
+
def self.capture(pattern, &block)
|
|
27
|
+
new(pattern).capture(&block)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(pattern)
|
|
31
|
+
@pattern = pattern
|
|
32
|
+
@events = []
|
|
33
|
+
@mutex = Mutex.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def capture
|
|
37
|
+
subscription = subscribe
|
|
38
|
+
yield
|
|
39
|
+
@mutex.synchronize { @events.dup }
|
|
40
|
+
ensure
|
|
41
|
+
ActiveSupport::Notifications.unsubscribe(subscription) if subscription
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def subscribe
|
|
47
|
+
# A 5-arity block opts into the "timed" subscriber, which yields the
|
|
48
|
+
# start and finish times along with the transaction id and payload.
|
|
49
|
+
ActiveSupport::Notifications.subscribe(@pattern) do |name, started, finished, id, payload|
|
|
50
|
+
event = Event.new(
|
|
51
|
+
name: name,
|
|
52
|
+
payload: payload.dup,
|
|
53
|
+
started: started,
|
|
54
|
+
finished: finished,
|
|
55
|
+
transaction_id: id,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@mutex.synchronize { @events << event }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require "active_support/isolated_execution_state"
|
|
2
|
+
require "active_support/notifications"
|
|
3
|
+
require "rspec/expectations"
|
|
4
|
+
|
|
5
|
+
require_relative "notifications/version"
|
|
6
|
+
require_relative "notifications/subscriber"
|
|
7
|
+
require_relative "notifications/payload_matcher"
|
|
8
|
+
require_relative "notifications/matcher"
|
|
9
|
+
|
|
10
|
+
module RSpec
|
|
11
|
+
module Matchers
|
|
12
|
+
# Asserts that the block emits a matching ActiveSupport::Notifications event.
|
|
13
|
+
#
|
|
14
|
+
# expect { service.call }.to emit_notification("user.created")
|
|
15
|
+
# expect { service.call }.to emit_notification("user.created").with(user_id: 1)
|
|
16
|
+
# expect { service.call }.to emit_notification("user.created").twice
|
|
17
|
+
# expect { service.call }.to emit_notification("user.*")
|
|
18
|
+
# expect { service.call }.to emit_notification(/user\./)
|
|
19
|
+
#
|
|
20
|
+
# +pattern+ may be an exact String, a String with "*" wildcards, or a Regexp.
|
|
21
|
+
def emit_notification(pattern)
|
|
22
|
+
RSpec::Notifications::Matcher.new(pattern)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
alias_method :instrument_notification, :emit_notification
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "rspec/notifications"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require_relative "lib/rspec/notifications/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |s|
|
|
4
|
+
s.name = "rspec-notifications"
|
|
5
|
+
s.version = RSpec::Notifications::VERSION
|
|
6
|
+
s.authors = ["Daniel Pepper"]
|
|
7
|
+
s.description = "RSpec matchers for ActiveSupport::Notifications"
|
|
8
|
+
s.files = `git ls-files * ':!:spec'`.split("\n")
|
|
9
|
+
s.homepage = "https://github.com/dpep/rspec-notifications"
|
|
10
|
+
s.license = "MIT"
|
|
11
|
+
s.summary = "RSpec::Notifications"
|
|
12
|
+
|
|
13
|
+
s.required_ruby_version = ">= 3.2"
|
|
14
|
+
|
|
15
|
+
s.add_dependency "activesupport", ">= 7"
|
|
16
|
+
s.add_dependency "rspec-expectations", ">= 3"
|
|
17
|
+
|
|
18
|
+
s.add_development_dependency "debug"
|
|
19
|
+
s.add_development_dependency "rspec"
|
|
20
|
+
s.add_development_dependency "simplecov"
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rspec-notifications
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Daniel Pepper
|
|
8
|
+
bindir: bin
|
|
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: '7'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec-expectations
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: debug
|
|
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: rspec
|
|
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: simplecov
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
description: RSpec matchers for ActiveSupport::Notifications
|
|
83
|
+
executables: []
|
|
84
|
+
extensions: []
|
|
85
|
+
extra_rdoc_files: []
|
|
86
|
+
files:
|
|
87
|
+
- CHANGELOG.md
|
|
88
|
+
- Gemfile
|
|
89
|
+
- Gemfile.lock
|
|
90
|
+
- LICENSE.txt
|
|
91
|
+
- README.md
|
|
92
|
+
- lib/rspec-notifications.rb
|
|
93
|
+
- lib/rspec/notifications.rb
|
|
94
|
+
- lib/rspec/notifications/matcher.rb
|
|
95
|
+
- lib/rspec/notifications/payload_matcher.rb
|
|
96
|
+
- lib/rspec/notifications/subscriber.rb
|
|
97
|
+
- lib/rspec/notifications/version.rb
|
|
98
|
+
- rspec-notifications.gemspec
|
|
99
|
+
homepage: https://github.com/dpep/rspec-notifications
|
|
100
|
+
licenses:
|
|
101
|
+
- MIT
|
|
102
|
+
metadata: {}
|
|
103
|
+
rdoc_options: []
|
|
104
|
+
require_paths:
|
|
105
|
+
- lib
|
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '3.2'
|
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0'
|
|
116
|
+
requirements: []
|
|
117
|
+
rubygems_version: 3.6.9
|
|
118
|
+
specification_version: 4
|
|
119
|
+
summary: RSpec::Notifications
|
|
120
|
+
test_files: []
|