Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'qa/qa/tools/reliable_report.rb')
-rw-r--r--qa/qa/tools/reliable_report.rb299
1 files changed, 210 insertions, 89 deletions
diff --git a/qa/qa/tools/reliable_report.rb b/qa/qa/tools/reliable_report.rb
index 9d2079171c1..40a452be36e 100644
--- a/qa/qa/tools/reliable_report.rb
+++ b/qa/qa/tools/reliable_report.rb
@@ -1,111 +1,223 @@
# frozen_string_literal: true
+require_relative "../../qa"
+
require "influxdb-client"
require "terminal-table"
require "slack-notifier"
+require "colorize"
module QA
module Tools
class ReliableReport
- def initialize(run_type, range = 30)
- @results = 10
- @slack_channel = "#quality-reports"
+ include Support::API
+
+ # Project for report creation: https://gitlab.com/gitlab-org/gitlab
+ PROJECT_ID = 278964
+
+ def initialize(range)
@range = range
- @run_type = run_type
- @stable_title = "Top #{results} stable specs for past #{@range} days in '#{run_type}' runs"
- @unstable_title = "Top #{results} unstable reliable specs for past #{@range} days in '#{run_type}' runs"
+ @influxdb_bucket = "e2e-test-stats"
+ @slack_channel = "#quality-reports"
+ @influxdb_url = ENV["QA_INFLUXDB_URL"] || raise("Missing QA_INFLUXDB_URL env variable")
+ @influxdb_token = ENV["QA_INFLUXDB_TOKEN"] || raise("Missing QA_INFLUXDB_TOKEN env variable")
end
- # Print top stable specs
+ # Run reliable reporter
#
+ # @param [Integer] range amount of days for results range
+ # @param [String] report_in_issue_and_slack
# @return [void]
- def show_top_stable
- puts terminal_table(
- rows: top_stable.map { |k, v| [name_column(k, v[:file]), *table_params(v.values)] },
- title: stable_title
- )
+ def self.run(range: 14, report_in_issue_and_slack: "false")
+ reporter = new(range)
+
+ reporter.print_report
+ reporter.report_in_issue_and_slack if report_in_issue_and_slack == "true"
+ rescue StandardError => e
+ puts "Report creation failed! Error: '#{e}'".colorize(:red)
+ reporter.notify_failure(e)
+ exit(1)
end
- # Post top stable spec report to slack
- # Slice table in to multiple messages due to max char limitation
+ # Print top stable specs
#
# @return [void]
- def notify_top_stable
- tables = top_stable.each_slice(5).map do |slice|
- terminal_table(
- rows: slice.map { |spec| [name_column(spec[0], spec[1][:file]), *table_params(spec[1].values)] }
- )
- end
+ def print_report
+ puts "#{stable_summary_table}\n\n"
+ stable_results_tables.each { |stage, table| puts "#{table}\n\n" }
+ return puts("No unstable reliable tests present!".colorize(:yellow)) if unstable_reliable_test_runs.empty?
- puts "\nSending top stable spec report to #{slack_channel} slack channel"
- slack_args = { icon_emoji: ":mtg_green:", username: "Stable Spec Report" }
- notifier.post(text: "*#{stable_title}*", **slack_args)
- tables.each { |table| notifier.post(text: "```#{table}```", **slack_args) }
+ puts "#{unstable_summary_table}\n\n"
+ unstable_reliable_results_tables.each { |stage, table| puts "#{table}\n\n" }
end
- # Print top unstable specs
+ # Create report issue
#
# @return [void]
- def show_top_unstable
- return puts("No unstable tests present!") if top_unstable_reliable.empty?
+ def report_in_issue_and_slack
+ puts "Creating report".colorize(:green)
+ response = post(
+ "#{gitlab_api_url}/projects/#{PROJECT_ID}/issues",
+ { title: "Reliable spec report", description: report_issue_body, labels: "Quality,test" },
+ headers: { "PRIVATE-TOKEN" => gitlab_access_token }
+ )
+ web_url = parse_body(response)[:web_url]
+ puts "Created report issue: #{web_url}"
- puts terminal_table(
- rows: top_unstable_reliable.map { |k, v| [name_column(k, v[:file]), *table_params(v.values)] },
- title: unstable_title
+ puts "Sending slack notification".colorize(:green)
+ notifier.post(
+ icon_emoji: ":tanuki-protect:",
+ text: <<~TEXT
+ ```#{stable_summary_table}```
+ ```#{unstable_summary_table}```
+
+ #{web_url}
+ TEXT
)
+ puts "Done!"
end
- # Post top unstable reliable spec report to slack
- # Slice table in to multiple messages due to max char limitation
+ # Notify failure
#
+ # @param [StandardError] error
# @return [void]
- def notify_top_unstable
- return puts("No unstable tests present!") if top_unstable_reliable.empty?
+ def notify_failure(error)
+ notifier.post(
+ text: "Reliable reporter failed to create report. Error: ```#{error}```",
+ icon_emoji: ":sadpanda:"
+ )
+ end
- tables = top_unstable_reliable.each_slice(5).map do |slice|
- terminal_table(
- rows: slice.map { |spec| [name_column(spec[0], spec[1][:file]), *table_params(spec[1].values)] }
- )
- end
+ private
- puts "\nSending top unstable reliable spec report to #{slack_channel} slack channel"
- slack_args = { icon_emoji: ":sadpanda:", username: "Unstable Spec Report" }
- notifier.post(text: "*#{unstable_title}*", **slack_args)
- tables.each { |table| notifier.post(text: "```#{table}```", **slack_args) }
+ attr_reader :range, :influxdb_bucket, :slack_channel, :influxdb_url, :influxdb_token
+
+ # Markdown formatted report issue body
+ #
+ # @return [String]
+ def report_issue_body
+ issue = []
+ issue << "[[_TOC_]]"
+ issue << "# Candidates for promotion to reliable\n\n```\n#{stable_summary_table}\n```"
+ issue << results_markdown(stable_results_tables)
+ return issue.join("\n\n") if unstable_reliable_test_runs.empty?
+
+ issue << "# Reliable specs with failures\n\n```\n#{unstable_summary_table}\n```"
+ issue << results_markdown(unstable_reliable_results_tables)
+ issue.join("\n\n")
end
- private
+ # Stable spec summary table
+ #
+ # @return [Terminal::Table]
+ def stable_summary_table
+ @stable_summary_table ||= terminal_table(
+ rows: stable_test_runs.map { |stage, specs| [stage, specs.length] },
+ title: "Stable spec summary for past #{range} days".ljust(50),
+ headings: %w[STAGE COUNT]
+ )
+ end
- attr_reader :results,
- :slack_channel,
- :range,
- :run_type,
- :stable_title,
- :unstable_title
+ # Unstable reliable summary table
+ #
+ # @return [Terminal::Table]
+ def unstable_summary_table
+ @unstable_summary_table ||= terminal_table(
+ rows: unstable_reliable_test_runs.map { |stage, specs| [stage, specs.length] },
+ title: "Unstable spec summary for past #{range} days".ljust(50),
+ headings: %w[STAGE COUNT]
+ )
+ end
- # Top stable specs
+ # Result tables for stable specs
#
# @return [Hash]
- def top_stable
- @top_stable ||= runs(reliable: false).sort_by { |k, v| [v[:failure_rate], -v[:runs]] }[0..results - 1].to_h
+ def stable_results_tables
+ @stable_results ||= results_tables(:stable)
end
- # Top unstable reliable specs
+ # Result table for unstable specs
#
# @return [Hash]
- def top_unstable_reliable
- @top_unstable_reliable ||= runs(reliable: true)
- .reject { |k, v| v[:failure_rate] == 0 }
- .sort_by { |k, v| -v[:failure_rate] }[0..results - 1]
- .to_h
+ def unstable_reliable_results_tables
+ @unstable_results ||= results_tables(:unstable)
+ end
+
+ # Markdown formatted tables
+ #
+ # @param [Hash] results
+ # @return [String]
+ def results_markdown(results)
+ results.map do |stage, table|
+ <<~STAGE.strip
+ ## #{stage}
+
+ <details>
+ <summary>Executions table</summary>
+
+ ```
+ #{table}
+ ```
+
+ </details>
+ STAGE
+ end.join("\n\n")
+ end
+
+ # Results table
+ #
+ # @param [Symbol] type result type - :stable, :unstable
+ # @return [Hash<Symbol, Terminal::Table>]
+ def results_tables(type)
+ (type == :stable ? stable_test_runs : unstable_reliable_test_runs).to_h do |stage, specs|
+ headings = ["name", "runs", "failures", "failure rate"]
+
+ [stage, terminal_table(
+ rows: specs.map { |k, v| [name_column(k, v[:file]), *table_params(v.values)] },
+ title: "Top #{type} specs in '#{stage}' stage for past #{range} days",
+ headings: headings.map(&:upcase)
+ )]
+ end
+ end
+
+ # Stable specs
+ #
+ # @return [Hash]
+ def stable_test_runs
+ @top_stable ||= begin
+ stable_specs = test_runs(reliable: false).transform_values do |specs|
+ specs
+ .reject { |k, v| v[:failure_rate] != 0 }
+ .sort_by { |k, v| -v[:runs] }
+ .to_h
+ end
+
+ stable_specs.reject { |k, v| v.empty? }
+ end
+ end
+
+ # Unstable reliable specs
+ #
+ # @return [Hash]
+ def unstable_reliable_test_runs
+ @top_unstable_reliable ||= begin
+ unstable = test_runs(reliable: true).transform_values do |specs|
+ specs
+ .reject { |k, v| v[:failure_rate] == 0 }
+ .sort_by { |k, v| -v[:failure_rate] }
+ .to_h
+ end
+
+ unstable.reject { |k, v| v.empty? }
+ end
end
# Terminal table for result formatting
#
# @return [Terminal::Table]
- def terminal_table(rows:, title: nil)
+ def terminal_table(rows:, headings:, title: nil)
Terminal::Table.new(
- headings: ["name", "runs", "failed", "failure rate"],
+ headings: headings,
style: { all_separators: true },
title: title,
rows: rows
@@ -126,30 +238,32 @@ module QA
# @param [String] file
# @return [String]
def name_column(name, file)
- spec_name = name.length > 100 ? "#{name} ".scan(/.{1,100} /).map(&:strip).join("\n") : name
+ spec_name = name.length > 150 ? "#{name} ".scan(/.{1,150} /).map(&:strip).join("\n") : name
name_line = "name: '#{spec_name}'"
file_line = "file: '#{file}'"
- "#{name_line}\n#{file_line.ljust(110)}"
+ "#{name_line}\n#{file_line.ljust(160)}"
end
# Test executions grouped by name
#
# @param [Boolean] reliable
- # @return [Hash]
- def runs(reliable:)
- puts("Fetching data on #{reliable ? 'reliable ' : ''}test execution for past 30 days in '#{run_type}' runs")
- puts
+ # @return [Hash<String, Hash>]
+ def test_runs(reliable:)
+ puts("Fetching data on #{reliable ? 'reliable ' : ''}test execution for past #{range} days\n".colorize(:green))
- query_api.query(query: query(reliable)).values.each_with_object({}) do |table, result|
+ all_runs = query_api.query(query: query(reliable)).values
+ all_runs.each_with_object(Hash.new { |hsh, key| hsh[key] = {} }) do |table, result|
records = table.records
name = records.last.values["name"]
file = records.last.values["file_path"].split("/").last
+ stage = records.last.values["stage"] || "unknown"
+
runs = records.count
failed = records.count { |r| r.values["status"] == "failed" }
failure_rate = (failed.to_f / runs.to_f) * 100
- result[name] = {
+ result[stage][name] = {
file: file,
runs: runs,
failed: failed,
@@ -164,17 +278,24 @@ module QA
# @return [String]
def query(reliable)
<<~QUERY
- from(bucket: "e2e-test-stats")
- |> range(start: -#{range}d)
- |> filter(fn: (r) => r._measurement == "test-stats" and
- r.run_type == "#{run_type}" and
- r.status != "pending" and
- r.merge_request == "false" and
- r.quarantined == "false" and
- r.reliable == "#{reliable}" and
- r._field == "id"
- )
- |> group(columns: ["name"])
+ from(bucket: "#{influxdb_bucket}")
+ |> range(start: -#{range}d)
+ |> filter(fn: (r) => r._measurement == "test-stats")
+ |> filter(fn: (r) => r.run_type == "staging-full" or
+ r.run_type == "staging-sanity" or
+ r.run_type == "staging-sanity-no-admin" or
+ r.run_type == "production-full" or
+ r.run_type == "production-sanity" or
+ r.run_type == "package-and-qa" or
+ r.run_type == "nightly"
+ )
+ |> filter(fn: (r) => r.status != "pending" and
+ r.merge_request == "false" and
+ r.quarantined == "false" and
+ r.reliable == "#{reliable}" and
+ r._field == "id"
+ )
+ |> group(columns: ["name"])
QUERY
end
@@ -192,7 +313,7 @@ module QA
@influx_client ||= InfluxDB2::Client.new(
influxdb_url,
influxdb_token,
- bucket: "e2e-test-stats",
+ bucket: influxdb_bucket,
org: "gitlab-qa",
precision: InfluxDB2::WritePrecision::NANOSECOND
)
@@ -205,29 +326,29 @@ module QA
@notifier ||= Slack::Notifier.new(
slack_webhook_url,
channel: slack_channel,
- username: "Reliable spec reporter"
+ username: "Reliable Spec Report"
)
end
- # InfluxDb instance url
+ # Gitlab access token
#
# @return [String]
- def influxdb_url
- @influxdb_url ||= ENV["QA_INFLUXDB_URL"] || raise("Missing QA_INFLUXDB_URL environment variable")
+ def gitlab_access_token
+ @gitlab_access_token ||= ENV["GITLAB_ACCESS_TOKEN"] || raise("Missing GITLAB_ACCESS_TOKEN env variable")
end
- # Influxdb token
+ # Gitlab api url
#
# @return [String]
- def influxdb_token
- @influxdb_token ||= ENV["QA_INFLUXDB_TOKEN"] || raise("Missing QA_INFLUXDB_TOKEN environment variable")
+ def gitlab_api_url
+ @gitlab_api_url ||= ENV["CI_API_V4_URL"] || raise("Missing CI_API_V4_URL env variable")
end
# Slack webhook url
#
# @return [String]
def slack_webhook_url
- @slack_webhook_url ||= ENV["CI_SLACK_WEBHOOK_URL"] || raise("Missing CI_SLACK_WEBHOOK_URL environment variable")
+ @slack_webhook_url ||= ENV["SLACK_WEBHOOK"] || raise("Missing SLACK_WEBHOOK env variable")
end
end
end