test-cmd.rb 2.4.0 → 2.6.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/.github/workflows/specs.yml +1 -1
- data/README.md +35 -5
- data/lib/test/command/version.rb +1 -1
- data/lib/test/command.rb +67 -5
- data/test/cmd_test.rb +72 -0
- 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: 8f3880288c628fc25dc0a16dcee05222dc776ffb8cab098d2fad3610534ebded
|
|
4
|
+
data.tar.gz: 39573f323c733d4fef70b3e784c3b6c4f956f7da60e0c1bbe7e454d51487f003
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b8e25c54804ac4cfa14f4356a2ea9f3e7b97f994792ae3f77b60e19151d3ee66d84959a40347b82adf1e0b5c277c3d28ef81b85992a5a496881db8e695ca867
|
|
7
|
+
data.tar.gz: face7b9c865c69666ff8eb29393af0ce856777bdf70d617889021f3a8496c0d331be88c527eec3f861b606773d93b1ca70e634c7e08e4a12f60320d57777e5bd
|
data/.github/workflows/specs.yml
CHANGED
data/README.md
CHANGED
|
@@ -34,7 +34,6 @@ test-cmd.rb can be installed via rubygems.org:
|
|
|
34
34
|
|
|
35
35
|
### Commands
|
|
36
36
|
|
|
37
|
-
|
|
38
37
|
```ruby
|
|
39
38
|
require "test-cmd"
|
|
40
39
|
|
|
@@ -60,19 +59,50 @@ p Test::Command
|
|
|
60
59
|
.new("tr", "a-z", "A-Z")
|
|
61
60
|
.stdin("hello")
|
|
62
61
|
.stdout # => "HELLO"
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# Limit how much data is read from stdout and
|
|
65
|
+
# stderr. At most 20k bytes is read from both:
|
|
66
|
+
p Test::Command
|
|
67
|
+
.new("git")
|
|
68
|
+
.limit(stdout: 20_000, stderr: 20_000)
|
|
69
|
+
.argv("diff", "--cached")
|
|
70
|
+
.stdout
|
|
63
71
|
```
|
|
64
72
|
|
|
65
73
|
<details>
|
|
66
74
|
<summary>Environment</summary>
|
|
67
75
|
<br>
|
|
68
76
|
|
|
77
|
+
A command that is not given an environment inherits the parent's
|
|
78
|
+
environment. When `env` is called, only the variables that are
|
|
79
|
+
given to `env` are set on the spawned process, alongside `$PATH`
|
|
80
|
+
which is always inherited. `$PATH` is inherited so that the
|
|
81
|
+
command can still be found via path lookup.
|
|
82
|
+
|
|
69
83
|
```ruby
|
|
70
|
-
|
|
84
|
+
##
|
|
85
|
+
# Inherit the parent's environment. All variables
|
|
86
|
+
# from the parent process are available.
|
|
87
|
+
p Test::Command
|
|
88
|
+
.new("env")
|
|
89
|
+
.stdout
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# Only $FOO and $BAR are set, plus the inherited
|
|
93
|
+
# $PATH. Everything else is left behind.
|
|
94
|
+
p Test::Command
|
|
95
|
+
.new("env")
|
|
96
|
+
.env("FOO" => "42", "BAR" => "7")
|
|
97
|
+
.stdout
|
|
71
98
|
|
|
99
|
+
##
|
|
100
|
+
# An empty hash yields only $PATH and the rest of
|
|
101
|
+
# environment is left empty.
|
|
72
102
|
p Test::Command
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
103
|
+
.new("env")
|
|
104
|
+
.env({})
|
|
105
|
+
.stdout
|
|
76
106
|
```
|
|
77
107
|
</details>
|
|
78
108
|
|
data/lib/test/command/version.rb
CHANGED
data/lib/test/command.rb
CHANGED
|
@@ -28,12 +28,13 @@ class Test::Command
|
|
|
28
28
|
def initialize(cmd, *argv)
|
|
29
29
|
@cmd = cmd
|
|
30
30
|
@argv = argv.dup
|
|
31
|
-
@env =
|
|
31
|
+
@env = nil
|
|
32
32
|
@status = nil
|
|
33
33
|
@spawned = false
|
|
34
34
|
@stdout = ""
|
|
35
35
|
@stderr = ""
|
|
36
36
|
@enoent = false
|
|
37
|
+
@limit = {stdout: nil, stderr: nil}
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
##
|
|
@@ -49,7 +50,7 @@ class Test::Command
|
|
|
49
50
|
# Environment variables to set for the spawned command
|
|
50
51
|
# @return [Test::Command]
|
|
51
52
|
def env(env)
|
|
52
|
-
tap { @env.merge!(env) }
|
|
53
|
+
tap { (@env ||= {}).merge!(env) }
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
##
|
|
@@ -67,6 +68,19 @@ class Test::Command
|
|
|
67
68
|
tap { @stdin = data }
|
|
68
69
|
end
|
|
69
70
|
|
|
71
|
+
##
|
|
72
|
+
# Limits how many bytes of each stream are captured.
|
|
73
|
+
# A stream output beyond the limit is discarded, keeping
|
|
74
|
+
# memory bounded even for a command that streams a lot.
|
|
75
|
+
# @param [Integer, nil] stdout
|
|
76
|
+
# Max bytes of stdout to read, or nil for unlimited
|
|
77
|
+
# @param [Integer, nil] stderr
|
|
78
|
+
# Max bytes of stderr to read, or nil for unlimited
|
|
79
|
+
# @return [Test::Command]
|
|
80
|
+
def limit(stdout: nil, stderr: nil)
|
|
81
|
+
tap { @limit = {stdout:, stderr:} }
|
|
82
|
+
end
|
|
83
|
+
|
|
70
84
|
##
|
|
71
85
|
# Spawns a command
|
|
72
86
|
# @return [Test::Command]
|
|
@@ -77,7 +91,7 @@ class Test::Command
|
|
|
77
91
|
@spawned = true
|
|
78
92
|
@out, @err = Pipe.pair, Pipe.pair
|
|
79
93
|
@pid = Process.spawn(
|
|
80
|
-
|
|
94
|
+
envp,
|
|
81
95
|
@cmd, *@argv.map(&:to_s),
|
|
82
96
|
{in: @in_r, out: @out.w, err: @err.w}
|
|
83
97
|
)
|
|
@@ -85,8 +99,8 @@ class Test::Command
|
|
|
85
99
|
@out.w.close
|
|
86
100
|
@err.w.close
|
|
87
101
|
@producer = Thread.new do
|
|
88
|
-
@stdout = @out.r
|
|
89
|
-
@stderr = @err.r
|
|
102
|
+
@stdout = read_limited(@out.r, @limit[:stdout])
|
|
103
|
+
@stderr = read_limited(@err.r, @limit[:stderr])
|
|
90
104
|
Process.wait
|
|
91
105
|
@status = $?
|
|
92
106
|
ensure
|
|
@@ -232,6 +246,54 @@ class Test::Command
|
|
|
232
246
|
|
|
233
247
|
private
|
|
234
248
|
|
|
249
|
+
##
|
|
250
|
+
# Builds the environment passed to Process.spawn.
|
|
251
|
+
# The spawned process receives only the variables
|
|
252
|
+
# set with #env: every inherited variable is
|
|
253
|
+
# cleared, so an empty @env spawns with an empty
|
|
254
|
+
# environment.
|
|
255
|
+
# @return [Hash{String => String}]
|
|
256
|
+
def envp
|
|
257
|
+
if @env.nil?
|
|
258
|
+
##
|
|
259
|
+
# Inherit the parent's environment
|
|
260
|
+
ENV.to_h
|
|
261
|
+
else
|
|
262
|
+
##
|
|
263
|
+
# Clear all environment variables except those
|
|
264
|
+
# that are set by `@env`. Always inherit ${PATH}.
|
|
265
|
+
envp = ENV.except("PATH")
|
|
266
|
+
.each_key
|
|
267
|
+
.to_h { |key| [key, nil] }
|
|
268
|
+
envp.merge!(@env)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
##
|
|
273
|
+
# Reads up to +max+ bytes from a stream, then drains
|
|
274
|
+
# the remainder so a child writing more than the limit
|
|
275
|
+
# does not block on a full pipe. When +max+ is nil the
|
|
276
|
+
# entire stream is read.
|
|
277
|
+
# @param [IO] io
|
|
278
|
+
# @param [Integer, nil] max
|
|
279
|
+
# @return [String]
|
|
280
|
+
def read_limited(io, max)
|
|
281
|
+
return io.read if max.nil?
|
|
282
|
+
buf = +""
|
|
283
|
+
remaining = max
|
|
284
|
+
while remaining > 0 && (chunk = io.read(remaining))
|
|
285
|
+
buf << chunk
|
|
286
|
+
remaining -= chunk.bytesize
|
|
287
|
+
end
|
|
288
|
+
##
|
|
289
|
+
# Drain whatever is left so the child can exit:
|
|
290
|
+
# the drained bytes are discarded, keeping memory
|
|
291
|
+
# bounded.
|
|
292
|
+
while (chunk = io.read(16_384))
|
|
293
|
+
end
|
|
294
|
+
buf
|
|
295
|
+
end
|
|
296
|
+
|
|
235
297
|
##
|
|
236
298
|
# Returns a pipe pair used for the command's
|
|
237
299
|
# standard input. When no input is set, the child
|
data/test/cmd_test.rb
CHANGED
|
@@ -38,6 +38,41 @@ class Test::Command
|
|
|
38
38
|
.env("FOO" => "42")
|
|
39
39
|
.stdout
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
def test_ruby_env_only_given_keys
|
|
43
|
+
assert_equal "BAR\nFOO\nPATH\n",
|
|
44
|
+
::Test::Command.new("ruby", "-e", "puts ENV.keys.sort")
|
|
45
|
+
.env("FOO" => "42", "BAR" => "7")
|
|
46
|
+
.stdout
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_ruby_env_empty_is_empty_env
|
|
50
|
+
assert_equal "PATH",
|
|
51
|
+
::Test::Command.new("ruby", "-e", "print ENV.keys.sort.join(\",\")")
|
|
52
|
+
.env({})
|
|
53
|
+
.stdout
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_ruby_env_does_not_leak_inherited
|
|
57
|
+
assert_equal "true",
|
|
58
|
+
::Test::Command.new("ruby", "-e", "print ENV.key?('PATH')")
|
|
59
|
+
.stdout
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_ruby_env_path_carries_over_inherited
|
|
63
|
+
parent_path = ENV["PATH"]
|
|
64
|
+
assert_equal parent_path,
|
|
65
|
+
::Test::Command.new("ruby", "-e", "print ENV['PATH']")
|
|
66
|
+
.stdout
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_ruby_env_path_carries_over_empty
|
|
70
|
+
parent_path = ENV["PATH"]
|
|
71
|
+
assert_equal parent_path,
|
|
72
|
+
::Test::Command.new("ruby", "-e", "print ENV['PATH']")
|
|
73
|
+
.env({})
|
|
74
|
+
.stdout
|
|
75
|
+
end
|
|
41
76
|
end
|
|
42
77
|
|
|
43
78
|
##
|
|
@@ -58,6 +93,43 @@ class Test::Command
|
|
|
58
93
|
end
|
|
59
94
|
end
|
|
60
95
|
|
|
96
|
+
##
|
|
97
|
+
# Test::Command#limit
|
|
98
|
+
class LimitTest < Test
|
|
99
|
+
def test_ruby_limit_stdout
|
|
100
|
+
assert_equal "happy",
|
|
101
|
+
::Test::Command.new("printf", "%s", "happy days")
|
|
102
|
+
.limit(stdout: 5)
|
|
103
|
+
.stdout
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_ruby_limit_stderr
|
|
107
|
+
assert_equal "smile",
|
|
108
|
+
::Test::Command.new("sh", "-c", "echo smile >&2")
|
|
109
|
+
.limit(stderr: 5)
|
|
110
|
+
.stderr
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_ruby_limit_nil_is_unlimited
|
|
114
|
+
expect = "a" * 20_000
|
|
115
|
+
assert_equal expect,
|
|
116
|
+
::Test::Command.new("ruby", "-e", "print 'a' * 20_000")
|
|
117
|
+
.stdout
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_ruby_limit_drains_remaining_output
|
|
121
|
+
cmd = ::Test::Command.new("sh", "-c", "printf 'happy days'")
|
|
122
|
+
.limit(stdout: 5)
|
|
123
|
+
.spawn
|
|
124
|
+
assert_equal true, cmd.spawned?
|
|
125
|
+
##
|
|
126
|
+
# A full pipe would block forever: the
|
|
127
|
+
# drain lets the child finish and capture
|
|
128
|
+
# its status
|
|
129
|
+
assert_equal 0, cmd.exit_status
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
61
133
|
##
|
|
62
134
|
# Test::Command#{exit_status, status, success?}
|
|
63
135
|
class ExitStatusTest < Test
|