gitlab_quality-test_tooling 3.20.2 → 3.21.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d63cdc47b44dd111330eaffb4e37c889d8785eb395094eec7398c97870f889f
4
- data.tar.gz: bbe3389c97486d566bb8a948da3a88cf94633857d8eee5ad9fbb53e6ba38aef5
3
+ metadata.gz: cbd765eef13187839210bb0dde07988168415170236b8734d3faf396ce95b9a9
4
+ data.tar.gz: '0877e97fd029bdb06028dad82aeae1a15f18b73e3f8ad2ad9c0448819335ca71'
5
5
  SHA512:
6
- metadata.gz: 81d28ad1a352c06db086546c5eef55f9f82aa982904e4f3e03ab1c7e509d0a18fde12f6e77e7a09eda3c549dd15ff1d0090f532a2da60eda68d31eba77cee93f
7
- data.tar.gz: 8aeb35ec5c0fc54e86c9cb029f44c62feb9dbc1e5031f6d4aaba07a9a3e3791a739841da7a1080bf1c28c05c81baee57a42742dca5ab60d1420e8e74360b9b9c
6
+ metadata.gz: f242e07dbdefd766fd8f2bec91b779c93bf68f91a913314982b8e3053db99704fa0330cb31dadeb8829c570f43b7f2f04c52265fcdd75cc1beb60f1486096554
7
+ data.tar.gz: b6cd28e7ad33b233fb6f7c68db39ade7b72118913eac8e3b3281e018d59a9e9e5ca99b50a7730d28f189b5a2517c2c933e254d15f7a50cff6800476346aa4b37
data/AGENTS.md CHANGED
@@ -75,6 +75,13 @@ Reference: https://docs.gitlab.com/ee/development/changelog.html
75
75
  - **CodeCoverage** — Coverage analysis and responsibility patterns
76
76
  - **ClickHouse::Client** — HTTP query/insert client for ClickHouse, used by the code coverage subsystem
77
77
 
78
+ ## Code Comments
79
+
80
+ Comments carry the non-obvious why, not what the code already says. Keep them
81
+ short. A header block that restates the query or narrates each column gets
82
+ trimmed to the design decisions a reader can't infer from the code. This
83
+ applies to AI-generated comments too: trim before committing.
84
+
78
85
  ## Review Guidance
79
86
 
80
87
  - `RelateFailureIssue#trigger_duo_analysis` deliberately rescues all `StandardError` (best-effort): a Duo/API failure must never block failure-issue creation. Preserve the broad rescue — don't narrow or remove it.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (3.20.2)
4
+ gitlab_quality-test_tooling (3.21.1)
5
5
  activesupport (>= 7.0)
6
6
  amatch (~> 0.4.1)
7
7
  fog-google (~> 1.24, >= 1.24.1)
data/exe/test-coverage CHANGED
@@ -73,7 +73,9 @@ options = OptionParser.new do |opts|
73
73
  'Inserts rows into code_coverage.test_coverage_per_file and runs the daily ' \
74
74
  'test_health_risk aggregation unless --skip-aggregation. Needs only the ' \
75
75
  '--clickhouse-* options; --test-reports and --jest-quarantine-file are ' \
76
- 'optional enrichment. (e.g., "tmp/per-test-coverage-*.ndjson")') do |pattern|
76
+ 'optional enrichment. --test-reports also requires --clickhouse-shared-database, ' \
77
+ 'since category ownership is read from the shared database. ' \
78
+ '(e.g., "tmp/per-test-coverage-*.ndjson")') do |pattern|
77
79
  params[:per_test_coverage] = pattern
78
80
  end
79
81
 
@@ -135,7 +137,16 @@ mode =
135
137
  clickhouse_required = [:clickhouse_url, :clickhouse_database, :clickhouse_username]
136
138
  full_export_required =
137
139
  [:test_reports, :coverage_report, :test_map, :responsibility_patterns, :clickhouse_shared_database] + clickhouse_required
