fbe 0.16.1 → 0.17.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: 011c5a07ab2d1b15194cd2950da1cd466698d37a9aa0bbd07225ce7bca335881
4
- data.tar.gz: 1fa420cebbdc70897a4779601a5cc8e4596a9542a223e64e020086ff0ec10826
3
+ metadata.gz: 7f70d456f08cfacd13990d45fd62834c1856260dd096583909fe4aa2b113f080
4
+ data.tar.gz: edd2a32689116bf7ae2940458f2e77cf6d33027097e3a01985af685d78f666b4
5
5
  SHA512:
6
- metadata.gz: 1790fa88d1d97b8509d61c85592c36ca5147e97e982015991fc15128315a210190505310cf1d53b618507b354e1b77b065bbc1cbfff4b8ffc771f50bea5778fa
7
- data.tar.gz: 4bf95961acba3192c824e48054c8a36d7aa72184645305c8fcc6ef9e5926cd08c7f661353bc6244b8e789b1fb5ea2e4dcf8a089969267bc55b80fdf9b641b775
6
+ metadata.gz: 5a5371878121330954776a6fc2d7cc32ef4207afd7c6e73b8ac4399c6af222abbfda3afd7d2636eadb7bf3f305984ed547d755666e02d38d3a54c0479c289e15
7
+ data.tar.gz: 76cf0627e9c1bd78b9ad1e348ff5c6fce2f0c9e8374d2a0b09ae93c1b4faa27b240446f30abb1fb7821bcf5c18b5bd118b7f54d3615843773ebe3b1edcbca4c8
data/.gitignore CHANGED
@@ -8,3 +8,4 @@ doc/
8
8
  node_modules/
9
9
  rdoc/
10
10
  vendor/
11
+ .claude/
data/.rubocop.yml CHANGED
@@ -14,6 +14,8 @@ plugins:
14
14
  - rubocop-minitest
15
15
  - rubocop-performance
16
16
  - rubocop-rake
17
+ Minitest/MultipleAssertions:
18
+ Max: 10
17
19
  Minitest/EmptyLineBeforeAssertionMethods:
18
20
  Enabled: false
19
21
  Style/GlobalVars:
@@ -54,4 +56,3 @@ Metrics/ParameterLists:
54
56
  Enabled: false
55
57
  Layout/MultilineAssignmentLayout:
56
58
  Enabled: true
57
- require: []
data/Rakefile CHANGED
@@ -16,7 +16,7 @@ def version
16
16
  Gem::Specification.load(Dir['*.gemspec'].first).version
17
17
  end
18
18
 
19
- task default: %i[clean test picks rubocop yard]
19
+ task default: %i[clean test rubocop picks yard]
20
20
 
21
21
  require 'rake/testtask'
22
22
  desc 'Run all unit tests'
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'faraday'
7
+ require_relative '../../fbe'
8
+ require_relative '../../fbe/middleware'
9
+
10
+ # Faraday middleware that traces all API calls.
11
+ #
12
+ # This middleware records all HTTP requests and responses in a trace array,
13
+ # capturing method, URL, status, and timing information for debugging and
14
+ # monitoring purposes.
15
+ #
16
+ # @example Usage in Faraday middleware stack
17
+ # trace = []
18
+ # connection = Faraday.new do |f|
19
+ # f.use Fbe::Middleware::Trace, trace
20
+ # end
21
+ # connection.get('/api/endpoint')
22
+ # trace.first[:method] #=> :get
23
+ # trace.first[:url] #=> 'https://example.com/api/endpoint'
24
+ # trace.first[:status] #=> 200
25
+ #
26
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
27
+ # Copyright:: Copyright (c) 2024-2025 Zerocracy
28
+ # License:: MIT
29
+ class Fbe::Middleware::Trace < Faraday::Middleware
30
+ # Initializes the trace middleware.
31
+ #
32
+ # @param [Object] app The next middleware in the stack
33
+ # @param [Array] trace The array to store trace entries
34
+ def initialize(app, trace)
35
+ super(app)
36
+ @trace = trace
37
+ end
38
+
39
+ # Processes the HTTP request and records trace information.
40
+ #
41
+ # @param [Faraday::Env] env The request environment
42
+ # @return [Faraday::Response] The response from the next middleware
43
+ def call(env)
44
+ started = Time.now
45
+ entry = {
46
+ method: env.method,
47
+ url: env.url.to_s,
48
+ started_at: started
49
+ }
50
+ @app.call(env).on_complete do |response_env|
51
+ finished = Time.now
52
+ entry[:status] = response_env.status
53
+ entry[:finished_at] = finished
54
+ entry[:duration] = finished - started
55
+ @trace << entry
56
+ end
57
+ end
58
+ end
data/lib/fbe/octo.rb CHANGED
@@ -9,10 +9,12 @@ require 'faraday/retry'
9
9
  require 'loog'
