binpacker 0.2.0 → 0.4.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.
@@ -7,7 +7,7 @@ module Binpacker
7
7
  def initialize(worker_count, tty: $stdout.tty?)
8
8
  @worker_count = worker_count
9
9
  @tty = tty
10
- @workers = Array.new(worker_count) { { done: 0, total: 0, file: "", elapsed: 0.0 } }
10
+ @workers = Array.new(worker_count) { { done: 0, total: 0, file: '', elapsed: 0.0 } }
11
11
  @start = Time.now
12
12
  @last_ci_output = Time.at(Time.now.to_f - CI_INTERVAL)
13
13
  @lines_written = 0
@@ -32,8 +32,9 @@ module Binpacker
32
32
  end
33
33
  end
34
34
 
35
- def finish(worker_stats = [])
35
+ def finish(_worker_stats = [])
36
36
  return unless @tty
37
+
37
38
  redraw
38
39
  $stdout.puts
39
40
  end
@@ -42,12 +43,13 @@ module Binpacker
42
43
  active = worker_stats.reject { |s| s[:files] == 0 && s[:examples] == 0 }
43
44
  return if active.empty?
44
45
 
45
- active.each_with_index do |s, i|
46
+ worker_stats.each_with_index do |s, wid|
47
+ next if s[:files] == 0 && s[:examples] == 0
48
+
46
49
  t = format_time(s[:total_time])
47
- wid = worker_stats.index(s)
48
- $stdout.puts " Worker #{wid}: #{s[:files]} files, #{t} | #{s[:examples]} examples, #{s[:passed]} passed"
50
+ $stdout.puts " Worker #{wid}: #{s[:files]} tests, #{t} | #{s[:examples]} examples, #{s[:passed]} passed"
49
51
  end
50
- total_files = active.sum { |s| s[:files] }
52
+ total_tests = active.sum { |s| s[:files] }
51
53
  total_time = active.sum { |s| s[:total_time] }
52
54
  total_examples = active.sum { |s| s[:examples] }
53
55
  times = active.map { |s| s[:total_time] }
@@ -55,8 +57,8 @@ module Binpacker
55
57
  max_dev = times.map { |t| (t - mean).abs }.max
56
58
  dev_pct = mean > 0 ? (max_dev / mean * 100).round(1) : 0
57
59
 
58
- $stdout.puts " ──"
59
- $stdout.puts " Total: #{total_files} files, #{format_time(total_time)} | #{total_examples} examples"
60
+ $stdout.puts ' ──'
61
+ $stdout.puts " Total: #{total_tests} tests, #{format_time(total_time)} | #{total_examples} examples"
60
62
  $stdout.puts " Balance: max deviation #{format_time(max_dev)} (#{dev_pct}%)"
61
63
  end
62
64
 
@@ -67,7 +69,7 @@ module Binpacker
67
69
  clear_lines
68
70
  @workers.each_with_index do |w, i|
69
71
  bar = build_bar(w[:done], w[:total])
70
- status = w[:total] > 0 && w[:done] >= w[:total] ? "done" : w[:file][-50..] || ""
72
+ status = w[:total] > 0 && w[:done] >= w[:total] ? 'done' : w[:file][-50..] || ''
71
73
  $stdout.puts format_line(i, bar, w[:done], w[:total], status, w[:elapsed])
72
74
  end
73
75
  @lines_written = @worker_count
@@ -76,13 +78,15 @@ module Binpacker
76
78
 
77
79
  def clear_lines
78
80
  return if @lines_written == 0
81
+
79
82
  @lines_written.times do
80
83
  $stdout.print "\033[A\033[K"
81
84
  end
82
85
  end
83
86
 
84
87
  def build_bar(done, total)
85
- return "[----------]" if total == 0
88
+ return '[----------]' if total == 0
89
+
86
90
  width = 10
87
91
  filled = (done.to_f / total * width).round
88
92
  "[#{'█' * filled}#{'░' * (width - filled)}]"
@@ -96,21 +100,21 @@ module Binpacker
96
100
  def periodic_output
97
101
  now = Time.now
98
102
  return if now - @last_ci_output < CI_INTERVAL
103
+
99
104
  @last_ci_output = now
100
105
 
101
106
  parts = @workers.map.with_index do |w, i|
102
- if w[:total] > 0 && w[:done] >= w[:total]
103
- "W#{i}: done"
104
- else
105
- "W#{i}: #{w[:done]}/#{w[:total]}"
106
- end
107
+ ratio = w[:total] > 0 ? "#{w[:done]}/#{w[:total]}" : '0/?'
108
+ "W#{i}: #{ratio}"
107
109
  end
