binpacker 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d0c30480b18ff759a0611116ea6c21fd5e5034525aa4ebfa95ab65fb3f2fdf2
4
- data.tar.gz: 9b273d060dfbef0b2812bc2617e7eeaed00bee6247c6381d51d8b075132b28e8
3
+ metadata.gz: 991ac5d941b69573b4f6d03c211ff862465a91ee31ddd82f5ce8d8150a2d4f0d
4
+ data.tar.gz: 8ad2f245f73a46a4fdd64272049835c36d3b2dc4a0bb5df9cf67f851789ae59a
5
5
  SHA512:
6
- metadata.gz: d765fe4fc0741dbd13445707da9f937e4571090697c6767ddfa9ec3391e0a9a9a76935fab3ba820eb410e5bcb1d69c617668e514dd4656c46134b1a15cc87097
7
- data.tar.gz: 03cd6766727a30e69e259c356b53461ac54bbed7d454904535a91e9e448c4b3fe6750104e63bf7bd1ceed5eb111f47f11e88231397d58ae52304339dcdbbfca9
6
+ metadata.gz: 4345168c76cd0e072ab8f0775df54fe705e13f873a1f7f8ce764b897f42f91f75c5b4b0dee07329b577200230a7707a90284e73b554a339798575e084ce93f15
7
+ data.tar.gz: 40408cf8b88b878033226df711fbf3e57c217293b85b6eb7f94f55289eb7740d02dcc66598c5f594a3cd21821b5186eb38942c7e03747838a6487ccc37e018d1
data/exe/binpacker-worker CHANGED
@@ -3,19 +3,34 @@
3
3
 
4
4
  require "json"
5
5
  require "optparse"
6
+ require "set"
6
7
  require "tempfile"
7
8
 
9
+ begin
10
+ require "minitest"
11
+ rescue LoadError
12
+ # Minitest not installed — rspec-only use
13
+ end
14
+
8
15
  def run_rspec(files, wid, rspec_args)
9
16
  outfile = Tempfile.new("binpacker-rspec-out")
17
+ progress_outfile = Tempfile.new("binpacker-rspec-progress")
10
18
  outfile.close
19
+ progress_outfile.close
20
+
11
21
  cmd = [
12
22
  "rspec",
13
23
  "--format", "json", "--out", outfile.path,
14
- "--format", "progress", "--out", "/dev/stderr",
24
+ "--format", "progress", "--out", progress_outfile.path,
15
25
  *rspec_args,
16
26
  *files
17
27
  ]
18
- success = system(*cmd)
28
+ system(*cmd, exception: false)
29
+ exit_status = $?.exitstatus || 1
30
+
31
+ if File.exist?(progress_outfile.path) && File.size(progress_outfile.path) > 0
32
+ $stderr.write File.read(progress_outfile.path, encoding: "UTF-8")
33
+ end
19
34
 
20
35
  data = (File.exist?(outfile.path) && File.size(outfile.path) > 0) ? JSON.parse(File.read(outfile.path, encoding: "UTF-8")) : {}
21
36
  examples = data["examples"] || []
@@ -25,65 +40,170 @@ def run_rspec(files, wid, rspec_args)
25
40
  { file: ex["file_path"], name: ex["full_description"] || ex["description"], time: ex["run_time"] || 0.0 }
26
41
  end
27
42
 
