scout-essentials 1.8.7 → 1.9.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/.vimproject +26 -12
- data/README.md +83 -112
- data/VERSION +1 -1
- data/doc/Improvements.md +226 -0
- data/doc/StartHere.md +122 -0
- data/doc/developer/AnnotationSystem.md +184 -0
- data/doc/developer/Architecture.md +147 -0
- data/doc/developer/Configuration.md +238 -0
- data/doc/developer/CoreUtilities.md +265 -0
- data/doc/developer/DesignPrinciples.md +129 -0
- data/doc/developer/ErrorHandling.md +203 -0
- data/doc/developer/LockingAndConcurrency.md +157 -0
- data/doc/developer/PathResolution.md +200 -0
- data/doc/developer/PersistenceAndResources.md +119 -0
- data/doc/developer/StreamingModel.md +236 -0
- data/doc/user/AnnotatingData.md +202 -0
- data/doc/user/CachingResults.md +183 -0
- data/doc/user/CommandLineOptions.md +189 -0
- data/doc/user/Cookbook.md +211 -0
- data/doc/user/HandlingStreams.md +236 -0
- data/doc/user/LoggingAndProgress.md +158 -0
- data/doc/user/ProducingResources.md +177 -0
- data/doc/user/RemoteData.md +157 -0
- data/doc/user/RunningCommands.md +218 -0
- data/doc/user/WorkingWithFiles.md +217 -0
- data/lib/scout/cmd.rb +343 -40
- data/lib/scout/concurrent_stream.rb +14 -1
- data/lib/scout/indiferent_hash.rb +1 -1
- data/lib/scout/log/fingerprint.rb +13 -8
- data/lib/scout/log/progress/report.rb +1 -1
- data/lib/scout/log.rb +4 -1
- data/lib/scout/misc/digest.rb +6 -5
- data/lib/scout/misc/format.rb +24 -0
- data/lib/scout/named_array.rb +1 -1
- data/lib/scout/open/stream.rb +2 -2
- data/lib/scout/open/util.rb +8 -4
- data/lib/scout/open.rb +3 -3
- data/lib/scout/path/find.rb +3 -2
- data/lib/scout/persist.rb +14 -10
- data/lib/scout/resource/produce.rb +9 -1
- data/research/annotations-data-analysis.md +206 -0
- data/research/behavior-probes.md +1925 -0
- data/research/commands-streaming-analysis.md +272 -0
- data/research/design-philosophy-analysis.md +383 -0
- data/research/doc-audit-findings.md +294 -0
- data/research/ecosystem-attribution.md +118 -0
- data/research/implementation-inventory-core.md +1029 -0
- data/research/implementation-inventory-open.md +417 -0
- data/research/implementation-inventory-path-persist-resource.md +774 -0
- data/research/io-paths-analysis.md +228 -0
- data/research/persistence-resources-analysis.md +244 -0
- data/research/synthesis-report.md +80 -0
- data/scout-essentials.gemspec +37 -15
- data/test/scout/open/test_remote.rb +1 -2
- data/test/scout/test_cmd.rb +411 -0
- metadata +36 -14
- data/doc/Annotation.md +0 -352
- data/doc/CMD.md +0 -363
- data/doc/ConcurrentStream.md +0 -163
- data/doc/IndiferentHash.md +0 -240
- data/doc/Log.md +0 -235
- data/doc/NamedArray.md +0 -174
- data/doc/Open.md +0 -331
- data/doc/Path.md +0 -217
- data/doc/Persist.md +0 -214
- data/doc/Resource.md +0 -229
- data/doc/SimpleOPT.md +0 -236
- data/doc/TmpFile.md +0 -154
data/test/scout/test_cmd.rb
CHANGED
|
@@ -16,6 +16,19 @@ class TestCmd < Test::Unit::TestCase
|
|
|
16
16
|
assert(CMD.process_cmd_options("--user-agent" => "firefox", "-q" => true) =~ /-q/)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def test_cmd_option_array
|
|
20
|
+
assert_equal(["--user-agent", "firefox"], CMD.process_cmd_options_array("--user-agent" => "firefox"))
|
|
21
|
+
assert_equal(["--user-agent=firefox"], CMD.process_cmd_options_array("--user-agent=" => "firefox"))
|
|
22
|
+
assert_equal(["-q"], CMD.process_cmd_options_array("-q" => true))
|
|
23
|
+
assert_equal([], CMD.process_cmd_options_array("-q" => nil))
|
|
24
|
+
assert_equal([], CMD.process_cmd_options_array("-q" => false))
|
|
25
|
+
|
|
26
|
+
result = CMD.process_cmd_options_array("--user-agent" => "firefox", "-q" => true)
|
|
27
|
+
assert_include result, "--user-agent"
|
|
28
|
+
assert_include result, "firefox"
|
|
29
|
+
assert_include result, "-q"
|
|
30
|
+
end
|
|
31
|
+
|
|
19
32
|
def test_cmd
|
|
20
33
|
assert_equal("test\n", CMD.cmd("echo '{opt}' test").read)
|
|
21
34
|
assert_equal("test", CMD.cmd("echo '{opt}' test", "-n" => true).read)
|
|
@@ -96,4 +109,402 @@ line33
|
|
|
96
109
|
assert_equal "TEST", CMD.bash("echo TEST").read.strip
|
|
97
110
|
assert_equal ENV["HOME"], CMD.bash("echo $HOME").read.strip
|
|
98
111
|
end
|
|
112
|
+
|
|
113
|
+
def test_cmd_array_basic
|
|
114
|
+
assert_equal("hello\n", CMD.cmd(["echo", "hello"]).read)
|
|
115
|
+
assert_equal("hello world\n", CMD.cmd(["echo", "hello world"]).read)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_cmd_array_special_chars
|
|
119
|
+
# Special characters should be treated literally, NOT interpreted by shell
|
|
120
|
+
assert_equal("hello; rm -rf /\n", CMD.cmd(["echo", "hello; rm -rf /"]).read)
|
|
121
|
+
assert_equal("$HOME\n", CMD.cmd(["echo", "$HOME"]).read)
|
|
122
|
+
assert_equal("hello | cat\n", CMD.cmd(["echo", "hello | cat"]).read)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_cmd_array_with_options
|
|
126
|
+
# Hash options with boolean flag add the flag to the command array
|
|
127
|
+
# Options hash with flag => value are appended as separate args
|
|
128
|
+
assert_equal("two\n", CMD.cmd(["cut"], "-f" => 2, "-d" => " ", :in => "one two three").read)
|
|
129
|
+
# String second argument is appended to the command array
|
|
130
|
+
assert_equal("world", CMD.cmd(["echo", "-n"], "world").read)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_cmd_array_with_stdin
|
|
134
|
+
assert_equal("data", CMD.cmd(["cat"], :in => "data").read)
|
|
135
|
+
assert_equal("TRANSFORMED\n", CMD.cmd(["tr", "a-z", "A-Z"], :in => "transformed\n").read)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_cmd_array_pipe
|
|
139
|
+
assert_equal("test\n", CMD.cmd(["echo", "test"], :pipe => true).read)
|
|
140
|
+
|
|
141
|
+
text = "line1\nline2\nline3\n"
|
|
142
|
+
io = CMD.cmd(["cat"], :in => text, :pipe => true)
|
|
143
|
+
assert_equal(text, io.read)
|
|
144
|
+
io.join
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def test_cmd_array_pipe_chain
|
|
148
|
+
text = "line1\nline2\nline3\nline4\n"
|
|
149
|
+
io1 = CMD.cmd(["cat"], :in => text, :pipe => true)
|
|
150
|
+
io2 = CMD.cmd(["grep", "line"], :in => io1, :pipe => true)
|
|
151
|
+
assert_equal(4, io2.read.split(/\n/).length)
|
|
152
|
+
io2.join
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def test_cmd_array_error
|
|
156
|
+
Log.with_severity 6 do
|
|
157
|
+
assert_raise ProcessFailed do CMD.cmd(["fake-command"]) end
|
|
158
|
+
assert_raise ProcessFailed do CMD.cmd(["false"]) end
|
|
159
|
+
|
|
160
|
+
assert_nothing_raised ProcessFailed do CMD.cmd(["fake-command"], :no_fail => true, :pipe => true) end
|
|
161
|
+
assert_nothing_raised ProcessFailed do CMD.cmd(["false"], :no_fail => true, :pipe => true) end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_cmd_array_save_stderr
|
|
166
|
+
# save_stderr captures stderr onto the result's std_err
|
|
167
|
+
result = CMD.cmd(["ls", "/nonexistent_dir_12345"], :no_fail => true, :save_stderr => true)
|
|
168
|
+
assert_match(/No such file/, result.std_err)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def test_cmd_array_no_fail_nonpipe
|
|
172
|
+
# no_fail in non-pipe mode should not raise
|
|
173
|
+
Log.with_severity 6 do
|
|
174
|
+
assert_nothing_raised ProcessFailed do CMD.cmd(["false"], :no_fail => true) end
|
|
175
|
+
assert_nothing_raised ProcessFailed do CMD.cmd(["fake-command"], :no_fail => true) end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
### :timeout option
|
|
180
|
+
|
|
181
|
+
def test_cmd_timeout_nonpipe_raises
|
|
182
|
+
Log.with_severity 6 do
|
|
183
|
+
e = assert_raise(CMD::Timeout) do
|
|
184
|
+
CMD.cmd("sleep 5", :timeout => 0.3)
|
|
185
|
+
end
|
|
186
|
+
assert_match(/sleep 5/, e.message)
|
|
187
|
+
assert_match(/0\.3/, e.message)
|
|
188
|
+
assert_match(/sleep 5/, e.command)
|
|
189
|
+
assert_equal(0.3, e.timeout)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def test_cmd_timeout_nonpipe_no_zombie
|
|
194
|
+
Log.with_severity 6 do
|
|
195
|
+
e = assert_raise(CMD::Timeout) do
|
|
196
|
+
CMD.cmd("sleep 30", :timeout => 0.3)
|
|
197
|
+
end
|
|
198
|
+
pid = e.pid
|
|
199
|
+
|
|
200
|
+
# The pid is reaped by the watchdog: once reaped, waitpid raises
|
|
201
|
+
# Errno::ECHILD. Retry for a while to allow for the kill grace period
|
|
202
|
+
reaped = false
|
|
203
|
+
40.times do
|
|
204
|
+
begin
|
|
205
|
+
Process.waitpid(pid, Process::WNOHANG)
|
|
206
|
+
sleep 0.05
|
|
207
|
+
rescue Errno::ECHILD
|
|
208
|
+
reaped = true
|
|
209
|
+
break
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
assert(reaped, "pid #{pid} was not reaped after the timeout")
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def test_cmd_timeout_nonpipe_completes
|
|
217
|
+
# A command that finishes before the timeout works as if no timeout was
|
|
218
|
+
# given, and the option does not leak into the command line
|
|
219
|
+
assert_equal("test\n", CMD.cmd("echo test", :timeout => 5).read)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def test_cmd_timeout_pipe_read_raises
|
|
223
|
+
Log.with_severity 6 do
|
|
224
|
+
assert_raise(CMD::Timeout) do
|
|
225
|
+
CMD.cmd("sleep 5", :pipe => true, :timeout => 0.3).read
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def test_cmd_timeout_pipe_join_raises
|
|
231
|
+
Log.with_severity 6 do
|
|
232
|
+
io = CMD.cmd("sleep 5", :pipe => true, :timeout => 0.3)
|
|
233
|
+
assert_raise(CMD::Timeout){ io.join }
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def test_cmd_timeout_pipe_partial_output
|
|
238
|
+
Log.with_severity 6 do
|
|
239
|
+
io = CMD.cmd("bash -c 'echo START; sleep 5'", :pipe => true, :timeout => 0.5)
|
|
240
|
+
line = nil
|
|
241
|
+
begin
|
|
242
|
+
line = io.gets
|
|
243
|
+
rescue IOError
|
|
244
|
+
# closing the stream may interrupt the read first
|
|
245
|
+
end
|
|
246
|
+
assert_equal("START\n", line)
|
|
247
|
+
assert_raise(CMD::Timeout){ io.join }
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def test_cmd_timeout_pipe_completes
|
|
252
|
+
io = CMD.cmd("echo test", :pipe => true, :timeout => 5)
|
|
253
|
+
assert_equal("test\n", io.read)
|
|
254
|
+
assert_nothing_raised{ io.join }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def test_cmd_timeout_pipe_stderr_thread
|
|
258
|
+
# Exercises the stderr consumer thread cleanup on timeout
|
|
259
|
+
Log.with_severity 6 do
|
|
260
|
+
assert_raise(CMD::Timeout) do
|
|
261
|
+
CMD.cmd("bash -c 'echo OUT; echo ERR >&2; sleep 5'", :pipe => true, :timeout => 0.3, :stderr => 0).read
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def test_cmd_timeout_pipe_blocked_input
|
|
267
|
+
# The stdin feeder thread blocks writing to the process; it must be
|
|
268
|
+
# interrupted when the timeout aborts the stream
|
|
269
|
+
Log.with_severity 6 do
|
|
270
|
+
content = "line\n" * 100_000
|
|
271
|
+
assert_raise(CMD::Timeout) do
|
|
272
|
+
CMD.cmd("bash -c 'read line; sleep 5'", :in => content, :pipe => true, :timeout => 0.5).read
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def test_cmd_timeout_is_process_failed
|
|
278
|
+
Log.with_severity 6 do
|
|
279
|
+
caught = nil
|
|
280
|
+
begin
|
|
281
|
+
CMD.cmd("sleep 5", :timeout => 0.3)
|
|
282
|
+
rescue ProcessFailed
|
|
283
|
+
caught = $!
|
|
284
|
+
end
|
|
285
|
+
assert(CMD::Timeout === caught)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def test_cmd_failure_without_timeout_is_not_timeout
|
|
290
|
+
Log.with_severity 6 do
|
|
291
|
+
e = assert_raise(ProcessFailed){ CMD.cmd("false") }
|
|
292
|
+
assert(! (CMD::Timeout === e))
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def test_cmd_no_timeout_option_long_command
|
|
297
|
+
# Without :timeout the behaviour is unchanged: the caller waits
|
|
298
|
+
assert_equal("done\n", CMD.cmd("bash -c 'sleep 0.2; echo done'").read)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def stderr_emitting_command(lines = 3, sleep_time = 0.3, pause_after = 0)
|
|
302
|
+
"bash -c '#{(1..lines).map{|i| "sleep #{sleep_time}; echo error#{i} >&2"}.join('; ')}; sleep #{pause_after}'"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def test_save_stderr_pipe_string_file_lives_tails
|
|
306
|
+
Log.with_severity 6 do
|
|
307
|
+
TmpFile.with_file(".log", false) do |tmp|
|
|
308
|
+
# The child also prints stdout lines that the parent consumes slowly,
|
|
309
|
+
# so the pipe is genuinely being consumed while stderr reaches the log
|
|
310
|
+
cmd = "bash -c 'sleep 0.2; echo error1 >&2; echo out1; sleep 0.4; echo error2 >&2; echo out2; sleep 0.4; echo error3 >&2; echo out3'"
|
|
311
|
+
io = CMD.cmd(cmd, :pipe => true, :stderr => 0, :save_stderr => tmp)
|
|
312
|
+
assert ! io.closed?
|
|
313
|
+
|
|
314
|
+
# Poll the log size while reading stdout one line at a time: the file
|
|
315
|
+
# must grow before the command ends, proving incremental flushing
|
|
316
|
+
sizes = []
|
|
317
|
+
lines = []
|
|
318
|
+
while line = io.gets
|
|
319
|
+
lines << line
|
|
320
|
+
sleep 0.1
|
|
321
|
+
sizes << File.size(tmp) if File.exist?(tmp)
|
|
322
|
+
end
|
|
323
|
+
io.join
|
|
324
|
+
|
|
325
|
+
assert_equal %w(out1 out2 out3), lines.map(&:chomp)
|
|
326
|
+
assert(sizes.any?{|s| s > 0}, "log file should grow while the command runs: #{sizes.inspect}")
|
|
327
|
+
# Sizes must be non-decreasing, and more than one positive size means
|
|
328
|
+
# the file was already being filled before the command finished
|
|
329
|
+
assert_equal(sizes.sort, sizes, "log file size must not shrink: #{sizes.inspect}")
|
|
330
|
+
assert(sizes.count{|s| s > 0} >= 2, "log file should have been flushed several times while running: #{sizes.inspect}")
|
|
331
|
+
assert io.closed?
|
|
332
|
+
content = File.read(tmp)
|
|
333
|
+
assert_equal "error1\nerror2\nerror3\n", content
|
|
334
|
+
assert_equal "error1\nerror2\nerror3\n", io.std_err
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def test_save_stderr_pipe_scout_path
|
|
340
|
+
Log.with_severity 6 do
|
|
341
|
+
tmp = Path.setup(File.join(TmpFile.tmpdir, 'scout_path_stderr.log'))
|
|
342
|
+
CMD.cmd("bash -c 'echo error1 >&2; echo error2 >&2'", :pipe => true, :stderr => 0, :save_stderr => tmp).read
|
|
343
|
+
assert_equal "error1\nerror2\n", File.read(tmp.find)
|
|
344
|
+
assert(File.exist?(tmp.find))
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def test_save_stderr_pipe_pathname
|
|
349
|
+
Log.with_severity 6 do
|
|
350
|
+
TmpFile.with_file(".log", false) do |tmp|
|
|
351
|
+
pathname = Pathname.new(tmp)
|
|
352
|
+
CMD.cmd("bash -c 'echo error1 >&2; echo error2 >&2'", :pipe => true, :stderr => 0, :save_stderr => pathname).read
|
|
353
|
+
assert_equal "error1\nerror2\n", File.read(tmp)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def test_save_stderr_pipe_io_is_not_closed
|
|
359
|
+
Log.with_severity 6 do
|
|
360
|
+
TmpFile.with_file(".log", false) do |tmp|
|
|
361
|
+
io_dst = File.open(tmp, 'w')
|
|
362
|
+
CMD.cmd("bash -c 'echo error1 >&2; echo error2 >&2'", :pipe => true, :stderr => 0, :save_stderr => io_dst).read
|
|
363
|
+
assert ! io_dst.closed?
|
|
364
|
+
io_dst.close
|
|
365
|
+
assert_equal "error1\nerror2\n", File.read(tmp)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
string_io = StringIO.new
|
|
369
|
+
CMD.cmd("bash -c 'echo error1 >&2'", :pipe => true, :stderr => 0, :save_stderr => string_io).read
|
|
370
|
+
assert ! string_io.closed?
|
|
371
|
+
assert_equal "error1\n", string_io.string
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def test_save_stderr_pipe_file_closed_by_cmd
|
|
376
|
+
Log.with_severity 6 do
|
|
377
|
+
TmpFile.with_file(".log", false) do |tmp|
|
|
378
|
+
fds_before = Dir["/proc/self/fd/*"].length
|
|
379
|
+
CMD.cmd("bash -c 'echo error1 >&2; echo error2 >&2'", :pipe => true, :stderr => 0, :save_stderr => tmp).read
|
|
380
|
+
Misc.insist(10, 0.05) do
|
|
381
|
+
raise "fd leaked" unless Dir["/proc/self/fd/*"].length <= fds_before + 1
|
|
382
|
+
end
|
|
383
|
+
fds_after = Dir["/proc/self/fd/*"].length
|
|
384
|
+
assert(fds_after <= fds_before + 1, "CMD should close the file it opened: #{fds_before} -> #{fds_after}")
|
|
385
|
+
|
|
386
|
+
# A second run truncates correctly: proves the previous run closed it
|
|
387
|
+
CMD.cmd("bash -c 'echo only >&2'", :pipe => true, :stderr => 0, :save_stderr => tmp).read
|
|
388
|
+
assert_equal "only\n", File.read(tmp)
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def test_save_stderr_nonpipe_path
|
|
394
|
+
Log.with_severity 6 do
|
|
395
|
+
TmpFile.with_file(".log", false) do |tmp|
|
|
396
|
+
CMD.cmd("bash -c 'echo error1 >&2; echo error2 >&2'", :save_stderr => tmp, :stderr => 0)
|
|
397
|
+
assert_equal "error1\nerror2\n", File.read(tmp)
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def test_save_stderr_nonpipe_io
|
|
403
|
+
Log.with_severity 6 do
|
|
404
|
+
string_io = StringIO.new
|
|
405
|
+
CMD.cmd("bash -c 'echo error1 >&2'", :save_stderr => string_io, :stderr => 0)
|
|
406
|
+
assert_equal "error1\n", string_io.string
|
|
407
|
+
assert ! string_io.closed?
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def test_save_stderr_boolean_still_populates_std_err
|
|
412
|
+
Log.with_severity 6 do
|
|
413
|
+
io = CMD.cmd("bash -c 'echo error1 >&2'", :pipe => true, :stderr => 0, :save_stderr => true)
|
|
414
|
+
io.join
|
|
415
|
+
assert_equal "error1\n", io.std_err
|
|
416
|
+
|
|
417
|
+
out = CMD.cmd("bash -c 'echo error1 >&2'", :save_stderr => true, :stderr => 0)
|
|
418
|
+
assert_equal "error1\n", out.std_err
|
|
419
|
+
|
|
420
|
+
out = CMD.cmd("bash -c 'echo error1 >&2'", :stderr => 0)
|
|
421
|
+
assert ! out.respond_to?(:std_err) || out.std_err.nil? || out.std_err.empty?
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def test_save_stderr_timeout_keeps_file
|
|
426
|
+
Log.with_severity 6 do
|
|
427
|
+
TmpFile.with_file(".log", false) do |tmp|
|
|
428
|
+
assert_raise(CMD::Timeout) do
|
|
429
|
+
CMD.cmd("bash -c 'echo error1 >&2; sleep 5'", :pipe => true, :stderr => 0, :save_stderr => tmp, :timeout => 0.5).read
|
|
430
|
+
end
|
|
431
|
+
Misc.insist(3, 0.2, 'timeout log file to be flushed') do
|
|
432
|
+
raise "missing #{tmp}" unless File.exist?(tmp)
|
|
433
|
+
end
|
|
434
|
+
assert(File.exist?(tmp))
|
|
435
|
+
assert_equal "error1\n", File.read(tmp)
|
|
436
|
+
|
|
437
|
+
# The destination was closed even though the command timed out
|
|
438
|
+
fds_before = Dir["/proc/self/fd/*"].length
|
|
439
|
+
assert_equal(fds_before, Dir["/proc/self/fd/*"].length)
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def test_save_stderr_nonpipe_failure_writes_log
|
|
445
|
+
Log.with_severity 6 do
|
|
446
|
+
TmpFile.with_file(".log", false) do |tmp|
|
|
447
|
+
assert_raise(ProcessFailed) do
|
|
448
|
+
CMD.cmd("bash -c 'echo error1 >&2; exit 3'", :save_stderr => tmp, :stderr => 0)
|
|
449
|
+
end
|
|
450
|
+
# The ProcessFailed message embeds stderr and the destination keeps it
|
|
451
|
+
# even though the command failed
|
|
452
|
+
assert(File.exist?(tmp))
|
|
453
|
+
assert_equal "error1\n", File.read(tmp)
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def test_save_stderr_spawn_failure_closes_destination
|
|
459
|
+
Log.with_severity 6 do
|
|
460
|
+
TmpFile.with_file(".log", false) do |tmp|
|
|
461
|
+
3.times do |i|
|
|
462
|
+
assert_raise(ProcessFailed) do
|
|
463
|
+
CMD.cmd(['/no/such/binary_xyz'], :save_stderr => tmp, :stderr => 0)
|
|
464
|
+
end
|
|
465
|
+
# No fd still points at the destination file: CMD closed what it
|
|
466
|
+
# opened even though the command never started (the popen3 pipes
|
|
467
|
+
# themselves are pre-existing noise here, they are not related to
|
|
468
|
+
# the destination)
|
|
469
|
+
leaked = Dir["/proc/self/fd/*"].select do |fd|
|
|
470
|
+
(File.readlink(fd) rescue nil) == tmp
|
|
471
|
+
end
|
|
472
|
+
assert(leaked.empty?, "destination fd leaked on spawn failure: #{leaked.inspect}")
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# And the path is reusable: a later write is not clobbered by a
|
|
476
|
+
# buffered handle left behind
|
|
477
|
+
File.open(tmp, 'w') { |f| f.write "marker\n" }
|
|
478
|
+
assert_equal "marker\n", File.read(tmp)
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def test_save_stderr_creates_parent_dirs
|
|
484
|
+
Log.with_severity 6 do
|
|
485
|
+
subdir = File.join(TmpFile.tmpdir, 'missing1', 'missing2')
|
|
486
|
+
tmp = File.join(subdir, 'log.txt')
|
|
487
|
+
assert ! File.exist?(subdir)
|
|
488
|
+
CMD.cmd("bash -c 'echo error1 >&2'", :save_stderr => tmp, :stderr => 0)
|
|
489
|
+
assert File.exist?(tmp)
|
|
490
|
+
assert_equal "error1\n", File.read(tmp)
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def test_save_stderr_unwritable_path_raises
|
|
495
|
+
Log.with_severity 6 do
|
|
496
|
+
TmpFile.with_file("blocked", false) do |blocked|
|
|
497
|
+
File.open(blocked, 'w') do |f| f.puts 'not a dir' end
|
|
498
|
+
bad = File.join(blocked, 'sub', 'log.txt')
|
|
499
|
+
|
|
500
|
+
raised = false
|
|
501
|
+
begin
|
|
502
|
+
CMD.cmd("echo x", :save_stderr => bad, :stderr => 0)
|
|
503
|
+
rescue Exception
|
|
504
|
+
raised = true
|
|
505
|
+
end
|
|
506
|
+
assert raised, "an unwritable path must raise, not be silently ignored"
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
end
|
|
99
510
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: scout-essentials
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Miguel Vazquez
|
|
@@ -150,18 +150,28 @@ files:
|
|
|
150
150
|
- README.md
|
|
151
151
|
- Rakefile
|
|
152
152
|
- VERSION
|
|
153
|
-
- doc/
|
|
154
|
-
- doc/
|
|
155
|
-
- doc/
|
|
156
|
-
- doc/
|
|
157
|
-
- doc/
|
|
158
|
-
- doc/
|
|
159
|
-
- doc/
|
|
160
|
-
- doc/
|
|
161
|
-
- doc/
|
|
162
|
-
- doc/
|
|
163
|
-
- doc/
|
|
164
|
-
- doc/
|
|
153
|
+
- doc/Improvements.md
|
|
154
|
+
- doc/StartHere.md
|
|
155
|
+
- doc/developer/AnnotationSystem.md
|
|
156
|
+
- doc/developer/Architecture.md
|
|
157
|
+
- doc/developer/Configuration.md
|
|
158
|
+
- doc/developer/CoreUtilities.md
|
|
159
|
+
- doc/developer/DesignPrinciples.md
|
|
160
|
+
- doc/developer/ErrorHandling.md
|
|
161
|
+
- doc/developer/LockingAndConcurrency.md
|
|
162
|
+
- doc/developer/PathResolution.md
|
|
163
|
+
- doc/developer/PersistenceAndResources.md
|
|
164
|
+
- doc/developer/StreamingModel.md
|
|
165
|
+
- doc/user/AnnotatingData.md
|
|
166
|
+
- doc/user/CachingResults.md
|
|
167
|
+
- doc/user/CommandLineOptions.md
|
|
168
|
+
- doc/user/Cookbook.md
|
|
169
|
+
- doc/user/HandlingStreams.md
|
|
170
|
+
- doc/user/LoggingAndProgress.md
|
|
171
|
+
- doc/user/ProducingResources.md
|
|
172
|
+
- doc/user/RemoteData.md
|
|
173
|
+
- doc/user/RunningCommands.md
|
|
174
|
+
- doc/user/WorkingWithFiles.md
|
|
165
175
|
- lib/scout-essentials.rb
|
|
166
176
|
- lib/scout/annotation.rb
|
|
167
177
|
- lib/scout/annotation/annotated_object.rb
|
|
@@ -228,6 +238,18 @@ files:
|
|
|
228
238
|
- lib/scout/simple_opt/parse.rb
|
|
229
239
|
- lib/scout/simple_opt/setup.rb
|
|
230
240
|
- lib/scout/tmpfile.rb
|
|
241
|
+
- research/annotations-data-analysis.md
|
|
242
|
+
- research/behavior-probes.md
|
|
243
|
+
- research/commands-streaming-analysis.md
|
|
244
|
+
- research/design-philosophy-analysis.md
|
|
245
|
+
- research/doc-audit-findings.md
|
|
246
|
+
- research/ecosystem-attribution.md
|
|
247
|
+
- research/implementation-inventory-core.md
|
|
248
|
+
- research/implementation-inventory-open.md
|
|
249
|
+
- research/implementation-inventory-path-persist-resource.md
|
|
250
|
+
- research/io-paths-analysis.md
|
|
251
|
+
- research/persistence-resources-analysis.md
|
|
252
|
+
- research/synthesis-report.md
|
|
231
253
|
- scout-essentials.gemspec
|
|
232
254
|
- share/color/color_names
|
|
233
255
|
- share/color/diverging_colors.hex
|
|
@@ -298,7 +320,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
298
320
|
- !ruby/object:Gem::Version
|
|
299
321
|
version: '0'
|
|
300
322
|
requirements: []
|
|
301
|
-
rubygems_version: 3.7.
|
|
323
|
+
rubygems_version: 3.7.2
|
|
302
324
|
specification_version: 4
|
|
303
325
|
summary: Scout essential tools
|
|
304
326
|
test_files: []
|