108
110
  elapsed = (now - @start).round(1)
109
111
  $stdout.puts "[binpacker #{elapsed}s] #{parts.join(' | ')}"
112
+ $stdout.flush
110
113
  end
111
114
 
112
115
  def format_time(seconds)
113
- return " 0.0s" if seconds < 0.001
116
+ return ' 0.0s' if seconds < 0.001
117
+
114
118
  m = (seconds / 60).floor
115
119
  s = (seconds % 60).round(1)
116
120
  m > 0 ? "#{m}m#{s.to_s.rjust(4, '0')}s" : "#{s.to_s.rjust(5)}s"
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Binpacker
4
+ # Inspects the working directory to report binpacker's setup state and
5
+ # recommend the next skill. Backs `binpacker describe`.
6
+ # See docs/design/agent-workflows.md.
7
+ class ProjectState
8
+ CONFIG_FILE = "binpacker.yml"
9
+ DEFAULT_TIMING_FILE = "binpacker.timings"
10
+ SUPPORTED_FRAMEWORKS = %w[rspec minitest].freeze
11
+ TESTUNIT_HINT = %r{test-unit|test/unit}
12
+
13
+ def config_present?
14
+ File.exist?(CONFIG_FILE)
15
+ end
16
+
17
+ def timing_present?
18
+ File.exist?(timing_file)
19
+ end
20
+
21
+ # "rspec", "minitest", "test-unit", or nil. Minitest and test-unit share
22
+ # the *_test.rb convention, so they are told apart by dependency hints.
23
+ def framework
24
+ return minitest_family if minitest_globs?
25
+ return "rspec" if Dir.glob("spec/**/*_spec.rb").any?
26
+ nil
27
+ end
28
+
29
+ # binpacker ships runners for rspec and minitest only. A detected framework
30
+ # outside that set (e.g. test-unit) is unsupported; nil is merely unknown.
31
+ def supported_framework?
32
+ fw = framework
33
+ fw.nil? || SUPPORTED_FRAMEWORKS.include?(fw)
34
+ end
35
+
36
+ def ci_wired?
37
+ Dir.glob(".github/workflows/*.{yml,yaml}").any? do |wf|
38
+ File.read(wf).include?("binpacker run")
39
+ end
40
+ end
41
+
42
+ # The skill the agent should run next.
43
+ def recommendation
44
+ return "binpacker-setup" unless config_present? && timing_present?
45
+ "binpacker-improve"
46
+ end
47
+
48
+ def to_h
49
+ {
50
+ config_present: config_present?,
51
+ timing_present: timing_present?,
52
+ framework: framework,
53
+ ci_wired: ci_wired?,
54
+ recommendation: recommendation
55
+ }
56
+ end
57
+
58
+ private
59
+
60
+ def minitest_globs?
61
+ Dir.glob("test*/**/*_test.rb").any? || Dir.glob("test*/**/test_*.rb").any?
62
+ end
63
+
64
+ def minitest_family
65
+ testunit_hinted? ? "test-unit" : "minitest"
66
+ end
67
+
68
+ def testunit_hinted?
69
+ sources = Dir.glob("Gemfile") + Dir.glob("*.gemspec") + Dir.glob("test*/**/*_test.rb").first(5)
70
+ sources.any? { |file| File.read(file).match?(TESTUNIT_HINT) }
71
+ rescue StandardError
72
+ false
73
+ end
74
+
75
+ def timing_file
76
+ return DEFAULT_TIMING_FILE unless config_present?
77
+ Config.new.timing_file
78
+ rescue StandardError
79
+ DEFAULT_TIMING_FILE
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Binpacker
6
+ # Builds the machine-readable Run report: predicted versus actual per-Worker
7
+ # durations plus the worst per-Test Drift. See docs/design/run-report-format.md.
8
+ class Report
9
+ SCHEMA = 1
10
+ DRIFT_LIMIT = 10
11
+
12
+ def initialize(profile:, algorithm:, predicted_loads:, worker_stats:, all_timings:, timings:)
13
+ @profile = profile
14
+ @algorithm = algorithm
15
+ @predicted_loads = predicted_loads
16
+ @worker_stats = worker_stats
17
+ @all_timings = all_timings
18
+ @timings = timings
19
+ end
20
+
21
+ def to_h
22
+ predicted = @predicted_loads
23
+ actual = @worker_stats.map { |s| s[:total_time] }
24
+
25
+ {
26
+ schema: SCHEMA,
27
+ profile: @profile,
28
+ algorithm: @algorithm,
29
+ worker_count: @worker_stats.size,
30
+ predicted_makespan: round(predicted.max || 0.0),
31
+ actual_makespan: round(actual.max || 0.0),
32
+ workers: workers,
33
+ balance: {
34
+ predicted_deviation_pct: deviation_pct(predicted),
35
+ actual_deviation_pct: deviation_pct(actual)
36
+ },
37
+ drift: drift
38
+ }
39
+ end
40
+
41
+ def write(path)
42
+ File.write(path, JSON.pretty_generate(to_h))
43
+ end
44
+
45
+ private
46
+
47
+ def workers
48
+ @worker_stats.map.with_index do |s, i|
49
+ {
50
+ id: i,
51
+ predicted: round(@predicted_loads[i] || 0.0),
52
+ actual: round(s[:total_time]),
53
+ tests: s[:files],
54
+ examples: s[:examples]
55
+ }
56
+ end
57
+ end
58
+
59
+ # Drift is reported per file (the scheduling unit), so predicted and actual
60
+ # are compared at the same granularity even when timings are recorded
61
+ # per example. Both sides are aggregated to normalized file paths.
62
+ def drift
63
+ actual = aggregate_by_file(@all_timings.map { |e| [e[:file], e[:time]] })
64
+ predicted = aggregate_by_file(@timings.map { |(file, _name), weight| [file, weight] })
65
+
66
+ (actual.keys | predicted.keys)
67
+ .map { |file| drift_entry(file, predicted[file], actual[file]) }
68
+ .sort_by { |d| -(d[:actual] - d[:predicted]).abs }
69
+ .first(DRIFT_LIMIT)
70
+ end
71
+
72
+ def drift_entry(file, predicted, actual)
73
+ {
74
+ file: file,
75
+ predicted: round(predicted || Timing::DEFAULT_WEIGHT),
76
+ actual: round(actual || 0.0)
77
+ }
78
+ end
79
+
80
+ def aggregate_by_file(pairs)
81
+ pairs.each_with_object(Hash.new(0.0)) do |(file, time), acc|
82
+ acc[normalize_file(file)] += time
83
+ end
84
+ end
85
+
86
+ def normalize_file(file)
87
+ file.to_s.sub(%r{\A\./}, "")
88
+ end
89
+
90
+ def deviation_pct(loads)
91
+ return 0.0 if loads.empty?
92
+ mean = loads.sum / loads.size
93
+ return 0.0 unless mean.positive?
94
+ max_dev = loads.map { |t| (t - mean).abs }.max
95
+ round(max_dev / mean * 100)
96
+ end
97
+
98
+ def round(value)
99
+ value.to_f.round(3)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Binpacker
4
+ # Registry for the gem-shipped, user-facing agent skills. Skills live in the
5
+ # gem's top-level skills/ directory, each as skills/<name>/SKILL.md.
6
+ # See docs/adr/0003-agent-driven-install-and-skills.md.
7
+ module Skills
8
+ ROOT = File.expand_path("../../skills", __dir__)
9
+
10
+ module_function
11
+
12
+ # [{ name:, path: }], sorted by name.
13
+ def list
14
+ Dir.glob(File.join(ROOT, "*", "SKILL.md")).sort.map do |file|
15
+ { name: File.basename(File.dirname(file)), path: file }
16
+ end
17
+ end
18
+
19
+ def exist?(name)
20
+ !path(name).nil?
21
+ end
22
+
23
+ # Absolute path of a skill's SKILL.md, or nil when unknown.
24
+ def path(name)
25
+ file = File.join(ROOT, name, "SKILL.md")
26
+ File.file?(file) ? file : nil
27
+ end
28
+
29
+ # The SKILL.md body for a skill.
30
+ def body(name)
31
+ file = path(name)
32
+ raise Error, "unknown skill: #{name}" unless file
33
+ File.read(file)
34
+ end
35
+ end
36
+ end
@@ -69,7 +69,13 @@ module Binpacker
69
69
 
