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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2236097d6377167a8f7a07f9d37e1d23ca71110aabdfcf0e6b79ca2d9783c8e5
4
- data.tar.gz: d9b596f350b3efef6f8fe67513aa7dd03c933208196cbb31808b8d79e305411b
3
+ metadata.gz: 4bad43592a970ed079b0883df9ffe00474007e363c44647866721dc945a3fd89
4
+ data.tar.gz: 6f0c0134b10dc3305ad99bdfd7bb0074734371e5592162688dc1f5550d9c95b9
5
5
  SHA512:
6
- metadata.gz: 3f8aab7a4360a8226ca00e4501eedca92b7c2fdebffcffb89ae5f92e6e7a700b4e225d37445140e6888e8fa58643dea80be4c06d6ef849a02ddb0aa3297532e2
7
- data.tar.gz: 006735ff24aa6e288d72d043f7efad35169d4af3c481de695ae3dcc2d5cfc868cc29c28139c7107badf77025fbee846a2e90885e6556d709822982711bf23e81
6
+ metadata.gz: 775b1ddce8ca6055891d5b12db00d5e3b25aa40b4c6c14697f6231ed6e45b372ecf9db94666ab7a2d2c251a52a7ba9e0d81de85f48a132e900cd67d76da5dff8
7
+ data.tar.gz: e9097575f7a1d33c07fd339a68294d6b61c9c65397504ef556fc36740c2fde87e48cf154e3b3d50007b0642803476242fc88ca0c5b5f03584dcc5cd5d0745451
data/CHANGELOG.md CHANGED
@@ -1,4 +1,6 @@
1
- ## [Unreleased]
1
+ ## [0.2.0] - 2025-04-22
2
+ - Experimental new implementation, using a single-threaded monitoring approach
3
+
2
4
 
3
5
  ## [0.1.0] - 2025-03-11
4
6
 
data/README.md CHANGED
@@ -4,30 +4,33 @@
4
4
 
5
5
  # RspecTimeGuard
6
6
 
7
- `RspecTimeGuard` helps you identify and manage slow-running tests in your RSpec test suite by setting time limits on individual examples or globally across your test suite.
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
- Add this line to your application's Gemfile:
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
- ```bash
21
- $ bundle install
21
+ # In spec_helper.rb or rails_helper.rb
22
+ require 'rspec_time_guard'
22
23
  ```
23
24
 
24
- Or install it globally:
25
+ Tag individual tests with specific time limits:
25
26
 
26
- ```bash
27
- $ gem install rspec-time-guard
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RspecTimeGuard
4
- VERSION = "0.1.0"
4
+ VERSION = '0.2.0'
5
5
  end
@@ -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
- completed = false
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
- completed = true
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
- # NOTE: We explicitly clean up the monitoring thread in case the example completes before the time limit.
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.1.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-03 00:00:00.000000000 Z
11
+ date: 2025-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler