angry_io 0.5.0 → 0.10.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 +11 -25
- data/README.md +9 -4
- data/lib/angry_io/minitest.rb +6 -6
- data/lib/angry_io/rspec.rb +9 -10
- data/lib/angry_io/version.rb +1 -1
- data/lib/angry_io.rb +143 -115
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b3f6e7d3135cb218b85c49be54b4be8f023d3349d73ad3caf225addcf161fbe
|
|
4
|
+
data.tar.gz: b7e58a351af2597e9cf33598f8e08c70428384da5ce125d24f4f7ac86dc6cb54
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc6d5b2543aa8e10b3eef820ce422d3df46fa80b970d87de294d7266bcfc2ca0124cee0fc365a38c35029248f6cbb3daa4b0ad1a943e3295ec08521b3ac56cc1
|
|
7
|
+
data.tar.gz: 66448faebe489ae41a5a3f031339e646738cea271c4e8cc9764fb022dd469c0960baa8990a3de9c2ab72c93a391d9cf89ab8f078da28c8d3b4c31aad666410da
|
data/CHANGELOG.md
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
## [0.
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
## [0.3.0] - 2026-09-06
|
|
15
|
-
|
|
16
|
-
- Zero-byte writes (e.g. `$stderr.print("")`) no longer raise; `AngryIo::Stream` now raises only when a write would actually emit output. The error message changed from StringIO's `not opened for writing` to `AngryIo::Stream is not writable: ...`, which includes the offending output.
|
|
17
|
-
- Minitest's `capture_subprocess_io` and RSpec's `to_stdout_from_any_process` / `to_stderr_from_any_process` matchers now work alongside AngryIo. These helpers reopen `$stdout`/`$stderr` onto Tempfiles so subprocesses inherit the file descriptors, which fails on the StringIO-based `AngryIo::Stream`; AngryIo now restores the real streams for their duration via a new `AngryIo.with_real_streams` helper.
|
|
18
|
-
|
|
19
|
-
## [0.2.0] - 2026-09-04
|
|
20
|
-
|
|
21
|
-
- Remove the `angry_io/enable_for_ci_true` convenience require; require `angry_io/rspec` or `angry_io/minitest` directly instead.
|
|
22
|
-
- Remove the configurable `opt_out_metadata` field; the RSpec opt-out metadata is now always `:i_absolutely_need_to_write_to_stdout`.
|
|
23
|
-
- Adapters no longer swallow `LoadError` when their framework is missing; requiring an adapter now hard-requires its framework.
|
|
24
|
-
|
|
25
|
-
## [0.1.0] - 2026-09-04
|
|
26
|
-
|
|
27
|
-
- Initial release
|
|
3
|
+
## [0.10.0] - 2026-09-18
|
|
4
|
+
|
|
5
|
+
- AngryIO guards the real `STDOUT`/`STDERR` objects during tests — a write goes through to the stream and then raises `IOError`, so accidental output fails loudly and the offending line is visible in the test output right before the failure. Because the guard lives on the stream objects themselves, code holding earlier references (e.g. `Logger.new($stdout)` at boot) cannot bypass it; `Kernel#warn` and `Warning.warn` are intercepted at the source, since they write to stderr without dispatching to `$stderr#write`.
|
|
6
|
+
- Self-registering adapters for Minitest and RSpec arm the guard around each test, with an opt-out per test class / example via `i_absolutely_need_to_write_to_stdout`.
|
|
7
|
+
- `AngryIO.enabled` is a callable gating enforcement (default `-> { true }`), invoked once per test — e.g. `-> { ENV["CI"] == "true" }` to enforce on CI while leaving local debuggers usable.
|
|
8
|
+
- Known limits: the arming flag is thread-local, so writes from threads spawned mid-test are not guarded, and rebinding `$stdout` to another IO bypasses the guard.
|
|
9
|
+
- Zero-byte writes (e.g. `$stderr.print("")`) emit nothing and are allowed; anything that would produce output raises. `warn("")` still raises: `warn` appends a newline per message, so an empty message is not a zero-byte write.
|
|
10
|
+
- Minitest's `capture_subprocess_io` and RSpec's `to_stdout_from_any_process` / `to_stderr_from_any_process` matchers work alongside AngryIO: they reopen the streams onto Tempfiles so subprocesses inherit the file descriptors, and the adapters run them under the new `AngryIO.disarmed` so the block's Ruby-level writes are captured instead of raising. `capture_io`, `assert_output`, and `assert_silent` rebind the globals to unguarded StringIOs and already worked.
|
|
11
|
+
- Reopening a guarded stream (e.g. `$stdout.reopen(File::NULL)` after a fork) is treated as a deliberate redirect of the process's output: it goes through on the real IO and releases that stream from the guard for the rest of the test. A failed reopen raises before releasing, so the guard stays in place.
|
|
12
|
+
- ActiveSupport's `capture`, `quietly`, and `silence_stream` (from `ActiveSupport::Testing::Stream`) work alongside AngryIO, disarmed like the other capture helpers.
|
|
13
|
+
- A guarded write that raises flushes the stream first, so the offending line really does appear right before the failure even when the stream is block-buffered (Ruby buffers non-TTY stdout, e.g. on CI; Minitest sets `$stdout.sync = true` but RSpec leaves it false). While armed, `syswrite` and `write_nonblock` also flush before writing, so buffered output (e.g. the test runner's progress dots, printed while the guard is unarmed) goes out in order instead of tripping Ruby's `syswrite for buffered IO` warning, which `WarningGuard` would otherwise turn into a raise against an innocent write.
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Friends don't let friends write tests or specs that output to stdout/stderr. When you look at your test output, you should only see green dots, right?
|
|
4
4
|
|
|
5
|
-
AngryIO
|
|
5
|
+
AngryIO guards the real `$stdout` and `$stderr` during your test suite: a write goes through to the stream and then immediately _raises_, so accidental output fails loudly instead of quietly cluttering your output — and the offending line is right there in your test output, next to the failure, making it easy to find what printed. Because the guard lives on the stream objects themselves (not on the globals), code that captured a reference earlier — e.g. `Logger.new($stdout)` at boot — can't bypass it. AngryIO ships self-registering adapters for both RSpec and Minitest, gated by a configurable `enabled` callable.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -29,10 +29,11 @@ Depending on your test framework, you should require either `angry_io/rspec` or
|
|
|
29
29
|
require "angry_io/rspec" # or "angry_io/minitest"
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
By default, AngryIO is always on, and every test that writes to `$stdout` or `$stderr` now raises:
|
|
32
|
+
By default, AngryIO is always on, and every test that writes to `$stdout` or `$stderr` now prints the offending output and then raises:
|
|
33
33
|
|
|
34
34
|
```
|
|
35
|
-
|
|
35
|
+
your output here
|
|
36
|
+
IOError: AngryIO: a test wrote to $stdout: "your output here"
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
Zero-byte writes (e.g. `$stderr.print("")`) emit nothing, so they are allowed.
|
|
@@ -86,7 +87,11 @@ end
|
|
|
86
87
|
|
|
87
88
|
## How it works
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
At load time, AngryIO prepends a small guard module onto the `STDOUT` and `STDERR` objects, overriding their write methods. Around each test, the adapter arms the guard via a thread-local flag, and disarms it in an `ensure`. When armed, a write passes through to the real stream and then raises an `IOError`; zero-byte writes (e.g. `$stderr.print("")`) emit nothing and are allowed.
|
|
91
|
+
|
|
92
|
+
Guarding the stream objects — rather than swapping the `$stdout`/`$stderr` globals — means anything holding a reference to the real streams (like a `Logger.new($stdout)` from boot time) is guarded too. `Kernel#warn` and `Warning.warn` write to stderr at C level without dispatching to `$stderr#write`, so AngryIO intercepts them at the source as well. Helpers that capture output on purpose keep working: `capture_io` rebinds the globals to unguarded `StringIO`s, and the adapters disarm the guard around `capture_subprocess_io`, RSpec's `to_*_from_any_process` matchers, and ActiveSupport's `capture`/`silence_stream`, which reopen the streams onto Tempfiles. Reopening a guarded stream yourself (e.g. `$stdout.reopen(File::NULL)` after a fork) is treated as a deliberate redirect: it goes through and releases that stream from the guard for the rest of the test.
|
|
93
|
+
|
|
94
|
+
Two limits to be aware of: the arming flag is thread-local, so writes from threads spawned mid-test are not guarded; and rebinding `$stdout` to another IO bypasses the guard (we can't guard an object we've never seen).
|
|
90
95
|
|
|
91
96
|
## Development
|
|
92
97
|
|
data/lib/angry_io/minitest.rb
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "angry_io"
|
|
4
4
|
|
|
5
|
-
# Prepends an override of +Minitest::Test#run+ that
|
|
6
|
-
#
|
|
5
|
+
# Prepends an override of +Minitest::Test#run+ that arms AngryIO's write-guard
|
|
6
|
+
# on STDOUT/STDERR around each test. A test class opts out by calling
|
|
7
7
|
# +i_absolutely_need_to_write_to_stdout!+ in its body. Force-loads Minitest so
|
|
8
8
|
# registration works regardless of Bundler.require ordering.
|
|
9
9
|
module AngryIO
|
|
@@ -25,12 +25,12 @@ module AngryIO
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# capture_subprocess_io reopens $stdout/$stderr onto Tempfiles so
|
|
28
|
-
# subprocesses inherit the file descriptors
|
|
29
|
-
#
|
|
30
|
-
#
|
|
28
|
+
# subprocesses inherit the file descriptors — a deliberate redirect whose
|
|
29
|
+
# output is captured, not pollution. Disarm the guard for the duration of
|
|
30
|
+
# the capture.
|
|
31
31
|
module CaptureSubprocessIo
|
|
32
32
|
def capture_subprocess_io(&block)
|
|
33
|
-
AngryIO.
|
|
33
|
+
AngryIO.disarmed { super }
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
data/lib/angry_io/rspec.rb
CHANGED
|
@@ -2,32 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
require "angry_io"
|
|
4
4
|
|
|
5
|
-
# Registers an +around+ hook that
|
|
6
|
-
#
|
|
5
|
+
# Registers an +around+ hook that arms AngryIO's write-guard on STDOUT/STDERR
|
|
6
|
+
# during each example, unless the example opts out via the
|
|
7
7
|
# +:i_absolutely_need_to_write_to_stdout+ metadata. Force-loads rspec-core so
|
|
8
8
|
# registration works regardless of Bundler.require ordering.
|
|
9
9
|
module AngryIO
|
|
10
10
|
module RSpec
|
|
11
11
|
# The to_*_from_any_process matchers reopen $stdout/$stderr onto Tempfiles
|
|
12
|
-
# so subprocesses inherit the file descriptors
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# stream...
|
|
12
|
+
# so subprocesses inherit the file descriptors. An example using these
|
|
13
|
+
# matchers captures output on purpose, so disarm the guard while the
|
|
14
|
+
# matcher grabs the stream...
|
|
16
15
|
module FromAnyProcess
|
|
17
16
|
def to_stdout_from_any_process
|
|
18
|
-
AngryIO.
|
|
17
|
+
AngryIO.disarmed { super }
|
|
19
18
|
end
|
|
20
19
|
|
|
21
20
|
def to_stderr_from_any_process
|
|
22
|
-
AngryIO.
|
|
21
|
+
AngryIO.disarmed { super }
|
|
23
22
|
end
|
|
24
23
|
end
|
|
25
24
|
|
|
26
25
|
# ...and while the block runs, so Ruby-level writes inside the expect block
|
|
27
|
-
# are captured by the Tempfile instead of raising against the
|
|
26
|
+
# are captured by the Tempfile instead of raising against the guard.
|
|
28
27
|
module CaptureStreamToTempfile
|
|
29
28
|
def capture(block)
|
|
30
|
-
AngryIO.
|
|
29
|
+
AngryIO.disarmed { super }
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
32
|
|
data/lib/angry_io/version.rb
CHANGED
data/lib/angry_io.rb
CHANGED
|
@@ -1,85 +1,104 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "stringio"
|
|
4
3
|
require_relative "angry_io/version"
|
|
5
4
|
|
|
6
5
|
module AngryIO
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
# Prepended to the STDOUT/STDERR singletons. While armed (a test is running
|
|
7
|
+
# under AngryIO.around_streams and hasn't opted out), a write goes through to
|
|
8
|
+
# the real stream and *then* raises IOError — the offending output appears in
|
|
9
|
+
# the test output right before the failure, so it's easy to see what printed.
|
|
10
|
+
# Guarding the real stream objects, instead of swapping the globals, means
|
|
11
|
+
# code that captured a reference earlier (e.g. `Logger.new($stdout)` at boot)
|
|
12
|
+
# can't bypass the guard.
|
|
13
|
+
module Guard
|
|
14
|
+
# IO#print, puts, printf and << all funnel through #write, so this one
|
|
15
|
+
# guard catches them.
|
|
16
|
+
def write(*strings)
|
|
17
|
+
result = super
|
|
18
|
+
AngryIO.check_output!(self, strings)
|
|
19
|
+
result
|
|
13
20
|
end
|
|
14
21
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
# syswrite and write_nonblock bypass #write, so they need their own guards.
|
|
23
|
+
# Both warn ("syswrite for buffered IO") when the stream has buffered
|
|
24
|
+
# output pending — and buffered output can be there without us knowing
|
|
25
|
+
# (e.g. the test runner's progress dots, printed between tests while the
|
|
26
|
+
# guard is unarmed). The WarningGuard would turn that warning into a raise
|
|
27
|
+
# against an innocent write, so flush first while armed: the pending output
|
|
28
|
+
# goes out in order, the buffer stays clean, and no warning fires.
|
|
29
|
+
def syswrite(string)
|
|
30
|
+
flush if AngryIO.armed?
|
|
31
|
+
result = super
|
|
32
|
+
AngryIO.check_output!(self, [string])
|
|
33
|
+
result
|
|
34
|
+
end
|
|
20
35
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
super
|
|
36
|
+
def write_nonblock(string, **options)
|
|
37
|
+
flush if AngryIO.armed?
|
|
38
|
+
result = super
|
|
39
|
+
AngryIO.check_output!(self, [string])
|
|
40
|
+
result
|
|
24
41
|
end
|
|
25
42
|
|
|
26
|
-
#
|
|
27
|
-
# byte, so it always refuses.
|
|
43
|
+
# putc writes directly in C, bypassing #write.
|
|
28
44
|
def putc(char)
|
|
29
|
-
|
|
45
|
+
result = super
|
|
46
|
+
AngryIO.check_output!(self, [char])
|
|
47
|
+
result
|
|
30
48
|
end
|
|
31
49
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
# Reopening the stream (e.g. `$stdout.reopen(File::NULL)` after a fork) is
|
|
51
|
+
# a deliberate redirect of the process's output: let it through and release
|
|
52
|
+
# this stream from the guard for the rest of the test, so subsequent writes
|
|
53
|
+
# reach the reopened target instead of raising. A failed reopen raises
|
|
54
|
+
# before releasing, so the guard stays in place.
|
|
55
|
+
#
|
|
56
|
+
# `reopen(other_io)` also turns the receiver into a copy of the other IO,
|
|
57
|
+
# replacing its singleton class — which strips this very module off the
|
|
58
|
+
# stream. Re-prepend it afterwards so the guard survives; a no-op when the
|
|
59
|
+
# module is still present (e.g. after a path reopen).
|
|
60
|
+
def reopen(*args)
|
|
61
|
+
result = super
|
|
62
|
+
singleton_class.prepend(AngryIO::Guard)
|
|
63
|
+
AngryIO.release!(self) if AngryIO.armed?
|
|
64
|
+
result
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Kernel#warn writes to stderr at C level without dispatching to
|
|
69
|
+
# $stderr#write (when $stderr is a real IO), so it bypasses Guard. Intercept
|
|
70
|
+
# it at the source. warn always appends a newline per message, so even an
|
|
71
|
+
# empty message emits output; a message-less warn emits nothing.
|
|
72
|
+
module WarnGuard
|
|
73
|
+
def warn(*messages, **options)
|
|
74
|
+
result = super
|
|
75
|
+
AngryIO.check_output!($stderr, messages.map { |m| m.to_s.empty? ? "\n" : m })
|
|
76
|
+
result
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Warning.warn (deprecations and other Ruby-level warnings) is the same:
|
|
81
|
+
# it writes to stderr without dispatching to $stderr#write.
|
|
82
|
+
module WarningGuard
|
|
83
|
+
def warn(message, **options)
|
|
84
|
+
result = super
|
|
85
|
+
AngryIO.check_output!($stderr, [message])
|
|
86
|
+
result
|
|
62
87
|
end
|
|
63
88
|
end
|
|
64
89
|
|
|
65
90
|
# ActiveSupport::Testing::Stream#capture reopens $stdout/$stderr onto a
|
|
66
|
-
# Tempfile and
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
91
|
+
# Tempfile and #silence_stream reopens onto IO::NULL — deliberate redirects
|
|
92
|
+
# whose block output is captured or silenced on purpose, not test pollution.
|
|
93
|
+
# Disarm the guard for the duration, the same way Minitest's
|
|
94
|
+
# capture_subprocess_io is handled.
|
|
70
95
|
module ActiveSupportStreamCapture
|
|
71
96
|
def capture(stream)
|
|
72
|
-
AngryIO.
|
|
97
|
+
AngryIO.disarmed { super }
|
|
73
98
|
end
|
|
74
99
|
|
|
75
100
|
def silence_stream(stream)
|
|
76
|
-
|
|
77
|
-
# buffer — which AS can't actually reopen onto IO::NULL (it just resets
|
|
78
|
-
# the buffer), leaking the block's writes to the real stream. Substitute
|
|
79
|
-
# the real stream it replaced so AS silences (and the block writes to) the
|
|
80
|
-
# real IO.
|
|
81
|
-
real = AngryIO.real_stream_for(stream)
|
|
82
|
-
AngryIO.with_real_streams { super(real || stream) }
|
|
101
|
+
AngryIO.disarmed { super }
|
|
83
102
|
end
|
|
84
103
|
|
|
85
104
|
private :capture, :silence_stream
|
|
@@ -90,79 +109,80 @@ module AngryIO
|
|
|
90
109
|
class << self
|
|
91
110
|
attr_accessor :enabled
|
|
92
111
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
# +
|
|
96
|
-
# runs untouched.
|
|
112
|
+
# Arm the write-guard on STDOUT/STDERR around the given block, restoring
|
|
113
|
+
# the previous state in an ensure. No-ops when +opted_out+ is true or when
|
|
114
|
+
# +enabled+ returns false, so the block runs untouched.
|
|
97
115
|
def around_streams(opted_out: false)
|
|
98
116
|
return yield if opted_out
|
|
99
117
|
return yield unless enabled.call
|
|
100
118
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
$stdout = stdout_buffer
|
|
106
|
-
$stderr = stderr_buffer
|
|
107
|
-
previous_swap = Thread.current.thread_variable_get(:angry_io_swap)
|
|
108
|
-
Thread.current.thread_variable_set(:angry_io_swap, [original_stdout, original_stderr, stdout_buffer, stderr_buffer])
|
|
119
|
+
previous_armed = Thread.current.thread_variable_get(:angry_io_armed)
|
|
120
|
+
previous_released = Thread.current.thread_variable_get(:angry_io_released)
|
|
121
|
+
Thread.current.thread_variable_set(:angry_io_armed, true)
|
|
122
|
+
Thread.current.thread_variable_set(:angry_io_released, [])
|
|
109
123
|
|
|
110
124
|
begin
|
|
111
125
|
yield
|
|
112
126
|
ensure
|
|
113
|
-
Thread.current.thread_variable_set(:
|
|
114
|
-
|
|
115
|
-
$stderr = original_stderr
|
|
116
|
-
stdout_buffer.close
|
|
117
|
-
stderr_buffer.close
|
|
127
|
+
Thread.current.thread_variable_set(:angry_io_armed, previous_armed)
|
|
128
|
+
Thread.current.thread_variable_set(:angry_io_released, previous_released)
|
|
118
129
|
end
|
|
119
130
|
end
|
|
120
131
|
|
|
121
|
-
# Run the block with the
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return yield unless swap
|
|
132
|
-
|
|
133
|
-
real_stdout, real_stderr, stdout_buffer, stderr_buffer = swap
|
|
134
|
-
return yield if $stdout.equal?(real_stdout) && $stderr.equal?(real_stderr)
|
|
132
|
+
# Run the block with the guard disarmed. Helpers like Minitest's
|
|
133
|
+
# capture_subprocess_io or ActiveSupport's capture need this: they redirect
|
|
134
|
+
# the streams on purpose, so the block's writes are captured, not errors.
|
|
135
|
+
def disarmed
|
|
136
|
+
previous = Thread.current.thread_variable_get(:angry_io_armed)
|
|
137
|
+
Thread.current.thread_variable_set(:angry_io_armed, false)
|
|
138
|
+
yield
|
|
139
|
+
ensure
|
|
140
|
+
Thread.current.thread_variable_set(:angry_io_armed, previous)
|
|
141
|
+
end
|
|
135
142
|
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
# Whether the write-guard is armed on the current thread. The flag is
|
|
144
|
+
# thread-local (not fiber-local), so captures running inside a Fiber —
|
|
145
|
+
# e.g. Enumerator-based code — see the same state as their test.
|
|
146
|
+
def armed?
|
|
147
|
+
Thread.current.thread_variable_get(:angry_io_armed) || false
|
|
148
|
+
end
|
|
138
149
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
end
|
|
150
|
+
# Release a stream from the guard for the rest of the surrounding test
|
|
151
|
+
# (see Guard#reopen).
|
|
152
|
+
def release!(io)
|
|
153
|
+
released = Thread.current.thread_variable_get(:angry_io_released)
|
|
154
|
+
released << io if released && !released.include?(io)
|
|
145
155
|
end
|
|
146
156
|
|
|
147
|
-
#
|
|
148
|
-
# real stream
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
# Called by Guard (and the warn intercepts) after a write has gone through
|
|
158
|
+
# to the real stream. Raises IOError if the guard is armed, the target is
|
|
159
|
+
# one of the guarded streams that hasn't been released, and the write would
|
|
160
|
+
# actually emit output (zero-byte writes are allowed). A $stdout/$stderr
|
|
161
|
+
# the user rebound to another object (e.g. capture_io's StringIO) is not
|
|
162
|
+
# guarded, so writes to it are fine.
|
|
163
|
+
# standard:disable Style/GlobalStdStream — we mean the real stream objects, not the globals
|
|
164
|
+
def check_output!(io, strings)
|
|
165
|
+
return unless armed?
|
|
166
|
+
return unless io.equal?(STDOUT) || io.equal?(STDERR)
|
|
167
|
+
return if Thread.current.thread_variable_get(:angry_io_released)&.include?(io)
|
|
168
|
+
|
|
169
|
+
offending = strings.map(&:to_s).reject(&:empty?)
|
|
170
|
+
return if offending.empty?
|
|
171
|
+
|
|
172
|
+
name = io.equal?(STDOUT) ? "$stdout" : "$stderr"
|
|
173
|
+
# Flush so the offending line really does appear right before the failure
|
|
174
|
+
# even when the stream is block-buffered ($stdout.sync == false, e.g. on
|
|
175
|
+
# CI or under RSpec) — and so the buffer is left empty, keeping a later
|
|
176
|
+
# syswrite from tripping Ruby's "syswrite for buffered IO" warning (which
|
|
177
|
+
# WarningGuard turns into a raise) in an innocent test.
|
|
178
|
+
io.flush
|
|
179
|
+
raise IOError, "AngryIO: a test wrote to #{name}:\n #{offending.join}"
|
|
161
180
|
end
|
|
181
|
+
# standard:enable Style/GlobalStdStream
|
|
162
182
|
|
|
163
183
|
# Prepend hooks so ActiveSupport::Testing::Stream#capture / #silence_stream
|
|
164
|
-
# run with the
|
|
165
|
-
#
|
|
184
|
+
# run with the guard disarmed (see ActiveSupportStreamCapture). No-ops when
|
|
185
|
+
# ActiveSupport isn't loaded.
|
|
166
186
|
def hook_active_support_stream!
|
|
167
187
|
require "active_support/testing/stream"
|
|
168
188
|
ActiveSupport::Testing::Stream.prepend(ActiveSupportStreamCapture)
|
|
@@ -171,3 +191,11 @@ module AngryIO
|
|
|
171
191
|
end
|
|
172
192
|
end
|
|
173
193
|
end
|
|
194
|
+
|
|
195
|
+
# standard:disable Style/GlobalStdStream — guard the real stream objects, not the globals
|
|
196
|
+
STDOUT.singleton_class.prepend(AngryIO::Guard)
|
|
197
|
+
STDERR.singleton_class.prepend(AngryIO::Guard)
|
|
198
|
+
# standard:enable Style/GlobalStdStream
|
|
199
|
+
Kernel.prepend(AngryIO::WarnGuard)
|
|
200
|
+
Kernel.singleton_class.prepend(AngryIO::WarnGuard) # warn is a module_function
|
|
201
|
+
Warning.singleton_class.prepend(AngryIO::WarningGuard)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: angry_io
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Benjamin Quorning
|
|
@@ -9,10 +9,10 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description: AngryIO
|
|
13
|
-
|
|
14
|
-
RSpec and Minitest adapters that self-register,
|
|
15
|
-
callable.
|
|
12
|
+
description: 'AngryIO guards the real $stdout/$stderr during tests: a write goes through
|
|
13
|
+
to the stream and then raises, so accidental output fails loudly and the offending
|
|
14
|
+
line is visible in the test output. Ships RSpec and Minitest adapters that self-register,
|
|
15
|
+
gated by a configurable enabled callable.'
|
|
16
16
|
email:
|
|
17
17
|
- bquorning@zendesk.com
|
|
18
18
|
executables: []
|
|
@@ -51,5 +51,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
51
51
|
requirements: []
|
|
52
52
|
rubygems_version: 4.0.20
|
|
53
53
|
specification_version: 4
|
|
54
|
-
summary:
|
|
54
|
+
summary: 'Guards stdout/stderr during tests: writes go through, then raise.'
|
|
55
55
|
test_files: []
|