70
70
  class MinitestDiscovery < TestDiscovery
71
71
  def enumerate
72
- require "minitest"
72
+ begin
73
+ require "minitest"
74
+ rescue LoadError
75
+ raise DiscoveryError,
76
+ "minitest is not available. binpacker supports rspec and minitest; " \
77
+ "test-unit and other frameworks are not supported."
78
+ end
73
79
  add_project_load_paths
74
80
  def Minitest.autorun; end
75
81
  Minitest.seed = 42
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
3
+ require 'json'
4
4
 
5
5
  module Binpacker
6
6
  class Timing
@@ -8,70 +8,173 @@ module Binpacker
8
8
 
9
9
  DEFAULT_WEIGHT = 1.0
10
10
 
11
+ # Samples retained per test by #compact! and consulted by the
12
+ # median in #load_per_file. Three samples make a single anomalous
13
+ # run (GC pause, noisy CI neighbour) unable to move the weight.
14
+ MAX_SAMPLES_PER_TEST = 3
15
+
11
16
  def initialize(path)
12
17
  @path = Pathname(path)
13
18
  end
14
19
 
20
+ # True once any timing samples exist, i.e. the project has been
21
+ # calibrated at least once. Callers use this to tell a measured
22
+ # run (weights in seconds) from a pure cold start (fallbacks only).
23
+ def calibrated?
24
+ !samples_by_test.empty?
25
+ end
26
+
15
27
  def load_with_fallback(tests)
