gitlab_quality-test_tooling 2.16.0 → 2.25.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 +4 -4
- data/.ruby-version +1 -1
- data/.tool-versions +1 -1
- data/Gemfile.lock +30 -28
- data/README.md +1 -1
- data/exe/epic-readiness-notification +58 -0
- data/exe/post-to-slack +4 -0
- data/exe/relate-failure-issue +9 -0
- data/exe/test-coverage +113 -0
- data/lib/gitlab_quality/test_tooling/click_house/client.rb +111 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +77 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/category_owners.rb +158 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb +62 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +109 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +73 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +82 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +91 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb +43 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/test_map.rb +93 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/utils.rb +18 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +1 -1
- data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +11 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/epic_readiness_notifier.rb +308 -0
- data/lib/gitlab_quality/test_tooling/gcs_tools.rb +49 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +2 -9
- data/lib/gitlab_quality/test_tooling/gitlab_client/group_labels_client.rb +34 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +1 -1
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +2 -2
- data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +2 -2
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +2 -2
- data/lib/gitlab_quality/test_tooling/report/group_issues/error_message_normalizer.rb +49 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/error_pattern_matcher.rb +36 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/failure_processor.rb +73 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/group_results_in_issues.rb +48 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/incident_checker.rb +61 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_base.rb +48 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_creator.rb +44 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_finder.rb +81 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_formatter.rb +83 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_manager.rb +33 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_updater.rb +87 -0
- data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +6 -3
- data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +2 -6
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +176 -5
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +0 -1
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +2 -1
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +9 -4
- data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +103 -3
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +1 -1
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +1 -1
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +1 -1
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +1 -1
- data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +39 -11
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +115 -15
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb +61 -36
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +125 -80
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +95 -0
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +6 -2
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- data/lib/gitlab_quality/test_tooling.rb +3 -0
- metadata +82 -55
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/log_test_metrics.rb +0 -117
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +0 -49
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/influxdb_tools.rb +0 -33
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c4f4d94f6b20e886a63d66dfa3d5a83c3148157f5f6f1f9cd636584bc91725c
|
|
4
|
+
data.tar.gz: 6364dc1082fa13c109bf69fafab81f8b80e6a9737a0d5b40a72e9f109fa5e282
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d2116ba2746a0e91c3a299899c939b2c85cb70349c20174266ef4fb8ce5a4a09906af15b688a7fb3b3d5f7d19da8f387d3e58540a3c7ec253f4665fa406704c8
|
|
7
|
+
data.tar.gz: 7ac75026422eaf93c582e0e09fc291bd111f83c55d13ea2fe0cb86446e84608303610003685826cdd8e8b83a01a029b42d934b082d55b013fc7bc7d5f6f9830b
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.3.9
|
data/.tool-versions
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
ruby 3.
|
|
1
|
+
ruby 3.3.9
|
|
2
2
|
lefthook 1.7.14
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
gitlab_quality-test_tooling (2.
|
|
4
|
+
gitlab_quality-test_tooling (2.25.1)
|
|
5
5
|
activesupport (>= 7.0, < 7.3)
|
|
6
6
|
amatch (~> 0.4.1)
|
|
7
7
|
fog-google (~> 1.24, >= 1.24.1)
|
|
@@ -128,13 +128,15 @@ GEM
|
|
|
128
128
|
danger (>= 8.4.5)
|
|
129
129
|
danger-gitlab (>= 8.0.0)
|
|
130
130
|
rake
|
|
131
|
-
gitlab-styles (
|
|
132
|
-
rubocop (
|
|
133
|
-
rubocop-
|
|
134
|
-
rubocop-
|
|
135
|
-
rubocop-
|
|
136
|
-
rubocop-
|
|
137
|
-
rubocop-
|
|
131
|
+
gitlab-styles (13.1.0)
|
|
132
|
+
rubocop (= 1.71.1)
|
|
133
|
+
rubocop-capybara (~> 2.21.0)
|
|
134
|
+
rubocop-factory_bot (~> 2.26.1)
|
|
135
|
+
rubocop-graphql (~> 1.5.4)
|
|
136
|
+
rubocop-performance (~> 1.21.1)
|
|
137
|
+
rubocop-rails (~> 2.26.0)
|
|
138
|
+
rubocop-rspec (~> 3.0.4)
|
|
139
|
+
rubocop-rspec_rails (~> 2.30.0)
|
|
138
140
|
google-apis-compute_v1 (0.108.0)
|
|
139
141
|
google-apis-core (>= 0.15.0, < 2.a)
|
|
140
142
|
google-apis-core (0.15.1)
|
|
@@ -262,7 +264,7 @@ GEM
|
|
|
262
264
|
pry (>= 0.13, < 0.15)
|
|
263
265
|
public_suffix (6.0.1)
|
|
264
266
|
racc (1.8.1)
|
|
265
|
-
rack (3.1
|
|
267
|
+
rack (3.2.1)
|
|
266
268
|
rainbow (3.1.1)
|
|
267
269
|
rake (13.2.1)
|
|
268
270
|
rb-fsevent (0.11.2)
|
|
@@ -270,7 +272,7 @@ GEM
|
|
|
270
272
|
ffi (~> 1.0)
|
|
271
273
|
rbs (2.8.4)
|
|
272
274
|
rchardet (1.8.0)
|
|
273
|
-
regexp_parser (2.
|
|
275
|
+
regexp_parser (2.11.2)
|
|
274
276
|
representable (3.2.0)
|
|
275
277
|
declarative (< 0.1.0)
|
|
276
278
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
|
@@ -311,37 +313,37 @@ GEM
|
|
|
311
313
|
rspec-support (3.13.1)
|
|
312
314
|
rspec_junit_formatter (0.6.0)
|
|
313
315
|
rspec-core (>= 2, < 4, != 2.12.0)
|
|
314
|
-
rubocop (1.
|
|
316
|
+
rubocop (1.71.1)
|
|
315
317
|
json (~> 2.3)
|
|
316
318
|
language_server-protocol (>= 3.17.0)
|
|
317
319
|
parallel (~> 1.10)
|
|
318
320
|
parser (>= 3.3.0.2)
|
|
319
321
|
rainbow (>= 2.2.2, < 4.0)
|
|
320
|
-
regexp_parser (>=
|
|
321
|
-
|
|
322
|
-
rubocop-ast (>= 1.31.1, < 2.0)
|
|
322
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
323
|
+
rubocop-ast (>= 1.38.0, < 2.0)
|
|
323
324
|
ruby-progressbar (~> 1.7)
|
|
324
|
-
unicode-display_width (>= 2.4.0, <
|
|
325
|
-
rubocop-ast (1.
|
|
325
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
326
|
+
rubocop-ast (1.40.0)
|
|
326
327
|
parser (>= 3.3.1.0)
|
|
327
328
|
rubocop-capybara (2.21.0)
|
|
328
329
|
rubocop (~> 1.41)
|
|
329
|
-
rubocop-factory_bot (2.
|
|
330
|
-
rubocop (~> 1.
|
|
330
|
+
rubocop-factory_bot (2.26.1)
|
|
331
|
+
rubocop (~> 1.61)
|
|
331
332
|
rubocop-graphql (1.5.4)
|
|
332
333
|
rubocop (>= 1.50, < 2)
|
|
333
|
-
rubocop-performance (1.
|
|
334
|
+
rubocop-performance (1.21.1)
|
|
334
335
|
rubocop (>= 1.48.1, < 2.0)
|
|
335
|
-
rubocop-ast (>= 1.
|
|
336
|
-
rubocop-rails (2.
|
|
336
|
+
rubocop-ast (>= 1.31.1, < 2.0)
|
|
337
|
+
rubocop-rails (2.26.2)
|
|
337
338
|
activesupport (>= 4.2.0)
|
|
338
339
|
rack (>= 1.1)
|
|
339
|
-
rubocop (>= 1.
|
|
340
|
+
rubocop (>= 1.52.0, < 2.0)
|
|
340
341
|
rubocop-ast (>= 1.31.1, < 2.0)
|
|
341
|
-
rubocop-rspec (
|
|
342
|
-
rubocop (~> 1.
|
|
343
|
-
|
|
344
|
-
rubocop
|
|
342
|
+
rubocop-rspec (3.0.5)
|
|
343
|
+
rubocop (~> 1.61)
|
|
344
|
+
rubocop-rspec_rails (2.30.0)
|
|
345
|
+
rubocop (~> 1.61)
|
|
346
|
+
rubocop-rspec (~> 3, >= 3.0.1)
|
|
345
347
|
ruby-progressbar (1.13.0)
|
|
346
348
|
sawyer (0.9.2)
|
|
347
349
|
addressable (>= 2.3.5)
|
|
@@ -409,7 +411,7 @@ PLATFORMS
|
|
|
409
411
|
DEPENDENCIES
|
|
410
412
|
climate_control (~> 1.2)
|
|
411
413
|
gitlab-dangerfiles (~> 3.8)
|
|
412
|
-
gitlab-styles (~>
|
|
414
|
+
gitlab-styles (~> 13.1)
|
|
413
415
|
gitlab_quality-test_tooling!
|
|
414
416
|
guard-rspec (~> 4.7)
|
|
415
417
|
lefthook (~> 1.3)
|
|
@@ -425,4 +427,4 @@ DEPENDENCIES
|
|
|
425
427
|
webmock (= 3.7.0)
|
|
426
428
|
|
|
427
429
|
BUNDLED WITH
|
|
428
|
-
2.
|
|
430
|
+
2.7.2
|
data/README.md
CHANGED
|
@@ -324,7 +324,7 @@ See an [example](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92580) fo
|
|
|
324
324
|
|
|
325
325
|
[Automated gem release process](https://gitlab.com/gitlab-org/quality/pipeline-common#release-process) is used to release new version of `gitlab_quality-test_tooling` through pipelines, and this will:
|
|
326
326
|
|
|
327
|
-
- Publish the gem: https://rubygems.org/gems/gitlab_quality-test_tooling
|
|
327
|
+
- Publish the gem: https://rubygems.org/gems/gitlab_quality-test_tooling (once the version bump is done below in [Steps to release](https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling#steps-to-release))
|
|
328
328
|
- Add a release in the `gitlab_quality-test_tooling` project: https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling/-/releases
|
|
329
329
|
- Populate the release log with the API contents. For example: https://gitlab.com/api/v4/projects/19861191/repository/changelog?version=3.4.4
|
|
330
330
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require 'active_support/core_ext/hash'
|
|
7
|
+
|
|
8
|
+
require_relative "../lib/gitlab_quality/test_tooling"
|
|
9
|
+
params = {}
|
|
10
|
+
|
|
11
|
+
options = OptionParser.new do |opts|
|
|
12
|
+
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
|
13
|
+
|
|
14
|
+
opts.on('-u', '--epic-urls URLS', String, 'Comma-separated list of epic URLs') do |urls|
|
|
15
|
+
params[:epic_urls] = urls.split(',').map(&:strip)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
opts.on('-f', '--epic-urls-file FILE', String, 'File containing epic URLs (one per line)') do |file|
|
|
19
|
+
params[:epic_urls_file] = file
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and appropriate permissions') do |token|
|
|
23
|
+
params[:token] = token
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
opts.on('-m', '--message MESSAGE', String, 'Custom message template (optional)') do |message|
|
|
27
|
+
params[:message] = message
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
opts.on('--dry-run', "Perform a dry-run (don't post comments)") do
|
|
31
|
+
params[:dry_run] = true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
opts.on_tail('-v', '--version', 'Show the version') do
|
|
35
|
+
require_relative "../lib/gitlab_quality/test_tooling/version"
|
|
36
|
+
puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
|
|
37
|
+
exit
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
opts.on_tail('-h', '--help', 'Show the usage') do
|
|
41
|
+
puts "Purpose: Send feature readiness assessment notifications to epic authors via @mentions"
|
|
42
|
+
puts opts
|
|
43
|
+
exit
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
opts.parse(ARGV)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Validate required arguments
|
|
50
|
+
raise ArgumentError, "Missing required argument: --token" unless params[:token]
|
|
51
|
+
raise ArgumentError, "Must provide either --epic-urls or --epic-urls-file" unless params[:epic_urls] || params[:epic_urls_file]
|
|
52
|
+
|
|
53
|
+
if params.any?
|
|
54
|
+
GitlabQuality::TestTooling::FeatureReadiness::EpicReadinessNotifier.new(**params).invoke!
|
|
55
|
+
else
|
|
56
|
+
puts options
|
|
57
|
+
exit 1
|
|
58
|
+
end
|
data/exe/post-to-slack
CHANGED
|
@@ -77,6 +77,10 @@ options = OptionParser.new do |opts|
|
|
|
77
77
|
params[:icon_emoji] = icon_emoji
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
opts.on('-e', '--environment-issues-file FILE', String, 'Add environment issues alert based on grouped failure data in a file') do |file|
|
|
81
|
+
params[:environment_issues_file] = file
|
|
82
|
+
end
|
|
83
|
+
|
|
80
84
|
opts.on_tail('-v', '--version', 'Show the version') do
|
|
81
85
|
require_relative "../lib/gitlab_quality/test_tooling/version"
|
|
82
86
|
puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
|
data/exe/relate-failure-issue
CHANGED
|
@@ -58,6 +58,14 @@ options = OptionParser.new do |opts|
|
|
|
58
58
|
params[:dry_run] = true
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
opts.on("--group-similar", "Enable grouping similar issues") do
|
|
62
|
+
params[:group_similar] = true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
opts.on('--environment-issues-output-file FILE', String, 'Output file for environment issues data (JSON)') do |file|
|
|
66
|
+
params[:environment_issues_output_file] = file
|
|
67
|
+
end
|
|
68
|
+
|
|
61
69
|
opts.on_tail('-v', '--version', 'Show the version') do
|
|
62
70
|
require_relative "../lib/gitlab_quality/test_tooling/version"
|
|
63
71
|
puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
|
|
@@ -66,6 +74,7 @@ options = OptionParser.new do |opts|
|
|
|
66
74
|
|
|
67
75
|
opts.on_tail('-h', '--help', 'Show the usage') do
|
|
68
76
|
puts "Purpose: Relate test failures to failure issues from RSpec report files (JSON or JUnit XML)"
|
|
77
|
+
puts ""
|
|
69
78
|
puts opts
|
|
70
79
|
exit
|
|
71
80
|
end
|
data/exe/test-coverage
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
require_relative "../lib/gitlab_quality/test_tooling"
|
|
7
|
+
|
|
8
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/category_owners'
|
|
9
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table'
|
|
10
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table'
|
|
11
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/coverage_data'
|
|
12
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/lcov_file'
|
|
13
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/artifacts'
|
|
14
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/rspec_report'
|
|
15
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_map'
|
|
16
|
+
|
|
17
|
+
params = {}
|
|
18
|
+
required_params = [:rspec_reports, :coverage_report, :test_map]
|
|
19
|
+
|
|
20
|
+
options = OptionParser.new do |opts|
|
|
21
|
+
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
|
22
|
+
|
|
23
|
+
opts.on('--rspec-reports GLOB', 'Glob pattern for RSpec JSON reports (e.g., "rspec/rspec-*.json")') do |pattern|
|
|
24
|
+
params[:rspec_reports] = pattern
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
opts.on('--coverage-report PATH', 'Path to the LCOV coverage report (e.g., "coverage/lcov/gitlab.lcov")') do |path|
|
|
28
|
+
params[:coverage_report] = path
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
opts.on('--test-map PATH', 'Path to the test map file (e.g., "crystalball/packed-mapping.json.gz")') do |path|
|
|
32
|
+
params[:test_map] = path
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
opts.on('-h', '--help', 'Show the usage') do
|
|
36
|
+
puts opts
|
|
37
|
+
puts "\nExamples:"
|
|
38
|
+
puts " #{$PROGRAM_NAME}"
|
|
39
|
+
exit
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
opts.on_tail('-v', '--version', 'Show the version') do
|
|
43
|
+
require_relative "../lib/gitlab_quality/test_tooling/version"
|
|
44
|
+
puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
|
|
45
|
+
exit
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
opts.parse(ARGV)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if params.any? && (required_params - params.keys).none?
|
|
52
|
+
artifacts = GitlabQuality::TestTooling::CodeCoverage::Artifacts.new(
|
|
53
|
+
rspec_reports: params[:rspec_reports],
|
|
54
|
+
coverage_report: params[:coverage_report],
|
|
55
|
+
test_map: params[:test_map]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
coverage_report = artifacts.coverage_report
|
|
59
|
+
rspec_reports = artifacts.rspec_reports
|
|
60
|
+
test_map = artifacts.test_map
|
|
61
|
+
|
|
62
|
+
code_coverage_by_source_file = GitlabQuality::TestTooling::CodeCoverage::LcovFile.new(coverage_report).parsed_content
|
|
63
|
+
|
|
64
|
+
source_file_to_tests = GitlabQuality::TestTooling::CodeCoverage::TestMap.new(test_map).source_to_tests
|
|
65
|
+
|
|
66
|
+
tests_to_categories = rspec_reports.reduce({}) do |combined_hash, rspec_report_file|
|
|
67
|
+
file_categories = GitlabQuality::TestTooling::CodeCoverage::RspecReport.new(rspec_report_file).tests_to_categories
|
|
68
|
+
combined_hash.merge(file_categories) { |_, old_val, new_val| (old_val + new_val).uniq }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
category_owners = GitlabQuality::TestTooling::CodeCoverage::CategoryOwners.new
|
|
72
|
+
|
|
73
|
+
coverage_data = GitlabQuality::TestTooling::CodeCoverage::CoverageData.new(
|
|
74
|
+
code_coverage_by_source_file,
|
|
75
|
+
source_file_to_tests,
|
|
76
|
+
tests_to_categories,
|
|
77
|
+
category_owners.categories_to_teams
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if ENV.fetch('CLICKHOUSE_URL', nil) &&
|
|
81
|
+
ENV.fetch('CLICKHOUSE_DATABASE', nil) &&
|
|
82
|
+
ENV.fetch('CLICKHOUSE_USERNAME', nil) &&
|
|
83
|
+
ENV.fetch('CLICKHOUSE_PASSWORD', nil)
|
|
84
|
+
|
|
85
|
+
clickhouse_data = {
|
|
86
|
+
url: ENV.fetch('CLICKHOUSE_URL', nil),
|
|
87
|
+
database: ENV.fetch('CLICKHOUSE_DATABASE', nil),
|
|
88
|
+
username: ENV.fetch('CLICKHOUSE_USERNAME', nil),
|
|
89
|
+
password: ENV.fetch('CLICKHOUSE_PASSWORD', nil)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
category_owners_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable.new(**clickhouse_data)
|
|
93
|
+
coverage_metrics_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CoverageMetricsTable.new(**clickhouse_data)
|
|
94
|
+
|
|
95
|
+
category_owners_table.create if ENV['CLICKHOUSE_CREATE_CATEGORY_OWNERS_TABLE'] == 'true'
|
|
96
|
+
coverage_metrics_table.create if ENV['CLICKHOUSE_CREATE_COVERAGE_METRICS_TABLE'] == 'true'
|
|
97
|
+
|
|
98
|
+
if ENV['CLICKHOUSE_PUSH_CATEGORY_DATA'] == 'true'
|
|
99
|
+
category_owners_table.truncate
|
|
100
|
+
category_owners_table.push(category_owners.as_db_table)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
coverage_metrics_table.push(coverage_data.as_db_table)
|
|
104
|
+
else
|
|
105
|
+
puts "ClickHouse configuration not found.\n" \
|
|
106
|
+
'Set CLICKHOUSE_URL, CLICKHOUSE_DATABASE, CLICKHOUSE_USERNAME, ' \
|
|
107
|
+
'CLICKHOUSE_PASSWORD environment variables to enable ClickHouse export.'
|
|
108
|
+
end
|
|
109
|
+
else
|
|
110
|
+
puts "Missing argument(s). Required arguments are: #{required_params}\nPassed arguments are: #{params}\n"
|
|
111
|
+
puts options
|
|
112
|
+
exit 1
|
|
113
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "httparty"
|
|
4
|
+
require "json"
|
|
5
|
+
require "logger"
|
|
6
|
+
require "active_support/core_ext/object/blank"
|
|
7
|
+
|
|
8
|
+
module GitlabQuality
|
|
9
|
+
module TestTooling
|
|
10
|
+
module ClickHouse
|
|
11
|
+
class Client
|
|
12
|
+
DEFAULT_BATCH_SIZE = 100_000
|
|
13
|
+
LOG_PREFIX = "[ClickHouse]"
|
|
14
|
+
|
|
15
|
+
def initialize(url:, database:, username: nil, password: nil, logger: ::Logger.new($stdout, level: 1))
|
|
16
|
+
@url = url
|
|
17
|
+
@database = database
|
|
18
|
+
@username = username
|
|
19
|
+
@password = password
|
|
20
|
+
@logger = logger
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Perform sql query
|
|
24
|
+
#
|
|
25
|
+
# @param sql [String]
|
|
26
|
+
# @param format [String]
|
|
27
|
+
# @return [Array, Hash, String]
|
|
28
|
+
def query(sql, format: "JSONEachRow")
|
|
29
|
+
logger.debug("Running #{sql}")
|
|
30
|
+
response = post(
|
|
31
|
+
body: sql,
|
|
32
|
+
content_type: "text/plain",
|
|
33
|
+
query_opts: { default_format: format }
|
|
34
|
+
)
|
|
35
|
+
raise "ClickHouse query failed: code: #{response.code}, error: #{response.body}" if response.code != 200
|
|
36
|
+
|
|
37
|
+
if format == "JSONEachRow"
|
|
38
|
+
response.body.split("\n").map { |row| JSON.parse(row) }
|
|
39
|
+
elsif %w[JSON JSONCompact].include?(format)
|
|
40
|
+
JSON.parse(response.body.presence || "{}")
|
|
41
|
+
else
|
|
42
|
+
response.body
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Push data to ClickHouse
|
|
47
|
+
#
|
|
48
|
+
# @param table_name [String]
|
|
49
|
+
# @param data [Array<Hash>]
|
|
50
|
+
# @param batch_size [Integer]
|
|
51
|
+
# @return [void]
|
|
52
|
+
def insert_json_data(table_name, data, batch_size: DEFAULT_BATCH_SIZE) # rubocop:disable Metrics/AbcSize
|
|
53
|
+
raise ArgumentError, "Expected data to be an Array, got #{data.class}" unless data.is_a?(Array)
|
|
54
|
+
raise ArgumentError, "Expected all elements of array to be hashes" unless data.is_a?(Array) && data.all?(Hash)
|
|
55
|
+
raise ArgumentError, "Expected data to not be empty" if data.empty?
|
|
56
|
+
|
|
57
|
+
total_batches = (data.size.to_f / batch_size).ceil
|
|
58
|
+
results = data.each_slice(batch_size).with_index.map do |batch, index|
|
|
59
|
+
logger.debug("#{LOG_PREFIX} Pushing batch #{index + 1} of #{total_batches}")
|
|
60
|
+
send_batch(table_name, batch)
|
|
61
|
+
end
|
|
62
|
+
logger.debug("#{LOG_PREFIX} Processed #{results.size} result batches")
|
|
63
|
+
return if results.all? { |res| res[:success] }
|
|
64
|
+
|
|
65
|
+
err = results
|
|
66
|
+
.reject { |res| res[:success] }
|
|
67
|
+
.map { |res| "batch_size: #{res[:count]}, err: #{res[:error]}" }
|
|
68
|
+
.join("\n")
|
|
69
|
+
raise "Failures detected when pushing data to ClickHouse, errors:\n#{err}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
attr_reader :url, :database, :username, :password, :logger
|
|
75
|
+
|
|
76
|
+
# Push batch of data
|
|
77
|
+
#
|
|
78
|
+
# @param table_name [String] table name
|
|
79
|
+
# @param batch [Array<Hash>] data batch
|
|
80
|
+
# @return [Hash]
|
|
81
|
+
def send_batch(table_name, batch)
|
|
82
|
+
response = post(
|
|
83
|
+
body: batch.map(&:to_json).join("\n"),
|
|
84
|
+
content_type: 'application/json',
|
|
85
|
+
query_opts: { query: "INSERT INTO #{table_name} FORMAT JSONEachRow" }
|
|
86
|
+
)
|
|
87
|
+
return { success: true, count: batch.size, response: response.body } if response.code == 200
|
|
88
|
+
|
|
89
|
+
{ success: false, count: batch.size, error: response.body }
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
{ success: false, count: batch.size, error: e.message }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Execute post request
|
|
95
|
+
#
|
|
96
|
+
# @param body [String]
|
|
97
|
+
# @param content_type [String]
|
|
98
|
+
# @param query_opts [Hash] additional query options
|
|
99
|
+
# @return [HTTParty::Response]
|
|
100
|
+
def post(body:, content_type:, query_opts: {})
|
|
101
|
+
HTTParty.post(url, {
|
|
102
|
+
body: body,
|
|
103
|
+
headers: { "Content-Type" => content_type },
|
|
104
|
+
query: { database: database, **query_opts }.compact,
|
|
105
|
+
basic_auth: !!(username && password) ? { username: username, password: password } : nil
|
|
106
|
+
}.compact)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
require 'zlib'
|
|
6
|
+
|
|
7
|
+
module GitlabQuality
|
|
8
|
+
module TestTooling
|
|
9
|
+
module CodeCoverage
|
|
10
|
+
class Artifacts
|
|
11
|
+
# Loads coverage artifacts from the filesystem
|
|
12
|
+
#
|
|
13
|
+
# @param rspec_reports [String] Glob pattern for RSpec JSON report files (e.g., "rspec/rspec-*.json")
|
|
14
|
+
# @param coverage_report [String] Path to the LCOV coverage report file (e.g., "coverage/lcov/gitlab.lcov")
|
|
15
|
+
# @param test_map [String] Path to the test map file, gzipped or plain JSON (e.g., "crystalball/packed-mapping.json.gz")
|
|
16
|
+
def initialize(rspec_reports:, coverage_report:, test_map:)
|
|
17
|
+
@rspec_reports_glob = rspec_reports
|
|
18
|
+
@coverage_report_path = coverage_report
|
|
19
|
+
@test_map_path = test_map
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def rspec_reports
|
|
23
|
+
@rspec_report_files ||= rspec_reports_paths.map do |report_path|
|
|
24
|
+
JSON.parse(File.read(report_path))
|
|
25
|
+
rescue JSON::ParserError => e
|
|
26
|
+
raise "Invalid JSON in RSpec report file #{report_path}: #{e.message}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def coverage_report
|
|
31
|
+
@coverage_report ||= read_coverage_reports
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_map
|
|
35
|
+
@test_map ||= fetch_test_map
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def rspec_reports_paths
|
|
41
|
+
@rspec_reports_paths ||= begin
|
|
42
|
+
paths = Dir.glob(@rspec_reports_glob)
|
|
43
|
+
|
|
44
|
+
raise "No RSpec reports found matching pattern: #{@rspec_reports_glob}" if paths.empty?
|
|
45
|
+
|
|
46
|
+
paths
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def read_coverage_reports
|
|
51
|
+
raise "Coverage report not found in: #{@coverage_report_path}" unless File.exist?(@coverage_report_path)
|
|
52
|
+
|
|
53
|
+
File.read(@coverage_report_path)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def fetch_test_map
|
|
57
|
+
raise "Test map file not found: #{@test_map_path}" unless File.exist?(@test_map_path)
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
content = File.read(@test_map_path)
|
|
61
|
+
|
|
62
|
+
# If it's a gzipped file, decompress it
|
|
63
|
+
content = decompressed_gzip(content) if @test_map_path.end_with?('.gz')
|
|
64
|
+
|
|
65
|
+
JSON.parse(content)
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
raise "Failed to read test map from #{@test_map_path}: #{e.message}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def decompressed_gzip(gzipped_data)
|
|
72
|
+
Zlib::GzipReader.new(StringIO.new(gzipped_data)).read
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|