scout-essentials 1.9.1 → 1.9.3

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +18 -1
  3. data/Rakefile +9 -0
  4. data/VERSION +1 -1
  5. data/doc/Improvements.md +20 -0
  6. data/doc/StartHere.md +7 -4
  7. data/doc/developer/AnnotationSystem.md +10 -15
  8. data/doc/developer/Configuration.md +20 -30
  9. data/doc/developer/CoreUtilities.md +52 -22
  10. data/doc/developer/DesignPrinciples.md +7 -9
  11. data/doc/developer/DocumentationPractices.md +476 -0
  12. data/doc/developer/ErrorHandling.md +11 -20
  13. data/doc/developer/LockingAndConcurrency.md +20 -13
  14. data/doc/developer/StreamingModel.md +13 -14
  15. data/doc/developer/Testing.md +389 -0
  16. data/doc/user/AnnotatingData.md +9 -13
  17. data/doc/user/CommandLineOptions.md +9 -12
  18. data/doc/user/Cookbook.md +7 -10
  19. data/doc/user/HandlingStreams.md +22 -29
  20. data/doc/user/LoggingAndProgress.md +9 -11
  21. data/doc/user/ProducingResources.md +0 -1
  22. data/doc/user/RemoteData.md +10 -13
  23. data/doc/user/RunningCommands.md +12 -17
  24. data/doc/user/WorkingWithFiles.md +10 -2
  25. data/lib/scout/config.rb +1 -0
  26. data/lib/scout/indiferent_hash.rb +8 -2
  27. data/lib/scout/log/fingerprint.rb +1 -1
  28. data/lib/scout/named_array.rb +4 -0
  29. data/lib/scout/open/final.rb +1 -1
  30. data/lib/scout/open/stream.rb +14 -13
  31. data/lib/scout/path/util.rb +1 -1
  32. data/lib/scout/testing/availability.rb +264 -0
  33. data/lib/scout/testing/scout_state.rb +193 -0
  34. data/lib/scout/testing.rb +82 -0
  35. data/research/doc-audit-findings.md +7 -5
  36. data/research/doc-structure.md +89 -0
  37. data/research/lock-namespace-blindness.md +73 -0
  38. data/research/probing-guide.md +311 -0
  39. data/research/testing-availability-scout-ai-handoff.md +235 -0
  40. data/research/testing-availability-scout-rig-handoff.md +355 -0
  41. data/scout-essentials.gemspec +24 -3
  42. data/test/scout/annotation/test_dup_clone.rb +31 -0
  43. data/test/scout/indiferent_hash/test_case_insensitive_staleness.rb +27 -0
  44. data/test/scout/indiferent_hash/test_string2hash_false.rb +13 -0
  45. data/test/scout/misc/test_timespan.rb +30 -0
  46. data/test/scout/open/test_compression_detection.rb +32 -0
  47. data/test/scout/open/test_forced_gzip_broken.rb +25 -0
  48. data/test/scout/test_named_array_resolution.rb +49 -0
  49. data/test/scout/test_testing.rb +195 -0
  50. data/test/scout/test_tmpfile_cleanup.rb +23 -0
  51. data/test/scout/testing/test_availability.rb +222 -0
  52. data/test/scout/testing/test_state_bindings.rb +163 -0
  53. data/test/test_helper.rb +26 -12
  54. metadata +23 -2
data/doc/user/Cookbook.md CHANGED
@@ -1,10 +1,7 @@
1
1
  # Cookbook
2
2
 
3
3
  Short, self-contained recipes that combine the scout-essentials modules.
4
- Every snippet here was executed; the backing probes are listed under each
5
- heading and live in `tmp/rewrite_C/` (older probes in `tmp/rewrite_A/`,
6
- `tmp/rewrite_B/` are referenced by their P-numbers, which map to
7
- `research/behavior-probes.md`).
4
+ Every snippet here was executed against the current `lib/scout/`.
8
5
 
9
6
  The theme of the library: plain objects annotated with provenance, paths
10
7
  resolved from a declaration, work cached on disk, streams piped without
@@ -31,8 +28,8 @@ produced (a type like `:string`/`:proc`, literal contents, or a block); the
31
28
  claimed `Path` is annotated and its `produce` materialises it into the
32
29
  resource tree (`lib/scout/resource/path.rb:2`,
33
30
  `lib/scout/resource/produce.rb`). `produce_and_find` produces when needed and
34
- returns `self.find`. Probe: `tmp/rewrite_C/probe_03_cookbook.rb`
35
- (`claim` + first/second call returning the same path, `Open.read` content).
31
+ returns `self.find` (first and second call return the same path, `Open.read`
32
+ yields the claimed content).
36
33
 
37
34
  ## Cache invalidation with `:update` and `:check`
38
35
 
