gitlab_quality-test_tooling 2.16.0 → 3.0.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.
Files changed (69) 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 +155 -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 +92 -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 +80 -0
  14. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +140 -0
  15. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +75 -0
  16. data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +100 -0
  17. data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +169 -0
  18. data/lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb +43 -0
  19. data/lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier.rb +94 -0
  20. data/lib/gitlab_quality/test_tooling/code_coverage/test_map.rb +93 -0
  21. data/lib/gitlab_quality/test_tooling/code_coverage/test_report.rb +43 -0
  22. data/lib/gitlab_quality/test_tooling/code_coverage/utils.rb +18 -0
  23. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +1 -1
  24. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +11 -0
  25. data/lib/gitlab_quality/test_tooling/feature_readiness/epic_readiness_notifier.rb +308 -0
  26. data/lib/gitlab_quality/test_tooling/gcs_tools.rb +49 -0
  27. data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +2 -9
  28. data/lib/gitlab_quality/test_tooling/gitlab_client/group_labels_client.rb +34 -0
  29. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +1 -1
  30. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +2 -2
  31. data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +1 -1
  32. data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +1 -1
  33. data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +2 -2
  34. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +2 -2
  35. data/lib/gitlab_quality/test_tooling/report/group_issues/error_message_normalizer.rb +49 -0
  36. data/lib/gitlab_quality/test_tooling/report/group_issues/error_pattern_matcher.rb +36 -0
  37. data/lib/gitlab_quality/test_tooling/report/group_issues/failure_processor.rb +73 -0
  38. data/lib/gitlab_quality/test_tooling/report/group_issues/group_results_in_issues.rb +48 -0
  39. data/lib/gitlab_quality/test_tooling/report/group_issues/incident_checker.rb +61 -0
  40. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_base.rb +48 -0
  41. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_creator.rb +44 -0
  42. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_finder.rb +81 -0
  43. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_formatter.rb +83 -0
  44. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_manager.rb +33 -0
  45. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_updater.rb +87 -0
  46. data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +6 -3
  47. data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
  48. data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +2 -6
  49. data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +176 -5
  50. data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +0 -1
  51. data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +2 -1
  52. data/lib/gitlab_quality/test_tooling/runtime/env.rb +9 -4
  53. data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +103 -3
  54. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +1 -1
  55. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +1 -1
  56. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +1 -1
  57. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +1 -1
  58. data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +39 -11
  59. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +115 -15
  60. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb +61 -36
  61. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +126 -80
  62. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +96 -0
  63. data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +6 -2
  64. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  65. data/lib/gitlab_quality/test_tooling.rb +3 -0
  66. metadata +84 -55
  67. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/log_test_metrics.rb +0 -117
  68. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +0 -49
  69. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/influxdb_tools.rb +0 -33
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'yaml'
5
+
6
+ require_relative 'utils'
7
+
8
+ module GitlabQuality
9
+ module TestTooling
10
+ module CodeCoverage
11
+ class CategoryOwners
12
+ include Utils
13
+
14
+ SOURCE_URL = URI('https://gitlab.com/gitlab-com/www-gitlab-com/raw/master/data/stages.yml')
15
+ BASE_DELAY = 1 # seconds
16
+ MAX_RETRIES = 3
17
+
18
+ # @return [Hash] Category ownership hierarchy, section -> stage -> group -> [categories]
19
+ # @example Return value
20
+ # {
21
+ # "team_planning" => { # section
22
+ # "project_management" => { # stage
23
+ # "plan" => [ # group
24
+ # "dev", # category
25
+ # "service_desk" # category
26
+ # ],
27
+ # "product_planning" => [ # group
28
+ # "portfolio_management", # category
29
+ # ...
30
+ # ]
31
+ # }
32
+ # },
33
+ # ...
34
+ # }
35
+ attr_reader :hierarchy
36
+
37
+ def initialize
38
+ @categories_map = {}
39
+ @hierarchy = {}
40
+
41
+ yaml_file = fetch_yaml_file
42
+ yaml_content = YAML.load(yaml_file)
43
+ populate_ownership_hierarchy(yaml_content)
44
+ end
45
+
46
+ # @return [Array<Hash>] Flattened category ownership
47
+ # @example Return value
48
+ # [
49
+ # { category: "team_planning", group: "project_management", stage: "plan", section: "dev" },
50
+ # { category: "service_desk", group: "project_management", stage: "plan", section: "dev" },
51
+ # { category: "portfolio_management", group: "product_planning", stage: "plan", section: "dev" }
52
+ # ...
53
+ # ]
54
+ def as_db_table
55
+ hierarchy.each_with_object([]) do |(section, stages), flattened_hierarchy|
56
+ next unless stages
57
+
58
+ stages.each do |stage, groups|
59
+ next unless groups
60
+
61
+ groups.each do |group, categories|
62
+ Array(categories).each do |category|
63
+ flattened_hierarchy << {
64
+ category: category,
65
+ group: group,
66
+ stage: stage,
67
+ section: section
68
+ }
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ # @return [Hash] Mapping of categories to teams (i.e., groups, stages, sections)
76
+ # @example Return value
77
+ # {
78
+ # "team_planning" => { group: "project_management", stage: "plan", section: "dev" },
79
+ # "service_desk" => { group: "project_management", stage: "plan", section: "dev" },
80
+ # "portfolio_management" => { group: "product_planning", stage: "plan", section: "dev" },
81
+ # ...
82
+ # }
83
+ def categories_to_teams
84
+ populate_categories_map(hierarchy)
85
+ @categories_map
86
+ end
87
+
88
+ private
89
+
90
+ def fetch_yaml_file
91
+ retries = 0
92
+
93
+ begin
94
+ response = Net::HTTP.get_response(SOURCE_URL)
95
+ raise "Failed to fetch file: HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
96
+
97
+ response.body
98
+ rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, Errno::ETIMEDOUT, IOError, RuntimeError => e
99
+ retries += 1
100
+ raise "Failed to fetch YAML after #{MAX_RETRIES} retries: #{e.message}" if retries >= MAX_RETRIES
101
+
102
+ delay = exponential_delay_with_jitter(retries)
103
+ warn "Fetch attempt #{retries} failed: #{e.message}. Retrying in #{delay.round(2)}s..."
104
+ sleep(delay)
105
+ retry
106
+ end
107
+ end
108
+
109
+ def populate_ownership_hierarchy(data)
110
+ stages = data['stages'] || {}
111
+
112
+ stages.each do |stage, stage_data|
113
+ next unless stage_data.is_a?(Hash)
114
+
115
+ section = stage_data['section']
116
+ groups = stage_data['groups'] || {}
117
+ next unless section
118
+
119
+ groups.each { |group, group_data| add_hierarchy_entry(section, stage, group, group_data['categories']) }
120
+ end
121
+ end
122
+
123
+ def add_hierarchy_entry(section, stage, group, categories)
124
+ @hierarchy[section] ||= {}
125
+ @hierarchy[section][stage] ||= {}
126
+ @hierarchy[section][stage][group] = categories || []
127
+ end
128
+
129
+ def populate_categories_map(data, current_section = nil, current_stage = nil, current_group = nil)
130
+ case data
131
+ when Hash # Sections / Stages / Groups
132
+ data.each do |key, value|
133
+ if current_section.nil? # Sections
134
+ populate_categories_map(value, key, nil, nil)
135
+ elsif current_stage.nil? # Stages
136
+ populate_categories_map(value, current_section, key, nil)
137
+ elsif current_group.nil? # Groups
138
+ populate_categories_map(value, current_section, current_stage, key)
139
+ else # Categories
140
+ populate_categories_map(value, current_section, current_stage, current_group)
141
+ end
142
+ end
143
+ when Array # Categories array
144
+ data.each do |category|
145
+ next unless category.is_a?(String)
146
+
147
+ @categories_map[category] = {
148
+ section: current_section,
149
+ stage: current_stage,
150
+ group: current_group
151
+ }
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'table'
4
+
5
+ module GitlabQuality
6
+ module TestTooling
7
+ module CodeCoverage
8
+ module ClickHouse
9
+ class CategoryOwnersTable < GitlabQuality::TestTooling::CodeCoverage::ClickHouse::Table
10
+ TABLE_NAME = "category_owners"
11
+
12
+ MissingMappingError = Class.new(StandardError)
13
+
14
+ # Creates the ClickHouse table, if it doesn't exist already
15
+ # @return [nil]
16
+ def create
17
+ logger.debug("#{LOG_PREFIX} Creating category_owners table if it doesn't exist ...")
18
+
19
+ client.query(<<~SQL)
20
+ CREATE TABLE IF NOT EXISTS #{table_name} (
21
+ timestamp DateTime64(6, 'UTC') DEFAULT now64(),
22
+ category String,
23
+ group String,
24
+ stage String,
25
+ section String,
26
+ INDEX idx_group group TYPE set(360) GRANULARITY 1,
27
+ INDEX idx_stage stage TYPE set(360) GRANULARITY 1,
28
+ INDEX idx_section section TYPE set(360) GRANULARITY 1
29
+ ) ENGINE = MergeTree()
30
+ ORDER BY (category, timestamp)
31
+ SETTINGS index_granularity = 8192;
32
+ SQL
33
+
34
+ logger.info("#{LOG_PREFIX} Category owners table created/verified successfully")
35
+ end
36
+
37
+ def truncate
38
+ logger.debug("#{LOG_PREFIX} Truncating table #{full_table_name} ...")
39
+
40
+ client.query("TRUNCATE TABLE #{full_table_name}")
41
+
42
+ logger.info("#{LOG_PREFIX} Successfully truncated table #{full_table_name}")
43
+ end
44
+
45
+ # Owners of particular category as group, stage and section
46
+ #
47
+ # @param category_name [String]
48
+ # @return [Hash]
49
+ def owners(category_name)
50
+ records.fetch(category_name)
51
+ rescue KeyError
52
+ raise(MissingMappingError, "Category '#{category_name}' not found in table '#{table_name}'")
53
+ end
54
+
55
+ private
56
+
57
+ def records
58
+ @records ||= client
59
+ .query("SELECT category, group, stage, section FROM #{table_name}")
60
+ .each_with_object({}) { |record, hsh| hsh[record["category"]] = record.slice("group", "stage", "section") }
61
+ end
62
+
63
+ # @return [Boolean] True if the record is valid, false otherwise
64
+ def valid_record?(record)
65
+ required_fields = %i[category group stage section]
66
+
67
+ required_fields.each do |field|
68
+ if record[field].nil?
69
+ logger.warn("#{LOG_PREFIX} Skipping record with nil #{field}: #{record}")
70
+ return false
71
+ end
72
+ end
73
+
74
+ true
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require_relative 'table'
5
+
6
+ module GitlabQuality
7
+ module TestTooling
8
+ module CodeCoverage
9
+ module ClickHouse
10
+ class CoverageMetricsTable < GitlabQuality::TestTooling::CodeCoverage::ClickHouse::Table
11
+ TABLE_NAME = "coverage_metrics"
12
+
13
+ # Creates the ClickHouse table, if it doesn't exist already
14
+ # @return [nil]
15
+ def create
16
+ logger.debug("#{LOG_PREFIX} Creating coverage_metrics table if it doesn't exist ...")
17
+
18
+ client.query(<<~SQL)
19
+ CREATE TABLE IF NOT EXISTS #{table_name} (
20
+ timestamp DateTime64(6, 'UTC'),
21
+ file String,
22
+ line_coverage Float64,
23
+ branch_coverage Nullable(Float64),
24
+ function_coverage Nullable(Float64),
25
+ source_file_type String,
26
+ category Nullable(String),
27
+ ci_project_id Nullable(UInt32),
28
+ ci_project_path Nullable(String),
29
+ ci_job_name Nullable(String),
30
+ ci_job_id Nullable(UInt64),
31
+ ci_pipeline_id Nullable(UInt64),
32
+ ci_merge_request_iid Nullable(UInt32),
33
+ ci_branch Nullable(String),
34
+ ci_target_branch Nullable(String)
35
+ ) ENGINE = MergeTree()
36
+ PARTITION BY toYYYYMM(timestamp)
37
+ ORDER BY (ci_project_path, timestamp, file, ci_pipeline_id)
38
+ SETTINGS index_granularity = 8192, allow_nullable_key = 1;
39
+ SQL
40
+
41
+ logger.info("#{LOG_PREFIX} Coverage metrics table created/verified successfully")
42
+ end
43
+
44
+ private
45
+
46
+ # @return [Boolean] True if the record is valid, false otherwise
47
+ def valid_record?(record)
48
+ valid_file?(record) &&
49
+ valid_line_coverage?(record) &&
50
+ valid_branch_coverage?(record) &&
51
+ valid_function_coverage?(record)
52
+ end
53
+
54
+ # @return [Boolean] True if the file field is present
55
+ def valid_file?(record)
56
+ return true unless record[:file].nil?
57
+
58
+ logger.warn("#{LOG_PREFIX} Skipping record with nil file: #{record}")
59
+ false
60
+ end
61
+
62
+ # @return [Boolean] True if line_coverage is present and not NaN
63
+ def valid_line_coverage?(record)
64
+ if record[:line_coverage].nil?
65
+ logger.warn("#{LOG_PREFIX} Skipping record with nil line_coverage: #{record}")
66
+ return false
67
+ end
68
+
69
+ return true unless record[:line_coverage].nan?
70
+
71
+ logger.warn("#{LOG_PREFIX} Skipping record with NaN line_coverage: #{record}")
72
+ false
73
+ end
74
+
75
+ # @return [Boolean] True if branch_coverage is not NaN (or is nil)
76
+ def valid_branch_coverage?(record)
77
+ return true unless record[:branch_coverage]&.nan?
78
+
79
+ logger.warn("#{LOG_PREFIX} Skipping record with NaN branch_coverage: #{record}")
80
+ false
81
+ end
82
+
83
+ # @return [Boolean] True if function_coverage is not NaN (or is nil)
84
+ def valid_function_coverage?(record)
85
+ return true unless record[:function_coverage]&.nan?
86
+
87
+ logger.warn("#{LOG_PREFIX} Skipping record with NaN function_coverage: #{record}")
88
+ false
89
+ end
90
+
91
+ # @return [Hash] Transformed coverage data including timestamp and CI metadata
92
+ def sanitized_data_record(record)
93
+ {
94
+ timestamp: time,
95
+ file: record[:file],
96
+ line_coverage: record[:line_coverage],
97
+ branch_coverage: record[:branch_coverage],
98
+ function_coverage: record[:function_coverage],
99
+ source_file_type: record[:source_file_type],
100
+ category: record[:category],
101
+ **ci_metadata
102
+ }
103
+ end
104
+
105
+ # @return [Time] Common timestamp for all coverage records
106
+ def time
107
+ @time ||= begin
108
+ ci_created_at = ENV.fetch('CI_PIPELINE_CREATED_AT', nil)
109
+ ci_created_at ? Time.strptime(ci_created_at, '%Y-%m-%dT%H:%M:%S%z') : Time.now.utc
110
+ end
111
+ end
112
+
113
+ # @return [Hash] CI-related metadata
114
+ def ci_metadata
115
+ {
116
+ ci_project_id: env_to_int('CI_PROJECT_ID'),
117
+ ci_project_path: ENV.fetch('CI_PROJECT_PATH', nil),
118
+ ci_job_name: ENV.fetch('CI_JOB_NAME', nil)&.gsub(%r{ \d{1,2}/\d{1,2}}, ''),
119
+ ci_job_id: env_to_int('CI_JOB_ID'),
120
+ ci_pipeline_id: env_to_int('CI_PIPELINE_ID'),
121
+ ci_merge_request_iid: env_to_int('CI_MERGE_REQUEST_IID') || env_to_int('TOP_UPSTREAM_MERGE_REQUEST_IID'),
122
+ ci_branch: ENV.fetch('CI_COMMIT_REF_NAME', nil),
123
+ ci_target_branch: ENV.fetch('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', nil)
124
+ }
125
+ end
126
+
127
+ # @param name [String] Environment variable name
128
+ # @return [Integer, nil] Environment variable converted to integer or
129
+ # nil if not present or empty
130
+ def env_to_int(name)
131
+ value = ENV.fetch(name, nil)
132
+ return nil if value.nil? || value.empty?
133
+
134
+ value.to_i
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module CodeCoverage
6
+ module ClickHouse
7
+ class Table
8
+ LOG_PREFIX = "[CodeCoverage]"
9
+
10
+ def initialize(url:, database:, username: nil, password: nil, logger: nil)
11
+ @url = url
12
+ @database = database
13
+ @username = username
14
+ @password = password
15
+ @logger = logger || Runtime::Logger.logger
16
+ end
17
+
18
+ # @param data [Array<Hash>] Code coverage related data to be pushed to ClickHouse
19
+ # @return [nil]
20
+ def push(data) # rubocop:disable Metrics/AbcSize
21
+ return logger.warn("#{LOG_PREFIX} No data found, skipping ClickHouse export!") if data.empty?
22
+
23
+ logger.debug("#{LOG_PREFIX} Starting data export to ClickHouse")
24
+ sanitized_data = sanitize(data)
25
+
26
+ return logger.warn("#{LOG_PREFIX} No valid data found after sanitization, skipping ClickHouse export!") if sanitized_data.empty?
27
+
28
+ client.insert_json_data(table_name, sanitized_data)
29
+ logger.info("#{LOG_PREFIX} Successfully pushed #{sanitized_data.size} records to #{full_table_name}!")
30
+ rescue StandardError => e
31
+ logger.error("#{LOG_PREFIX} Error occurred while pushing data to #{full_table_name}: #{e.message}")
32
+ raise
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :url, :database, :username, :password, :logger
38
+
39
+ def sanitize(data)
40
+ data.filter_map { |record| sanitized_data_record(record) if valid_record?(record) }
41
+ end
42
+
43
+ def sanitized_data_record(record)
44
+ record
45
+ end
46
+
47
+ def full_table_name
48
+ "#{database}.#{table_name}"
49
+ end
50
+
51
+ def table_name
52
+ self.class::TABLE_NAME
53
+ rescue NameError
54
+ raise NotImplementedError, "#{self.class} must define the TABLE_NAME constant"
55
+ end
56
+
57
+ def valid_record?(_record)
58
+ raise NotImplementedError, "#{self.class}##{__method__} method must be implemented in a subclass"
59
+ end
60
+
61
+ # @return [GitlabQuality::TestTooling::ClickHouse::Client]
62
+ def client
63
+ @client ||= GitlabQuality::TestTooling::ClickHouse::Client.new(
64
+ url: url,
65
+ database: database,
66
+ username: username,
67
+ password: password,
68
+ logger: logger
69
+ )
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module CodeCoverage
6
+ class CoverageData
7
+ # @param [Hash<String, Hash>] code_coverage_by_source_file Source file
8
+ # mapped to test coverage data
9
+ # @param [Hash<String, Array<String>>] source_file_to_tests Source files
10
+ # mapped to all test files testing them
11
+ # @param [Hash<String, Array<String>>] tests_to_categories Test files
12
+ # mapped to all feature categories they belong to
13
+ # @param [Hash<String, Hash>] categories_to_teams Mapping of categories
14
+ # to teams (i.e., groups, stages, sections)
15
+ # @param [Hash<String, String>] source_file_types Mapping of source files
16
+ # to their types (frontend, backend, etc.)
17
+ def initialize(code_coverage_by_source_file, source_file_to_tests, tests_to_categories, categories_to_teams, source_file_types = {})
18
+ @code_coverage_by_source_file = code_coverage_by_source_file
19
+ @source_file_to_tests = source_file_to_tests
20
+ @tests_to_categories = tests_to_categories
21
+ @categories_to_teams = categories_to_teams
22
+ @source_file_types = source_file_types
23
+ end
24
+
25
+ # @return [Array<Hash<Symbol, String>>] Mapping of column name to row
26
+ # value
27
+ # @example Return value
28
+ # [
29
+ # {
30
+ # file: "app/channels/application_cable/channel.rb"
31
+ # line_coverage: 100.0
32
+ # branch_coverage: 95.0
33
+ # function_coverage: 100.0
34
+ # source_file_type: "backend"
35
+ # category: "team_planning"
36
+ # group: "project_management"
37
+ # stage: "plan"
38
+ # section: "dev"
39
+ # },
40
+ # ...
41
+ # ]
42
+ def as_db_table
43
+ all_files.flat_map do |file|
44
+ coverage_data = @code_coverage_by_source_file[file]
45
+ line_coverage = coverage_data&.dig(:percentage)
46
+ branch_coverage = coverage_data&.dig(:branch_percentage)
47
+ function_coverage = coverage_data&.dig(:function_percentage)
48
+
49
+ categories = categories_for(file)
50
+ base_data = {
51
+ file: file,
52
+ line_coverage: line_coverage,
53
+ branch_coverage: branch_coverage,
54
+ function_coverage: function_coverage,
55
+ source_file_type: @source_file_types[file] || 'other'
56
+ }
57
+
58
+ if categories.empty?
59
+ base_data.merge(no_owner_info)
60
+ else
61
+ categories.map do |category|
62
+ base_data.merge(owner_info(category))
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def no_owner_info
71
+ {
72
+ category: nil,
73
+ group: nil,
74
+ stage: nil,
75
+ section: nil
76
+ }
77
+ end
78
+
79
+ def owner_info(category)
80
+ owner_info = @categories_to_teams[category]
81
+
82
+ {
83
+ category: category,
84
+ group: owner_info&.dig(:group),
85
+ stage: owner_info&.dig(:stage),
86
+ section: owner_info&.dig(:section)
87
+ }
88
+ end
89
+
90
+ def categories_for(file)
91
+ @source_file_to_tests[file]&.flat_map { |test_file| @tests_to_categories[test_file] || [] }&.uniq || []
92
+ end
93
+
94
+ def all_files
95
+ @all_files ||= @code_coverage_by_source_file.keys
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end