28
- outfile.unlink
29
- [$?.exitstatus || 1, results, examples.size, (summary["failure_count"] || 0) == 0 ? examples.size : examples.size - summary["failure_count"]]
43
+ [exit_status, results, examples.size, (summary["failure_count"] || 0) == 0 ? examples.size : examples.size - summary["failure_count"]]
44
+ ensure
45
+ outfile&.unlink
46
+ progress_outfile&.unlink
47
+ end
48
+
49
+ def add_project_load_paths
50
+ %w[lib test].each do |path|
51
+ expanded = File.expand_path(path)
52
+ $LOAD_PATH.unshift(expanded) if Dir.exist?(expanded) && !$LOAD_PATH.include?(expanded)
53
+ end
54
+ end
55
+
56
+ def run_minitest_after_run_hooks
57
+ return unless Minitest.class_variable_defined?(:@@after_run)
58
+
59
+ Minitest.class_variable_get(:@@after_run).reverse_each(&:call)
60
+ end
61
+
62
+ def run_minitest(tests, wid, minitest_args)
63
+ unless defined?(Minitest)
64
+ $stderr.puts "[worker-#{wid}] minitest is not available"
65
+ return [1, [], 0, 0]
66
+ end
67
+
68
+ def Minitest.autorun; end
69
+ add_project_load_paths
70
+
71
+ options = Minitest.process_args(minitest_args.dup)
72
+ options[:io] = $stderr
73
+ Minitest.seed = options[:seed]
74
+ srand Minitest.seed
75
+
76
+ selected = []
77
+ tests.group_by { |test| test["file"] }.each do |file, test_entries|
78
+ path = File.expand_path(file)
79
+ requested_names = test_entries.map { |test| test["name"] }.to_set
80
+ klasses_before = Minitest::Runnable.runnables.dup
81
+ $stderr.puts "[worker-#{wid}] loading #{file}"
82
+ load path
83
+
84
+ (Minitest::Runnable.runnables - klasses_before).each do |klass|
85
+ klass.filter_runnable_methods(options).each do |method_name|
86
+ full_name = "#{klass}##{method_name}"
87
+ selected << [klass, method_name, file, full_name] if requested_names.include?(full_name)
88
+ end
89
+ end
90
+ rescue StandardError => e
91
+ $stderr.puts "[worker-#{wid}] minitest load failed for #{file}: #{e.class}: #{e.message}"
92
+ return [1, [], 0, 0]
93
+ end
94
+
95
+ return [0, [], 0, 0] if selected.empty?
96
+
97
+ timing_reporter = TimingReporter.new
98
+ reporter = Minitest::CompositeReporter.new
99
+ reporter << Minitest::SummaryReporter.new($stderr, options)
100
+ reporter << Minitest::ProgressReporter.new($stderr, options) unless options[:quiet]
101
+ reporter << timing_reporter
102
+
103
+ Minitest.reporter = reporter
104
+ Minitest.init_plugins(options)
105
+ Minitest.reporter = nil
106
+
107
+ selected_files = selected.to_h { |_klass, _method_name, file, full_name| [full_name, file] }
108
+
109
+ Minitest.parallel_executor.start if Minitest.parallel_executor.respond_to?(:start)
110
+ reporter.start
111
+ hook_error = nil
112
+ begin
113
+ selected.each do |klass, method_name, _file, _full_name|
114
+ klass.run(klass, method_name, reporter)
115
+ end
116
+ ensure
117
+ Minitest.parallel_executor.shutdown if Minitest.parallel_executor.respond_to?(:shutdown)
118
+ reporter.report
119
+ begin
120
+ run_minitest_after_run_hooks
121
+ rescue StandardError => e
122
+ hook_error = e
123
+ $stderr.puts "[worker-#{wid}] minitest after_run failed: #{e.class}: #{e.message}"
124
+ end
125
+ end
126
+
127
+ failures = timing_reporter.results.count { |result| !result.passed? }
128
+ results = timing_reporter.results.map do |result|
129
+ full_name = "#{result.klass}##{result.name}"
130
+ {
131
+ file: selected_files.fetch(full_name, result.source_location.first),
132
+ name: full_name,
133
+ time: result.time
134
+ }
135
+ end
136
+
137
+ exit_code = reporter.passed? && hook_error.nil? ? 0 : 1
138
+ passed = results.size - failures
139
+ [exit_code, results, results.size, passed]
30
140
  end
31
141
 
32
- def run_minitest(files, wid, _args)
33
- results = []
34
- exit_code = 0
35
- files.each do |file|
36
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
37
- s = system("ruby", "-Ilib:test", file, exception: false)
38
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
39
- results << { file: file, name: file, time: elapsed }
40
- exit_code = s.exitstatus unless s.success?
142
+ if defined?(Minitest)
143
+ class TimingReporter < Minitest::AbstractReporter
144
+ attr_reader :results
145
+
146
+ def initialize
147
+ super()
148
+ @results = []
149
+ end
150
+
151
+ def record(result)
152
+ @results << result
153
+ end
41
154
  end
42
- [exit_code, results, results.size, exit_code == 0 ? results.size : 0]
43
155
  end
44
156
 
45
157
  options = { runner: "rspec", id: 0 }
46
158
  rspec_args = []
159
+ minitest_args = []
47
160
  OptionParser.new do |opts|
