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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b091ee25f1e890969728e516747d3104c20076d2a71973e02ccc346a9eb1cc8e
4
- data.tar.gz: 4dbb449b245f30e89285b7cae84de42564e352a4f0790fcc3a8a048e21b3ede2
3
+ metadata.gz: 8f3880288c628fc25dc0a16dcee05222dc776ffb8cab098d2fad3610534ebded
4
+ data.tar.gz: 39573f323c733d4fef70b3e784c3b6c4f956f7da60e0c1bbe7e454d51487f003
5
5
  SHA512:
6
- metadata.gz: 43fb7bb3700e9f384c253c8ab36ec3a1ebb2ec5e807b3af1d57883a64bddb132838086fc15b242ce3a65b85092941d23a040b866b3e173bed4bf5254e2829690
7
- data.tar.gz: c314163ab04470596296e8644414bec98414955b7423a1cc5affe202184befedffaff5f1f63427b18dd192b356fe491d3ec85e1b1c5140857c185b3a7ea37402
6
+ metadata.gz: 3b8e25c54804ac4cfa14f4356a2ea9f3e7b97f994792ae3f77b60e19151d3ee66d84959a40347b82adf1e0b5c277c3d28ef81b85992a5a496881db8e695ca867
7
+ data.tar.gz: face7b9c865c69666ff8eb29393af0ce856777bdf70d617889021f3a8496c0d331be88c527eec3f861b606773d93b1ca70e634c7e08e4a12f60320d57777e5bd
@@ -12,7 +12,7 @@ jobs:
12
12
  fail-fast: false
13
13
  matrix:
14
14
  os: [ubuntu-latest, macos-latest]
15
- ruby: [3.1, 3.2, 3.3]
15
+ ruby: [3.1, 3.2, 3.3, 4.0]
16
16
  runs-on: ${{ matrix.os }}
17
17
  steps:
18
18
  - uses: actions/checkout@v2
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
- require "test-cmd"
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
- .new("ruby", "-e", "puts ENV['FOO']")
74
- .env("FOO" => "42")
75
- .stdout # => "42\n"
103
+ .new("env")
104
+ .env({})
105
+ .stdout
76
106
  ```
77
107
  </details>
78
108
 
@@ -4,5 +4,5 @@ end unless defined?(Test)
4
4
  class Test::Command
5
5
  ##
6
6
  # @string [String]
7
- VERSION = "2.4.0"
7
+ VERSION = "2.6.0"
8
8
  end
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
- @env,
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.read
89
- @stderr = @err.r.read
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-cmd.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Gleeson