judges 0.60.3 → 0.60.4

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,8 +7,8 @@ require 'backtrace'
7
7
  require 'elapsed'
8
8
  require 'factbase'
9
9
  require 'factbase/churn'
10
- require 'factbase/logged'
11
10
  require 'factbase/fact_as_yaml'
11
+ require 'factbase/logged'
12
12
  require 'logger'
13
13
  require 'tago'
14
14
  require 'timeout'
@@ -21,7 +21,7 @@ require_relative '../../judges/to_rel'
21
21
 
22
22
  # The +update+ command.
23
23
  #
24
- # This class is instantiated by the +bin/judge+ command line interface. You
24
+ # This class is instantiated by the +bin/judges+ command line interface. You
25
25
  # are not supposed to instantiate it yourself.
26
26
  #
27
27
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -40,31 +40,13 @@ class Judges::Update
40
40
  # @param [Array] args List of command line arguments
41
41
  # @raise [RuntimeError] If not exactly two arguments provided or directory is missing
42
42
  def run(opts, args)
43
- raise 'Exactly two arguments required' unless args.size == 2
43
+ raise(ArgumentError, 'Exactly two arguments required') unless args.size == 2
44
44
  dir = args[0]
45
- raise "The directory is absent: #{dir.to_rel}" unless File.exist?(dir)
45
+ raise(StandardError, "The directory is absent: #{dir.to_rel}") unless File.exist?(dir)
46
46
  impex = Judges::Impex.new(@loog, args[1])
47
47
  fb = impex.import(strict: false)
48
48
  fb = Factbase::Logged.new(fb, @loog) if opts['log']