16
28
  per_file = load_per_file
29
+ coefficient = seconds_per_kb(per_file)
17
30
  tests.each_with_object({}) do |test, hash|
18
31
  key = normalize_path(test.file)
19
- hash[test.key] = per_file.fetch(key) { filesize_weight(test.file) }
32
+ hash[test.key] = per_file.fetch(key) { fallback_weight(test.file, coefficient) }
20
33
  end
21
34
  end
22
35
 
23
36
  def load_raw
24
- return {} unless @path.exist?
25
-
26
- @path.each_line(encoding: "UTF-8")
27
- .map { |line| parse_line(line) }
28
- .compact
29
- .group_by { |e| [normalize_path(e.file), e.name] }
30
- .transform_values { |entries| entries.last.time }
37
+ @load_raw ||= samples_by_test.transform_values(&:last)
31
38
  end
32
39
 
40
+ # Predicted weight per file: the median of each test's recent
41
+ # samples, summed per file. The append-only history must NOT be
42
+ # summed wholesale — a file present in N historical runs would
43
+ # weigh ~N times its true cost, so long-lived files dominate and
44
+ # newly added ones are starved, skewing the partition.
33
45
  def load_per_file
34
- return {} unless @path.exist?
46
+ samples_by_test.each_with_object({}) do |((file, _name), times), per_file|
47
+ weight = median(times.last(MAX_SAMPLES_PER_TEST))
48
+ per_file[file] = per_file.fetch(file, 0.0) + weight
49
+ end
50
+ end
35
51
 
36
- @path.each_line(encoding: "UTF-8")
37
- .map { |line| parse_line(line) }
38
- .compact
39
- .group_by { |e| normalize_path(e.file) }
40
- .transform_values { |entries| entries.sum(&:time) }
52
+ # Rewrites the timing file keeping only the most recent
53
+ # MAX_SAMPLES_PER_TEST samples per test, so the append-only
54
+ # history (and any CI cache built from it) stays bounded instead
55
+ # of growing by one run per invocation.
56
+ def compact!
57
+ samples = samples_by_test
58
+ return if samples.empty?
59
+
60
+ tmp = Pathname("#{@path}.tmp")
61
+ tmp.open('w', encoding: 'UTF-8') do |io|
62
+ samples.each do |(file, name), times|
63
+ times.last(MAX_SAMPLES_PER_TEST).each do |time|
64
+ io.puts JSON.generate({ file: file, name: name, time: time })
65
+ end
66
+ end
67
+ end
68
+ File.rename(tmp.to_s, @path.to_s)
69
+ invalidate
41
70
  end
42
71
 
43
72
  def normalize_path(path)
