judges 0.55.0 → 0.57.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: eb2df3f4875124734e1fc6698c45a9bae82e187a73ae6632bc31906ec8039452
4
- data.tar.gz: 14639995b236e8f4ce02fb794b4050b9fc578a1816e09510312f78c3811a09da
3
+ metadata.gz: 25236f211b356d3dfe62fd6ba9eb7eb70a04e2de64d2581d682db989967cefa9
4
+ data.tar.gz: c804cf366c81db1834496d71852422dbb245edfc76f0a24511621a4aa7979ada
5
5
  SHA512:
6
- metadata.gz: f0b6906cfe65ed01d7d7d3af2677ba45fb6dc875060bcad82d055c18ceccc69973d510a7448748caf1f1bc01680d68fabbe9a223294b0707ddf721a2e1b66d3e
7
- data.tar.gz: 7d92dfe3ea479bfef86366efe451ed415bb8a54d6c8fe9cddb99348408975282dee4e061b7b89c988221177eefecc3b8a228fb3bb08710a2d34c8004403f6a78
6
+ metadata.gz: 1c33f8ae0312222d0ae2ec848d031648fc5f04cb2a64d49e63a26176157a2d9e82bb2fa1f8e010aa0fe8a482fe16690145bdca92f367f05afa851687a18f09af
7
+ data.tar.gz: 6bfafca7127eca496160d7b57325e92815ced1872da8f929380aa0fee185d5f3931cdf5eb753d39a3c68c9c19800a2cfa100b99da60288d9dbae33ccb9680309
data/Gemfile.lock CHANGED
@@ -78,8 +78,9 @@ GEM
78
78
  loog (~> 0.6)
79
79
  tago (~> 0.1)
80
80
  ellipsized (0.3.0)
81
- ethon (0.17.0)
81
+ ethon (0.18.0)
82
82
  ffi (>= 1.15.0)
83
+ logger
83
84
  factbase (0.16.8)
84
85
  backtrace (~> 0.4)
85
86
  decoor (~> 0.1)
@@ -99,8 +100,8 @@ GEM
99
100
  faraday (>= 0.8)
100
101
  faraday-multipart (1.1.1)
101
102
  multipart-post (~> 2.0)
102
- faraday-net_http (3.4.1)
103
- net-http (>= 0.5.0)
103
+ faraday-net_http (3.4.2)
104
+ net-http (~> 0.5)
104
105
  faraday-retry (2.3.2)
105
106
  faraday (~> 2.0)
106
107
  ffi (1.17.2-aarch64-linux-gnu)
@@ -116,7 +117,7 @@ GEM
116
117
  ostruct
117
118
  hashdiff (1.2.1)
118
119
  iri (0.11.2)
119
- json (2.15.1)
120
+ json (2.16.0)
120
121
  language_server-protocol (3.17.0.5)
121
122
  lint_roller (1.1.0)
122
123
  logger (1.7.0)
@@ -124,7 +125,7 @@ GEM
124
125
  logger (~> 1.0)
125
126
  memoist3 (1.0.0)
126
127
  mini_mime (1.1.5)
127
- minitest (5.26.0)
128
+ minitest (5.26.1)
128
129
  minitest-reporters (1.7.1)
129
130
  ansi
130
131
  builder
@@ -135,7 +136,7 @@ GEM
135
136
  moments (0.3.0)
136
137
  multi_test (1.1.0)
137
138
  multipart-post (2.4.1)
138
- net-http (0.6.0)
139
+ net-http (0.7.0)
139
140
  uri
140
141
  nokogiri (1.18.10-aarch64-linux-gnu)
141
142
  racc (~> 1.4)
@@ -161,25 +162,25 @@ GEM
161
162
  ostruct (0.6.3)
162
163
  others (0.1.1)
163
164
  parallel (1.27.0)
164
- parser (3.3.9.0)
165
+ parser (3.3.10.0)
165
166
  ast (~> 2.4.1)
166
167
  racc
167
168
  prism (1.6.0)
168
169
  public_suffix (6.0.2)
