bitfab 0.29.1 → 0.30.1
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/lib/bitfab/client.rb +6 -2
- data/lib/bitfab/http_client.rb +7 -1
- data/lib/bitfab/replay.rb +193 -2
- data/lib/bitfab/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a3e03384934c51dd57e27ac1213dca8f88b41ead5a0980c04bb10ba0ad464d8
|
|
4
|
+
data.tar.gz: da9c5574b00bd66ba481a78e0f1e0ce4f8e9336b35a7b2e9013498b475622237
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97f2755495103e92c94258f1763a1066bfc5a1944168e10dec1ad1e0bd24a58ca916791122cf3673f52326ca0d5b89b7ca8324071d40b6c408f3153167f6c95c
|
|
7
|
+
data.tar.gz: 61ba2d4b92cfa2c2665753319c6acf67dda2d48a547088d1ab47b2e9006835fe869185f3861d4e378f5c8e76167009f2580f385b5f6416467a24ae5b9dce8242
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -83,6 +83,10 @@ module Bitfab
|
|
|
83
83
|
# replay runs into a single experiment batch
|
|
84
84
|
# @param dataset_id [String, nil] optional UUID of the dataset this replay
|
|
85
85
|
# runs against, stored on the resulting experiment for durable attribution
|
|
86
|
+
# @param grader_ids [Array<String>, nil] optional UUIDs of graders attached
|
|
87
|
+
# directly to this experiment, graded as the union with the dataset's
|
|
88
|
+
# runnable graders at completion; each must be an active/live grader in the
|
|
89
|
+
# same org and trace function or the server rejects the replay
|
|
86
90
|
# @param mock [String] mock strategy for child spans: "marked" (default),
|
|
87
91
|
# "none", or "all". "marked" only mocks spans declared with
|
|
88
92
|
# mock_on_replay: true; "all" mocks every child span.
|
|
@@ -109,12 +113,12 @@ module Bitfab
|
|
|
109
113
|
# A raising callback never crashes the run.
|
|
110
114
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
111
115
|
def replay(receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, max_concurrency: 10,
|
|
112
|
-
name: nil, code_change_description: nil, code_change_files: nil, experiment_group_id: nil, dataset_id: nil, mock: "marked",
|
|
116
|
+
name: nil, code_change_description: nil, code_change_files: nil, experiment_group_id: nil, dataset_id: nil, grader_ids: nil, mock: "marked",
|
|
113
117
|
adapt_inputs: nil, mock_override: nil, environment: nil, on_progress: nil)
|
|
114
118
|
Replay.run(
|
|
115
119
|
self, receiver, method_name,
|
|
116
120
|
trace_function_key:, limit:, trace_ids:, name:, max_concurrency:,
|
|
117
|
-
code_change_description:, code_change_files:, experiment_group_id:, dataset_id:, mock:, adapt_inputs:,
|
|
121
|
+
code_change_description:, code_change_files:, experiment_group_id:, dataset_id:, grader_ids:, mock:, adapt_inputs:,
|
|
118
122
|
mock_override:, environment:,
|
|
119
123
|
on_progress:
|
|
120
124
|
)
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -112,8 +112,13 @@ module Bitfab
|
|
|
112
112
|
# experiment/test run
|
|
113
113
|
# @param dataset_id [String, nil] optional UUID of the dataset this replay
|
|
114
114
|
# runs against, stored on the resulting experiment for durable attribution
|
|
115
|
+
# @param grader_ids [Array<String>, nil] optional UUIDs of graders attached
|
|
116
|
+
# directly to this experiment, graded as the union with the dataset's
|
|
117
|
+
# runnable graders at completion; each must be an active/live grader in the
|
|
118
|
+
# same org and trace function or the server rejects the replay
|
|
115
119
|
def start_replay(trace_function_key, limit, trace_ids: nil, code_change_description: nil,
|
|
116
|
-
code_change_files: nil, experiment_group_id: nil, name: nil, include_db_branch_lease: false, dataset_id: nil
|
|
120
|
+
code_change_files: nil, experiment_group_id: nil, name: nil, include_db_branch_lease: false, dataset_id: nil,
|
|
121
|
+
grader_ids: nil)
|
|
117
122
|
payload = {
|
|
118
123
|
"traceFunctionKey" => trace_function_key
|
|
119
124
|
}
|
|
@@ -127,6 +132,7 @@ module Bitfab
|
|
|
127
132
|
payload["experimentGroupId"] = experiment_group_id unless experiment_group_id.nil?
|
|
128
133
|
payload["includeDbBranchLease"] = true if include_db_branch_lease
|
|
129
134
|
payload["datasetId"] = dataset_id unless dataset_id.nil?
|
|
135
|
+
payload["graderIds"] = grader_ids unless grader_ids.nil?
|
|
130
136
|
|
|
131
137
|
# When DB branching is on, the server resolves a Neon preview branch per
|
|
132
138
|
# item (snapshot + restore + poll), which can run several seconds each.
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "json"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "timeout"
|
|
5
7
|
|
|
6
8
|
require_relative "constants"
|
|
7
9
|
require_relative "mock_override"
|
|
@@ -104,6 +106,10 @@ module Bitfab
|
|
|
104
106
|
# replay runs into a single experiment batch
|
|
105
107
|
# @param dataset_id [String, nil] optional UUID of the dataset this replay
|
|
106
108
|
# runs against, stored on the resulting experiment for durable attribution
|
|
109
|
+
# @param grader_ids [Array<String>, nil] optional UUIDs of graders attached
|
|
110
|
+
# directly to this experiment, graded as the union with the dataset's
|
|
111
|
+
# runnable graders at completion; each must be an active/live grader in the
|
|
112
|
+
# same org and trace function or the server rejects the replay
|
|
107
113
|
# @param mock [String] mock strategy for child spans: "marked" (default),
|
|
108
114
|
# "none", or "all". "marked" only mocks spans declared with
|
|
109
115
|
# mock_on_replay: true; "all" mocks every child span.
|
|
@@ -129,7 +135,7 @@ module Bitfab
|
|
|
129
135
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
130
136
|
def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil,
|
|
131
137
|
max_concurrency: 10, code_change_description: nil, code_change_files: nil, experiment_group_id: nil,
|
|
132
|
-
dataset_id: nil, mock: "marked",
|
|
138
|
+
dataset_id: nil, grader_ids: nil, mock: "marked",
|
|
133
139
|
adapt_inputs: nil, mock_override: nil, environment: nil, on_progress: nil)
|
|
134
140
|
unless MOCK_STRATEGIES.include?(mock.to_s)
|
|
135
141
|
raise ArgumentError, "Invalid mock strategy '#{mock}'. Must be one of: #{MOCK_STRATEGIES.join(", ")}"
|
|
@@ -175,6 +181,19 @@ module Bitfab
|
|
|
175
181
|
|
|
176
182
|
include_db_branch_lease = !environment.nil?
|
|
177
183
|
|
|
184
|
+
# An explicit code change always wins; only when the caller passed neither
|
|
185
|
+
# field do we fall back to the payload the replay wrapper captured from git
|
|
186
|
+
# and handed over via BITFAB_CODE_CHANGE_PATH. This is what makes an
|
|
187
|
+
# experiment show its diff even when the run happened outside the
|
|
188
|
+
# assistant's make-change rails.
|
|
189
|
+
if code_change_description.nil? && code_change_files.nil?
|
|
190
|
+
captured = resolve_auto_code_change(name)
|
|
191
|
+
unless captured.nil?
|
|
192
|
+
code_change_description = captured[0]
|
|
193
|
+
code_change_files = captured[1]
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
178
197
|
replay_data = http_client.start_replay(
|
|
179
198
|
trace_function_key,
|
|
180
199
|
effective_limit,
|
|
@@ -184,7 +203,8 @@ module Bitfab
|
|
|
184
203
|
code_change_files:,
|
|
185
204
|
experiment_group_id:,
|
|
186
205
|
include_db_branch_lease:,
|
|
187
|
-
dataset_id
|
|
206
|
+
dataset_id:,
|
|
207
|
+
grader_ids:
|
|
188
208
|
)
|
|
189
209
|
test_run_id = replay_data["testRunId"]
|
|
190
210
|
test_run_url = replay_data["testRunUrl"]
|
|
@@ -290,6 +310,177 @@ module Bitfab
|
|
|
290
310
|
result
|
|
291
311
|
end
|
|
292
312
|
|
|
313
|
+
# Read the code-change payload the replay wrapper captured and pointed us at
|
|
314
|
+
# via BITFAB_CODE_CHANGE_PATH. Returns [description, files] or nil.
|
|
315
|
+
# Best-effort: any error (no env var, missing file, bad JSON) yields nil so
|
|
316
|
+
# the replay proceeds with no code change rather than raising.
|
|
317
|
+
TRUNK_CANDIDATES = ["origin/HEAD", "origin/main", "origin/master", "main", "master"].freeze
|
|
318
|
+
CC_MAX_FILES = 60
|
|
319
|
+
CC_MAX_FILE_BYTES = 500_000
|
|
320
|
+
CC_MAX_TOTAL_BYTES = 2_000_000
|
|
321
|
+
|
|
322
|
+
# Auto-capture the code change to attach when the caller passed none. Lives
|
|
323
|
+
# in replay() (the one call guaranteed to run on every replay) so it works no
|
|
324
|
+
# matter how the replay was launched. Precedence: a BITFAB_CODE_CHANGE_PATH
|
|
325
|
+
# override, else the git diff vs trunk. Best-effort: any failure yields nil.
|
|
326
|
+
# An explicit code_change_files always wins over this.
|
|
327
|
+
def resolve_auto_code_change(label = nil)
|
|
328
|
+
return nil if ENV["BITFAB_DISABLE_CODE_CHANGE_CAPTURE"]
|
|
329
|
+
|
|
330
|
+
from_env = read_code_change_file
|
|
331
|
+
return from_env unless from_env.nil?
|
|
332
|
+
|
|
333
|
+
capture_code_change_from_git(Dir.pwd, label)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def read_code_change_file
|
|
337
|
+
path = ENV["BITFAB_CODE_CHANGE_PATH"]
|
|
338
|
+
return nil if path.nil? || path.empty?
|
|
339
|
+
|
|
340
|
+
begin
|
|
341
|
+
parsed = JSON.parse(File.read(path))
|
|
342
|
+
files = parsed["files"]
|
|
343
|
+
# A malformed payload (non-array, or entries that aren't hashes) must
|
|
344
|
+
# yield no code change, never crash normalize_code_change_files.
|
|
345
|
+
files = nil unless files.is_a?(Array) && files.all?(Hash)
|
|
346
|
+
description = parsed["description"]
|
|
347
|
+
description = nil unless description.is_a?(String)
|
|
348
|
+
return nil if files.nil? && description.nil?
|
|
349
|
+
|
|
350
|
+
[description, files]
|
|
351
|
+
rescue
|
|
352
|
+
nil
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def git(cwd, args)
|
|
357
|
+
# capture3 so git's stderr (e.g. "not a git repository") is swallowed, not
|
|
358
|
+
# leaked to the host process's console.
|
|
359
|
+
out, _err, status = Timeout.timeout(30) do
|
|
360
|
+
Open3.capture3("git", *args, chdir: cwd)
|
|
361
|
+
end
|
|
362
|
+
return nil unless status.success?
|
|
363
|
+
|
|
364
|
+
# Tag as UTF-8 and scrub invalid bytes so blob contents compare cleanly
|
|
365
|
+
# against the UTF-8 working-file reads and never break JSON.generate later
|
|
366
|
+
# (a non-UTF-8 file that slips past the null-byte check would otherwise
|
|
367
|
+
# abort the whole start-replay request).
|
|
368
|
+
out.force_encoding("UTF-8").scrub
|
|
369
|
+
rescue
|
|
370
|
+
nil
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def cc_ref_exists?(root, ref)
|
|
374
|
+
!git(root, ["rev-parse", "--verify", "#{ref}^{object}"]).nil?
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Returns [base, from_trunk]; from_trunk is false on the HEAD fallback.
|
|
378
|
+
def cc_resolve_base(root)
|
|
379
|
+
forced = ENV["BITFAB_CODE_CHANGE_BASE"]
|
|
380
|
+
if forced && cc_ref_exists?(root, forced)
|
|
381
|
+
mb = git(root, ["merge-base", "HEAD", forced])
|
|
382
|
+
return [mb.strip, true] if mb && !mb.strip.empty?
|
|
383
|
+
|
|
384
|
+
rp = git(root, ["rev-parse", "--verify", forced])
|
|
385
|
+
return (rp && !rp.strip.empty?) ? [rp.strip, true] : nil
|
|
386
|
+
end
|
|
387
|
+
TRUNK_CANDIDATES.each do |candidate|
|
|
388
|
+
next unless cc_ref_exists?(root, candidate)
|
|
389
|
+
|
|
390
|
+
mb = git(root, ["merge-base", "HEAD", candidate])
|
|
391
|
+
return [mb.strip, true] if mb && !mb.strip.empty?
|
|
392
|
+
end
|
|
393
|
+
cc_ref_exists?(root, "HEAD") ? ["HEAD", false] : nil
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def cc_blob_bytes(root, base, path)
|
|
397
|
+
out = git(root, ["cat-file", "-s", "#{base}:#{path}"])
|
|
398
|
+
(out && !out.strip.empty?) ? out.strip.to_i : 0
|
|
399
|
+
rescue
|
|
400
|
+
0
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def cc_working_bytes(root, path)
|
|
404
|
+
File.size(File.join(root, path))
|
|
405
|
+
rescue
|
|
406
|
+
0
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def capture_code_change_from_git(cwd, label = nil)
|
|
410
|
+
root = git(cwd, ["rev-parse", "--show-toplevel"])
|
|
411
|
+
return nil if root.nil? || root.strip.empty?
|
|
412
|
+
|
|
413
|
+
root = root.strip
|
|
414
|
+
resolved = cc_resolve_base(root)
|
|
415
|
+
return nil if resolved.nil?
|
|
416
|
+
|
|
417
|
+
base, from_trunk = resolved
|
|
418
|
+
|
|
419
|
+
tracked = git(root, ["diff", "--name-status", "--no-renames", "-z", base, "--", ":!.bitfab"]) || ""
|
|
420
|
+
untracked = git(root, ["ls-files", "--others", "--exclude-standard", "-z", "--", ":!.bitfab"]) || ""
|
|
421
|
+
|
|
422
|
+
entries = cc_parse_name_status_z(tracked)
|
|
423
|
+
untracked.split("\0").reject(&:empty?).each { |p| entries << ["A", p] }
|
|
424
|
+
return nil if entries.empty?
|
|
425
|
+
|
|
426
|
+
files = []
|
|
427
|
+
total = 0
|
|
428
|
+
entries.each do |status, path|
|
|
429
|
+
break if files.length >= CC_MAX_FILES
|
|
430
|
+
|
|
431
|
+
# Skip oversized files by size BEFORE reading their full contents, so a
|
|
432
|
+
# huge changed file can't OOM/stall replay just to be discarded.
|
|
433
|
+
before_bytes = (status == "A") ? 0 : cc_blob_bytes(root, base, path)
|
|
434
|
+
after_bytes = (status == "D") ? 0 : cc_working_bytes(root, path)
|
|
435
|
+
next if before_bytes > CC_MAX_FILE_BYTES || after_bytes > CC_MAX_FILE_BYTES
|
|
436
|
+
|
|
437
|
+
# Normalize CRLF -> LF on both sides: git's blob is already LF-normalized
|
|
438
|
+
# (autocrlf clean), so a raw CRLF working file would otherwise skew every
|
|
439
|
+
# line as changed. Comparing/storing LF keeps the diff meaningful.
|
|
440
|
+
before = (status == "A") ? "" : (git(root, ["show", "#{base}:#{path}"]) || "").gsub("\r\n", "\n")
|
|
441
|
+
after = (status == "D") ? "" : cc_read_working_file(root, path).gsub("\r\n", "\n")
|
|
442
|
+
next if before == after
|
|
443
|
+
|
|
444
|
+
size = before.bytesize + after.bytesize
|
|
445
|
+
next if total + size > CC_MAX_TOTAL_BYTES ||
|
|
446
|
+
cc_looks_binary?(before) || cc_looks_binary?(after)
|
|
447
|
+
|
|
448
|
+
total += size
|
|
449
|
+
files << {"path" => path, "before" => before, "after" => after}
|
|
450
|
+
end
|
|
451
|
+
return nil if files.empty?
|
|
452
|
+
|
|
453
|
+
subject = git(root, ["log", "-1", "--format=%s", "HEAD"])
|
|
454
|
+
subject = subject ? subject.strip : ""
|
|
455
|
+
head = ((label && !label.strip.empty?) ? label.strip : nil) || (subject.empty? ? nil : subject) || "Working-tree change"
|
|
456
|
+
word = (files.length == 1) ? "file" : "files"
|
|
457
|
+
against = from_trunk ? "vs trunk" : "uncommitted (vs HEAD)"
|
|
458
|
+
["#{head} (#{files.length} #{word} changed #{against})", files]
|
|
459
|
+
rescue
|
|
460
|
+
nil
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def cc_parse_name_status_z(raw)
|
|
464
|
+
parts = raw.split("\0").reject(&:empty?)
|
|
465
|
+
out = []
|
|
466
|
+
i = 0
|
|
467
|
+
while i + 1 < parts.length
|
|
468
|
+
out << [parts[i][0], parts[i + 1]]
|
|
469
|
+
i += 2
|
|
470
|
+
end
|
|
471
|
+
out
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def cc_read_working_file(root, path)
|
|
475
|
+
File.read(File.join(root, path), encoding: "UTF-8")
|
|
476
|
+
rescue
|
|
477
|
+
""
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def cc_looks_binary?(str)
|
|
481
|
+
str[0, 8000].include?("\0")
|
|
482
|
+
end
|
|
483
|
+
|
|
293
484
|
def write_replay_result_file(result)
|
|
294
485
|
result_path = ENV["BITFAB_REPLAY_RESULT_PATH"]
|
|
295
486
|
return if result_path.nil? || result_path.empty?
|
data/lib/bitfab/version.rb
CHANGED