jrf 0.1.12 → 0.1.13

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.
data/test/jrf_test.rb DELETED
@@ -1,1103 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- begin
4
- require "bundler/setup"
5
- rescue LoadError
6
- # Allow running tests in plain Ruby environments with globally installed gems.
7
- end
8
-
9
- require "json"
10
- require "open3"
11
- require "stringio"
12
- require "tmpdir"
13
- require "zlib"
14
- require_relative "../lib/jrf/cli/runner"
15
-
16
- def run_jrf(expr, input, *opts)
17
- Open3.capture3("./exe/jrf", *opts, expr, stdin_data: input)
18
- end
19
-
20
- def assert_equal(expected, actual, msg = nil)
21
- return if expected == actual
22
-
23
- raise "assert_equal failed#{msg ? " (#{msg})" : ""}\nexpected: #{expected.inspect}\nactual: #{actual.inspect}"
24
- end
25
-
26
- def assert_includes(text, fragment, msg = nil)
27
- return if text.include?(fragment)
28
-
29
- raise "assert_includes failed#{msg ? " (#{msg})" : ""}\ntext: #{text.inspect}\nfragment: #{fragment.inspect}"
30
- end
31
-
32
- def assert_success(status, stderr, msg = nil)
33
- return if status.success?
34
-
35
- raise "expected success#{msg ? " (#{msg})" : ""}, got failure\nstderr: #{stderr}"
36
- end
37
-
38
- def assert_failure(status, msg = nil)
39
- return unless status.success?
40
-
41
- raise "expected failure#{msg ? " (#{msg})" : ""}, got success"
42
- end
43
-
44
- def assert_float_close(expected, actual, epsilon = 1e-9, msg = nil)
45
- return if (expected - actual).abs <= epsilon
46
-
47
- raise "assert_float_close failed#{msg ? " (#{msg})" : ""}\nexpected: #{expected}\nactual: #{actual}\nepsilon: #{epsilon}"
48
- end
49
-
50
- def lines(str)
51
- str.lines.map(&:strip).reject(&:empty?)
52
- end
53
-
54
- class RecordingRunner < Jrf::CLI::Runner
55
- attr_reader :writes
56
-
57
- def initialize(**kwargs)
58
- super
59
- @writes = []
60
- end
61
-
62
- private
63
-
64
- def write_output(str)
65
- return if str.empty?
66
-
67
- @writes << str
68
- end
69
- end
70
-
71
- class ChunkedSource
72
- def initialize(str, chunk_size: 5)
73
- @str = str
74
- @chunk_size = chunk_size
75
- @offset = 0
76
- end
77
-
78
- def read(length = nil, outbuf = nil)
79
- raise "expected chunked reads" if length.nil?
80
-
81
- chunk = @str.byteslice(@offset, [length, @chunk_size].min)
82
- return nil unless chunk
83
-
84
- @offset += chunk.bytesize
85
- if outbuf
86
- outbuf.replace(chunk)
87
- else
88
- chunk
89
- end
90
- end
91
- end
92
-
93
- File.chmod(0o755, "./exe/jrf")
94
-
95
- input = <<~NDJSON
96
- {"foo":1,"x":5}
97
- {"foo":2,"x":11}
98
- {"foo":{"bar":"ok"},"x":50}
99
- {"x":70}
100
- NDJSON
101
-
102
- stdout, stderr, status = run_jrf('_["foo"]', input)
103
- assert_success(status, stderr, "simple extract")
104
- assert_equal(%w[1 2 {"bar":"ok"} null], lines(stdout), "extract output")
105
-
106
- input_nested = <<~NDJSON
107
- {"foo":{"bar":"a"}}
108
- {"foo":{"bar":"b"}}
109
- NDJSON
110
-
111
- stdout, stderr, status = run_jrf('_["foo"]["bar"]', input_nested)
112
- assert_success(status, stderr, "nested extract")
113
- assert_equal(%w["a" "b"], lines(stdout), "nested output")
114
-
115
- stdout, stderr, status = run_jrf('select(_["x"] > 10) >> _["foo"]', input)
116
- assert_success(status, stderr, "select + extract")
117
- assert_equal(%w[2 {"bar":"ok"} null], lines(stdout), "filtered output")
118
-
119
- stdout, stderr, status = run_jrf('select(_["x"] > 10)', input)
120
- assert_success(status, stderr, "select only")
121
- assert_equal(
122
- ['{"foo":2,"x":11}', '{"foo":{"bar":"ok"},"x":50}', '{"x":70}'],
123
- lines(stdout),
124
- "select-only output"
125
- )
126
-
127
- input_hello = <<~NDJSON
128
- {"hello":123}
129
- {"hello":456}
130
- NDJSON
131
-
132
- stdout, stderr, status = run_jrf('select(_["hello"] == 123)', input_hello)
133
- assert_success(status, stderr, "select-only hello")
134
- assert_equal(['{"hello":123}'], lines(stdout), "select-only hello output")
135
-
136
- stdout, stderr, status = run_jrf('select(_["hello"] == 123) >> _["hello"]', input_hello, "-v")
137
- assert_success(status, stderr, "dump stages")
138
- assert_equal(%w[123], lines(stdout), "dump stages output")
139
- assert_includes(stderr, 'stage[0]: select(_["hello"] == 123)')
140
- assert_includes(stderr, 'stage[1]: _["hello"]')
141
-
142
- stdout, stderr, status = Open3.capture3("./exe/jrf", "--help")
143
- assert_success(status, stderr, "help option")
144
- assert_includes(stdout, "usage: jrf [options] 'STAGE >> STAGE >> ...'")
145
- assert_includes(stdout, "JSON filter with the power and speed of Ruby.")
146
- assert_includes(stdout, "--lax")
147
- assert_includes(stdout, "--pretty")
148
- assert_includes(stdout, "--require LIBRARY")
149
- assert_includes(stdout, "--no-jit")
150
- assert_includes(stdout, "-V")
151
- assert_includes(stdout, "--version")
152
- assert_includes(stdout, "--atomic-write-bytes N")
153
- assert_includes(stdout, "Pipeline:")
154
- assert_includes(stdout, "Connect stages with top-level >>.")
155
- assert_includes(stdout, "The current value in each stage is available as _.")
156
- assert_includes(stdout, "See Also:")
157
- assert_includes(stdout, "https://github.com/kazuho/jrf#readme")
158
- assert_equal([], lines(stderr), "help stderr output")
159
-
160
- stdout, stderr, status = Open3.capture3("./exe/jrf", "--version")
161
- assert_success(status, stderr, "version long option")
162
- assert_equal([Jrf::VERSION], lines(stdout), "version long option output")
163
- assert_equal([], lines(stderr), "version long option stderr")
164
-
165
- stdout, stderr, status = Open3.capture3("./exe/jrf", "-V")
166
- assert_success(status, stderr, "version short option")
167
- assert_equal([Jrf::VERSION], lines(stdout), "version short option output")
168
- assert_equal([], lines(stderr), "version short option stderr")
169
-
170
- threshold_input = StringIO.new((1..4).map { |i| "{\"foo\":\"#{'x' * 1020}\",\"i\":#{i}}\n" }.join)
171
- buffered_runner = RecordingRunner.new(inputs: [threshold_input], out: StringIO.new, err: StringIO.new)
172
- buffered_runner.run('_')
173
- expected_line = JSON.generate({"foo" => "x" * 1020, "i" => 1}) + "\n"
174
- assert_equal(2, buffered_runner.writes.length, "default atomic write limit buffers records until the configured threshold")
175
- assert_equal(expected_line.bytesize * 3, buffered_runner.writes.first.bytesize, "default atomic write limit flushes before the next record would exceed the threshold")
176
- assert_equal(expected_line.bytesize, buffered_runner.writes.last.bytesize, "final buffer flush emits the remaining record")
177
-
178
- small_limit_runner = RecordingRunner.new(inputs: [StringIO.new("{\"foo\":1}\n{\"foo\":2}\n")], out: StringIO.new, err: StringIO.new, atomic_write_bytes: 1)
179
- small_limit_runner.run('_["foo"]')
180
- assert_equal(["1\n", "2\n"], small_limit_runner.writes, "small atomic write limit emits oversized records directly")
181
-
182
- error_runner = RecordingRunner.new(inputs: [StringIO.new("{\"foo\":1}\n{\"foo\":")], out: StringIO.new, err: StringIO.new)
183
- begin
184
- error_runner.run('_["foo"]')
185
- raise "expected parse error for buffered flush test"
186
- rescue JSON::ParserError
187
- assert_equal(["1\n"], error_runner.writes, "buffer flushes pending output before parse errors escape")
188
- end
189
-
190
- stdout, stderr, status = run_jrf('select(_["hello"] == 123) >> _["hello"]', input_hello, "--verbose")
191
- assert_success(status, stderr, "dump stages verbose alias")
192
- assert_equal(%w[123], lines(stdout), "dump stages verbose alias output")
193
- assert_includes(stderr, 'stage[0]: select(_["hello"] == 123)')
194
-
195
- stdout, stderr, status = run_jrf('_["hello"]', input_hello, "--atomic-write-bytes", "512")
196
- assert_success(status, stderr, "atomic write bytes option")
197
- assert_equal(%w[123 456], lines(stdout), "atomic write bytes option output")
198
-
199
- stdout, stderr, status = run_jrf('_["hello"]', input_hello, "--atomic-write-bytes=512")
200
- assert_success(status, stderr, "atomic write bytes equals form")
201
- assert_equal(%w[123 456], lines(stdout), "atomic write bytes equals form output")
202
-
203
- stdout, stderr, status = Open3.capture3("./exe/jrf", "--atomic-write-bytes", "0", '_["hello"]', stdin_data: input_hello)
204
- assert_failure(status, "atomic write bytes rejects zero")
205
- assert_includes(stderr, "--atomic-write-bytes requires a positive integer")
206
-
207
- Dir.mktmpdir do |dir|
208
- helper = File.join(dir, "helpers.rb")
209
- File.write(helper, <<~RUBY)
210
- def double(value)
211
- value * 2
212
- end
213
- RUBY
214
-
215
- stdout, stderr, status = Open3.capture3("./exe/jrf", "-r", helper, 'double(_["hello"])', stdin_data: input_hello)
216
- assert_success(status, stderr, "require helper option")
217
- assert_equal(%w[246 912], lines(stdout), "require helper option output")
218
- end
219
-
220
- if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enabled?)
221
- yjit_probe = "{\"probe\":1}\n"
222
-
223
- stdout, stderr, status = run_jrf('RubyVM::YJIT.enabled?', yjit_probe)
224
- assert_success(status, stderr, "default jit enablement")
225
- assert_equal(%w[true], lines(stdout), "default jit enablement output")
226
-
227
- stdout, stderr, status = run_jrf('RubyVM::YJIT.enabled?', yjit_probe, "--no-jit")
228
- assert_success(status, stderr, "no-jit option")
229
- assert_equal(%w[false], lines(stdout), "no-jit option output")
230
- end
231
-
232
- Dir.mktmpdir do |dir|
233
- gz_path = File.join(dir, "input.ndjson.gz")
234
- Zlib::GzipWriter.open(gz_path) do |io|
235
- io.write("{\"foo\":10}\n{\"foo\":20}\n")
236
- end
237
-
238
- stdout, stderr, status = Open3.capture3("./exe/jrf", '_["foo"]', gz_path)
239
- assert_success(status, stderr, "compressed input by suffix")
240
- assert_equal(%w[10 20], lines(stdout), "compressed input output")
241
-
242
- lax_gz_path = File.join(dir, "input-lax.json.gz")
243
- Zlib::GzipWriter.open(lax_gz_path) do |io|
244
- io.write("{\"foo\":30}\n\x1e{\"foo\":40}\n")
245
- end
246
-
247
- stdout, stderr, status = Open3.capture3("./exe/jrf", "--lax", '_["foo"]', lax_gz_path)
248
- assert_success(status, stderr, "compressed lax input by suffix")
249
- assert_equal(%w[30 40], lines(stdout), "compressed lax input output")
250
-
251
- second_gz_path = File.join(dir, "input2.ndjson.gz")
252
- Zlib::GzipWriter.open(second_gz_path) do |io|
253
- io.write("{\"foo\":50}\n")
254
- end
255
-
256
- stdout, stderr, status = Open3.capture3("./exe/jrf", '_["foo"]', gz_path, second_gz_path)
257
- assert_success(status, stderr, "multiple compressed inputs by suffix")
258
- assert_equal(%w[10 20 50], lines(stdout), "multiple compressed input output")
259
- end
260
-
261
- stdout, stderr, status = run_jrf('_', input_hello, "--pretty")
262
- assert_success(status, stderr, "pretty output")
263
- assert_equal(
264
- [
265
- "{",
266
- "\"hello\": 123",
267
- "}",
268
- "{",
269
- "\"hello\": 456",
270
- "}"
271
- ],
272
- lines(stdout),
273
- "pretty output lines"
274
- )
275
-
276
- input_regex = <<~NDJSON
277
- {"foo":{"bar":"ok"},"x":50}
278
- {"foo":{"bar":"ng"},"x":70}
279
- NDJSON
280
-
281
- stdout, stderr, status = run_jrf('select(/ok/.match(_["foo"]["bar"])) >> _["x"]', input_regex)
282
- assert_success(status, stderr, "regex in select")
283
- assert_equal(%w[50], lines(stdout), "regex filter output")
284
-
285
- input_split = <<~NDJSON
286
- {"x":1}
287
- NDJSON
288
-
289
- stdout, stderr, status = run_jrf('[1 >> 2] >> _', input_split)
290
- assert_success(status, stderr, "no split inside []")
291
- assert_equal(['[0]'], lines(stdout), "no split inside [] output")
292
-
293
- stdout, stderr, status = run_jrf('{a: 1 >> 2} >> _[:a]', input_split)
294
- assert_success(status, stderr, "no split inside {}")
295
- assert_equal(%w[0], lines(stdout), "no split inside {} output")
296
-
297
- stdout, stderr, status = run_jrf('(-> { 1 >> 2 }).call >> _ + 1', input_split)
298
- assert_success(status, stderr, "no split inside block")
299
- assert_equal(%w[1], lines(stdout), "no split inside block output")
300
-
301
- input_flat = <<~NDJSON
302
- {"items":[1,2]}
303
- {"items":[3]}
304
- {"items":[]}
305
- NDJSON
306
-
307
- stdout, stderr, status = run_jrf('_["items"] >> flat', input_flat)
308
- assert_success(status, stderr, "flat basic")
309
- assert_equal(%w[1 2 3], lines(stdout), "flat basic output")
310
-
311
- input_flat_hash = <<~NDJSON
312
- {"items":[{"x":1},{"x":2}]}
313
- NDJSON
314
-
315
- stdout, stderr, status = run_jrf('_["items"] >> flat >> _["x"]', input_flat_hash)
316
- assert_success(status, stderr, "flat then extract")
317
- assert_equal(%w[1 2], lines(stdout), "flat then extract output")
318
-
319
- stdout, stderr, status = run_jrf('_["items"] >> flat >> sum(_)', input_flat)
320
- assert_success(status, stderr, "flat then sum")
321
- assert_equal(%w[6], lines(stdout), "flat then sum output")
322
-
323
- stdout, stderr, status = run_jrf('_["items"] >> flat >> group', input_flat)
324
- assert_success(status, stderr, "flat then group")
325
- assert_equal(['[1,2,3]'], lines(stdout), "flat then group output")
326
-
327
- stdout, stderr, status = run_jrf('map { |x| flat }', "[[1,2],[3],[4,5,6]]\n")
328
- assert_success(status, stderr, "flat inside map")
329
- assert_equal(['[1,2,3,4,5,6]'], lines(stdout), "flat inside map output")
330
-
331
- stdout, stderr, status = run_jrf('map_values { |v| flat }', "{\"a\":[1,2],\"b\":[3]}\n")
332
- assert_failure(status, "flat inside map_values")
333
- assert_includes(stderr, "flat is not supported inside map_values")
334
-
335
- stdout, stderr, status = run_jrf('_["foo"] >> flat', input)
336
- assert_failure(status, "flat requires array")
337
- assert_includes(stderr, "flat expects Array")
338
-
339
- input_sum = <<~NDJSON
340
- {"foo":1,"x":5}
341
- {"foo":2,"x":11}
342
- {"foo":3,"x":50}
343
- {"foo":4,"x":70}
344
- NDJSON
345
-
346
- stdout, stderr, status = run_jrf('sum(_["foo"])', input_sum)
347
- assert_success(status, stderr, "sum only")
348
- assert_equal(%w[10], lines(stdout), "sum output")
349
-
350
- stdout, stderr, status = run_jrf('count()', input_sum)
351
- assert_success(status, stderr, "count only")
352
- assert_equal(%w[4], lines(stdout), "count output")
353
-
354
- stdout, stderr, status = run_jrf('count(_["foo"])', input_sum)
355
- assert_success(status, stderr, "count(expr) only")
356
- assert_equal(%w[4], lines(stdout), "count(expr) output")
357
-
358
- stdout, stderr, status = run_jrf('min(_["foo"])', input_sum)
359
- assert_success(status, stderr, "min only")
360
- assert_equal(%w[1], lines(stdout), "min output")
361
-
362
- stdout, stderr, status = run_jrf('max(_["foo"])', input_sum)
363
- assert_success(status, stderr, "max only")
364
- assert_equal(%w[4], lines(stdout), "max output")
365
-
366
- stdout, stderr, status = run_jrf('select(_["x"] > 10) >> sum(_["foo"])', input_sum)
367
- assert_success(status, stderr, "select + sum")
368
- assert_equal(%w[9], lines(stdout), "select + sum output")
369
-
370
- stdout, stderr, status = run_jrf('{total: sum(_["foo"]), n: count()}', input_sum)
371
- assert_success(status, stderr, "structured reducer result")
372
- assert_equal(['{"total":10,"n":4}'], lines(stdout), "structured reducer result output")
373
-
374
- stdout, stderr, status = run_jrf('average(_["foo"])', input_sum)
375
- assert_success(status, stderr, "average")
376
- assert_float_close(2.5, lines(stdout).first.to_f, 1e-12, "average output")
377
-
378
- stdout, stderr, status = run_jrf('stdev(_["foo"])', input_sum)
379
- assert_success(status, stderr, "stdev")
380
- assert_float_close(1.118033988749895, lines(stdout).first.to_f, 1e-12, "stdev output")
381
-
382
- stdout, stderr, status = run_jrf('_["foo"] >> sum(_ * 2)', input_sum)
383
- assert_success(status, stderr, "extract + sum")
384
- assert_equal(%w[20], lines(stdout), "extract + sum output")
385
-
386
- stdout, stderr, status = run_jrf('sum(2 * _["foo"])', input_sum)
387
- assert_success(status, stderr, "sum with literal on left")
388
- assert_equal(%w[20], lines(stdout), "sum with literal on left output")
389
-
390
- stdout, stderr, status = run_jrf('select(_["x"] > 1000) >> sum(_["foo"])', input_sum)
391
- assert_success(status, stderr, "sum no matches")
392
- assert_equal([], lines(stdout), "sum no matches output")
393
-
394
- stdout, stderr, status = run_jrf('select(_["x"] > 1000) >> count()', input_sum)
395
- assert_success(status, stderr, "count no matches")
396
- assert_equal([], lines(stdout), "count no matches output")
397
-
398
- stdout, stderr, status = run_jrf('select(_["x"] > 1000) >> count(_["foo"])', input_sum)
399
- assert_success(status, stderr, "count(expr) no matches")
400
- assert_equal([], lines(stdout), "count(expr) no matches output")
401
-
402
- stdout, stderr, status = run_jrf('select(_["x"] > 1000) >> average(_["foo"])', input_sum)
403
- assert_success(status, stderr, "average no matches")
404
- assert_equal([], lines(stdout), "average no matches output")
405
-
406
- stdout, stderr, status = run_jrf('select(_["x"] > 1000) >> stdev(_["foo"])', input_sum)
407
- assert_success(status, stderr, "stdev no matches")
408
- assert_equal([], lines(stdout), "stdev no matches output")
409
-
410
- stdout, stderr, status = run_jrf('select(_["x"] > 1000) >> min(_["foo"])', input_sum)
411
- assert_success(status, stderr, "min no matches")
412
- assert_equal([], lines(stdout), "min no matches output")
413
-
414
- stdout, stderr, status = run_jrf('select(_["x"] > 1000) >> max(_["foo"])', input_sum)
415
- assert_success(status, stderr, "max no matches")
416
- assert_equal([], lines(stdout), "max no matches output")
417
-
418
- stdout, stderr, status = run_jrf('sum(_["foo"]) >> _ + 1', input_sum)
419
- assert_success(status, stderr, "reduce in middle")
420
- assert_equal(%w[11], lines(stdout), "reduce in middle output")
421
-
422
- stdout, stderr, status = run_jrf('select(_["x"] > 10) >> _["foo"] >> sum(_ * 2) >> select(_ > 10) >> _ + 1', input_sum)
423
- assert_success(status, stderr, "reduce mixed with select/extract")
424
- assert_equal(%w[19], lines(stdout), "reduce mixed output")
425
-
426
- stdout, stderr, status = run_jrf('_["foo"] >> sum(_) >> _ * 10 >> sum(_)', input_sum)
427
- assert_success(status, stderr, "multiple reducers")
428
- assert_equal(%w[100], lines(stdout), "multiple reducers output")
429
-
430
- stdout, stderr, status = run_jrf('_["foo"] >> min(_) >> _ * 10 >> max(_)', input_sum)
431
- assert_success(status, stderr, "min/max mixed reducers")
432
- assert_equal(%w[10], lines(stdout), "min/max mixed reducers output")
433
-
434
- input_sort_rows = <<~NDJSON
435
- {"foo":"b","at":2}
436
- {"foo":"c","at":3}
437
- {"foo":"a","at":1}
438
- NDJSON
439
-
440
- stdout, stderr, status = run_jrf('sort(_["at"]) >> _["foo"]', input_sort_rows)
441
- assert_success(status, stderr, "sort rows by field")
442
- assert_equal(%w["a" "b" "c"], lines(stdout), "sort rows by field output")
443
-
444
- stdout, stderr, status = run_jrf('sort { |a, b| b["at"] <=> a["at"] } >> _["foo"]', input_sort_rows)
445
- assert_success(status, stderr, "sort rows by comparator")
446
- assert_equal(%w["c" "b" "a"], lines(stdout), "sort rows by comparator output")
447
-
448
- stdout, stderr, status = run_jrf('sort(_["at"]) >> _["foo"] >> group', input_sort_rows)
449
- assert_success(status, stderr, "sort then group")
450
- assert_equal(['["a","b","c"]'], lines(stdout), "sort then group output")
451
-
452
- stdout, stderr, status = run_jrf('select(_["x"] > 1000) >> sort(_["x"]) >> _["foo"]', input_sum)
453
- assert_success(status, stderr, "sort no matches")
454
- assert_equal([], lines(stdout), "sort no matches output")
455
-
456
- stdout, stderr, status = run_jrf('select(_["x"] > 1000) >> _["foo"] >> group', input_sum)
457
- assert_success(status, stderr, "group no matches")
458
- assert_equal([], lines(stdout), "group no matches output")
459
-
460
- input_group_multi = <<~NDJSON
461
- {"x":1,"y":"a"}
462
- {"x":2,"y":"b"}
463
- {"x":3,"y":"c"}
464
- NDJSON
465
-
466
- stdout, stderr, status = run_jrf('{a: group(_["x"]), b: group(_["y"])}', input_group_multi)
467
- assert_success(status, stderr, "group in hash")
468
- assert_equal(['{"a":[1,2,3],"b":["a","b","c"]}'], lines(stdout), "group in hash output")
469
-
470
- stdout, stderr, status = run_jrf('select(_["x"] > 1000) >> {a: group(_["x"]), b: group(_["y"])}', input_group_multi)
471
- assert_success(status, stderr, "group in hash no matches")
472
- assert_equal([], lines(stdout), "group in hash no-match output")
473
-
474
- stdout, stderr, status = run_jrf('percentile(_["foo"], 0.50)', input_sum)
475
- assert_success(status, stderr, "single percentile")
476
- assert_equal(%w[2], lines(stdout), "single percentile output")
477
-
478
- stdout, stderr, status = run_jrf('percentile(_["foo"], [0.25, 0.50, 1.0])', input_sum)
479
- assert_success(status, stderr, "array percentile")
480
- assert_equal(
481
- ['[1,2,4]'],
482
- lines(stdout),
483
- "array percentile output"
484
- )
485
-
486
- stdout, stderr, status = run_jrf('percentile(_["foo"], 0.25.step(1.0, 0.25))', input_sum)
487
- assert_success(status, stderr, "enumerable percentile")
488
- assert_equal(
489
- ['[1,2,3,4]'],
490
- lines(stdout),
491
- "enumerable percentile output"
492
- )
493
-
494
- input_with_nil = <<~NDJSON
495
- {"foo":1}
496
- {"foo":null}
497
- {"bar":999}
498
- {"foo":3}
499
- NDJSON
500
-
501
- stdout, stderr, status = run_jrf('sum(_["foo"])', input_with_nil)
502
- assert_success(status, stderr, "sum ignores nil")
503
- assert_equal(%w[4], lines(stdout), "sum ignores nil output")
504
-
505
- stdout, stderr, status = run_jrf('min(_["foo"])', input_with_nil)
506
- assert_success(status, stderr, "min ignores nil")
507
- assert_equal(%w[1], lines(stdout), "min ignores nil output")
508
-
509
- stdout, stderr, status = run_jrf('max(_["foo"])', input_with_nil)
510
- assert_success(status, stderr, "max ignores nil")
511
- assert_equal(%w[3], lines(stdout), "max ignores nil output")
512
-
513
- stdout, stderr, status = run_jrf('average(_["foo"])', input_with_nil)
514
- assert_success(status, stderr, "average ignores nil")
515
- assert_float_close(2.0, lines(stdout).first.to_f, 1e-12, "average ignores nil output")
516
-
517
- stdout, stderr, status = run_jrf('stdev(_["foo"])', input_with_nil)
518
- assert_success(status, stderr, "stdev ignores nil")
519
- assert_float_close(1.0, lines(stdout).first.to_f, 1e-12, "stdev ignores nil output")
520
-
521
- stdout, stderr, status = run_jrf('percentile(_["foo"], [0.5, 1.0])', input_with_nil)
522
- assert_success(status, stderr, "percentile ignores nil")
523
- assert_equal(
524
- ['[1,3]'],
525
- lines(stdout),
526
- "percentile ignores nil output"
527
- )
528
-
529
- stdout, stderr, status = run_jrf('count()', input_with_nil)
530
- assert_success(status, stderr, "count with nil rows")
531
- assert_equal(%w[4], lines(stdout), "count with nil rows output")
532
-
533
- stdout, stderr, status = run_jrf('count(_["foo"])', input_with_nil)
534
- assert_success(status, stderr, "count(expr) ignores nil")
535
- assert_equal(%w[2], lines(stdout), "count(expr) ignores nil output")
536
-
537
- # count_if
538
- input_count_if = <<~NDJSON
539
- {"x":1}
540
- {"x":-2}
541
- {"x":3}
542
- {"x":-4}
543
- {"x":5}
544
- NDJSON
545
-
546
- stdout, stderr, status = run_jrf('count_if(_["x"] > 0)', input_count_if)
547
- assert_success(status, stderr, "count_if")
548
- assert_equal(%w[3], lines(stdout), "count_if output")
549
-
550
- stdout, stderr, status = run_jrf('[count_if(_["x"] > 0), count_if(_["x"] < 0)]', input_count_if)
551
- assert_success(status, stderr, "count_if multiple")
552
- assert_equal(["[3,2]"], lines(stdout), "count_if multiple output")
553
-
554
- input_all_nil = <<~NDJSON
555
- {"foo":null}
556
- {"bar":1}
557
- NDJSON
558
-
559
- stdout, stderr, status = run_jrf('sum(_["foo"])', input_all_nil)
560
- assert_success(status, stderr, "sum all nil")
561
- assert_equal(%w[0], lines(stdout), "sum all nil output")
562
-
563
- stdout, stderr, status = run_jrf('min(_["foo"])', input_all_nil)
564
- assert_success(status, stderr, "min all nil")
565
- assert_equal(%w[null], lines(stdout), "min all nil output")
566
-
567
- stdout, stderr, status = run_jrf('max(_["foo"])', input_all_nil)
568
- assert_success(status, stderr, "max all nil")
569
- assert_equal(%w[null], lines(stdout), "max all nil output")
570
-
571
- stdout, stderr, status = run_jrf('average(_["foo"])', input_all_nil)
572
- assert_success(status, stderr, "average all nil")
573
- assert_equal(%w[null], lines(stdout), "average all nil output")
574
-
575
- stdout, stderr, status = run_jrf('stdev(_["foo"])', input_all_nil)
576
- assert_success(status, stderr, "stdev all nil")
577
- assert_equal(%w[null], lines(stdout), "stdev all nil output")
578
-
579
- stdout, stderr, status = run_jrf('percentile(_["foo"], 0.5)', input_all_nil)
580
- assert_success(status, stderr, "percentile all nil")
581
- assert_equal(%w[null], lines(stdout), "percentile all nil output")
582
-
583
- stdout, stderr, status = run_jrf('count(_["foo"])', input_all_nil)
584
- assert_success(status, stderr, "count(expr) all nil")
585
- assert_equal(%w[0], lines(stdout), "count(expr) all nil output")
586
-
587
- input_multi_cols = <<~NDJSON
588
- {"a":1,"b":10}
589
- {"a":2,"b":20}
590
- {"a":3,"b":30}
591
- {"a":4,"b":40}
592
- NDJSON
593
-
594
- stdout, stderr, status = run_jrf('{a: percentile(_["a"], [0.25, 0.50, 1.0]), b: percentile(_["b"], [0.25, 0.50, 1.0])}', input_multi_cols)
595
- assert_success(status, stderr, "nested array percentile for multiple columns")
596
- assert_equal(
597
- ['{"a":[1,2,4],"b":[10,20,40]}'],
598
- lines(stdout),
599
- "nested array percentile output"
600
- )
601
-
602
- input_reduce = <<~NDJSON
603
- {"s":"hello"}
604
- {"s":"world"}
605
- {"s":"jrf"}
606
- NDJSON
607
-
608
- stdout, stderr, status = run_jrf('_["s"] >> reduce("") { |acc, v| acc.empty? ? v : "#{acc} #{v}" }', input_reduce)
609
- assert_success(status, stderr, "reduce with implicit value")
610
- assert_equal(['"hello world jrf"'], lines(stdout), "reduce implicit value output")
611
-
612
- stdout, stderr, status = run_jrf('_["s"] >> reduce("") { |acc, v| acc.empty? ? v : "#{acc} #{v}" }', input_reduce)
613
- assert_success(status, stderr, "reduce in two-stage form")
614
- assert_equal(['"hello world jrf"'], lines(stdout), "reduce in two-stage form output")
615
-
616
- stdout, stderr, status = run_jrf('sum(_["foo"]) >> select(_ > 100)', input_sum)
617
- assert_success(status, stderr, "post-reduce select drop")
618
- assert_equal([], lines(stdout), "post-reduce select drop output")
619
-
620
- input_whitespace_stream = "{\"foo\":1} {\"foo\":2}\n\t{\"foo\":3}\n"
621
- stdout, stderr, status = run_jrf('_["foo"]', input_whitespace_stream)
622
- assert_failure(status, "default NDJSON should reject same-line multi-values")
623
- assert_includes(stderr, "JSON::ParserError")
624
-
625
- stdout, stderr, status = run_jrf('_["foo"]', input_whitespace_stream, "--lax")
626
- assert_success(status, stderr, "whitespace-separated JSON stream with --lax")
627
- assert_equal(%w[1 2 3], lines(stdout), "whitespace-separated stream output")
628
-
629
- input_json_seq = "\x1e{\"foo\":10}\n\x1e{\"foo\":20}\n"
630
- stdout, stderr, status = run_jrf('_["foo"]', input_json_seq)
631
- assert_failure(status, "RS framing requires --lax")
632
- assert_includes(stderr, "JSON::ParserError")
633
-
634
- stdout, stderr, status = run_jrf('_["foo"]', input_json_seq, "--lax")
635
- assert_success(status, stderr, "json-seq style RS framing with --lax")
636
- assert_equal(%w[10 20], lines(stdout), "json-seq style output")
637
-
638
- input_lax_multiline = <<~JSONS
639
- {
640
- "foo": 101,
641
- "bar": {"x": 1}
642
- }
643
- {
644
- "foo": 202,
645
- "bar": {"x": 2}
646
- }
647
- JSONS
648
- stdout, stderr, status = run_jrf('_["foo"]', input_lax_multiline)
649
- assert_failure(status, "default NDJSON rejects multiline objects")
650
- assert_includes(stderr, "JSON::ParserError")
651
-
652
- stdout, stderr, status = run_jrf('_["bar"]["x"]', input_lax_multiline, "--lax")
653
- assert_success(status, stderr, "lax accepts multiline objects")
654
- assert_equal(%w[1 2], lines(stdout), "lax multiline object output")
655
-
656
- input_lax_mixed_separators = "{\"foo\":1}\n\x1e{\"foo\":2}\t{\"foo\":3}\n"
657
- stdout, stderr, status = run_jrf('_["foo"]', input_lax_mixed_separators, "--lax")
658
- assert_success(status, stderr, "lax accepts mixed whitespace and RS separators")
659
- assert_equal(%w[1 2 3], lines(stdout), "lax mixed separators output")
660
-
661
- input_lax_with_escaped_newline = "{\"s\":\"line1\\nline2\"}\n{\"s\":\"ok\"}\n"
662
- stdout, stderr, status = run_jrf('_["s"]', input_lax_with_escaped_newline, "--lax")
663
- assert_success(status, stderr, "lax handles escaped newlines in strings")
664
- assert_equal(['"line1\nline2"', '"ok"'], lines(stdout), "lax escaped newline string output")
665
-
666
- input_lax_trailing_rs = "\x1e{\"foo\":9}\n\x1e"
667
- stdout, stderr, status = run_jrf('_["foo"]', input_lax_trailing_rs, "--lax")
668
- assert_success(status, stderr, "lax ignores trailing separator")
669
- assert_equal(%w[9], lines(stdout), "lax trailing separator output")
670
-
671
- chunked_lax_out = RecordingRunner.new(
672
- inputs: [ChunkedSource.new("{\"foo\":1}\n\x1e{\"foo\":2}\n\t{\"foo\":3}\n")],
673
- out: StringIO.new,
674
- err: StringIO.new,
675
- lax: true
676
- )
677
- chunked_lax_out.run('_["foo"]')
678
- assert_equal(%w[1 2 3], lines(chunked_lax_out.writes.join), "lax mode streams chunked input without whole-input reads")
679
-
680
- Dir.mktmpdir do |dir|
681
- one = File.join(dir, "one.json")
682
- two = File.join(dir, "two.json")
683
- File.write(one, "1")
684
- File.write(two, "2")
685
-
686
- stdout, stderr, status = Open3.capture3("./exe/jrf", "--lax", "_", one, two)
687
- assert_success(status, stderr, "lax keeps file boundaries")
688
- assert_equal(%w[1 2], lines(stdout), "lax does not merge JSON across file boundaries")
689
- end
690
-
691
- stdout, stderr, status = run_jrf('select(_["x"] > ) >> _["foo"]', "")
692
- assert_failure(status, "syntax error should fail before row loop")
693
- assert_includes(stderr, "syntax error")
694
-
695
- stdout, stderr, status = run_jrf('([)] >> _', "")
696
- assert_failure(status, "mismatched delimiter should fail")
697
- assert_includes(stderr, "mismatched delimiter")
698
-
699
- stdout, stderr, status = run_jrf('(_["x"] >> _["y"]', "")
700
- assert_failure(status, "unclosed delimiter should fail")
701
- assert_includes(stderr, "unclosed delimiter")
702
-
703
- input_broken_tail = <<~NDJSON
704
- {"foo":1}
705
- {"foo":2}
706
- {"foo":
707
- NDJSON
708
-
709
- stdout, stderr, status = run_jrf('sum(_["foo"])', input_broken_tail)
710
- assert_failure(status, "broken input should fail")
711
- assert_equal(%w[3], lines(stdout), "reducers flush before parse error")
712
- assert_includes(stderr, "JSON::ParserError")
713
-
714
- input_chain = <<~NDJSON
715
- {"foo":{"bar":{"z":1},"keep":true}}
716
- {"foo":{"bar":{"z":2},"keep":false}}
717
- {"foo":{"bar":{"z":3},"keep":true}}
718
- NDJSON
719
-
720
- stdout, stderr, status = run_jrf('_["foo"] >> select(_["keep"]) >> _["bar"] >> select(_["z"] > 1) >> _["z"]', input_chain)
721
- assert_success(status, stderr, "select/extract chain")
722
- assert_equal(%w[3], lines(stdout), "chain output")
723
-
724
- input_map = <<~NDJSON
725
- {"values":[1,10,100]}
726
- {"values":[2,20,200]}
727
- {"values":[3,30,300]}
728
- NDJSON
729
-
730
- stdout, stderr, status = run_jrf('_["values"] >> map { |x| sum(x) }', input_map)
731
- assert_success(status, stderr, "map with sum")
732
- assert_equal(['[6,60,600]'], lines(stdout), "map with sum output")
733
-
734
- stdout, stderr, status = run_jrf('_["values"] >> map { |x| min(x) }', input_map)
735
- assert_success(status, stderr, "map with min")
736
- assert_equal(['[1,10,100]'], lines(stdout), "map with min output")
737
-
738
- stdout, stderr, status = run_jrf('_["values"] >> map { |x| max(x) }', input_map)
739
- assert_success(status, stderr, "map with max")
740
- assert_equal(['[3,30,300]'], lines(stdout), "map with max output")
741
-
742
- stdout, stderr, status = run_jrf('_["values"] >> map { |x| sum(_[0] + x) }', input_map)
743
- assert_success(status, stderr, "map keeps ambient _")
744
- assert_equal(['[12,66,606]'], lines(stdout), "map ambient _ output")
745
-
746
- stdout, stderr, status = run_jrf('_["values"] >> map { |x| reduce(0) { |acc, v| acc + v } }', input_map)
747
- assert_success(status, stderr, "map with reduce")
748
- assert_equal(['[6,60,600]'], lines(stdout), "map with reduce output")
749
-
750
- input_map_varying = <<~NDJSON
751
- [1,10]
752
- [2,20,200]
753
- [3]
754
- NDJSON
755
-
756
- stdout, stderr, status = run_jrf('map { |x| sum(x) }', input_map_varying)
757
- assert_success(status, stderr, "map varying lengths")
758
- assert_equal(['[6,30,200]'], lines(stdout), "map varying lengths output")
759
-
760
- input_map_unsorted = <<~NDJSON
761
- {"values":[3,30]}
762
- {"values":[1,10]}
763
- {"values":[2,20]}
764
- NDJSON
765
-
766
- stdout, stderr, status = run_jrf('_["values"] >> map { |x| group }', input_map)
767
- assert_success(status, stderr, "map with group")
768
- assert_equal(['[[1,2,3],[10,20,30],[100,200,300]]'], lines(stdout), "map with group output")
769
-
770
- stdout, stderr, status = run_jrf('_["values"] >> map { |x| sort }', input_map_unsorted)
771
- assert_success(status, stderr, "map with sort default key")
772
- assert_equal(['[[1,2,3],[10,20,30]]'], lines(stdout), "map with sort default key output")
773
-
774
- input_map_values = <<~NDJSON
775
- {"a":1,"b":10}
776
- {"a":2,"b":20}
777
- {"a":3,"b":30}
778
- NDJSON
779
-
780
- stdout, stderr, status = run_jrf('map_values { |v| sum(v) }', input_map_values)
781
- assert_success(status, stderr, "map_values with sum")
782
- assert_equal(['{"a":6,"b":60}'], lines(stdout), "map_values with sum output")
783
-
784
- stdout, stderr, status = run_jrf('map_values { |v| min(v) }', input_map_values)
785
- assert_success(status, stderr, "map_values with min")
786
- assert_equal(['{"a":1,"b":10}'], lines(stdout), "map_values with min output")
787
-
788
- input_map_values_varying = <<~NDJSON
789
- {"a":1}
790
- {"a":2,"b":20}
791
- {"a":3,"b":30}
792
- NDJSON
793
-
794
- stdout, stderr, status = run_jrf('map_values { |v| sum(v) }', input_map_values_varying)
795
- assert_success(status, stderr, "map_values varying keys")
796
- assert_equal(['{"a":6,"b":50}'], lines(stdout), "map_values varying keys output")
797
-
798
- stdout, stderr, status = run_jrf('map_values { |v| count(v) }', input_map_values)
799
- assert_success(status, stderr, "map_values with count")
800
- assert_equal(['{"a":3,"b":3}'], lines(stdout), "map_values with count output")
801
-
802
- stdout, stderr, status = run_jrf('map_values { |v| group }', input_map_values)
803
- assert_success(status, stderr, "map_values with group")
804
- assert_equal(['{"a":[1,2,3],"b":[10,20,30]}'], lines(stdout), "map_values with group output")
805
-
806
- stdout, stderr, status = run_jrf('map_values { |v| sum(_["a"] + v) }', input_map_values)
807
- assert_success(status, stderr, "map_values keeps ambient _")
808
- assert_equal(['{"a":12,"b":66}'], lines(stdout), "map_values ambient _ output")
809
-
810
- stdout, stderr, status = run_jrf('map_values { |v| reduce(0) { |acc, x| acc + x } }', input_map_values)
811
- assert_success(status, stderr, "map_values with reduce")
812
- assert_equal(['{"a":6,"b":60}'], lines(stdout), "map_values with reduce output")
813
-
814
- stdout, stderr, status = run_jrf('map { |k, v| "#{k}:#{v}" }', input_map_values)
815
- assert_success(status, stderr, "map over hash transform")
816
- assert_equal(['["a:1","b:10"]', '["a:2","b:20"]', '["a:3","b:30"]'], lines(stdout), "map over hash transform output")
817
-
818
- stdout, stderr, status = run_jrf('map { |pair| pair }', input_map_values)
819
- assert_success(status, stderr, "map over hash single block arg")
820
- assert_equal(['[["a",1],["b",10]]', '[["a",2],["b",20]]', '[["a",3],["b",30]]'], lines(stdout), "map over hash single block arg output")
821
-
822
- stdout, stderr, status = run_jrf('map { |k, v| select(v >= 10 && k != "a") }', input_map_values)
823
- assert_success(status, stderr, "map over hash transform with select")
824
- assert_equal(['[10]', '[20]', '[30]'], lines(stdout), "map over hash transform with select output")
825
-
826
- stdout, stderr, status = run_jrf('map { |k, v| sum(v + k.length) }', input_map_values)
827
- assert_success(status, stderr, "map over hash with sum")
828
- assert_equal(['[9,63]'], lines(stdout), "map over hash with sum output")
829
-
830
- stdout, stderr, status = run_jrf('map { |k, v| sum(_["a"] + v + k.length) }', input_map_values)
831
- assert_success(status, stderr, "map over hash keeps ambient _")
832
- assert_equal(['[15,69]'], lines(stdout), "map over hash ambient _ output")
833
-
834
- stdout, stderr, status = run_jrf('select(false) >> map { |x| sum(x) }', input_map)
835
- assert_success(status, stderr, "map no matches")
836
- assert_equal([], lines(stdout), "map no matches output")
837
-
838
- stdout, stderr, status = run_jrf('select(false) >> map_values { |v| sum(v) }', input_map_values)
839
- assert_success(status, stderr, "map_values no matches")
840
- assert_equal([], lines(stdout), "map_values no matches output")
841
-
842
- stdout, stderr, status = run_jrf('map_values { |v| sum(v) } >> map_values { |v| v * 10 }', input_map_values)
843
- assert_success(status, stderr, "map_values piped to map_values passthrough")
844
- assert_equal(['{"a":60,"b":600}'], lines(stdout), "map_values piped output")
845
-
846
- # map/map_values transformation (no reducers)
847
- stdout, stderr, status = run_jrf('_["values"] >> map { |x| x + 1 }', input_map)
848
- assert_success(status, stderr, "map transform")
849
- assert_equal(['[2,11,101]', '[3,21,201]', '[4,31,301]'], lines(stdout), "map transform output")
850
-
851
- stdout, stderr, status = run_jrf('_["values"] >> map { |x| select(x >= 20) }', input_map)
852
- assert_success(status, stderr, "map transform with select")
853
- assert_equal(['[100]', '[20,200]', '[30,300]'], lines(stdout), "map transform with select output")
854
-
855
- stdout, stderr, status = run_jrf('map_values { |v| v * 2 }', input_map_values)
856
- assert_success(status, stderr, "map_values transform")
857
- assert_equal(['{"a":2,"b":20}', '{"a":4,"b":40}', '{"a":6,"b":60}'], lines(stdout), "map_values transform output")
858
-
859
- stdout, stderr, status = run_jrf('map_values { |v| select(v >= 10) }', input_map_values)
860
- assert_success(status, stderr, "map_values transform with select")
861
- assert_equal(['{"b":10}', '{"b":20}', '{"b":30}'], lines(stdout), "map_values transform with select output")
862
-
863
- stdout, stderr, status = run_jrf('_["values"] >> map { |x| x + 1 } >> map { |x| x * 10 }', input_map)
864
- assert_success(status, stderr, "chained map transforms")
865
- assert_equal(['[20,110,1010]', '[30,210,2010]', '[40,310,3010]'], lines(stdout), "chained map transforms output")
866
-
867
- stdout, stderr, status = run_jrf('map { map { |y| [ sum(y[0]), sum(y[1]) ] } }', "[[[1,2]]]\n[[[3,4]]]\n")
868
- assert_success(status, stderr, "nested map reducer binds to current target")
869
- assert_equal(['[[[4,6]]]'], lines(stdout), "nested map reducer output")
870
-
871
- stdout, stderr, status = run_jrf('map_values { |obj| map_values { |v| sum(v) } }', "{\"a\":{\"x\":1,\"y\":2},\"b\":{\"x\":10,\"y\":20}}\n{\"a\":{\"x\":3,\"y\":4},\"b\":{\"x\":30,\"y\":40}}\n")
872
- assert_success(status, stderr, "nested map_values reducer binds to current target")
873
- assert_equal(['{"a":{"x":4,"y":6},"b":{"x":40,"y":60}}'], lines(stdout), "nested map_values reducer output")
874
-
875
- # apply - aggregation
876
- stdout, stderr, status = run_jrf('[apply { |x| sum(x["foo"]) }, _.length]', '[{"foo":1},{"foo":2}]' + "\n" + '[{"foo":10}]' + "\n")
877
- assert_success(status, stderr, "apply with sum")
878
- assert_equal(["[3,2]", "[10,1]"], lines(stdout), "apply with sum output")
879
-
880
- # apply - passthrough
881
- stdout, stderr, status = run_jrf('apply { |x| x["foo"] }', '[{"foo":1},{"foo":2}]' + "\n")
882
- assert_success(status, stderr, "apply passthrough")
883
- assert_equal(["[1,2]"], lines(stdout), "apply passthrough output")
884
-
885
- # apply - percentile
886
- stdout, stderr, status = run_jrf('apply { |x| percentile(x, 0.5) }', '[10,20,30]' + "\n")
887
- assert_success(status, stderr, "apply with percentile")
888
- assert_equal(["20"], lines(stdout), "apply with percentile output")
889
-
890
- # apply with explicit collection inside map
891
- stdout, stderr, status = run_jrf('map { |o| [apply(o["vals"]) { |x| sum(x) }, o["name"]] }', '[{"name":"a","vals":[1,2]},{"name":"b","vals":[10,20]}]' + "\n")
892
- assert_success(status, stderr, "apply with explicit collection")
893
- assert_equal(['[[3,"a"],[30,"b"]]'], lines(stdout), "apply with explicit collection output")
894
-
895
- # map with explicit collection
896
- stdout, stderr, status = run_jrf('map(_["items"]) { |x| x * 2 }', '{"items":[1,2,3]}' + "\n")
897
- assert_success(status, stderr, "map with explicit collection")
898
- assert_equal(["[2,4,6]"], lines(stdout), "map with explicit collection output")
899
-
900
- # map_values with explicit collection
901
- stdout, stderr, status = run_jrf('map_values(_["data"]) { |v| v * 10 }', '{"data":{"a":1,"b":2}}' + "\n")
902
- assert_success(status, stderr, "map_values with explicit collection")
903
- assert_equal(['{"a":10,"b":20}'], lines(stdout), "map_values with explicit collection output")
904
-
905
- input_gb = <<~NDJSON
906
- {"status":200,"path":"/a","latency":10}
907
- {"status":404,"path":"/b","latency":50}
908
- {"status":200,"path":"/c","latency":30}
909
- {"status":200,"path":"/d","latency":20}
910
- NDJSON
911
-
912
- stdout, stderr, status = run_jrf('group_by(_["status"]) { count() }', input_gb)
913
- assert_success(status, stderr, "group_by with count")
914
- assert_equal(['{"200":3,"404":1}'], lines(stdout), "group_by with count output")
915
-
916
- stdout, stderr, status = run_jrf('group_by(_["status"]) { |row| sum(row["latency"]) }', input_gb)
917
- assert_success(status, stderr, "group_by with sum")
918
- assert_equal(['{"200":60,"404":50}'], lines(stdout), "group_by with sum output")
919
-
920
- stdout, stderr, status = run_jrf('group_by(_["status"]) { |row| average(row["latency"]) }', input_gb)
921
- assert_success(status, stderr, "group_by with average")
922
- result = JSON.parse(lines(stdout).first)
923
- assert_float_close(20.0, result["200"], 1e-12, "group_by average 200")
924
- assert_float_close(50.0, result["404"], 1e-12, "group_by average 404")
925
-
926
- stdout, stderr, status = run_jrf('group_by(_["status"])', input_gb)
927
- assert_success(status, stderr, "group_by default (collect rows)")
928
- result = JSON.parse(lines(stdout).first)
929
- assert_equal(3, result["200"].length, "group_by default 200 count")
930
- assert_equal(1, result["404"].length, "group_by default 404 count")
931
- assert_equal("/a", result["200"][0]["path"], "group_by default first row")
932
-
933
- stdout, stderr, status = run_jrf('group_by(_["status"]) { |row| group(row["path"]) }', input_gb)
934
- assert_success(status, stderr, "group_by with group(expr)")
935
- assert_equal(['{"200":["/a","/c","/d"],"404":["/b"]}'], lines(stdout), "group_by with group(expr) output")
936
-
937
- stdout, stderr, status = run_jrf('group_by(_["status"]) { group }', input_gb)
938
- assert_success(status, stderr, "group_by with implicit group")
939
- result = JSON.parse(lines(stdout).first)
940
- assert_equal(3, result["200"].length, "group_by implicit group 200 count")
941
- assert_equal("/a", result["200"][0]["path"], "group_by implicit group first row")
942
-
943
- stdout, stderr, status = run_jrf('group_by(_["status"]) { |row| min(row["latency"]) }', input_gb)
944
- assert_success(status, stderr, "group_by with min")
945
- assert_equal(['{"200":10,"404":50}'], lines(stdout), "group_by with min output")
946
-
947
- stdout, stderr, status = run_jrf('group_by(_["status"]) { |row| {total: sum(row["latency"]), n: count()} }', input_gb)
948
- assert_success(status, stderr, "group_by with multi-reducer")
949
- assert_equal(['{"200":{"total":60,"n":3},"404":{"total":50,"n":1}}'], lines(stdout), "group_by multi-reducer output")
950
-
951
- stdout, stderr, status = run_jrf('group_by(_["status"]) { reduce(0) { |acc, row| acc + row["latency"] } }', input_gb)
952
- assert_success(status, stderr, "group_by with reduce")
953
- assert_equal(['{"200":60,"404":50}'], lines(stdout), "group_by with reduce output")
954
-
955
- stdout, stderr, status = run_jrf('select(false) >> group_by(_["status"]) { count() }', input_gb)
956
- assert_success(status, stderr, "group_by no matches")
957
- assert_equal([], lines(stdout), "group_by no matches output")
958
-
959
- stdout, stderr, status = run_jrf('group_by(_["status"]) { count() } >> _[200]', input_gb)
960
- assert_success(status, stderr, "group_by then extract")
961
- assert_equal(%w[3], lines(stdout), "group_by then extract output")
962
-
963
- # === Library API (Jrf.new) ===
964
-
965
- require_relative "../lib/jrf"
966
-
967
- # passthrough
968
- j = Jrf.new(proc { _ })
969
- assert_equal([{"a" => 1}, {"a" => 2}], j.call([{"a" => 1}, {"a" => 2}]), "library passthrough")
970
-
971
- # extract
972
- j = Jrf.new(proc { _["a"] })
973
- assert_equal([1, 2], j.call([{"a" => 1}, {"a" => 2}]), "library extract")
974
-
975
- # select + extract (two stages)
976
- j = Jrf.new(
977
- proc { select(_["a"] > 1) },
978
- proc { _["a"] }
979
- )
980
- assert_equal([2, 3], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library select + extract")
981
-
982
- # sum
983
- j = Jrf.new(proc { sum(_["a"]) })
984
- assert_equal([6], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library sum")
985
-
986
- # sum with literal on left
987
- j = Jrf.new(proc { sum(2 * _["a"]) })
988
- assert_equal([12], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library sum literal on left")
989
-
990
- # structured reducers
991
- j = Jrf.new(proc { {total: sum(_["a"]), n: count()} })
992
- assert_equal([{total: 6, n: 3}], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library structured reducers")
993
-
994
- # map transform
995
- j = Jrf.new(proc { map { |x| x + 1 } })
996
- assert_equal([[2, 3], [4, 5]], j.call([[1, 2], [3, 4]]), "library map transform")
997
-
998
- # map reduce
999
- j = Jrf.new(proc { map { |x| sum(x) } })
1000
- assert_equal([[4, 6]], j.call([[1, 2], [3, 4]]), "library map reduce")
1001
-
1002
- # nested map reduce binds to current target
1003
- j = Jrf.new(proc { map { map { |y| [sum(y[0]), sum(y[1])] } } })
1004
- assert_equal([[[[4, 6]]]], j.call([[[[1, 2]]], [[[3, 4]]]]), "library nested map reduce")
1005
-
1006
- # map_values transform
1007
- j = Jrf.new(proc { map_values { |v| v * 10 } })
1008
- assert_equal([{"a" => 10, "b" => 20}], j.call([{"a" => 1, "b" => 2}]), "library map_values transform")
1009
-
1010
- # nested map_values reduce binds to current target
1011
- j = Jrf.new(proc { map_values { |obj| map_values { |v| sum(v) } } })
1012
- assert_equal([{"a" => {"x" => 4, "y" => 6}, "b" => {"x" => 40, "y" => 60}}], j.call([{"a" => {"x" => 1, "y" => 2}, "b" => {"x" => 10, "y" => 20}}, {"a" => {"x" => 3, "y" => 4}, "b" => {"x" => 30, "y" => 40}}]), "library nested map_values reduce")
1013
-
1014
- # map hash transform
1015
- j = Jrf.new(proc { map { |k, v| "#{k}=#{v}" } })
1016
- assert_equal([["a=1", "b=2"]], j.call([{"a" => 1, "b" => 2}]), "library map hash transform")
1017
-
1018
- # map hash single block arg
1019
- j = Jrf.new(proc { map { |pair| pair } })
1020
- assert_equal([[["a", 1], ["b", 2]]], j.call([{"a" => 1, "b" => 2}]), "library map hash single block arg")
1021
-
1022
- # map hash reduce
1023
- j = Jrf.new(proc { map { |k, v| sum(v + k.length) } })
1024
- assert_equal([[5, 7]], j.call([{"a" => 1, "b" => 2}, {"a" => 2, "b" => 3}]), "library map hash reduce")
1025
-
1026
- # apply - aggregation
1027
- j = Jrf.new(proc { [apply { |x| sum(x["foo"]) }, _.length] })
1028
- assert_equal([[3, 2], [10, 1]], j.call([[{"foo" => 1}, {"foo" => 2}], [{"foo" => 10}]]), "library apply reducer")
1029
-
1030
- # apply - passthrough
1031
- j = Jrf.new(proc { apply { |x| x["foo"] } })
1032
- assert_equal([[1, 2]], j.call([[{"foo" => 1}, {"foo" => 2}]]), "library apply passthrough")
1033
-
1034
- # apply - percentile
1035
- j = Jrf.new(proc { apply { |x| percentile(x, 0.5) } })
1036
- assert_equal([20], j.call([[10, 20, 30]]), "library apply percentile")
1037
-
1038
- # apply with explicit collection inside map
1039
- j = Jrf.new(proc { map { |o| [apply(o["vals"]) { |x| sum(x) }, o["name"]] } })
1040
- assert_equal([[[3, "a"], [30, "b"]]], j.call([[{"name" => "a", "vals" => [1, 2]}, {"name" => "b", "vals" => [10, 20]}]]), "library apply explicit collection")
1041
-
1042
- # map with explicit collection
1043
- j = Jrf.new(proc { map(_["items"]) { |x| x * 2 } })
1044
- assert_equal([[2, 4, 6]], j.call([{"items" => [1, 2, 3]}]), "library map explicit collection")
1045
-
1046
- # map_values with explicit collection
1047
- j = Jrf.new(proc { map_values(_["data"]) { |v| v * 10 } })
1048
- assert_equal([{"a" => 10, "b" => 20}], j.call([{"data" => {"a" => 1, "b" => 2}}]), "library map_values explicit collection")
1049
-
1050
- # group_by
1051
- j = Jrf.new(proc { group_by(_["k"]) { count() } })
1052
- assert_equal([{"x" => 2, "y" => 1}], j.call([{"k" => "x"}, {"k" => "x"}, {"k" => "y"}]), "library group_by")
1053
-
1054
- # reducer configuration is fixed by the first row
1055
- j = Jrf.new(proc { percentile(_["a"], _["p"]) })
1056
- assert_equal([2], j.call([{"a" => 1, "p" => 0.5}, {"a" => 2, "p" => [0.5, 1.0]}, {"a" => 3, "p" => [0.5, 1.0]}]), "library percentile configuration fixed by first row")
1057
-
1058
- counting_percentiles = Class.new do
1059
- include Enumerable
1060
-
1061
- attr_reader :each_calls
1062
-
1063
- def initialize(values)
1064
- @values = values
1065
- @each_calls = 0
1066
- end
1067
-
1068
- def each(&block)
1069
- @each_calls += 1
1070
- @values.each(&block)
1071
- end
1072
- end.new([0.25, 0.5, 1.0])
1073
-
1074
- j = Jrf.new(proc { percentile(_["a"], counting_percentiles) })
1075
- assert_equal([[1, 2, 3]], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library percentile enumerable values")
1076
- assert_equal(1, counting_percentiles.each_calls, "library percentile materializes enumerable once")
1077
-
1078
- # reducer then passthrough
1079
- j = Jrf.new(
1080
- proc { sum(_["a"]) },
1081
- proc { _ + 1 }
1082
- )
1083
- assert_equal([7], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library reducer then passthrough")
1084
-
1085
- # closure over local variables
1086
- threshold = 2
1087
- j = Jrf.new(proc { select(_["a"] > threshold) })
1088
- assert_equal([{"a" => 3}], j.call([{"a" => 1}, {"a" => 2}, {"a" => 3}]), "library closure")
1089
-
1090
- # empty input
1091
- j = Jrf.new(proc { sum(_) })
1092
- assert_equal([], j.call([]), "library empty input")
1093
-
1094
- ctx = Jrf::RowContext.new
1095
- stage = Jrf::Stage.new(ctx, proc { })
1096
- first_token = stage.step_reduce(1, initial: 0) { |acc, v| acc + v }
1097
- assert_equal(0, first_token.index, "step_reduce returns token while classifying reducer stage")
1098
- stage.instance_variable_set(:@mode, :reducer)
1099
- stage.instance_variable_set(:@cursor, 0)
1100
- second_token = stage.step_reduce(2, initial: 0) { |acc, v| acc + v }
1101
- raise "expected DROPPED for established reducer slot" unless second_token.equal?(Jrf::Control::DROPPED)
1102
-
1103
- puts "ok"