gitlab_quality-test_tooling 2.16.0 → 2.24.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.tool-versions +1 -1
  4. data/Gemfile.lock +30 -28
  5. data/README.md +1 -1
  6. data/exe/epic-readiness-notification +58 -0
  7. data/exe/post-to-slack +4 -0
  8. data/exe/relate-failure-issue +9 -0
  9. data/exe/test-coverage +123 -0
  10. data/lib/gitlab_quality/test_tooling/click_house/client.rb +111 -0
  11. data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +98 -0
  12. data/lib/gitlab_quality/test_tooling/code_coverage/category_owners.rb +158 -0
  13. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb +62 -0
  14. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +108 -0
  15. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +73 -0
  16. data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +82 -0
  17. data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +91 -0
  18. data/lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb +43 -0
  19. data/lib/gitlab_quality/test_tooling/code_coverage/test_map.rb +93 -0
  20. data/lib/gitlab_quality/test_tooling/code_coverage/utils.rb +18 -0
  21. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +1 -1
  22. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +11 -0
  23. data/lib/gitlab_quality/test_tooling/feature_readiness/epic_readiness_notifier.rb +308 -0
  24. data/lib/gitlab_quality/test_tooling/gcs_tools.rb +49 -0
  25. data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +2 -9
  26. data/lib/gitlab_quality/test_tooling/gitlab_client/group_labels_client.rb +34 -0
  27. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +1 -1
  28. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +2 -2
  29. data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +1 -1
  30. data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +1 -1
  31. data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +2 -2
  32. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +2 -2
  33. data/lib/gitlab_quality/test_tooling/report/group_issues/error_message_normalizer.rb +49 -0
  34. data/lib/gitlab_quality/test_tooling/report/group_issues/error_pattern_matcher.rb +36 -0
  35. data/lib/gitlab_quality/test_tooling/report/group_issues/failure_processor.rb +73 -0
  36. data/lib/gitlab_quality/test_tooling/report/group_issues/group_results_in_issues.rb +48 -0
  37. data/lib/gitlab_quality/test_tooling/report/group_issues/incident_checker.rb +61 -0
  38. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_base.rb +48 -0
  39. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_creator.rb +44 -0
  40. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_finder.rb +81 -0
  41. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_formatter.rb +83 -0
  42. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_manager.rb +33 -0
  43. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_updater.rb +87 -0
  44. data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +6 -3
  45. data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
  46. data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +2 -6
  47. data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +176 -5
  48. data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +0 -1
  49. data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +2 -1
  50. data/lib/gitlab_quality/test_tooling/runtime/env.rb +9 -4
  51. data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +103 -3
  52. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +1 -1
  53. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +1 -1
  54. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +1 -1
  55. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +1 -1
  56. data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +39 -11
  57. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +115 -15
  58. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb +61 -36
  59. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +125 -80
  60. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +95 -0
  61. data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +6 -2
  62. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  63. data/lib/gitlab_quality/test_tooling.rb +3 -0
  64. metadata +82 -55
  65. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/log_test_metrics.rb +0 -117
  66. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +0 -49
  67. 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: 0017f4373fb21a5d7313301ca050e381a4e462ec74a50a41aab37ab2afbd31d7
4
- data.tar.gz: 10c3f5d6750b27058de16f482b3286b5bc96c57b3565f2494965d2d255512462
3
+ metadata.gz: b18e1abd81ec1c5b3c2d215c28fe53d68880a94d76b12c8693afb64d4b3cc9c2
4
+ data.tar.gz: f9b3ede1d0e05492f9395ffedfd3132ee528df4343074a89518cffe1521f466d
5
5
  SHA512:
6
- metadata.gz: c4963020e2f5e405efd8e984966e918430eedb95ba354a6aff4fef068330cd3a152d68d5008b0d4325c5871654c54251206f11e2386f58fa34893e7338f5bb76
7
- data.tar.gz: 5489a80567198e869517b5aca8e9f1d35871ec470d9e716f62c97e6b15e8afae7f5f7f7622e62e6273294f9268845e1874bcba9677c5f2f52bf5ccefb806774e
6
+ metadata.gz: 4f36f26d06eda97591aa4b46b5beb6db2563b269cc97e8cc987b6fb17310e836efeb2b600fef03a0d70305fbf82282bc251a6206c1e7123b80500fe3f6d6b6d5
7
+ data.tar.gz: 53f3dd0472c5539605440db234c7c6b3f361831af54e418dbc482a5ae9b608b33179c9bdc2d671fe2b0a99b11b873433c2fe3c8b6c0d0394fae2a58021fd7f08
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.2.5
1
+ 3.3.9
data/.tool-versions CHANGED
@@ -1,2 +1,2 @@
1
- ruby 3.2.5
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.16.0)
4
+ gitlab_quality-test_tooling (2.24.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 (12.0.1)
132
- rubocop (~> 1.62.1)
133
- rubocop-factory_bot (~> 2.25.1)
134
- rubocop-graphql (~> 1.5.0)
135
- rubocop-performance (~> 1.20.2)
136
- rubocop-rails (~> 2.24.0)
137
- rubocop-rspec (~> 2.27.1)
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.7)
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.9.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.62.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 (>= 1.8, < 3.0)
321
- rexml (>= 3.2.5, < 4.0)
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, < 3.0)
325
- rubocop-ast (1.32.3)
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.25.1)
330
- rubocop (~> 1.41)
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.20.2)
334
+ rubocop-performance (1.21.1)
334
335
  rubocop (>= 1.48.1, < 2.0)
