angry_io 0.3.0 → 0.4.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 +6 -0
- data/lib/angry_io/minitest.rb +1 -0
- data/lib/angry_io/rspec.rb +1 -0
- data/lib/angry_io/version.rb +1 -1
- data/lib/angry_io.rb +87 -1
- 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: efd2cce37a69f805e637ea406088049b150bb0ff9192a7423100add8b7a29342
|
|
4
|
+
data.tar.gz: 4e25659b95836fc83544b295cf40b6428d094cf194356ff1e4e0c0b8e7c1ad42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 72682994c9e8647d1405a46aa15366c377325f31396767f6f6efe0ef6e6722f875bdce839a0094c04db309e39cb691a05d3daa43d5b4aeff2372430f1e69cee1
|
|
7
|
+
data.tar.gz: 51ee62f1212076d50984ed31c221760464fbd2f15990abd8f4c241fd4294bcc259bffa5b9c46ce54942ab6a5b178582272cd9c09de08dae7fa67baa874bf7e0b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-09-15
|
|
4
|
+
|
|
5
|
+
- Reopening `$stdout`/`$stderr` onto a real IO (e.g. `$stdout.reopen(File::NULL)`) no longer raises `TypeError` under `AngryIo::Stream`. `Stream#reopen` now delegates an IO argument to the original real stream it replaced (a `dup2` that survives the caller closing the other IO) and hands the global back so writes reach the reopened IO; non-IO arguments still defer to StringIO's own buffer reset. The new `AngryIo.real_stream_for` returns the real stream a swapped buffer replaced.
|
|
6
|
+
- ActiveSupport's `capture` and `silence_stream` (from `ActiveSupport::Testing::Stream`) now work alongside AngryIo. They use the same reopen-onto-IO pattern as `capture_subprocess_io`, so AngryIo now runs them under `with_real_streams`, substituting the real stream for the StringIO buffer that `silence_stream` binds at call time.
|
|
7
|
+
- `AngryIo.with_real_streams` is now reentrant: a nested call (e.g. ActiveSupport's `capture` wrapping `quietly`, whose nested `silence_stream` re-enters the helper) no longer swaps the globals back to the Angry buffers while the outer call still expects the real streams. It no-ops when the real streams are already current.
|
|
8
|
+
|
|
3
9
|
## [0.3.0] - 2026-09-06
|
|
4
10
|
|
|
5
11
|
- 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.
|
data/lib/angry_io/minitest.rb
CHANGED
data/lib/angry_io/rspec.rb
CHANGED
data/lib/angry_io/version.rb
CHANGED
data/lib/angry_io.rb
CHANGED
|
@@ -28,6 +28,61 @@ module AngryIo
|
|
|
28
28
|
def putc(char)
|
|
29
29
|
raise IOError, "AngryIo::Stream is not writable: #{char.inspect}"
|
|
30
30
|
end
|
|
31
|
+
|
|
32
|
+
# `IO#reopen(io)` redirects a real $stdout/$stderr onto another IO (a
|
|
33
|
+
# socket, a File, etc.) so the process's output flows there. StringIO only
|
|
34
|
+
# accepts a String, so reopening onto an IO would raise
|
|
35
|
+
# `TypeError: can't convert IO into StringIO` — which breaks code that
|
|
36
|
+
# reopens the streams after a fork. Delegate to the original real stream we
|
|
37
|
+
# replaced (which dup2's the other IO's fd onto the stdout/stderr fd,
|
|
38
|
+
# surviving the caller later closing the other IO), then hand the global
|
|
39
|
+
# back so subsequent writes reach the reopened IO instead of this guard.
|
|
40
|
+
# Non-IO args defer to StringIO's own reopen (buffer reset).
|
|
41
|
+
def reopen(other = nil, *rest)
|
|
42
|
+
if other.is_a?(IO) && (swap = Thread.current.thread_variable_get(:angry_io_swap))
|
|
43
|
+
real_stdout, real_stderr, stdout_buffer, stderr_buffer = swap
|
|
44
|
+
if equal?(stdout_buffer)
|
|
45
|
+
real_stdout.reopen(other)
|
|
46
|
+
$stdout = real_stdout
|
|
47
|
+
elsif equal?(stderr_buffer)
|
|
48
|
+
real_stderr.reopen(other)
|
|
49
|
+
$stderr = real_stderr
|
|
50
|
+
else
|
|
51
|
+
# Not one of the swapped buffers (a standalone Stream someone assigned
|
|
52
|
+
# to $stdout outside the adapter): no original to delegate to, so match
|
|
53
|
+
# StringIO and raise TypeError instead of silently no-op'ing.
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
self
|
|
57
|
+
elsif other.nil? && rest.empty?
|
|
58
|
+
super() # StringIO#reopen() resets the buffer
|
|
59
|
+
else
|
|
60
|
+
super
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# ActiveSupport::Testing::Stream#capture reopens $stdout/$stderr onto a
|
|
66
|
+
# Tempfile and later restores them via `reopen(dup)` — a dup2-then-restore
|
|
67
|
+
# pattern that only works on real IOs (Tempfile is a Delegator, and StringIO
|
|
68
|
+
# can't be reopened onto one). Restore the real streams for the duration of a
|
|
69
|
+
# capture/silence, the same way Minitest's capture_subprocess_io is handled.
|
|
70
|
+
module ActiveSupportStreamCapture
|
|
71
|
+
def capture(stream)
|
|
72
|
+
AngryIo.with_real_streams { super }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def silence_stream(stream)
|
|
76
|
+
# `stream` is bound at call time, so under AngryIo it's the StringIO
|
|
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) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private :capture, :silence_stream
|
|
31
86
|
end
|
|
32
87
|
|
|
33
88
|
Config = Struct.new(:enabled) do
|
|
@@ -77,12 +132,17 @@ module AngryIo
|
|
|
77
132
|
# AngryIo::Stream buffers afterwards. Helpers like Minitest's
|
|
78
133
|
# capture_subprocess_io need this: they reopen $stdout/$stderr onto
|
|
79
134
|
# Tempfiles so subprocesses inherit the file descriptors, which only works
|
|
80
|
-
# on real IOs. No-ops when no swap is active
|
|
135
|
+
# on real IOs. No-ops when no swap is active, or when the real streams are
|
|
136
|
+
# already current (a nested call inside another with_real_streams — e.g.
|
|
137
|
+
# ActiveSupport's `capture` wrapping `quietly` — so the inner ensure doesn't
|
|
138
|
+
# clobber the outer's swap-back).
|
|
81
139
|
def with_real_streams
|
|
82
140
|
swap = Thread.current.thread_variable_get(:angry_io_swap)
|
|
83
141
|
return yield unless swap
|
|
84
142
|
|
|
85
143
|
real_stdout, real_stderr, stdout_buffer, stderr_buffer = swap
|
|
144
|
+
return yield if $stdout.equal?(real_stdout) && $stderr.equal?(real_stderr)
|
|
145
|
+
|
|
86
146
|
$stdout = real_stdout
|
|
87
147
|
$stderr = real_stderr
|
|
88
148
|
|
|
@@ -93,5 +153,31 @@ module AngryIo
|
|
|
93
153
|
$stderr = stderr_buffer
|
|
94
154
|
end
|
|
95
155
|
end
|
|
156
|
+
|
|
157
|
+
# If `stream` is one of the active swap's AngryIo::Stream buffers, return the
|
|
158
|
+
# real stream it replaced; otherwise nil. Used by
|
|
159
|
+
# ActiveSupportStreamCapture#silence_stream to redirect silencing onto the
|
|
160
|
+
# real IO instead of the StringIO buffer.
|
|
161
|
+
def real_stream_for(stream)
|
|
162
|
+
swap = Thread.current.thread_variable_get(:angry_io_swap)
|
|
163
|
+
return nil unless swap
|
|
164
|
+
|
|
165
|
+
real_stdout, real_stderr, stdout_buffer, stderr_buffer = swap
|
|
166
|
+
if stream.equal?(stdout_buffer)
|
|
167
|
+
real_stdout
|
|
168
|
+
elsif stream.equal?(stderr_buffer)
|
|
169
|
+
real_stderr
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Prepend hooks so ActiveSupport::Testing::Stream#capture / #silence_stream
|
|
174
|
+
# run with the real $stdout/$stderr restored (see ActiveSupportStreamCapture).
|
|
175
|
+
# No-ops when ActiveSupport isn't loaded.
|
|
176
|
+
def hook_active_support_stream!
|
|
177
|
+
require "active_support/testing/stream"
|
|
178
|
+
ActiveSupport::Testing::Stream.prepend(ActiveSupportStreamCapture)
|
|
179
|
+
rescue LoadError
|
|
180
|
+
# ActiveSupport isn't available; nothing to hook.
|
|
181
|
+
end
|
|
96
182
|
end
|
|
97
183
|
end
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Benjamin Quorning
|
|
@@ -49,7 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
49
49
|
- !ruby/object:Gem::Version
|
|
50
50
|
version: '0'
|
|
51
51
|
requirements: []
|
|
52
|
-
rubygems_version: 4.0.
|
|
52
|
+
rubygems_version: 4.0.20
|
|
53
53
|
specification_version: 4
|
|
54
54
|
summary: An IO that raises on write, to keep tests from polluting stdout/stderr.
|
|
55
55
|
test_files: []
|