10
10
  require 'obk'
11
11
  require 'octokit'
12
+ require 'uri'
12
13
  require 'verbose'
13
14
  require_relative '../fbe'
14
15
  require_relative 'middleware'
15
16
  require_relative 'middleware/formatter'
17
+ require_relative 'middleware/trace'
16
18
 
17
19
  # Makes a call to the GitHub API.
18
20
  #
@@ -30,6 +32,7 @@ def Fbe.octo(options: $options, global: $global, loog: $loog)
30
32
  raise 'The $loog is not set' if loog.nil?
31
33
  global[:octo] ||=
32
34
  begin
35
+ trace = []
33
36
  if options.testing.nil?
34
37
  o = Octokit::Client.new
35
38
  token = options.github_token
@@ -50,8 +53,10 @@ def Fbe.octo(options: $options, global: $global, loog: $loog)
50
53
  loog.warn('The GitHub API token is an empty string, won\'t use it')
51
54
  else
52
55
  o = Octokit::Client.new(access_token: token)
53
- loog.info("Accessing GitHub API with a token (#{token.length} chars, ending by #{token[-4..].inspect}, " \
54
- "#{Octokit::Client.new(access_token: token).rate_limit.remaining} quota remaining)")
56
+ loog.info(
57
+ "Accessing GitHub API with a token (#{token.length} chars, ending by #{token[-4..].inspect}, " \
58
+ "#{Octokit::Client.new(access_token: token).rate_limit.remaining} quota remaining)"
59
+ )
55
60
  end
56
61
  o.auto_paginate = true
57
62
  o.per_page = 100
@@ -76,6 +81,7 @@ def Fbe.octo(options: $options, global: $global, loog: $loog)
76
81
  builder.use(Faraday::HttpCache, serializer: Marshal, shared_cache: false, logger: Loog::NULL)
77
82
  builder.use(Octokit::Response::RaiseError)
78
83
  builder.use(Faraday::Response::Logger, loog, formatter: Fbe::Middleware::Formatter)
84
+ builder.use(Fbe::Middleware::Trace, trace)
79
85
  builder.adapter(Faraday.default_adapter)
80
86
  end
81
87
  o.middleware = stack
@@ -84,7 +90,25 @@ def Fbe.octo(options: $options, global: $global, loog: $loog)
84
90
  loog.debug('The connection to GitHub API is mocked')
85
91
  o = Fbe::FakeOctokit.new
86
92
  end
87
- decoor(o, loog:) do
93
+ decoor(o, loog:, trace:) do
94
+ def print_trace!
95
+ if @trace.empty?
96
+ @loog.debug('GitHub API trace is empty')
97
+ else
98
+ grouped =
99
+ @trace.group_by do |entry|
100
+ uri = URI.parse(entry[:url])
101
+ "#{uri.scheme}://#{uri.host}#{uri.path}"
102
+ end
103
+ message = grouped
104
+ .sort_by { |_path, entries| -entries.count }
105
+ .map { |path, entries| " #{path}: #{entries.count}" }
106
+ .join("\n")
107
+ @loog.info("GitHub API trace (#{grouped.count} URLs vs #{@trace.count} requests):\n#{message}")
108
+ @trace.clear
109
+ end
110
+ end
111
+
88
112
  def off_quota(threshold: 50)
