rspec-time-guard 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +3 -1
- data/README.md +20 -15
- data/lib/rspec_time_guard/version.rb +1 -1
- data/lib/rspec_time_guard.rb +102 -28
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4bad43592a970ed079b0883df9ffe00474007e363c44647866721dc945a3fd89
|
4
|
+
data.tar.gz: 6f0c0134b10dc3305ad99bdfd7bb0074734371e5592162688dc1f5550d9c95b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 775b1ddce8ca6055891d5b12db00d5e3b25aa40b4c6c14697f6231ed6e45b372ecf9db94666ab7a2d2c251a52a7ba9e0d81de85f48a132e900cd67d76da5dff8
|
7
|
+
data.tar.gz: e9097575f7a1d33c07fd339a68294d6b61c9c65397504ef556fc36740c2fde87e48cf154e3b3d50007b0642803476242fc88ca0c5b5f03584dcc5cd5d0745451
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -4,30 +4,33 @@
|
|
4
4
|
|
5
5
|
# RspecTimeGuard
|
6
6
|
|
7
|
-
`RspecTimeGuard`
|
7
|
+
`RspecTimeGuard` monitors your tests as they run and provides safeguards against tests that take too long, helping you maintain a fast, reliable test suite.
|
8
8
|
|
9
|
+
Why `RspecTimeGuard`?
|
10
|
+
- **🔍 Catches slow tests** with customizable time limits and warnings
|
11
|
+
- **🛑 Prevents runaway tests** from blocking your CI pipeline
|
12
|
+
- **📊 Identifies bottlenecks** in your test suite to help you optimize
|
9
13
|
|
10
|
-
## Installation
|
11
14
|
|
12
|
-
|
15
|
+
## Quick Start
|
13
16
|
|
14
17
|
```ruby
|
18
|
+
# Add to your Gemfile
|
15
19
|
gem 'rspec-time-guard'
|
16
|
-
```
|
17
|
-
|
18
|
-
And then execute:
|
19
20
|
|
20
|
-
|
21
|
-
|
21
|
+
# In spec_helper.rb or rails_helper.rb
|
22
|
+
require 'rspec_time_guard'
|
22
23
|
```
|
23
24
|
|
24
|
-
|
25
|
+
Tag individual tests with specific time limits:
|
25
26
|
|
26
|
-
```
|
27
|
-
|
27
|
+
```ruby
|
28
|
+
it "completes quickly", time_limit_seconds: 0.5 do
|
29
|
+
# This test will fail if it takes more than 0.5 seconds
|
30
|
+
expect(fast_operation).to be_truthy
|
31
|
+
end
|
28
32
|
```
|
29
33
|
|
30
|
-
|
31
34
|
## Usage
|
32
35
|
|
33
36
|
### Basic Setup
|
@@ -56,7 +59,7 @@ Create an initializer at `config/initializers/rspec_time_guard.rb` (for Rails) o
|
|
56
59
|
RspecTimeGuard.configure do |config|
|
57
60
|
# Set a global time limit in seconds for all examples (nil = no global limit)
|
58
61
|
config.global_time_limit_seconds = 1.0
|
59
|
-
|
62
|
+
|
60
63
|
# Whether to continue running tests that exceed their time limit
|
61
64
|
# true = shows warning but allows test to complete
|
62
65
|
# false = raises TimeLimitExceededError and stops the test (default)
|
@@ -91,7 +94,7 @@ describe "operations that need more time", time_limit_seconds: 5 do
|
|
91
94
|
it "does a complex operation" do
|
92
95
|
# ...
|
93
96
|
end
|
94
|
-
|
97
|
+
|
95
98
|
it "does another complex operation" do
|
96
99
|
# ...
|
97
100
|
end
|
@@ -149,7 +152,7 @@ RSpec.describe User, type: :model do
|
|
149
152
|
user = User.new(email: "invalid")
|
150
153
|
expect(user.valid?).to be false
|
151
154
|
end
|
152
|
-
|
155
|
+
|
153
156
|
# This test will use the global time limit if configured
|
154
157
|
it "can generate a profile" do
|
155
158
|
user = User.create(name: "John", email: "john@example.com")
|
@@ -168,6 +171,8 @@ RSpec Time Guard works by:
|
|
168
171
|
3. Monitoring execution time
|
169
172
|
4. Taking action if the time limit is exceeded
|
170
173
|
|
174
|
+
### Performance Considerations
|
175
|
+
> ⚠️ **Note**: Setting a global time limit with `global_time_limit_seconds` creates a monitoring thread for each test in your suite. This may result in slightly reduced performance, especially in large test suites. For optimal performance, you might consider applying time limits only to specific tests that are prone to slowness rather than setting a global limit.
|
171
176
|
|
172
177
|
## Contributing
|
173
178
|
|
data/lib/rspec_time_guard.rb
CHANGED
@@ -7,6 +7,94 @@ require "rspec_time_guard/railtie" if defined?(Rails)
|
|
7
7
|
module RspecTimeGuard
|
8
8
|
class TimeLimitExceededError < StandardError; end
|
9
9
|
|
10
|
+
class TimeoutMonitor
|
11
|
+
def initialize
|
12
|
+
@active_tests = {}
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@monitor_thread = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def register_test(example, timeout, thread)
|
18
|
+
@mutex.synchronize do
|
19
|
+
@active_tests[example.object_id] = {
|
20
|
+
example: example,
|
21
|
+
start_time: Time.now,
|
22
|
+
timeout: timeout,
|
23
|
+
thread_id: thread.object_id,
|
24
|
+
warned: false
|
25
|
+
}
|
26
|
+
|
27
|
+
# NOTE: We start monitor thread if not already running
|
28
|
+
start_monitor_thread if @monitor_thread.nil? || !@monitor_thread.alive?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def unregister_test(example)
|
33
|
+
@mutex.synchronize do
|
34
|
+
@active_tests.delete(example.object_id)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def start_monitor_thread
|
41
|
+
@monitor_thread = Thread.new do
|
42
|
+
Thread.current[:name] = "rspec_time_guard_monitor"
|
43
|
+
|
44
|
+
loop do
|
45
|
+
check_for_timeouts
|
46
|
+
sleep 0.5 # Check every half second, adjust as needed
|
47
|
+
|
48
|
+
# Exit thread if no more tests to monitor
|
49
|
+
break if @mutex.synchronize { @active_tests.empty? }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def check_for_timeouts
|
55
|
+
now = Time.now
|
56
|
+
timed_out_examples = []
|
57
|
+
|
58
|
+
@mutex.synchronize do
|
59
|
+
@active_tests.each do |_, info|
|
60
|
+
elapsed = now - info[:start_time]
|
61
|
+
timed_out_examples << info[:example] if elapsed > info[:timeout]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# NOTE: We handle timeouts outside the mutex to avoid deadlocks
|
66
|
+
timed_out_examples.each do |example|
|
67
|
+
group_name = example.example_group.description
|
68
|
+
test_name = example.description
|
69
|
+
timeout = @active_tests[example.object_id][:timeout]
|
70
|
+
elapsed = now - @active_tests[example.object_id][:start_time]
|
71
|
+
thread = begin
|
72
|
+
ObjectSpace._id2ref(@active_tests[example.object_id][:thread_id])
|
73
|
+
rescue
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
next unless thread.alive?
|
78
|
+
|
79
|
+
# NOTE: We create an error for RSpec to report
|
80
|
+
error = TimeLimitExceededError.new("Test '#{group_name} #{test_name}' timed out after #{timeout}s (took #{elapsed.round(2)}s)")
|
81
|
+
error.set_backtrace(thread.backtrace || [])
|
82
|
+
|
83
|
+
if RspecTimeGuard.configuration.continue_on_timeout
|
84
|
+
next if @active_tests[example.object_id][:warned]
|
85
|
+
|
86
|
+
warn "WARNING [RSpecTimeGuard] - #{error.message}"
|
87
|
+
|
88
|
+
@active_tests[example.object_id][:warned] = true
|
89
|
+
else
|
90
|
+
# NOTE: We use Thread.raise which is safer than Thread.kill
|
91
|
+
# This allows the thread to clean up properly
|
92
|
+
thread.raise(error)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
10
98
|
class << self
|
11
99
|
def configure
|
12
100
|
yield(configuration)
|
@@ -16,10 +104,17 @@ module RspecTimeGuard
|
|
16
104
|
@_configuration ||= RspecTimeGuard::Configuration.new
|
17
105
|
end
|
18
106
|
|
107
|
+
def monitor
|
108
|
+
@_monitor ||= TimeoutMonitor.new
|
109
|
+
end
|
110
|
+
|
111
|
+
# TODO: check warnings on test suite
|
112
|
+
# TODO: run test suite with new implementation and compare perfs
|
113
|
+
# TODO: speed up test suite
|
114
|
+
# => implement the 'single monitor thread' alternative
|
115
|
+
# TODO: CHeck how it works with other Ruby interpreters? (check which implem was tested)
|
19
116
|
# TODO: Handle RSpec summary manually?
|
20
|
-
# TODO: Check that it doesn't break RSpec parallel runs in CI (if any)
|
21
117
|
# TODO: Run profiling on the whole test suite to check for performance issues
|
22
|
-
# TODO: add PR template
|
23
118
|
def setup
|
24
119
|
RSpec.configure do |config|
|
25
120
|
config.around(:each) do |example|
|
@@ -27,36 +122,15 @@ module RspecTimeGuard
|
|
27
122
|
|
28
123
|
next example.run unless time_limit_seconds
|
29
124
|
|
30
|
-
|
31
|
-
|
32
|
-
# NOTE: We instantiate a monitoring thread, to allow the example to run in the main RSpec thread.
|
33
|
-
# This is required to keep the RSpec context.
|
34
|
-
monitor_thread = Thread.new do
|
35
|
-
Thread.current.report_on_exception = false
|
36
|
-
|
37
|
-
# NOTE: The following logic:
|
38
|
-
# - Waits for the duration of the time limit
|
39
|
-
# - If the main thread is still running at that stage, raises a TimeLimitExceededError
|
40
|
-
sleep time_limit_seconds
|
41
|
-
|
42
|
-
unless completed
|
43
|
-
message = "[RspecTimeGuard] Example exceeded timeout of #{time_limit_seconds} seconds"
|
44
|
-
if RspecTimeGuard.configuration.continue_on_timeout
|
45
|
-
warn "#{message} - Running the example anyway (:continue_on_timeout option set to TRUE)"
|
46
|
-
example.run
|
47
|
-
else
|
48
|
-
Thread.main.raise RspecTimeGuard::TimeLimitExceededError, message
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
125
|
+
RspecTimeGuard.monitor.register_test(example, time_limit_seconds, Thread.current)
|
52
126
|
|
53
|
-
# NOTE: Main RSpec thread execution
|
54
127
|
begin
|
55
128
|
example.run
|
56
|
-
|
129
|
+
rescue RspecTimeGuard::TimeLimitExceededError => e
|
130
|
+
# NOTE: This changes the example's status to failed and records our error
|
131
|
+
example.exception = e
|
57
132
|
ensure
|
58
|
-
|
59
|
-
monitor_thread.kill if monitor_thread.alive?
|
133
|
+
RspecTimeGuard.monitor.unregister_test(example)
|
60
134
|
end
|
61
135
|
end
|
62
136
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-time-guard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lucas Montorio
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-04-
|
11
|
+
date: 2025-04-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|