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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2236097d6377167a8f7a07f9d37e1d23ca71110aabdfcf0e6b79ca2d9783c8e5
4
- data.tar.gz: d9b596f350b3efef6f8fe67513aa7dd03c933208196cbb31808b8d79e305411b
3
+ metadata.gz: d69f2e3a7c72240510df20aa4d415f38ef214b875e3e5158767b60f578e024f5
4
+ data.tar.gz: 79ed632e66858a540b9a73b3616477f43877f6a395e022a64de7959362a283d2
5
5
  SHA512:
6
- metadata.gz: 3f8aab7a4360a8226ca00e4501eedca92b7c2fdebffcffb89ae5f92e6e7a700b4e225d37445140e6888e8fa58643dea80be4c06d6ef849a02ddb0aa3297532e2
7
- data.tar.gz: 006735ff24aa6e288d72d043f7efad35169d4af3c481de695ae3dcc2d5cfc868cc29c28139c7107badf77025fbee846a2e90885e6556d709822982711bf23e81
6
+ metadata.gz: c9dad05a04963ee2ca7bbb561cd219a99e03869fb42d5e77af3045e3ce4affccd8c606802682c48c2f30b925417ae80dc0cb65c0c8dc27246c8973887dcde3e1
7
+ data.tar.gz: 98c02103e8ae43889661e373c0544c60fd9fffee2e78f79049653bffdd3bf05b6721f38d053a7b51d2543822e6e58a75fe0a575159f4867441ebd022db867003
data/CHANGELOG.md CHANGED
@@ -1,4 +1,12 @@
1
- ## [Unreleased]
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` 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.
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
- Add this line to your application's Gemfile:
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
- ```bash
21
- $ bundle install
17
+ # In spec_helper.rb or rails_helper.rb
18
+ require 'rspec_time_guard'
22
19
  ```
23
20
 
24
- Or install it globally:
21
+ Tag individual tests with specific time limits:
25
22
 
26
- ```bash
27
- $ gem install rspec-time-guard
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
 
@@ -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.1"
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: 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
- # TODO: Handle RSpec summary manually?
20
- # TODO: Check that it doesn't break RSpec parallel runs in CI (if any)
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
- 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
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
- # 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?
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.0
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: 2025-04-03 00:00:00.000000000 Z
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: 3.5.11
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: []