angry_io 0.2.0 → 0.3.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 +5 -0
- data/README.md +20 -5
- data/lib/angry_io/minitest.rb +13 -3
- data/lib/angry_io/rspec.rb +27 -0
- data/lib/angry_io/version.rb +1 -1
- data/lib/angry_io.rb +44 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 597667423eb4ee7ad9daaf21e0ee59e86f361a07e37a5b6d7bdc03a89978ead6
|
|
4
|
+
data.tar.gz: df59a437ba441870cc0d6788748ea115d632b9c7770266a1ef901e4a966cb413
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44154fa02cb72e8cbf89c5c24b3a4996a4600b0e3d17c484a50222875ea1b7fdc9c830150a7b618cee54eb05ce6cdab82fb1bc67dcebc4b42d7facd4578a5429
|
|
7
|
+
data.tar.gz: '083c923145f4247d847613ba082f1f9d509fd7dd7cdb9bce34b5db89f722051e5ca07ef4057af6c111a63656fa3fdf8be19957dce330a1cdf98babbb2c80e046'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-09-06
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- 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.
|
|
7
|
+
|
|
3
8
|
## [0.2.0] - 2026-09-04
|
|
4
9
|
|
|
5
10
|
- Remove the `angry_io/enable_for_ci_true` convenience require; require `angry_io/rspec` or `angry_io/minitest` directly instead.
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AngryIo
|
|
2
2
|
|
|
3
|
-
Friends don't let friends write tests or specs that
|
|
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
5
|
AngryIo replaces `$stdout` and `$stderr` during your test suite with an IO that _raises on write_, so accidental output fails loudly instead of quietly cluttering your output. It ships self-registering adapters for both RSpec and Minitest, gated by a configurable `enabled` callable.
|
|
6
6
|
|
|
@@ -32,10 +32,12 @@ require "angry_io/rspec" # or "angry_io/minitest"
|
|
|
32
32
|
By default, AngryIo is always on, and every test that writes to `$stdout` or `$stderr` now raises:
|
|
33
33
|
|
|
34
34
|
```
|
|
35
|
-
IOError: not
|
|
35
|
+
IOError: AngryIo::Stream is not writable: "your output here"
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Zero-byte writes (e.g. `$stderr.print("")`) emit nothing, so they are allowed.
|
|
39
|
+
|
|
40
|
+
If you want to allow test output in some environments but not in others — e.g., allow output when testing locally, but fail on CI — you can configure like this:
|
|
39
41
|
|
|
40
42
|
```ruby
|
|
41
43
|
# On by default; turn it off in environments where you'll allow real output.
|
|
@@ -66,9 +68,22 @@ class PrintTest < Minitest::Test
|
|
|
66
68
|
end
|
|
67
69
|
```
|
|
68
70
|
|
|
71
|
+
The same call works in Minitest's spec format, inside the `describe` block:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# Minitest spec format — opt out a whole describe block
|
|
75
|
+
describe "printing" do
|
|
76
|
+
i_absolutely_need_to_write_to_stdout!
|
|
77
|
+
|
|
78
|
+
it "prints" do
|
|
79
|
+
puts "ok"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
69
84
|
## Configuration
|
|
70
85
|
|
|
71
|
-
`AngryIo.configure` yields a config struct with
|
|
86
|
+
`AngryIo.configure` yields a config struct with one field:
|
|
72
87
|
|
|
73
88
|
| Field | Default | Description |
|
|
74
89
|
| --- | --- | --- |
|
|
@@ -76,7 +91,7 @@ end
|
|
|
76
91
|
|
|
77
92
|
## How it works
|
|
78
93
|
|
|
79
|
-
`AngryIo::Stream` is a `StringIO`
|
|
94
|
+
`AngryIo::Stream` is a `StringIO` whose write methods raise an `IOError` when given anything but an empty string. Around each test, the adapter swaps `$stdout` and `$stderr` to fresh `AngryIo::Stream` instances and restores the originals (closing the buffers) in an `ensure`.
|
|
80
95
|
|
|
81
96
|
## Development
|
|
82
97
|
|
data/lib/angry_io/minitest.rb
CHANGED
|
@@ -24,10 +24,20 @@ module AngryIo
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# capture_subprocess_io reopens $stdout/$stderr onto Tempfiles so
|
|
28
|
+
# subprocesses inherit the file descriptors, which fails on StringIO-based
|
|
29
|
+
# streams. A test calling it captures output on purpose, so restore the
|
|
30
|
+
# real streams for the duration of the capture.
|
|
31
|
+
module CaptureSubprocessIo
|
|
32
|
+
def capture_subprocess_io(&block)
|
|
33
|
+
AngryIo.with_real_streams { super }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
27
37
|
def self.setup!
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
::Minitest::Test.extend(ClassMethods)
|
|
39
|
+
::Minitest::Test.prepend(Adapter)
|
|
40
|
+
::Minitest::Assertions.prepend(CaptureSubprocessIo)
|
|
31
41
|
end
|
|
32
42
|
end
|
|
33
43
|
end
|
data/lib/angry_io/rspec.rb
CHANGED
|
@@ -8,6 +8,29 @@ require "angry_io"
|
|
|
8
8
|
# registration works regardless of Bundler.require ordering.
|
|
9
9
|
module AngryIo
|
|
10
10
|
module RSpec
|
|
11
|
+
# The to_*_from_any_process matchers reopen $stdout/$stderr onto Tempfiles
|
|
12
|
+
# so subprocesses inherit the file descriptors, which fails on
|
|
13
|
+
# StringIO-based streams. An example using these matchers captures output
|
|
14
|
+
# on purpose, so restore the real streams while the matcher grabs the
|
|
15
|
+
# stream...
|
|
16
|
+
module FromAnyProcess
|
|
17
|
+
def to_stdout_from_any_process
|
|
18
|
+
AngryIo.with_real_streams { super }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_stderr_from_any_process
|
|
22
|
+
AngryIo.with_real_streams { super }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# ...and while the block runs, so Ruby-level writes inside the expect block
|
|
27
|
+
# are captured by the Tempfile instead of raising against the Angry stream.
|
|
28
|
+
module CaptureStreamToTempfile
|
|
29
|
+
def capture(block)
|
|
30
|
+
AngryIo.with_real_streams { super }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
11
34
|
def self.setup!
|
|
12
35
|
::RSpec.configure do |config|
|
|
13
36
|
config.around do |example|
|
|
@@ -16,9 +39,13 @@ module AngryIo
|
|
|
16
39
|
AngryIo.around_streams(opted_out: opt_out) { example.run }
|
|
17
40
|
end
|
|
18
41
|
end
|
|
42
|
+
|
|
43
|
+
::RSpec::Matchers::BuiltIn::Output.prepend(FromAnyProcess)
|
|
44
|
+
::RSpec::Matchers::BuiltIn::CaptureStreamToTempfile.prepend(CaptureStreamToTempfile)
|
|
19
45
|
end
|
|
20
46
|
end
|
|
21
47
|
end
|
|
22
48
|
|
|
23
49
|
require "rspec/core"
|
|
50
|
+
require "rspec/expectations"
|
|
24
51
|
AngryIo::RSpec.setup!
|
data/lib/angry_io/version.rb
CHANGED
data/lib/angry_io.rb
CHANGED
|
@@ -5,11 +5,28 @@ require_relative "angry_io/version"
|
|
|
5
5
|
|
|
6
6
|
module AngryIo
|
|
7
7
|
# An IO that raises when you write to it, so tests can't silently pollute
|
|
8
|
-
# stdout/stderr.
|
|
9
|
-
#
|
|
8
|
+
# stdout/stderr. Zero-byte writes like `$stderr.print("")` emit nothing and
|
|
9
|
+
# are allowed; anything that would produce output raises an IOError.
|
|
10
10
|
class Stream < StringIO
|
|
11
11
|
def initialize
|
|
12
|
-
super(
|
|
12
|
+
super(+"")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# StringIO's print, puts, printf, <<, syswrite and write_nonblock all
|
|
16
|
+
# funnel through #write, so this one guard catches them.
|
|
17
|
+
def write(*strings)
|
|
18
|
+
strings.each do |string|
|
|
19
|
+
next if string.to_s.empty?
|
|
20
|
+
|
|
21
|
+
raise IOError, "AngryIo::Stream is not writable: #{string.to_s.inspect}"
|
|
22
|
+
end
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# StringIO#putc writes directly in C, bypassing #write. It always emits a
|
|
27
|
+
# byte, so it always refuses.
|
|
28
|
+
def putc(char)
|
|
29
|
+
raise IOError, "AngryIo::Stream is not writable: #{char.inspect}"
|
|
13
30
|
end
|
|
14
31
|
end
|
|
15
32
|
|
|
@@ -42,15 +59,39 @@ module AngryIo
|
|
|
42
59
|
stderr_buffer = Stream.new
|
|
43
60
|
$stdout = stdout_buffer
|
|
44
61
|
$stderr = stderr_buffer
|
|
62
|
+
previous_swap = Thread.current.thread_variable_get(:angry_io_swap)
|
|
63
|
+
Thread.current.thread_variable_set(:angry_io_swap, [original_stdout, original_stderr, stdout_buffer, stderr_buffer])
|
|
45
64
|
|
|
46
65
|
begin
|
|
47
66
|
yield
|
|
48
67
|
ensure
|
|
68
|
+
Thread.current.thread_variable_set(:angry_io_swap, previous_swap)
|
|
49
69
|
$stdout = original_stdout
|
|
50
70
|
$stderr = original_stderr
|
|
51
71
|
stdout_buffer.close
|
|
52
72
|
stderr_buffer.close
|
|
53
73
|
end
|
|
54
74
|
end
|
|
75
|
+
|
|
76
|
+
# Run the block with the pre-swap $stdout/$stderr restored, re-swapping the
|
|
77
|
+
# AngryIo::Stream buffers afterwards. Helpers like Minitest's
|
|
78
|
+
# capture_subprocess_io need this: they reopen $stdout/$stderr onto
|
|
79
|
+
# Tempfiles so subprocesses inherit the file descriptors, which only works
|
|
80
|
+
# on real IOs. No-ops when no swap is active.
|
|
81
|
+
def with_real_streams
|
|
82
|
+
swap = Thread.current.thread_variable_get(:angry_io_swap)
|
|
83
|
+
return yield unless swap
|
|
84
|
+
|
|
85
|
+
real_stdout, real_stderr, stdout_buffer, stderr_buffer = swap
|
|
86
|
+
$stdout = real_stdout
|
|
87
|
+
$stderr = real_stderr
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
yield
|
|
91
|
+
ensure
|
|
92
|
+
$stdout = stdout_buffer
|
|
93
|
+
$stderr = stderr_buffer
|
|
94
|
+
end
|
|
95
|
+
end
|
|
55
96
|
end
|
|
56
97
|
end
|