Commit 2a4304ae authored by Sean McGivern's avatar Sean McGivern

Merge branch 'rc/32308-rspec-retry-hack' into 'master'

Detect and keep track of flaky specs

See merge request !13021
parents 69eb4be7 40bbc9d9
...@@ -27,6 +27,7 @@ variables: ...@@ -27,6 +27,7 @@ variables:
GET_SOURCES_ATTEMPTS: "3" GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/${CI_PROJECT_NAME}/report-master.json
before_script: before_script:
- bundle --version - bundle --version
...@@ -45,16 +46,17 @@ stages: ...@@ -45,16 +46,17 @@ stages:
tags: tags:
- gitlab-org - gitlab-org
.knapsack-state: &knapsack-state .tests-metadata-state: &tests-metadata-state
services: [] services: []
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
KNAPSACK_S3_BUCKET: "gitlab-ce-cache" TESTS_METADATA_S3_BUCKET: "gitlab-ce-cache"
artifacts: artifacts:
expire_in: 31d expire_in: 31d
paths: paths:
- knapsack/ - knapsack/
- rspec_flaky/
.use-pg: &use-pg .use-pg: &use-pg
services: services:
...@@ -86,7 +88,7 @@ stages: ...@@ -86,7 +88,7 @@ stages:
except: except:
- /(^docs[\/-].*|.*-docs$)/ - /(^docs[\/-].*|.*-docs$)/
.rspec-knapsack: &rspec-knapsack .rspec-metadata: &rspec-metadata
<<: *dedicated-runner <<: *dedicated-runner
<<: *pull-cache <<: *pull-cache
stage: test stage: test
...@@ -96,8 +98,13 @@ stages: ...@@ -96,8 +98,13 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[-1]} - export CI_NODE_TOTAL=${JOB_NAME[-1]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- export ALL_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/all_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/new_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export FLAKY_RSPEC_GENERATE_REPORT=true
- export CACHE_CLASSES=true - export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- cp ${FLAKY_RSPEC_SUITE_REPORT_PATH} ${ALL_FLAKY_RSPEC_REPORT_PATH}
- '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}'
- scripts/gitaly-test-spawn - scripts/gitaly-test-spawn
- knapsack rspec "--color --format documentation" - knapsack rspec "--color --format documentation"
artifacts: artifacts:
...@@ -106,20 +113,21 @@ stages: ...@@ -106,20 +113,21 @@ stages:
paths: paths:
- coverage/ - coverage/
- knapsack/ - knapsack/
- rspec_flaky/
- tmp/capybara/ - tmp/capybara/
.rspec-knapsack-pg: &rspec-knapsack-pg .rspec-metadata-pg: &rspec-metadata-pg
<<: *rspec-knapsack <<: *rspec-metadata
<<: *use-pg <<: *use-pg
<<: *except-docs <<: *except-docs
.rspec-knapsack-mysql: &rspec-knapsack-mysql .rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-knapsack <<: *rspec-metadata
<<: *use-mysql <<: *use-mysql
<<: *only-if-want-mysql <<: *only-if-want-mysql
<<: *except-docs <<: *except-docs
.spinach-knapsack: &spinach-knapsack .spinach-metadata: &spinach-metadata
<<: *dedicated-runner <<: *dedicated-runner
<<: *pull-cache <<: *pull-cache
stage: test stage: test
...@@ -140,13 +148,13 @@ stages: ...@@ -140,13 +148,13 @@ stages:
- knapsack/ - knapsack/
- tmp/capybara/ - tmp/capybara/
.spinach-knapsack-pg: &spinach-knapsack-pg .spinach-metadata-pg: &spinach-metadata-pg
<<: *spinach-knapsack <<: *spinach-metadata
<<: *use-pg <<: *use-pg
<<: *except-docs <<: *except-docs
.spinach-knapsack-mysql: &spinach-knapsack-mysql .spinach-metadata-mysql: &spinach-metadata-mysql
<<: *spinach-knapsack <<: *spinach-metadata
<<: *use-mysql <<: *use-mysql
<<: *only-if-want-mysql <<: *only-if-want-mysql
<<: *except-docs <<: *except-docs
...@@ -176,40 +184,70 @@ build-package: ...@@ -176,40 +184,70 @@ build-package:
- //@gitlab-org/gitlab-ce - //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee - //@gitlab-org/gitlab-ee
# Prepare and merge knapsack tests # Retrieve knapsack and rspec_flaky reports
knapsack: retrieve-tests-metadata:
<<: *knapsack-state <<: *tests-metadata-state
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs
stage: prepare stage: prepare
cache: cache:
key: knapsack key: tests_metadata
paths:
- knapsack/
policy: pull policy: pull
script: script:
- mkdir -p knapsack/${CI_PROJECT_NAME}/ - mkdir -p knapsack/${CI_PROJECT_NAME}/
- wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
- wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}' - '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}' - '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
- mkdir -p rspec_flaky/${CI_PROJECT_NAME}/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
update-knapsack: update-tests-metadata:
<<: *knapsack-state <<: *tests-metadata-state
<<: *dedicated-runner <<: *dedicated-runner
<<: *only-canonical-masters <<: *only-canonical-masters
stage: post-test stage: post-test
cache: cache:
key: knapsack key: tests_metadata
paths: paths:
- knapsack/ - knapsack/
- rspec_flaky/
policy: push policy: push
script: script:
- retry gem install fog-aws mime-types - retry gem install fog-aws mime-types
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH' - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json
flaky-examples-check:
<<: *dedicated-runner
image: ruby:2.3-alpine
services: []
before_script: []
cache: {}
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
NEW_FLAKY_SPECS_REPORT: rspec_flaky/${CI_PROJECT_NAME}/new_rspec_flaky_examples.json
stage: post-test
allow_failure: yes
only:
- branches
except:
- master
artifacts:
expire_in: 30d
paths:
- rspec_flaky/
script:
- '[[ -f $NEW_FLAKY_SPECS_REPORT ]] || echo "{}" > ${NEW_FLAKY_SPECS_REPORT}'
- scripts/merge-reports $NEW_FLAKY_SPECS_REPORT rspec_flaky/${CI_PROJECT_NAME}/new_node_*.json
- scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT
setup-test-env: setup-test-env:
<<: *use-pg <<: *use-pg
...@@ -232,69 +270,69 @@ setup-test-env: ...@@ -232,69 +270,69 @@ setup-test-env:
- public/assets - public/assets
- tmp/tests - tmp/tests
rspec-pg 0 25: *rspec-knapsack-pg rspec-pg 0 25: *rspec-metadata-pg
rspec-pg 1 25: *rspec-knapsack-pg rspec-pg 1 25: *rspec-metadata-pg
rspec-pg 2 25: *rspec-knapsack-pg rspec-pg 2 25: *rspec-metadata-pg
rspec-pg 3 25: *rspec-knapsack-pg rspec-pg 3 25: *rspec-metadata-pg
rspec-pg 4 25: *rspec-knapsack-pg rspec-pg 4 25: *rspec-metadata-pg
rspec-pg 5 25: *rspec-knapsack-pg rspec-pg 5 25: *rspec-metadata-pg
rspec-pg 6 25: *rspec-knapsack-pg rspec-pg 6 25: *rspec-metadata-pg
rspec-pg 7 25: *rspec-knapsack-pg rspec-pg 7 25: *rspec-metadata-pg
rspec-pg 8 25: *rspec-knapsack-pg rspec-pg 8 25: *rspec-metadata-pg
rspec-pg 9 25: *rspec-knapsack-pg rspec-pg 9 25: *rspec-metadata-pg
rspec-pg 10 25: *rspec-knapsack-pg rspec-pg 10 25: *rspec-metadata-pg
rspec-pg 11 25: *rspec-knapsack-pg rspec-pg 11 25: *rspec-metadata-pg
rspec-pg 12 25: *rspec-knapsack-pg rspec-pg 12 25: *rspec-metadata-pg
rspec-pg 13 25: *rspec-knapsack-pg rspec-pg 13 25: *rspec-metadata-pg
rspec-pg 14 25: *rspec-knapsack-pg rspec-pg 14 25: *rspec-metadata-pg
rspec-pg 15 25: *rspec-knapsack-pg rspec-pg 15 25: *rspec-metadata-pg
rspec-pg 16 25: *rspec-knapsack-pg rspec-pg 16 25: *rspec-metadata-pg
rspec-pg 17 25: *rspec-knapsack-pg rspec-pg 17 25: *rspec-metadata-pg
rspec-pg 18 25: *rspec-knapsack-pg rspec-pg 18 25: *rspec-metadata-pg
rspec-pg 19 25: *rspec-knapsack-pg rspec-pg 19 25: *rspec-metadata-pg
rspec-pg 20 25: *rspec-knapsack-pg rspec-pg 20 25: *rspec-metadata-pg
rspec-pg 21 25: *rspec-knapsack-pg rspec-pg 21 25: *rspec-metadata-pg
rspec-pg 22 25: *rspec-knapsack-pg rspec-pg 22 25: *rspec-metadata-pg
rspec-pg 23 25: *rspec-knapsack-pg rspec-pg 23 25: *rspec-metadata-pg
rspec-pg 24 25: *rspec-knapsack-pg rspec-pg 24 25: *rspec-metadata-pg
rspec-mysql 0 25: *rspec-knapsack-mysql rspec-mysql 0 25: *rspec-metadata-mysql
rspec-mysql 1 25: *rspec-knapsack-mysql rspec-mysql 1 25: *rspec-metadata-mysql
rspec-mysql 2 25: *rspec-knapsack-mysql rspec-mysql 2 25: *rspec-metadata-mysql
rspec-mysql 3 25: *rspec-knapsack-mysql rspec-mysql 3 25: *rspec-metadata-mysql
rspec-mysql 4 25: *rspec-knapsack-mysql rspec-mysql 4 25: *rspec-metadata-mysql
rspec-mysql 5 25: *rspec-knapsack-mysql rspec-mysql 5 25: *rspec-metadata-mysql
rspec-mysql 6 25: *rspec-knapsack-mysql rspec-mysql 6 25: *rspec-metadata-mysql
rspec-mysql 7 25: *rspec-knapsack-mysql rspec-mysql 7 25: *rspec-metadata-mysql
rspec-mysql 8 25: *rspec-knapsack-mysql rspec-mysql 8 25: *rspec-metadata-mysql
rspec-mysql 9 25: *rspec-knapsack-mysql rspec-mysql 9 25: *rspec-metadata-mysql
rspec-mysql 10 25: *rspec-knapsack-mysql rspec-mysql 10 25: *rspec-metadata-mysql
rspec-mysql 11 25: *rspec-knapsack-mysql rspec-mysql 11 25: *rspec-metadata-mysql
rspec-mysql 12 25: *rspec-knapsack-mysql rspec-mysql 12 25: *rspec-metadata-mysql
rspec-mysql 13 25: *rspec-knapsack-mysql rspec-mysql 13 25: *rspec-metadata-mysql
rspec-mysql 14 25: *rspec-knapsack-mysql rspec-mysql 14 25: *rspec-metadata-mysql
rspec-mysql 15 25: *rspec-knapsack-mysql rspec-mysql 15 25: *rspec-metadata-mysql
rspec-mysql 16 25: *rspec-knapsack-mysql rspec-mysql 16 25: *rspec-metadata-mysql
rspec-mysql 17 25: *rspec-knapsack-mysql rspec-mysql 17 25: *rspec-metadata-mysql
rspec-mysql 18 25: *rspec-knapsack-mysql rspec-mysql 18 25: *rspec-metadata-mysql
rspec-mysql 19 25: *rspec-knapsack-mysql rspec-mysql 19 25: *rspec-metadata-mysql
rspec-mysql 20 25: *rspec-knapsack-mysql rspec-mysql 20 25: *rspec-metadata-mysql
rspec-mysql 21 25: *rspec-knapsack-mysql rspec-mysql 21 25: *rspec-metadata-mysql
rspec-mysql 22 25: *rspec-knapsack-mysql rspec-mysql 22 25: *rspec-metadata-mysql
rspec-mysql 23 25: *rspec-knapsack-mysql rspec-mysql 23 25: *rspec-metadata-mysql
rspec-mysql 24 25: *rspec-knapsack-mysql rspec-mysql 24 25: *rspec-metadata-mysql
spinach-pg 0 5: *spinach-knapsack-pg spinach-pg 0 5: *spinach-metadata-pg
spinach-pg 1 5: *spinach-knapsack-pg spinach-pg 1 5: *spinach-metadata-pg
spinach-pg 2 5: *spinach-knapsack-pg spinach-pg 2 5: *spinach-metadata-pg
spinach-pg 3 5: *spinach-knapsack-pg spinach-pg 3 5: *spinach-metadata-pg
spinach-pg 4 5: *spinach-knapsack-pg spinach-pg 4 5: *spinach-metadata-pg
spinach-mysql 0 5: *spinach-knapsack-mysql spinach-mysql 0 5: *spinach-metadata-mysql
spinach-mysql 1 5: *spinach-knapsack-mysql spinach-mysql 1 5: *spinach-metadata-mysql
spinach-mysql 2 5: *spinach-knapsack-mysql spinach-mysql 2 5: *spinach-metadata-mysql
spinach-mysql 3 5: *spinach-knapsack-mysql spinach-mysql 3 5: *spinach-metadata-mysql
spinach-mysql 4 5: *spinach-knapsack-mysql spinach-mysql 4 5: *spinach-metadata-mysql
# Static analysis jobs # Static analysis jobs
.ruby-static-analysis: &ruby-static-analysis .ruby-static-analysis: &ruby-static-analysis
......
...@@ -1045,7 +1045,7 @@ RSpec/BeforeAfterAll: ...@@ -1045,7 +1045,7 @@ RSpec/BeforeAfterAll:
RSpec/DescribeClass: RSpec/DescribeClass:
Enabled: false Enabled: false
# Use `described_class` for tested class / module. # Checks that the second argument to `describe` specifies a method.
RSpec/DescribeMethod: RSpec/DescribeMethod:
Enabled: false Enabled: false
...@@ -1053,8 +1053,7 @@ RSpec/DescribeMethod: ...@@ -1053,8 +1053,7 @@ RSpec/DescribeMethod:
RSpec/DescribeSymbol: RSpec/DescribeSymbol:
Enabled: true Enabled: true
# Checks that the second argument to top level describe is the tested method # Checks that tests use `described_class`.
# name.
RSpec/DescribedClass: RSpec/DescribedClass:
Enabled: true Enabled: true
......
...@@ -157,8 +157,9 @@ trade-off: ...@@ -157,8 +157,9 @@ trade-off:
- Unit tests are usually cheap, and you should consider them like the basement - Unit tests are usually cheap, and you should consider them like the basement
of your house: you need them to be confident that your code is behaving of your house: you need them to be confident that your code is behaving
correctly. However if you run only unit tests without integration / system tests, you might [miss] the [big] [picture]! correctly. However if you run only unit tests without integration / system
- Integration tests are a bit more expensive, but don't abuse them. A feature test tests, you might [miss] the [big] [picture]!
- Integration tests are a bit more expensive, but don't abuse them. A system test
is often better than an integration test that is stubbing a lot of internals. is often better than an integration test that is stubbing a lot of internals.
- System tests are expensive (compared to unit tests), even more if they require - System tests are expensive (compared to unit tests), even more if they require
a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed) a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed)
...@@ -195,11 +196,27 @@ Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md). ...@@ -195,11 +196,27 @@ Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md).
- Try to match the ordering of tests to the ordering within the class. - Try to match the ordering of tests to the ordering within the class.
- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines - Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases. to separate phases.
- Try to use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'` - Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
- Don't assert against the absolute value of a sequence-generated attribute (see
[Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
- Don't supply the `:each` argument to hooks since it's the default.
- On `before` and `after` hooks, prefer it scoped to `:context` over `:all` - On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
[four-phase-test]: https://robots.thoughtbot.com/four-phase-test [four-phase-test]: https://robots.thoughtbot.com/four-phase-test
### Automatic retries and flaky tests detection
On our CI, we use [rspec-retry] to automatically retry a failing example a few
times (see [`spec/spec_helper.rb`] for the precise retries count).
We also use a home-made `RspecFlaky::Listener` listener which records flaky
examples in a JSON report file on `master` (`retrieve-tests-metadata` and `update-tests-metadata` jobs), and warns when a new flaky example
is detected in any other branch (`flaky-examples-check` job). In the future, the
`flaky-examples-check` job will not be allowed to fail.
[rspec-retry]: https://github.com/NoRedInk/rspec-retry
[`spec/spec_helper.rb`]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/spec_helper.rb
### `let` variables ### `let` variables
GitLab's RSpec suite has made extensive use of `let` variables to reduce GitLab's RSpec suite has made extensive use of `let` variables to reduce
......
module RspecFlaky
# This is a wrapper class for RSpec::Core::Example
class Example
delegate :status, :exception, to: :execution_result
def initialize(rspec_example)
@rspec_example = rspec_example.try(:example) || rspec_example
end
def uid
@uid ||= Digest::MD5.hexdigest("#{description}-#{file}")
end
def example_id
rspec_example.id
end
def file
metadata[:file_path]
end
def line
metadata[:line_number]
end
def description
metadata[:full_description]
end
def attempts
rspec_example.try(:attempts) || 1
end
private
attr_reader :rspec_example
def metadata
rspec_example.metadata
end
def execution_result
rspec_example.execution_result
end
end
end
module RspecFlaky
# This represents a flaky RSpec example and is mainly meant to be saved in a JSON file
class FlakyExample < OpenStruct
def initialize(example)
if example.respond_to?(:example_id)
super(
example_id: example.example_id,
file: example.file,
line: example.line,
description: example.description,
last_attempts_count: example.attempts,
flaky_reports: 1)
else
super
end
end
def first_flaky_at
self[:first_flaky_at] || Time.now
end
def last_flaky_at
Time.now
end
def last_flaky_job
return unless ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID']
"#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}"
end
def to_h
super.merge(
first_flaky_at: first_flaky_at,
last_flaky_at: last_flaky_at,
last_flaky_job: last_flaky_job)
end
end
end
require 'json'
module RspecFlaky
class Listener
attr_reader :all_flaky_examples, :new_flaky_examples
def initialize
@new_flaky_examples = {}
@all_flaky_examples = init_all_flaky_examples
end
def example_passed(notification)
current_example = RspecFlaky::Example.new(notification.example)
return unless current_example.attempts > 1
flaky_example_hash = all_flaky_examples[current_example.uid]
all_flaky_examples[current_example.uid] =
if flaky_example_hash
FlakyExample.new(flaky_example_hash).tap do |ex|
ex.last_attempts_count = current_example.attempts
ex.flaky_reports += 1
end
else
FlakyExample.new(current_example).tap do |ex|
new_flaky_examples[current_example.uid] = ex
end
end
end
def dump_summary(_)
write_report_file(all_flaky_examples, all_flaky_examples_report_path)
if new_flaky_examples.any?
Rails.logger.warn "\nNew flaky examples detected:\n"
Rails.logger.warn JSON.pretty_generate(to_report(new_flaky_examples))
write_report_file(new_flaky_examples, new_flaky_examples_report_path)
end
end
def to_report(examples)
Hash[examples.map { |k, ex| [k, ex.to_h] }]
end
private
def init_all_flaky_examples
return {} unless File.exist?(all_flaky_examples_report_path)
all_flaky_examples = JSON.parse(File.read(all_flaky_examples_report_path))
Hash[(all_flaky_examples || {}).map { |k, ex| [k, FlakyExample.new(ex)] }]
end
def write_report_file(examples, file_path)
return unless ENV['FLAKY_RSPEC_GENERATE_REPORT'] == 'true'
report_path_dir = File.dirname(file_path)
FileUtils.mkdir_p(report_path_dir) unless Dir.exist?(report_path_dir)
File.write(file_path, JSON.pretty_generate(to_report(examples)))
end
def all_flaky_examples_report_path
@all_flaky_examples_report_path ||= ENV['ALL_FLAKY_RSPEC_REPORT_PATH'] ||
Rails.root.join("rspec_flaky/all-report.json")
end
def new_flaky_examples_report_path
@new_flaky_examples_report_path ||= ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] ||
Rails.root.join("rspec_flaky/new-report.json")
end
end
end
#!/usr/bin/env ruby
require 'json'
report_file = ARGV.shift
unless report_file
puts 'usage: detect-new-flaky-examples <report-file>'
exit 1
end
puts "Loading #{report_file}..."
report = JSON.parse(File.read(report_file))
if report.any?
puts "New flaky examples were detected!\n"
puts JSON.pretty_generate(report)
exit 1
else
puts "No new flaky examples detected.\n"
exit 0
end
...@@ -4,7 +4,7 @@ require 'json' ...@@ -4,7 +4,7 @@ require 'json'
main_report_file = ARGV.shift main_report_file = ARGV.shift
unless main_report_file unless main_report_file
puts 'usage: merge_reports <main-report> [extra reports...]' puts 'usage: merge-reports <main-report> [extra reports...]'
exit 1 exit 1
end end
......
...@@ -106,12 +106,6 @@ describe Gitlab::HealthChecks::FsShardsCheck do ...@@ -106,12 +106,6 @@ describe Gitlab::HealthChecks::FsShardsCheck do
}.with_indifferent_access }.with_indifferent_access
end end
# Unsolved intermittent failure in CI https://gitlab.com/gitlab-org/gitlab-ce/issues/31128
around do |example| # rubocop:disable RSpec/AroundBlock
times_to_try = ENV['CI'] ? 4 : 1
example.run_with_retry retry: times_to_try
end
it 'provides metrics' do it 'provides metrics' do
metrics = described_class.metrics metrics = described_class.metrics
......
require 'spec_helper'
describe RspecFlaky::Example do
let(:example_attrs) do
{
id: 'spec/foo/bar_spec.rb:2',
metadata: {
file_path: 'spec/foo/bar_spec.rb',
line_number: 2,
full_description: 'hello world'
},
execution_result: double(status: 'passed', exception: 'BOOM!'),
attempts: 1
}
end
let(:rspec_example) { double(example_attrs) }
describe '#initialize' do
shared_examples 'a valid Example instance' do
it 'returns valid attributes' do
example = described_class.new(args)
expect(example.example_id).to eq(example_attrs[:id])
end
end
context 'when given an Rspec::Core::Example that responds to #example' do
let(:args) { double(example: rspec_example) }
it_behaves_like 'a valid Example instance'
end
context 'when given an Rspec::Core::Example that does not respond to #example' do
let(:args) { rspec_example }
it_behaves_like 'a valid Example instance'
end
end
subject { described_class.new(rspec_example) }
describe '#uid' do
it 'returns a hash of the full description' do
expect(subject.uid).to eq(Digest::MD5.hexdigest("#{subject.description}-#{subject.file}"))
end
end
describe '#example_id' do
it 'returns the ID of the RSpec::Core::Example' do
expect(subject.example_id).to eq(rspec_example.id)
end
end
describe '#attempts' do
it 'returns the attempts of the RSpec::Core::Example' do
expect(subject.attempts).to eq(rspec_example.attempts)
end
end
describe '#file' do
it 'returns the metadata[:file_path] of the RSpec::Core::Example' do
expect(subject.file).to eq(rspec_example.metadata[:file_path])
end
end
describe '#line' do
it 'returns the metadata[:line_number] of the RSpec::Core::Example' do
expect(subject.line).to eq(rspec_example.metadata[:line_number])
end
end
describe '#description' do
it 'returns the metadata[:full_description] of the RSpec::Core::Example' do
expect(subject.description).to eq(rspec_example.metadata[:full_description])
end
end
describe '#status' do
it 'returns the execution_result.status of the RSpec::Core::Example' do
expect(subject.status).to eq(rspec_example.execution_result.status)
end
end
describe '#exception' do
it 'returns the execution_result.exception of the RSpec::Core::Example' do
expect(subject.exception).to eq(rspec_example.execution_result.exception)
end
end
end
require 'spec_helper'
describe RspecFlaky::FlakyExample do
let(:flaky_example_attrs) do
{
example_id: 'spec/foo/bar_spec.rb:2',
file: 'spec/foo/bar_spec.rb',
line: 2,
description: 'hello world',
first_flaky_at: 1234,
last_flaky_at: 2345,
last_attempts_count: 2,
flaky_reports: 1
}
end
let(:example_attrs) do
{
uid: 'abc123',
example_id: flaky_example_attrs[:example_id],
file: flaky_example_attrs[:file],
line: flaky_example_attrs[:line],
description: flaky_example_attrs[:description],
status: 'passed',
exception: 'BOOM!',
attempts: flaky_example_attrs[:last_attempts_count]
}
end
let(:example) { double(example_attrs) }
describe '#initialize' do
shared_examples 'a valid FlakyExample instance' do
it 'returns valid attributes' do
flaky_example = described_class.new(args)
expect(flaky_example.uid).to eq(flaky_example_attrs[:uid])
expect(flaky_example.example_id).to eq(flaky_example_attrs[:example_id])
end
end
context 'when given an Rspec::Example' do
let(:args) { example }
it_behaves_like 'a valid FlakyExample instance'
end
context 'when given a hash' do
let(:args) { flaky_example_attrs }
it_behaves_like 'a valid FlakyExample instance'
end
end
describe '#to_h' do
before do
# Stub these env variables otherwise specs don't behave the same on the CI
stub_env('CI_PROJECT_URL', nil)
stub_env('CI_JOB_ID', nil)
end
shared_examples 'a valid FlakyExample hash' do
let(:additional_attrs) { {} }
it 'returns a valid hash' do
flaky_example = described_class.new(args)
final_hash = flaky_example_attrs
.merge(last_flaky_at: instance_of(Time), last_flaky_job: nil)
.merge(additional_attrs)
expect(flaky_example.to_h).to match(hash_including(final_hash))
end
end
context 'when given an Rspec::Example' do
let(:args) { example }
context 'when run locally' do
it_behaves_like 'a valid FlakyExample hash' do
let(:additional_attrs) do
{ first_flaky_at: instance_of(Time) }
end
end
end
context 'when run on the CI' do
before do
stub_env('CI_PROJECT_URL', 'https://gitlab.com/gitlab-org/gitlab-ce')
stub_env('CI_JOB_ID', 42)
end
it_behaves_like 'a valid FlakyExample hash' do
let(:additional_attrs) do
{ first_flaky_at: instance_of(Time), last_flaky_job: "https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/42" }
end
end
end
end
context 'when given a hash' do
let(:args) { flaky_example_attrs }
it_behaves_like 'a valid FlakyExample hash'
end
end
end
require 'spec_helper'
describe RspecFlaky::Listener do
let(:flaky_example_report) do
{
'abc123' => {
example_id: 'spec/foo/bar_spec.rb:2',
file: 'spec/foo/bar_spec.rb',
line: 2,
description: 'hello world',
first_flaky_at: 1234,
last_flaky_at: instance_of(Time),
last_attempts_count: 2,
flaky_reports: 1,
last_flaky_job: nil
}
}
end
let(:example_attrs) do
{
id: 'spec/foo/baz_spec.rb:3',
metadata: {
file_path: 'spec/foo/baz_spec.rb',
line_number: 3,
full_description: 'hello GitLab'
},
execution_result: double(status: 'passed', exception: nil)
}
end
before do
# Stub these env variables otherwise specs don't behave the same on the CI
stub_env('CI_PROJECT_URL', nil)
stub_env('CI_JOB_ID', nil)
end
describe '#initialize' do
shared_examples 'a valid Listener instance' do
let(:expected_all_flaky_examples) { {} }
it 'returns a valid Listener instance' do
listener = described_class.new
expect(listener.to_report(listener.all_flaky_examples))
.to match(hash_including(expected_all_flaky_examples))
expect(listener.new_flaky_examples).to eq({})
end
end
context 'when no report file exists' do
it_behaves_like 'a valid Listener instance'
end
context 'when a report file exists and set by ALL_FLAKY_RSPEC_REPORT_PATH' do
let(:report_file) do
Tempfile.new(%w[rspec_flaky_report .json]).tap do |f|
f.write(JSON.pretty_generate(flaky_example_report))
f.rewind
end
end
before do
stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file.path)
end
after do
report_file.close
report_file.unlink
end
it_behaves_like 'a valid Listener instance' do
let(:expected_all_flaky_examples) { flaky_example_report }
end
end
end
describe '#example_passed' do
let(:rspec_example) { double(example_attrs) }
let(:notification) { double(example: rspec_example) }
shared_examples 'a non-flaky example' do
it 'does not change the flaky examples hash' do
expect { subject.example_passed(notification) }
.not_to change { subject.all_flaky_examples }
end
end
describe 'when the RSpec example does not respond to attempts' do
it_behaves_like 'a non-flaky example'
end
describe 'when the RSpec example has 1 attempt' do
let(:rspec_example) { double(example_attrs.merge(attempts: 1)) }
it_behaves_like 'a non-flaky example'
end
describe 'when the RSpec example has 2 attempts' do
let(:rspec_example) { double(example_attrs.merge(attempts: 2)) }
let(:expected_new_flaky_example) do
{
example_id: 'spec/foo/baz_spec.rb:3',
file: 'spec/foo/baz_spec.rb',
line: 3,
description: 'hello GitLab',
first_flaky_at: instance_of(Time),
last_flaky_at: instance_of(Time),
last_attempts_count: 2,
flaky_reports: 1,
last_flaky_job: nil
}
end
it 'does not change the flaky examples hash' do
expect { subject.example_passed(notification) }
.to change { subject.all_flaky_examples }
new_example = RspecFlaky::Example.new(rspec_example)
expect(subject.all_flaky_examples[new_example.uid].to_h)
.to match(hash_including(expected_new_flaky_example))
end
end
end
describe '#dump_summary' do
let(:rspec_example) { double(example_attrs) }
let(:notification) { double(example: rspec_example) }
context 'when a report file path is set by ALL_FLAKY_RSPEC_REPORT_PATH' do
let(:report_file_path) { Rails.root.join('tmp', 'rspec_flaky_report.json') }
before do
stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file_path)
FileUtils.rm(report_file_path) if File.exist?(report_file_path)
end
after do
FileUtils.rm(report_file_path) if File.exist?(report_file_path)
end
context 'when FLAKY_RSPEC_GENERATE_REPORT == "false"' do
before do
stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false')
end
it 'does not write the report file' do
subject.example_passed(notification)
subject.dump_summary(nil)
expect(File.exist?(report_file_path)).to be(false)
end
end
context 'when FLAKY_RSPEC_GENERATE_REPORT == "true"' do
before do
stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true')
end
it 'writes the report file' do
subject.example_passed(notification)
subject.dump_summary(nil)
expect(File.exist?(report_file_path)).to be(true)
end
end
end
end
describe '#to_report' do
it 'transforms the internal hash to a JSON-ready hash' do
expect(subject.to_report('abc123' => RspecFlaky::FlakyExample.new(flaky_example_report['abc123'])))
.to match(hash_including(flaky_example_report))
end
end
end
...@@ -69,6 +69,12 @@ RSpec.configure do |config| ...@@ -69,6 +69,12 @@ RSpec.configure do |config|
config.raise_errors_for_deprecations! config.raise_errors_for_deprecations!
if ENV['CI']
# This includes the first try, i.e. tests will be run 4 times before failing.
config.default_retry_count = 4
config.reporter.register_listener(RspecFlaky::Listener.new, :example_passed, :dump_summary)
end
config.before(:suite) do config.before(:suite) do
TestEnv.init TestEnv.init
end end
...@@ -97,12 +103,6 @@ RSpec.configure do |config| ...@@ -97,12 +103,6 @@ RSpec.configure do |config|
reset_delivered_emails! reset_delivered_emails!
end end
if ENV['CI']
config.around(:each) do |ex|
ex.run_with_retry retry: 2
end
end
config.around(:each, :use_clean_rails_memory_store_caching) do |example| config.around(:each, :use_clean_rails_memory_store_caching) do |example|
caching_store = Rails.cache caching_store = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new Rails.cache = ActiveSupport::Cache::MemoryStore.new
......
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