judges 0.56.0 → 0.57.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1bce8cfaa9c1567d94332da48e35b303ec5b963198d72a82bf69164cbd1928d
4
- data.tar.gz: 467ab5d5bab974f97be24720c3e44f15d353c38f2ecf553648431facb603864f
3
+ metadata.gz: 99e0f160f5dfa27ca34a0e3869bbc26d7f7ee4c7bb25beca1197e4e2dd462e68
4
+ data.tar.gz: 923b096bf564c2c2e03ae4ad1697df5c8b7331f211246fef0f2b1fffb28b8333
5
5
  SHA512:
6
- metadata.gz: dfd3eb005e029e442b5c924620a897df19c974599122e6ae20f0369718f02b8b6171b852e32d408c5a27c766dac92260ad59e4420e14c5d8ccdcc5c8b4ca38b5
7
- data.tar.gz: 66fbdd630846645475cab1daa36b326d92e37eeb08cf41c176b1a710e93d25acc3565b8bd4c43f6905e0bdb2752ac03dc609d6d5152e93b08b993b34463be0b2
6
+ metadata.gz: 9bae673902b779709eb86dbc939fc234baabc546a08b953a2e5f9c7006467ad5279c0bc59268c7b240d60da93f6e5aca28adbe3774748a8b08eaa2d499b8c7e7
7
+ data.tar.gz: 38fbbd8411637b54b6515e96599e91924a82ea63599315a9d166a26d6aeff4aa65c0be86eb09fb39b8ce4de2494c6bf09d7af9baa6abd97f4ba32c7da452468f
data/bin/judges CHANGED
@@ -27,6 +27,7 @@ class JudgesGLI extend GLI::App
27
27
  require_relative "../lib/judges/commands/#{ruby}"
28
28
  start = Time.now
29
29
  @@loog.debug("Running '#{ruby}' command...")
30
+ options = global.merge(options)
30
31
  begin
31
32
  Object.const_get("Judges::#{ruby.capitalize}").new(@@loog).run(options, args)
32
33
  @@loog.debug("Command '#{ruby}' completed in #{start.ago}")
@@ -135,6 +136,8 @@ class JudgesGLI extend GLI::App
135
136
  c.flag([:summary], must_match: %w[off add append], default_value: 'off')
136
137
  c.desc 'Use default logging facility'
137
138
  c.switch([:log], default_value: true)
139
+ c.desc 'Show execution statistics for each judge'
140
+ c.switch([:statistics], default_value: false)
138
141
  c.desc 'Expect at least one judge to be used (fail if none are used)'
139
142
  c.switch([:'expect-judges'], default_value: true)
140
143
  run_it(c, 'update')
@@ -18,6 +18,14 @@ Feature: Print
18
18
  Then Stdout contains "printed"
19
19
  And Exit code is zero
20
20
 
21
+ Scenario: Simple print of a small factbase, to HTML, in offline mode
22
+ Given I make a temp directory
23
+ Then I run bin/judges with "--verbose eval simple.fb '$fb.insert.foo = 42'"
24
+ Then I run bin/judges with "--offline print --format=html simple.fb simple.html"
25
+ Then Stdout contains "printed"
26
+ Then simple.html contains "sha256-offline"
27
+ And Exit code is zero
28
+
21
29
  Scenario: Simple print of a small factbase, to JSON
22
30
  Given I make a temp directory
23
31
  Then I run bin/judges with "--verbose eval simple.fb '$fb.insert.foo = 42'"
@@ -56,6 +56,11 @@ When(/^I run bash with:$/) do |text|
56
56
  @exitstatus = $CHILD_STATUS.exitstatus
57
57
  end
58
58
 
59
+ Then(/^([a-z].+) contains "([^"]*)"$/) do |file, txt|
60
+ data = File.read(file)
61
+ raise "The file #{file} doesn't contain '#{txt}':\n#{data}" unless data.include?(txt)
62
+ end
63
+
59
64
  Then(/^Stdout contains "([^"]*)"$/) do |txt|
60
65
  raise "STDOUT doesn't contain '#{txt}':\n#{@stdout}" unless @stdout.include?(txt)
61
66
  end
@@ -62,7 +62,7 @@ Feature: Update
62
62
  n = $fb.insert
63
63
  n.type = 'second'