138
- required_params = mode == :full_export ? full_export_required : clickhouse_required
140
+ # Per-test mode reads category_owners from the shared database only when it enriches
141
+ # rows (--test-reports present), so require the shared database exactly then.
142
+ per_test_enriching = mode == :per_test && params[:test_reports]
143
+ required_params =
144
+ case mode
145
+ when :full_export
146
+ full_export_required
147
+ else
148
+ per_test_enriching ? clickhouse_required + [:clickhouse_shared_database] : clickhouse_required
149
+ end
139
150
 
140
151
  unless params.any? && (required_params - params.keys).none?
141
152
  puts "Missing argument(s). Required arguments are: #{required_params}\nPassed arguments are: #{params}\n"
@@ -149,7 +160,13 @@ if clickhouse_password.to_s.strip.empty?
149
160
  exit 1
150
161
  end
151
162
 
152
- non_empty_params = mode == :full_export ? clickhouse_required + [:clickhouse_shared_database] : clickhouse_required
163
+ non_empty_params =
164
+ if mode == :full_export || per_test_enriching
165
+ clickhouse_required + [:clickhouse_shared_database]
166
+ else
167
+ clickhouse_required
168
+ end
169
+
153
170
  non_empty_params.each do |param|
154
171
  next unless params[param].to_s.strip.empty?
155
172
 