89
113
  left = @origin.rate_limit.remaining
90
114
  if left < threshold
data/lib/fbe.rb CHANGED
@@ -10,5 +10,5 @@
10
10
  # License:: MIT
11
11
  module Fbe
12
12
  # Current version of the gem (changed by +.rultor.yml+ on every release)
13
- VERSION = '0.16.1' unless const_defined?(:VERSION)
13
+ VERSION = '0.17.0' unless const_defined?(:VERSION)
14
14
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'faraday'
7
+ require 'webmock'
8
+ require_relative '../../test__helper'
9
+ require_relative '../../../lib/fbe'
10
+ require_relative '../../../lib/fbe/middleware'
11
+ require_relative '../../../lib/fbe/middleware/trace'
12
+
13
+ # Test.
14
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
15
+ # Copyright:: Copyright (c) 2024-2025 Zerocracy
16
+ # License:: MIT
17
+ class TraceTest < Fbe::Test
18
+ def test_traces_successful_request
19
+ trace = []
20
+ stub_request(:get, 'http://example.com/test')
21
+ .to_return(status: 200, body: 'success')
22
+ conn =
23
+ Faraday.new do |f|
24
+ f.use Fbe::Middleware::Trace, trace
25
+ f.adapter :net_http
26
+ end
27
+ conn.get('http://example.com/test')
28
+ assert_equal 1, trace.size
29
+ entry = trace.first
30
+ assert_equal :get, entry[:method]
31
+ assert_equal 'http://example.com/test', entry[:url]
32
+ assert_equal 200, entry[:status]
33
+ assert_instance_of Time, entry[:started_at]
34
+ assert_instance_of Time, entry[:finished_at]
35
+ assert_instance_of Float, entry[:duration]
36
+ assert_operator entry[:duration], :>=, 0
37
+ assert_operator entry[:finished_at], :>=, entry[:started_at]
38
+ end
39
+
40
+ def test_traces_multiple_requests
41
+ trace = []
42
+ stub_request(:get, 'http://example.com/endpoint1').to_return(status: 200)
43
+ stub_request(:post, 'http://example.com/endpoint2').to_return(status: 201)
44
+ stub_request(:delete, 'http://example.com/endpoint3').to_return(status: 404)
45
+ conn =
46
+ Faraday.new do |f|
47
+ f.use Fbe::Middleware::Trace, trace
48
+ f.adapter :net_http
49
+ end
50
+ conn.get('http://example.com/endpoint1')
51
+ conn.post('http://example.com/endpoint2')
52
+ conn.delete('http://example.com/endpoint3')
53
+ assert_equal 3, trace.size
54
+ assert_equal :get, trace[0][:method]
55
+ assert_equal 200, trace[0][:status]
56
+ assert_equal :post, trace[1][:method]
57
+ assert_equal 201, trace[1][:status]
58
+ assert_equal :delete, trace[2][:method]
59
+ assert_equal 404, trace[2][:status]
60
+ end
61
+
62
+ def test_traces_error_responses
63
+ trace = []
64
+ stub_request(:get, 'http://example.com/error').to_return(status: 500, body: 'error')
65
+ conn =
66
+ Faraday.new do |f|
67
+ f.use Fbe::Middleware::Trace, trace
68
+ f.adapter :net_http
69
+ end
70
+ conn.get('http://example.com/error')
71
+ assert_equal 1, trace.size
72
+ entry = trace.first
73
+ assert_equal 500, entry[:status]
74
+ assert_equal 'http://example.com/error', entry[:url]
75
+ end
76
+
77
+ def test_handles_connection_errors
78
+ trace = []
79
+ stub_request(:get, 'http://example.com/timeout').to_timeout
80
+ conn =
81
+ Faraday.new do |f|
82
+ f.use Fbe::Middleware::Trace, trace
83
+ f.adapter :net_http
84
+ end
85
+ assert_raises(Faraday::ConnectionFailed) do
86
+ conn.get('http://example.com/timeout')
87
+ end
88
+ assert_equal 0, trace.size
89
+ end
90
+
91
+ def test_preserves_request_with_query_params
92
+ trace = []
93
+ stub_request(:get, 'http://example.com/search').with(query: { 'q' => 'test', 'page' => '2' }).to_return(status: 200)
94
+ conn =
95
+ Faraday.new do |f|
96
+ f.use Fbe::Middleware::Trace, trace
97
+ f.adapter :net_http
98
+ end
99
+ conn.get('http://example.com/search?q=test&page=2')
100
+ assert_equal 1, trace.size
101
+ url = trace.first[:url]
102
+ assert url.start_with?('http://example.com/search?')
103
+ assert_includes url, 'q=test'
104
+ assert_includes url, 'page=2'
105
+ end
106
+ end
@@ -322,4 +322,48 @@ class TestOcto < Fbe::Test
322
322
  }