169
- qbash (0.4.5)
170
+ qbash (0.4.7)
170
171
  backtrace (> 0)
171
172
  elapsed (> 0)
172
173
  loog (> 0)
173
174
  tago (> 0)
174
175
  racc (1.8.1)
175
176
  rainbow (3.1.1)
176
- rake (13.3.0)
177
+ rake (13.3.1)
177
178
  random-port (0.7.6)
178
179
  tago (~> 0.0)
179
180
  regexp_parser (2.11.3)
180
181
  retries (0.0.5)
181
182
  rexml (3.4.4)
182
- rubocop (1.81.6)
183
+ rubocop (1.81.7)
183
184
  json (~> 2.3)
184
185
  language_server-protocol (~> 3.17.0.2)
185
186
  lint_roller (~> 1.1.0)
@@ -190,7 +191,7 @@ GEM
190
191
  rubocop-ast (>= 1.47.1, < 2.0)
191
192
  ruby-progressbar (~> 1.7)
192
193
  unicode-display_width (>= 2.4.0, < 4.0)
193
- rubocop-ast (1.47.1)
194
+ rubocop-ast (1.48.0)
194
195
  parser (>= 3.3.7.2)
195
196
  prism (~> 1.4)
196
197
  rubocop-minitest (0.38.2)
@@ -217,20 +218,20 @@ GEM
217
218
  sys-uname (1.4.1)
218
219
  ffi (~> 1.1)
219
220
  memoist3 (~> 1.0.0)
220
- tago (0.3.0)
221
- timeout (0.4.3)
221
+ tago (0.4.0)
222
+ timeout (0.4.4)
222
223
  total (0.4.1)
223
224
  typhoeus (1.4.1)
224
225
  ethon (>= 0.9.0)
225
226
  unicode-display_width (3.2.0)
226
227
  unicode-emoji (~> 4.1)
227
228
  unicode-emoji (4.1.0)
228
- uri (1.0.4)
229
+ uri (1.1.1)
229
230
  w3c_validators (1.3.7)
230
231
  json (>= 1.8)
231
232
  nokogiri (~> 1.6)
232
233
  rexml (~> 3.2)
233
- webmock (3.25.1)
234
+ webmock (3.26.1)
234
235
  addressable (>= 2.8.0)
235
236
  crack (>= 0.3.2)
236
237
  hashdiff (>= 0.4.0, < 2.0.0)
data/README.md CHANGED
@@ -20,10 +20,10 @@ of `.yml` files. A script in the Ruby file is executed with the following
20
20
  global variables available to it:
21
21
 
22
22
  * `$fb` — an instance