@@ -189,6 +206,7 @@ when :per_test
189
206
  GitlabQuality::TestTooling::CodeCoverage::PerTestCoverageExporter.new(
190
207
  coverage_glob: params[:per_test_coverage],
191
208
  clickhouse: clickhouse_data,
209
+ clickhouse_shared_database: params[:clickhouse_shared_database],
192
210
  test_reports: params[:test_reports],
193
211
  jest_quarantine_file: params[:jest_quarantine_file],
194
212
  skip_aggregation: params.fetch(:skip_aggregation, false)
@@ -1,52 +1,28 @@
1
- -- Daily aggregation of per-test coverage data into per-group risk summary.
1
+ -- Daily aggregation of per-test coverage into per-group risk rows: one
2
+ -- (snapshot_date, group, stage, section) row inserted into
3
+ -- code_coverage.test_health_risk_per_group.
2
4
  --
3
- -- Reads the bitmap line sets from `code_coverage.test_coverage_per_file`,
4
- -- joins per-test quarantine and flaky status from `test_metrics`, then
5
- -- computes for each source file:
6
- -- quarantined_union = union of line sets covered by *quarantined* tests
7
- -- flaky_union = union of line sets covered by *flaky* tests
8
- -- quarantined_or_flaky_union = union of line sets covered by *quarantined OR flaky* tests
9
- -- healthy_union = union of line sets covered by tests that are neither
5
+ -- "At risk" lines are covered only by quarantined/flaky tests. The healthy
6
+ -- baseline excludes flaky tests by design: a flaky test isn't a reliable
7
+ -- safety net, so its lines shouldn't offset quarantine-only risk.
10
8
  --
11
- -- The "at risk" line counts are:
12
- -- at_risk_lines_quarantine = lines uniquely covered by quarantined tests
13
- -- (not covered by any healthy test)
14
- -- at_risk_lines_flaky = lines uniquely covered by flaky tests
15
- -- at_risk_lines_combined = lines uniquely covered by quarantined OR flaky
9
+ -- Parameters use `{name}` braces, substituted as literal text by gsub in
10
+ -- TestHealthRiskAggregator#build_sql after validation. A typoed placeholder
11
+ -- silently survives gsub, so match the names exactly.
12
+ -- {snapshot_date} date stamp for this run
13
+ -- {coverage_start} day of the most recent full per-test capture
14
+ -- {risk_window} quarantine/flaky lookback, default '30 DAY'
16
15
  --
17
- -- Healthy union excludes flaky tests too, by design: a flaky test isn't a
18
- -- reliable coverage safety net, so its lines shouldn't count toward the
19
- -- baseline against which quarantine-only risk is measured.
16
+ -- The INSERT is unconditional. Idempotency comes from the target table's
17
+ -- ReplacingMergeTree(version) engine keyed by the row tuple: re-runs and
18
+ -- overlapping cross-run inserts replace rather than duplicate, latest wins.
20
19
  --
21
- -- Result: one row per (snapshot_date, group, stage, section) tuple inserted
22
- -- into `code_coverage.test_health_risk_per_group`.
23
- --
24
- -- Parameter substitution: this template uses `{name}` braces (not %{name} or
25
- -- :name). They're replaced by literal text via `gsub` in
26
- -- TestHealthRiskAggregator#build_sql, after each parameter passes a regex
27
- -- check. A typoed placeholder name will silently fall through gsub as a
28
- -- literal — match the names exactly.
29
- --
30
- -- {snapshot_date} : the date stamp for this run, e.g. '2026-05-07'
31
- -- {coverage_window} : interval to look back for fresh per-test rows,
32
- -- default '2 DAY' (see Resilience note below)
33
- -- {risk_window} : interval to consider quarantine/flaky status from
34
- -- the test_metrics summaries, default '30 DAY'
35
-
36
- -- Idempotency: this INSERT is unconditional. The target table must use
37
- -- SharedReplacingMergeTree(version) (or ReplacingMergeTree on non-Cloud
38
- -- ClickHouse; Cloud silently rewrites to Shared...) keyed by
39
- -- (snapshot_date, group, stage, section), with
40
- -- `version UInt64 MATERIALIZED toUnixTimestamp64Milli(now64(3))`, so re-running
41
- -- for the same snapshot_date produces a higher version that replaces the
42
- -- previous row on merge. Without that engine, re-runs would duplicate rows.
43
- --
44
- -- Resilience: `{coverage_window}` defaults to 2 DAY (see
45
- -- TestHealthRiskAggregator::DEFAULT_COVERAGE_WINDOW). One missed nightly
46
- -- run is recovered on the next night because the aggregation still sees
47
- -- the previous day's per-test rows. Cross-run race: if two jobs land
48
- -- inserts overlapping with the aggregation, the later aggregation wins
49
- -- because of the version-based replacement.
20
+ -- Per-test capture is sparse between full sweeps (roughly weekly), so a
21
+ -- fixed trailing window would flap. {coverage_start} anchors the universe at
22
+ -- the last day that cleared the full-capture floor (#find_coverage_start);
23
+ -- when no day qualifies within the lookback, nothing is written and the
24
+ -- previous snapshot carries forward. FINAL on the source keeps the latest
25
+ -- version per (test_file, source_file), so the wide span never double-counts.
50
26
  INSERT INTO code_coverage.test_health_risk_per_group
51
27
  WITH
52
28
  quarantine_status AS (
@@ -109,7 +85,7 @@ WITH
109
85
  LEFT JOIN quarantine_status qs ON qs.test_file = tc.test_file
110
86
  LEFT JOIN flaky_status fs ON fs.test_file = tc.test_file
111
87
  LEFT JOIN test_ownership own ON own.file_path = tc.test_file
112
- WHERE tc.timestamp >= now() - INTERVAL {coverage_window}
88
+ WHERE tc.timestamp >= toDate('{coverage_start}')
113
89
  ),
114
90
  per_file AS (
115
91
  SELECT
@@ -19,14 +19,25 @@ module GitlabQuality
19
19
 
20
20
  SQL_FILE = File.expand_path('test_health_risk_aggregation.sql', __dir__)
21
21
 
22
- # 2 DAY rather than 1 DAY makes the aggregation self-healing across
23
- # a single missed nightly run: if last night's export failed, this
24
- # night's run still sees yesterday's per-test rows and produces a
25
- # current snapshot. ReplacingMergeTree FINAL on the source table
26
- # ensures we read only the latest version per (test_file, source_file).
27
- # ClickHouse accepts both '2 DAY' and '2 DAYS'; we use the singular
28
- # form for consistency with `30 DAY` below.
29
- DEFAULT_COVERAGE_WINDOW = '2 DAY'
22
+ # Per-test capture is sparse between full captures (a full sweep of
23
+ # the suite lands roughly weekly; weekdays contribute only a small
24
+ # delta), so the coverage universe anchors at the most recent day
25
+ # whose capture volume clears this floor instead of a fixed trailing
26
+ # interval. Counted in distinct test files, because the weekend
27
+ # sweep enumerates the whole suite by test file: full sweeps land
28
+ # 22-27k test files, a single sweep bucket ~13k, and delta days at
29
+ # most ~16k even at their historical worst. Source files would not
30
+ # separate the cases: a few thousand tests already touch tens of
31
+ # thousands of source files through app boot, so a healthy delta
32
+ # day could pass a source-file floor and anchor the window at a
33
+ # partial capture.
34
+ DEFAULT_FULL_CAPTURE_FLOOR = 18_000
35
+ # Far enough back to bridge two missed weekly full captures before
36
+ # giving up, and well inside the source table's 90-day TTL. When no
37
+ # day within the lookback clears the floor, the run writes nothing
38
+ # so the previous snapshot carries forward rather than publishing a
39
+ # denominator built from sparse data.
40
+ DEFAULT_ANCHOR_LOOKBACK = '21 DAY'
30
41
  DEFAULT_RISK_WINDOW = '30 DAY'
31
42
 
32
43
  # `snapshot_date` is YYYY-MM-DD; intervals are `<integer> <unit>`
@@ -36,35 +47,38 @@ module GitlabQuality
36
47
 
37
48
  def initialize(
38
49
  url:, database:, username: nil, password: nil, logger: nil,
39
- coverage_window: DEFAULT_COVERAGE_WINDOW, risk_window: DEFAULT_RISK_WINDOW)
50
+ full_capture_floor: DEFAULT_FULL_CAPTURE_FLOOR, anchor_lookback: DEFAULT_ANCHOR_LOOKBACK,
51
+ risk_window: DEFAULT_RISK_WINDOW)
40
52
  @url = url
41
53
  @database = database
42
54
  @username = username
43
55
  @password = password
44
56
  @logger = logger || ::Logger.new($stdout, level: 1)
45
- @coverage_window = coverage_window
57
+ @full_capture_floor = full_capture_floor
58
+ @anchor_lookback = anchor_lookback
46
59
  @risk_window = risk_window
47
60
  end
48
61
 
49
62
  # @param snapshot_date [Date, String] date stamp for this run; defaults to today.
50
63
  # @return [void]
51
64
  def run(snapshot_date: Date.today) # rubocop:disable Metrics/AbcSize
52
- sql = build_sql(snapshot_date: snapshot_date)
53
- logger.info(
54
- "#{LOG_PREFIX} Running test_health_risk aggregation snapshot_date=#{snapshot_date} " \
55
- "coverage_window=#{coverage_window} risk_window=#{risk_window}"
56
- )
57
- client.query(sql, format: "TabSeparated")
58
- inserted = fetch_row_count(snapshot_date)
59
- if inserted.is_a?(Integer) && inserted.zero?
65
+ date = validate_date(snapshot_date)
66
+ coverage_start = find_coverage_start
67
+ if coverage_start.nil?
60
68
  logger.warn(
61
- "#{LOG_PREFIX} Aggregation wrote 0 rows for snapshot_date=#{snapshot_date}. " \
62
- "This is valid if no per-test data is in the coverage_window, but worth checking " \
63
- "test_coverage_per_file directly if a recent export ran."
69
+ "#{LOG_PREFIX} No day within #{anchor_lookback} captured >= #{full_capture_floor} " \
70
+ "distinct test files; skipping snapshot for #{date} so the previous row carries forward."
64
71
  )
65
- else
66
- logger.info("#{LOG_PREFIX} Aggregation wrote #{inserted} rows for snapshot_date=#{snapshot_date}")
72
+ return
67
73
  end
74
+
75
+ sql = build_sql(snapshot_date: date, coverage_start: coverage_start)
76
+ logger.info(
77
+ "#{LOG_PREFIX} Running test_health_risk aggregation snapshot_date=#{date} " \
78
+ "coverage_start=#{coverage_start} risk_window=#{risk_window}"
79
+ )
80
+ client.query(sql, format: "TabSeparated")
81
+ report_row_count(date)
68
82
  rescue StandardError => e
69
83
  logger.error("#{LOG_PREFIX} Aggregation failed for #{snapshot_date}: #{e.message}")
70
84
  raise
@@ -72,24 +86,62 @@ module GitlabQuality
72
86
 
73
87
  private
74
88
 
75
- attr_reader :url, :database, :username, :password, :logger, :coverage_window, :risk_window
89
+ attr_reader :url, :database, :username, :password, :logger,
90
+ :full_capture_floor, :anchor_lookback, :risk_window
91
+
92
+ # Most recent day whose capture volume looks like a full sweep of the
93
+ # suite. maxOrNull (rather than max) distinguishes "no qualifying day"
94
+ # from a real date when the subquery comes back empty.
95
+ def find_coverage_start
96
+ sql = <<~SQL
97
+ SELECT maxOrNull(day) AS coverage_start
98
+ FROM (
99
+ SELECT toDate(timestamp) AS day
100
+ FROM code_coverage.test_coverage_per_file
101
+ WHERE timestamp >= now() - INTERVAL #{validate_interval(anchor_lookback)}
102
+ GROUP BY day
103
+ HAVING uniqExact(test_file) >= #{validate_positive_integer(full_capture_floor)}
104
+ )
105
+ SQL
106
+ anchor = client.query(sql, format: "JSONCompact")&.dig('data', 0, 0)
107
+ anchor.nil? ? nil : validate_date(anchor, 'coverage_start')
108
+ end
109
+
110
+ def report_row_count(date)
111
+ inserted = fetch_row_count(date)
112
+ if inserted.is_a?(Integer) && inserted.zero?
113
+ logger.warn(
114
+ "#{LOG_PREFIX} Aggregation wrote 0 rows for snapshot_date=#{date}. " \
115
+ "This is valid if no per-test data landed since the coverage anchor, but worth " \
116
+ "checking test_coverage_per_file directly if a recent export ran."
117
+ )
118
+ else
119
+ logger.info("#{LOG_PREFIX} Aggregation wrote #{inserted} rows for snapshot_date=#{date}")
120
+ end
121
+ end
76
122
 
77
- def build_sql(snapshot_date:)
123
+ def build_sql(snapshot_date:, coverage_start:)
78
124
  template = File.read(SQL_FILE)
79
125
  template
80
126
  .gsub('{snapshot_date}', validate_date(snapshot_date))
81
- .gsub('{coverage_window}', validate_interval(coverage_window))
127
+ .gsub('{coverage_start}', coverage_start)
82
128
  .gsub('{risk_window}', validate_interval(risk_window))
83
129
  end
84
130
 
85
131
  # DateTime/Time `to_s` includes the time portion and is rejected.
86
- def validate_date(value)
132
+ def validate_date(value, name = 'snapshot_date')
87
133
  str = value.to_s
88
- raise ArgumentError, "Invalid snapshot_date: #{value.inspect}" unless DATE_PATTERN.match?(str)
134
+ raise ArgumentError, "Invalid #{name}: #{value.inspect}" unless DATE_PATTERN.match?(str)
89
135
 
90
136
  str
91
137
  end
92
138
 
139
+ def validate_positive_integer(value)
140
+ raise ArgumentError, "Invalid full_capture_floor: #{value.inspect}" unless value.is_a?(Integer) && value.positive?
141
+
142
+ value.to_s
143
+ end
144
+
93
145
  def validate_interval(value)
94
146
  raise ArgumentError, "Invalid interval expression: #{value.inspect}" unless INTERVAL_PATTERN.match?(value.to_s)
95
147
 
@@ -23,12 +23,15 @@ module GitlabQuality
23
23
  # aggregation once at the end.
24
24
  class PerTestCoverageExporter
25
25
  # @param clickhouse [Hash] connection params (:url, :database, :username, :password)
26
+ # @param clickhouse_shared_database [String, nil] the shared database holding the
27
+ # canonical `category_owners`. Required only when enriching (test_reports present).
26
28
  def initialize(
27
- coverage_glob:, clickhouse:,
29
+ coverage_glob:, clickhouse:, clickhouse_shared_database: nil,
28
30
  test_reports: nil, jest_quarantine_file: nil,
29
31
  captured_sha: ENV.fetch('CI_COMMIT_SHA', ''), skip_aggregation: false, logger: nil)
30
32
  @coverage_glob = coverage_glob
31
33
  @clickhouse = clickhouse
34
+ @clickhouse_shared_database = clickhouse_shared_database
32
35
  @test_reports = test_reports
33
36
  @jest_quarantine_file = jest_quarantine_file
34
37
  @captured_sha = captured_sha.to_s
@@ -65,8 +68,8 @@ module GitlabQuality
65
68
 
66
69
  private
67
70
 
68
- attr_reader :coverage_glob, :clickhouse, :test_reports, :jest_quarantine_file,
69
- :captured_sha, :skip_aggregation, :logger
71
+ attr_reader :coverage_glob, :clickhouse, :clickhouse_shared_database, :test_reports,
72
+ :jest_quarantine_file, :captured_sha, :skip_aggregation, :logger
70
73
 
71
74
  def insert(coverage_files)
72
75
  tests_to_categories, feature_categories_to_teams = resolve_categories
@@ -144,14 +147,21 @@ module GitlabQuality
144
147
  [tests_to_categories_from(report_files), feature_categories_to_teams]
145
148
  end
146
149
 
147
- # Category to group/stage/section, read from `code_coverage.category_owners`
148
- # (populated by sync-category-owners). This is the source the full
149
- # coverage export already uses, and it retains categories the live
150
- # stages.yml fetch has since dropped (global_search and the Duo/AI groups),
151
- # so more rows resolve to a real group instead of blank. `owner_records`
152
- # gives string-keyed values; PerTestCoverageData reads symbol keys.
150
+ # Category to group/stage/section, read from `category_owners` in the shared
151
+ # database, the same source the full coverage export uses. The shared copy is
152
+ # refreshed by sync-category-owners; the legacy `code_coverage.category_owners`
153
+ # stopped updating in December 2025, so reading it would drop every category
154
+ # added since. `owner_records` gives string-keyed values; PerTestCoverageData
155
+ # reads symbol keys.
153
156
  def feature_categories_to_teams
154
- ClickHouse::CategoryOwnersTable.new(**clickhouse_credentials).owner_records.transform_values do |owner|
157
+ if clickhouse_shared_database.to_s.strip.empty?
158
+ raise ArgumentError,
159
+ 'clickhouse_shared_database is required to enrich per-test rows with category owners'
160
+ end
161
+
162
+ shared_credentials = clickhouse_credentials.merge(database: clickhouse_shared_database)
163
+
164
+ ClickHouse::CategoryOwnersTable.new(**shared_credentials).owner_records.transform_values do |owner|
155
165
  { group: owner["group"], stage: owner["stage"], section: owner["section"] }
156
166
  end
157
167
  end
@@ -151,7 +151,6 @@ module GitlabQuality
151
151
  line_number: extract_line_number(test),
152
152
  exception: extract_exception_data(test),
153
153
  ci_job_url: test.ci_job_url,
154
- testcase: extract_test_id_or_name(test),
155
154
  product_group: extract_product_group(test),
156
155
  level: extract_level(test)
157
156
  }
@@ -184,14 +183,6 @@ module GitlabQuality
184
183
  }
