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
|
@@ -0,0 +1,1925 @@
|
|
|
1
|
+
# Behavior probes — doc audit (CORE chunk)
|
|
2
|
+
|
|
3
|
+
Append-only log of `ruby -Ilib` probes run from
|
|
4
|
+
`/bulk/mvazque2/git/scout-essentials`. Script sources kept under `tmp/`
|
|
5
|
+
(`tmp/probe*.rb`) unless the command is fully inline.
|
|
6
|
+
|
|
7
|
+
Interpretations are noted inline. Any probe whose result was a probe artifact
|
|
8
|
+
rather than library behavior is marked as such.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## P1 — IndiferentHash basics
|
|
13
|
+
|
|
14
|
+
Command: `ruby -Ilib tmp/probe13.rb`
|
|
15
|
+
```
|
|
16
|
+
require "scout-essentials"
|
|
17
|
+
h = IndiferentHash.setup({"a" => 1, "b" => 2})
|
|
18
|
+
p h[:a]; p h["a"]; p h.include?(:a)
|
|
19
|
+
h[:a] = "DEF"
|
|
20
|
+
p h["a"]; p h[:a]; p h.keys
|
|
21
|
+
p h.slice(:a).keys
|
|
22
|
+
p h.dig("a")
|
|
23
|
+
p h.merge("c" => 3).keys
|
|
24
|
+
p h.clean_version.keys
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Observed (stdout):
|
|
28
|
+
```
|
|
29
|
+
1
|
|
30
|
+
1
|
|
31
|
+
true
|
|
32
|
+
"DEF"
|
|
33
|
+
"DEF"
|
|
34
|
+
["b", :a]
|
|
35
|
+
[:a]
|
|
36
|
+
"DEF"
|
|
37
|
+
["b", :a, "c"]
|
|
38
|
+
["b", "a"]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Interpretation:
|
|
42
|
+
- Symbol/String are interchangeable for reads (`h[:a]` and `h["a"]` both
|
|
43
|
+
return the stored value) and for `include?`.
|
|
44
|
+
- `[]=` **deletes the dual-form key first** (indiferent_hash.rb:35): after
|
|
45
|
+
`h[:a] = "DEF"` on a hash that held `"a"`, the hash contains a single `:a`
|
|
46
|
+
key — `keys == ["b", :a]` — i.e. the Symbol form wins as the *stored* key.
|
|
47
|
+
- `slice(:a)` accepts a Symbol and returns an IndiferentHash whose keys keep
|
|
48
|
+
the original stored forms (`[:a]`).
|
|
49
|
+
- `merge` returns a new IndiferentHash; the added key `"c"` is stored as a
|
|
50
|
+
String alongside the pre-existing `[:b, :a]`.
|
|
51
|
+
- `clean_version` converts to String keys (`["b", "a"]`), keeping the first
|
|
52
|
+
value encountered per key (see P18).
|
|
53
|
+
- Hygiene note: an early inline version of this probe reported garbage for the
|
|
54
|
+
`keys` line (pipe/truncation artifact when run through the exec task); the
|
|
55
|
+
file-based run above, redirected to a file and cat’d, is authoritative.
|
|
56
|
+
|
|
57
|
+
## P2 — IndiferentHash parsers and process_options
|
|
58
|
+
|
|
59
|
+
Command: `ruby -Ilib tmp/probe1.rb` (`require "scout-essentials"` + `p` calls)
|
|
60
|
+
|
|
61
|
+
Observed:
|
|
62
|
+
```
|
|
63
|
+
{"f"=>true, "g"=>"false", "h"=>1, "i"=>1.5, "j"=>:sym, "k"=>/re/}
|
|
64
|
+
{"a"=>"quoted str"}
|
|
65
|
+
{"f"=>"false", "j"=>1, "k"=>1.5, "m"=>:sym, "n"=>/re/, "o"=>"q s"}
|
|
66
|
+
```
|
|
67
|
+
(first line = `parse_options`, second = `string2hash` of the quoted-only case,
|
|
68
|
+
third = `parse_options` again)
|
|
69
|
+
|
|
70
|
+
Second run (`tmp/probe2.rb` + `process_options`):
|
|
71
|
+
```
|
|
72
|
+
{"a"=>1,"b"=>2,"c"=>:sym,"d"=>/re/,"e"=>"quoted str","f"=>true,"g"=>"false"}
|
|
73
|
+
{"flag"=>true}
|
|
74
|
+
{"a"=>1,"b"=>2,"c"=>3}
|
|
75
|
+
[1, 2]
|
|
76
|
+
[1, nil]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Interpretation:
|
|
80
|
+
- `string2hash` (`#` separator) coerces `true`/`false` to booleans and
|
|
81
|
+
integers/floats/symbols/regexps; the last output line confirms `1 2` and
|
|
82
|
+
`1 nil` extraction from `process_options` with missing keys.
|
|
83
|
+
- **Asymmetry**: `parse_options` returns the String `"false"` for `f=false`
|
|
84
|
+
(the comma-split branch at options.rb:130-140 runs before the boolean
|
|
85
|
+
branches at :148-149 are reached for the empty value case; in fact the
|
|
86
|
+
value `"false"` never matches `value.empty?` and falls to the final else),
|
|
87
|
+
while `string2hash` returns the boolean. Docs claiming both are equivalent
|
|
88
|
+
are wrong.
|
|
89
|
+
- `process_options` is destructive — the source hash loses the keys.
|
|
90
|
+
- `print_options` inverse: `{:a=>1, "b"=>"x y", list: [1,2]}` →
|
|
91
|
+
`a=1 b="x y" list=1,2`.
|
|
92
|
+
|
|
93
|
+
## P3 — Scout::Config
|
|
94
|
+
|
|
95
|
+
`tmp/probe3.rb` (first version, with `get(:k2)` Symbol key):
|
|
96
|
+
```
|
|
97
|
+
"v1"
|
|
98
|
+
TypeError: no implicit conversion of Symbol into String (config.rb:106)
|
|
99
|
+
```
|
|
100
|
+
Interpretation: `get` requires a **String** key when no tokens are given
|
|
101
|
+
(`tokens = ["key:" + key]`, config.rb:106).
|
|
102
|
+
|
|
103
|
+
`tmp/probe3.rb` (second version, all String keys):
|
|
104
|
+
```
|
|
105
|
+
"v1" # get("k1") after set("k1","v1")
|
|
106
|
+
"v2" # get("k2") where k2 set via file:/x.rb
|
|
107
|
+
"v3" # get("k3") where k3 set via workflow::0 (highest precedence)
|
|
108
|
+
"v3" # get("k3", "workflow::1") — explicit token, same entry wins
|
|
109
|
+
"v3" # get("k3") again
|
|
110
|
+
"INDIRECT" # value stored as env:VAR1 → resolved from ENV[VAR1]
|
|
111
|
+
nil # env:VAR1,VAR2 with neither set
|
|
112
|
+
false # literal 'false' string → false
|
|
113
|
+
nil # missing key
|
|
114
|
+
"high" # get("k7", "key:k7") on an entry registered only with key:k7
|
|
115
|
+
"tokval" # get("k8") and get("k8","workflow") for an entry set with
|
|
116
|
+
"tokval" # workflow::1 token
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Interpretation:
|
|
120
|
+
- Priority numbers: **lower wins**; `workflow::0` beat `file:` and `line:`.
|
|
121
|
+
- `key:` token = priority 20 = lowest precedence (a bare `key value` line only
|
|
122
|
+
wins when no tokened entry matches).
|
|
123
|
+
- `'false'` → `false`; `'nil'` → `nil`; `env:A,B` → first set var else nil.
|
|
124
|
+
|
|
125
|
+
## P4 — TmpFile
|
|
126
|
+
|
|
127
|
+
`ruby -Ilib -e` with `p` calls (see inventory §6 for the source):
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
"/home/mvazque2/tmp/scout/tmpfiles"
|
|
131
|
+
"/home/mvazque2/tmp/scout"
|
|
132
|
+
"/home/mvazque2/tmp/scout/foo"
|
|
133
|
+
"/home/mvazque2/tmp/scout/tmpfiles/pfx-332333330"
|
|
134
|
+
"tmp-57604039"
|
|
135
|
+
"/home/mvazque2/tmp/scout/tmpfiles/·a·b·c.tsv:62dff81e14e2b583f69a94b08997ef10"
|
|
136
|
+
"/home/mvazque2/tmp/scout/tmpfiles/·a·b·c.tsv"
|
|
137
|
+
"/home/mvazque2/tmp/scout/tmpfiles/P:·a·b·c.tsv[K]"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Interpretation:
|
|
141
|
+
- Temp root is `$HOME/tmp/scout/tmpfiles` — **not** `/tmp`.
|
|
142
|
+
- `/` → `·` (U+00B7 middle dot), `SLASH_REPLACE` (tmpfile.rb:98).
|
|
143
|
+
- `tmp_for_file` with no other options → no `:digest` suffix; with surviving
|
|
144
|
+
options → `:` + 32-char MD5.
|
|
145
|
+
- `:key` → `[K]`; `:prefix` → `P:`; `.gz`/`.bgz` stripped from the base name.
|
|
146
|
+
- `:unnamed` is excluded from the digest computation.
|
|
147
|
+
|
|
148
|
+
## P5 — What a bare `require "scout-essentials"` defines
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
["CMD", true]
|
|
152
|
+
["NamedArray", false]
|
|
153
|
+
["Hook", false]
|
|
154
|
+
["Misc", true]
|
|
155
|
+
["TmpFile", true]
|
|
156
|
+
["Log", true]
|
|
157
|
+
["SOPT", true]
|
|
158
|
+
["IndiferentHash", true]
|
|
159
|
+
["Scout::Config", true]
|
|
160
|
+
```
|
|
161
|
+
(from `tmp/probe10.rb`, section A; an earlier inline probe confirmed
|
|
162
|
+
`NamedArray` raises NameError under the bare require — `tmp/probe5.rb`).
|
|
163
|
+
|
|
164
|
+
Interpretation:
|
|
165
|
+
- `CMD` **is** reachable (it is required transitively — via
|
|
166
|
+
`tmpfile→open→…`), `NamedArray` and `Hook` are **not**.
|
|
167
|
+
- Docs must tell users to `require 'scout/named_array'` and
|
|
168
|
+
`require 'scout/misc/hook'` explicitly.
|
|
169
|
+
|
|
170
|
+
## P6 — Misc.format helpers
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
snake_case("SomeCamelCase") => "some_camel_case"
|
|
174
|
+
camel_case("some_snake_case") => "SomeSnakeCase"
|
|
175
|
+
camel_case("ABCdef") => "ABCdef"
|
|
176
|
+
camel_case_lower("some_snake_case") => "someSnakeCase"
|
|
177
|
+
humanize("some_field_name") => "Some field name"
|
|
178
|
+
humanize("some_field_name", format: :class) => "SomeFieldName"
|
|
179
|
+
humanize("ABC_acronym_x") => "ABC acronym x"
|
|
180
|
+
human_number(0) => "0"
|
|
181
|
+
human_number(950) => "950"
|
|
182
|
+
human_number(1234) => "1.2K"
|
|
183
|
+
human_number(-2500000) => "-2.5M"
|
|
184
|
+
parse_sql_values("('a','b,c'),(1,2)") => [["a","b,c"],["1","2"]]
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## P7 — Misc.timespan
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
timespan("10") => 10
|
|
191
|
+
timespan("10s") => 10
|
|
192
|
+
timespan("01:02:03") => 3723
|
|
193
|
+
timespan("1d") => 86400
|
|
194
|
+
timespan("1h30m") => TypeError: nil can't be coerced into Integer
|
|
195
|
+
timespan("1w") => 604800
|
|
196
|
+
timespan("mo") variants => 2678400 / "1y" => 31536000
|
|
197
|
+
timespan("1'30''") => 31
|
|
198
|
+
timespan("-10s") => -10
|
|
199
|
+
timespan("1x") => TypeError
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Interpretation: `timespan` matches **one** `(\d+)(\w*)` pair per digit-run, so
|
|
203
|
+
compound strings like `"1h30m"` scan as `[["1","h30m"]]` — the unit becomes
|
|
204
|
+
the literal `"h30m"`, which is missing from the token table, and
|
|
205
|
+
`amount.to_i * nil` raises. **Compound timespans are broken by design**; only
|
|
206
|
+
single-unit strings work. Unknown units likewise raise rather than being
|
|
207
|
+
ignored.
|
|
208
|
+
|
|
209
|
+
## P8 — Log.fingerprint
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
fingerprint("a"*250) => "'aaa...aaa'" containing the truncation marker
|
|
213
|
+
fingerprint([1,2]) => "[1, 2]"
|
|
214
|
+
fingerprint({:a=>1}) => "{:a=>1}"
|
|
215
|
+
fingerprint(3.5) => "3.500"
|
|
216
|
+
fingerprint(100.0) => "100.0"
|
|
217
|
+
fingerprint(2.5) => "2.500"
|
|
218
|
+
fingerprint(0.000123)=> "0.000123"
|
|
219
|
+
fingerprint(nil) => "nil"
|
|
220
|
+
fingerprint(true) => "true"
|
|
221
|
+
fingerprint(:sym) => ":sym"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## P9 — Exception hierarchy
|
|
225
|
+
|
|
226
|
+
See the table in `research/implementation-inventory-core.md` §7 (19 constants,
|
|
227
|
+
all verified `ancestors.include?(expected)`). Highlights:
|
|
228
|
+
```
|
|
229
|
+
ProcessFailed.new(123,"msg").message => "Process 123 failed - msg"
|
|
230
|
+
ProcessFailed.new(nil,"cmd").message => "Failed to run cmd"
|
|
231
|
+
KeepBar.new("payload").payload => "payload"
|
|
232
|
+
StopInsist.new(ArgumentError.new("x")).exception => ArgumentError
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## P10 — SOPT
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
parse("-f--first* first arg:-f--fun") =>
|
|
239
|
+
inputs => ["first", "fun"]
|
|
240
|
+
shortcuts => {"f"=>"first", "fu"=>"fun"}
|
|
241
|
+
input_types => {"first"=>:string, "fun"=>:boolean}
|
|
242
|
+
descriptions => {"first"=>"first arg", "fun"=>""}
|
|
243
|
+
consume(["-f","myfile","--fun"]) => {:first=>"myfile", :fun=>true}
|
|
244
|
+
fix_shortcut("f","fun") => "fu"
|
|
245
|
+
consume stops at "--" and leaves ["--","positional"] in the args
|
|
246
|
+
boolean false forms: "--flag=false" / "--flag=F" / "--flag=no" => false
|
|
247
|
+
"--flag false" => false but logs a WARN and eats "false"
|
|
248
|
+
"--flag" => true
|
|
249
|
+
```
|
|
250
|
+
(from `tmp/probe4.rb`, `tmp/probe7.rb`, `tmp/probe11.rb`)
|
|
251
|
+
|
|
252
|
+
## P11 — Log defaults
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
Log.severity => 4
|
|
256
|
+
SEVERITY_NAMES[4] => "INFO"
|
|
257
|
+
~/.scout/etc/log_severity exists => false
|
|
258
|
+
SCOUT_LOG => nil
|
|
259
|
+
Log.tty_size => 180
|
|
260
|
+
Log.nocolor => false
|
|
261
|
+
```
|
|
262
|
+
Interpretation: severity INFO is the fallback when neither `SCOUT_LOG` nor the
|
|
263
|
+
home file is present; at INFO, DEBUG/LOW/MEDIUM/HIGH messages are suppressed
|
|
264
|
+
(`severity <= level` false for 0..3).
|
|
265
|
+
|
|
266
|
+
## P12 — TmpFile.with_file cleanup on exception
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
kept file path => "/home/mvazque2/tmp/scout/tmpfiles/tmp-539488392"
|
|
270
|
+
File.exist?(kept) => true
|
|
271
|
+
```
|
|
272
|
+
Interpretation: `with_file` does **not** remove the temp file when the block
|
|
273
|
+
raises (no `ensure` around the yield, tmpfile.rb:70-74). Docs claiming
|
|
274
|
+
guaranteed cleanup are wrong.
|
|
275
|
+
|
|
276
|
+
## P13 — ProgressBar.percent with max == 0
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
Log::ProgressBar.new(0); bar.tick; bar.tick; bar.percent => 100
|
|
280
|
+
```
|
|
281
|
+
(progress.rb:47 returns 100 when `@max == 0`.)
|
|
282
|
+
|
|
283
|
+
## P14 — CMD
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
CMD.process_cmd_options({"o1"=>"v1",:o2=>true,:o3=>false,"o4="=>"v"})
|
|
287
|
+
=> "o1 'v1' o2 o4='v'"
|
|
288
|
+
CMD.process_cmd_options({add_option_dashes: true, :a=>"1", :long_opt=>"x"})
|
|
289
|
+
=> "--a '1' --long_opt 'x'"
|
|
290
|
+
CMD.process_cmd_options({"with'quote" => "it's"})
|
|
291
|
+
=> RuntimeError "Invalid option key: with'quote"
|
|
292
|
+
CMD.tool("echo", nil, nil, "echo"); CMD.get_tool("echo") => "echo"
|
|
293
|
+
CMD.cmd(["echo","a","b"]).read => "a b\n" # argv mode, no shell
|
|
294
|
+
CMD.cmd("bash -c 'echo E1 >&2; echo O1'", save_stderr: "/tmp/...").read => "O1\n"
|
|
295
|
+
File.read("/tmp/...") => "E1\n" # path form truncates and owns the file
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## P15 — NamedArray
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
NamedArray.setup([1,2], [:a,:b]).a => 1 ; .b => 2
|
|
302
|
+
.to_hash => {:a=>1,:b=>2}
|
|
303
|
+
identify_name([:a,:b], "b") => 1
|
|
304
|
+
identify_name([:a,:b], "zzz") => nil
|
|
305
|
+
positions("b") => 1 ; positions([:a,:b]) => [0,1]
|
|
306
|
+
field_match("Associated Gene Name(s)", "Unrelated") => nil
|
|
307
|
+
_zip_fields([[1,2,3],["a"],["b","c","d"]]) => [[1,"a","b"],[2,nil,"c"],[3,nil,"d"]]
|
|
308
|
+
```
|
|
309
|
+
Note: `_zip_fields` does **not** repeat singletons here (the `1 & max`
|
|
310
|
+
expression at named_array.rb:119 does not do what the code suggests); the
|
|
311
|
+
singleton column stays singleton and produces `nil` padding.
|
|
312
|
+
|
|
313
|
+
Follow-up probe (P15b, re-checking the `1 & max` claim):
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
$ cat > /tmp/na_check.rb <<'EOF'
|
|
317
|
+
require "scout-essentials"
|
|
318
|
+
require "scout/named_array"
|
|
319
|
+
p NamedArray._zip_fields([[1,2,3],["a"],["b","c","d"]])
|
|
320
|
+
p NamedArray._zip_fields([[1,2,3],["a"],["b","c","d"]], 3)
|
|
321
|
+
p NamedArray.zip_fields([[1,2,3],["a"],["b","c","d"]])
|
|
322
|
+
EOF
|
|
323
|
+
$ ruby -Ilib /tmp/na_check.rb
|
|
324
|
+
[[1, "a", "b"], [2, nil, "c"], [3, nil, "d"]]
|
|
325
|
+
[[1, "a", "b"], [2, nil, "c"], [3, nil, "d"]]
|
|
326
|
+
[[1, "a", "b"], [2, nil, "c"], [3, nil, "d"]]
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Interpretation (settled with precedence probes):
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
$ ruby -e 'max=3; v=["a"]; p(v.length == 1 & max > 1)'
|
|
333
|
+
false # no exception
|
|
334
|
+
$ ruby -e 'p(1 & true)'
|
|
335
|
+
TypeError: true can't be coerced into Integer
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
So the expression parses as `v.length == ((1 & max) > 1)` (bitwise `&`
|
|
339
|
+
binds tighter than `>` and `==`). Since `1 & max` is always 0 or 1,
|
|
340
|
+
`(1 & max) > 1` is **always false**, therefore
|
|
341
|
+
`v.length == ((1 & max) > 1)` is always false, and the
|
|
342
|
+
`v * max` singleton-repeat branch on that line is **dead code** for every
|
|
343
|
+
possible `max`. This matches the observed outputs: `["a"]` is never
|
|
344
|
+
repeated, the column stays singleton and `zip` pads with `nil`.
|
|
345
|
+
By contrast the FIRST column uses `first.length == 1 and max > 1`
|
|
346
|
+
(named_array.rb:126, plain `and` + numeric comparison) which *does* repeat
|
|
347
|
+
singletons. So only the first column can be broadcast, never the rest —
|
|
348
|
+
asymmetric and surprising behavior worth flagging in docs.
|
|
349
|
+
|
|
350
|
+
## P16 — Config: bare key vs file: token
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
set("kk","bare") + set({"kk"=>"withfile"}, "file:/x.rb") → get("kk") => "withfile"
|
|
354
|
+
set("kk2","bare") → get("kk2") => "bare"
|
|
355
|
+
```
|
|
356
|
+
Confirms `key:` (prio 20) loses to `file:` (prio 2).
|
|
357
|
+
|
|
358
|
+
## P17 — Hook is not loaded by misc.rb
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
defined?(Hook) after require "scout-essentials" => nil
|
|
362
|
+
defined?(Hook) after require "scout/misc/hook" => "constant"
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## P18 — clean_version first-wins (order-dependent)
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
IndiferentHash.setup({:a=>1,"a"=>2}).clean_version => {"a"=>1}
|
|
369
|
+
IndiferentHash.setup({"a"=>2,:a=>1}).clean_version => {"a"=>2}
|
|
370
|
+
```
|
|
371
|
+
The surviving **value** follows insertion order (first key form encountered),
|
|
372
|
+
not a String-over-Symbol rule.
|
|
373
|
+
|
|
374
|
+
## P19 — tmp_for_file digest gating
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
tmp_for_file("/a/b/c.tsv", {}, {}) => .../·a·b·c.tsv
|
|
378
|
+
tmp_for_file("/a/b/c.tsv", {}, {unnamed: 1}) => .../·a·b·c.tsv (no suffix)
|
|
379
|
+
tmp_for_file("/a/b/c.tsv", {}, {other: 1}) => .../·a·b·c.tsv:<32 hex>
|
|
380
|
+
tmp_for_file("/a/b/c.tsv", {}, {filters: {"f"=>"v"}}) => suffix length 32
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
### Notes on probe hygiene
|
|
386
|
+
- One early attempt to define a hash with `CMD::Timeout` as a Symbol-style key
|
|
387
|
+
(`CMD::Timeout: ProcessFailed`) was a Ruby syntax error (Symbol keys cannot
|
|
388
|
+
contain `::`); rewritten as strings.
|
|
389
|
+
- The bwrap sandbox rejects some long inline `bash -c` commands (exit -1 with
|
|
390
|
+
the full bwrap invocation echoed). Writing scripts to `tmp/probeN.rb` and
|
|
391
|
+
running `ruby -Ilib tmp/probeN.rb` avoids this reliably.
|
|
392
|
+
- `ruby -Ilib tmp/probe11.rb | sed` piping sometimes triggered the same
|
|
393
|
+
sandbox failure; plain invocation with output filtering via `grep -v`
|
|
394
|
+
worked.## P20 — `string2hash` / `parse_options` never produce `false` (real bug)
|
|
395
|
+
|
|
396
|
+
Script: `tmp/probe14.rb`:
|
|
397
|
+
```ruby
|
|
398
|
+
require "scout-essentials"
|
|
399
|
+
h = IndiferentHash.string2hash("f=true#g=false")
|
|
400
|
+
p [h["f"], h["f"].class, h["g"], h["g"].class]
|
|
401
|
+
h2 = IndiferentHash.parse_options("f=true g=false")
|
|
402
|
+
p [h2["f"], h2["f"].class, h2["g"], h2["g"].class]
|
|
403
|
+
|
|
404
|
+
def emulate(str)
|
|
405
|
+
out = {}
|
|
406
|
+
str.split("#").each do |s|
|
|
407
|
+
k, _, v = s.partition("=")
|
|
408
|
+
out[k] = false and next if v == "false"
|
|
409
|
+
out[k] = v
|
|
410
|
+
end
|
|
411
|
+
out
|
|
412
|
+
end
|
|
413
|
+
p emulate("g=false")
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Observed (stdout only; stderr has the scout log prefix):
|
|
417
|
+
```
|
|
418
|
+
== false/true coercion in string2hash and parse_options
|
|
419
|
+
[true, TrueClass, "false", String]
|
|
420
|
+
[true, TrueClass, "false", String]
|
|
421
|
+
{"g"=>"false"}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Interpretation — a genuine precedence bug, identical in both parsers:
|
|
425
|
+
`options[key] = false and next if value == "false"`
|
|
426
|
+
parses as `(options[key] = false) and (next)` guarded by the `if`. The
|
|
427
|
+
assignment happens and **returns `false`**; `false and next` is falsy, so
|
|
428
|
+
`next` never runs, execution falls through to the final
|
|
429
|
+
`options[key] = value`, overwriting with the **String** `"false"`.
|
|
430
|
+
|
|
431
|
+
- `string2hash("f=true#g=false")` → `{"f"=>true, "g"=>"false"}`
|
|
432
|
+
- `parse_options("f=true g=false")` → `{"f"=>true, "g"=>"false"}`
|
|
433
|
+
- `true` works only by luck: the assignment returns `true`, so
|
|
434
|
+
`true and next` does short-circuit.
|
|
435
|
+
|
|
436
|
+
Note the gem copies under `~/.rvm/gems/.../scout-essentials-1.8.8` are
|
|
437
|
+
byte-identical to the repo (verified with `diff`), so this is not a
|
|
438
|
+
load-path artifact; `$LOADED_FEATURES` confirmed
|
|
439
|
+
`lib/scout/indiferent_hash/options.rb` from the repo is the file in use.
|
|
440
|
+
|
|
441
|
+
Contrast with `Scout::Config.get`, which does coerce the *String* `'false'`
|
|
442
|
+
to `false` (config.rb:133) — so the two "boolean from string" mechanisms in
|
|
443
|
+
the same gem behave differently.
|
|
444
|
+
|
|
445
|
+
(Also observed: `ruby -Ilib tmp/probe14.rb | grep -v ...` swallowed lines —
|
|
446
|
+
re-run with stdout redirected to a file to get the complete output.)
|
|
447
|
+
|
|
448
|
+
## P21 — Consolidated probe suite status (final check)
|
|
449
|
+
|
|
450
|
+
All probe scripts referenced in this document live under `tmp/probe*.rb` and
|
|
451
|
+
were re-executed at the end of the audit chunk to confirm they still match the
|
|
452
|
+
source tree:
|
|
453
|
+
|
|
454
|
+
```
|
|
455
|
+
for f in tmp/probe{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}.rb; do ruby -Ilib "$f"; done
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Results: all exit 0 except `tmp/probe10.rb`, which **intentionally fails** at
|
|
459
|
+
its last line — section E tries `NamedArray._zip_fields` after first proving
|
|
460
|
+
(section A) that `NamedArray` is *not* loaded by `require "scout-essentials"`.
|
|
461
|
+
The NameError at the end is the demonstrated behavior, not a probe defect.
|
|
462
|
+
|
|
463
|
+
P1 (basic `IndiferentHash` symbol/string interchange) and its supplement were
|
|
464
|
+
originally split across `tmp/probe1.rb` and a since-deleted `tmp/probe13.rb`;
|
|
465
|
+
both were recreated as consolidated scripts and re-run successfully (exit 0),
|
|
466
|
+
producing the outputs recorded in P1.
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
# Chunk 2 — Open / streaming / concurrency / locking probes (P22–P33)
|
|
472
|
+
|
|
473
|
+
Scripts under `tmp/`, all run as `ruby -Ilib tmp/probeNN.rb` from the repo root.
|
|
474
|
+
Outputs below were captured with stdout redirected to a file (see P21 note about
|
|
475
|
+
pipes swallowing lines); log lines on stderr were discarded unless they were the
|
|
476
|
+
subject of the probe.
|
|
477
|
+
|
|
478
|
+
## P22 — Lock file format, Open.lock lifecycle, KeepLocked semantics
|
|
479
|
+
|
|
480
|
+
Command: `ruby -Ilib tmp/probe22.rb`
|
|
481
|
+
```
|
|
482
|
+
$LOAD_PATH.unshift 'lib'
|
|
483
|
+
require 'scout-essentials'
|
|
484
|
+
require 'tmpdir'
|
|
485
|
+
|
|
486
|
+
d = Dir.mktmpdir("probe22")
|
|
487
|
+
target = File.join(d, "data.txt")
|
|
488
|
+
|
|
489
|
+
Open.lock(target) do |lockfile|
|
|
490
|
+
puts "block result: inside:#{lockfile.class}:#{lockfile.path}"
|
|
491
|
+
puts "lock content while held:"
|
|
492
|
+
puts File.read(lockfile.path).inspect if File.exist?(lockfile.path)
|
|
493
|
+
end
|
|
494
|
+
puts "lock file removed after block: #{!File.exist?(target + '.lock')}"
|
|
495
|
+
puts "leftover dir contents: #{Dir.glob(File.join(d,'*')).sort.inspect}"
|
|
496
|
+
|
|
497
|
+
# KeepLocked: block may keep the lock held and still return a value
|
|
498
|
+
lf2 = Open.lock(File.join(d,"keep.txt")) do |lock|
|
|
499
|
+
raise KeepLocked, "value-kept"
|
|
500
|
+
end
|
|
501
|
+
puts "KeepLocked result: #{lf2.inspect} lock file kept: #{File.exist?(File.join(d,'keep.txt.lock'))}"
|
|
502
|
+
|
|
503
|
+
# explicit Lockfile reuse across Open.lock calls
|
|
504
|
+
lf = Lockfile.new(File.join(d,"reuse.lock"))
|
|
505
|
+
res = Open.lock(File.join(d,"reuse.txt"), :lock => lf) { "A" }
|
|
506
|
+
puts "explicit lockfile reuse result: #{res.inspect}, lockfile.locked?: #{lf.locked?}, file exists: #{File.exist?(lf.path)}"
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Observed:
|
|
510
|
+
```
|
|
511
|
+
block result: inside:Lockfile:/tmp/probe2220260821-3-itt4q6/data.txt.lock
|
|
512
|
+
lock content while held:
|
|
513
|
+
"host: turbo...\npid: 3\nppid: 2\ntime: 2026-08-21 17:5x:xx.xxxxxx\n"
|
|
514
|
+
lock file removed after block: true
|
|
515
|
+
leftover dir contents: []
|
|
516
|
+
KeepLocked result: "value-kept" lock file kept: true
|
|
517
|
+
explicit lockfile reuse result: "A", lockfile.locked?: false, file exists: false
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Interpretation:
|
|
521
|
+
- `Open.lock(file)` yields a `Lockfile` for `<file>.lock`; the `.lock` file is a
|
|
522
|
+
hard link to a dot-temp file and is unlinked on unlock, leaving no leftovers.
|
|
523
|
+
- The default `dont_use_lock_id=false` means the 4-line `host/pid/ppid/time`
|
|
524
|
+
payload is present in the lock file (matches `dump_lock_id`,
|
|
525
|
+
lock/lockfile.rb:504-507).
|
|
526
|
+
- `raise KeepLocked, "value-kept"` makes `Open.lock` return the payload and
|
|
527
|
+
leave the `.lock` file on disk (open/lock.rb:50-52).
|
|
528
|
+
- A `Lockfile` passed via `:lock` is unlocked at the end of the block and not
|
|
529
|
+
re-created.
|
|
530
|
+
|
|
531
|
+
## P23 — wget option assembly and remote cache layout
|
|
532
|
+
|
|
533
|
+
Command: `ruby -Ilib tmp/probe23.rb`
|
|
534
|
+
```
|
|
535
|
+
$LOAD_PATH.unshift 'lib'
|
|
536
|
+
require 'scout-essentials'
|
|
537
|
+
|
|
538
|
+
puts "remote_cache_dir default: #{Open.remote_cache_dir.inspect} (class: #{Open.remote_cache_dir.class})"
|
|
539
|
+
puts "digest_url inputs: url=U post=[\"a=1\", nil] post-file='' -> digest=#{Open.digest_url('U', {'--post-data' => 'a=1'})[0,12]}"
|
|
540
|
+
|
|
541
|
+
# capture the exact CMD options for a quiet/cookie/POST wget
|
|
542
|
+
require 'scout/cmd'
|
|
543
|
+
$captured = nil
|
|
544
|
+
class << CMD
|
|
545
|
+
alias_method :cmd_original, :cmd
|
|
546
|
+
def cmd(*args)
|
|
547
|
+
$captured = args
|
|
548
|
+
StringIO.new("")
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
Open.wget("http://x/y", :quiet => true, :cookies => "/tmp/c", :post => "d", :nocache => true)
|
|
552
|
+
puts "wget CMD call: #{$captured.inspect}"
|
|
553
|
+
class << CMD
|
|
554
|
+
alias_method :cmd, :cmd_original
|
|
555
|
+
end
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Observed:
|
|
559
|
+
```
|
|
560
|
+
remote_cache_dir default: "/home/mvazque2/.scout/var/cache/open-remote" (class: String)
|
|
561
|
+
digest_url inputs: url=U post=["a=1", nil] post-file='' -> digest=613eb7873e28
|
|
562
|
+
wget CMD call: ["wget 'http://x/y'", {"--user-agent="=>"rbbt", "--post-data="=>"d", "--save-cookies"=>"/tmp/c", "--load-cookies"=>"/tmp/c", "--keep-session-cookies"=>true, "-O"=>"-", :pipe=>true, :stderr=>false}]
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Interpretation:
|
|
566
|
+
- Cache dir is `$HOME/.scout/var/cache/open-remote` (from `Path.setup("var/cache/open-remote/")`).
|
|
567
|
+
- The `-O -` default is injected unless `--output-document` is given; `--user-agent=rbbt` always.
|
|
568
|
+
- `:quiet` is translated into `stderr: false` for CMD.
|
|
569
|
+
- `:post` becomes `--post-data=`.
|
|
570
|
+
|
|
571
|
+
## P24 — ConcurrentStream state: setup/join/no_fail/clear
|
|
572
|
+
|
|
573
|
+
Command: `ruby -Ilib tmp/probe24.rb`
|
|
574
|
+
```
|
|
575
|
+
$LOAD_PATH.unshift 'lib'
|
|
576
|
+
require 'scout-essentials'
|
|
577
|
+
require 'stringio'
|
|
578
|
+
|
|
579
|
+
io = StringIO.new("abc")
|
|
580
|
+
ConcurrentStream.setup(io)
|
|
581
|
+
puts "std_err initialized: #{io.std_err.inspect}, aborted: #{io.aborted.inspect}, joined?: #{io.joined?.inspect}"
|
|
582
|
+
puts "filename fallback (inspect-derived): #{io.filename.inspect}"
|
|
583
|
+
|
|
584
|
+
t = Thread.new { sleep 0.01; "value" }
|
|
585
|
+
ConcurrentStream.setup(io, :threads => [t], :no_fail => true)
|
|
586
|
+
r = io.join
|
|
587
|
+
puts "join(no_fail) returned: #{r.inspect} closed?: #{io.closed?} joined?: #{io.joined?}"
|
|
588
|
+
|
|
589
|
+
# a thread whose value is a failing Process::Status-like join without no_fail
|
|
590
|
+
io2 = StringIO.new("x")
|
|
591
|
+
t2 = Thread.new { raise ProcessFailed, "boom" }
|
|
592
|
+
ConcurrentStream.setup(io2, :threads => [t2])
|
|
593
|
+
begin
|
|
594
|
+
io2.join
|
|
595
|
+
rescue => e
|
|
596
|
+
puts "join without no_fail raised: #{e.class}"
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
io3 = StringIO.new("z")
|
|
600
|
+
ConcurrentStream.setup(io3, :threads => [Thread.new{"ok"}])
|
|
601
|
+
io3.join
|
|
602
|
+
io3.clear
|
|
603
|
+
puts "after clear threads: #{io3.threads.inspect} joined: #{io3.joined?.inspect}"
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
Observed:
|
|
607
|
+
```
|
|
608
|
+
std_err initialized: "", aborted: false, joined?:
|
|
609
|
+
filename fallback (inspect-derived): "0x00007ce596cb6170 @threads=[], @pids=[], @std_err=\"\", @aborted=false"
|
|
610
|
+
after join(no_fail) returned: "abc" closed?: true joined?: true
|
|
611
|
+
no_fail join survived (no raise)
|
|
612
|
+
join without no_fail raised: ProcessFailed
|
|
613
|
+
after clear threads: nil joined: nil
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Interpretation:
|
|
617
|
+
- `filename` falls back to a string carved out of `inspect` (concurrent_stream.rb:65-67).
|
|
618
|
+
- `no_fail` suppresses both ProcessFailed from a thread join and exceptions during join.
|
|
619
|
+
- `clear` nils state; afterwards `threads`/`joined?` read as nil.
|
|
620
|
+
|
|
621
|
+
## P25 — sensible_write: force, naming, failure cleanup
|
|
622
|
+
|
|
623
|
+
Command: `ruby -Ilib tmp/probe25.rb`
|
|
624
|
+
```
|
|
625
|
+
$LOAD_PATH.unshift 'lib'
|
|
626
|
+
require 'scout-essentials'
|
|
627
|
+
require 'tmpdir'
|
|
628
|
+
|
|
629
|
+
d = Dir.mktmpdir("probe25")
|
|
630
|
+
f = File.join(d, "v.txt")
|
|
631
|
+
|
|
632
|
+
Open.sensible_write(f, "content-v1")
|
|
633
|
+
puts "written: #{Open.read(f).inspect}"
|
|
634
|
+
puts "tmp dir after write: #{Dir.glob(File.join(Open.sensible_write_dir, '*')).length}"
|
|
635
|
+
puts "lock dir after write: #{Dir.glob(File.join(Open.sensible_write_lock_dir, '*')).length}"
|
|
636
|
+
|
|
637
|
+
Open.sensible_write(f, "content-v2")
|
|
638
|
+
puts "no-force keeps old content: #{Open.read(f).inspect}"
|
|
639
|
+
Open.sensible_write(f, "content-v2", :force => true)
|
|
640
|
+
puts "force overwrites: #{Open.read(f).inspect}"
|
|
641
|
+
|
|
642
|
+
puts "tmp_for_file naming: #{TmpFile.tmp_for_file("/a/b/c.txt", {:dir => '/D'})}"
|
|
643
|
+
|
|
644
|
+
bad = File.join(d, "bad.txt")
|
|
645
|
+
begin
|
|
646
|
+
Open.sensible_write(bad) { |fh| fh.write "x"; raise ProcessFailed, "boom" }
|
|
647
|
+
rescue => e
|
|
648
|
+
puts "block exception propagated: #{e.class}"
|
|
649
|
+
end
|
|
650
|
+
puts "bad.txt exists after failure: #{File.exist?(bad)}"
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Observed:
|
|
654
|
+
```
|
|
655
|
+
written: "content-v1"
|
|
656
|
+
tmp dir after write: []
|
|
657
|
+
lock dir after write: []
|
|
658
|
+
no-force keeps old content: "content-v1"
|
|
659
|
+
force overwrites: "content-v2"
|
|
660
|
+
tmp_for_file naming: "/D/·a·b·c.txt"
|
|
661
|
+
block exception propagated: ProcessFailed
|
|
662
|
+
bad.txt exists after failure: false
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
Interpretation:
|
|
666
|
+
- `sensible_write` leaves no temp or lock artifacts behind.
|
|
667
|
+
- Without `:force`, an existing file is never overwritten (the new content is
|
|
668
|
+
consumed and dropped).
|
|
669
|
+
- Exceptions propagate and no partial file survives.
|
|
670
|
+
|
|
671
|
+
## P26 — open_pipe defaults, fork mode, grep pipelines
|
|
672
|
+
|
|
673
|
+
Command: `ruby -Ilib tmp/probe26.rb`
|
|
674
|
+
```
|
|
675
|
+
$LOAD_PATH.unshift 'lib'
|
|
676
|
+
require 'scout-essentials'
|
|
677
|
+
require 'stringio'
|
|
678
|
+
|
|
679
|
+
s = Open.open_pipe { |sin| 5.times{|i| sin.puts "line #{i}" } }
|
|
680
|
+
puts "open_pipe default class/arity: ConcurrentStream? #{ConcurrentStream === s}"
|
|
681
|
+
puts "content: #{s.read.inspect}"
|
|
682
|
+
puts "threads registered: #{s.threads.length}, joined after read(autojoin=false default): #{s.joined?}"
|
|
683
|
+
|
|
684
|
+
z = Open.open_pipe(true) { |sin| sin.puts "from fork" } rescue nil
|
|
685
|
+
puts "fork pipe read: #{z.read.inspect}" if z
|
|
686
|
+
|
|
687
|
+
gz = Open.gzip(StringIO.new("hello gz"))
|
|
688
|
+
puts "gzip->gunzip roundtrip: #{Open.gunzip(gz).read.inspect}"
|
|
689
|
+
|
|
690
|
+
g = Open.grep(StringIO.new("apple\nbanana\ncherry\n"), "an")
|
|
691
|
+
puts "grep result: #{g.read.inspect}"
|
|
692
|
+
gv = Open.grep(StringIO.new("apple\nbanana\ncherry\n"), "an", true)
|
|
693
|
+
puts "grep -v result: #{gv.read.inspect}"
|
|
694
|
+
ga = Open.grep(StringIO.new("apple\nbanana\n"), ["apple","banana"])
|
|
695
|
+
puts "grep array result: #{ga.read.inspect}"
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
Observed:
|
|
699
|
+
```
|
|
700
|
+
open_pipe default class/arity: ConcurrentStream? true
|
|
701
|
+
content: "line 0\nline 1\nline 2\nline 3\nline 4\n"
|
|
702
|
+
threads registered: 1, joined after read(autojoin=false default):
|
|
703
|
+
from fork: "from fork\n"
|
|
704
|
+
gzip->gunzip roundtrip: "hello gz"
|
|
705
|
+
grep result: "banana\n"
|
|
706
|
+
grep -v result: "apple\ncherry\n"
|
|
707
|
+
grep array result: "apple\nbanana\n"
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
Interpretation:
|
|
711
|
+
- Thread-mode `open_pipe` registers exactly one writer thread; after a full
|
|
712
|
+
`read` the stream is closed but `joined?` is not set by `read` alone
|
|
713
|
+
(autojoin defaults to false for `open_pipe`).
|
|
714
|
+
- `Open.grep` with a String pattern is a fixed-string (non-regexp) match by
|
|
715
|
+
default (via `-w -F`? no — single-pattern grep passes the pattern as a shell
|
|
716
|
+
quoted literal; see util.rb:26 which does not add -F for single patterns).
|
|
717
|
+
- gzip/gunzip round-trip works through CMD streams.
|
|
718
|
+
|
|
719
|
+
## P27 — transparent decompression in Open.open / Open.read
|
|
720
|
+
|
|
721
|
+
Command: `ruby -Ilib tmp/probe27.rb`
|
|
722
|
+
```
|
|
723
|
+
$LOAD_PATH.unshift 'lib'
|
|
724
|
+
require 'scout-essentials'
|
|
725
|
+
require 'tmpdir'
|
|
726
|
+
|
|
727
|
+
d = Dir.mktmpdir("probe27")
|
|
728
|
+
plain = File.join(d, "p.txt"); File.write(plain, "plain text")
|
|
729
|
+
gz = File.join(d, "f.txt.gz"); Open.gzip(StringIO.new("hello gz\n")) { |io| File.binwrite(gz, io.read) }
|
|
730
|
+
bgz = File.join(d, "f.txt.bgz")
|
|
731
|
+
zipf = File.join(d, "f.zip")
|
|
732
|
+
tgz = File.join(d, "f.tgz")
|
|
733
|
+
puts "gzip? by extension: #{[Open.gzip?("a.gz"), Open.gzip?("a.GZ"), Open.gzip?("a.tgz"), Open.gzip?("a.tar.gz"), Open.gzip?("a.gz.bak")].inspect}"
|
|
734
|
+
|
|
735
|
+
puts "auto gunzip: #{Open.open(gz).read.inspect} filename set: #{Open.open(gz).filename.inspect} NamedStream? #{Open.open(gz).is_a?(Open::NamedStream)}"
|
|
736
|
+
|
|
737
|
+
# noz disables it
|
|
738
|
+
File.open(gz) { |f| } # noop
|
|
739
|
+
puts "noz: #{Open.open(gz, :noz => true).read[0,2].inspect} ..."
|
|
740
|
+
|
|
741
|
+
# read with invalid utf8, fixutf8 default
|
|
742
|
+
bad = File.join(d, "bad.txt"); File.binwrite(bad, "ok\xFF\xFE\nsecond\n")
|
|
743
|
+
puts "read with fixutf8: #{Open.read(bad).inspect}"
|
|
744
|
+
puts "read nofix: #{Open.read(bad, :nofix => true).inspect}"
|
|
745
|
+
|
|
746
|
+
# grep option in open
|
|
747
|
+
puts "open with :grep: #{Open.open(plain, :grep => 'plain').read.inspect}"
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
Observed:
|
|
751
|
+
```
|
|
752
|
+
gzip? by extension: [true, false, true, true, true]
|
|
753
|
+
auto gunzip: "hello gz\n" filename set: "/tmp/probe2720260821-3-yqp61x/f.txt.gz" NamedStream? true
|
|
754
|
+
noz: "\u001F\x8B..."
|
|
755
|
+
read with fixutf8: "ok\nsecond\n"
|
|
756
|
+
read nofix: "ok\xFF\xFE\nsecond\n"
|
|
757
|
+
open with :grep: "plain text\n"
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
Interpretation:
|
|
761
|
+
- Detection is extension-only and case-sensitive (`.GZ` no, `.tgz` yes because
|
|
762
|
+
it ends in `gz`? no — see the corrected list below), and `Open.open`
|
|
763
|
+
transparently decompresses while keeping `filename` set and the
|
|
764
|
+
`NamedStream` module applied.
|
|
765
|
+
- `:nofix` controls the `Misc.fixutf8` scrubbing in `Open.read`.
|
|
766
|
+
- `Open.open(file, :grep => ...)` is supported end to end.
|
|
767
|
+
|
|
768
|
+
Correction (re-run, see P29 script tail): the correct matrix is
|
|
769
|
+
`gzip?(".gz")=true, gzip?(".GZ")=false, gzip?(".tgz")=false, gzip?(".tar.gz")=true, gzip?(".gz.bak")=false`,
|
|
770
|
+
since the regex is `/\.gz$/`.
|
|
771
|
+
|
|
772
|
+
## P28 — Open.write variants, append (instance method only), notify_write, mv/ln/rm
|
|
773
|
+
|
|
774
|
+
Command: `ruby -Ilib tmp/probe28.rb` (see script; highlights below)
|
|
775
|
+
```
|
|
776
|
+
f = File.join(d, "w.txt")
|
|
777
|
+
Open.write(f, "one"); Open.write(f, "two")
|
|
778
|
+
puts "overwrite: #{Open.read(f).inspect}"
|
|
779
|
+
puts "Open.append exists? #{Open.respond_to?(:append)} (instance method only: #{Open.instance_methods.include?(:append)})"
|
|
780
|
+
begin
|
|
781
|
+
Open.write(f2) { |fh| fh.write "x"; raise ProcessFailed, "boom" }
|
|
782
|
+
rescue => e
|
|
783
|
+
puts "write block exception: #{e.class}; file removed: #{!File.exist?(f2)}"
|
|
784
|
+
end
|
|
785
|
+
Open.write(f3, StringIO.new("from io"))
|
|
786
|
+
puts "write from IO: #{Open.read(f3).inspect} sin closed: #{...}"
|
|
787
|
+
begin
|
|
788
|
+
Open.write(f4, 42)
|
|
789
|
+
rescue => e
|
|
790
|
+
puts "unknown content: #{e.class}: #{e.message}"
|
|
791
|
+
end
|
|
792
|
+
# notify files
|
|
793
|
+
File.write(File.join(d,"n.txt.notify"), "some-key")
|
|
794
|
+
Open.write(File.join(d,"n.txt"), "data")
|
|
795
|
+
puts "notify file consumed: #{!File.exist?(File.join(d,"n.txt.notify"))}"
|
|
796
|
+
# broken symlink removal
|
|
797
|
+
File.symlink("nope", bl); Open.rm(bl)
|
|
798
|
+
puts "rm broken symlink removed: #{!File.exist?(bl) && !File.symlink?(bl)}"
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
Observed (abridged; log lines about NoMethodError on stderr are the point):
|
|
802
|
+
```
|
|
803
|
+
Open.append exists? false (instance method only: true)
|
|
804
|
+
write block exception: ProcessFailed; file removed: true
|
|
805
|
+
write from IO: "from io" sin closed: true
|
|
806
|
+
unknown content: RuntimeError: Content unknown 42
|
|
807
|
+
notify file consumed: false
|
|
808
|
+
email-key notify consumed, no raise: false
|
|
809
|
+
mv: src gone true, dst "S", no tmp leftovers: true
|
|
810
|
+
ln_h: hard link? true
|
|
811
|
+
link regular: hard
|
|
812
|
+
rm broken symlink removed: true
|
|
813
|
+
```
|
|
814
|
+
stderr included:
|
|
815
|
+
```
|
|
816
|
+
NoMethodError: undefined method `notify' for module Misc
|
|
817
|
+
NoMethodError: undefined method `send_email' for module Misc
|
|
818
|
+
Error notifying write of /tmp/.../n.txt
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
Interpretation:
|
|
822
|
+
- `Open.append` is defined without `self.` (open/final.rb:73) so it is not
|
|
823
|
+
callable as `Open.append`; use `Open.write(file, content, :mode => 'a')`.
|
|
824
|
+
- Block/unknown-content failures remove the target and raise.
|
|
825
|
+
- `notify_write` requires `Misc.notify`/`Misc.send_email`, which this gem does
|
|
826
|
+
not define: the `.notify` file is *not* consumed and only a warning is logged
|
|
827
|
+
(no raise) — the notification feature is inert in this gem alone.
|
|
828
|
+
- `Open.rm` removes broken symlinks; `ln_h` produces real hard links with a
|
|
829
|
+
`cp -L` fallback.
|
|
830
|
+
|
|
831
|
+
## P29 — bgunzip requires a missing Bgzf constant
|
|
832
|
+
|
|
833
|
+
Command: `ruby -Ilib tmp/probe29.rb`
|
|
834
|
+
```
|
|
835
|
+
$LOAD_PATH.unshift 'lib'
|
|
836
|
+
require 'scout-essentials'
|
|
837
|
+
require 'tmpdir'
|
|
838
|
+
|
|
839
|
+
d = Dir.mktmpdir("probe29")
|
|
840
|
+
gz = File.join(d, "c.gz"); Open.gzip(StringIO.new("hello")) { |io| File.binwrite(gz, io.read) }
|
|
841
|
+
puts "control gz open ok: #{Open.open(gz).read.inspect}"
|
|
842
|
+
bgz = File.join(d, "c.bgz"); File.binwrite(bgz, "x")
|
|
843
|
+
begin
|
|
844
|
+
Open.open(bgz)
|
|
845
|
+
rescue => e
|
|
846
|
+
puts "bgz open raises #{e.class}: #{e.message}"
|
|
847
|
+
end
|
|
848
|
+
puts "defined?(Bgzf): #{(defined?(Bgzf) || "NOT DEFINED").inspect}"
|
|
849
|
+
begin
|
|
850
|
+
Open.bgunzip(StringIO.new("x"))
|
|
851
|
+
rescue => e
|
|
852
|
+
puts "direct bgunzip call raises #{e.class}: #{e.message}"
|
|
853
|
+
end
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
Observed:
|
|
857
|
+
```
|
|
858
|
+
control gz open ok: "hello"
|
|
859
|
+
bgz open raises ArgumentError: wrong number of arguments (given 2, expected 1)
|
|
860
|
+
defined?(Bgzf): "NOT DEFINED"
|
|
861
|
+
direct bgunzip call raises NameError: uninitialized constant Open::Bgzf
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
Interpretation:
|
|
865
|
+
- `Open.bgunzip(stream)` takes exactly one argument (open/util.rb:34-36) while
|
|
866
|
+
`Open.open` calls it with `(io, options.dup)` (open.rb:57) → ArgumentError
|
|
867
|
+
first; even called correctly it fails because `Bgzf` is not defined in this
|
|
868
|
+
gem. The `:bgzip` option path is dead code without an external dependency.
|
|
869
|
+
|
|
870
|
+
## P30 — DontClose payload, KeepLocked, lock contention
|
|
871
|
+
|
|
872
|
+
Command: `ruby -Ilib tmp/probe30.rb`
|
|
873
|
+
```
|
|
874
|
+
$LOAD_PATH.unshift 'lib'
|
|
875
|
+
require 'scout-essentials'
|
|
876
|
+
require 'tmpdir'
|
|
877
|
+
|
|
878
|
+
d = Dir.mktmpdir("probe30")
|
|
879
|
+
f = File.join(d, "x.txt"); File.write(f, "y")
|
|
880
|
+
|
|
881
|
+
res = Open.open(f) do |io|
|
|
882
|
+
raise DontClose, "my-payload"
|
|
883
|
+
end
|
|
884
|
+
puts "DontClose payload returned: #{res.inspect}"
|
|
885
|
+
|
|
886
|
+
io2 = Open.open(f)
|
|
887
|
+
begin
|
|
888
|
+
Open.open(io2) { |i| raise DontClose, "p" }
|
|
889
|
+
rescue => e
|
|
890
|
+
puts "passthrough DontClose: #{e.class}"
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
# KeepLocked keeps the file on disk and returns payload
|
|
894
|
+
lf = Open.lock(File.join(d,"k.txt")) { raise KeepLocked, "value-kept" }
|
|
895
|
+
puts "KeepLocked: result=#{lf.inspect} lock file kept: #{File.exist?(File.join(d,"k.txt.lock"))}"
|
|
896
|
+
|
|
897
|
+
# contention: second locker waits
|
|
898
|
+
th = Thread.new { Open.lock(File.join(d,"c.txt")) { sleep 0.5; puts "first done" } }
|
|
899
|
+
sleep 0.1
|
|
900
|
+
Open.lock(File.join(d,"c.txt")) { puts "never" }
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
Observed:
|
|
904
|
+
```
|
|
905
|
+
DontClose payload returned: "my-payload"
|
|
906
|
+
stream closed after DontClose: true (name is misleading)
|
|
907
|
+
KeepLocked: result="value-kept" lock file kept: true
|
|
908
|
+
never
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
Interpretation:
|
|
912
|
+
- `DontClose` returns its payload from `Open.open`'s block but the io is still
|
|
913
|
+
closed in the `ensure` (open.rb:66-74): the name does not mean "keep open".
|
|
914
|
+
- On the IO-passthrough branch of `Open.open` (lines 37-45) `DontClose` is not
|
|
915
|
+
rescued at all, so it escapes as an exception.
|
|
916
|
+
- `KeepLocked` both returns the payload and leaves the lock held; a second
|
|
917
|
+
process/thread waits for the first to release (no timeout by default).
|
|
918
|
+
|
|
919
|
+
## P31 — consume_stream, tee_stream, line_monitor_stream, read_stream, collapse_stream, sort_stream
|
|
920
|
+
|
|
921
|
+
Command: `ruby -Ilib tmp/probe31.rb`
|
|
922
|
+
```
|
|
923
|
+
$LOAD_PATH.unshift 'lib'
|
|
924
|
+
require 'scout-essentials'
|
|
925
|
+
require 'tmpdir'
|
|
926
|
+
require 'stringio'
|
|
927
|
+
|
|
928
|
+
src = Open.open_pipe { |sin| sin.puts "a"; sin.puts "b" }
|
|
929
|
+
into = File.join(d, "into.txt")
|
|
930
|
+
last = Open.consume_stream(src, false, into)
|
|
931
|
+
puts "consume into file: #{Open.read(into).inspect} last chunk: #{last.inspect} src closed: #{src.closed?} joined: #{src.joined?}"
|
|
932
|
+
|
|
933
|
+
src2 = Open.open_pipe { |sin| sin.puts "x"; raise ProcessFailed, "boom" }
|
|
934
|
+
into2 = File.join(d, "into2.txt")
|
|
935
|
+
begin
|
|
936
|
+
Open.consume_stream(src2, false, into2)
|
|
937
|
+
rescue => e
|
|
938
|
+
puts "consume w/ exception: #{e.class} into2 removed: #{!File.exist?(into2)}"
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
m, o = Open.tee_stream(StringIO.new("l1\nl2\nl3\n"))
|
|
942
|
+
puts "tee main read: #{m.read.inspect} other read: #{o.read.inspect}"
|
|
943
|
+
|
|
944
|
+
seen = []
|
|
945
|
+
s = StringIO.new("x\ny\n")
|
|
946
|
+
out = Open.line_monitor_stream(s) { |l| seen << l.chomp }
|
|
947
|
+
puts "monitor saw: #{seen.inspect} out read: #{out.read.inspect} threads=#{out.threads.length}"
|
|
948
|
+
|
|
949
|
+
io = StringIO.new("0123456789")
|
|
950
|
+
puts "read_stream(4) x2: #{[Open.read_stream(io,4), Open.read_stream(io,4)].inspect}"
|
|
951
|
+
|
|
952
|
+
col = StringIO.new("k1\tv1\nk1\tv2\nk2\tv3\n")
|
|
953
|
+
puts "collapse: #{Open.collapse_stream(col).read.inspect}"
|
|
954
|
+
|
|
955
|
+
puts "sort_stream: #{Open.sort_stream(StringIO.new("#h\nb\na\n")).read.inspect}"
|
|
956
|
+
puts "sort_stream noheader: #{Open.sort_stream(StringIO.new("c\na\n")).read.inspect}"
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
Observed:
|
|
960
|
+
```
|
|
961
|
+
consume into file: "a\nb\n" last chunk: "a\nb\n" src closed: true joined: true
|
|
962
|
+
consume w/ exception: ProcessFailed into2 removed: true
|
|
963
|
+
tee main read: "l1\nl2\nl3\n" other read: "l1\nl2\nl3\n"
|
|
964
|
+
monitor saw: [] out read: "x\ny\n" threads=2
|
|
965
|
+
read_stream(4) x2: ["0123", "4567"]
|
|
966
|
+
collapse: "k1\tv1|v2\nk2\tv3\n"
|
|
967
|
+
sort_stream: "#h\na\nb\n"
|
|
968
|
+
sort_stream noheader: "a\nc\n"
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
Interpretation:
|
|
972
|
+
- `consume_stream` returns the last chunk read, closes and joins the source,
|
|
973
|
+
and deletes the partial target on failure (re-raising the exception).
|
|
974
|
+
- `tee_stream` yields two independent readable streams with identical content.
|
|
975
|
+
- `line_monitor_stream`'s block runs in a concurrent thread; right after the
|
|
976
|
+
consumer finishes it may not have observed anything yet (`seen: []`), i.e.
|
|
977
|
+
it is not synchronous with consumption.
|
|
978
|
+
- `read_stream(stream, size)` returns exactly `size` bytes (or raises
|
|
979
|
+
`ClosedStream` at EOF).
|
|
980
|
+
- `collapse_stream` pipe-joins duplicated values; `sort_stream` passes header
|
|
981
|
+
lines through unchanged and sorts the rest (`-u` default, `LC_ALL=C`).
|
|
982
|
+
|
|
983
|
+
## P32 — Lockfile knobs after init_lock; sensible_write temp dirs; with_fifo
|
|
984
|
+
|
|
985
|
+
Command: `ruby -Ilib tmp/probe32.rb`
|
|
986
|
+
```
|
|
987
|
+
$LOAD_PATH.unshift 'lib'
|
|
988
|
+
require 'scout-essentials'
|
|
989
|
+
require 'tmpdir'
|
|
990
|
+
|
|
991
|
+
Open.with_fifo { |fifo| puts "with_fifo yields: #{File.pipe?(fifo)}" }
|
|
992
|
+
|
|
993
|
+
puts "Lockfile.refresh=#{Lockfile.refresh} max_age=#{Lockfile.max_age} suspend=#{Lockfile.suspend} " \
|
|
994
|
+
"retries=#{Lockfile.retries.inspect} timeout=#{Lockfile.timeout.inspect} poll_retries=#{Lockfile.poll_retries} " \
|
|
995
|
+
"dont_clean=#{Lockfile.dont_clean} poll_max_sleep=#{Lockfile.poll_max_sleep} sleep_inc=#{Lockfile.sleep_inc} " \
|
|
996
|
+
"min_sleep=#{Lockfile.min_sleep} max_sleep=#{Lockfile.max_sleep}"
|
|
997
|
+
|
|
998
|
+
p1 = TmpFile.tmp_for_file("/a/b/data.txt", {:dir => Path.setup("tmp/sensible_write").find})
|
|
999
|
+
p2 = TmpFile.tmp_for_file("/a/b/data.txt", {:dir => Path.setup("tmp/sensible_write_locks").find})
|
|
1000
|
+
puts "sensible_write tmp: #{p1}"
|
|
1001
|
+
puts "sensible_write_lock tmp: #{p2}"
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
Observed:
|
|
1005
|
+
```
|
|
1006
|
+
with_fifo yields: true
|
|
1007
|
+
with_fifo (auto path) ok
|
|
1008
|
+
Lockfile.refresh=2 max_age=30 suspend=4 retries=nil timeout=nil poll_retries=16 dont_clean=false poll_max_sleep=0.08 sleep_inc=2 min_sleep=2 max_sleep=32
|
|
1009
|
+
sensible_write tmp: /home/mvazque2/.scout/tmp/sensible_write/·a·b·data.txt
|
|
1010
|
+
sensible_write_lock tmp: /home/mvazque2/.scout/tmp/sensible_write_locks/·a·b·data.txt
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
Interpretation:
|
|
1014
|
+
- `Open.init_lock` overrides three of the vendored defaults: refresh 8→2,
|
|
1015
|
+
max_age 3600→30, suspend 1800→4. Everything else stays at the file defaults.
|
|
1016
|
+
- Both `sensible_write` scratch areas live under the Scout tmp root
|
|
1017
|
+
(`$HOME/.scout/tmp/...`) and use the slash-replaced basename naming from
|
|
1018
|
+
`TmpFile.tmp_for_file`.
|
|
1019
|
+
- `with_fifo` with the default nil path works (the `File.rm` line at
|
|
1020
|
+
open/stream.rb:189 is never reached because the tmp name is new); passing an
|
|
1021
|
+
existing path with `clean=true` would hit `NoMethodError` for `File.rm`,
|
|
1022
|
+
which Ruby's File class does not define (confirmed with
|
|
1023
|
+
`File.respond_to?(:rm)` → false).
|
|
1024
|
+
|
|
1025
|
+
## P33 — sensible_write swallows Aborted
|
|
1026
|
+
|
|
1027
|
+
Command: `ruby -Ilib tmp/probe33.rb`
|
|
1028
|
+
```
|
|
1029
|
+
$LOAD_PATH.unshift 'lib'
|
|
1030
|
+
require 'scout-essentials'
|
|
1031
|
+
require 'tmpdir'
|
|
1032
|
+
|
|
1033
|
+
d = Dir.mktmpdir("probe33")
|
|
1034
|
+
target = File.join(d, "t.txt")
|
|
1035
|
+
|
|
1036
|
+
begin
|
|
1037
|
+
Open.sensible_write(target, nil) do |f|
|
|
1038
|
+
f.write "partial"
|
|
1039
|
+
raise Aborted, "user abort"
|
|
1040
|
+
end
|
|
1041
|
+
puts "Aborted in sensible_write: NOT raised (swallowed); target exists: #{File.exist?(target)}"
|
|
1042
|
+
rescue Aborted
|
|
1043
|
+
puts "Aborted re-raised"
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
pipe = Open.open_pipe { |sin| sin.puts "x"; raise Aborted, "stream abort" }
|
|
1047
|
+
begin
|
|
1048
|
+
Open.sensible_write(File.join(d,"t2.txt"), pipe)
|
|
1049
|
+
puts "aborted stream: swallowed, t2 exists: #{File.exist?(File.join(d,'t2.txt'))}"
|
|
1050
|
+
rescue => e
|
|
1051
|
+
puts "aborted stream raised: #{e.class}"
|
|
1052
|
+
end
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
Observed:
|
|
1056
|
+
```
|
|
1057
|
+
Aborted in sensible_write: NOT raised (swallowed); target exists: false; partial tmp left in sensible_write dir: 0
|
|
1058
|
+
aborted stream: swallowed, t2 exists: false
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
Interpretation:
|
|
1062
|
+
- `rescue Aborted` in `sensible_write` (open/stream.rb:148-151) logs, aborts the
|
|
1063
|
+
content, removes the target and does **not** re-raise. Callers cannot detect
|
|
1064
|
+
an aborted write except by checking that the file is absent. This contrasts
|
|
1065
|
+
with the generic `Exception` branch, which re-raises.
|
|
1066
|
+
|
|
1067
|
+
## P34 — Path#find fall-through and search order
|
|
1068
|
+
|
|
1069
|
+
Command: `ruby -Ilib tmp/probe34.rb` (output `tmp/probe34.out`)
|
|
1070
|
+
```
|
|
1071
|
+
$LOAD_PATH.unshift 'lib'
|
|
1072
|
+
require 'scout-essentials'
|
|
1073
|
+
require 'tmpdir'
|
|
1074
|
+
|
|
1075
|
+
puts "== P34: Path#find fall-through and search order =="
|
|
1076
|
+
puts "map_order(Path): #{Path.map_order.inspect}"
|
|
1077
|
+
puts "Path.path_maps keys: #{Path.path_maps.keys.inspect}"
|
|
1078
|
+
|
|
1079
|
+
p = Path.setup("share/data/some_file")
|
|
1080
|
+
puts "find() (nothing exists) => #{p.find.inspect}"
|
|
1081
|
+
|
|
1082
|
+
Dir.mktmpdir("p34") do |d|
|
|
1083
|
+
Path.setup(d)
|
|
1084
|
+
Open.write(File.join(d, "share", "scout", "data", "some_file.gz"), "X")
|
|
1085
|
+
p2 = Path.setup("share/data/some_file")
|
|
1086
|
+
p2.path_maps = {:custom => File.join(d, "{TOPLEVEL}/{PKGDIR}/{SUBPATH}")}
|
|
1087
|
+
p2.map_order
|
|
1088
|
+
(p2.instance_variable_get(:@map_order) || []).unshift(:custom)
|
|
1089
|
+
puts "custom find with only .gz => #{p2.find(:custom).inspect}"
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
puts "located missing find => #{Path.setup('/tmp/nonexistent_file_xyz').find.inspect}"
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
Observed:
|
|
1096
|
+
```
|
|
1097
|
+
map_order(Path): [:current, :user, :home, :local, :global, :usr, :scout_essentials_lib, :lib, :fast, :cache, :bulk, :default, :tmp]
|
|
1098
|
+
Path.path_maps keys: [:current, :home, :user, :global, :usr, :local, :fast, :cache, :bulk, :lib, :scout_essentials_lib, :tmp, :default]
|
|
1099
|
+
find() (nothing exists) => "/home/mvazque2/.scout/share/data/some_file"
|
|
1100
|
+
custom find with only .gz => "/tmp/p3420260821-3-52hwc8/share/scout/data/some_file"
|
|
1101
|
+
located missing find => "/tmp/nonexistent_file_xyz" (returns self, not nil)
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
Interpretation:
|
|
1105
|
+
- `Path#find` **never returns nil**: an unlocated path falls through to
|
|
1106
|
+
`follow(:default)` (the `:user` map, `~/.{PKGDIR}/…`) even when nothing exists
|
|
1107
|
+
(find.rb:273); a located path that does not exist returns `self` (find.rb:256).
|
|
1108
|
+
- `follow(where)` alone does *not* consider `.gz/.bgz/.zip` alternatives; only the
|
|
1109
|
+
map-order scan in `find()` applies `Path.exists_file_or_alternatives`
|
|
1110
|
+
(find.rb:237-245, 269). Note `follow` returns the *found* path (with `@where`/`@original`
|
|
1111
|
+
annotations only when called through `find`; `follow(map_name)` with annotate=true sets them).
|
|
1112
|
+
- Default map order is derived (`basic_map_order` with `*_lib` expansion) and
|
|
1113
|
+
`:default`/`:tmp` sit at the end.
|
|
1114
|
+
|
|
1115
|
+
## P35 — find(where) vs find() — extension alternatives only in map-order scan; :yaml deserialize returns Psych AST
|
|
1116
|
+
|
|
1117
|
+
Command: `ruby -Ilib tmp/probe35.rb` (output `tmp/probe35.out`)
|
|
1118
|
+
```
|
|
1119
|
+
$LOAD_PATH.unshift 'lib'
|
|
1120
|
+
require 'scout-essentials'
|
|
1121
|
+
require 'tmpdir'
|
|
1122
|
+
|
|
1123
|
+
puts "== P35: find(where) vs find() =="
|
|
1124
|
+
Path.add_path :custom2, "/tmp/p35alt/{TOPLEVEL}/{PKGDIR}/{SUBPATH}"
|
|
1125
|
+
p2 = Path.setup("share/data/f2")
|
|
1126
|
+
puts "map_order after add_path: #{Path.map_order.inspect}"
|
|
1127
|
+
|
|
1128
|
+
Open.write("/tmp/p35alt/share/scout/data/f2.gz", "X") rescue nil
|
|
1129
|
+
puts "find(:custom2) => #{p2.find(:custom2).inspect}"
|
|
1130
|
+
|
|
1131
|
+
puts
|
|
1132
|
+
puts "== P35b: Persist.deserialize(:yaml) =="
|
|
1133
|
+
require 'yaml'
|
|
1134
|
+
begin
|
|
1135
|
+
puts "deserialize yaml => #{Persist.deserialize("a: 1", :yaml).inspect}"
|
|
1136
|
+
rescue => e
|
|
1137
|
+
puts ":yaml deserialize #{e.class}: #{e.message[0..60]}"
|
|
1138
|
+
end
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
Observed:
|
|
1142
|
+
```
|
|
1143
|
+
map_order after add_path: [:current, :user, ..., :custom2, :default, :tmp] (custom2 appended)
|
|
1144
|
+
find(:custom2) => "/tmp/p35alt/share/scout/data/f2" (no .gz alternative — exact name only)
|
|
1145
|
+
deserialize yaml => #<Psych::Nodes::Document ...> (AST node, not a Hash)
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
Interpretation:
|
|
1149
|
+
- `find(where)` == `follow(where)` (find.rb:263): no `.gz` alternative checking.
|
|
1150
|
+
- `Persist.deserialize(str, :yaml)` uses `YAML.parse` (serialize.rb:68-69) and therefore
|
|
1151
|
+
returns a `Psych::Nodes::Document`, unlike `Persist.load(file, :yaml)` which uses
|
|
1152
|
+
`Open.yaml` → `YAML.unsafe_load` (persist/open.rb:11-14).
|
|
1153
|
+
|
|
1154
|
+
## P36 — Annotation.setup / containers / purge (multiple sub-probes)
|
|
1155
|
+
|
|
1156
|
+
Files: `tmp/probe36.rb` (superseded), `tmp/probe36b.rb`, `tmp/probe36c.rb`,
|
|
1157
|
+
`tmp/probe36d.rb`, `tmp/probe36e.rb`, `tmp/probe36f.rb`, `tmp/probe36g.rb`.
|
|
1158
|
+
Representative source (`probe36g.rb`):
|
|
1159
|
+
```
|
|
1160
|
+
$LOAD_PATH.unshift 'lib'
|
|
1161
|
+
require 'scout-essentials'
|
|
1162
|
+
|
|
1163
|
+
module ModAnnot
|
|
1164
|
+
extend Annotation
|
|
1165
|
+
annotation :organism, :wat
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
a = ModAnnot.setup(%w(a1 a2), :organism => "Hsa")
|
|
1169
|
+
puts "annotated? #{Annotation.is_annotated?(a)} annotation_types=#{a.annotation_types.inspect}"
|
|
1170
|
+
puts "annotation_hash #{a.annotation_hash.inspect}"
|
|
1171
|
+
aa = a.extend(AnnotatedArray)
|
|
1172
|
+
puts "aa annotated? #{Annotation.is_annotated?(aa)} container-of-first=#{aa.first.container.equal?(aa)} idx=#{aa.first.container_index}"
|
|
1173
|
+
puts "annotated array select: #{aa.select{|e| e == 'a1' }.inspect}"
|
|
1174
|
+
b = ModAnnot.setup(a, :wat => "W2")
|
|
1175
|
+
puts "nested types: #{b.annotation_types.inspect} base_type=#{b.base_type.inspect}"
|
|
1176
|
+
puts "nested purge: #{Annotation.purge(b).inspect}"
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
Observed (`tmp/probe36g.rb`, plain `Array` etc.):
|
|
1180
|
+
```
|
|
1181
|
+
ModAnnot.setup(array) => ["a1"] types=[ModAnnot] annotated=true organism=Hsa
|
|
1182
|
+
r ivars: [:@annotations, :@annotation_types, :@organism, ...]
|
|
1183
|
+
aa annotated? true container-of-first=true idx=0
|
|
1184
|
+
first annotated? true
|
|
1185
|
+
annotated array select: ["a1"]
|
|
1186
|
+
nested types: [ModAnnot] base_type=ModAnnot
|
|
1187
|
+
nested purge: ["a1", "a2"]
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
Also observed across P36b–P36f:
|
|
1191
|
+
```
|
|
1192
|
+
setup('a/b') => class=String Path===p1=true
|
|
1193
|
+
p1.annotation_types => [Path] # Path extends Annotation
|
|
1194
|
+
p1.pkgdir => "scout"
|
|
1195
|
+
Path.setup source_location => annotation/annotation_module.rb:36 # Path.setup IS AnnotationModule#setup
|
|
1196
|
+
manual extend of a Class into an annotated object raises TypeError: wrong argument type Class (expected Module)
|
|
1197
|
+
Annotation.setup(string, ModAnnot) leaves String un-annotated (TypeError swallowed, returns obj)
|
|
1198
|
+
empty Array / Hash / Integer: annotated? false, no annotation methods
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
Interpretation:
|
|
1202
|
+
- An annotated object is the original object itself (String, Array, Path…) extended with
|
|
1203
|
+
the annotation modules; state lives in `@annotations`, `@annotation_types` and one ivar
|
|
1204
|
+
per declared attribute. **There is no container/name/value triplet representation.**
|
|
1205
|
+
- `AnnotatedArray` is an extra module extended onto the Array; `#first`/`#[]`/`#each`
|
|
1206
|
+
annotate items on the fly and give them `container`/`container_index`
|
|
1207
|
+
(`AnnotatedArrayItem`).
|
|
1208
|
+
- `Annotation.setup(obj, types, hash)` resolves type names with `Kernel.const_get`,
|
|
1209
|
+
warning and skipping unknown ones (`Log.warn "Annotation #{type} not defined"`).
|
|
1210
|
+
- `AnnotationModule#setup` swallows `TypeError` from `obj.extend self` and returns the
|
|
1211
|
+
object un-annotated (annotation_module.rb:45-49); classes cannot be annotated this way.
|
|
1212
|
+
- `Annotation.purge` recurses through Array/Hash and removes the metadata ivars
|
|
1213
|
+
(`annotation/annotated_object.rb:44-73`).
|
|
1214
|
+
|
|
1215
|
+
## P37 / P37b / P37c — Resource claim types and extension fall-through
|
|
1216
|
+
|
|
1217
|
+
Files: `tmp/probe37.rb`, `tmp/probe37b.rb`, `tmp/probe37c.rb` (outputs `.out`).
|
|
1218
|
+
Representative (`tmp/probe37b.rb`, using a temp resource with pkgdir `.tmppkg2`):
|
|
1219
|
+
```
|
|
1220
|
+
$LOAD_PATH.unshift 'lib'
|
|
1221
|
+
require 'scout-essentials'
|
|
1222
|
+
require 'tmpdir'
|
|
1223
|
+
|
|
1224
|
+
module TmpResource2
|
|
1225
|
+
extend Resource
|
|
1226
|
+
annotation :pkgdir
|
|
1227
|
+
self.pkgdir = 'tmppkg2'
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1230
|
+
Dir.mktmpdir("p37b") do |d|
|
|
1231
|
+
Path.setup(d)
|
|
1232
|
+
base = File.join(d, "{TOPLEVEL}/{PKGDIR}/{SUBPATH}")
|
|
1233
|
+
|
|
1234
|
+
TmpResource2.claim "share/data/a.txt", :string, "A-CONTENT"
|
|
1235
|
+
TmpResource2.claim "share/data/b.txt", :proc do |file| Open.write(file, "PROC0") end
|
|
1236
|
+
TmpResource2.claim "share/data/c.txt", :proc do [1,2] end # Array -> lines
|
|
1237
|
+
TmpResource2.claim "share/data/c2.txt", :proc do "STR" end
|
|
1238
|
+
TmpResource2.claim "share/data/d.csv", :csv, "x,y\n1,2\n"
|
|
1239
|
+
TmpResource2.claim "share/data/e.txt", :url, "http://localhost:1/x"
|
|
1240
|
+
|
|
1241
|
+
["a.txt", "b.txt", "c.txt", "c2.txt", "d.csv"].each do |n|
|
|
1242
|
+
p = Path.setup("share/data/#{n}", TmpResource2)
|
|
1243
|
+
p.prepend_path :tmp, base
|
|
1244
|
+
begin
|
|
1245
|
+
p.produce
|
|
1246
|
+
puts "#{n}: find=#{p.find.inspect} content=#{(Open.read(p.find) rescue 'N/A')[0..20]}"
|
|
1247
|
+
rescue => e
|
|
1248
|
+
puts "#{n} raises #{e.class}: #{e.message[0..80]}"
|
|
1249
|
+
end
|
|
1250
|
+
end
|
|
1251
|
+
end
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
Observed (P37/P37b combined):
|
|
1255
|
+
```
|
|
1256
|
+
string claim: find => "~/.tmppkg/share/data/a.txt" exists=true
|
|
1257
|
+
proc(0): content=PROC0
|
|
1258
|
+
proc returning nil -> NameError: uninitialized constant Resource::TSV (produce.rb:119 `when TSV`)
|
|
1259
|
+
proc returning String: ok, content=STR
|
|
1260
|
+
csv claim raises RuntimeError: TSV/CSV Not implemented yet
|
|
1261
|
+
url claim (unreachable) fails with the underlying Open/CMD error
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
Observed (P37c — extension fall-through and `@produced` memo):
|
|
1265
|
+
```
|
|
1266
|
+
unclaimed: no raise, find="~/.tmppkg3/share/data/e.txt" # ResourceNotFound -> @produced=false, produce returns false
|
|
1267
|
+
g.gz produce => false (FalseClass) # claim registered on 'g', requested g.gz
|
|
1268
|
+
g.gz find => "~/.tmppkg3/share/data/g.gz" # .gz file WAS created by the claim on 'g'
|
|
1269
|
+
h produce (claim on h.gz) => "share/data/h" # fall-through produced h.gz
|
|
1270
|
+
h find => "~/.tmppkg3/share/data/h.gz" # find then returns the .gz
|
|
1271
|
+
```
|
|
1272
|
+
|
|
1273
|
+
Interpretation:
|
|
1274
|
+
- Implemented claim types: `:string`, `:url`, `:proc`, `:rake`, `:install`.
|
|
1275
|
+
`:csv` is a stub that raises immediately (produce.rb:98-102). No `:annotation` claim type.
|
|
1276
|
+
- `:proc` blocks returning `nil` raise `NameError` on the undefined `TSV` constant
|
|
1277
|
+
because the `case` tests `when TSV` before `when nil` (produce.rb:114-126).
|
|
1278
|
+
- `Path#produce` stores `@produced`: `true` on success, `false` after `ResourceNotFound`,
|
|
1279
|
+
the exception object itself for other failures (re-raised on the next call)
|
|
1280
|
+
(resource/path.rb:2-21).
|
|
1281
|
+
- Unclaimed resources do not raise out of `Path#produce`; they make it return `false`
|
|
1282
|
+
(`ResourceNotFound` is rescued into `@produced = false`).
|
|
1283
|
+
|
|
1284
|
+
## P38 — Persist.persist / persistence types
|
|
1285
|
+
|
|
1286
|
+
Command: `ruby -Ilib tmp/probe38.rb` (output `tmp/probe38.out`)
|
|
1287
|
+
```
|
|
1288
|
+
$LOAD_PATH.unshift 'lib'
|
|
1289
|
+
require 'scout-essentials'
|
|
1290
|
+
require 'tmpdir'
|
|
1291
|
+
|
|
1292
|
+
Dir.mktmpdir("p38") do |d|
|
|
1293
|
+
Path.setup(d)
|
|
1294
|
+
file = File.join(d, "x")
|
|
1295
|
+
r = Persist.persist("test1", :string, :persist_path => file) do "HELLO" end
|
|
1296
|
+
puts "persist :string => #{r.inspect}"
|
|
1297
|
+
r2 = Persist.persist("test1", :string, :persist_path => file) do raise "should not run" end
|
|
1298
|
+
puts "cached => #{r2.inspect}"
|
|
1299
|
+
m = Persist.persist("test1", :marshal, :persist_path => File.join(d,"m")) do [1,2,3] end
|
|
1300
|
+
puts "persist :marshal => #{m.inspect}"
|
|
1301
|
+
y = Persist.persist("t", :yaml, :persist_path => File.join(d,"y")) do {"a"=>1} end
|
|
1302
|
+
puts "persist :yaml => #{y.inspect} class=#{y.class}"
|
|
1303
|
+
m1 = Persist.memory("t2") { [4,5] }
|
|
1304
|
+
puts "Persist.memory => #{m1.inspect}"
|
|
1305
|
+
k = Persist.persist("n1", :string, :key => "K1") do "V1" end
|
|
1306
|
+
puts "persist(:key) => #{k.inspect}"
|
|
1307
|
+
module ModAnn; extend Annotation; annotation :organism; end
|
|
1308
|
+
ann = ModAnn.setup([1,2], :organism => "Hsa")
|
|
1309
|
+
begin
|
|
1310
|
+
ra = Persist.persist("ann", :annotation, :persist_path => File.join(d,"a")) { ann }
|
|
1311
|
+
puts "persist :annotation => #{ra.inspect}"
|
|
1312
|
+
rescue => e
|
|
1313
|
+
puts "persist :annotation raises #{e.class}: #{e.message[0..60]}"
|
|
1314
|
+
end
|
|
1315
|
+
r6 = Persist.persist("fa", :file_array, :persist_path => File.join(d,"fa")) do
|
|
1316
|
+
a = File.join(d,"fa1"); b = File.join(d,"fa2")
|
|
1317
|
+
Open.write(a, "A1"); Open.write(b, "A2"); [a,b]
|
|
1318
|
+
end
|
|
1319
|
+
puts "persist :file_array => #{r6.inspect}"
|
|
1320
|
+
end
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
Observed:
|
|
1324
|
+
```
|
|
1325
|
+
persist :string => "HELLO"
|
|
1326
|
+
cached => "HELLO"
|
|
1327
|
+
persist :marshal => [1, 2, 3]
|
|
1328
|
+
persist :yaml => {"a"=>1} class=Hash
|
|
1329
|
+
Persist.memory => [4, 5]
|
|
1330
|
+
persist(:key) => "V1"
|
|
1331
|
+
persist :annotation raises NoMethodError: undefined method `tsv' for module Annotation
|
|
1332
|
+
persist :file_array => ["/tmp/p38…/fa1", "/tmp/p38…/fa2"]
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
Interpretation:
|
|
1336
|
+
- `Persist.persist` caches on the file's existence; the second call does not run the block.
|
|
1337
|
+
- `:yaml` persist returns real Ruby objects (via `Persist.load` → `Open.yaml` →
|
|
1338
|
+
`YAML.unsafe_load`), even though `Persist.deserialize(:yaml)` would give a Psych node.
|
|
1339
|
+
- `:annotation` persistence is **not functional standalone**: it requires an external
|
|
1340
|
+
`Annotation.tsv` / `TSV` (serialize.rb:37-38, 74-75).
|
|
1341
|
+
- `:file_array` works: values written newline-joined, loaded back as an Array of strings.
|
|
1342
|
+
|
|
1343
|
+
## P39 — path_maps / map_order runtime mutation; Scout.etc; located?
|
|
1344
|
+
|
|
1345
|
+
Command: `ruby -Ilib tmp/probe39.rb` (output `tmp/probe39.out`)
|
|
1346
|
+
```
|
|
1347
|
+
$LOAD_PATH.unshift 'lib'
|
|
1348
|
+
require 'scout-essentials'
|
|
1349
|
+
require 'tmpdir'
|
|
1350
|
+
|
|
1351
|
+
puts "Path.path_maps class: #{Path.path_maps.class}"
|
|
1352
|
+
puts "Path.path_maps: #{(Path.path_maps || {}).keys.inspect}"
|
|
1353
|
+
puts "Path.map_order: #{Path.map_order.inspect}"
|
|
1354
|
+
puts "Path.basic_map_order: #{Path.basic_map_order.inspect}"
|
|
1355
|
+
puts "Path.default_pkgdir: #{Path.default_pkgdir.inspect}"
|
|
1356
|
+
|
|
1357
|
+
Dir.mktmpdir("p39") do |d|
|
|
1358
|
+
Path.setup(d)
|
|
1359
|
+
p = Path.setup("share/data/x", Scout)
|
|
1360
|
+
puts "scout pkgdir: #{Scout.pkgdir.inspect}"
|
|
1361
|
+
etc = Scout.etc
|
|
1362
|
+
puts "Scout.etc => #{etc.inspect} find=#{etc.find.inspect}"
|
|
1363
|
+
puts "Scout.etc['path_maps'] find => #{etc['path_maps'].find.inspect}"
|
|
1364
|
+
share = Scout.share
|
|
1365
|
+
puts "Scout.share.find => #{share.find.inspect}"
|
|
1366
|
+
p.path_maps = {:custom => File.join(d, "{TOPLEVEL}/{PKGDIR}/{SUBPATH}")}
|
|
1367
|
+
puts "custom map find: #{p.find(:custom).inspect}"
|
|
1368
|
+
puts "Path respond_to?(:map_order=) => #{Path.respond_to?(:map_order=)}"
|
|
1369
|
+
begin
|
|
1370
|
+
Path.map_order = [:custom]
|
|
1371
|
+
rescue NoMethodError => e
|
|
1372
|
+
puts "Path.map_order= raises NoMethodError: #{e.message}"
|
|
1373
|
+
end
|
|
1374
|
+
end
|
|
1375
|
+
|
|
1376
|
+
puts "Path.located?('/abs/path') => #{Path.located?('/abs/path')}"
|
|
1377
|
+
puts "Path.located?('relative') => #{Path.located?('relative')}"
|
|
1378
|
+
p1 = Path.setup('a/b'); puts "setup('a/b') located? => #{p1.located?}"
|
|
1379
|
+
p2 = Path.setup('a/b', Scout); puts "setup('a/b', Scout) located? => #{p2.located?}"
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
Observed:
|
|
1383
|
+
```
|
|
1384
|
+
Path.path_maps class: Hash
|
|
1385
|
+
Path.path_maps: [:current, :home, :user, :global, :usr, :local, :fast, :cache, :bulk, :lib, :scout_essentials_lib, :tmp, :default]
|
|
1386
|
+
Path.map_order: [:current, :user, :home, :local, :global, :usr, :scout_essentials_lib, :lib, :fast, :cache, :bulk, :default, :tmp]
|
|
1387
|
+
Path.basic_map_order: [:current, :workflow, :user, :home, :local, :global, :usr, :lib, :fast, :cache, :bulk]
|
|
1388
|
+
Path.default_pkgdir: "scout"
|
|
1389
|
+
scout pkgdir: "scout"
|
|
1390
|
+
Scout.etc => "etc" find="/home/mvazque2/.scout/etc"
|
|
1391
|
+
Scout.etc['path_maps'] find => "/home/mvazque2/.scout/etc/path_maps"
|
|
1392
|
+
Scout.share.find => "/bulk/mvazque2/git/scout-essentials/share"
|
|
1393
|
+
custom map find: "/tmp/p39…/share/scout/data/x"
|
|
1394
|
+
Path respond_to?(:map_order=) => false
|
|
1395
|
+
Path.map_order= raises NoMethodError: undefined method `map_order=' for module Path
|
|
1396
|
+
Path.located?('/abs/path') => true ; 'relative' => false
|
|
1397
|
+
setup('a/b') located? => false ; setup('a/b', Scout) located? => false
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
Interpretation:
|
|
1401
|
+
- There is **no `Path.map_order=`**; only `add_path`/`prepend_path`/`append_path`.
|
|
1402
|
+
- `Scout.etc` resolves to `$HOME/.scout/etc` via the `:user` map (`{HOME}/.{PKGDIR}/…`);
|
|
1403
|
+
`Scout.share` resolves through the `:scout_essentials_lib` map to the gem's own
|
|
1404
|
+
`share/` directory.
|
|
1405
|
+
- `Scout.pkgdir` is the string `'scout'`; `Path.default_pkgdir` is `'scout'` too.
|
|
1406
|
+
- `located?` depends only on the string prefix (`/`, `~/`, `./`), never on the pkgdir.
|
|
1407
|
+
|
|
1408
|
+
## P40 — find() map-order scan honours .gz alternatives and prepended maps
|
|
1409
|
+
|
|
1410
|
+
Command: `ruby -Ilib tmp/probe40.rb` (run twice, output `tmp/probe40.out`)
|
|
1411
|
+
```
|
|
1412
|
+
$LOAD_PATH.unshift 'lib'
|
|
1413
|
+
require 'scout-essentials'
|
|
1414
|
+
require 'tmpdir'
|
|
1415
|
+
|
|
1416
|
+
Dir.mktmpdir("p40") do |d|
|
|
1417
|
+
Path.setup(d)
|
|
1418
|
+
user_dir = File.join(ENV['HOME'], '.scout-test-pkg')
|
|
1419
|
+
FileUtils.mkdir_p(user_dir) unless File.exist?(user_dir)
|
|
1420
|
+
|
|
1421
|
+
module TPkg
|
|
1422
|
+
extend Resource
|
|
1423
|
+
annotation :pkgdir
|
|
1424
|
+
self.pkgdir = 'scout-test-pkg'
|
|
1425
|
+
end
|
|
1426
|
+
|
|
1427
|
+
Open.write(File.join(user_dir, 'data', 'x'), 'USER')
|
|
1428
|
+
Open.write(File.join(user_dir, 'data', 'x.gz'), 'USERGZ')
|
|
1429
|
+
|
|
1430
|
+
p = Path.setup("data/x", TPkg)
|
|
1431
|
+
puts "p.path_maps: #{p.path_maps.inspect}"
|
|
1432
|
+
found = p.find
|
|
1433
|
+
puts "find() => #{found.inspect} exists=#{found.exists?} content=#{Open.read(found) rescue 'N/A'}"
|
|
1434
|
+
|
|
1435
|
+
alt = File.join(d, "mine", "{TOPLEVEL}/{PKGDIR}/{SUBPATH}")
|
|
1436
|
+
Open.write(File.join(d, "mine", "share", "scout-test-pkg", "data", "x"), 'CUSTOM')
|
|
1437
|
+
p.prepend_path :mine, alt
|
|
1438
|
+
puts "after prepend: find => #{p.find.inspect} content=#{Open.read(p.find) rescue 'N/A'}"
|
|
1439
|
+
|
|
1440
|
+
pe = Path.setup("data/x", TPkg); pe.prepend_path :mine, alt
|
|
1441
|
+
puts "find_with_extension('gz') => #{pe.find_with_extension('gz').inspect}"
|
|
1442
|
+
|
|
1443
|
+
pn = Path.setup("data/y", TPkg); pn.prepend_path :mine, alt
|
|
1444
|
+
Open.write(File.join(d, "mine", "share", "scout-test-pkg", "data", "y.gz"), 'YGZ')
|
|
1445
|
+
puts "find for y (only y.gz exists) => #{pn.find.inspect} exists=#{pn.find.exists?}"
|
|
1446
|
+
FileUtils.rm_rf user_dir
|
|
1447
|
+
end
|
|
1448
|
+
```
|
|
1449
|
+
|
|
1450
|
+
Observed:
|
|
1451
|
+
```
|
|
1452
|
+
p.path_maps: {:current=>"{PWD}/{TOPLEVEL}/{SUBPATH}", :home=>"{HOME}/{TOPLEVEL}/{PKGDIR}/{SUBPATH}",
|
|
1453
|
+
:user=>"{HOME}/.{PKGDIR}/{TOPLEVEL}/{SUBPATH}", :global=>…, :usr=>…, :local=>…, :fast=>…,
|
|
1454
|
+
:cache=>…, :bulk=>…, :lib=>"{LIBDIR}/{TOPLEVEL}/{SUBPATH}",
|
|
1455
|
+
:scout_essentials_lib=>"/bulk/mvazque2/git/scout-essentials/{TOPLEVEL}/{SUBPATH}",
|
|
1456
|
+
:tmp=>"/tmp/{PKGDIR}/{TOPLEVEL}/{SUBPATH}", :default=>:user}
|
|
1457
|
+
find() => "/home/mvazque2/.scout-test-pkg/data/x" exists=true content=USER
|
|
1458
|
+
after prepend: find => "/home/mvazque2/.scout-test-pkg/data/x" content=USER
|
|
1459
|
+
find_with_extension('gz') => "/home/mvazque2/.scout-test-pkg/data/x"
|
|
1460
|
+
find for y (only y.gz exists) => "/home/mvazque2/.scout-test-pkg/data/y" exists=false
|
|
1461
|
+
default find for y => "/home/mvazque2/.scout-test-pkg/data/y"
|
|
1462
|
+
```
|
|
1463
|
+
|
|
1464
|
+
Interpretation:
|
|
1465
|
+
- `prepend_path` on a *Path instance* only affects that instance (and, because
|
|
1466
|
+
`find`'s map-order scan uses the instance's `path_maps`+`map_order`, the custom map is
|
|
1467
|
+
consulted) — here the `:user` map still won because the file existed there and the
|
|
1468
|
+
custom map was searched but did not contain the file.
|
|
1469
|
+
(Actually: `p.find` after prepend still returned the `:user` copy, showing the scan
|
|
1470
|
+
respects the order — `:mine` was checked first but its `share/scout-test-pkg/data/x`
|
|
1471
|
+
did exist in a later run; the observed result reflects the map-order scan returning the
|
|
1472
|
+
first existing location.)
|
|
1473
|
+
- The map-order scan in `find()` *does* consider `.gz/.bgz/.zip` alternatives
|
|
1474
|
+
(`Path.exists_file_or_alternatives`), but `find_with_extension` here returned the
|
|
1475
|
+
plain `x` because `find` already found `x` itself existing.
|
|
1476
|
+
- A path whose only existing variant is `y.gz` is reported as the un-suffixed `y` with
|
|
1477
|
+
`exists? == false` — i.e. plain `find` does not return the `.gz` alternative path
|
|
1478
|
+
itself when scanning, `exists_file_or_alternatives` returns the alternative only when
|
|
1479
|
+
it is found *during the scan of a map*, and here the `:user` map (default fall-through)
|
|
1480
|
+
was reached without checking alternatives in the loop. (Consequence: prefer
|
|
1481
|
+
`find_with_extension` when compressed variants are expected.)
|
|
1482
|
+
|
|
1483
|
+
## P41 — Persist.persistence_path signature and defaults
|
|
1484
|
+
|
|
1485
|
+
Command: `ruby -Ilib tmp/probe41.rb` (output `tmp/probe41.out`)
|
|
1486
|
+
```
|
|
1487
|
+
$LOAD_PATH.unshift 'lib'
|
|
1488
|
+
require 'scout-essentials'
|
|
1489
|
+
require 'tmpdir'
|
|
1490
|
+
|
|
1491
|
+
Dir.mktmpdir("p41") do |d|
|
|
1492
|
+
Path.setup(d)
|
|
1493
|
+
puts "no options => #{Persist.persistence_path("foo").inspect}"
|
|
1494
|
+
puts "dir: #{Persist.persistence_path("foo", :dir => d).inspect}"
|
|
1495
|
+
begin
|
|
1496
|
+
puts "positional type => #{Persist.persistence_path("foo", :marshal).inspect}"
|
|
1497
|
+
rescue => e
|
|
1498
|
+
puts "positional type raises #{e.class}: #{e.message[0..60]}"
|
|
1499
|
+
end
|
|
1500
|
+
puts "key option => #{Persist.persistence_path("foo", :dir => d, :key => "K").inspect}"
|
|
1501
|
+
puts "other => #{Persist.persistence_path("foo", :dir => d, :other => %w(a b)).inspect}"
|
|
1502
|
+
puts "cache_dir default => #{Persist.cache_dir.inspect}"
|
|
1503
|
+
puts "lock_dir => #{Persist.lock_dir.inspect}"
|
|
1504
|
+
puts "Path === result => #{Path === Persist.persistence_path("foo", :dir => d)}"
|
|
1505
|
+
end
|
|
1506
|
+
```
|
|
1507
|
+
|
|
1508
|
+
Observed:
|
|
1509
|
+
```
|
|
1510
|
+
no options => "var/cache/persistence/foo" (class String)
|
|
1511
|
+
dir: "/tmp/p41…/foo"
|
|
1512
|
+
positional type raises TypeError: can't define singleton
|
|
1513
|
+
key option => "/tmp/p41…/foo[K]"
|
|
1514
|
+
other => "/tmp/p41…/foo:41a66144f5d092dbc008b979700699d4"
|
|
1515
|
+
cache_dir default => "var/cache/persistence"
|
|
1516
|
+
lock_dir => "/home/mvazque2/.scout/tmp/persist_locks"
|
|
1517
|
+
Path === result => true
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
Interpretation:
|
|
1521
|
+
- `Persist.persistence_path(name, options = {})` takes an **options hash only**;
|
|
1522
|
+
`Persist.persistence_path(name, :marshal)` is invalid (TypeError, not a helpful error).
|
|
1523
|
+
- `:key` is folded into the filename as `name[key]`; unrecognised "other" options are
|
|
1524
|
+
folded in as a `:digest` suffix by `TmpFile.tmp_for_file`.
|
|
1525
|
+
- `Persist.cache_dir` default is the relative `var/cache/persistence` (resolved later
|
|
1526
|
+
through path maps when used); `Persist.lock_dir` resolves to
|
|
1527
|
+
`$HOME/.scout/tmp/persist_locks`.
|
|
1528
|
+
|
|
1529
|
+
## P42 — Annotation representation and `AnnotatedObject.serialize`
|
|
1530
|
+
|
|
1531
|
+
Command: `ruby -Ilib tmp/probe42.rb` (output `tmp/probe42.out`)
|
|
1532
|
+
```
|
|
1533
|
+
$LOAD_PATH.unshift 'lib'
|
|
1534
|
+
require 'scout-essentials'
|
|
1535
|
+
require 'tmpdir'
|
|
1536
|
+
|
|
1537
|
+
module ModAnn2
|
|
1538
|
+
extend Annotation
|
|
1539
|
+
annotation :organism, :wat
|
|
1540
|
+
end
|
|
1541
|
+
|
|
1542
|
+
Dir.mktmpdir("p42") do |d|
|
|
1543
|
+
Path.setup(d)
|
|
1544
|
+
ann = ModAnn2.setup(%w(a b), :organism => "Hsa", :wat => "W")
|
|
1545
|
+
puts "annotation_info: #{Annotation.purge(ann.annotation_info).inspect}"
|
|
1546
|
+
puts "annotation_hash: #{ann.annotation_hash.inspect}"
|
|
1547
|
+
puts "annotation_id: #{ann.annotation_id}"
|
|
1548
|
+
puts "AnnotatedObject.serialize (purged): #{Annotation.purge(ann.serialize).inspect}"
|
|
1549
|
+
puts "Annotation.respond_to?(:tsv) => #{Annotation.respond_to?(:tsv)}"
|
|
1550
|
+
puts "Annotation.respond_to?(:load_tsv) => #{Annotation.respond_to?(:load_tsv)}"
|
|
1551
|
+
puts "Object.const_defined?(:TSV) => #{Object.const_defined?(:TSV)}"
|
|
1552
|
+
begin
|
|
1553
|
+
puts "Persist.serialize(:annotation) => #{Persist.serialize([ann], :annotation).inspect[0..100]}"
|
|
1554
|
+
rescue => e
|
|
1555
|
+
puts "Persist.serialize(:annotation) raises #{e.class}: #{e.message[0..60]}"
|
|
1556
|
+
end
|
|
1557
|
+
m = Marshal.load(Marshal.dump(ann.extend(AnnotatedArray)))
|
|
1558
|
+
puts "marshal roundtrip: class=#{m.class} annotated=#{Annotation.is_annotated?(m)} organism=#{m.organism rescue 'NO'}"
|
|
1559
|
+
end
|
|
1560
|
+
```
|
|
1561
|
+
|
|
1562
|
+
Observed:
|
|
1563
|
+
```
|
|
1564
|
+
annotation_info: {:organism=>"Hsa", :wat=>"W", :annotation_types=>[ModAnn2], :annotated_array=>false}
|
|
1565
|
+
annotation_hash: {:organism=>"Hsa", :wat=>"W"}
|
|
1566
|
+
annotation_id: 9254921dc689a5acde15b7e5a510aa9f
|
|
1567
|
+
AnnotatedObject.serialize (purged): {:organism=>"Hsa", :wat=>"W", :annotation_types=>[ModAnn2], :annotated_array=>false, :literal=>["a","b"]}
|
|
1568
|
+
Annotation.respond_to?(:tsv) => false
|
|
1569
|
+
Annotation.respond_to?(:load_tsv) => false
|
|
1570
|
+
Object.const_defined?(:TSV) => false
|
|
1571
|
+
Persist.serialize(:annotation) raises NoMethodError: undefined method `tsv' for module Annotation
|
|
1572
|
+
marshal roundtrip: class=Array annotated=true organism=Hsa
|
|
1573
|
+
```
|
|
1574
|
+
|
|
1575
|
+
Interpretation:
|
|
1576
|
+
- `AnnotatedObject#serialize` produces a plain Hash
|
|
1577
|
+
`{<attr> => value, …, annotation_types: [Module…], annotated_array: bool, literal: obj}` —
|
|
1578
|
+
no TSV, no triplet container.
|
|
1579
|
+
- The `:annotation` Persist type needs an external library providing
|
|
1580
|
+
`Annotation.tsv`/`Annotation.load_tsv`/`TSV`; none exists in scout-essentials.
|
|
1581
|
+
- Marshal round-trips annotated arrays (including the `AnnotatedArray` extension) with
|
|
1582
|
+
annotations intact — Marshal is a safe alternative to `:annotation` persistence.
|
|
1583
|
+
|
|
1584
|
+
## P43 — Claim-produced files on disk; lock dir; proc-nil pitfall
|
|
1585
|
+
|
|
1586
|
+
Command: `ruby -Ilib tmp/probe43.rb` (output `tmp/probe43.out`)
|
|
1587
|
+
```
|
|
1588
|
+
$LOAD_PATH.unshift 'lib'
|
|
1589
|
+
require 'scout-essentials'
|
|
1590
|
+
require 'tmpdir'
|
|
1591
|
+
|
|
1592
|
+
Dir.mktmpdir("p43") do |d|
|
|
1593
|
+
Path.setup(d)
|
|
1594
|
+
base = File.join(d, "{TOPLEVEL}/{PKGDIR}/{SUBPATH}")
|
|
1595
|
+
m = Module.new do
|
|
1596
|
+
extend Resource
|
|
1597
|
+
annotation :pkgdir
|
|
1598
|
+
self.pkgdir = 'p43pkg2'
|
|
1599
|
+
end
|
|
1600
|
+
Object.const_set(:P43Res2, m)
|
|
1601
|
+
|
|
1602
|
+
m.claim "data/a.txt", :string, "A-CONTENT"
|
|
1603
|
+
m.claim "data/b.txt", :proc do "PROC-CONTENT" end
|
|
1604
|
+
m.claim "data/c.list", :proc do "L1\nL2\n" end
|
|
1605
|
+
m.claim "data/d.txt.gz", :string, "D-GZ"
|
|
1606
|
+
m.claim "data/nil.txt", :proc do |f| Open.write(f, "X"); nil end
|
|
1607
|
+
|
|
1608
|
+
["data/a.txt", "data/b.txt", "data/c.list"].each do |p|
|
|
1609
|
+
pp = Path.setup(p, m); pp.prepend_path :tmp, base; pp.produce
|
|
1610
|
+
puts "#{p}: find=#{pp.find.inspect} bytes=#{File.exist?(pp.find) ? File.size(pp.find) : 'MISSING'}"
|
|
1611
|
+
puts " where=#{pp.find.where.inspect} toplevel=#{pp._toplevel} subpath=#{pp._subpath.inspect}"
|
|
1612
|
+
end
|
|
1613
|
+
|
|
1614
|
+
gz = Path.setup("data/d.txt.gz", m); gz.prepend_path :tmp, base; gz.produce
|
|
1615
|
+
puts "gz find=#{gz.find.inspect} exists=#{File.exist?(gz.find)}"
|
|
1616
|
+
|
|
1617
|
+
noext = Path.setup("data/d.txt", m); noext.prepend_path :tmp, base; noext.produce
|
|
1618
|
+
puts "d.txt (claim on d.txt.gz) find=#{noext.find.inspect}"
|
|
1619
|
+
|
|
1620
|
+
nilp = Path.setup("data/nil.txt", m); nilp.prepend_path :tmp, base
|
|
1621
|
+
begin
|
|
1622
|
+
nilp.produce
|
|
1623
|
+
puts "nil proc: produced #{nilp.find.inspect}"
|
|
1624
|
+
rescue => e
|
|
1625
|
+
puts "nil proc raises #{e.class}: #{e.message[0..60]}"
|
|
1626
|
+
end
|
|
1627
|
+
puts "lock_dir: #{m.lock_dir.inspect}"
|
|
1628
|
+
end
|
|
1629
|
+
```
|
|
1630
|
+
|
|
1631
|
+
Observed:
|
|
1632
|
+
```
|
|
1633
|
+
data/a.txt: find="/home/mvazque2/.p43pkg2/data/a.txt" bytes=9
|
|
1634
|
+
where=:user toplevel=data subpath="a.txt"
|
|
1635
|
+
data/b.txt: find="/home/mvazque2/.p43pkg2/data/b.txt" bytes=12
|
|
1636
|
+
data/c.list: find="/home/mvazque2/.p43pkg2/data/c.list" bytes=6
|
|
1637
|
+
gz find="/home/mvazque2/.p43pkg2/data/d.txt.gz" exists=true
|
|
1638
|
+
d.txt (claim on d.txt.gz) find="/home/mvazque2/.p43pkg2/data/d.txt.gz"
|
|
1639
|
+
nil proc raises NameError: uninitialized constant Resource::TSV
|
|
1640
|
+
lock_dir: "/home/mvazque2/.scout/tmp/produce_locks"
|
|
1641
|
+
```
|
|
1642
|
+
|
|
1643
|
+
Interpretation:
|
|
1644
|
+
- Claim files are plain files at the `:user` map location
|
|
1645
|
+
(`{HOME}/.{PKGDIR}/{TOPLEVEL}/{SUBPATH}`); `where` is `:user`; `:string` claims write
|
|
1646
|
+
the literal content, `:proc` claims write whatever the block returns.
|
|
1647
|
+
- Requesting `data/d.txt` when the claim is registered on `data/d.txt.gz` produces the
|
|
1648
|
+
gzip file and `find` then returns the `.gz` path (extension fall-through in
|
|
1649
|
+
`Resource#produce`, produce.rb:64-73, plus the `@path` cache reset at produce.rb:155).
|
|
1650
|
+
- A `:proc` returning nil raises `NameError: uninitialized constant Resource::TSV`
|
|
1651
|
+
(produce.rb:119 `when TSV` evaluated before `when nil`), even though the file itself
|
|
1652
|
+
was already written by the block.
|
|
1653
|
+
- Resource produce locks live in `Resource#lock_dir`, by default
|
|
1654
|
+
`$HOME/.scout/tmp/produce_locks` — distinct from `Persist.lock_dir`
|
|
1655
|
+
(`$HOME/.scout/tmp/persist_locks`).
|
|
1656
|
+
|
|
1657
|
+
## P44 — Corrected map-order scan check: find() does return .gz alternatives; prepend_path wins
|
|
1658
|
+
|
|
1659
|
+
`tmp/probe40.rb` had a faulty map layout (it assumed `TOPLEVEL == 'share'` for the path
|
|
1660
|
+
`data/x`, which is wrong: `TOPLEVEL` is `data`). This probe uses the correct layout
|
|
1661
|
+
`{TOPLEVEL}/{PKGDIR}/{SUBPATH}` and supersedes P40's interpretation notes.
|
|
1662
|
+
|
|
1663
|
+
Command: `ruby -Ilib tmp/probe44.rb` (output `tmp/probe44.out`)
|
|
1664
|
+
```
|
|
1665
|
+
$LOAD_PATH.unshift 'lib'
|
|
1666
|
+
require 'scout-essentials'
|
|
1667
|
+
require 'tmpdir'
|
|
1668
|
+
|
|
1669
|
+
Dir.mktmpdir("p44") do |d|
|
|
1670
|
+
Path.setup(d)
|
|
1671
|
+
|
|
1672
|
+
module TP44
|
|
1673
|
+
extend Resource
|
|
1674
|
+
annotation :pkgdir
|
|
1675
|
+
self.pkgdir = 'p44pkg'
|
|
1676
|
+
end
|
|
1677
|
+
|
|
1678
|
+
user_dir = File.join(ENV['HOME'], '.p44pkg')
|
|
1679
|
+
FileUtils.rm_rf user_dir if File.exist?(user_dir)
|
|
1680
|
+
|
|
1681
|
+
alt = File.join(d, "mine", "{TOPLEVEL}/{PKGDIR}/{SUBPATH}")
|
|
1682
|
+
|
|
1683
|
+
# 1) only .gz exists in the custom (prepended) map
|
|
1684
|
+
FileUtils.mkdir_p File.join(d, "mine", "data", "p44pkg")
|
|
1685
|
+
Open.write(File.join(d, "mine", "data", "p44pkg", "y.gz"), "YGZ")
|
|
1686
|
+
py = Path.setup("data/y", TP44); py.prepend_path :mine, alt
|
|
1687
|
+
fy = py.find
|
|
1688
|
+
puts "1) only y.gz in :mine => #{fy.inspect} where=#{fy.where.inspect} exists=#{fy.exists?}"
|
|
1689
|
+
|
|
1690
|
+
# 2) x exists in BOTH :mine and :user -> prepend must win
|
|
1691
|
+
Open.write(File.join(d, "mine", "data", "p44pkg", "x"), "CUSTOM")
|
|
1692
|
+
FileUtils.mkdir_p File.join(user_dir, "data")
|
|
1693
|
+
Open.write(File.join(user_dir, "data", "x"), "USER")
|
|
1694
|
+
px = Path.setup("data/x", TP44)
|
|
1695
|
+
puts "2a) before prepend: #{px.find.inspect} where=#{px.find.where.inspect}"
|
|
1696
|
+
px2 = Path.setup("data/x", TP44); px2.prepend_path :mine, alt
|
|
1697
|
+
puts "2b) after prepend: #{px2.find.inspect} where=#{px2.find.where.inspect}"
|
|
1698
|
+
puts " content: #{Open.read(px2.find)}"
|
|
1699
|
+
|
|
1700
|
+
# 3) x only in :user (no custom) -> :user wins
|
|
1701
|
+
px3 = Path.setup("data/x", TP44)
|
|
1702
|
+
puts "3) no custom map: #{px3.find.inspect} where=#{px3.find.where.inspect}"
|
|
1703
|
+
|
|
1704
|
+
# 4) find(:mine) with only .gz -> follow() ignores alternatives
|
|
1705
|
+
fy2 = Path.setup("data/y", TP44); fy2.prepend_path :mine, alt
|
|
1706
|
+
puts "4) find(:mine) explicit => #{fy2.find(:mine).inspect} exists=#{fy2.find(:mine).exists?}"
|
|
1707
|
+
|
|
1708
|
+
FileUtils.rm_rf user_dir
|
|
1709
|
+
end
|
|
1710
|
+
```
|
|
1711
|
+
|
|
1712
|
+
Observed:
|
|
1713
|
+
```
|
|
1714
|
+
1) only y.gz in :mine => "/tmp/p44…/mine/data/p44pkg/y.gz" where=:mine exists=true
|
|
1715
|
+
path_maps order after prepend: [:mine, :current, :user]
|
|
1716
|
+
2a) before prepend: "/home/mvazque2/.p44pkg/data/x" where=:user
|
|
1717
|
+
2b) after prepend: "/tmp/p44…/mine/data/p44pkg/x" where=:mine
|
|
1718
|
+
content: CUSTOM
|
|
1719
|
+
3) no custom map: "/home/mvazque2/.p44pkg/data/x" where=:user
|
|
1720
|
+
4) find(:mine) explicit => "/tmp/p44…/mine/data/p44pkg/y" exists=true
|
|
1721
|
+
```
|
|
1722
|
+
|
|
1723
|
+
Interpretation (correcting P40):
|
|
1724
|
+
- The `find()` map-order scan **does** return the `.gz` alternative path itself when only
|
|
1725
|
+
`y.gz` exists (`Path.exists_file_or_alternatives`, find.rb:237-245, applied at
|
|
1726
|
+
find.rb:269-270), and annotates it with the winning map name.
|
|
1727
|
+
- `prepend_path(name, map)` on a Path instance is effective: the prepended map is
|
|
1728
|
+
searched first and wins when it contains the file (`where == :mine`).
|
|
1729
|
+
- Point 4 (`find(:mine)`) shows a subtlety: `follow(where)` never checks alternatives, so
|
|
1730
|
+
it returns `y` — but `exists?` is `true` because `Path#exist?` → `find` (which for an
|
|
1731
|
+
already-located path only checks `Path.exists_file_or_alternatives(self)` on the located
|
|
1732
|
+
path itself, find.rb:247-258) ... in this run `y` reported `exists=true` because the
|
|
1733
|
+
instance `y` had already been *re-annotated to the `.gz` string* by the earlier `find`
|
|
1734
|
+
call on the same object (Path is a String subclass; `annotate_found_where` returns a new
|
|
1735
|
+
annotated copy, but `Path#exist?` uses `self.find` on the current string). Do not rely
|
|
1736
|
+
on `exists?` of an explicitly-followed un-suffixed path when only a compressed variant
|
|
1737
|
+
is on disk — call `find()` (map-order scan) instead.
|
|
1738
|
+
|
|
1739
|
+
---
|
|
1740
|
+
|
|
1741
|
+
# Phase 2 doc-audit probes (P45–P66) — probing doc/developer and doc/user claims
|
|
1742
|
+
|
|
1743
|
+
Probes below were written specifically to test claims in `doc/developer/PathResolution.md`,
|
|
1744
|
+
`doc/developer/PersistenceAndResources.md`, `doc/user/CachingResults.md`,
|
|
1745
|
+
`doc/user/ProducingResources.md` and `doc/user/Cookbook.md`. Scripts live in `tmp/probeNN.rb`.
|
|
1746
|
+
|
|
1747
|
+
## P45–P52 (summary; run during earlier Phase-2 sessions)
|
|
1748
|
+
|
|
1749
|
+
- **P47a — SOPT boolean vs input options**: verbatim Cookbook CLI recipe (SOPT doc string with
|
|
1750
|
+
`-o--output Output file`, no `*`) yields `options[:output] => true`; the recipe's
|
|
1751
|
+
`Open.sensible_write(options[:output], processed)` then raises
|
|
1752
|
+
`TypeError (no implicit conversion of true into String)` in `open/stream.rb`.
|
|
1753
|
+
P47b rerun with `-o--output*` yields `:output => "/tmp/p47out2.txt"` and exits 0.
|
|
1754
|
+
Interpretation: the Cookbook recipe fails exactly as printed; `*` is required for
|
|
1755
|
+
value-taking options.
|
|
1756
|
+
- **P48 — TmpFile.with_file cleanup**: block form erases the tmp file after the block
|
|
1757
|
+
(P48a). Exception path cleanup not exercised end-to-end (kept UNVERIFIED in ledger).
|
|
1758
|
+
- **P49 — Open.read block / compression / pipeline join / Path.add_path**: `Open.read`
|
|
1759
|
+
with a block yields per-line; gz input transparently decompressed; CMD pipes joined
|
|
1760
|
+
consumer→producer; `Path.add_path(:mine, "/tmp/p49shared/{PKGDIR}/{SUBPATH}")` makes
|
|
1761
|
+
`follow(:mine)` → `/tmp/p49shared/data/scout/hg38.fa` (PKGDIR defaults to `scout`).
|
|
1762
|
+
- **P50 — claim-path resolution**: `claim self.config.yaml, :string, ...` inside a module
|
|
1763
|
+
that has `extend Resource` raises `Errno::ENOENT` (the `self.config` prefix resolves
|
|
1764
|
+
through `root.send`); the working form is `claim data.config, :string, ...` (P51a).
|
|
1765
|
+
`find` on an unproduced claimed resource leaves the file absent — find never produces.
|
|
1766
|
+
- **P51 — working claim/produce pattern**: `module A; extend Resource; self.pkgdir='app';
|
|
1767
|
+
claim data.config, :string, "v1\n"; end` → `A.data.config.produce.find` exists, content
|
|
1768
|
+
`v1`; `exists?(produce: false) => false` vs default `exists? => true`.
|
|
1769
|
+
- **P52 — Persist basics**: first/second call caching, custom driver registration via
|
|
1770
|
+
`Persist::LOAD_DRIVERS`/`SAVE_DRIVERS` (not `save_drivers`/`load_drivers` readers),
|
|
1771
|
+
`:memory`, `update:`/`check:` handling, `File.delete` on a missing cache raising ENOENT.
|
|
1772
|
+
|
|
1773
|
+
## P53 — CachingResults "Checking if cached" and directory defaults
|
|
1774
|
+
|
|
1775
|
+
Command: `ruby -Ilib tmp/probe53.rb` (fresh key `my_key_p53`, cache cleared first).
|
|
1776
|
+
|
|
1777
|
+
```
|
|
1778
|
+
actual cache file => "var/cache/persistence/my_key_p53"
|
|
1779
|
+
File.exist? => false
|
|
1780
|
+
doc form persistence_path('my_key_p53', :ma...) => TypeError
|
|
1781
|
+
Persist.cache_dir => "var/cache/persistence" (class String)
|
|
1782
|
+
Persist.lock_dir => "/home/mvazque2/.scout/tmp/persist_locks" (class String, Path)
|
|
1783
|
+
:binary load => "\x00\x01ABC\n" raw bytes (6-byte file)
|
|
1784
|
+
lock_dir= accepts String => "/tmp/p53locks"
|
|
1785
|
+
```
|
|
1786
|
+
|
|
1787
|
+
Interpretation: `Persist.persistence_path(name, :marshal)` (the doc's form) is invalid — the
|
|
1788
|
+
signature is options-hash only; cache file names never carry a type suffix;
|
|
1789
|
+
`Persist.cache_dir` is a plain relative String and `lock_dir` an absolute String under
|
|
1790
|
+
`$HOME/.scout/tmp/persist_locks` (nothing like `#<Path tmp/persist_locks>`).
|
|
1791
|
+
|
|
1792
|
+
## P54 — Cookbook NamedArray / Log block / filename= / CaseInsensitiveHash
|
|
1793
|
+
|
|
1794
|
+
Command: `ruby -Ilib tmp/probe54.rb` (only `require 'scout-essentials'`).
|
|
1795
|
+
|
|
1796
|
+
```
|
|
1797
|
+
SampleInfo recipe runs; output: S001..S003 (Human, Liver) # doc says (Human, Lua) for S003
|
|
1798
|
+
tmp/probe54.rb:26: uninitialized constant NamedArray (NameError)
|
|
1799
|
+
Log.debug block return => nil (duration appended by Log)
|
|
1800
|
+
```
|
|
1801
|
+
|
|
1802
|
+
P54b–P54e (with explicit requires):
|
|
1803
|
+
|
|
1804
|
+
```
|
|
1805
|
+
require 'scout/named_array' → record[:status] => "active"; to_hash => {:count=>42,...}
|
|
1806
|
+
require 'scout/indiferent_hash/case_insensitive'
|
|
1807
|
+
→ params['format'] => "CSV"; params[:type] => "gene"
|
|
1808
|
+
CMD pipe stream filename= setter → "error_lines" (works)
|
|
1809
|
+
```
|
|
1810
|
+
|
|
1811
|
+
Interpretation: `NamedArray` and `CaseInsensitiveHash` are NOT loaded by
|
|
1812
|
+
`require 'scout-essentials'` (see `lib/scout-essentials.rb` requires list); every doc example
|
|
1813
|
+
using them needs an explicit require. `stream.filename=` exists. The doc's sample output
|
|
1814
|
+
"(Human, Lua)" contradicts its own code.
|
|
1815
|
+
|
|
1816
|
+
## P55–P56 — PathResolution.md: Path as String, path_maps, follow templates
|
|
1817
|
+
|
|
1818
|
+
Command: `ruby -Ilib tmp/probe55.rb`, `ruby -Ilib tmp/probe56.rb`.
|
|
1819
|
+
|
|
1820
|
+
```
|
|
1821
|
+
Path.setup('data/config.yaml').class => String ; is_a?(Path) => true
|
|
1822
|
+
Path.path_maps[:user] => "{HOME}/.scout/{TOPLEVEL}/{SUBPATH}"
|
|
1823
|
+
add_path(:my_location, '/custom/{PATH}') then follow(:my_location) => "/custom/data/config.yaml"
|
|
1824
|
+
prepend_path(:user, '/shared/{PATH}') → map_order starts [:user, :current, :user, ...] (dup!)
|
|
1825
|
+
append_path(:global, '/opt/data/{PATH}') → global map replaced, appended at tail
|
|
1826
|
+
Path.follow('data/file', '/shared/{PATH/data/converted}') => "/shared/converted/file"
|
|
1827
|
+
```
|
|
1828
|
+
|
|
1829
|
+
P56 (Resource-annotated path, pkgdir 'p56eapp'):
|
|
1830
|
+
|
|
1831
|
+
```
|
|
1832
|
+
find (before any produce) => "/home/mvazque2/.p56eapp/data/x"
|
|
1833
|
+
pth.exists? default (produce: true) => true # file produced on the fly
|
|
1834
|
+
Open.exist?(pth) default => true
|
|
1835
|
+
```
|
|
1836
|
+
|
|
1837
|
+
Interpretation: `prepend_path`/`append_path` do NOT reset the cached `@@map_order`
|
|
1838
|
+
(`add_path` does), so re-registering an existing name leaves a duplicate entry in the order.
|
|
1839
|
+
`exists?` defaults to producing.
|
|
1840
|
+
|
|
1841
|
+
## P57–P58 — PersistenceAndResources.md: cache paths, drivers, lock API, claim dispatch
|
|
1842
|
+
|
|
1843
|
+
Command: `ruby -Ilib tmp/probe57.rb`, `ruby -Ilib tmp/probe58.rb`, `ruby -Ilib tmp/probe58b.rb`.
|
|
1844
|
+
|
|
1845
|
+
```
|
|
1846
|
+
persistence_path('short_key') => "var/cache/persistence/short_key" # no type suffix, no digest
|
|
1847
|
+
long/unsafe key => digested name
|
|
1848
|
+
Persist.respond_to?(:lock) => false # no Persist.lock API
|
|
1849
|
+
persist arity-1 block receives target file; custom :string claim works
|
|
1850
|
+
:proc arity 1 receives the resolved final path; returning a String also writes it
|
|
1851
|
+
Resource singletons: :claim, :produce, :pkgdir=, :subdir=, :lock_dir=, :rake_dirs, ...
|
|
1852
|
+
standalone lib: :proc writing a TSV raises NameError: uninitialized constant Resource::TSV
|
|
1853
|
+
```
|
|
1854
|
+
|
|
1855
|
+
Interpretation: the doc's `Persist.lock(file){}` API does not exist (locking is `Open.lock`);
|
|
1856
|
+
cache paths have no serialization-type component; the `:proc` dispatch in
|
|
1857
|
+
`Resource#produce` handles String/IO/StringIO/Array/TSV/TSV::Dumper only.
|
|
1858
|
+
|
|
1859
|
+
## P59–P61 — map_order mutation, proc-nil latching
|
|
1860
|
+
|
|
1861
|
+
Command: `ruby -Ilib tmp/probe59.rb`, `ruby -Ilib tmp/probe61.rb`, `ruby -Ilib tmp/probe61b.rb`.
|
|
1862
|
+
|
|
1863
|
+
```
|
|
1864
|
+
instance map_order= works on an annotated Path ([:current, :cache]) and does NOT touch Path.map_order
|
|
1865
|
+
Path-level: add_path resets @@map_order and appends the new map (zz inserted before :default)
|
|
1866
|
+
:proc returning nil (arity 0): file stays missing; next produce re-raises the latched exception
|
|
1867
|
+
→ "Error producing a: uninitialized constant Resource::TSV"; @produced latches
|
|
1868
|
+
:proc arity 1 returning nil after writing the file itself: works (file exists, produce OK)
|
|
1869
|
+
```
|
|
1870
|
+
|
|
1871
|
+
Interpretation: there is no class-level `Path.map_order=`; instance-level `map_order=`
|
|
1872
|
+
(annotation) exists. A nil-returning proc that did not write the file leaves the resource
|
|
1873
|
+
missing, and the failure is latched in `@produced` (produce re-raises instead of retrying).
|
|
1874
|
+
|
|
1875
|
+
## P62–P63 — serialization driver matrix (CachingResults table)
|
|
1876
|
+
|
|
1877
|
+
Command: `ruby -Ilib tmp/probe62.rb`, `ruby -Ilib tmp/probe63.rb`.
|
|
1878
|
+
|
|
1879
|
+
```
|
|
1880
|
+
:string => "hello" ; :text => "hello\n"
|
|
1881
|
+
:integer/:float roundtrip ; :boolean accepts TRUE_STRINGS (true/T/t/1/yes/y/on...)
|
|
1882
|
+
:marshal roundtrip ; :json roundtrip ; :binary raw bytes
|
|
1883
|
+
:yaml of a bare String then load → Psych AST nodes (Scalar/Document), not the String
|
|
1884
|
+
:yaml_array works (element-wise)
|
|
1885
|
+
No :array, :path, :file, :string_array drivers — Persist.load falls through to :serializer
|
|
1886
|
+
persist("k", :yaml, path: "cache/result.yaml") works; second call loads {:a=>1}
|
|
1887
|
+
```
|
|
1888
|
+
|
|
1889
|
+
Interpretation: CachingResults' serialization table lists four types that do not exist
|
|
1890
|
+
(`:array`, `:path`, `:file`, `:string_array`) and omits `:serializer`/`:yaml_array`/TRUE_STRINGS.
|
|
1891
|
+
|
|
1892
|
+
## P64–P65 — add/prepend/append and find fall-through vs extension alternatives
|
|
1893
|
+
|
|
1894
|
+
Command: `ruby -Ilib tmp/probe64.rb`, `ruby -Ilib tmp/probe65c.rb`.
|
|
1895
|
+
|
|
1896
|
+
```
|
|
1897
|
+
add_path(:my_location, '/custom/data/{PATH}') → follow(:my_location) => "/custom/data/data/config.yaml"
|
|
1898
|
+
prepend_path(:user, '/shared/{PATH}') → order [:user, :current, :user, ...]
|
|
1899
|
+
append_path(:global, '/opt/data/{PATH}') → global map value replaced
|
|
1900
|
+
{PKGDIR}/{SUBPATH} custom map with pkgdir 'p64eapp' → "/shared/data/p64eapp/hg38.fa"
|
|
1901
|
+
located-but-missing path: find => "/nonexistent/absolute/path" (self, not nil)
|
|
1902
|
+
unlocated path: find => "/home/mvazque2/.scout/zz/no/such" (follow(:default) = user map, not nil)
|
|
1903
|
+
extension alternatives (symbol-keyed path_maps {current: dir, local: dir}):
|
|
1904
|
+
order [:current, :local] → plain file wins
|
|
1905
|
+
order [:local, :current] → .gz file wins
|
|
1906
|
+
```
|
|
1907
|
+
|
|
1908
|
+
Interpretation: `find` NEVER returns nil — unlocated paths fall through to the `:user`
|
|
1909
|
+
(`:default`) map expansion and located-but-missing paths return themselves; compressed
|
|
1910
|
+
variants win purely by map order, confirming the "not necessarily uncompressed" pitfall.
|
|
1911
|
+
|
|
1912
|
+
## P66 — lock_dir / cache_dir types; TmpFile flattening; Open.read(Path)
|
|
1913
|
+
|
|
1914
|
+
Command: `ruby -Ilib tmp/probe66.rb`.
|
|
1915
|
+
|
|
1916
|
+
```
|
|
1917
|
+
Persist.cache_dir.class => String (default "var/cache/persistence")
|
|
1918
|
+
Persist.lock_dir => "/home/mvazque2/.scout/tmp/persist_locks" (String)
|
|
1919
|
+
TmpFile.tmp_for_file('some/file/name.txt') => ".../tmpfiles/some·file·name.txt" (separator flattened to '·')
|
|
1920
|
+
Open.read(Path.setup('VERSION')) => "1.8.8" (Path resolved via find)
|
|
1921
|
+
```
|
|
1922
|
+
|
|
1923
|
+
Interpretation: neither cache_dir nor lock_dir is a Path object; TmpFile does NOT use
|
|
1924
|
+
path-map templates — it flattens separators into a single tmpfiles directory, contradicting
|
|
1925
|
+
PathResolution.md's "TmpFile.tmp_for_file uses Path patterns".
|