@@ -51,7 +48,7 @@ d = Persist.persist('dependent', :marshal,
51
48
  ```
52
49
 
53
50
  `:update => true` always re-runs the block; `:check` names a file whose mtime
54
- invalidates the entry. Probe: probe_03 (a == b, block not re-run; `:update`
51
+ invalidates the entry (a == b, block not re-run; `:update`
55
52
  re-runs; `:check` path resolves).
56
53
 
57
54
  ## Fetch a remote file
@@ -95,7 +92,7 @@ restored.tissue # => "Liver"
95
92
 
96
93
  `Annotation.setup(obj, "A|B", hash)` is the module-level deserialiser; the
97
94
  `"A|B"` string is split on `|` and each name is looked up as a constant. An
98
- unknown type name is **warned about and skipped** (probe_03 prints
95
+ unknown type name is **warned about and skipped** (the run prints
99
96
  `Annotation NoSuchAnnotation not defined` on STDERR, then
100
97
  `Annotation.setup('S004', 'NoSuchAnnotation', ...)` returns the plain
101
98
  un-annotated string). `serialize` produces a plain `Hash` with `:literal`,
@@ -138,7 +135,7 @@ rescue Aborted, AbortedStream
138
135
  end
139
136
  ```
140
137
 
141
- Probe: probe_03 (open_pipe data, tee_stream producing a real gzip file).
138
+ (open_pipe data, tee_stream producing a real gzip file).
142
139
  See [Handling Streams](HandlingStreams.md) and the
143
140
  [Streaming Model](../developer/StreamingModel.md).
144
141
 
@@ -193,7 +190,7 @@ SOPT.require(options, :organism) # ParameterException when nil
193
190
  ```
194
191
 
195
192
  The `*` marks an option as **taking a string value**; without it the option is
196
- a boolean. Probe: probe_02 (options hash, `argv_left == ["positional"]`,
193
+ a boolean. (options hash, `argv_left == ["positional"]`,
197
194
  `SOPT.require` raising). See
198
195
  [Command-Line Options](CommandLineOptions.md).
199
196
 
@@ -24,7 +24,7 @@ Attributes come from `ConcurrentStream.setup` (concurrent_stream.rb:12-70) and
24
24
  `:no_fail` on the returned stream. `exit_status` exists but is *not* reliable
25
25
  after a normal read+join — it stays `nil` because only `join_pids` sets it, and
26
26
  that method empties `pids` when it runs; see
27
- [Running Commands](RunningCommands.md) for the probe.
27
+ [Running Commands](RunningCommands.md).
28
28
 
29
29
  ## `close` vs `join` vs abort
30
30
 
@@ -45,7 +45,7 @@ that method empties `pids` when it runs; see
45
45
  `Aborted.new` raised in them, so producer bodies should rescue `Aborted`.
46
46
  - **`force_close` does not exist** in this repo. The only reference is a dead
47
47
  `respond_to?` guard inside `Open.grep` (open/util.rb:26); a plain IO has no
48
- such method (probe `tmp/rewrite_B/probe_13_force_close.rb`: `IO#respond_to?
48
+ such method (`IO#respond_to?
49
49
  (:force_close) => false`). The early-close tool is `abort`.
50
50
 
51
51
  Consumers should follow this rescue contract:
@@ -67,9 +67,8 @@ end
67
67
  ## Callbacks
68
68
 
69
69
  - `stream.add_callback(&block)` **composes**: it wraps the existing callback so
70
- the *new* block runs *after* the old one (probe
71
- `tmp/rewrite_B/probe_19_streaming_apis.rb`: `add_callback order:
72
- [:first, :second]`). `ConcurrentStream.setup(stream, &block)` also composes in
70
+ the *new* block runs *after* the old one (observed order
71
+ `[:first, :second]`). `ConcurrentStream.setup(stream, &block)` also composes in
73
72
  that order, which is why calling setup twice on the same stream is safe.
74
73
  - `callback` / `abort_callback` are plain accessors over single chained procs
75
74
  (built by setup when you pass `:callback` / `:abort_callback` options or a
@@ -96,8 +95,9 @@ there; `into_close` (default true) closes `into` when it responds to `close`.
96
95
  On `Aborted` or any exception it aborts the source, closes `into`, **removes the
97
96
  partial output file** and re-raises. With `in_thread: true` the whole drain runs
98
97
  in a new thread that is pushed onto `io.threads`. The block runs after a
99
- successful drain. Verified: `probe_19_streaming_apis.rb` (`consume_stream
100
- return: "data"`, `consume_stream into path: "into-file\n"`).
98
+ successful drain. (`consume_stream
99
+ return: "data"`, `consume_stream into path: "into-file\n"`)
100
+ .
101
101
 
102
102
  ### `Open.sensible_write(path, content, options = {}, &block)`
103
103
 
@@ -114,24 +114,20 @@ behaviours for streams:
114
114
  - After a successful copy the content stream is joined (but not if it is a
115
115
  `Path` or already joined).
116
116
 
117
- Verified: P25/P33 in `research/behavior-probes.md` ("Aborted in
118
- sensible_write: NOT raised (swallowed); partial tmp left: 0").
119
-
120
117
  ### `Open.open_pipe(do_fork = false, close = true, &block)`
121
118
 
122
119
  Creates a pipe and returns the **read end** (`sout`), with the block executed in
123
120
  a producer thread that writes the other end (`sin`).
124
121
 
125
122
  - **Block arity**: the block always receives `sin` whether it declares a
126
- parameter or not (arity 0 blocks simply ignore it). Verified:
127
- `probe_20_open_pipe_arity.rb` (`arity-0 block: "arity0\n"`, `arity-1: "w\n"`).
123
+ parameter or not (arity-0 blocks simply ignore it; `arity-0 block: "arity0\n"`, `arity-1: "w\n"`).
128
124
  - **No block** raises `RuntimeError "No block given"`.
129
125
  - **Fork mode** (`do_fork: true`) runs the block in a child process instead of
130
126
  a thread: the child purges registered input pipes, closes `sout`, yields,
131
127
  `exit! 0`; the parent closes `sin` and sets the pid on `sout` via
132
128
  `ConcurrentStream.setup(sout, :pids => [pid])` — no threads, no callbacks.
133
129
  `close: false` in the child leaves `sin` open after the block returns
134
- (verified: `fork mode: "from-fork\n"`, `fork noclose: "fork-noclose\n"`).
130
+ (same results in fork mode with and without `close`).
135
131
  - **Thread mode** pairs the two ends (`pair`), runs the block through
136
132
  `ConcurrentStream.process_stream` (close+join on exit, abort on error), and
137
133
  registers the thread on both ends. An exception in the block aborts the
@@ -141,8 +137,7 @@ a producer thread that writes the other end (`sin`).
141
137
 
142
138
  Takes **no arguments** and returns the raw `[sout, sin]` pair from `IO.pipe`
143
139
  (plus registering `sin` in `OPEN_PIPE_IN`). Calling it with a positional
144
- argument raises `ArgumentError` (probe `probe_19_streaming_apis.rb`:
145
- `Open.pipe positional: ArgumentError`). There is no multi-command helper here —
140
+ argument raises `ArgumentError`. There is no multi-command helper here —
146
141
  chain commands by feeding one stream into `:in` of the next `CMD.cmd`.
147
142
 
148
143
  ### `Open.tee_stream(stream)` / `tee_stream_thread_multiple(stream, num)`
@@ -152,8 +147,9 @@ Returns an **Array** of streams (`num` copies, default 2): the first is the
152
147
  reads the source once and writes every chunk to all copies; the main copy's
153
148
  callback joins the source and closes the extra write ends, and its
154
149
  `abort_callback` propagates an abort to the source and the other copies.
155
- Verified: P31/P33 and `probe_19_streaming_apis.rb` (`tee_stream count: 2`,
156
- `tee[0].autojoin => true`, `tee[1].autojoin => nil`).
150
+ (`tee_stream count: 2`,
151
+ `tee[0].autojoin => true`, `tee[1].autojoin => nil`)
152
+ .
157
153
 
158
154
  ```ruby
159
155
  main, copy = Open.tee_stream(CMD.cmd('gzip -c', :pipe => true, :in => input))
@@ -167,14 +163,13 @@ Builds a tee, then a monitor thread reads the monitor copy line by line calling
167
163
  consumes the returned stream, not after. Failures in the block abort the monitor
168
164
  and are re-raised into the returned stream (`out.raise $!` when supported). The
169
165
  returned stream is the second copy, annotated from the source and set up with
170
- the monitor thread. Verified: P33 and `probe_19_streaming_apis.rb`.
166
+ the monitor thread.
171
167
 
172
168
  ### `Open.read_stream(stream, size)`
173
169
 
174
170
  Blocking read of exactly `size` bytes (plain `stream.read(missing)` loop,
175
171
  `lib/scout/open/stream.rb:401`), raising `ClosedStream` if EOF is reached
176
- first. Useful for binary framing; `probe_19_streaming_apis.rb` shows
177
- `read_stream(4) => "0123"`.
172
+ first; useful for binary framing: `read_stream(4) => "0123"`.
178
173
 
179
174
  ### `Open.sort_stream(stream, header_hash: '#', cmd_args: nil, memory: false)`
180
175
 
@@ -183,8 +178,7 @@ Streams `header_hash`-prefixed lines straight through, then sorts the rest.
183
178
  <cmd_args>` (`-u` by default when `cmd_args` is nil) and consumes that stream
184
179
  into the output; `memory: true` reads the whole remainder, sorts it in Ruby and
185
180
  writes it out. Everything runs inside `ConcurrentStream.process_stream`, so the
186
- source is closed+joined and aborted on error. Verified:
187
- probe_19_streaming_apis.rb: feeding `"# header\nc\na\nb\n"` returns
181
+ source is closed+joined and aborted on error. feeding `"# header\nc\na\nb\n"` returns
188
182
  `"# header\na\nb\nc\n"` — the header passes through untouched, the rest is
189
183
  sorted.
190
184
 
@@ -193,7 +187,7 @@ sorted.
193
187
  Merges consecutive lines sharing the same first field, joining the other
194
188
  columns with `|` (or dropping empty parts when `compact: true`). An optional
195
189
  block receives the accumulated column array and its return value becomes the
196
- row payload. Verified: `probe_19_streaming_apis.rb`.
190
+ row payload.
197
191
 
198
192
  ## `Open.open` block form and `DontClose`
199
193
 
@@ -207,16 +201,15 @@ end
207
201
  `Open.open` yields and **always closes and joins** the IO afterwards (the
208
202
  `ensure` at open.rb:70-77). Raising `DontClose` with a payload makes the block
209
203
  form *return* the payload instead of the IO, while still closing — it is an
210
- early-return mechanism, not a way to keep the handle. Verified:
211
- `probe_21_dontclose.rb` (`DontClose returns payload: "payload"`, `closed after
212
- DontClose: true`). Any other exception aborts, joins and re-raises the stream.
204
+ early-return mechanism, not a way to keep the handle (`DontClose returns
205
+ payload: "payload"`, `closed after DontClose: true`). Any other exception
206
+ aborts, joins and re-raises the stream.
213
207
 
214
208
  ## Progress bars
215
209
 
216
210
  Pass `:progress_bar` (a `Log::ProgressBar`, `lib/scout/log/progress.rb`; the option key is `:progress_bar` — there is no `:bar` key)
217
- to `CMD.cmd` and each stderr line ticks it (`bar.process(line)` at cmd.rb:643):
218
- `probe_24_bar.rb` counts 2 ticks for a 2-line stderr, in both pipe and
219
- non-pipe mode. With `:log => true` (`CMD.cmd_log`) the stderr text is also
211
+ to `CMD.cmd` and each stderr line ticks it (`bar.process(line)` at cmd.rb:643): 2 ticks for a 2-line stderr, in both
212
+ pipe and non-pipe mode. With `:log => true` (`CMD.cmd_log`) the stderr text is also
220
213
  recorded in `stream.log`. `Log::ProgressBar` itself supports `:process =>
221
214
  proc{|elem| elem.length}` so a tick can be weighted per element.
222
215
 
@@ -19,8 +19,6 @@ unrecognised value — falls back to `Log.default_severity`, which reads
19
19
  `~/.scout/etc/log_severity` if that file exists and is `INFO` otherwise
20
20
  (log.rb:23-34).
21
21
 
22
- Verified by `tmp/rewrite_C/probe_04_log_severity.rb` (out04.txt): ladder
23
- order, `Log.severity == 4` with no env/file, `Log.with_severity(level){}`
24
22
  restoring the original value after the block.
25
23
 
26
24
  Change it at runtime with `Log.severity = Log::DEBUG`, or scope a block with
@@ -38,7 +36,7 @@ Change it at runtime with `Log.severity = Log::DEBUG`, or scope a block with
38
36
  - `Log::LAST` is one shared mutable `String` (starts as `"log"`, never frozen)
39
37
  used as a protocol between `logn` and progress-bar printing so bars know how
40
38
  many lines to move up and which kind of output came last
41
- (`progress/report.rb` `print`/`report`). Probe out04.txt confirms
39
+ (`progress/report.rb` `print`/`report`).
42
40
  `class == String`, `frozen? == false`.
43
41
 
44
42
  ## Message forms
@@ -46,8 +44,8 @@ Change it at runtime with `Log.severity = Log::DEBUG`, or scope a block with
46
44
  - `Log.debug "msg"` .. `Log.error "msg"` print when severity allows; the top
47
45
  of the ladder is the `Log::NONE` constant, not a method.
48
46
  - `Log.debug { "expensive " + build }` — **the block is lazy message
49
- evaluation, not a timer**: it is only called if the level passes. Probe
50
- (out05.txt): a counter incremented inside a block passed to `Log.debug`
47
+ evaluation, not a timer**: it is only called if the level passes. A
48
+ counter incremented inside a block passed to `Log.debug`
51
49
  stays at 0 under ERROR severity and reaches 1 under DEBUG severity.
52
50
  - `Log.exception(e)` (log.rb:254) prints `BACKTRACE` lines derived from
53
51
  `e.backtrace` after fingerprinting messages longer than 1000 chars; it
@@ -55,7 +53,7 @@ Change it at runtime with `Log.severity = Log::DEBUG`, or scope a block with
55
53
  it contains `NOSTACK`.
56
54
  - `Log.stack(stack)` (log.rb:318) prints a magenta header then the stack
57
55
  **reversed** (innermost frame first) unless `SCOUT_ORIGINAL_STACK=true`.
58
- Probe (out05.txt): a caller list `a.rb:1, b.rb:2, c.rb:3` prints as
56
+ A caller list `a.rb:1, b.rb:2, c.rb:3` prints as
59
57
  `c.rb:3`, `b.rb:2`, `a.rb:1`.
60
58
 
61
59
  ## Top-level debug helpers (defined on Object)
@@ -66,7 +64,7 @@ DEBUG. `ddd/lll/mmm/iii/wwww/eee` inspect an object at the matching severity,
66
64
  and the `f`-suffixed variants (`ddf`, `llf`, `mmf`, `iif`, `wwwf`, `eef`)
67
65
  fingerprint it first. `sss(level[, &blk])` sets or scopes severity. `ccc`
68
66
  (log.rb:439) enables `$scout_debug_log` around a block so nested `ccc` calls
69
- print. Probe out05.txt confirms all five are top-level methods defined inside
67
+ print. All five are top-level methods defined inside
70
68
  `lib/scout/log.rb`.
71
69
 
72
70
  ## Colors
@@ -90,7 +88,7 @@ show first/middle/last elements; hashes longer than 10 fall back to
90
88
  keys+values fingerprints; floats get 1/3/6 decimals depending on magnitude.
91
89
 
92
90
  The signature is `fingerprint(obj)` — **one positional argument**; there are no
93
- `max_length`/`sep` options (probe_05). Hash rendering separates pairs with a
91
+ `max_length`/`sep` options . Hash rendering separates pairs with a
94
92
  space, not a comma: `Log.fingerprint({a: 1, b: 2, c: 3})` gives
95
93
  `"{:a=>1 :b=>2 :c=>3}"`.
96
94
 
@@ -130,13 +128,13 @@ bytes/process/callback/severity` accessors.
130
128
  movement. `report_msg` shows **elapsed time and rate** and, when `max` is
131
129
  set, an ETA: `· <rate> per sec. -- <dots> <pct>% <eta> => <elapsed> -
132
130
  <ticks> of <max> items · <desc>`; with no `max` the ETA part is replaced
133
- by `<ticks> items` (probe_12 shows both forms).
131
+ by `<ticks> items`; both forms occur.
134
132
  - `add_offset` / `remove_offset` / `offset` (util.rb:9-28) indent bars created
135
133
  in nested threads.
136
134
  - `file:` option: `save` writes the bar state to YAML (`:desc, :last_count,
137
135
  :last_percent, :last_time, :max, :start, :ticks`) and `done`/`error` remove
138
136
  the file, so a bar given a `file:` **resumes across runs** — the persisted
139
- YAML is reloaded into `ticks` (probe_06 round-trips 3 ticks).
137
+ YAML is reloaded into `ticks` (round-trips 3 ticks).
140
138
 
141
139
  All bar bookkeeping is guarded by `BAR_MUTEX`; per-bar `tick` state is not
142
140
  synchronized and races by design.
@@ -147,7 +145,7 @@ synchronized and races by design.
147
145
  `Hash`. In a plain `require 'scout-essentials'` process neither `TSV` nor
148
146
  `Step` exists, so the `when TSV` arm raises `NameError`, the rescue turns it
149
147
  into `nil`, and **`guess_obj_max` always returns `nil`** — i.e.
150
- `get_obj_bar(list, true).max == nil` (probe_06). Pass an explicit Numeric or a
148
+ `get_obj_bar(list, true).max == nil` . Pass an explicit Numeric or a
151
149
  `:max` hash, or define those constants in the embedding repo, to get a real
152
150
  max.
153
151
 
@@ -34,7 +34,6 @@ module MyApp
34
34
  end
35
35
  ```
36
36
 
37
-
38
37
  - `claim(path, type, content = nil, &block)` — `type` is a **mandatory
39
38
  positional** argument (`claim path { }` raises `ArgumentError`). Passing
40
39
  only the path plus a block is not a valid call.
@@ -5,9 +5,8 @@ Fetching remote files over HTTP(S)/FTP/SSH, the URL cache that backs
5
5
  Source: `lib/scout/open/remote.rb`, `lib/scout/open/sync.rb`,
6
6
  `lib/scout/resource/sync.rb`.
7
7
 
8
- Executed examples come from `tmp/rewrite_D/probe_07_remote.rb`, which
9
- starts a throwaway local `python3 -m http.server` to serve real URLs;
10
- transcripts live in `tmp/rewrite_D/BATCH_D_PROBE_TRANSCRIPTS.txt`.
8
+ Executed examples run against a throwaway local `python3 -m http.server`
9
+ serving real URLs.
11
10
 
12
11
  For the local side of `Open` see [WorkingWithFiles.md](WorkingWithFiles.md);
13
12
  for `Resource` itself see [ProducingResources.md](ProducingResources.md).
@@ -25,8 +24,7 @@ Open.ssh?('http://a') # => false
25
24
  ```
26
25
 
27
26
  Both are pure regexes (`/^(?:https?|ftp|ssh):\/\//` and `/^ssh:\/\//`). A
28
- path without a scheme is never remote. Verified by
29
- `tmp/rewrite_D/probe_07_remote.rb`.
27
+ path without a scheme is never remote.
30
28
 
31
29
  ## `Open.ssh` — reading over SSH
32
30
 
@@ -46,20 +44,19 @@ behaviour is to serve from that cache without touching the network.
46
44
 
47
45
  ```ruby
48
46
  Open.remote_cache_dir # $HOME/.scout/var/cache/open-remote
49
- Open.remote_cache_dir = dir # override (probe sets it to a tmpdir)
47
+ Open.remote_cache_dir = dir # override (e.g. a tmpdir)
50
48
  Open.cache_file(url, options) # <cache_dir>/<digest>
51
49
  ```
52
50
 
53
51
  The digest (`Open.digest_url`) covers the URL, the `--post-data` value and
54
52
  the **sorted lines** of `--post-file`. Different post-data therefore gets a
55
- different cache entry (probe: three URLs/options => three distinct cache
56
- paths).
53
+ different cache entry (three URLs/options => three distinct cache paths).
57
54
 
58
55
  ### No TTL, no expiry
59
56
 
60
57
  There is no timestamp check and no maximum age. Once a URL is cached, a
61
58
  later `Open.wget` of the same URL/options returns the cached bytes even if
62
- the server has changed. The probe demonstrates it: the file was edited on
59
+ the server has changed: the file can be edited on
63
60
  the server three times (`HELLO` → `FIRST` → `SECOND` → `THIRD`) while the
64
61
  second `Open.wget` still reported the old cached content.
65
62
 
@@ -72,7 +69,7 @@ Open.remove_from_cache(url, options) # delete the cache entry
72
69
  ```
73
70
 
74
71
  **Exactly `'update'`** re-fetches and re-writes the cache, then *reads it
75
- back* (probe: after `Open.wget(url, :nocache => 'update')` the returned file
72
+ back*: after `Open.wget(url, :nocache => 'update')` the returned file
76
73
  is the cache file again, not the raw pipe). Any other truthy `nocache`
77
74
  value takes the other branch and returns the raw wget pipe **without**
78
75
  touching the cache. `:force` only bypasses the initial `in_cache?` lookup,
@@ -90,7 +87,7 @@ wrapped in `OpenURLError` with the message
90
87
 
91
88
  `Open.download(url, file)` runs `wget '<url>' -O '<file>'` via
92
89
  `CMD.cmd_log`; on failure it removes the partial output file and re-raises
93
- the original error. Verified: downloading a live URL produced the current
90
+ the original error: downloading a live URL writes the current
94
91
  server bytes (no cache involved).
95
92
 
96
93
  `Open.scp(source_file, target_file, target:, source:)` first creates the
@@ -123,7 +120,7 @@ directory itself. A local-to-local sync of the identical path logs a warning
123
120
  and returns.
124
121
 
125
122
  `Open.sync(...)` is a literal alias: `Open.sync` and `Open.rsync` are the
126
- same implementation forwarded with Ruby's `...` (probe: same owner). Use
123
+ same implementation forwarded with Ruby's `...` (same owner). Use
127
124
  either name — they are interchangeable.
128
125
 
129
126
  ## `Resource.sync(path, map, options)`
@@ -143,7 +140,7 @@ Open.wait(lag, key = nil)
143
140
 
144
141
  A tiny rate limiter backed by the module-level `LAST_TIME` hash. If the
145
142
  last call *for that key* was less than `lag` seconds ago, it sleeps the
146
- remainder. `Open.wget` uses it via `:nice` / `:nice_key`. Probe: two
143
+ remainder. `Open.wget` uses it via `:nice` / `:nice_key`. Two
147
144
  `Open.wait(0.3, :k)` calls in a row took 0.3 s wall clock.
148
145
 
149
146
  Note the process-local `LAST_TIME` map is shared mutable state with no
@@ -26,7 +26,6 @@ CMD.cmd('echo', 'arg2', '-n' => true, '-r' => true).read
26
26
  - **Hash-only** (`CMD.cmd({'echo' => 'x'})`) is *not* a command form; the Hash is
27
27
  treated as options, `cmd`/`tool` stay nil and the call fails.
28
28
 
29
- Verified: probe `tmp/rewrite_B/probe_15_cmd_forms.rb` (string, string+cmd,
30
29
  array, array+string, symbol tool with nil cmd, ProcessFailed on hash-only).
31
30
 
32
31
  ### The `'{opt}'` placeholder
@@ -44,8 +43,6 @@ CMD.cmd("echo '{opt}' a 1", 'x' => 1).read # => "a 1\n" (note: trailing space)
44
43
 
45
44
  Unquoted `{opt}` is left untouched — the substitution matches `'\{opt\}'`
46
45
  exactly. Without a placeholder the option string is appended to the command.
47
- Verified: probe `probe_14_cmd_opts.rb` lines 10-11 and the C8 fix in
48
- `research/doc_audit/RunningCommands.md`.
49
46
 
50
47
  ## Option quoting: `process_cmd_options`
51
48
 
@@ -71,10 +68,10 @@ raises `Invalid option key` before anything runs.
71
68
  **Arrays are not expanded.** An Array value is stringified (`to_s`) and quoted
72
69
  like any other value — `"#{value}"` produces `n '["a", "b"]'` in String mode
73
70
  and `['n', '["a", "b"]']` in array mode. Pass separate calls or build the
74
- command yourself if you need repeated flags. Verified: `probe_14_cmd_opts.rb`
71
+ command yourself if you need repeated flags.
75
72
  lines 4-5 and the cmd-level Array check.
76
73
 
77
- Verified examples: `probe_14_cmd_opts.rb` (dashes, `=`, nil/false, true,
74
+ (dashes, `=`, nil/false, true,
78
75
  apostrophes, invalid key, arrays).
79
76
 
80
77
  ## stderr: severities, capture and files
@@ -83,7 +80,7 @@ apostrophes, invalid key, arrays).
83
80
  (added by `cmd.rb:181`) is **`Log::DEBUG`** — stderr lines are only logged at
84
81
  debug severity, i.e. invisible unless you raise `Log.severity` above it.
85
82
 
86
- - `:stderr => true` is normalised to `Log::HIGH` (probe `probe_23_bar_log.rb`).
83
+ - :stderr => true is normalised to `Log::HIGH`.
87
84
  - Any other Integer is a `Log` severity: the ladder is
88
85
  `DEBUG=0, LOW=1, MEDIUM=2, HIGH=3, INFO=4, WARN=5, ERROR=6, NONE=7`
89
86
  (`lib/scout/log.rb` `SEVERITY_NAMES`), so `:stderr => Log::MEDIUM` shows
@@ -118,9 +115,7 @@ CMD.cmd(..., :save_stderr => dst) # dst stays open
118
115
  Implementation: `cmd.rb:194-217` (destination setup), the writer thread /
119
116
  inline writer, and the `ensure` at `cmd.rb:600-612` that flushes and closes a
120
117
  CMD-owned file. Verified by `test/scout/test_cmd_save_stderr.rb` (13 tests,
121
- incl. live `tail -f` polling) and probes `probe_17_save_stderr.rb`
122
- (boolean/String/Path/Pathname/IO/StringIO, pipe and non-pipe, truncation) and
123
- `probe_16_exitstatus.rb` (`std_err` populated in both modes).
118
+ incl. live `tail -f` polling).
124
119
 
125
120
  ## Exit status, `no_fail` and failure
126
121
 
@@ -129,19 +124,19 @@ incl. live `tail -f` polling) and probes `probe_17_save_stderr.rb`
129
124
  `:nofail`) is given.
130
125
  - `:no_fail => true` **suppresses** `ProcessFailed`/`ConcurrentStreamProcessFailed`
131
126
  — and `exit_status` then stays `nil`, in pipe mode too. If you need the code,
132
- call `join_pids` yourself. Verified: `probe_16_exitstatus.rb`
127
+ call `join_pids` yourself.
133
128
  (`pipe read+join exit_status: nil`, `explicit join_pids exit_status: 0`,
134
129
  `non-pipe exit_status: 0`).
135
130
  - `exit_status` is only ever set by `ConcurrentStream#join_pids`
136
131
  (`concurrent_stream.rb:127`), which also empties `pids`, so it can only be
137
132
  used once. A stream that is read and joined normally has `exit_status == nil`
138
- (probe `probe_26_join_es.rb`: `read+join: es=nil` with `joined?` true; only
133
+ (`read+join: es=nil` with `joined?` true; only
139
134
  an explicit early `join_pids` yields `0`). Do not rely on `stream.exit_status`.
140
135
  - If the process never starts (bad executable, no such file) `ProcessFailed` is
141
136
  raised immediately — also suppressed by `no_fail`, which then returns `nil`.
142
137
  - A failed *producer thread* in pipe mode surfaces as
143
138
  `ConcurrentStreamProcessFailed` when the consumer closes/joins the stream
144
- (`probe_13_force_close.rb`).
139
+
145
140
 
146
141
  ## Timeout
147
142
 
@@ -182,7 +177,8 @@ returns only entries matching `/\d+\./`.
182
177
 
183
178
  | key | effect |
184
179
  |---|---|
185
- | `:in` | stdin: a String is written by a thread; an IO/StringIO is read; a ConcurrentStream is streamed (and closed unless `:dont_close_in`). Also `:in_pipe` for a pipe-backed writer. Verified: `probe_25_keeping_in.rb` (String, IO, stream, `in_pipe` returns an IO). |
180
+ | `:in` | stdin: a String is written by a thread; an IO/StringIO is read; a ConcurrentStream is streamed (and closed unless `:dont_close_in`). Also `:in_pipe` for a pipe-backed writer. (String, IO, stream, `in_pipe` returns an IO)
181
+ . |
186
182
  | `:pipe` | return a ConcurrentStream instead of the text/StringIO |
187
183
  | `:stderr` | severity for stderr logging (default `Log::DEBUG`); `true` → `Log::HIGH` |
188
184
  | `:save_stderr` | `true` / path / IO — see above |
@@ -191,7 +187,7 @@ returns only entries matching `/\d+\./`.
191
187
  | `:no_wait` | alias used to default `autojoin` (`autojoin = no_wait if autojoin.nil?`) |
192
188
  | `:timeout` | seconds; watchdog; only runtime bound |
193
189
  | `:post` | proc run after the command/stream finishes (teardown, forcing upstream closes) |
194
- | `:progress_bar` | a `Log::ProgressBar`; stderr lines tick it (`probe_24_bar.rb`: 2 ticks in both pipe and non-pipe mode) |
190
+ | `:progress_bar` | a `Log::ProgressBar`; stderr lines tick it (2 ticks for a 2-line stderr, pipe and non-pipe) |
195
191
  | `:log` | defaults to `true` (`log = true if log.nil?`, cmd.rb:224): pipe-mode stderr lines are `Log.log`ged at the chosen `:stderr` severity. `:log => false` silences that logging. Not the same as `CMD.cmd_log` (a separate helper that forces `:pipe`/`:log`). |
196
192
  | `:sudo`, `:xvfb` | prefix the command |
197
193
  | `:dont_close_in` | keep the `:in` stream open |
@@ -201,13 +197,12 @@ returns only entries matching `/\d+\./`.
201
197
  `CMD.cmd` in this repo — `cmd.rb` deletes none of them, so they would be
202
198
  forwarded to `process_cmd_options` and end up as command-line text. `:canfail`
203
199
  exists on `Persist` (`persist.rb:133`) and on `Resource` claims, not here.
204
- Verified by grepping `lib/` for the four names (`probe_25_keeping_in.rb` tail).
200
+ Verified by grepping `lib/` for the four names.
205
201
 
206
202
  ## The block is ignored
207
203
 
208
204
  `CMD.cmd(...) { ... }` accepts a block but **never calls it** — the only block
209
- invocation inside `cmd.rb` is in `CMD.tool`. Verified live by
210
- `tmp/docaudit/probe_hs_c15_block.rb`: in both pipe and non-pipe mode the block
205
+ invocation inside `cmd.rb` is in `CMD.tool`. In both pipe and non-pipe mode the block
211
206
  body never runs, while `:post` and `stream.add_callback` do. Use
212
207
  `:post => proc{}` or stream callbacks for post-join work.
213
208
 
@@ -48,12 +48,20 @@ Open.read('file.gz.bak')# raw gzip bytes — NOT detected
48
48
  Open.gzip?('file.tgz') # => false
49
49
  ```
50
50
 
51
- Force decompression explicitly when the name does not cooperate:
51
+ To decompress content that does not carry a recognised name, run
52
+ `Open.gunzip` yourself — it is `zcat` behind a `CMD.cmd` pipe:
52
53
 
53
54
  ```ruby
54
- Open.open('file.tgz', :gzip => true).read
55
+ Open.read('file.tgz', :gzip => false, :noz => true) # raw bytes, if that is what you want
56
+ stream = Open.open('file.tgz', :noz => true) # raw gzip bytes as a stream
57
+ Open.read(Open.gunzip(stream)) # decompressed
55
58
  ```
56
59
 
60
+ Note that the historical `:gzip => true` option is **broken as a
61
+ decompression force**: it is forwarded to `CMD.cmd('zcat', …)` as a
62
+ command-line flag, the command fails, and — because `:no_fail` is set —
63
+ you get an empty string instead of an error.
64
+
57
65
  ## Grepping lines
58
66
 
59
67
  `:grep` and `:invert_grep` select lines using the system `grep`. **With a
data/lib/scout/config.rb CHANGED
@@ -129,6 +129,7 @@ module Scout::Config
129
129
  end
130
130
  end
131
131
 
132
+
132
133
  value = priorities.empty? ? default : priorities.collect{|p| p }.sort_by{|p,v| p}.first.last.first
133
134
  value = false if value == 'false'
134
135
 
@@ -79,10 +79,16 @@ module IndiferentHash
79
79
  case key
80
80
  when Symbol, Module
81
81
  v = super(key)
82
- v.nil? ? super(key.to_s) : v
82
+ vn = super(key.to_s)
83
+ v.nil? ? vn : v
83
84
  when String
84
85
  v = super(key)
85
- v.nil? ? super(key.to_sym) : v
86
+ vn = begin
87
+ super(key.to_sym)
88
+ rescue
89
+ nil
90
+ end
91
+ v.nil? ? vn : v
86
92
  else
87
93
  super(key)
88
94
  end
@@ -1,7 +1,7 @@
1
1
  require 'digest/md5'
2
2
  module Log
3
3
  FP_MAX_STRING = 150
4
- FP_MAX_ARRAY = 20
4
+ FP_MAX_ARRAY = 25
5
5
  FP_MAX_HASH = 10
6
6
 
7
7
  def self.truncate_string(string, max=FP_MAX_STRING)
@@ -142,6 +142,10 @@ module NamedArray
142
142
  new
143
143
  end
144
144
  end
145
+
146
+ def zip_fields
147
+ NamedArray.zip_fields(self).collect{|v| self.annotate(v) }
148
+ end
145
149
 
146
150
  def self.add_zipped(source, new)
147
151
  source.zip(new).each do |s,n|
@@ -147,7 +147,7 @@ module Open
147
147
  info_file = file + '.info'
148
148
  if File.exist?(info_file) && defined?(Step)
149
149
  done = Persist.memory([info_file, File.mtime(info_file)], persist: true ) do
150
- Persist.load(info_file, Step::SERIALIZER)[:done]
150
+ Step.load_info(info_file)[:done]
151
151
  end
152
152
  return done if done
153
153
  end
@@ -16,14 +16,14 @@ module Open
16
16
  end
17
17
  end
18
18
 
19
- def self.consume_stream(io, in_thread = false, into = nil, into_close = true, &block)
19
+ def self.consume_stream(io, in_thread = false, into = nil, into_close = true, clean = true, &block)
20
20
  return if Path === io
21
21
  return unless io.respond_to? :read
22
22
 
23
- if io.respond_to? :closed? and io.closed?
24
- io.join if io.respond_to? :join
25
- return
26
- end
23
+ #if io.respond_to? :closed? and io.closed?
24
+ # io.join if io.respond_to? :join
25
+ # return
26
+ #end
27
27
 
28
28
  if in_thread
29
29
  consumer_thread = Thread.new(Thread.current) do |parent|
@@ -54,10 +54,13 @@ module Open
54
54
 
55
55
  into_close = false unless into.respond_to? :close
56
56
 
57
- while c = io.read(BLOCK_SIZE)
58
- into << c if into
59
- last_c = c if c
60
- break if io.closed?
57
+ begin
58
+ while c = io.readpartial(BLOCK_SIZE)
59
+ into << c if into
60
+ last_c = c if c
61
+ break if io.closed?
62
+ end
63
+ rescue EOFError, IOError
61
64
  end
62
65
 
63
66
  io.join if io.respond_to? :join
@@ -72,7 +75,7 @@ module Open
72
75
  Log.low "Consume stream Aborted #{Log.fingerprint io} into #{into_path || into}"
73
76
  io.abort $! if io.respond_to? :abort
74
77
  into.close if into.respond_to?(:closed?) && ! into.closed?
75
- FileUtils.rm into_path if into_path and File.exist?(into_path)
78
+ FileUtils.rm into_path if into_path and clean and File.exist?(into_path)
76
79
  rescue Exception
77
80
  Thread.current["exception"] = true
78
81
  Log.low "Consume stream Exception reading #{Log.fingerprint io} into #{into_path || into} - #{$!.message}"
@@ -80,9 +83,7 @@ module Open
80
83
  io.abort exception if io.respond_to? :abort
81
84
  into.close if into.respond_to?(:closed?) && ! into.closed?
82
85
  into_path = into if into_path.nil? && String === into
83
- if into_path and File.exist?(into_path)
84
- FileUtils.rm into_path
85
- end
86
+ FileUtils.rm into_path if into_path and clean and File.exist?(into_path)
86
87
  raise exception
87
88
  end
88
89
  end
@@ -42,7 +42,7 @@ module Path
42
42
 
43
43
  post_fix = "--#{filename.length}--#{Misc.digest(filename)[0..4]}" + extension
44
44
 
45
- filename = filename[0..(length - post_fix.length - 1)] << post_fix
45
+ filename = Annotation.purge(filename.to_s)[0..(length - post_fix.length - 1)] << post_fix
46
46
  else
47
47
  filename
48
48
  end