185
184
  end
186
185
 
187
- def extract_test_id_or_name(test)
188
- return test.example_id if test.respond_to?(:example_id)
189
- return test.id if test.respond_to?(:id)
190
- return test.name if test.respond_to?(:name)
191
-
192
- "#{test.relative_file}:#{test.respond_to?(:line_number) ? test.line_number : 'unknown'}"
193
- end
194
-
195
186
  def new_issue_labels(test)
196
187
  up_to_date_labels(test: test, new_labels: NEW_ISSUE_LABELS + group_and_category_labels_for_test(test))
197
188
  end
@@ -63,7 +63,6 @@ module GitlabQuality
63
63
  | Test level | `#{test.level}` |
64
64
  | Hash | `#{test_hash(test)}` |
65
65
  | Max expected duration | < #{test.max_duration_for_test} seconds |
66
- #{"| Test case | #{test.testcase} |" if test.testcase}
67
66
  <!-- Don't modify this section as it's auto-generated -->
68
67
  DESCRIPTION
69
68
  end
@@ -49,16 +49,11 @@ module GitlabQuality
49
49
 
50
50
  report_test(test)
51
51
  end
52
-
53
- test_results.write
54
52
  end
55
53
  end
56
54
 
57
55
  def report_test(test)
58
56
  testcase = testcase_project_reporter.find_or_create_testcase(test)