64
64
  """
65
- Then I run bin/judges with "--verbose update --quiet --lifetime 4 --timeout 3 --max-cycles 5 . simple.fb"
65
+ Then I run bin/judges with "--verbose update --quiet --lifetime 4 --timeout 3 --max-cycles 5 --shuffle 'first' . simple.fb"
66
66
  Then Stdout contains "Update completed in 2 cycle(s), did 3i/0d/3a"
67
67
  And Exit code is zero
68
68
 
@@ -214,3 +214,44 @@ Feature: Update
214
214
  Then Stdout contains "Running delayed"
215
215
  Then Stdout contains "3 judge(s) processed"
216
216
  And Exit code is zero
217
+
218
+ Scenario: Show statistics for judge execution
219
+ Given I make a temp directory
220
+ Then I have a "alpha/alpha.rb" file with content:
221
+ """
222
+ $fb.insert.name = 'alpha'
223
+ """
224
+ Then I have a "beta/beta.rb" file with content:
225
+ """
226
+ $fb.insert.name = 'beta'
227
+ """
228
+ Then I run bin/judges with "update --statistics --quiet --max-cycles 1 . stats.fb"
229
+ Then Stdout contains "Judge execution summary:"
230
+ Then Stdout contains "Judge"
231
+ Then Stdout contains "Seconds"
232
+ Then Stdout contains "Changes"
233
+ Then Stdout contains "Cycles"
234
+ Then Stdout contains "Result"
235
+ Then Stdout contains "alpha"
236
+ Then Stdout contains "beta"
237
+ Then Stdout contains "OK"
238
+ And Exit code is zero
239
+
240
+ Scenario: Summarize results in statistics
241
+ Given I make a temp directory
242
+ Then I have a "alpha/alpha.rb" file with content:
243
+ """
244
+ $fb.insert.name = 'alpha'
245
+ sleep 1.91
246
+ """
247
+ Then I have a "beta/beta.rb" file with content:
248
+ """
249
+ $fb.insert.name = 'beta'
250
+ """
251
+ Then I run bin/judges with "update --statistics --quiet --max-cycles 2 --lifetime 4 --timeout 3 --shuffle 'alpha' . stats.fb"
252
+ Then Stdout contains "Judge execution summary:"
253
+ Then Stdout contains "alpha"
254
+ Then Stdout contains "beta"
255
+ Then Stdout contains "1xOK"
256
+ Then Stdout contains "1xSKIPPED (timeout)"
257
+ And Exit code is zero
data/judges.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
9
9
  s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
10
10
  s.required_ruby_version = '>=3.2'
11
11
  s.name = 'judges'
12
- s.version = '0.56.0'
12
+ s.version = '0.57.1'
13
13
  s.license = 'MIT'
14
14
  s.summary = 'Command-Line Tool for a Factbase'
15
15
  s.description =
@@ -16,6 +16,7 @@ require_relative '../../judges'
16
16
  require_relative '../../judges/impex'
17
17
  require_relative '../../judges/judges'
18
18
  require_relative '../../judges/options'
19
+ require_relative '../../judges/statistics'
19
20
  require_relative '../../judges/to_rel'
20
21
 
21
22
  # The +update+ command.
@@ -88,6 +89,7 @@ class Judges::Update
88
89
  c = 0
89
90
  churn = Factbase::Churn.new
90
91
  errors = []
92
+ statistics = opts['statistics'] ? Judges::Statistics.new : nil
91
93
  sum = fb.query('(eq what "judges-summary")').each.to_a
92
94
  if sum.empty?
93
95
  @loog.info('Summary fact not found') unless opts['summary'] == 'off'
@@ -108,7 +110,7 @@ class Judges::Update
108
110
  end
109
111
  @loog.info("\nStarting cycle ##{c}#{" (out of #{opts['max-cycles']})" if opts['max-cycles']}...")
110
112
  end
111
- delta = cycle(opts, judges, fb, options, errors)
113
+ delta = cycle(opts, judges, fb, options, errors, statistics)
112
114
  churn += delta
113
115
  impex.export(fb)
114
116
  if delta.zero?
@@ -127,6 +129,7 @@ class Judges::Update
127
129
  end
128
130
  throw :"👍 Update completed in #{c} cycle(s), did #{churn}"
129
131
  end
132
+ statistics&.report(@loog)
130
133
  return unless %w[add append].include?(opts['summary'])
131
134
  summarize(fb, churn, errors, c)
132
135
  impex.export(fb)
@@ -172,9 +175,10 @@ class Judges::Update
172
175
  # @param [Factbase] fb The factbase
173
176
  # @param [Judges::Options] options The options
174
177
  # @param [Array<String>] errors List of errors
178
+ # @param [Judges::Statistics] statistics Statistics tracking object (optional)
175
179
  # @return [Factbase::Churn] How many modifications have been made
176
- def cycle(opts, judges, fb, options, errors)
177
- churn = Factbase::Churn.new
180
+ def cycle(opts, judges, fb, options, errors, statistics = nil)
181
+ delta = Factbase::Churn.new
178
182
  global = {}
179
183
  used = 0
180
184
  elapsed(@loog, level: Logger::INFO) do
@@ -182,27 +186,35 @@ class Judges::Update
182
186
  judges.each_with_index do |judge, i|
183
187
  if opts['fail-fast'] && !errors.empty?
184
188
  @loog.info("Not running #{judge.name.inspect} due to #{errors.count} errors above, in --fail-fast mode")
189
+ statistics&.record(judge.name, 0, 'SKIPPED (fail-fast)') if include?(opts, judge.name)
185
190
  next
186
191
  end
187
192
  if opts['lifetime'] && opts['timeout']
188
193
  remained = @start + opts['lifetime'] - Time.now
189
194
  if remained < opts['timeout'].to_f / 16
190
195
  @loog.info("Not running #{judge.name.inspect}, not enough time left (just #{remained.seconds})")
196
+ statistics&.record(judge.name, 0, 'SKIPPED (timeout)') if include?(opts, judge.name)
191
197
  next
192
198
  end
193
199
  end
194
200
  next unless include?(opts, judge.name)
195
201
  @loog.info("\n👉 Running #{judge.name} (##{i}) at #{judge.dir.to_rel} (#{@start.ago} already)...")
196
202
  used += 1
203
+ start_time = Time.now
204
+ result = 'OK'
205
+ impact = nil
197
206
  elapsed(@loog, level: Logger::INFO) do
198
- c = one_judge(opts, fb, judge, global, options, errors)
199
- churn += c
200
- throw :"👍 The '#{judge.name}' judge made zero changes to #{fb.size} facts" if c.zero?
201
- throw :"👍 The '#{judge.name}' judge #{c} out of #{fb.size} facts"
207
+ impact = one_judge(opts, fb, judge, global, options, errors)
208
+ delta += impact
209
+ throw :"👍 The '#{judge.name}' judge made zero changes to #{fb.size} facts" if impact.zero?
210
+ throw :"👍 The '#{judge.name}' judge #{impact} out of #{fb.size} facts"
202
211
  end
203
212
  rescue StandardError, SyntaxError => e
204
213
  @loog.warn(Backtrace.new(e))
205
214
  errors << e.message
215
+ result = 'ERROR'
216
+ ensure
217
+ statistics&.record(judge.name, Time.now - start_time, result, impact) if start_time
206
218
  end
207
219
  throw :"👍 #{done} judge(s) processed" if errors.empty?
208
220
  throw :"❌ #{done} judge(s) processed with #{errors.size} errors"
@@ -215,7 +227,7 @@ class Judges::Update
215
227
  raise "Failed to update correctly (#{errors.size} errors)" unless opts['quiet']
216
228
  @loog.info('Not failing because of the --quiet flag provided')
217
229
  end
218
- churn
230
+ delta
219
231
  end
220
232
 
221
233
  # Run a single judge.
data/lib/judges/judges.rb CHANGED
@@ -62,10 +62,10 @@ class Judges::Judges
62
62
  # This method discovers all judge directories, validates them (ensuring they contain
63
63
  # a corresponding .rb file), and yields them in a specific order. The order is
64
64
  # determined by:
65
- # 1. Judges whose names match the boost list are placed first
66
- # 2. Judges whose names start with the shuffle prefix are randomly reordered
65
+ # 1. Randomly reorder judges (if shuffle prefix is empty, shuffle all judges;
66
+ # if prefix is not empty, shuffle only those NOT starting with the prefix)
67
+ # 2. Judges whose names match the boost list are placed first
67
68
  # 3. Judges whose names match the demote list are placed last
68
- # 4. All other judges maintain their alphabetical order
69
69
  #
70
70
  # @yield [Judges::Judge] Yields each valid judge object
71
71
  # @return [Enumerator] Returns an enumerator if no block is given
@@ -84,7 +84,7 @@ class Judges::Judges
84
84
  good = all.dup
85
85
  mapping = all
86
86
  .map { |a| [a[0].name, a[1], a[1]] }
87
- .reject { |a| a[0].start_with?(@shuffle) }
87
+ .reject { |a| !@shuffle.empty? && a[0].start_with?(@shuffle) }
88
88
  .to_h { |a| [a[1], a[2]] }
89
89
  positions = mapping.values.shuffle
90
90
  mapping.keys.zip(positions).to_h.each do |before, after|
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative '../judges'
7
+
8
+ # Statistics collector for judge executions.
9
+ #
10
+ # This class collects and aggregates statistics about judge executions
11
+ # across multiple cycles, providing insights into performance and results.
12
+ #
13
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
14
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
15
+ # License:: MIT
16
+ class Judges::Statistics
17
+ # Initialize empty statistics.
18
+ def initialize
19
+ @data = {}
20
+ end
21
+
22
+ # Check if statistics are empty.
23
+ # @return [Boolean] True if no statistics have been collected
24
+ def empty?
25
+ @data.empty?
26
+ end
27
+
28
+ # Record statistics for a judge execution.
29
+ # @param [String] name The judge name
30
+ # @param [Float] time The execution time for this run
31
+ # @param [String] result The result for this run
32
+ # @param [Churn] churn The churn for this run (can be nil)
33
+ def record(name, time, result, churn = nil)
34
+ unless @data[name]
35
+ @data[name] = {
36
+ total_time: 0.0,
37
+ cycles: 0,
38
+ results: [],
39
+ total_churn: nil
40
+ }
41
+ end
42
+ stats = @data[name]
43
+ stats[:total_time] += time
44
+ stats[:cycles] += 1
45
+ stats[:results] << result
46
+ return unless churn
47
+ if stats[:total_churn]
48
+ stats[:total_churn] += churn
49
+ else
50
+ stats[:total_churn] = churn
51
+ end
52
+ end
53
+
54
+ # Generate a formatted statistics report.
55
+ # @param [Loog] loog Logging facility for output
56
+ def report(loog)
57
+ return if empty?
58
+ fmt = "%-30s\t%9s\t%7s\t%15s\t%-15s"
59
+ loog.info(
60
+ [
61
+ 'Judge execution summary:',
62
+ format(fmt, 'Judge', 'Seconds', 'Cycles', 'Changes', 'Results'),
63
+ format(fmt, '---', '---', '---', '---', '---'),
64
+ @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]))
67
+ end.join("\n ")
68
+ ].join("\n ")
69
+ )
70
+ end
71
+
72
+ private
73
+
74
+ # Summarize results across multiple cycles into a compact string.
75
+ # @param [Array<String>] results Array of result strings from different cycles
76
+ # @return [String] Compact summary of results
77
+ def summarize(results)
78
+ return 'N/A' if results.empty?
79
+ counts = results.each_with_object(Hash.new(0)) { |result, hash| hash[result] += 1 }
80
+ return results.first if counts.size == 1
81
+ counts.sort_by { |_, count| -count }.map { |result, count| "#{count}x#{result}" }.join(', ')
82
+ end
83
+ end
data/lib/judges.rb CHANGED
@@ -8,5 +8,5 @@
8
8
  # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
9
9
  # License:: MIT
10
10
  module Judges
11
- VERSION = '0.56.0' unless const_defined?(:VERSION)
11
+ VERSION = '0.57.1' unless const_defined?(:VERSION)
12
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: judges
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.56.0
4
+ version: 0.57.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -298,6 +298,7 @@ files:
298
298
  - lib/judges/judge.rb
299
299
  - lib/judges/judges.rb
300
300
  - lib/judges/options.rb
301
+ - lib/judges/statistics.rb
301
302
  - lib/judges/to_rel.rb
302
303
  - package-lock.json
303
304
  - package.json