48
161
  opts.on("--runner RUNNER", "rspec or minitest") { |v| options[:runner] = v }
49
162
  opts.on("--id ID", "Worker ID") { |v| options[:id] = v.to_i }
50
163
  opts.on("--rspec-arg ARG", "Pass-through to rspec") { |v| rspec_args << v }
164
+ opts.on("--minitest-arg ARG", "Pass-through to minitest") { |v| minitest_args << v }
51
165
  end.parse!
52
166
 
53
167
  wid = options[:id]
168
+ exit_code = 0
54
169
  $stderr.puts "[worker-#{wid}] #{options[:runner]} worker started"
55
- $stdout.puts JSON.generate({ type: "ready" })
56
- $stdout.flush
57
-
58
- tests = []
59
- $stdin.each_line do |line|
60
- data = JSON.parse(line.strip)
61
- break if data["type"] == "done"
62
- tests << data
63
- rescue JSON::ParserError
64
- $stderr.puts "[worker-#{wid}] skip: #{line.strip[0..60]}"
65
- end
66
170
 
67
- if tests.empty?
68
- $stdout.puts JSON.generate({ type: "result", exit_code: 0, passed: true, total: 0, passed_count: 0 })
171
+ loop do
172
+ $stdout.puts JSON.generate({ type: "ready" })
69
173
  $stdout.flush
70
- exit 0
71
- end
72
174
 
73
- files = tests.map { |t| t["file"] }.uniq
74
- $stderr.puts "[worker-#{wid}] #{files.size} files, #{tests.size} test entries"
175
+ tests = []
176
+ done_or_eof = false
177
+ $stdin.each_line do |line|
178
+ data = JSON.parse(line.strip)
179
+ if data["type"] == "done"
180
+ done_or_eof = true
181
+ break
182
+ end
183
+ tests << data
184
+ rescue JSON::ParserError
185
+ $stderr.puts "[worker-#{wid}] skip: #{line.strip[0..60]}"
186
+ end
75
187
 
76
- exit_code, results, total, passed = case options[:runner]
77
- when "rspec" then run_rspec(files, wid, rspec_args)
78
- when "minitest" then run_minitest(files, wid, [])
79
- else
80
- $stderr.puts "[worker-#{wid}] unknown runner: #{options[:runner]}"
81
- [1, [], 0, 0]
82
- end
188
+ break if done_or_eof && tests.empty?
189
+ break if tests.empty? && ($stdin.eof? || $stdin.closed?)
190
+
191
+ files = tests.map { |t| t["file"] }.uniq
192
+ $stderr.puts "[worker-#{wid}] batch: #{files.size} files, #{tests.size} entries"
83
193
 
84
- results.each do |r|
85
- $stdout.puts JSON.generate({ type: "timing", file: r[:file], name: r[:name], time: r[:time] })
194
+ exit_code, results, total, passed = case options[:runner]
195
+ when "rspec" then run_rspec(files, wid, rspec_args)
196
+ when "minitest" then run_minitest(tests, wid, minitest_args)
197
+ else
198
+ $stderr.puts "[worker-#{wid}] unknown runner: #{options[:runner]}"
199
+ [1, [], 0, 0]
200
+ end
201
+
202
+ results.each do |r|
203
+ $stdout.puts JSON.generate({ type: "timing", file: r[:file], name: r[:name], time: r[:time] })
204
+ end
205
+ $stdout.puts JSON.generate({ type: "batch_result", exit_code: exit_code, passed: exit_code == 0, total: total, passed_count: passed })
206
+ $stdout.flush
86
207
  end
87
- $stdout.puts JSON.generate({ type: "result", exit_code: exit_code, passed: exit_code == 0, total: total, passed_count: passed })
88
- $stdout.flush
89
- exit exit_code
208
+
209
+ exit exit_code || 0
@@ -13,10 +13,13 @@ module Binpacker
13
13
  @timing = Timing.new(config.timing_file)
14
14
  end
15
15
 
16
- def run(tests)
16
+ # Runs each Test serially and appends its measured Weight to the Timing file.
17
+ # With incremental: true, Tests that already have a measured Weight are skipped.
18
+ def run(tests, incremental: false)
19
+ targets = incremental ? unmeasured(tests) : tests
17
20
  results = []
18
21
 
19
- tests.each do |test|
22
+ targets.each do |test|
20
23
  elapsed = run_single(test)