59
- # The API returns the test case with an issue URL since it is technically a type of issue.
60
- # This updates the URL to a valid test case link.
61
- test.testcase = testcase.web_url.sub('/issues/', '/quality/test_cases/')
62
57
 
63
58
  result_issue, is_new = results_issue_project_reporter.get_related_issue(testcase, test)
64
59
 
@@ -18,18 +18,17 @@ module GitlabQuality
18
18
  find_testcase(test) || create_issue(test)
19
19
  end
20
20
 
21
- def add_result_issue_link_to_testcase(testcase, result_issue, test)
21
+ def add_result_issue_link_to_testcase(testcase, result_issue, _test)
22
22
  results_section = testcase.description.include?(TEST_CASE_RESULTS_SECTION_TEMPLATE) ? '' : TEST_CASE_RESULTS_SECTION_TEMPLATE
23
23
 
24
24
  gitlab.edit_issue(iid: testcase.iid,
25
25
  options: { description: (testcase.description + results_section + "\n\n#{result_issue.web_url}") })
26
- # We are using test.testcase for the url here instead of testcase.web_url since it has the updated test case path
27
- puts "Added results issue #{result_issue.web_url} link to test case #{test.testcase}"
26
+ puts "Added results issue #{result_issue.web_url} link to test case #{testcase.web_url}"
28
27
  end
