rspec-time-guard 0.1.0 → 0.2.1
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 +9 -1
- data/README.md +23 -19
- data/lib/rspec_time_guard/railtie.rb +0 -4
- data/lib/rspec_time_guard/version.rb +1 -1
- data/lib/rspec_time_guard.rb +99 -29
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d69f2e3a7c72240510df20aa4d415f38ef214b875e3e5158767b60f578e024f5
|
|
4
|
+
data.tar.gz: 79ed632e66858a540b9a73b3616477f43877f6a395e022a64de7959362a283d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9dad05a04963ee2ca7bbb561cd219a99e03869fb42d5e77af3045e3ce4affccd8c606802682c48c2f30b925417ae80dc0cb65c0c8dc27246c8973887dcde3e1
|
|
7
|
+
data.tar.gz: 98c02103e8ae43889661e373c0544c60fd9fffee2e78f79049653bffdd3bf05b6721f38d053a7b51d2543822e6e58a75fe0a575159f4867441ebd022db867003
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.2.1] - 2026-10-28
|
|
2
|
+
- Use monotonic clock to prevent false timeouts with Timecop
|
|
3
|
+
- Drop invalid `Exception` rescue clause
|
|
4
|
+
- Remove useless logging from Railtie
|
|
5
|
+
- Bump json dependency
|
|
6
|
+
|
|
7
|
+
## [0.2.0] - 2025-04-22
|
|
8
|
+
- Experimental new implementation, using a single-threaded monitoring approach
|
|
9
|
+
|
|
2
10
|
|
|
3
11
|
## [0.1.0] - 2025-03-11
|
|
4
12
|
|
data/README.md
CHANGED
|
@@ -1,33 +1,32 @@
|
|
|
1
|
-
[//]: # (TODO: Add a public TODO list?)
|
|
2
|
-
|
|
3
|
-
# ⚠️ **_This project is a Work In Progress_** ⚠️
|
|
4
|
-
|
|
5
1
|
# RspecTimeGuard
|
|
6
2
|
|
|
7
|
-
`RspecTimeGuard`
|
|
3
|
+
`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
4
|
|
|
5
|
+
Why `RspecTimeGuard`?
|
|
6
|
+
- **🔍 Catches slow tests** with customizable time limits and warnings
|
|
7
|
+
- **🛑 Prevents runaway tests** from blocking your CI pipeline
|
|
8
|
+
- **📊 Identifies bottlenecks** in your test suite to help you optimize
|
|
9
9
|
|
|
10
|
-
## Installation
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
## Quick Start
|
|
13
12
|
|
|
14
13
|
```ruby
|
|
14
|
+
# Add to your Gemfile
|
|
15
15
|
gem 'rspec-time-guard'
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
And then execute:
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
# In spec_helper.rb or rails_helper.rb
|
|
18
|
+
require 'rspec_time_guard'
|
|
22
19
|
```
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
Tag individual tests with specific time limits:
|
|
25
22
|
|
|
26
|
-
```
|
|
27
|
-
|
|
23
|
+
```ruby
|
|
24
|
+
it "completes quickly", time_limit_seconds: 0.5 do
|
|
25
|
+
# This test will fail if it takes more than 0.5 seconds
|
|
26
|
+
expect(fast_operation).to be_truthy
|
|
27
|
+
end
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
31
30
|
## Usage
|
|
32
31
|
|
|
33
32
|
### Basic Setup
|
|
@@ -56,7 +55,7 @@ Create an initializer at `config/initializers/rspec_time_guard.rb` (for Rails) o
|
|
|
56
55
|
RspecTimeGuard.configure do |config|
|
|
57
56
|
# Set a global time limit in seconds for all examples (nil = no global limit)
|
|
58
57
|
config.global_time_limit_seconds = 1.0
|
|
59
|
-
|
|
58
|
+
|
|
60
59
|
# Whether to continue running tests that exceed their time limit
|
|
61
60
|
# true = shows warning but allows test to complete
|
|
62
61
|
# false = raises TimeLimitExceededError and stops the test (default)
|
|
@@ -91,7 +90,7 @@ describe "operations that need more time", time_limit_seconds: 5 do
|
|
|
91
90
|
it "does a complex operation" do
|
|
92
91
|
# ...
|
|
93
92
|
end
|
|
94
|
-
|
|
93
|
+
|
|
95
94
|
it "does another complex operation" do
|
|
96
95
|
# ...
|
|
97
96
|
end
|
|
@@ -149,7 +148,7 @@ RSpec.describe User, type: :model do
|
|
|
149
148
|
user = User.new(email: "invalid")
|
|
150
149
|
expect(user.valid?).to be false
|
|
151
150
|
end
|
|
152
|
-
|
|
151
|
+
|
|
153
152
|
# This test will use the global time limit if configured
|
|
154
153
|
it "can generate a profile" do
|
|
155
154
|
user = User.create(name: "John", email: "john@example.com")
|
|
@@ -168,6 +167,11 @@ RSpec Time Guard works by:
|
|
|
168
167
|
3. Monitoring execution time
|
|
169
168
|
4. Taking action if the time limit is exceeded
|
|
170
169
|
|
|
170
|
+
## Performance and Feedback
|
|
171
|
+
|
|
172
|
+
RSpec Time Guard is a relatively new gem, and while our initial benchmarks show promising results, we're still gathering comprehensive performance data across different environments and use cases. The gem's impact on your test suite may vary depending on factors like test suite size, Ruby interpreter, and CI environment.
|
|
173
|
+
|
|
174
|
+
I'd love to hear about your experience using RSpec Time Guard.
|
|
171
175
|
|
|
172
176
|
## Contributing
|
|
173
177
|
|
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
module RspecTimeGuard
|
|
4
4
|
class Railtie < Rails::Railtie
|
|
5
5
|
initializer "rspec_time_guard.configure" do
|
|
6
|
-
Rails.logger.info("INITIALIZER")
|
|
7
|
-
|
|
8
6
|
config.after_initialize do
|
|
9
|
-
Rails.logger.info("AFTER INITIALIZE")
|
|
10
|
-
|
|
11
7
|
if defined?(RSpec) && Rails.env.test?
|
|
12
8
|
require "rspec_time_guard"
|
|
13
9
|
|
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: Process.clock_gettime(Process::CLOCK_MONOTONIC),
|
|
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 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
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,16 @@ module RspecTimeGuard
|
|
|
16
104
|
@_configuration ||= RspecTimeGuard::Configuration.new
|
|
17
105
|
end
|
|
18
106
|
|
|
19
|
-
|
|
20
|
-
|
|
107
|
+
def monitor
|
|
108
|
+
@_monitor ||= TimeoutMonitor.new
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# TODO: Write article (in 2 parts?)
|
|
112
|
+
# TODO: check warnings on test suite
|
|
113
|
+
# TODO: CHeck how it works with other Ruby interpreters? (check which implem was tested)
|
|
114
|
+
# TODO: Handle RSpec summary manually
|
|
115
|
+
# -> Add a TimeGuard section
|
|
21
116
|
# TODO: Run profiling on the whole test suite to check for performance issues
|
|
22
|
-
# TODO: add PR template
|
|
23
117
|
def setup
|
|
24
118
|
RSpec.configure do |config|
|
|
25
119
|
config.around(:each) do |example|
|
|
@@ -27,36 +121,12 @@ module RspecTimeGuard
|
|
|
27
121
|
|
|
28
122
|
next example.run unless time_limit_seconds
|
|
29
123
|
|
|
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
|
|
124
|
+
RspecTimeGuard.monitor.register_test(example, time_limit_seconds, Thread.current)
|
|
52
125
|
|
|
53
|
-
# NOTE: Main RSpec thread execution
|
|
54
126
|
begin
|
|
55
127
|
example.run
|
|
56
|
-
completed = true
|
|
57
128
|
ensure
|
|
58
|
-
|
|
59
|
-
monitor_thread.kill if monitor_thread.alive?
|
|
129
|
+
RspecTimeGuard.monitor.unregister_test(example)
|
|
60
130
|
end
|
|
61
131
|
end
|
|
62
132
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec-time-guard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Montorio
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: bundler
|
|
@@ -66,7 +65,6 @@ metadata:
|
|
|
66
65
|
homepage_uri: https://github.com/LucasMontorio/rspec-time-guard
|
|
67
66
|
source_code_uri: https://github.com/LucasMontorio/rspec-time-guard
|
|
68
67
|
changelog_uri: https://github.com/LucasMontorio/rspec-time-guard/blob/main/CHANGELOG.md
|
|
69
|
-
post_install_message:
|
|
70
68
|
rdoc_options: []
|
|
71
69
|
require_paths:
|
|
72
70
|
- lib
|
|
@@ -81,8 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
81
79
|
- !ruby/object:Gem::Version
|
|
82
80
|
version: '0'
|
|
83
81
|
requirements: []
|
|
84
|
-
rubygems_version:
|
|
85
|
-
signing_key:
|
|
82
|
+
rubygems_version: 4.0.16
|
|
86
83
|
specification_version: 4
|
|
87
84
|
summary: A simple time keeper for RSpec tests.
|
|
88
85
|
test_files: []
|