23
- of [`Factbase`](https://www.rubydoc.info/gems/factbase/0.0.22/Factbase),
23
+ of [Factbase](https://www.rubydoc.info/gems/factbase/0.0.22/Factbase),
24
24
  where facts may be added/updated;
25
25
  * `$loog` — an instance
26
- of [`Loog`](https://www.rubydoc.info/gems/loog/0.5.1/Loog),
26
+ of [Loog](https://www.rubydoc.info/gems/loog/0.5.1/Loog),
27
27
  where `.info` and `.debug` logs are welcome;
28
28
  * `$options` — a holder of options coming from either the `--option` command
29
29
  line flag or the `.yml` file during testing;
data/Rakefile CHANGED
@@ -56,6 +56,7 @@ require 'yard'
56
56
  desc 'Build Yard documentation'
57
57
  YARD::Rake::YardocTask.new do |t|
58
58
  t.files = ['lib/**/*.rb']
59
+ t.options = ['--fail-on-warning']
59
60
  end
60
61
 
61
62
  require 'rubocop/rake_task'
data/bin/judges CHANGED
@@ -13,6 +13,7 @@ require 'tago'
13
13
  require 'time'
14
14
  require 'total'
15
15
  require_relative '../lib/judges'
16
+ require_relative '../lib/judges/ascii_loog'
16
17
 
17
18
  Encoding.default_external = Encoding::UTF_8
18
19
  Encoding.default_internal = Encoding::UTF_8
@@ -57,11 +58,16 @@ class JudgesGLI extend GLI::App
57
58
  switch([:echo])
58
59
  desc 'Stay strictly offline, never attempt to reach Internet resources'
59
60
  switch([:offline])
61
+ desc 'Suppress all Unicode symbols in the output'
62
+ switch([:ascii])
60
63
 
61
64
  pre do |global, command, options, args|
62
65
  if global[:verbose]
63
66
  @@loog = Loog::VERBOSE
64
67
  end
68
+ if global[:ascii]
69
+ @@loog = Judges::AsciiLoog.new(@@loog)
70
+ end
65
71
  if global[:echo]
66
72
  @@loog.info("+ #{File.absolute_path($0)} #{@@args.join(' ')}")
67
73
  end
@@ -73,15 +79,20 @@ class JudgesGLI extend GLI::App
73
79
  end
74
80
  require 'factbase'
75
81
  if global[:hello]
76
- [
82
+ info = [
77
83
  "Judges #{Judges::VERSION}",
78
84
  "Factbase #{Factbase::VERSION}",
79
85
  "Baza-rb #{BazaRb::VERSION}",
80
86
  "Ruby: #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}",
81
87
  "Current directory: #{Dir.getwd}",
82
- "Time: #{Time.now.utc.iso8601}",
83
- "Total memory: #{Total::Mem.new.bytes / (1024 * 1024)}Mb"
84
- ].each { |m| @@loog.info(m) }
88
+ "Time: #{Time.now.utc.iso8601}"
89
+ ]
90
+ begin
91
+ info << "Total memory: #{Total::Mem.new.bytes / (1024 * 1024)}Mb"
92
+ rescue Total::CantDetect => e
93
+ @@loog.debug("Can't detect memory: #{e.message}")
94
+ end
95
+ info.each { |m| @@loog.info(m) }
85
96
  end
86
97
  true
87
98
  end
@@ -94,6 +105,7 @@ class JudgesGLI extend GLI::App
94
105
  end
95
106
 
96
107
  desc 'Update the factbase by executing all judges sequentially'
108
+ arg_name '<dir> <factbase>'
97
109
  command :update do |c|
98
110
  c.desc 'Options to pass to each judge'
99
111
  c.flag([:o, :option], multiple: true, arg_name: '<key=value>')
@@ -123,12 +135,15 @@ class JudgesGLI extend GLI::App
123
135
  c.flag([:summary], must_match: %w[off add append], default_value: 'off')
124
136
  c.desc 'Use default logging facility'
125
137
  c.switch([:log], default_value: true)
138
+ c.desc 'Show execution statistics for each judge'
139
+ c.switch([:statistics], default_value: false)
126
140
  c.desc 'Expect at least one judge to be used (fail if none are used)'
127
141
  c.switch([:'expect-judges'], default_value: true)
128
142
  run_it(c, 'update')
129
143
  end
130
144
 
131
145
  desc 'Evaluate a single Ruby expression against the factbase'
146
+ arg_name '<factbase> <expression>'
132
147
  command :eval do |c|
133
148
  c.desc 'Use default logging facility'
134
149
  c.switch([:log], default_value: true)
@@ -136,11 +151,13 @@ class JudgesGLI extend GLI::App
136
151
  end
137
152
 
138
153
  desc 'Join two factbases'
154
+ arg_name '<first> <second>'
139
155
  command :join do |c|
140
156
  run_it(c, 'join')
141
157
  end
142
158
 
143
159
  desc 'Import YAML into a factbase'
160
+ arg_name '<factbase> <yaml>'
144
161
  command :import do |c|
145
162
  c.desc 'Use default logging facility'
146
163
  c.switch([:log], default_value: true)
@@ -148,6 +165,7 @@ class JudgesGLI extend GLI::App
148
165
  end
149
166
 
150
167
  desc 'Remove outdated facts from the factbase'
168
+ arg_name 'factbase'
151
169
  command :trim do |c|
152
170
  c.desc 'Delete only facts matching the specified expression'
153
171
  c.flag([:query], default_value: '(never)')
@@ -155,6 +173,7 @@ class JudgesGLI extend GLI::App
155
173
  end
156
174
 
157
175
  desc 'Convert the factbase to a human-readable format (YAML, JSON, etc.)'
176
+ arg_name '<factbase> [<output>]'
158
177
  command :print do |c|
159
178
  c.desc 'Output format (xml, json, or yaml)'
160
179
  c.flag([:format], default_value: 'yaml')
@@ -174,11 +193,13 @@ class JudgesGLI extend GLI::App
174
193
  end
175
194
 
176
195
  desc 'Inspect the factbase and display all available metadata'
196
+ arg_name '<factbase>'
177
197
  command :inspect do |c|
178
198
  run_it(c, 'inspect')
179
199
  end
180
200
 
181
201
  desc 'Run automated tests for all judges'
202
+ arg_name '<dir>'
182
203
  command :test do |c|
183
204
  c.desc 'Options to pass to each judge (may be overridden by YAML)'
184
205
  c.flag([:o, :option], multiple: true, arg_name: '<key=value>')
@@ -200,6 +221,7 @@ class JudgesGLI extend GLI::App
200
221
  end
201
222
 
202
223
  desc 'Push the factbase to the server and unlock it remotely'
224
+ arg_name '<name> <factbase>'
203
225
  command :push do |c|
204
226
  c.desc 'Authentication token'
205
227
  c.flag([:token])
@@ -223,6 +245,7 @@ class JudgesGLI extend GLI::App
223
245
  end
224
246
 
225
247
  desc 'Pull the factbase from the server and lock it remotely'
248
+ arg_name '<name> <factbase>'
226
249
  command :pull do |c|
227
250
  c.desc 'Authentication token'
228
251
  c.flag([:token])
@@ -244,6 +267,7 @@ class JudgesGLI extend GLI::App
244
267
  end
245
268
 
246
269
  desc 'Download a durable from the server by ID'
270
+ arg_name '<id> <factbase>'
247
271
  command :download do |c|
248
272
  c.desc 'Authentication token'
249
273
  c.flag([:token])
@@ -263,6 +287,7 @@ class JudgesGLI extend GLI::App
263
287
  end
264
288
 
265
289
  desc 'Upload a file as a durable to the server'
290
+ arg_name '<file> <path>'
266
291
  command :upload do |c|
267
292
  c.desc 'Authentication token'
268
293
  c.flag([:token])
@@ -55,14 +55,14 @@ Feature: Update
55
55
  """
56
56
  n = $fb.insert
57
57
  n.type = 'first'
58
- sleep 1.9
58
+ sleep 1.91
59
59
  """
60
60
  Then I have a "second/second.rb" file with content:
61
61
  """
62
62
  n = $fb.insert
63
63
  n.type = 'second'
64
64
  """
65
- Then I run bin/judges with "--verbose update --quiet --lifetime 2 --timeout 1 --max-cycles 5 . simple.fb"
65
+ Then I run bin/judges with "--verbose update --quiet --lifetime 4 --timeout 3 --max-cycles 5 . 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 . 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.55.0'
12
+ s.version = '0.57.0'
13
13
  s.license = 'MIT'
14
14
  s.summary = 'Command-Line Tool for a Factbase'
15
15
  s.description =
@@ -0,0 +1,81 @@
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
+ # ASCII wrapper for Loog logging facility.
9
+ #
10
+ # This class wraps any Loog logger and converts Unicode symbols to ASCII
11
+ # equivalents when the --ascii option is enabled.
12
+ #
13
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
14
+ # Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
15
+ # License:: MIT
16
+ class Judges::AsciiLoog
17
+ # Unicode to ASCII symbol mapping
18
+ UNICODE_TO_ASCII = {
19
+ '👍' => '+',
20
+ '👎' => '-',
21
+ '❌' => '!',
22
+ '👉' => '>',
23
+ '✓' => '+',
24
+ '✗' => '!',
25
+ '►' => '>',
26
+ '◄' => '<',
27
+ '▼' => 'v',
28
+ '▲' => '^'
29
+ }.freeze
30
+
31
+ # Initialize the ASCII wrapper.
32
+ # @param [Loog] loog The original logging facility to wrap
33
+ def initialize(loog)
34
+ @loog = loog
35
+ end
36
+
37
+ # Convert Unicode symbols to ASCII equivalents.
38
+ # @param [String] message The message to convert
39
+ # @return [String] The converted message with ASCII symbols
40
+ def to_ascii(message)
41
+ result = message.to_s
42
+ UNICODE_TO_ASCII.each do |unicode, ascii|
43
+ result = result.gsub(unicode, ascii)
44
+ end
45
+ result
46
+ end
47
+
48
+ # Log an info message, converting Unicode to ASCII.
49
+ # @param [String] message The message to log
50
+ def info(message)
51
+ @loog.info(to_ascii(message))
52
+ end
53
+
54
+ # Log a warning message, converting Unicode to ASCII.
55
+ # @param [String] message The message to log
56
+ def warn(message)
57
+ @loog.warn(to_ascii(message))
58
+ end
59
+
60
+ # Log an error message, converting Unicode to ASCII.
61
+ # @param [String] message The message to log
62
+ def error(message)
63
+ @loog.error(to_ascii(message))
64
+ end
65
+
66
+ # Log a debug message, converting Unicode to ASCII.
67
+ # @param [String] message The message to log
68
+ def debug(message)
69
+ @loog.debug(to_ascii(message))
70
+ end
71
+
72
+ # Delegate all other methods to the original logger.
73
+ def method_missing(method, *, &)
74
+ @loog.send(method, *, &)
75
+ end
76
+
77
+ # Check if the original logger responds to a method.
78
+ def respond_to_missing?(method, include_private = false)
79
+ @loog.respond_to?(method, include_private) || super
80
+ end
81
+ end
@@ -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
- if remained < opts['timeout'] / 16
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.
@@ -45,10 +45,15 @@ class Judges::Upload
45
45
  id = baza.durable_find(jname, name)
46
46
  size = File.size(path)
47
47
  if id.nil? || id.to_s.strip.empty?
48
- Tempfile.create do |f|
48
+ f = Tempfile.new('placeholder')
49
+ begin
49
50
  File.write(f.path, 'placeholder')
51
+ f.close
50
52
  id = baza.durable_place(jname, f.path)
51
53
  @loog.info("Placed a placeholder to new durable '#{name}' in '#{jname}' (ID: #{id})")
54
+ ensure
55
+ f.close unless f.closed?
56
+ f.unlink
52
57
  end
53
58
  end
54
59
  id = id.to_i
@@ -144,8 +144,11 @@ class Judges::Options
144
144
  # Method names are automatically converted to uppercase symbols to match
145
145
  # the keys in the options hash.
146
146
  #
147
- # @param [Symbol, String] method_name The option name to retrieve
148
- # @return [Object, nil] The value of the option, or nil if not found
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
149
152
  # @example Access options as methods
150
153
  # options = Judges::Options.new(["token=abc123", "max_speed=100"])
151
154
  # options.token # => "abc123"
@@ -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.55.0' unless const_defined?(:VERSION)
11
+ VERSION = '0.57.0' 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.55.0
4
+ version: 0.57.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -280,6 +280,7 @@ files:
280
280
  - fixtures/try/try.rb
281
281
  - judges.gemspec
282
282
  - lib/judges.rb
283
+ - lib/judges/ascii_loog.rb
283
284
  - lib/judges/categories.rb
284
285
  - lib/judges/commands/download.rb
285
286
  - lib/judges/commands/eval.rb
@@ -297,6 +298,7 @@ files:
297
298
  - lib/judges/judge.rb
298
299
  - lib/judges/judges.rb
299
300
  - lib/judges/options.rb
301
+ - lib/judges/statistics.rb
300
302
  - lib/judges/to_rel.rb
301
303
  - package-lock.json
302
304
  - package.json