21
24
  results << { file: test.file, name: test.name, time: elapsed }
22
25
  end
@@ -27,21 +30,30 @@ module Binpacker
27
30
 
28
31
  private
29
32
 
33
+ def unmeasured(tests)
34
+ measured = @timing.load_raw
35
+ tests.reject { |test| measured.key?([@timing.normalize_path(test.file), test.name]) }
36
+ end
37
+
30
38
  def run_single(test)
31
39
  outfile = Tempfile.new("binpacker-cal")
32
40
  outfile.close
33
41
 
34
42
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
35
43
 
44
+ # At file granularity the Test's name is its file path (see Test
45
+ # discovery), so there is no single example/method to filter — run the
46
+ # whole file. A per-test filter would match nothing and measure only
47
+ # load time.
48
+ file_unit = test.name == test.file
49
+
36
50
  case @config.test_runner
37
51
  when "rspec"
38
- cmd = [
39
- "rspec", test.file,
40
- "--example", test.name,
41
- "--format", "json", "--out", outfile.path
42
- ]
52
+ cmd = ["rspec", test.file, "--format", "json", "--out", outfile.path]
53
+ cmd.push("--example", test.name) unless file_unit
43
54
  when "minitest"
44
- cmd = ["ruby", "-Ilib:test", test.file, "--name", test.name]
55
+ cmd = ["ruby", "-Ilib:test", test.file]
56
+ cmd.push("--name", "/^#{Regexp.escape(test.name)}$/") unless file_unit
45
57
  else
46
58
  raise ConfigError, "unsupported runner for calibration: #{@config.test_runner}"
47
59
  end
data/lib/binpacker/cli.rb CHANGED
@@ -14,6 +14,13 @@ module Binpacker
14
14
  @command = nil
15
15
  @profile = nil
16
16
  @passthrough = []
17
+ @quiet = false
18
+ @incremental = false
19
+ @report_path = nil
20
+ @skill_list = false
21
+ @skill_path = false
22
+ @skill_describe = false
23
+ @skill_name = nil
17
24
  parse!
18
25
  end
19
26
 
@@ -23,6 +30,12 @@ module Binpacker
23
30
  cmd_calibrate
24
31
  when "run"
25
32
  cmd_run
33
+ when "init"
34
+ cmd_init
35
+ when "skill"
36
+ cmd_skill
37
+ when "describe"
38
+ cmd_describe
26
39
  when "--version", "-v"
27
40
  puts "binpacker #{Binpacker::VERSION}"
28
41
  when "--help", "-h", nil
@@ -51,6 +64,30 @@ module Binpacker
51
64
  opts.on("--help", "Show help") do
52
65
  @command ||= "--help"
53
66
  end
67
+
68
+ opts.on("--quiet", "Suppress worker output") do
69
+ @quiet = true
70
+ end
71
+
72
+ opts.on("--incremental", "Calibrate only tests without timing data") do
73
+ @incremental = true
74
+ end
75
+
76
+ opts.on("--report PATH", "(run) Write a JSON run report to PATH") do |v|
77
+ @report_path = v
78
+ end
79
+
80
+ opts.on("--list", "(skill) List bundled skills") do
81
+ @skill_list = true
82
+ end
83
+
84
+ opts.on("--path", "(skill) Print the SKILL.md path for <name>") do
85
+ @skill_path = true
86
+ end
87
+
88
+ opts.on("--describe", "(skill) Report project state and next skill") do
89
+ @skill_describe = true
90
+ end
54
91
  end
55
92
 
56
93
  # Extract passthrough arguments after "--"
@@ -60,7 +97,50 @@ module Binpacker
60
97
  end
61
98
 
62
99
  remaining = parser.parse(@args)