323
323
  end
324
324
  end
325
+
326
+ def test_print_trace
327
+ loog = Loog::Buffer.new
328
+ WebMock.disable_net_connect!
329
+ stub_request(:get, 'https://api.github.com/user/123').to_return(
330
+ status: 200,
331
+ body: '{"id":123,"login":"test"}'
332
+ )
333
+ stub_request(:get, 'https://api.github.com/repos/foo/bar').to_return(
334
+ status: 200,
335
+ body: '{"id":456,"full_name":"foo/bar"}'
336
+ )
337
+ octo = Fbe.octo(loog:, global: {}, options: Judges::Options.new)
338
+ octo.user(123)
339
+ octo.repository('foo/bar')
340
+ octo.repository('foo/bar')
341
+ octo.print_trace!
342
+ output = loog.to_s
343
+ assert_includes output, 'GitHub API trace (2 URLs vs 3 requests)'
344
+ assert_includes output, 'https://api.github.com/user/123: 1'
345
+ assert_includes output, 'https://api.github.com/repos/foo/bar: 2'
346
+ repo_index = output.index('https://api.github.com/repos/foo/bar: 2')
347
+ user_index = output.index('https://api.github.com/user/123: 1')
348
+ assert_operator repo_index, :<, user_index, 'URLs should be sorted by request count (highest first)'
349
+ end
350
+
351
+ def test_trace_gets_cleared_after_print
352
+ WebMock.disable_net_connect!
353
+ stub_request(:get, 'https://api.github.com/user/456').to_return(
354
+ status: 200,
355
+ body: '{"id":456,"login":"testuser"}'
356
+ )
357
+ first_loog = Loog::Buffer.new
358
+ octo = Fbe.octo(loog: first_loog, global: {}, options: Judges::Options.new)
359
+ octo.user(456)
360
+ octo.print_trace!
361
+ first_output = first_loog.to_s
362
+ assert_includes first_output, 'GitHub API trace'
363
+ second_loog = Loog::Buffer.new
364
+ octo.instance_variable_set(:@loog, second_loog)
365
+ octo.print_trace!
366
+ second_output = second_loog.to_s
367
+ assert_includes second_output, 'GitHub API trace is empty'
368
+ end
325
369
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fbe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.1
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -307,6 +307,7 @@ files:
307
307
  - lib/fbe/just_one.rb
308
308
  - lib/fbe/middleware.rb
309
309
  - lib/fbe/middleware/formatter.rb
310
+ - lib/fbe/middleware/trace.rb
310
311
  - lib/fbe/octo.rb
311
312
  - lib/fbe/overwrite.rb
312
313
  - lib/fbe/pmp.rb
@@ -318,6 +319,7 @@ files:
318
319
  - renovate.json
319
320
  - rules/basic.fe
320
321
  - test/fbe/middleware/test_formatter.rb
322
+ - test/fbe/middleware/test_trace.rb
321
323
  - test/fbe/test_award.rb
322
324
  - test/fbe/test_bylaws.rb
323
325
  - test/fbe/test_conclude.rb