29
28
 
30
29
  def update_testcase(testcase, test)
31
- puts "Labels updated for test case #{test.testcase}." if update_labels(testcase, test)
32
- puts "Quarantine section updated for test case #{test.testcase}." if update_quarantine_section(testcase, test)
30
+ puts "Labels updated for test case #{testcase.web_url}." if update_labels(testcase, test)
31
+ puts "Quarantine section updated for test case #{testcase.web_url}." if update_quarantine_section(testcase, test)
33
32
  end
34
33
 
35
34
  private
@@ -37,7 +37,7 @@ module GitlabQuality
37
37
  description_start_index + description_length
38
38
  end
39
39
 
40
- # List specs in markdown with details such as link to code, testcase, metrics and failure issue
40
+ # List specs in markdown with details such as link to code, metrics and failure issue
41
41
  #
42
42
  # @param [Hash<String,Hash>] commits The commits hash to use for spec details
43
43
  # @return String
@@ -45,7 +45,7 @@ module GitlabQuality
45
45
  commits.each_with_index.map do |(changed_line_number, spec), index|
46
46
  <<~MARKDOWN
47
47
  #{index + 1}. [`#{spec['name']}`](https://gitlab.com/#{context.project}/-/blob/#{context.ref}/#{spec['file_path']}#L#{changed_line_number.to_i + 1})