335
- rubocop-ast (>= 1.30.0, < 2.0)
336
- rubocop-rails (2.24.1)
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.33.0, < 2.0)
340
+ rubocop (>= 1.52.0, < 2.0)
340
341
  rubocop-ast (>= 1.31.1, < 2.0)
341
- rubocop-rspec (2.27.1)
342
- rubocop (~> 1.40)
343
- rubocop-capybara (~> 2.17)
344
- rubocop-factory_bot (~> 2.22)
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 (~> 12.0)
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.5.19
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}"
@@ -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,123 @@
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 = [:working_dir, :rspec_reports_jobs, :rspec_reports_glob, :coverage_report_path, :test_map_url]
19
+
20
+ options = OptionParser.new do |opts|
21
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
22
+
23
+ opts.on('--working_dir PATH', 'Working directory path') do |path|
24
+ params[:working_dir] = path
25
+ end
26
+
27
+ opts.on('--rspec-reports-jobs JOB1,...', Array, 'Comma-separated names of RSpec JSON jobs') do |jobs|
28
+ params[:rspec_reports_jobs] = jobs
29
+ end
30
+
31
+ opts.on('--rspec-reports-glob GLOB', 'Glob pattern for RSpec JSON reports') do |pattern|
32
+ params[:rspec_reports_glob] = pattern
33
+ end
34
+
35
+ opts.on('--coverage-report-path PATH', 'Path to LCOV coverage report') do |path|
36
+ params[:coverage_report_path] = path
37
+ end
38
+
39
+ opts.on('--test-map-url URL', 'URL for the test map') do |url|
40
+ params[:test_map_url] = url
41
+ end
42
+
43
+ opts.on('-h', '--help', 'Show the usage') do
44
+ puts opts
45
+ puts "\nExamples:"
46
+ puts " #{$PROGRAM_NAME}"
47
+ exit
48
+ end
49
+
50
+ opts.on_tail('-v', '--version', 'Show the version') do
51
+ require_relative "../lib/gitlab_quality/test_tooling/version"
52
+ puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
53
+ exit
54
+ end
55
+
56
+ opts.parse(ARGV)
57
+ end
58
+
59
+ if params.any? && (required_params - params.keys).none?
60
+ artifacts = GitlabQuality::TestTooling::CodeCoverage::Artifacts.new(
61
+ working_dir: params[:working_dir],
62
+ rspec_reports_jobs: params[:rspec_reports_jobs],
63
+ rspec_reports_glob: params[:rspec_reports_glob],
64
+ coverage_report_path: params[:coverage_report_path],
65
+ test_map_url: params[:test_map_url]
66
+ )
67
+
68
+ coverage_report = artifacts.coverage_report
69
+ rspec_reports = artifacts.rspec_reports
70
+ test_map = artifacts.test_map
71
+
72
+ code_coverage_by_source_file = GitlabQuality::TestTooling::CodeCoverage::LcovFile.new(coverage_report).parsed_content
73
+
74
+ source_file_to_tests = GitlabQuality::TestTooling::CodeCoverage::TestMap.new(test_map).source_to_tests
75
+
76
+ tests_to_categories = rspec_reports.reduce({}) do |combined_hash, rspec_report_file|
77
+ file_categories = GitlabQuality::TestTooling::CodeCoverage::RspecReport.new(rspec_report_file).tests_to_categories
78
+ combined_hash.merge(file_categories) { |_, old_val, new_val| (old_val + new_val).uniq }
79
+ end
80
+
81
+ category_owners = GitlabQuality::TestTooling::CodeCoverage::CategoryOwners.new
82
+
83
+ coverage_data = GitlabQuality::TestTooling::CodeCoverage::CoverageData.new(
84
+ code_coverage_by_source_file,
85
+ source_file_to_tests,
86
+ tests_to_categories,
87
+ category_owners.categories_to_teams
88
+ )
89
+
90
+ if ENV.fetch('CLICKHOUSE_URL', nil) &&
91
+ ENV.fetch('CLICKHOUSE_DATABASE', nil) &&
92
+ ENV.fetch('CLICKHOUSE_USERNAME', nil) &&
93
+ ENV.fetch('CLICKHOUSE_PASSWORD', nil)
94
+
95
+ clickhouse_data = {
96
+ url: ENV.fetch('CLICKHOUSE_URL', nil),
97
+ database: ENV.fetch('CLICKHOUSE_DATABASE', nil),
98
+ username: ENV.fetch('CLICKHOUSE_USERNAME', nil),
99
+ password: ENV.fetch('CLICKHOUSE_PASSWORD', nil)
100
+ }
101
+
102
+ category_owners_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable.new(**clickhouse_data)
103
+ coverage_metrics_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CoverageMetricsTable.new(**clickhouse_data)
104
+
105
+ category_owners_table.create if ENV['CLICKHOUSE_CREATE_CATEGORY_OWNERS_TABLE'] == 'true'
106
+ coverage_metrics_table.create if ENV['CLICKHOUSE_CREATE_COVERAGE_METRICS_TABLE'] == 'true'
107
+
108
+ if ENV['CLICKHOUSE_PUSH_CATEGORY_DATA'] == 'true'
109
+ category_owners_table.truncate
110
+ category_owners_table.push(category_owners.as_db_table)
111
+ end
112
+
113
+ coverage_metrics_table.push(coverage_data.as_db_table)
114
+ else
115
+ puts "ClickHouse configuration not found.\n" \
116
+ 'Set CLICKHOUSE_URL, CLICKHOUSE_DATABASE, CLICKHOUSE_USERNAME, ' \
117
+ 'CLICKHOUSE_PASSWORD environment variables to enable ClickHouse export.'
118
+ end
119
+ else
120
+ puts "Missing argument(s). Required arguments are: #{required_params}\nPassed arguments are: #{params}\n"
121
+ puts options
122
+ exit 1
123
+ 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,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'stringio'
6
+ require 'uri'
7
+ require 'zlib'
8
+
9
+ require_relative 'utils'
10
+
11
+ module GitlabQuality
12
+ module TestTooling
13
+ module CodeCoverage
14
+ class Artifacts
15
+ include Utils
16
+
17
+ MAX_RETRIES = 7 # retries with exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 64s (+ 0-1s jitter)
18
+
19
+ def initialize(working_dir:, coverage_report_path:, rspec_reports_jobs:, rspec_reports_glob:, test_map_url:)
20
+ @working_dir = working_dir
21
+ @rspec_reports_jobs = rspec_reports_jobs
22
+ @rspec_reports_glob = rspec_reports_glob
23
+ @coverage_report_path = coverage_report_path
24
+ @test_map_uri = URI.parse(test_map_url)
25
+ end
26
+
27
+ def rspec_reports
28
+ @rspec_report_files ||= rspec_reports_paths.map do |report_path|
29
+ JSON.parse(File.read(report_path))
30
+ rescue JSON::ParserError => e
31
+ raise "Invalid JSON in RSpec report file #{report_path}: #{e.message}"
32
+ end
33
+ end
34
+
35
+ def coverage_report
36
+ @coverage_report ||= read_coverage_reports
37
+ end
38
+
39
+ def test_map
40
+ @test_map ||= fetch_test_map_from_url
41
+ end
42
+
43
+ private
44
+
45
+ def rspec_reports_paths
46
+ @rspec_reports_paths ||= begin
47
+ paths = @rspec_reports_jobs.flat_map do |job_name|
48
+ Dir.glob(File.join(@working_dir, job_name, @rspec_reports_glob))
49
+ end.compact
50
+
51
+ if paths.empty? || paths.none? { |path| File.exist?(path) } # rubocop:disable Style/IfUnlessModifier
52
+ raise "No RSpec reports found in #{@working_dir}/<job-name>/ for these jobs: #{@rspec_reports_jobs}."
53
+ end
54
+
55
+ paths
56
+ end
57
+ end
58
+
59
+ def read_coverage_reports
60
+ raise "Coverage report not found in: #{@coverage_report_path}" unless File.exist?(@coverage_report_path)
61
+
62
+ File.read(@coverage_report_path)
63
+ end
64
+
65
+ def fetch_test_map_from_url
66
+ attempt = 0
67
+
68
+ begin
69
+ attempt += 1
70
+ response = http_get(@test_map_uri)
71
+ raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
72
+
73
+ JSON.parse(decompressed_gzip(response.body))
74
+ rescue StandardError => e
75
+ if attempt <= MAX_RETRIES
76
+ sleep_duration = exponential_delay_with_jitter(attempt)
77
+ warn "Attempt #{attempt}/#{MAX_RETRIES} failed: #{e.message}. Retrying in #{sleep_duration.round(2)}s..."
78
+ sleep(sleep_duration)
79
+ retry
80
+ end
81
+
82
+ raise "Failed to fetch test map from #{@test_map_uri} after #{MAX_RETRIES} attempts: #{e.message}"
83
+ end
84
+ end
85
+
86
+ def http_get(uri)
87
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
88
+ http.get(uri.request_uri)
89
+ end
90
+ end
91
+
92
+ def decompressed_gzip(gzipped_data)
93
+ Zlib::GzipReader.new(StringIO.new(gzipped_data)).read
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end