63
- @command = remaining.shift
100
+ # A positional command wins; otherwise keep any command an option set
101
+ # (e.g. --version), so `binpacker --version` isn't clobbered to nil.
102
+ @command = remaining.shift || @command
103
+ @skill_name = remaining.shift
104
+ end
105
+
106
+ def cmd_init
107
+ config_path = Pathname.pwd.join("binpacker.yml")
108
+ if config_path.exist?
109
+ puts "binpacker.yml already exists at #{config_path}"
110
+ exit 1
111
+ end
112
+
113
+ framework = detect_framework
114
+ unless ProjectState::SUPPORTED_FRAMEWORKS.include?(framework)
115
+ $stderr.puts "Detected #{framework} — binpacker supports rspec and minitest only."
116
+ $stderr.puts "No binpacker.yml was created."
117
+ exit 1
118
+ end
119
+ pattern = framework == "minitest" ? "test/**/*_test.rb" : "spec/**/*_spec.rb"
120
+ runner = framework
121
+
122
+ yaml = <<~YAML
123
+ profiles:
124
+ default:
125
+ test_runner: #{runner}
126
+ workers: auto
127
+ timing_file: binpacker.timings
128
+ test_pattern: "#{pattern}"
129
+ scheduler:
130
+ algorithm: multifit
131
+ steal_enabled: true
132
+ ci:
133
+ extends: default
134
+ workers: 4
135
+ YAML
136
+
137
+ config_path.write(yaml)
138
+ puts "Created #{config_path}"
139
+ puts "Detected test framework: #{framework}"
140
+ puts ""
141
+ puts "Next steps:"
142
+ puts " 1. binpacker calibrate (seed timing data)"
143
+ puts " 2. binpacker run (run in parallel)"
64
144
  end
65
145
 
66
146
  def cmd_calibrate
@@ -68,32 +148,121 @@ module Binpacker
68
148
  discovery_klass = config.test_runner == "rspec" ? RSpecDiscovery : MinitestDiscovery
69
149
  tests = discovery_klass.new(config).enumerate
70
150
 
71
- puts "Calibrating #{tests.size} tests..."
72
151
  cal = Calibration.new(config)
73
- timings = cal.run(tests)
152
+
153
+ if @incremental
154
+ puts "Calibrating (incremental) #{tests.size} tests..."
155
+ else
156
+ puts "Calibrating #{tests.size} tests..."
157
+ end
158
+
159
+ timings = cal.run(tests, incremental: @incremental)
74
160
 
75
161
  total = timings.sum { |t| t[:time] }
76
- puts "Calibration complete: #{tests.size} tests in #{total.round(2)}s"
162
+ measured = timings.size
163
+ skipped = tests.size - measured
164
+ summary = "Calibration complete: #{measured} #{pluralize(measured, 'test')} in #{total.round(2)}s"
165
+ summary += " (#{skipped} skipped, already measured)" if @incremental && skipped.positive?
166
+ puts summary
77
167
  puts "Timing data written to #{config.timing_file}"
78
168
  end
79
169
 
170
+ def cmd_skill
171
+ return cmd_describe if @skill_describe
172
+
173
+ if @skill_path
174
+ path = Skills.path(@skill_name)
175
+ if path
176
+ puts path
177
+ else
178
+ $stderr.puts "unknown skill: #{@skill_name}"
179
+ exit 1
180
+ end
181
+ elsif @skill_name && !@skill_list
182
+ print_skill(@skill_name)
183
+ else
184
+ list_skills
185
+ end
186
+ end
187
+
188
+ def cmd_describe
189
+ state = ProjectState.new
190
+ puts "binpacker project state:"
191
+ puts " config (binpacker.yml): #{state.config_present? ? 'present' : 'missing'}"
192
+ puts " timing data: #{state.timing_present? ? 'present' : 'missing'}"
193
+ puts " test framework: #{state.framework || 'not detected'}"
194
+ puts " CI wired for binpacker: #{state.ci_wired? ? 'yes' : 'no'}"
195
+ puts ""
196
+
197
+ unless state.supported_framework?
198
+ puts "binpacker supports rspec and minitest; #{state.framework} is not supported yet."
199
+ puts "Setup cannot proceed against a #{state.framework} suite."
200
+ return
201
+ end
202
+
203
+ rec = state.recommendation
204
+ puts "Recommended next skill: #{rec}"
205
+ puts "Run `binpacker skill #{rec}` to load its instructions."
206
+ end
207
+
208
+ def list_skills
209
+ skills = Skills.list
210
+ if skills.empty?
211
+ puts "No bundled skills found."
212
+ return
213
+ end
214
+ puts "Bundled skills:"
215
+ skills.each { |s| puts " #{s[:name]}" }
216
+ puts ""
217
+ puts "Print a skill's instructions with `binpacker skill <name>`."
218
+ end
219
+
220
+ def print_skill(name)
221
+ unless Skills.exist?(name)
222
+ $stderr.puts "unknown skill: #{name}"
223
+ $stderr.puts "Run `binpacker skill` to list bundled skills."
224
+ exit 1
225
+ end
226
+
227
+ puts "# Skill: #{name}"
228
+ puts "# Source: #{Skills.path(name)}"
229
+ puts ""
230
+ puts Skills.body(name)
231
+ end
232
+
80
233
  def cmd_run