48
- | [Testcase](#{spec['testcase']}) | [Spec metrics](#{context.single_spec_metrics_link(spec['file_path'])})
48
+ | [Spec metrics](#{context.single_spec_metrics_link(spec['file_path'])})
49
49
  #{failure_issue_text(spec)}
50
50
  MARKDOWN
51
51
  end.join("\n")
@@ -4,8 +4,6 @@ module GitlabQuality
4
4
  module TestTooling
5
5
  module TestResult
6
6
  class JUnitTestResult < BaseTestResult
7
- attr_accessor :testcase # Ignore it for now
8
-
9
7
  def name
10
8
  report['name']
11
9
  end
@@ -60,14 +60,6 @@ module GitlabQuality
60
60
  report.fetch('ci_job_url', '')
61
61
  end
62
62
 
63
- def testcase
64
- report.fetch('testcase', '')
65
- end
66
-
67
- def testcase=(new_testcase)
68
- report['testcase'] = new_testcase
69
- end
70
-
71
63
  def quarantine_type
72
64
  quarantine['type'] if quarantine?
73
65
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "3.20.2"
5
+ VERSION = "3.21.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab_quality-test_tooling
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.20.2
4
+ version: 3.21.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab Quality
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-16 00:00:00.000000000 Z
11
+ date: 2026-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control