Commit 2ff1fbe7 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge branch 'retry-failing-tests-2' into 'master'

ci: Retry failed specs in a new process after the initial run

See merge request gitlab-org/gitlab!76839
parents 52ce12b6 5952e70b
...@@ -79,6 +79,7 @@ eslint-report.html ...@@ -79,6 +79,7 @@ eslint-report.html
/deprecations/ /deprecations/
/knapsack/ /knapsack/
/rspec_flaky/ /rspec_flaky/
/rspec/
/locale/**/LC_MESSAGES /locale/**/LC_MESSAGES
/locale/**/*.time_stamp /locale/**/*.time_stamp
/.rspec /.rspec
......
...@@ -69,10 +69,14 @@ variables: ...@@ -69,10 +69,14 @@ variables:
GET_SOURCES_ATTEMPTS: "3" GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/report-master.json KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json FLAKY_RSPEC_SUITE_REPORT_PATH: rspec/flaky/report-suite.json
RSPEC_TESTS_MAPPING_PATH: crystalball/mapping.json RSPEC_TESTS_MAPPING_PATH: crystalball/mapping.json
RSPEC_PACKED_TESTS_MAPPING_PATH: crystalball/packed-mapping.json RSPEC_PACKED_TESTS_MAPPING_PATH: crystalball/packed-mapping.json
RSPEC_PROFILING_FOLDER_PATH: rspec/profiling
FRONTEND_FIXTURES_MAPPING_PATH: crystalball/frontend_fixtures_mapping.json FRONTEND_FIXTURES_MAPPING_PATH: crystalball/frontend_fixtures_mapping.json
RSPEC_LAST_RUN_RESULTS_FILE: rspec/rspec_last_run_results.txt
JUNIT_RESULT_FILE: rspec/junit_rspec.xml
JUNIT_RETRY_FILE: rspec/junit_rspec-retry.xml
ES_JAVA_OPTS: "-Xms256m -Xmx256m" ES_JAVA_OPTS: "-Xms256m -Xmx256m"
ELASTIC_URL: "http://elastic:changeme@elasticsearch:9200" ELASTIC_URL: "http://elastic:changeme@elasticsearch:9200"
......
...@@ -29,7 +29,9 @@ ...@@ -29,7 +29,9 @@
GITLAB_USE_MODEL_LOAD_BALANCING: "true" GITLAB_USE_MODEL_LOAD_BALANCING: "true"
.rspec-base: .rspec-base:
extends: .rails-job-base extends:
- .rails-job-base
- .base-artifacts
stage: test stage: test
variables: variables:
RUBY_GC_MALLOC_LIMIT: 67108864 RUBY_GC_MALLOC_LIMIT: 67108864
...@@ -40,6 +42,8 @@ ...@@ -40,6 +42,8 @@
script: script:
- !reference [.base-script, script] - !reference [.base-script, script]
- rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag ~level:migration" - rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag ~level:migration"
.base-artifacts:
artifacts: artifacts:
expire_in: 31d expire_in: 31d
when: always when: always
...@@ -48,16 +52,17 @@ ...@@ -48,16 +52,17 @@
- crystalball/ - crystalball/
- deprecations/ - deprecations/
- knapsack/ - knapsack/
- rspec_flaky/ - rspec/
- rspec_profiling/
- tmp/capybara/ - tmp/capybara/
- tmp/memory_test/ - tmp/memory_test/
- log/*.log - log/*.log
reports: reports:
junit: junit_rspec.xml junit: ${JUNIT_RESULT_FILE}
.rspec-base-migration: .rspec-base-migration:
extends: .rails:rules:ee-and-foss-migration extends:
- .base-artifacts
- .rails:rules:ee-and-foss-migration
script: script:
- !reference [.base-script, script] - !reference [.base-script, script]
- rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag level:migration" - rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag level:migration"
...@@ -617,54 +622,26 @@ rspec:feature-flags: ...@@ -617,54 +622,26 @@ rspec:feature-flags:
run_timed_command "bundle exec scripts/used-feature-flags"; run_timed_command "bundle exec scripts/used-feature-flags";
fi fi
rspec:skipped-flaky-tests-report: rspec:flaky-tests-report:
extends: extends:
- .default-retry - .default-retry
- .rails:rules:skipped-flaky-tests-report - .rails:rules:flaky-tests-report
image: ruby:2.7-alpine
stage: post-test stage: post-test
# We cannot use needs since it would mean needing 84 jobs (since most are parallelized) # We cannot use needs since it would mean needing 84 jobs (since most are parallelized)
# so we use `dependencies` here. # so we use `dependencies` here.
dependencies: dependencies: !reference ["rspec:coverage", "dependencies"]
# FOSS/EE jobs
- rspec migration pg12
- rspec unit pg12
- rspec integration pg12
- rspec system pg12
# FOSS/EE minimal jobs
- rspec migration pg12 minimal
- rspec unit pg12 minimal
- rspec integration pg12 minimal
- rspec system pg12 minimal
# EE jobs
- rspec-ee migration pg12
- rspec-ee unit pg12
- rspec-ee integration pg12
- rspec-ee system pg12
# EE minimal jobs
- rspec-ee migration pg12 minimal
- rspec-ee unit pg12 minimal
- rspec-ee integration pg12 minimal
- rspec-ee system pg12 minimal
# Geo jobs
- rspec-ee unit pg12 geo
- rspec-ee integration pg12 geo
- rspec-ee system pg12 geo
# Geo minimal jobs
- rspec-ee unit pg12 geo minimal
- rspec-ee integration pg12 geo minimal
- rspec-ee system pg12 geo minimal
variables: variables:
SKIPPED_FLAKY_TESTS_REPORT: skipped_flaky_tests_report.txt SKIPPED_FLAKY_TESTS_REPORT_PATH: rspec/flaky/skipped_flaky_tests_report.txt
RETRIED_TESTS_REPORT_PATH: rspec/flaky/retried_tests_report.txt
before_script: before_script:
- 'echo "SKIP_FLAKY_TESTS_AUTOMATICALLY: $SKIP_FLAKY_TESTS_AUTOMATICALLY"' - source scripts/utils.sh
- mkdir -p rspec_flaky - source scripts/rspec_helpers.sh
script: script:
- find rspec_flaky/ -type f -name 'skipped_flaky_tests_*_report.txt' -exec cat {} + >> "${SKIPPED_FLAKY_TESTS_REPORT}" - generate_flaky_tests_reports
artifacts: artifacts:
expire_in: 31d expire_in: 31d
paths: paths:
- ${SKIPPED_FLAKY_TESTS_REPORT} - rspec/
# EE/FOSS: default refs (MRs, default branch, schedules) jobs # # EE/FOSS: default refs (MRs, default branch, schedules) jobs #
####################################################### #######################################################
......
...@@ -121,9 +121,6 @@ ...@@ -121,9 +121,6 @@
.if-security-pipeline-merge-result: &if-security-pipeline-merge-result .if-security-pipeline-merge-result: &if-security-pipeline-merge-result
if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH && $CI_PROJECT_NAMESPACE == "gitlab-org/security" && $GITLAB_USER_LOGIN == "gitlab-release-tools-bot"' if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH && $CI_PROJECT_NAMESPACE == "gitlab-org/security" && $GITLAB_USER_LOGIN == "gitlab-release-tools-bot"'
.if-skip-flaky-tests-automatically: &if-skip-flaky-tests-automatically
if: '$SKIP_FLAKY_TESTS_AUTOMATICALLY == "true"'
#################### ####################
# Changes patterns # # Changes patterns #
#################### ####################
...@@ -1436,13 +1433,16 @@ ...@@ -1436,13 +1433,16 @@
when: never when: never
- changes: *code-backstage-patterns - changes: *code-backstage-patterns
.rails:rules:skipped-flaky-tests-report: .rails:rules:flaky-tests-report:
rules: rules:
- <<: *if-not-ee - <<: *if-not-ee
when: never when: never
- <<: *if-skip-flaky-tests-automatically - if: '$SKIP_FLAKY_TESTS_AUTOMATICALLY == "true" || $RETRY_FAILED_TESTS_IN_NEW_PROCESS == "true"'
changes: *code-backstage-patterns changes: *code-backstage-patterns
- changes: *ci-patterns when: always
- if: '$SKIP_FLAKY_TESTS_AUTOMATICALLY == "true" || $RETRY_FAILED_TESTS_IN_NEW_PROCESS == "true"'
changes: *ci-patterns
when: always
######################### #########################
# Static analysis rules # # Static analysis rules #
......
...@@ -6,8 +6,7 @@ ...@@ -6,8 +6,7 @@
expire_in: 31d expire_in: 31d
paths: paths:
- knapsack/ - knapsack/
- rspec_flaky/ - rspec/
- rspec_profiling/
- crystalball/ - crystalball/
retrieve-tests-metadata: retrieve-tests-metadata:
...@@ -44,6 +43,6 @@ update-tests-metadata: ...@@ -44,6 +43,6 @@ update-tests-metadata:
script: script:
- run_timed_command "retry gem install fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document" - run_timed_command "retry gem install fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document"
- source ./scripts/rspec_helpers.sh - source ./scripts/rspec_helpers.sh
- test -f rspec_flaky/report-suite.json || echo -e "\e[31m" 'Consider add ~"pipeline:run-all-rspec" to run full rspec jobs' "\e[0m" - test -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" || echo -e "\e[31m" 'Consider add ~"pipeline:run-all-rspec" to run full rspec jobs' "\e[0m"
- update_tests_metadata - update_tests_metadata
- update_tests_mapping - update_tests_mapping
...@@ -62,7 +62,7 @@ RspecProfiling.configure do |config| ...@@ -62,7 +62,7 @@ RspecProfiling.configure do |config|
config.collector = RspecProfilingExt::Collectors::CSVWithTimestamps config.collector = RspecProfilingExt::Collectors::CSVWithTimestamps
config.csv_path = -> do config.csv_path = -> do
prefix = "#{ENV['CI_JOB_NAME']}-".gsub(%r{[ /]}, '-') if ENV['CI_JOB_NAME'] prefix = "#{ENV['CI_JOB_NAME']}-".gsub(%r{[ /]}, '-') if ENV['CI_JOB_NAME']
"rspec_profiling/#{prefix}#{Time.now.to_i}-#{SecureRandom.hex(8)}-rspec-data.csv" "#{ENV['RSPEC_PROFILING_FOLDER_PATH']}/#{prefix}#{Time.now.to_i}-#{SecureRandom.hex(8)}-rspec-data.csv"
end end
end end
end end
...@@ -170,10 +170,19 @@ After that, the next pipeline uses the up-to-date `knapsack/report-master.json` ...@@ -170,10 +170,19 @@ After that, the next pipeline uses the up-to-date `knapsack/report-master.json`
### Flaky tests ### Flaky tests
#### Automatic skipping of flaky tests
Tests that are [known to be flaky](testing_guide/flaky_tests.md#automatic-retries-and-flaky-tests-detection) are Tests that are [known to be flaky](testing_guide/flaky_tests.md#automatic-retries-and-flaky-tests-detection) are
skipped unless the `$SKIP_FLAKY_TESTS_AUTOMATICALLY` variable is set to `false` or if the `~"pipeline:run-flaky-tests"` skipped unless the `$SKIP_FLAKY_TESTS_AUTOMATICALLY` variable is set to `false` or if the `~"pipeline:run-flaky-tests"`
label is set on the MR. label is set on the MR.
#### Automatic retry of failing tests in a separate process
When the `$RETRY_FAILED_TESTS_IN_NEW_PROCESS` variable is set to `true`, RSpec tests that failed are automatically retried once in a separate
RSpec process. The goal is to get rid of most side-effects from previous tests that may lead to a subsequent test failure.
We keep track of retried tests in the `$RETRIED_TESTS_REPORT_FILE` file saved as artifact by the `rspec:flaky-tests-report` job.
### Monitoring ### Monitoring
The GitLab test suite is [monitored](performance.md#rspec-profiling) for the `main` branch, and any branch The GitLab test suite is [monitored](performance.md#rspec-profiling) for the `main` branch, and any branch
......
...@@ -43,4 +43,4 @@ def insert_data(path) ...@@ -43,4 +43,4 @@ def insert_data(path)
end end
end end
insert_data('rspec_profiling') if ENV['RSPEC_PROFILING_POSTGRES_URL'].present? insert_data(ENV['RSPEC_PROFILING_FOLDER_PATH']) if ENV['RSPEC_PROFILING_POSTGRES_URL'].present?
#!/usr/bin/env bash #!/usr/bin/env bash
function retrieve_tests_metadata() { function retrieve_tests_metadata() {
mkdir -p $(dirname "$KNAPSACK_RSPEC_SUITE_REPORT_PATH") $(dirname "$FLAKY_RSPEC_SUITE_REPORT_PATH") rspec_profiling/ mkdir -p $(dirname "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}") $(dirname "${FLAKY_RSPEC_SUITE_REPORT_PATH}") "${RSPEC_PROFILING_FOLDER_PATH}"
if [[ -n "${RETRIEVE_TESTS_METADATA_FROM_PAGES}" ]]; then if [[ -n "${RETRIEVE_TESTS_METADATA_FROM_PAGES}" ]]; then
if [[ ! -f "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" ]]; then if [[ ! -f "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" ]]; then
curl --location -o "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" "https://gitlab-org.gitlab.io/gitlab/${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" curl --location -o "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" "https://gitlab-org.gitlab.io/gitlab/${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" ||
echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"
fi fi
if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then
curl --location -o "${FLAKY_RSPEC_SUITE_REPORT_PATH}" "https://gitlab-org.gitlab.io/gitlab/${FLAKY_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}" curl --location -o "${FLAKY_RSPEC_SUITE_REPORT_PATH}" "https://gitlab-org.gitlab.io/gitlab/${FLAKY_RSPEC_SUITE_REPORT_PATH}" ||
curl --location -o "${FLAKY_RSPEC_SUITE_REPORT_PATH}" "https://gitlab-org.gitlab.io/gitlab/rspec_flaky/report-suite.json" || # temporary back-compat
echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
fi fi
else else
# ${CI_DEFAULT_BRANCH} might not be master in other forks but we want to # ${CI_DEFAULT_BRANCH} might not be master in other forks but we want to
...@@ -31,7 +34,14 @@ function retrieve_tests_metadata() { ...@@ -31,7 +34,14 @@ function retrieve_tests_metadata() {
fi fi
if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then
scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${FLAKY_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}" scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ||
scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "rspec_flaky/report-suite.json" || # temporary back-compat
echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
# temporary back-compat
if [[ -f "rspec_flaky/report-suite.json" ]]; then
mv "rspec_flaky/report-suite.json" "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
fi
fi fi
else else
echo "test_metadata_job_id couldn't be found!" echo "test_metadata_job_id couldn't be found!"
...@@ -42,21 +52,24 @@ function retrieve_tests_metadata() { ...@@ -42,21 +52,24 @@ function retrieve_tests_metadata() {
} }
function update_tests_metadata() { function update_tests_metadata() {
local rspec_flaky_folder_path="$(dirname "${FLAKY_RSPEC_SUITE_REPORT_PATH}")/"
local knapsack_folder_path="$(dirname "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}")/"
echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"
scripts/merge-reports "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" knapsack/rspec*.json scripts/merge-reports "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" ${knapsack_folder_path}rspec*.json
rm -f knapsack/rspec*.json
export FLAKY_RSPEC_GENERATE_REPORT="true" export FLAKY_RSPEC_GENERATE_REPORT="true"
scripts/merge-reports "${FLAKY_RSPEC_SUITE_REPORT_PATH}" rspec_flaky/all_*.json scripts/merge-reports "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ${rspec_flaky_folder_path}all_*.json
scripts/flaky_examples/prune-old-flaky-examples "${FLAKY_RSPEC_SUITE_REPORT_PATH}" scripts/flaky_examples/prune-old-flaky-examples "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json
if [[ "$CI_PIPELINE_SOURCE" == "schedule" ]]; then if [[ "$CI_PIPELINE_SOURCE" == "schedule" ]]; then
scripts/insert-rspec-profiling-data scripts/insert-rspec-profiling-data
else else
echo "Not inserting profiling data as the pipeline is not a scheduled one." echo "Not inserting profiling data as the pipeline is not a scheduled one."
fi fi
cleanup_individual_job_reports
} }
function retrieve_tests_mapping() { function retrieve_tests_mapping() {
...@@ -158,12 +171,20 @@ function retrieve_previous_failed_tests() { ...@@ -158,12 +171,20 @@ function retrieve_previous_failed_tests() {
scripts/failed_tests.rb --previous-tests-report-path "${pipeline_report_path}" --output-directory "${directory_for_output_reports}" --rspec-pg-regex "${rspec_pg_regex}" --rspec-ee-pg-regex "${rspec_ee_pg_regex}" scripts/failed_tests.rb --previous-tests-report-path "${pipeline_report_path}" --output-directory "${directory_for_output_reports}" --rspec-pg-regex "${rspec_pg_regex}" --rspec-ee-pg-regex "${rspec_ee_pg_regex}"
} }
function rspec_simple_job() { function rspec_args() {
local rspec_opts="${1}" local rspec_opts="${1}"
local junit_report_file="${2:-${JUNIT_RESULT_FILE}}"
echo "-Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out ${junit_report_file} ${rspec_opts}"
}
function rspec_simple_job() {
export NO_KNAPSACK="1" export NO_KNAPSACK="1"
eval "bin/rspec -Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}" local rspec_cmd="bin/rspec $(rspec_args "${1}" "${2}")"
echoinfo "Running RSpec command: ${rspec_cmd}"
eval "${rspec_cmd}"
} }
function rspec_db_library_code() { function rspec_db_library_code() {
...@@ -172,6 +193,24 @@ function rspec_db_library_code() { ...@@ -172,6 +193,24 @@ function rspec_db_library_code() {
rspec_simple_job "-- ${db_files}" rspec_simple_job "-- ${db_files}"
} }
function debug_rspec_variables() {
echoinfo "SKIP_FLAKY_TESTS_AUTOMATICALLY: ${SKIP_FLAKY_TESTS_AUTOMATICALLY}"
echoinfo "RETRY_FAILED_TESTS_IN_NEW_PROCESS: ${RETRY_FAILED_TESTS_IN_NEW_PROCESS}"
echoinfo "KNAPSACK_GENERATE_REPORT: ${KNAPSACK_GENERATE_REPORT}"
echoinfo "FLAKY_RSPEC_GENERATE_REPORT: ${FLAKY_RSPEC_GENERATE_REPORT}"
echoinfo "KNAPSACK_TEST_FILE_PATTERN: ${KNAPSACK_TEST_FILE_PATTERN}"
echoinfo "KNAPSACK_LOG_LEVEL: ${KNAPSACK_LOG_LEVEL}"
echoinfo "KNAPSACK_REPORT_PATH: ${KNAPSACK_REPORT_PATH}"
echoinfo "FLAKY_RSPEC_SUITE_REPORT_PATH: ${FLAKY_RSPEC_SUITE_REPORT_PATH}"
echoinfo "FLAKY_RSPEC_REPORT_PATH: ${FLAKY_RSPEC_REPORT_PATH}"
echoinfo "NEW_FLAKY_RSPEC_REPORT_PATH: ${NEW_FLAKY_RSPEC_REPORT_PATH}"
echoinfo "SKIPPED_FLAKY_TESTS_REPORT_PATH: ${SKIPPED_FLAKY_TESTS_REPORT_PATH}"
echoinfo "RETRIED_TESTS_REPORT_PATH: ${RETRIED_TESTS_REPORT_PATH}"
}
function rspec_paralellized_job() { function rspec_paralellized_job() {
read -ra job_name <<< "${CI_JOB_NAME}" read -ra job_name <<< "${CI_JOB_NAME}"
local test_tool="${job_name[0]}" local test_tool="${job_name[0]}"
...@@ -179,6 +218,9 @@ function rspec_paralellized_job() { ...@@ -179,6 +218,9 @@ function rspec_paralellized_job() {
local report_name=$(echo "${CI_JOB_NAME}" | sed -E 's|[/ ]|_|g') # e.g. 'rspec unit pg12 1/24' would become 'rspec_unit_pg12_1_24' local report_name=$(echo "${CI_JOB_NAME}" | sed -E 's|[/ ]|_|g') # e.g. 'rspec unit pg12 1/24' would become 'rspec_unit_pg12_1_24'
local rspec_opts="${1}" local rspec_opts="${1}"
local spec_folder_prefixes="" local spec_folder_prefixes=""
local rspec_flaky_folder_path="$(dirname "${FLAKY_RSPEC_SUITE_REPORT_PATH}")/"
local knapsack_folder_path="$(dirname "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}")/"
local rspec_run_status=0
if [[ "${test_tool}" =~ "-ee" ]]; then if [[ "${test_tool}" =~ "-ee" ]]; then
spec_folder_prefixes="'ee/'" spec_folder_prefixes="'ee/'"
...@@ -193,7 +235,7 @@ function rspec_paralellized_job() { ...@@ -193,7 +235,7 @@ function rspec_paralellized_job() {
fi fi
export KNAPSACK_LOG_LEVEL="debug" export KNAPSACK_LOG_LEVEL="debug"
export KNAPSACK_REPORT_PATH="knapsack/${report_name}_report.json" export KNAPSACK_REPORT_PATH="${knapsack_folder_path}${report_name}_report.json"
# There's a bug where artifacts are sometimes not downloaded. Since specs can run without the Knapsack report, we can # There's a bug where artifacts are sometimes not downloaded. Since specs can run without the Knapsack report, we can
# handle the missing artifact gracefully here. See https://gitlab.com/gitlab-org/gitlab/-/issues/212349. # handle the missing artifact gracefully here. See https://gitlab.com/gitlab-org/gitlab/-/issues/212349.
...@@ -208,16 +250,14 @@ function rspec_paralellized_job() { ...@@ -208,16 +250,14 @@ function rspec_paralellized_job() {
export KNAPSACK_TEST_FILE_PATTERN="${pattern}" export KNAPSACK_TEST_FILE_PATTERN="${pattern}"
fi fi
echo "KNAPSACK_TEST_FILE_PATTERN: ${KNAPSACK_TEST_FILE_PATTERN}" export FLAKY_RSPEC_REPORT_PATH="${rspec_flaky_folder_path}all_${report_name}_report.json"
echo "SKIP_FLAKY_TESTS_AUTOMATICALLY: ${SKIP_FLAKY_TESTS_AUTOMATICALLY}" export NEW_FLAKY_RSPEC_REPORT_PATH="${rspec_flaky_folder_path}new_${report_name}_report.json"
export SKIPPED_FLAKY_TESTS_REPORT_PATH="${rspec_flaky_folder_path}skipped_flaky_tests_${report_name}_report.txt"
export RETRIED_TESTS_REPORT_PATH="${rspec_flaky_folder_path}retried_tests_${report_name}_report.txt"
if [[ -d "ee/" ]]; then if [[ -d "ee/" ]]; then
export KNAPSACK_GENERATE_REPORT="true" export KNAPSACK_GENERATE_REPORT="true"
export FLAKY_RSPEC_GENERATE_REPORT="true" export FLAKY_RSPEC_GENERATE_REPORT="true"
export SUITE_FLAKY_RSPEC_REPORT_PATH="${FLAKY_RSPEC_SUITE_REPORT_PATH}"
export FLAKY_RSPEC_REPORT_PATH="rspec_flaky/all_${report_name}_report.json"
export NEW_FLAKY_RSPEC_REPORT_PATH="rspec_flaky/new_${report_name}_report.json"
export SKIPPED_FLAKY_TESTS_REPORT_PATH="rspec_flaky/skipped_flaky_tests_${report_name}_report.txt"
if [[ ! -f $FLAKY_RSPEC_REPORT_PATH ]]; then if [[ ! -f $FLAKY_RSPEC_REPORT_PATH ]]; then
echo "{}" > "${FLAKY_RSPEC_REPORT_PATH}" echo "{}" > "${FLAKY_RSPEC_REPORT_PATH}"
...@@ -228,19 +268,44 @@ function rspec_paralellized_job() { ...@@ -228,19 +268,44 @@ function rspec_paralellized_job() {
fi fi
fi fi
debug_rspec_variables
mkdir -p tmp/memory_test mkdir -p tmp/memory_test
export MEMORY_TEST_PATH="tmp/memory_test/${report_name}_memory.csv" export MEMORY_TEST_PATH="tmp/memory_test/${report_name}_memory.csv"
local rspec_args="-Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}"
if [[ -n $RSPEC_TESTS_MAPPING_ENABLED ]]; then if [[ -n $RSPEC_TESTS_MAPPING_ENABLED ]]; then
tooling/bin/parallel_rspec --rspec_args "${rspec_args}" --filter "tmp/matching_tests.txt" tooling/bin/parallel_rspec --rspec_args "$(rspec_args)" --filter "tmp/matching_tests.txt" || rspec_run_status=$?
else
tooling/bin/parallel_rspec --rspec_args "$(rspec_args "${rspec_opts}")" || rspec_run_status=$?
fi
echoinfo "RSpec exited with ${rspec_run_status}."
# Experiment to retry failed examples in a new RSpec process: https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/1148
if [[ $rspec_run_status -ne 0 ]]; then
if [[ "${RETRY_FAILED_TESTS_IN_NEW_PROCESS}" == "true" ]]; then
# Keep track of the tests that are retried, later consolidated in a single file by the `rspec:flaky-tests-report` job
local failed_examples=$(grep " failed" ${RSPEC_LAST_RUN_RESULTS_FILE})
echo "${CI_JOB_URL}" > "${RETRIED_TESTS_REPORT_PATH}"
echo $failed_examples >> "${RETRIED_TESTS_REPORT_PATH}"
echoinfo "Retrying the failing examples in a new RSpec proces..."
install_junit_merge_gem
# Retry only the tests that failed on first try
rspec_simple_job "--only-failures" "${JUNIT_RETRY_FILE}"
rspec_run_status=$?
# Merge the JUnit report from retry into the first-try report
junit_merge "${JUNIT_RETRY_FILE}" "${JUNIT_RESULT_FILE}"
fi
else else
tooling/bin/parallel_rspec --rspec_args "${rspec_args}" echosuccess "No examples to retry, congrats!"
fi fi
date exit $rspec_run_status
} }
function rspec_rerun_previous_failed_tests() { function rspec_rerun_previous_failed_tests() {
...@@ -330,3 +395,30 @@ function generate_frontend_fixtures_mapping() { ...@@ -330,3 +395,30 @@ function generate_frontend_fixtures_mapping() {
rspec_simple_job "--pattern \"${pattern}\"" rspec_simple_job "--pattern \"${pattern}\""
} }
function cleanup_individual_job_reports() {
local rspec_flaky_folder_path="$(dirname "${FLAKY_RSPEC_SUITE_REPORT_PATH}")/"
local knapsack_folder_path="$(dirname "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}")/"
rm -rf ${knapsack_folder_path}rspec*.json \
${rspec_flaky_folder_path}all_*.json \
${rspec_flaky_folder_path}new_*.json \
${rspec_flaky_folder_path}skipped_flaky_tests_*_report.txt \
${rspec_flaky_folder_path}retried_tests_*_report.txt \
${RSPEC_LAST_RUN_RESULTS_FILE} \
${RSPEC_PROFILING_FOLDER_PATH}/**/*
rmdir ${RSPEC_PROFILING_FOLDER_PATH} || true
}
function generate_flaky_tests_reports() {
local rspec_flaky_folder_path="$(dirname "${FLAKY_RSPEC_SUITE_REPORT_PATH}")/"
debug_rspec_variables
mkdir -p ${rspec_flaky_folder_path}
find ${rspec_flaky_folder_path} -type f -name 'skipped_flaky_tests_*_report.txt' -exec cat {} + >> "${SKIPPED_FLAKY_TESTS_REPORT_PATH}"
find ${rspec_flaky_folder_path} -type f -name 'retried_tests_*_report.txt' -exec cat {} + >> "${RETRIED_TESTS_REPORT_PATH}"
cleanup_individual_job_reports
}
...@@ -64,16 +64,20 @@ function setup_db() { ...@@ -64,16 +64,20 @@ function setup_db() {
} }
function install_api_client_dependencies_with_apk() { function install_api_client_dependencies_with_apk() {
apk add --update openssl curl jq run_timed_command "apk add --update openssl curl jq"
} }
function install_gitlab_gem() { function install_gitlab_gem() {
gem install httparty --no-document --version 0.18.1 run_timed_command "gem install httparty --no-document --version 0.18.1"
gem install gitlab --no-document --version 4.17.0 run_timed_command "gem install gitlab --no-document --version 4.17.0"
} }
function install_tff_gem() { function install_tff_gem() {
gem install test_file_finder --version 0.1.1 run_timed_command "gem install test_file_finder --no-document --version 0.1.1"
}
function install_junit_merge_gem() {
run_timed_command "gem install junit_merge --no-document --version 0.1.2"
} }
function run_timed_command() { function run_timed_command() {
......
...@@ -112,10 +112,10 @@ RSpec.configure do |config| ...@@ -112,10 +112,10 @@ RSpec.configure do |config|
# falling back to all tests when there is no `:focus` example. # falling back to all tests when there is no `:focus` example.
config.filter_run focus: true config.filter_run focus: true
config.run_all_when_everything_filtered = true config.run_all_when_everything_filtered = true
end
# Re-run failures locally with `--only-failures` # Re-run failures locally with `--only-failures`
config.example_status_persistence_file_path = './spec/examples.txt' config.example_status_persistence_file_path = ENV.fetch('RSPEC_LAST_RUN_RESULTS_FILE', './spec/examples.txt')
end
config.define_derived_metadata(file_path: %r{(ee)?/spec/.+_spec\.rb\z}) do |metadata| config.define_derived_metadata(file_path: %r{(ee)?/spec/.+_spec\.rb\z}) do |metadata|
location = metadata[:location] location = metadata[:location]
...@@ -208,7 +208,9 @@ RSpec.configure do |config| ...@@ -208,7 +208,9 @@ RSpec.configure do |config|
config.exceptions_to_hard_fail = [DeprecationToolkitEnv::DeprecationBehaviors::SelectiveRaise::RaiseDisallowedDeprecation] config.exceptions_to_hard_fail = [DeprecationToolkitEnv::DeprecationBehaviors::SelectiveRaise::RaiseDisallowedDeprecation]
end end
if ENV['FLAKY_RSPEC_GENERATE_REPORT'] require_relative '../tooling/rspec_flaky/config'
if RspecFlaky::Config.generate_report?
require_relative '../tooling/rspec_flaky/listener' require_relative '../tooling/rspec_flaky/listener'
config.reporter.register_listener( config.reporter.register_listener(
......
...@@ -4,14 +4,14 @@ return unless ENV['CI'] ...@@ -4,14 +4,14 @@ return unless ENV['CI']
return if ENV['SKIP_FLAKY_TESTS_AUTOMATICALLY'] == "false" return if ENV['SKIP_FLAKY_TESTS_AUTOMATICALLY'] == "false"
return if ENV['CI_MERGE_REQUEST_LABELS'].to_s.include?('pipeline:run-flaky-tests') return if ENV['CI_MERGE_REQUEST_LABELS'].to_s.include?('pipeline:run-flaky-tests')
require_relative '../../tooling/rspec_flaky/config'
require_relative '../../tooling/rspec_flaky/report' require_relative '../../tooling/rspec_flaky/report'
RSpec.configure do |config| RSpec.configure do |config|
$flaky_test_example_ids = begin # rubocop:disable Style/GlobalVars $flaky_test_example_ids = begin # rubocop:disable Style/GlobalVars
raise "$SUITE_FLAKY_RSPEC_REPORT_PATH is empty." if ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'].to_s.empty? raise "#{RspecFlaky::Config.suite_flaky_examples_report_path} doesn't exist" unless File.exist?(RspecFlaky::Config.suite_flaky_examples_report_path)
raise "#{ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']} doesn't exist" unless File.exist?(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'])
RspecFlaky::Report.load(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']).map { |_, flaky_test_data| flaky_test_data.to_h[:example_id] } RspecFlaky::Report.load(RspecFlaky::Config.suite_flaky_examples_report_path).map { |_, flaky_test_data| flaky_test_data.to_h[:example_id] }
rescue => e # rubocop:disable Style/RescueStandardError rescue => e # rubocop:disable Style/RescueStandardError
puts e puts e
[] []
...@@ -29,8 +29,9 @@ RSpec.configure do |config| ...@@ -29,8 +29,9 @@ RSpec.configure do |config|
end end
config.after(:suite) do config.after(:suite) do
next unless ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'] next unless RspecFlaky::Config.skipped_flaky_tests_report_path
next if $skipped_flaky_tests_report.empty? # rubocop:disable Style/GlobalVars
File.write(ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'], "#{$skipped_flaky_tests_report.join("\n")}\n") # rubocop:disable Style/GlobalVars File.write(RspecFlaky::Config.skipped_flaky_tests_report_path, "#{ENV['CI_JOB_URL']}\n#{$skipped_flaky_tests_report.join("\n")}\n\n") # rubocop:disable Style/GlobalVars
end end
end end
...@@ -18,7 +18,9 @@ RSpec.describe Tooling::ParallelRSpecRunner do # rubocop:disable RSpec/FilePath ...@@ -18,7 +18,9 @@ RSpec.describe Tooling::ParallelRSpecRunner do # rubocop:disable RSpec/FilePath
allow(File).to receive(:exist?).with(filter_tests_file).and_return(true) allow(File).to receive(:exist?).with(filter_tests_file).and_return(true)
allow(File).to receive(:read).and_call_original allow(File).to receive(:read).and_call_original
allow(File).to receive(:read).with(filter_tests_file).and_return(filter_tests) allow(File).to receive(:read).with(filter_tests_file).and_return(filter_tests)
allow(subject).to receive(:exec) allow(Process).to receive(:spawn)
allow(Process).to receive(:wait)
allow(Process).to receive(:last_status).and_return(double(exitstatus: 0))
end end
subject { described_class.new(allocator: allocator, filter_tests_file: filter_tests_file, rspec_args: rspec_args) } subject { described_class.new(allocator: allocator, filter_tests_file: filter_tests_file, rspec_args: rspec_args) }
...@@ -86,7 +88,7 @@ RSpec.describe Tooling::ParallelRSpecRunner do # rubocop:disable RSpec/FilePath ...@@ -86,7 +88,7 @@ RSpec.describe Tooling::ParallelRSpecRunner do # rubocop:disable RSpec/FilePath
end end
def expect_command(cmd) def expect_command(cmd)
expect(subject).to receive(:exec).with(*cmd) expect(Process).to receive(:spawn).with(*cmd)
end end
end end
end end
...@@ -11,9 +11,10 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do ...@@ -11,9 +11,10 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do
before do before do
# Stub these env variables otherwise specs don't behave the same on the CI # Stub these env variables otherwise specs don't behave the same on the CI
stub_env('FLAKY_RSPEC_GENERATE_REPORT', nil) stub_env('FLAKY_RSPEC_GENERATE_REPORT', nil)
stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', nil) stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', nil)
stub_env('FLAKY_RSPEC_REPORT_PATH', nil) stub_env('FLAKY_RSPEC_REPORT_PATH', nil)
stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', nil) stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', nil)
stub_env('SKIPPED_FLAKY_TESTS_REPORT_PATH', nil)
# Ensure the behavior is the same locally and on CI (where Rails is defined since we run this test as part of the whole suite), i.e. Rails isn't defined # Ensure the behavior is the same locally and on CI (where Rails is defined since we run this test as part of the whole suite), i.e. Rails isn't defined
allow(described_class).to receive(:rails_path).and_wrap_original do |method, path| allow(described_class).to receive(:rails_path).and_wrap_original do |method, path|
path path
...@@ -51,15 +52,15 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do ...@@ -51,15 +52,15 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do
end end
describe '.suite_flaky_examples_report_path' do describe '.suite_flaky_examples_report_path' do
context "when ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] is not set" do context "when ENV['FLAKY_RSPEC_SUITE_REPORT_PATH'] is not set" do
it 'returns the default path' do it 'returns the default path' do
expect(described_class.suite_flaky_examples_report_path).to eq('rspec_flaky/suite-report.json') expect(described_class.suite_flaky_examples_report_path).to eq('rspec/flaky/suite-report.json')
end end
end end
context "when ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] is set" do context "when ENV['FLAKY_RSPEC_SUITE_REPORT_PATH'] is set" do
before do before do
stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', 'foo/suite-report.json') stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', 'foo/suite-report.json')
end end
it 'returns the value of the env variable' do it 'returns the value of the env variable' do
...@@ -71,7 +72,7 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do ...@@ -71,7 +72,7 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do
describe '.flaky_examples_report_path' do describe '.flaky_examples_report_path' do
context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is not set" do context "when ENV['FLAKY_RSPEC_REPORT_PATH'] is not set" do
it 'returns the default path' do it 'returns the default path' do
expect(described_class.flaky_examples_report_path).to eq('rspec_flaky/report.json') expect(described_class.flaky_examples_report_path).to eq('rspec/flaky/report.json')
end end
end end
...@@ -89,7 +90,7 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do ...@@ -89,7 +90,7 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do
describe '.new_flaky_examples_report_path' do describe '.new_flaky_examples_report_path' do
context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is not set" do context "when ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] is not set" do
it 'returns the default path' do it 'returns the default path' do
expect(described_class.new_flaky_examples_report_path).to eq('rspec_flaky/new-report.json') expect(described_class.new_flaky_examples_report_path).to eq('rspec/flaky/new-report.json')
end end
end end
...@@ -103,4 +104,22 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do ...@@ -103,4 +104,22 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do
end end
end end
end end
describe '.skipped_flaky_tests_report_path' do
context "when ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'] is not set" do
it 'returns the default path' do
expect(described_class.skipped_flaky_tests_report_path).to eq('rspec/flaky/skipped_flaky_tests_report.txt')
end
end
context "when ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'] is set" do
before do
stub_env('SKIPPED_FLAKY_TESTS_REPORT_PATH', 'foo/skipped_flaky_tests_report.txt')
end
it 'returns the value of the env variable' do
expect(described_class.skipped_flaky_tests_report_path).to eq('foo/skipped_flaky_tests_report.txt')
end
end
end
end end
...@@ -54,7 +54,7 @@ RSpec.describe RspecFlaky::Listener, :aggregate_failures do ...@@ -54,7 +54,7 @@ RSpec.describe RspecFlaky::Listener, :aggregate_failures do
before do before do
# Stub these env variables otherwise specs don't behave the same on the CI # Stub these env variables otherwise specs don't behave the same on the CI
stub_env('CI_JOB_URL', nil) stub_env('CI_JOB_URL', nil)
stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', nil) stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', nil)
end end
describe '#initialize' do describe '#initialize' do
...@@ -73,11 +73,11 @@ RSpec.describe RspecFlaky::Listener, :aggregate_failures do ...@@ -73,11 +73,11 @@ RSpec.describe RspecFlaky::Listener, :aggregate_failures do
it_behaves_like 'a valid Listener instance' it_behaves_like 'a valid Listener instance'
end end
context 'when SUITE_FLAKY_RSPEC_REPORT_PATH is set' do context 'when FLAKY_RSPEC_SUITE_REPORT_PATH is set' do
let(:report_file_path) { 'foo/report.json' } let(:report_file_path) { 'foo/report.json' }
before do before do
stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', report_file_path) stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', report_file_path)
end end
context 'and report file exists' do context 'and report file exists' do
......
...@@ -16,4 +16,4 @@ OptionParser.new do |opts| ...@@ -16,4 +16,4 @@ OptionParser.new do |opts|
end end
end.parse! end.parse!
Tooling::ParallelRSpecRunner.run(**options) exit Tooling::ParallelRSpecRunner.run(**options)
...@@ -38,12 +38,8 @@ module Tooling ...@@ -38,12 +38,8 @@ module Tooling
Knapsack.logger.info tests_to_run Knapsack.logger.info tests_to_run
Knapsack.logger.info Knapsack.logger.info
if tests_to_run.empty? Process.wait Process.spawn(*rspec_command)
Knapsack.logger.info 'No tests to run on this node, exiting.' Process.last_status.exitstatus
return
end
exec(*rspec_command)
end end
private private
......
...@@ -7,15 +7,19 @@ module RspecFlaky ...@@ -7,15 +7,19 @@ module RspecFlaky
end end
def self.suite_flaky_examples_report_path def self.suite_flaky_examples_report_path
ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] || rails_path("rspec_flaky/suite-report.json") ENV['FLAKY_RSPEC_SUITE_REPORT_PATH'] || rails_path("rspec/flaky/suite-report.json")
end end
def self.flaky_examples_report_path def self.flaky_examples_report_path
ENV['FLAKY_RSPEC_REPORT_PATH'] || rails_path("rspec_flaky/report.json") ENV['FLAKY_RSPEC_REPORT_PATH'] || rails_path("rspec/flaky/report.json")
end end
def self.new_flaky_examples_report_path def self.new_flaky_examples_report_path
ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] || rails_path("rspec_flaky/new-report.json") ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] || rails_path("rspec/flaky/new-report.json")
end
def self.skipped_flaky_tests_report_path
ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'] || rails_path("rspec/flaky/skipped_flaky_tests_report.txt")
end end
def self.rails_path(path) def self.rails_path(path)
......
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