81
234
  config = Config.new(profile: @profile)
82
- orchestrator = Orchestrator.new(config, passthrough: @passthrough)
235
+ report_path = @report_path || config.report_file
236
+ orchestrator = Orchestrator.new(
237
+ config, passthrough: @passthrough, quiet: @quiet, report_path: report_path
238
+ )
83
239
 
84
240
  puts "binpacker starting (#{config.worker_count} workers, profile: #{config.profile})"
85
241
  result = orchestrator.run
242
+ puts "Run report written to #{report_path}" if report_path
243
+ unit = test_unit_label(config)
86
244
 
87
245
  if result[:passed]
88
- puts "All #{result[:total]} examples passed across #{config.worker_count} workers."
246
+ puts "All #{result[:total]} #{pluralize(result[:total], unit)} passed across #{config.worker_count} workers."
89
247
  exit 0
248
+ elsif result[:empty_filter]
249
+ puts "No tests matched the Minitest filter."
250
+ exit 1
90
251
  else
91
252
  failed = result[:total] - result[:passed_count]
92
- puts "#{failed}/#{result[:total]} examples failed."
253
+ puts "#{failed}/#{result[:total]} #{pluralize(failed, unit)} failed."
93
254
  exit 1
94
255
  end
95
256
  end
96
257
 
258
+ def test_unit_label(config)
259
+ config.test_runner == "rspec" ? "example" : "test"
260
+ end
261
+
262
+ def pluralize(count, word)
263
+ count == 1 ? word : "#{word}s"
264
+ end
265
+
97
266
  def print_help
98
267
  puts <<~HELP
99
268
  binpacker #{Binpacker::VERSION}
@@ -101,16 +270,31 @@ module Binpacker
101
270
  Commands:
102
271
  run Execute tests across worker processes
103
272
  calibrate Run tests serially to generate timing data
273
+ init Create binpacker.yml with auto-detected settings
274
+ describe Report project state and recommend the next skill
275
+ skill List or print bundled agent skills
104
276
 
105
277
  Options:
106
278
  --profile NAME Select profile from binpacker.yml
279
+ --incremental (calibrate) Measure only tests without timing data
280
+ --report PATH (run) Write a JSON run report to PATH
281
+ --list (skill) List bundled skills
282
+ --path (skill) Print the SKILL.md path for <name>
283
+ --describe (skill) Report project state and next skill
107
284
  --help Show this message
108
285
 
109
286
  Examples:
287
+ binpacker init
110
288
  binpacker run --profile ci
111
289
  binpacker run -- --tag ~slow
112
- binpacker calibrate
290
+ binpacker calibrate --incremental
291
+ binpacker describe
292
+ binpacker skill binpacker-setup
113
293
  HELP
114
294
  end
295
+
296
+ def detect_framework
297
+ ProjectState.new.framework || "rspec"
298
+ end
115
299
  end
116
300
  end
@@ -10,13 +10,15 @@ module Binpacker
10
10
  DEFAULTS = {
11
11
  "test_runner" => "rspec",
12
12
  "workers" => "auto",
13
+ "test_granularity" => "file",
13
14
  "timing_file" => "binpacker.timings",
14
15
  "test_pattern" => "spec/**/*_spec.rb",
15
16
  "test_exclude" => [],
17
+ "report_file" => nil,
16
18
  "scheduler" => {
17
19
  "strategy" => "static",
18
20
  "steal_enabled" => false,
19
- "algorithm" => "lpt"
21
+ "algorithm" => "multifit"
20
22
  }
21
23
  }.freeze
22
24
 
@@ -77,6 +79,7 @@ module Binpacker
77
79
 
78
80
  def build_profile(name)
79
81
  profiles = @raw.fetch("profiles", {})
82
+ return DEFAULTS.dup if profiles.empty? && name == "default"
80
83
  entry = profiles[name]
81
84
  raise ConfigError, "profile '#{name}' not found in binpacker.yml" unless entry
82
85
  parent = entry["extends"]