44
- Pathname(path).cleanpath.to_s.sub(/\A\.\//, "")
73
+ Pathname(path).cleanpath.to_s.sub(%r{\A\./}, '')
45
74
  end
46
75
 
47
- def weight_for(file:, name:)
48
- measured = load_raw
49
- measured.fetch([file, name]) { filesize_weight(file) }
76
+ # True when a measured Weight already exists for this Test.
77
+ def measured?(file:, name:)
78
+ load_raw.key?([normalize_path(file), name])
50
79
  end
51
80
 
52
81
  def append(file:, name:, time:)
53
82
  @path.dirname.mkpath unless @path.dirname.directory?
54
- @path.open("a", encoding: "UTF-8") { |io| io.puts JSON.generate({ file: file, name: name, time: time }) }
83
+ @path.open('a', encoding: 'UTF-8') { |io| io.puts JSON.generate({ file: file, name: name, time: time }) }
84
+ invalidate
55
85
  end
56
86
 
57
87
  def append_all(entries)
58
88
  return if entries.empty?
89
+
59
90
  @path.dirname.mkpath unless @path.dirname.directory?
60
- @path.open("a", encoding: "UTF-8") do |io|
91
+ @path.open('a', encoding: 'UTF-8') do |io|
61
92
  entries.each { |e| io.puts JSON.generate({ file: e[:file], name: e[:name], time: e[:time] }) }
62
93
  end
94
+ invalidate
63
95
  end
64
96
 
65
97
  private
66
98
 
99
+ # [normalized file, name] => [sample, ...] in append (= run) order.
100
+ def samples_by_test
101
+ @samples_by_test ||= begin
102
+ samples = Hash.new { |h, k| h[k] = [] }
103
+ if @path.exist?
104
+ @path.each_line(encoding: 'UTF-8') do |line|
105
+ entry = parse_line(line)
106
+ samples[[normalize_path(entry.file), entry.name]] << entry.time if entry
107
+ end
108
+ end
109
+ samples.default_proc = nil
110
+ samples
111
+ end
112
+ end
113
+
114
+ def invalidate
115
+ @samples_by_test = nil
116
+ @load_raw = nil
117
+ end
118
+
119
+ def median(values)
120
+ sorted = values.sort
121
+ mid = sorted.size / 2
122
+ sorted.size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0
123
+ end
124
+
67
125
  def filesize_weight(file)
68
126
  path = Pathname(file)
69
127
  path.exist? ? [path.size / 1024.0, DEFAULT_WEIGHT].max : DEFAULT_WEIGHT
70
128
  end
71
129
 
130
+ # Seconds-per-KB coefficient used to scale filesize fallbacks onto
131
+ # the same axis as measured Weights. Measured Weights are seconds;
132
+ # the raw filesize fallback is KB, so mixing them (batch floors,
133
+ # donor selection) compares apples to oranges — 30s of predicted
134
+ # work reads as "30 KB of files". We estimate a conversion from
135
+ # the files we DO have timings for: for each measured file that
136
+ # still exists on disk with size > 0, take measured_seconds /
137
+ # size_kb, and use the median of those ratios (robust to a few
138
+ # outlier files that are unusually fast or slow for their size).
139
+ # nil when nothing can be estimated (no measurements, or none of
140
+ # the measured files exist on disk) — callers then keep raw KB.
141
+ def seconds_per_kb(per_file)
142
+ ratios = per_file.filter_map do |file, seconds|
143
+ kb = size_kb(file)
144
+ next if kb.nil? || kb <= 0
145
+
146
+ seconds / kb
147
+ end
148
+ ratios.empty? ? nil : median(ratios)
149
+ end
150
+
151
+ # Predicted Weight for an unmeasured Test. Without a coefficient we
152
+ # keep the legacy raw-KB behavior (floored at 1.0). With one we
153
+ # scale filesize into seconds: an existing file becomes
154
+ # size_kb * coefficient (floored at 0.01 to avoid degenerate zero
155
+ # weights, but NOT at 1.0 — sub-second predictions are meaningful
156
+ # now that the unit is seconds); a missing/unreadable file falls
157
+ # back to DEFAULT_WEIGHT as a plausible seconds-scale default.
158
+ def fallback_weight(file, coefficient)
159
+ return filesize_weight(file) if coefficient.nil?
160
+
161
+ kb = size_kb(file)
162
+ return DEFAULT_WEIGHT if kb.nil?
163
+
164
+ [kb * coefficient, 0.01].max
165
+ end
166
+
167
+ # File size in KB, or nil when the file does not exist / is unreadable.
168
+ def size_kb(file)
169
+ path = Pathname(file)
170
+ path.exist? ? path.size / 1024.0 : nil
171
+ rescue SystemCallError
172
+ nil
173
+ end
174
+
72
175
  def parse_line(line)
73
176
  data = JSON.parse(line.strip)
74
- Entry.new(file: data["file"], name: data["name"], time: data["time"])
177
+ Entry.new(file: data['file'], name: data['name'], time: data['time'])
75
178
  rescue JSON::ParserError
76
179
  nil
77
180
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Binpacker
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -37,7 +37,7 @@ module Binpacker
37
37
  end
38
38
 
39
39
  def total_weight(timings)
40
- remaining.sum { |t| timings.fetch(t.key, Timing::DEFAULT_WEIGHT) }
40
+ remaining.sum(0.0) { |t| timings.fetch(t.key, Timing::DEFAULT_WEIGHT) }
41
41
  end
42
42
 
43
43
  def push(test)
data/lib/binpacker.rb CHANGED
@@ -9,6 +9,9 @@ require_relative "binpacker/scheduler"
9
9
  require_relative "binpacker/worker"
10
10
  require_relative "binpacker/test_runner"
11
11
  require_relative "binpacker/calibration"
12
+ require_relative "binpacker/report"
13
+ require_relative "binpacker/skills"
14
+ require_relative "binpacker/project_state"
12
15
  require_relative "binpacker/orchestrator"
13
16
  require_relative "binpacker/progress"
14
17
 
@@ -0,0 +1,6 @@
1
+ module Binpacker
2
+ class Calibration
3
+ def initialize: (untyped) -> void
4
+ def run: (untyped, ?incremental: bool) -> Array[untyped]
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module Binpacker
2
+ class CLI
3
+ def self.start: (untyped) -> nil
4
+ def initialize: (untyped) -> void
5
+ def run: () -> nil
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module Binpacker
2
+ class Config
3
+ def initialize: (?profile: untyped, ?config_path: untyped) -> void
4
+ def worker_count: () -> Integer
5
+ def test_runner: () -> String
6
+ def timing_file: () -> String
7
+ def test_pattern: () -> String
8
+ def test_exclude: () -> Array[String]
9
+ def test_granularity: () -> String
10
+ def report_file: () -> String?
11
+ def profile: () -> String
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ module Binpacker
2
+ class Orchestrator
3
+ MIN_BATCH_WEIGHT: Float
4
+ COLD_START_BATCHES_PER_WORKER: Integer
5
+
6
+ @config: untyped
7
+ @passthrough: untyped
8
+ @quiet: untyped
9
+ @report_path: untyped
10
+ @timings: Hash[untyped, Float]
11
+ @predicted_loads: Array[Float]
12
+
13
+ def initialize: (untyped, ?passthrough: untyped, ?quiet: untyped, ?report_path: untyped) -> void
14
+ def run: () -> { passed: untyped, total: untyped, passed_count: untyped, timings: Array[untyped], empty_filter: untyped }
15
+
16
+ private
17
+
18
+ def discover: () -> untyped
19
+ def run_static: (untyped, untyped, Binpacker::Timing, untyped) -> untyped
20
+ def run_dynamic: (untyped, untyped, Binpacker::Timing, untyped, Float) -> untyped
21
+ def write_report: (untyped, Array[untyped]) -> void
22
+ def min_batch_weight: (Binpacker::Timing, Hash[untyped, Float]) -> Float
23
+ def drain_batch: (Binpacker::WorkerQueue?, Float) -> Array[untyped]
24
+ def finalize: (Binpacker::Timing, Array[untyped], untyped, untyped, untyped, untyped) -> { passed: untyped, total: untyped, passed_count: untyped, timings: Array[untyped], empty_filter: untyped }
25
+ def minitest_empty_filter?: (untyped, untyped) -> bool
26
+ def minitest_include_filter?: () -> bool
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ module Binpacker
2
+ class ProgressDisplay
3
+ def initialize: (untyped, ?tty: untyped) -> void
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ module Binpacker
2
+ class ProjectState
3
+ CONFIG_FILE: String
4
+ DEFAULT_TIMING_FILE: String
5
+ SUPPORTED_FRAMEWORKS: Array[String]
6
+ TESTUNIT_HINT: Regexp
7
+
8
+ def config_present?: () -> bool
9
+ def timing_present?: () -> bool
10
+ def framework: () -> String?
11
+ def supported_framework?: () -> bool
12
+ def ci_wired?: () -> bool
13
+ def recommendation: () -> String
14
+ def to_h: () -> Hash[Symbol, untyped]
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module Binpacker
2
+ class Report
3
+ SCHEMA: Integer
4
+ DRIFT_LIMIT: Integer
5
+
6
+ def initialize: (profile: untyped, algorithm: untyped, predicted_loads: untyped, worker_stats: untyped, all_timings: untyped, timings: untyped) -> void
7
+ def to_h: () -> Hash[Symbol, untyped]
8
+ def write: (untyped) -> untyped
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ module Binpacker
2
+ class Scheduler
3
+ def self.for: (untyped) -> (Binpacker::LptScheduler | Binpacker::MultifitScheduler)
4
+ end
5
+ end