49
- options = Judges::Options.new(timeout: opts['timeout']&.to_i, lifetime: opts['lifetime']&.to_i)
50
- if options.lifetime && options.timeout && options.lifetime < options.timeout * 1.1
51
- raise "The --timeout=#{options.timeout} must be at least 10 percent smaller than --lifetime=#{options.lifetime}"
52
- end
53
- options += Judges::Options.new(opts['option'])
54
- if opts['options-file']
55
- options += Judges::Options.new(
56
- File.readlines(opts['options-file'])
57
- .compact
58
- .reject(&:empty?)
59
- .map { |ln| ln.strip.split('=', 1).map(&:strip).join('=') }
60
- )
61
- @loog.debug("Options loaded from #{opts['options-file']}")
62
- end
63
- if options.empty?
64
- @loog.debug('No options provided by the --option flag')
65
- else
66
- @loog.debug("The following options provided:\n\t#{options.to_s.gsub("\n", "\n\t")}")
67
- end
49
+ options = build_options(opts)
68
50
  judges = Judges::Judges.new(
69
51
  dir, opts['lib'], @loog,
70
52
  epoch: @epoch, shuffle: opts['shuffle'], boost: opts['boost'],
@@ -77,7 +59,7 @@ class Judges::Update
77
59
  end
78
60
  rescue Timeout::Error, Timeout::ExitException => e
79
61
  @loog.error("Terminated due to --lifetime=#{opts['lifetime']}")
80
- raise e unless opts['quiet']
62
+ raise(e) unless opts['quiet']
81
63
  @loog.info("Had to stop due to the --lifetime=#{opts['lifetime']}")
82
64
  ensure
83
65
  impex.export(fb)
@@ -90,20 +72,52 @@ class Judges::Update
90
72
 
91
73
  private
92
74
 
93
- def loop_them(judges, fb, churn, opts, options)
94
- c = 0
95
- ch = Factbase::Churn.new
96
- errors = []
97
- statistics = opts['statistics'] ? Judges::Statistics.new : nil
75
+ def build_options(opts)
76
+ options = Judges::Options.new(timeout: opts['timeout']&.to_i, lifetime: opts['lifetime']&.to_i)
77
+ if options.lifetime && options.timeout && options.lifetime < options.timeout * 1.1
78
+ raise(
79
+ StandardError,
80
+ "The --timeout=#{options.timeout} must be at least 10 percent smaller than --lifetime=#{options.lifetime}"
81
+ )
82
+ end
83
+ options += Judges::Options.new(opts['option'])
84
+ if opts['options-file']
85
+ options += Judges::Options.new(
86
+ File.readlines(opts['options-file'])
87
+ .compact
88
+ .reject(&:empty?)
89
+ .map { |ln| ln.strip.split('=', 1).map(&:strip).join('=') }
90
+ )
91
+ @loog.debug("Options loaded from #{opts['options-file']}")
92
+ end
93
+ if options.empty?
94
+ @loog.debug('No options provided by the --option flag')
95
+ else
96
+ @loog.debug("The following options provided:\n\t#{options.to_s.gsub("\n", "\n\t")}")
97
+ end
98
+ options
99
+ end
100
+
101
+ def log_summary(opts, fb)
98
102
  sum = fb.query('(eq what "judges-summary")').each.to_a
99
103
  if sum.empty?
100
104
  @loog.info('Summary fact not found') unless opts['summary'] == 'off'
101
105
  else
102
106
  @loog.info("Summary fact found:\n\t#{Factbase::FactAsYaml.new(sum.first).to_s.gsub("\n", "\n\t")}")
103
107
  end
104
- if !sum.empty? && opts['summary'] == 'add' && fb.query('(eq what "judges-summary")').delete!
105
- @loog.info('Summary fact deleted')
106
- end
108
+ return if sum.empty?
109
+ return unless opts['summary'] == 'add'
110
+ fb.query('(eq what "judges-summary")').delete!
111
+ @loog.info('Summary fact deleted')
112
+ end
113
+
114
+ # rubocop:disable Metrics/MethodLength
115
+ def loop_them(judges, fb, churn, opts, options)
116
+ c = 0
117
+ ch = Factbase::Churn.new
118
+ errors = []
119
+ statistics = opts['statistics'] ? Judges::Statistics.new : nil
120
+ log_summary(opts, fb)
107
121
  elapsed(@loog, level: Logger::INFO) do
108
122
  loop do
109
123
  c += 1
@@ -131,11 +145,12 @@ class Judges::Update
131
145
  end
132
146
  @loog.info("The cycle #{c} did #{delta}")
133
147
  end
134
- throw :"👍 Update completed in #{c} cycle(s), did #{ch}"
148
+ throw(:"👍 Update completed in #{c} cycle(s), did #{ch}")
135
149
  end
136
150
  statistics&.report(@loog)
137
151
  summarize(fb, ch, errors, c) if %w[add append].include?(opts['summary'])
138
152
  end
153
+ # rubocop:enable Metrics/MethodLength
139
154
 
140
155
  # Update the summary.
141
156
  # @param [Factbase] fb The factbase
@@ -186,58 +201,71 @@ class Judges::Update
186
201
  used = 0
187
202
  elapsed(@loog, level: Logger::INFO) do
188
203
  done =
189
- judges.each_with_index do |judge, i|
190
- if opts['fail-fast'] && !errors.empty?
191
- @loog.info("Not running #{judge.name.inspect} due to #{errors.count} errors above, in --fail-fast mode")
192
- statistics&.record(judge.name, 0, 'SKIPPED (fail-fast)') if include?(opts, judge.name)
193
- next
194
- end
195
- if opts['lifetime'] && opts['timeout']
196
- remained = @start + opts['lifetime'] - Time.now
197
- if remained < opts['timeout'].to_f / 16
198
- @loog.info("Not running #{judge.name.inspect}, not enough time left (just #{remained.seconds})")
199
- statistics&.record(judge.name, 0, 'SKIPPED (timeout)') if include?(opts, judge.name)
200
- next
201
- end
202
- end
203
- next unless include?(opts, judge.name)
204
- @loog.info("\n👉 Running #{judge.name} (##{i}) at #{judge.dir.to_rel} (#{@start.ago} already)...")
205
- used += 1
206
- start_time = Time.now
207
- result = 'OK'
208
- impact = nil
209
- elapsed(@loog, level: Logger::INFO) do
210
- impact = one_judge(opts, fb, judge, global, options, errors)
211
- delta += impact
212
- churn.append(impact.inserted, impact.deleted, impact.added)
213
- throw :"👍 The '#{judge.name}' judge made zero changes to #{fb.size} facts" if impact.zero?
214
- throw :"👍 The '#{judge.name}' judge #{impact} out of #{fb.size} facts"
215
- end
216
- rescue StandardError, SyntaxError => e
217
- if e.is_a?(RuntimeError) && e.message == 'skip'
218
- result = 'SKIPPED'
219
- else
220
- @loog.warn(Backtrace.new(e))
221
- errors << e.message
222
- result = 'ERROR'
204
+ judges.each_with_index do |judge, idx|
205
+ result = run_judge_in_cycle(judge, idx, opts, fb, churn, options, errors, global, statistics)
206
+ if result
207
+ used += 1
208
+ delta += result if result.is_a?(Factbase::Churn)
223
209
  end
224
- ensure
225
- statistics&.record(judge.name, Time.now - start_time, result, impact) if start_time
226
210
  end
227
- throw :"👍 #{done} judge(s) processed" if errors.empty?
228
- throw :"❌ #{done} judge(s) processed with #{errors.size} errors"
211
+ throw(:"👍 #{done} judge(s) processed") if errors.empty?
212
+ throw(:"❌ #{done} judge(s) processed with #{errors.size} errors")
229
213
  end
230
214
  if used.zero?
231
- raise 'No judges were used, while at least one expected to run' if opts['expect-judges']
215
+ raise(StandardError, 'No judges were used, while at least one expected to run') if opts['expect-judges']
232
216
  @loog.info('No judges were used (looks like an error); not failing because of --no-expect-judges')
233
217
  end
234
218
  unless errors.empty?
235
- raise "Failed to update correctly (#{errors.size} errors)" unless opts['quiet']
219
+ raise(StandardError, "Failed to update correctly (#{errors.size} errors)") unless opts['quiet']
236
220
  @loog.info('Not failing because of the --quiet flag provided')
237
221
  end
238
222
  delta
239
223
  end
240
224
 
225
+ def run_judge_in_cycle(judge, idx, opts, fb, churn, options, errors, global, statistics)
226
+ return if skip_judge?(judge, idx, opts, errors, statistics)
227
+ return unless include?(opts, judge.name)
228
+ @loog.info("\n👉 Running #{judge.name} (##{idx}) at #{judge.dir.to_rel} (#{@start.ago} already)...")
229
+ start = Time.now
230
+ result = 'OK'
231
+ impact = nil
232
+ elapsed(@loog, level: Logger::INFO) do
233
+ impact = one_judge(opts, fb, judge, global, options, errors)
234
+ churn.append(impact.inserted, impact.deleted, impact.added)
235
+ throw(:"👍 The '#{judge.name}' judge made zero changes to #{fb.size} facts") if impact.zero?
236
+ throw(:"👍 The '#{judge.name}' judge #{impact} out of #{fb.size} facts")
237
+ end
238
+ impact
239
+ rescue StandardError, SyntaxError => e
240
+ if e.is_a?(RuntimeError) && e.message == 'skip'
241
+ result = 'SKIPPED'
242
+ else
243
+ @loog.warn(Backtrace.new(e))
244
+ errors << e.message
245
+ result = 'ERROR'
246
+ end
247
+ impact || true
248
+ ensure
249
+ statistics&.record(judge.name, Time.now - start, result, impact) if start
250
+ end
251
+
252
+ def skip_judge?(judge, _idx, opts, errors, statistics)
253
+ if opts['fail-fast'] && !errors.empty?
254
+ @loog.info("Not running #{judge.name.inspect} due to #{errors.count} errors above, in --fail-fast mode")
255
+ statistics&.record(judge.name, 0, 'SKIPPED (fail-fast)') if include?(opts, judge.name)
256
+ return true
257
+ end
258
+ if opts['lifetime'] && opts['timeout']
259
+ remained = @start + opts['lifetime'] - Time.now
260
+ if remained < opts['timeout'].to_f / 16
261
+ @loog.info("Not running #{judge.name.inspect}, not enough time left (just #{remained.seconds})")
262
+ statistics&.record(judge.name, 0, 'SKIPPED (timeout)') if include?(opts, judge.name)
263
+ return true
264
+ end
265
+ end
266
+ false
267
+ end
268
+
241
269
  # Run a single judge.
242
270
  #
243
271
  # @param [Hash] opts The command line options
@@ -253,7 +281,7 @@ class Judges::Update
253
281
  fb = Factbase::Tallied.new(fb)
254
282
  begin
255
283
  if opts['lifetime'] && Time.now - @start > opts['lifetime']
256
- throw :"👎 The '#{judge.name}' judge skipped, no time left"
284
+ throw(:"👎 The '#{judge.name}' judge skipped, no time left")
257
285
  end
258
286
  Timeout.timeout(opts['timeout']) do
259
287
  judge.run(fb, global, local, options)
@@ -3,15 +3,15 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- require 'typhoeus'
7
- require 'iri'
8
6
  require 'baza-rb'
9
7
  require 'elapsed'
8
+ require 'iri'
9
+ require 'typhoeus'
10
10
  require_relative '../../judges'
11
11
 
12
12
  # The +upload+ command, to send a durable to Zerocracy.
13
13
  #
14
- # This class is instantiated by the +bin/judge+ command line interface. You
14
+ # This class is instantiated by the +bin/judges+ command line interface. You
15
15
  # are not supposed to instantiate it yourself.
16
16
  #
17
17
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -28,11 +28,12 @@ class Judges::Upload
28
28
  # @param [Hash] opts Command line options (start with '--')
29
29
  # @param [Array] args List of command line arguments
30
30
  # @raise [RuntimeError] If not exactly two arguments provided
31
+ # rubocop:disable Metrics/MethodLength
31
32
  def run(opts, args)
32
- raise 'Exactly two arguments required' unless args.size == 2
33
+ raise(ArgumentError, 'Exactly two arguments required') unless args.size == 2
33
34
  jname = args[0]
34
35
  path = args[1]
35
- raise "File not found: #{path}" unless File.exist?(path)
36
+ raise(StandardError, "File not found: #{path}") unless File.exist?(path)
36
37
  name = File.basename(path)
37
38
  baza = BazaRb.new(
38
39
  opts['host'], opts['port'].to_i, opts['token'],
@@ -44,8 +45,6 @@ class Judges::Upload
44
45
  elapsed(@loog, level: Logger::INFO) do
45
46
  id = baza.durable_find(jname, name)
46
47
  if id.nil? || id.to_s.strip.empty?
47
- # Block form of Dir.mkdir causes error Errno::EACCESS on windows
48
- # so we use non-block form
49
48
  tmp = Dir.mktmpdir
50
49
  begin
51
50
  f = File.join(tmp, name)
@@ -61,10 +60,11 @@ class Judges::Upload
61
60
  baza.durable_lock(id, opts['owner'] || 'default')
62
61
  begin
63
62
  baza.durable_save(id, path)
64
- throw :"👍 Uploaded #{path} to existing durable '#{name}' in '#{jname}' (ID: #{id}, #{size} bytes)"
63
+ throw(:"👍 Uploaded #{path} to existing durable '#{name}' in '#{jname}' (ID: #{id}, #{size} bytes)")
65
64
  ensure
66
65
  baza.durable_unlock(id, opts['owner'] || 'default')
67
66
  end
68
67
  end
69
68
  end
69
+ # rubocop:enable Metrics/MethodLength
70
70
  end
data/lib/judges/impex.rb CHANGED
@@ -3,9 +3,9 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
+ require 'elapsed'
6
7
  require 'factbase'
7
8
  require 'fileutils'
8
- require 'elapsed'
9
9
  require_relative '../judges'
10
10
  require_relative '../judges/to_rel'
11
11
 
@@ -51,10 +51,10 @@ class Judges::Impex
51
51
  if File.exist?(@file)
52
52
  elapsed(@loog, level: Logger::INFO) do
53
53
  fb.import(File.binread(@file))
54
- throw :"The factbase imported from #{@file.to_rel} (#{File.size(@file)} bytes, #{fb.size} facts)"
54
+ throw(:"The factbase imported from #{@file.to_rel} (#{File.size(@file)} bytes, #{fb.size} facts)")
55
55
  end
56
56
  else
57
- raise "The factbase is absent at #{@file.to_rel}" if strict
57
+ raise(StandardError, "The factbase is absent at #{@file.to_rel}") if strict
58
58
  @loog.info("Nothing to import from #{@file.to_rel} (file not found)")
59
59
  end
60
60
  fb
@@ -74,10 +74,10 @@ class Judges::Impex
74
74
  # # ... populate fb with some data ...
75
75
  # impex.import_to(fb) # Adds data from file to existing facts
76
76
  def import_to(fb)
77
- raise "The factbase is absent at #{@file.to_rel}" unless File.exist?(@file)
77
+ raise(StandardError, "The factbase is absent at #{@file.to_rel}") unless File.exist?(@file)
78
78
  elapsed(@loog, level: Logger::INFO) do
79
79
  fb.import(File.binread(@file))
80
- throw :"The factbase loaded from #{@file.to_rel} (#{File.size(@file)} bytes, #{fb.size} facts)"
80
+ throw(:"The factbase loaded from #{@file.to_rel} (#{File.size(@file)} bytes, #{fb.size} facts)")
81
81
  end
82
82
  end
83
83
 
@@ -97,7 +97,7 @@ class Judges::Impex
97
97
  elapsed(@loog, level: Logger::INFO) do
98
98
  FileUtils.mkdir_p(File.dirname(@file))
99
99
  File.binwrite(@file, fb.export)
100
- throw :"Factbase exported to #{@file.to_rel} (#{File.size(@file)} bytes, #{fb.size} facts)"
100
+ throw(:"Factbase exported to #{@file.to_rel} (#{File.size(@file)} bytes, #{fb.size} facts)")
101
101
  end
102
102
  end
103
103
  end
data/lib/judges/judge.rb CHANGED
@@ -4,13 +4,13 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require 'elapsed'
7
- require 'tago'
8
- require 'timeout'
9
7
  require 'factbase/tallied'
10
8
  require 'octokit'
9
+ require 'tago'
10
+ require 'timeout'
11
11
  require_relative '../judges'
12
- require_relative '../judges/to_rel'
13
12
  require_relative '../judges/pretty_exception'
13
+ require_relative '../judges/to_rel'
14
14
 
15
15
  # A single judge.
16
16
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -57,42 +57,44 @@ class Judges::Judge
57
57
  # @param [Judges::Options] options Command-line options object
58
58
  # @return [nil] Nothing
59
59
  # @raise [RuntimeError] If the lib directory doesn't exist, the script can't be loaded, or execution fails
60
+ # rubocop:disable Metrics/MethodLength
60
61
  def run(fb, global, local, options)
61
62
  $fb = fb
62
63
  $judge = File.basename(@dir)
63
64
  $options = options
64
65
  $loog = @loog
65
66
  $global = global
66
- $global.delete(:fb) # to make sure Tallied is always actual
67
+ $global.delete(:fb)
67
68
  $local = local
68
69
  $epoch = @epoch
69
70
  $kickoff = Time.now
70
71
  options.to_h.each { |k, v| ENV.store(k.to_s, v.to_s) }
71
72
  unless @lib.nil?
72
- raise "Lib dir #{@lib.to_rel} is absent" unless File.exist?(@lib)
73
- raise "Lib #{@lib.to_rel} is not a directory" unless File.directory?(@lib)
73
+ raise(StandardError, "Lib dir #{@lib.to_rel} is absent") unless File.exist?(@lib)
74
+ raise(StandardError, "Lib #{@lib.to_rel} is not a directory") unless File.directory?(@lib)
74
75
  Dir.glob(File.join(@lib, '*.rb')).each do |f|
75
76
  require_relative(File.absolute_path(f))
76
77
  end
77
78
  end
78
79
  s = File.join(@dir, script)
79
- raise "Can't load '#{s}'" unless File.exist?(s)
80
+ raise(StandardError, "Can't load '#{s}'") unless File.exist?(s)
80
81
  elapsed(@loog, good: "#{$judge} completed", level: Logger::INFO) do
81
82
  load(s, true)
82
83
  nil
83
84
  # rubocop:disable Lint/RescueException
84
85
  rescue Exception => e
85
86
  # rubocop:enable Lint/RescueException
86
- raise e if e.is_a?(RuntimeError) && e.message == 'skip'
87
+ raise(e) if e.is_a?(RuntimeError) && e.message == 'skip'
87
88
  e = Judges::PrettyException.new(e) if e.is_a?(Octokit::ServerError)
88
89
  @loog.error(Backtrace.new(e))
89
- raise e if e.is_a?(StandardError)
90
- raise e if e.is_a?(Timeout::ExitException)
91
- raise "#{e.message} (#{e.class.name})"
90
+ raise(e) if e.is_a?(StandardError)
91
+ raise(e) if e.is_a?(Timeout::ExitException)
92
+ raise(StandardError, "#{e.message} (#{e.class.name})")
92
93
  ensure
93
94
  $fb = $judge = $options = $loog = $epoch = $kickoff = nil
94
95
  end
95
96
  end
97
+ # rubocop:enable Metrics/MethodLength
96
98
 
97
99
  # Returns the name of the judge.
98
100
  #
@@ -113,7 +115,7 @@ class Judges::Judge
113
115
  def script
114
116
  b = "#{File.basename(@dir)}.rb"
115
117
  files = Dir.glob(File.join(@dir, '*.rb')).map { |f| File.basename(f) }
116
- raise "No #{b} script in #{@dir.to_rel} among #{files}" unless files.include?(b)
118
+ raise(StandardError, "No #{b} script in #{@dir.to_rel} among #{files}") unless files.include?(b)
117
119
  b
118
120
  end
119
121
 
data/lib/judges/judges.rb CHANGED
@@ -55,7 +55,7 @@ class Judges::Judges
55
55
  # @raise [RuntimeError] If no judge directory exists with the given name
56
56
  def get(name)
57
57
  d = File.absolute_path(File.join(@dir, name))
58
- raise "Judge #{name} doesn't exist in #{@dir}" unless File.exist?(d)
58
+ raise(StandardError, "Judge #{name} doesn't exist in #{@dir}") unless File.exist?(d)
59
59
  Judges::Judge.new(d, @lib, @loog, epoch: @epoch)
60
60
  end
61
61
 
@@ -73,30 +73,11 @@ class Judges::Judges
73
73
  # @return [Enumerator] Returns an enumerator if no block is given
74
74
  def each(&)
75
75
  return to_enum(__method__) unless block_given?
76
- list =
77
- Dir.glob(File.join(@dir, '*')).each.to_a.map do |d|
78
- next unless File.directory?(d)
79
- b = File.basename(d)
80
- next unless File.exist?(File.join(d, "#{b}.rb"))
81
- Judges::Judge.new(File.absolute_path(d), @lib, @loog, epoch: @epoch)
82
- end
83
- list.compact!
84
- list.sort_by!(&:name)
85
- all = list.each_with_index.to_a
86
- good = all.dup
87
- mapping =
88
- all
89
- .map { |a| [a[0].name, a[1], a[1]] }
90
- .reject { |a| !@shuffle.empty? && a[0].start_with?(@shuffle) }
91
- .to_h { |a| [a[1], a[2]] }
92
- positions = mapping.values.shuffle(random: Random.new(@seed))
93
- mapping.keys.zip(positions).to_h.each do |before, after|
94
- good[after] = all[before]
95
- end
76
+ good = reorder_judges
96
77
  boosted = []
97
78
  demoted = []
98
79
  normal = []
99
- good.map { |a| a[0] }.each do |j|
80
+ good.each do |j|
100
81
  if fits?(j.name, @boost)
101
82
  boosted.append(j)
102
83
  elsif fits?(j.name, @demote)
@@ -105,8 +86,7 @@ class Judges::Judges
105
86
  normal.append(j)
106
87
  end
107
88
  end
108
- ret = boosted + normal + demoted
109
- ret.each(&)
89
+ (boosted + normal + demoted).each(&)
110
90
  end
111
91
 
112
92
  # Iterates over all judges while tracking their index position.
@@ -120,7 +100,7 @@ class Judges::Judges
120
100
  def each_with_index
121
101
  idx = 0
122
102
  each do |p|
123
- yield [p, idx]
103
+ yield([p, idx])
124
104
  idx += 1
125
105
  end
126
106
  idx
@@ -128,6 +108,31 @@ class Judges::Judges
128
108
 
129
109
  private
130
110
 
111
+ def reorder_judges
112
+ list = discover_judges
113
+ list.sort_by!(&:name)
114
+ all = list.each_with_index.to_a
115
+ good = all.dup
116
+ mapping =
117
+ all
118
+ .map { |a| [a[0].name, a[1], a[1]] }
119
+ .reject { |a| !@shuffle.empty? && a[0].start_with?(@shuffle) }
120
+ .to_h { |a| [a[1], a[2]] }
121
+ positions = mapping.values.shuffle(random: Random.new(@seed))
122
+ mapping.keys.zip(positions).to_h.each do |before, after|
123
+ good[after] = all[before]
124
+ end
125
+ good.map { |a| a[0] }
126
+ end
127
+
128
+ def discover_judges
129
+ Dir.glob(File.join(@dir, '*')).each.to_a.filter_map do |d|
130
+ next unless File.directory?(d)
131
+ next unless File.exist?(File.join(d, "#{File.basename(d)}.rb"))
132
+ Judges::Judge.new(File.absolute_path(d), @lib, @loog, epoch: @epoch)
133
+ end
134
+ end
135
+
131
136
  # Checks if a judge name matches any of the given patterns.
132
137
  # Patterns can contain '*' wildcards which are converted to '.*' regex patterns.
133
138
  #
@@ -111,49 +111,40 @@ class Judges::Options
111
111
  # options = Judges::Options.new("token=abc123,max_speed=100,debug")
112
112
  # options.to_h # => { TOKEN: "abc123", MAX_SPEED: 100, DEBUG: "true" }
113
113
  def to_h
114
- @to_h ||=
115
- begin
116
- pp = @pairs || []
117
- pp = pp.split(',') if pp.is_a?(String)
118
- if pp.is_a?(Array)
119
- pp = pp
120
- .compact
121
- .map(&:strip)
122
- .reject(&:empty?)
123
- .map { |s| s.split('=', 2) }
124
- .map { |a| a.size == 1 ? [a[0], nil] : a }
125
- .reject { |a| a[0].empty? }
126
- .to_h
127
- end
128
- pp
129
- .reject { |k, _| k.nil? }
130
- .compact
131
- .reject { |k, _| k.is_a?(String) && k.empty? }
132
- .to_h
133
- .transform_values { |v| v.nil? ? 'true' : v }
134
- .transform_values { |v| v.is_a?(String) ? v.strip : v }
135
- .transform_values { |v| v.is_a?(String) && v.match?(/^[0-9]+$/) ? v.to_i : v }
136
- .transform_keys { |k| k.to_s.strip.upcase.to_sym }
137
- end
114
+ @to_h ||= normalize_to_h
115
+ end
116
+
117
+ private
118
+
119
+ def normalize_to_h
120
+ pp = parse_pairs
121
+ pp
122
+ .reject { |k, _| k.nil? }
123
+ .compact
124
+ .reject { |k, _| k.is_a?(String) && k.empty? }
125
+ .to_h
126
+ .transform_values { |v| v.nil? ? 'true' : v }
127
+ .transform_values { |v| v.is_a?(String) ? v.strip : v }
128
+ .transform_values { |v| v.is_a?(String) && v.match?(/^[0-9]+$/) ? v.to_i : v }
129
+ .transform_keys { |k| k.to_s.strip.upcase.to_sym }
130
+ end
131
+
132
+ def parse_pairs
133
+ pp = @pairs || []
134
+ pp = pp.split(',') if pp.is_a?(String)
135
+ if pp.is_a?(Array)
136
+ pp = pp
137
+ .compact
138
+ .map(&:strip)
139
+ .reject(&:empty?)
140
+ .map { |s| s.split('=', 2) }
141
+ .map { |a| a.size == 1 ? [a[0], nil] : a }
142
+ .reject { |a| a[0].empty? }
143
+ .to_h
144
+ end
145
+ pp
138
146
  end
139
147
 
140
- # Get option by name.
141
- #
142
- # This method is implemented using the 'others' gem, which provides
143
- # dynamic method handling. It allows accessing options as method calls.
144
- # Method names are automatically converted to uppercase symbols to match
145
- # the keys in the options hash.
146
- #
147
- # @!method method_missing(method_name, *args)
148
- # Dynamic method to access option values
149
- # @param [Symbol] method_name The name of the option to retrieve
150
- # @param [Array] args Additional arguments (unused)
151
- # @return [Object, nil] The value of the option, or nil if not found
152
- # @example Access options as methods
153
- # options = Judges::Options.new(["token=abc123", "max_speed=100"])
154
- # options.token # => "abc123"
155
- # options.max_speed # => 100
156
- # options.missing_option # => nil
157
148
  others do |*args|
158
149
  to_h[args[0].upcase.to_sym]
159
150
  end
@@ -7,7 +7,7 @@ require 'delegate'
7
7
  require 'ellipsized'
8
8
  require_relative '../judges'
9
9
 
10
- # Decorates the exception for show ellipsed message.
10
+ # Decorates the exception to show an ellipsized message.
11
11
  class Judges::PrettyException < SimpleDelegator
12
12
  undef_method :class
13
13
  undef_method :instance_of?
@@ -32,12 +32,7 @@ class Judges::Statistics
32
32
  # @param [Churn] churn The churn for this run (can be nil)
33
33
  def record(name, time, result, churn = nil)
34
34
  unless @data[name]
35
- @data[name] = {
36
- total_time: 0.0,
37
- cycles: 0,
38
- results: [],
39
- total_churn: nil
40
- }
35
+ @data[name] = { total_time: 0.0, cycles: 0, results: [], total_churn: nil }
41
36
  end
42
37
  stats = @data[name]
43
38
  stats[:total_time] += time
@@ -62,8 +57,10 @@ class Judges::Statistics
62
57
  format(fmt, 'Judge', 'Seconds', 'Cycles', 'Changes', 'Results'),
63
58
  format(fmt, '---', '---', '---', '---', '---'),
64
59
  @data.sort_by { |_, stats| stats[:total_time] }.reverse.map do |name, stats|
65
- format(fmt, name, format('%.3f', stats[:total_time]), stats[:cycles],
66
- stats[:total_churn] ? stats[:total_churn].to_s : 'N/A', summarize(stats[:results]))
60
+ format(
61
+ fmt, name, format('%.3f', stats[:total_time]), stats[:cycles],
62
+ stats[:total_churn] ? stats[:total_churn].to_s : 'N/A', summarize(stats[:results])
63
+ )
67
64
  end.join("\n ")
68
65
  ].join("\n ")
69
66
  )
data/lib/judges.rb CHANGED
@@ -8,5 +8,5 @@
8
8
  # Copyright:: Copyright (c) 2024-2026 Yegor Bugayenko
9
9
  # License:: MIT
10
10
  module Judges
11
- VERSION = '0.60.3' unless const_defined?(:VERSION)
11
+ VERSION = '0.60.4' unless const_defined?(:VERSION)
12
12
  end