Commit efc34e82 authored by Andrejs Cunskis's avatar Andrejs Cunskis Committed by Anastasia McDonald

Fetch execution data and mark spec as flaky based on pass rate

Fetch historical data in mr runs
Filter out tests with pass rate < 98%
Add pass rate as test parameter
parent b627a22a
...@@ -4,7 +4,7 @@ source 'https://rubygems.org' ...@@ -4,7 +4,7 @@ source 'https://rubygems.org'
gem 'gitlab-qa', require: 'gitlab/qa' gem 'gitlab-qa', require: 'gitlab/qa'
gem 'activesupport', '~> 6.1.4.6' # This should stay in sync with the root's Gemfile gem 'activesupport', '~> 6.1.4.6' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.15.0' gem 'allure-rspec', '~> 2.16.0'
gem 'capybara', '~> 3.35.0' gem 'capybara', '~> 3.35.0'
gem 'capybara-screenshot', '~> 1.0.23' gem 'capybara-screenshot', '~> 1.0.23'
gem 'rake', '~> 13' gem 'rake', '~> 13'
......
...@@ -19,10 +19,10 @@ GEM ...@@ -19,10 +19,10 @@ GEM
rack-test (>= 1.1.0, < 2.0) rack-test (>= 1.1.0, < 2.0)
rest-client (>= 2.0.2, < 3.0) rest-client (>= 2.0.2, < 3.0)
rspec (~> 3.8) rspec (~> 3.8)
allure-rspec (2.15.0) allure-rspec (2.16.1)
allure-ruby-commons (= 2.15.0) allure-ruby-commons (= 2.16.1)
rspec-core (>= 3.8, < 4) rspec-core (>= 3.8, < 4)
allure-ruby-commons (2.15.0) allure-ruby-commons (2.16.1)
mime-types (>= 3.3, < 4) mime-types (>= 3.3, < 4)
oj (>= 3.10, < 4) oj (>= 3.10, < 4)
require_all (>= 2, < 4) require_all (>= 2, < 4)
...@@ -203,7 +203,7 @@ GEM ...@@ -203,7 +203,7 @@ GEM
octokit (4.21.0) octokit (4.21.0)
faraday (>= 0.9) faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
oj (3.13.8) oj (3.13.11)
os (1.1.4) os (1.1.4)
parallel (1.19.2) parallel (1.19.2)
parallel_tests (2.29.0) parallel_tests (2.29.0)
...@@ -324,7 +324,7 @@ PLATFORMS ...@@ -324,7 +324,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
activesupport (~> 6.1.4.6) activesupport (~> 6.1.4.6)
airborne (~> 0.3.4) airborne (~> 0.3.4)
allure-rspec (~> 2.15.0) allure-rspec (~> 2.16.0)
capybara (~> 3.35.0) capybara (~> 3.35.0)
capybara-screenshot (~> 1.0.23) capybara-screenshot (~> 1.0.23)
chemlab (~> 0.9) chemlab (~> 0.9)
......
...@@ -74,8 +74,8 @@ module QA ...@@ -74,8 +74,8 @@ module QA
# @return [void] # @return [void]
def configure_rspec def configure_rspec
RSpec.configure do |config| RSpec.configure do |config|
config.add_formatter(AllureRspecFormatter)
config.add_formatter(QA::Support::Formatters::AllureMetadataFormatter) config.add_formatter(QA::Support::Formatters::AllureMetadataFormatter)
config.add_formatter(AllureRspecFormatter)
config.append_after do |example| config.append_after do |example|
Allure.add_attachment( Allure.add_attachment(
......
...@@ -4,20 +4,41 @@ module QA ...@@ -4,20 +4,41 @@ module QA
module Support module Support
module Formatters module Formatters
class AllureMetadataFormatter < ::RSpec::Core::Formatters::BaseFormatter class AllureMetadataFormatter < ::RSpec::Core::Formatters::BaseFormatter
include Support::InfluxdbTools
::RSpec::Core::Formatters.register( ::RSpec::Core::Formatters.register(
self, self,
:example_started :start,
:example_finished
) )
# Starts example # Starts test run
# Fetch flakiness data in mr pipelines to help identify unrelated flaky failures
#
# @param [RSpec::Core::Notifications::StartNotification] _start_notification
# @return [void]
def start(_start_notification)
return unless merge_request_iid # on main runs allure native history has pass rate already
save_failures
log(:debug, "Fetched #{failures.length} flaky testcases!")
rescue StandardError => e
log(:error, "Failed to fetch flaky spec data for report: #{e}")
@failures = []
end
# Finished example
# Add additional metadata to report
#
# @param [RSpec::Core::Notifications::ExampleNotification] example_notification # @param [RSpec::Core::Notifications::ExampleNotification] example_notification
# @return [void] # @return [void]
def example_started(example_notification) def example_finished(example_notification)
example = example_notification.example example = example_notification.example
add_quarantine_issue_link(example) add_quarantine_issue_link(example)
add_failure_issues_link(example) add_failure_issues_link(example)
add_ci_job_link(example) add_ci_job_link(example)
set_flaky_status(example)
end end
private private
...@@ -55,6 +76,66 @@ module QA ...@@ -55,6 +76,66 @@ module QA
example.add_link(name: "Job(#{Runtime::Env.ci_job_name})", url: Runtime::Env.ci_job_url) example.add_link(name: "Job(#{Runtime::Env.ci_job_name})", url: Runtime::Env.ci_job_url)
end end
# Mark test as flaky
#
# @param [RSpec::Core::Example] example
# @return [void]
def set_flaky_status(example)
return unless merge_request_iid
return unless example.execution_result.status == :failed && failures.key?(example.metadata[:testcase])
example.set_flaky
example.parameter("pass_rate", "#{failures[example.metadata[:testcase]].round(1)}%")
log(:debug, "Setting spec as flaky due to present failures in last 14 days!")
end
# Failed spec testcases
#
# @return [Array]
def failures
@failures ||= influx_data.lazy.each_with_object({}) do |data, result|
# TODO: replace with mr_iid once stats are populated
records = data.records.reject { |r| r.values["_value"] == env("CI_PIPELINE_ID") }
runs = records.count
failed = records.count { |r| r.values["status"] == "failed" }
pass_rate = 100 - ((failed.to_f / runs.to_f) * 100)
# Consider spec with a pass rate less than 98% as flaky
result[records.last.values["testcase"]] = pass_rate if pass_rate < 98
end.compact
end
alias_method :save_failures, :failures
# Records of previous failures for runs of same type
#
# @return [Array]
def influx_data
return [] unless run_type
query_api.query(query: <<~QUERY).values
from(bucket: "#{Support::InfluxdbTools::INFLUX_TEST_METRICS_BUCKET}")
|> range(start: -14d)
|> filter(fn: (r) => r._measurement == "test-stats")
|> filter(fn: (r) => r.run_type == "#{run_type}" and
r.status != "pending" and
r.quarantined == "false" and
r._field == "pipeline_id"
)
|> group(columns: ["testcase"])
QUERY
end
# Print log message
#
# @param [Symbol] level
# @param [String] message
# @return [void]
def log(level, message)
QA::Runtime::Logger.public_send(level, "[Allure]: #{message}")
end
end end
end end
end end
......
...@@ -4,6 +4,8 @@ module QA ...@@ -4,6 +4,8 @@ module QA
module Support module Support
module Formatters module Formatters
class TestStatsFormatter < RSpec::Core::Formatters::BaseFormatter class TestStatsFormatter < RSpec::Core::Formatters::BaseFormatter
include Support::InfluxdbTools
RSpec::Core::Formatters.register(self, :stop) RSpec::Core::Formatters.register(self, :stop)
# Finish test execution # Finish test execution
...@@ -11,9 +13,6 @@ module QA ...@@ -11,9 +13,6 @@ module QA
# @param [RSpec::Core::Notifications::ExamplesNotification] notification # @param [RSpec::Core::Notifications::ExamplesNotification] notification
# @return [void] # @return [void]
def stop(notification) def stop(notification)
return log(:warn, 'Missing QA_INFLUXDB_URL, skipping metrics export!') unless influxdb_url
return log(:warn, 'Missing QA_INFLUXDB_TOKEN, skipping metrics export!') unless influxdb_token
push_test_stats(notification.examples) push_test_stats(notification.examples)
push_fabrication_stats push_fabrication_stats
end end
...@@ -27,7 +26,7 @@ module QA ...@@ -27,7 +26,7 @@ module QA
def push_test_stats(examples) def push_test_stats(examples)
data = examples.map { |example| test_stats(example) }.compact data = examples.map { |example| test_stats(example) }.compact
influx_client.write(data: data) write_api.write(data: data)
log(:debug, "Pushed #{data.length} test execution entries to influxdb") log(:debug, "Pushed #{data.length} test execution entries to influxdb")
rescue StandardError => e rescue StandardError => e
log(:error, "Failed to push test execution stats to influxdb, error: #{e}") log(:error, "Failed to push test execution stats to influxdb, error: #{e}")
...@@ -42,7 +41,7 @@ module QA ...@@ -42,7 +41,7 @@ module QA
end end
return if data.empty? return if data.empty?
influx_client.write(data: data) write_api.write(data: data)
log(:debug, "Pushed #{data.length} resource fabrication entries to influxdb") log(:debug, "Pushed #{data.length} resource fabrication entries to influxdb")
rescue StandardError => e rescue StandardError => e
log(:error, "Failed to push fabrication stats to influxdb, error: #{e}") log(:error, "Failed to push fabrication stats to influxdb, error: #{e}")
...@@ -70,7 +69,7 @@ module QA ...@@ -70,7 +69,7 @@ module QA
retried: ((example.metadata[:retry_attempts] || 0) > 0).to_s, retried: ((example.metadata[:retry_attempts] || 0) > 0).to_s,
job_name: job_name, job_name: job_name,
merge_request: merge_request, merge_request: merge_request,
run_type: env('QA_RUN_TYPE') || run_type, run_type: run_type,
stage: devops_stage(file_path), stage: devops_stage(file_path),
testcase: example.metadata[:testcase] testcase: example.metadata[:testcase]
}, },
...@@ -83,7 +82,8 @@ module QA ...@@ -83,7 +82,8 @@ module QA
retry_attempts: example.metadata[:retry_attempts] || 0, retry_attempts: example.metadata[:retry_attempts] || 0,
job_url: QA::Runtime::Env.ci_job_url, job_url: QA::Runtime::Env.ci_job_url,
pipeline_url: env('CI_PIPELINE_URL'), pipeline_url: env('CI_PIPELINE_URL'),
pipeline_id: env('CI_PIPELINE_ID') pipeline_id: env('CI_PIPELINE_ID'),
merge_request_iid: merge_request_iid
} }
} }
rescue StandardError => e rescue StandardError => e
...@@ -119,13 +119,6 @@ module QA ...@@ -119,13 +119,6 @@ module QA
} }
end end
# Project name
#
# @return [String]
def project_name
@project_name ||= QA::Runtime::Env.ci_project_name
end
# Base ci job name # Base ci job name
# #
# @return [String] # @return [String]
...@@ -148,26 +141,7 @@ module QA ...@@ -148,26 +141,7 @@ module QA
# #
# @return [String] # @return [String]
def merge_request def merge_request
@merge_request ||= (!!env('CI_MERGE_REQUEST_IID') || !!env('TOP_UPSTREAM_MERGE_REQUEST_IID')).to_s (!!merge_request_iid).to_s
end
# Test run type from staging (`gstg`, `gstg-cny`, `gstg-ref`), canary, preprod or production env
#
# @return [String, nil]
def run_type
return unless %w[staging staging-canary staging-ref canary preprod production].include?(project_name)
@run_type ||= begin
test_subset = if env('NO_ADMIN') == 'true'
'sanity-no-admin'
elsif env('SMOKE_ONLY') == 'true'
'sanity'
else
'full'
end
"#{project_name}-#{test_subset}"
end
end end
# Print log message # Print log message
...@@ -179,16 +153,6 @@ module QA ...@@ -179,16 +153,6 @@ module QA
QA::Runtime::Logger.public_send(level, "[influxdb exporter]: #{message}") QA::Runtime::Logger.public_send(level, "[influxdb exporter]: #{message}")
end end
# Return non empty environment variable value
#
# @param [String] name
# @return [String, nil]
def env(name)
return unless ENV[name] && !ENV[name].empty?
ENV[name]
end
# Get spec devops stage # Get spec devops stage
# #
# @param [String] location # @param [String] location
...@@ -196,33 +160,6 @@ module QA ...@@ -196,33 +160,6 @@ module QA
def devops_stage(file_path) def devops_stage(file_path)
file_path.match(%r{\d{1,2}_(\w+)/})&.captures&.first file_path.match(%r{\d{1,2}_(\w+)/})&.captures&.first
end end
# InfluxDb client
#
# @return [InfluxDB2::WriteApi]
def influx_client
@influx_client ||= InfluxDB2::Client.new(
influxdb_url,
influxdb_token,
bucket: 'e2e-test-stats',
org: 'gitlab-qa',
precision: InfluxDB2::WritePrecision::NANOSECOND
).create_write_api
end
# InfluxDb instance url
#
# @return [String]
def influxdb_url
@influxdb_url ||= env('QA_INFLUXDB_URL')
end
# Influxdb token
#
# @return [String]
def influxdb_token
@influxdb_token ||= env('QA_INFLUXDB_TOKEN')
end
end end
end end
end end
......
# frozen_string_literal: true
module QA
module Support
# Common tools for use with influxdb metrics setup
#
module InfluxdbTools
INFLUX_TEST_METRICS_BUCKET = "e2e-test-stats"
LIVE_ENVS = %w[staging staging-canary staging-ref canary preprod production].freeze
private
delegate :ci_project_name, to: "QA::Runtime::Env"
# Query client
#
# @return [QueryApi]
def query_api
@query_api ||= influx_client.create_query_api
end
# Write client
#
# @return [WriteApi]
def write_api
@write_api ||= influx_client.create_write_api
end
# InfluxDb client
#
# @return [InfluxDB2::Client]
def influx_client
@influx_client ||= InfluxDB2::Client.new(
ENV["QA_INFLUXDB_URL"] || raise("Missing QA_INFLUXDB_URL env variable"),
ENV["QA_INFLUXDB_TOKEN"] || raise("Missing QA_INFLUXDB_TOKEN env variable"),
bucket: INFLUX_TEST_METRICS_BUCKET,
org: "gitlab-qa",
precision: InfluxDB2::WritePrecision::NANOSECOND
)
end
# Test run type
# Automatically infer for staging (`gstg`, `gstg-cny`, `gstg-ref`), canary, preprod or production env
#
# @return [String, nil]
def run_type
@run_type ||= begin
return env('QA_RUN_TYPE') if env('QA_RUN_TYPE')
return unless LIVE_ENVS.include?(ci_project_name)
test_subset = if env('NO_ADMIN') == 'true'
'sanity-no-admin'
elsif env('SMOKE_ONLY') == 'true'
'sanity'
else
'full'
end
"#{ci_project_name}-#{test_subset}"
end
end
# Merge request iid
#
# @return [String]
def merge_request_iid
env('CI_MERGE_REQUEST_IID') || env('TOP_UPSTREAM_MERGE_REQUEST_IID')
end
# Return non empty environment variable value
#
# @param [String] name
# @return [String, nil]
def env(name)
return unless ENV[name] && !ENV[name].empty?
ENV[name]
end
end
end
end
...@@ -8,6 +8,7 @@ require "colorize" ...@@ -8,6 +8,7 @@ require "colorize"
module QA module QA
module Tools module Tools
class ReliableReport class ReliableReport
include Support::InfluxdbTools
include Support::API include Support::API
# Project for report creation: https://gitlab.com/gitlab-org/gitlab # Project for report creation: https://gitlab.com/gitlab-org/gitlab
...@@ -15,10 +16,7 @@ module QA ...@@ -15,10 +16,7 @@ module QA
def initialize(range) def initialize(range)
@range = range.to_i @range = range.to_i
@influxdb_bucket = "e2e-test-stats"
@slack_channel = "#quality-reports" @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 end
# Run reliable reporter # Run reliable reporter
...@@ -91,7 +89,7 @@ module QA ...@@ -91,7 +89,7 @@ module QA
private private
attr_reader :range, :influxdb_bucket, :slack_channel, :influxdb_url, :influxdb_token attr_reader :range, :slack_channel
# Markdown formatted report issue body # Markdown formatted report issue body
# #
...@@ -304,7 +302,7 @@ module QA ...@@ -304,7 +302,7 @@ module QA
# @return [String] # @return [String]
def query(reliable) def query(reliable)
<<~QUERY <<~QUERY
from(bucket: "#{influxdb_bucket}") from(bucket: "#{Support::InfluxdbTools::INFLUX_TEST_METRICS_BUCKET}")
|> range(start: -#{range}d) |> range(start: -#{range}d)
|> filter(fn: (r) => r._measurement == "test-stats") |> filter(fn: (r) => r._measurement == "test-stats")
|> filter(fn: (r) => r.run_type == "staging-full" or |> filter(fn: (r) => r.run_type == "staging-full" or
...@@ -325,26 +323,6 @@ module QA ...@@ -325,26 +323,6 @@ module QA
QUERY QUERY
end end
# Query client
#
# @return [QueryApi]
def query_api
@query_api ||= influx_client.create_query_api
end
# InfluxDb client
#
# @return [InfluxDB2::Client]
def influx_client
@influx_client ||= InfluxDB2::Client.new(
influxdb_url,
influxdb_token,
bucket: influxdb_bucket,
org: "gitlab-qa",
precision: InfluxDB2::WritePrecision::NANOSECOND
)
end
# Slack notifier # Slack notifier
# #
# @return [Slack::Notifier] # @return [Slack::Notifier]
......
...@@ -76,9 +76,10 @@ describe QA::Runtime::AllureReport do ...@@ -76,9 +76,10 @@ describe QA::Runtime::AllureReport do
end end
it 'adds rspec and metadata formatter' do it 'adds rspec and metadata formatter' do
expect(rspec_config).to have_received(:add_formatter).with(
QA::Support::Formatters::AllureMetadataFormatter
).ordered
expect(rspec_config).to have_received(:add_formatter).with(AllureRspecFormatter).ordered expect(rspec_config).to have_received(:add_formatter).with(AllureRspecFormatter).ordered
expect(rspec_config).to have_received(:add_formatter)
.with(QA::Support::Formatters::AllureMetadataFormatter).ordered
end end
it 'configures attachments saving' do it 'configures attachments saving' do
......
...@@ -14,6 +14,7 @@ describe QA::Support::Formatters::AllureMetadataFormatter do ...@@ -14,6 +14,7 @@ describe QA::Support::Formatters::AllureMetadataFormatter do
add_link: nil, add_link: nil,
attempts: 0, attempts: 0,
file_path: 'file/path/spec.rb', file_path: 'file/path/spec.rb',
execution_result: instance_double("RSpec::Core::Example::ExecutionResult", status: :passed),
metadata: { metadata: {
testcase: 'testcase', testcase: 'testcase',
quarantine: { issue: 'issue' } quarantine: { issue: 'issue' }
...@@ -31,7 +32,7 @@ describe QA::Support::Formatters::AllureMetadataFormatter do ...@@ -31,7 +32,7 @@ describe QA::Support::Formatters::AllureMetadataFormatter do
end end
it "adds additional data to report" do it "adds additional data to report" do
formatter.example_started(rspec_example_notification) formatter.example_finished(rspec_example_notification)
aggregate_failures do aggregate_failures do
expect(rspec_example).to have_received(:issue).with('Quarantine issue', 'issue') expect(rspec_example).to have_received(:issue).with('Quarantine issue', 'issue')
......
...@@ -60,7 +60,8 @@ describe QA::Support::Formatters::TestStatsFormatter do ...@@ -60,7 +60,8 @@ describe QA::Support::Formatters::TestStatsFormatter do
retry_attempts: 0, retry_attempts: 0,
job_url: ci_job_url, job_url: ci_job_url,
pipeline_url: ci_pipeline_url, pipeline_url: ci_pipeline_url,
pipeline_id: ci_pipeline_id pipeline_id: ci_pipeline_id,
merge_request_iid: nil
} }
} }
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment