angry_io 0.1.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 +11 -0
- data/README.md +27 -22
- data/lib/angry_io/minitest.rb +17 -13
- data/lib/angry_io/rspec.rb +34 -12
- data/lib/angry_io/version.rb +1 -1
- data/lib/angry_io.rb +45 -5
- metadata +1 -2
- data/lib/angry_io/enable_for_ci_true.rb +0 -16
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,16 @@
|
|
|
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
|
+
|
|
8
|
+
## [0.2.0] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
- Remove the `angry_io/enable_for_ci_true` convenience require; require `angry_io/rspec` or `angry_io/minitest` directly instead.
|
|
11
|
+
- Remove the configurable `opt_out_metadata` field; the RSpec opt-out metadata is now always `:i_absolutely_need_to_write_to_stdout`.
|
|
12
|
+
- Adapters no longer swallow `LoadError` when their framework is missing; requiring an adapter now hard-requires its framework.
|
|
13
|
+
|
|
3
14
|
## [0.1.0] - 2026-09-04
|
|
4
15
|
|
|
5
16
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
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
|
|
|
7
|
-
The name and the default opt-out metadata are a nod to [minitest](https://github.com/minitest/minitest)'s `i_suck_and_my_tests_are_order_dependent!`.
|
|
8
|
-
|
|
9
7
|
## Installation
|
|
10
8
|
|
|
11
9
|
Add to your application's Gemfile:
|
|
12
10
|
|
|
13
11
|
```ruby
|
|
14
12
|
group :test do
|
|
15
|
-
gem "angry_io"
|
|
13
|
+
gem "angry_io"
|
|
16
14
|
end
|
|
17
15
|
```
|
|
18
16
|
|
|
@@ -24,17 +22,28 @@ gem install angry_io
|
|
|
24
22
|
|
|
25
23
|
## Usage
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
Depending on your test framework, you should require either `angry_io/rspec` or `angry_io/minitest`:
|
|
28
26
|
|
|
29
27
|
```ruby
|
|
30
|
-
#
|
|
31
|
-
|
|
28
|
+
# spec_helper.rb (RSpec) or test_helper.rb (Minitest)
|
|
29
|
+
require "angry_io/rspec" # or "angry_io/minitest"
|
|
32
30
|
```
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
By default, AngryIo is always on, and every test that writes to `$stdout` or `$stderr` now raises:
|
|
35
33
|
|
|
36
34
|
```
|
|
37
|
-
IOError: not
|
|
35
|
+
IOError: AngryIo::Stream is not writable: "your output here"
|
|
36
|
+
```
|
|
37
|
+
|
|
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:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# On by default; turn it off in environments where you'll allow real output.
|
|
44
|
+
AngryIo.configure do |config|
|
|
45
|
+
config.enabled = -> { ENV["CI"] == "true" }
|
|
46
|
+
end
|
|
38
47
|
```
|
|
39
48
|
|
|
40
49
|
### Opting out
|
|
@@ -59,34 +68,30 @@ class PrintTest < Minitest::Test
|
|
|
59
68
|
end
|
|
60
69
|
```
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
If you'd rather control the gate yourself, require an adapter directly. `enabled` defaults to always-on, so it's most useful as a kill switch for environments where you need real output:
|
|
71
|
+
The same call works in Minitest's spec format, inside the `describe` block:
|
|
65
72
|
|
|
66
73
|
```ruby
|
|
67
|
-
#
|
|
68
|
-
|
|
74
|
+
# Minitest spec format — opt out a whole describe block
|
|
75
|
+
describe "printing" do
|
|
76
|
+
i_absolutely_need_to_write_to_stdout!
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
it "prints" do
|
|
79
|
+
puts "ok"
|
|
80
|
+
end
|
|
73
81
|
end
|
|
74
82
|
```
|
|
75
83
|
|
|
76
|
-
Requiring `angry_io/rspec` or `angry_io/minitest` self-registers the hook if that framework is loaded; requiring plain `angry_io` gives you just the `AngryIo::Stream` class with no side effects.
|
|
77
|
-
|
|
78
84
|
## Configuration
|
|
79
85
|
|
|
80
|
-
`AngryIo.configure` yields a config struct with
|
|
86
|
+
`AngryIo.configure` yields a config struct with one field:
|
|
81
87
|
|
|
82
88
|
| Field | Default | Description |
|
|
83
89
|
| --- | --- | --- |
|
|
84
90
|
| `enabled` | `-> { true }` | A callable returning whether AngryIo is active. Invoked once per test, so it can read env vars or feature flags live. |
|
|
85
|
-
| `opt_out_metadata` | `:i_absolutely_need_to_write_to_stdout` | The RSpec metadata symbol that opts an example out. |
|
|
86
91
|
|
|
87
92
|
## How it works
|
|
88
93
|
|
|
89
|
-
`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`.
|
|
90
95
|
|
|
91
96
|
## Development
|
|
92
97
|
|
data/lib/angry_io/minitest.rb
CHANGED
|
@@ -4,10 +4,8 @@ require "angry_io"
|
|
|
4
4
|
|
|
5
5
|
# Prepends an override of +Minitest::Test#run+ that swaps $stdout/$stderr to
|
|
6
6
|
# AngryIo::Stream instances around each test. A test class opts out by calling
|
|
7
|
-
# +i_absolutely_need_to_write_to_stdout!+ in its body
|
|
8
|
-
#
|
|
9
|
-
# registration works regardless of Bundler.require ordering; no-op if Minitest
|
|
10
|
-
# isn't installed.
|
|
7
|
+
# +i_absolutely_need_to_write_to_stdout!+ in its body. Force-loads Minitest so
|
|
8
|
+
# registration works regardless of Bundler.require ordering.
|
|
11
9
|
module AngryIo
|
|
12
10
|
module Minitest
|
|
13
11
|
module Adapter
|
|
@@ -26,17 +24,23 @@ module AngryIo
|
|
|
26
24
|
end
|
|
27
25
|
end
|
|
28
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
|
+
|
|
29
37
|
def self.setup!
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
::Minitest::Test.extend(ClassMethods)
|
|
39
|
+
::Minitest::Test.prepend(Adapter)
|
|
40
|
+
::Minitest::Assertions.prepend(CaptureSubprocessIo)
|
|
33
41
|
end
|
|
34
42
|
end
|
|
35
43
|
end
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
AngryIo::Minitest.setup!
|
|
40
|
-
rescue LoadError
|
|
41
|
-
# Minitest isn't installed; nothing to wire up.
|
|
42
|
-
end
|
|
45
|
+
require "minitest"
|
|
46
|
+
AngryIo::Minitest.setup!
|
data/lib/angry_io/rspec.rb
CHANGED
|
@@ -3,27 +3,49 @@
|
|
|
3
3
|
require "angry_io"
|
|
4
4
|
|
|
5
5
|
# Registers an +around+ hook that swaps $stdout/$stderr to AngryIo::Stream
|
|
6
|
-
# instances during each example, unless the example opts out via the
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# no-op if RSpec isn't installed.
|
|
6
|
+
# instances during each example, unless the example opts out via the
|
|
7
|
+
# +:i_absolutely_need_to_write_to_stdout+ metadata. Force-loads rspec-core so
|
|
8
|
+
# registration works regardless of Bundler.require ordering.
|
|
10
9
|
module AngryIo
|
|
11
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
|
+
|
|
12
34
|
def self.setup!
|
|
13
35
|
::RSpec.configure do |config|
|
|
14
36
|
config.around do |example|
|
|
15
|
-
opt_out = example.metadata.key?(
|
|
16
|
-
example.metadata[
|
|
37
|
+
opt_out = example.metadata.key?(:i_absolutely_need_to_write_to_stdout) &&
|
|
38
|
+
example.metadata[:i_absolutely_need_to_write_to_stdout]
|
|
17
39
|
AngryIo.around_streams(opted_out: opt_out) { example.run }
|
|
18
40
|
end
|
|
19
41
|
end
|
|
42
|
+
|
|
43
|
+
::RSpec::Matchers::BuiltIn::Output.prepend(FromAnyProcess)
|
|
44
|
+
::RSpec::Matchers::BuiltIn::CaptureStreamToTempfile.prepend(CaptureStreamToTempfile)
|
|
20
45
|
end
|
|
21
46
|
end
|
|
22
47
|
end
|
|
23
48
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
rescue LoadError
|
|
28
|
-
# RSpec isn't installed; nothing to wire up.
|
|
29
|
-
end
|
|
49
|
+
require "rspec/core"
|
|
50
|
+
require "rspec/expectations"
|
|
51
|
+
AngryIo::RSpec.setup!
|
data/lib/angry_io/version.rb
CHANGED
data/lib/angry_io.rb
CHANGED
|
@@ -5,18 +5,34 @@ 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
|
|
|
16
|
-
Config = Struct.new(:enabled
|
|
33
|
+
Config = Struct.new(:enabled) do
|
|
17
34
|
def initialize
|
|
18
35
|
self.enabled = -> { true }
|
|
19
|
-
self.opt_out_metadata = :i_absolutely_need_to_write_to_stdout
|
|
20
36
|
end
|
|
21
37
|
end
|
|
22
38
|
|
|
@@ -43,15 +59,39 @@ module AngryIo
|
|
|
43
59
|
stderr_buffer = Stream.new
|
|
44
60
|
$stdout = stdout_buffer
|
|
45
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])
|
|
46
64
|
|
|
47
65
|
begin
|
|
48
66
|
yield
|
|
49
67
|
ensure
|
|
68
|
+
Thread.current.thread_variable_set(:angry_io_swap, previous_swap)
|
|
50
69
|
$stdout = original_stdout
|
|
51
70
|
$stderr = original_stderr
|
|
52
71
|
stdout_buffer.close
|
|
53
72
|
stderr_buffer.close
|
|
54
73
|
end
|
|
55
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
|
|
56
96
|
end
|
|
57
97
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Benjamin Quorning
|
|
@@ -23,7 +23,6 @@ files:
|
|
|
23
23
|
- LICENSE.txt
|
|
24
24
|
- README.md
|
|
25
25
|
- lib/angry_io.rb
|
|
26
|
-
- lib/angry_io/enable_for_ci_true.rb
|
|
27
26
|
- lib/angry_io/minitest.rb
|
|
28
27
|
- lib/angry_io/rspec.rb
|
|
29
28
|
- lib/angry_io/version.rb
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Require this file from your Gemfile (+require: "angry_io/enable_for_ci_true"+)
|
|
4
|
-
# to turn AngryIo on only when the +CI+ environment variable is +"true"+ — a
|
|
5
|
-
# convention set by GitHub Actions, GitLab CI, CircleCI, Travis, and others.
|
|
6
|
-
# Whichever test framework is loaded (RSpec or Minitest) wires itself up; the
|
|
7
|
-
# other is a no-op.
|
|
8
|
-
|
|
9
|
-
require "angry_io"
|
|
10
|
-
|
|
11
|
-
AngryIo.configure do |config|
|
|
12
|
-
config.enabled = -> { ENV["CI"] == "true" }
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
require "angry_io/rspec"
|
|
16
|
-
require "angry_io/minitest"
|