gitlab_quality-test_tooling 3.21.0 → 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: 6fa78c58b83ed94504f42df7a78f5bdbddea78f9466134618b6c9b34fa99d679
4
- data.tar.gz: bb3b04f2dee89712fd561a944de2d397db62867351f562ac0b0e7cac721424d2
3
+ metadata.gz: cbd765eef13187839210bb0dde07988168415170236b8734d3faf396ce95b9a9
4
+ data.tar.gz: '0877e97fd029bdb06028dad82aeae1a15f18b73e3f8ad2ad9c0448819335ca71'
5
5
  SHA512:
6
- metadata.gz: d969c70fbf9efb0aac4020367999aca505d226125a63daeaa8dc6f5ad409f84d8f9bc6a411ada6c9aa5c91627ef8bec768a165bcf4258df0e7e65426d63776c3
7
- data.tar.gz: d3329d8d499320cd0fdad6a8a2c9ea669d7310b1076aa5c8130e3c4b79e6f12e2fc2a32ecaa0a11f057860f8c8f82db9551120f5f0b5ce28f9a1898597a74842
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.21.0)
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)
@@ -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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "3.21.0"
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.21.0
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-08-14 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