Commit 0042c61f authored by Ash McKenzie's avatar Ash McKenzie

Merge branch '339343-execute-related-jests-specs-for-mrs-with-backend-changes' into 'master'

Automatically detect Jest tests to run upon backend changes

See merge request gitlab-org/gitlab!74003
parents ef655b1f e58eaec4
......@@ -72,6 +72,7 @@ variables:
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json
RSPEC_TESTS_MAPPING_PATH: crystalball/mapping.json
RSPEC_PACKED_TESTS_MAPPING_PATH: crystalball/packed-mapping.json
FRONTEND_FIXTURES_MAPPING_PATH: crystalball/frontend_fixtures_mapping.json
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
ELASTIC_URL: "http://elastic:changeme@elasticsearch:9200"
......
......@@ -1694,6 +1694,16 @@
changes: *code-backstage-patterns
when: on_success
.setup:rules:generate-frontend-fixtures-mapping:
rules:
- <<: *if-not-ee
when: never
- <<: *if-dot-com-ee-2-hourly-schedule
- changes:
- ".gitlab/ci/setup.gitlab-ci.yml"
- ".gitlab/ci/test-metadata.gitlab-ci.yml"
- "scripts/rspec_helpers.sh"
.setup:rules:add-jh-folder:
rules:
- <<: *if-not-ee
......
......@@ -68,6 +68,24 @@ verify-tests-yml:
- install_tff_gem
- scripts/verify-tff-mapping
generate-frontend-fixtures-mapping:
extends:
- .setup:rules:generate-frontend-fixtures-mapping
- .use-pg12
- .rails-cache
needs: ["setup-test-env"]
stage: prepare
before_script:
- !reference [.default-before_script, before_script]
- source ./scripts/rspec_helpers.sh
- run_timed_command "scripts/gitaly-test-spawn"
script:
- generate_frontend_fixtures_mapping
artifacts:
expire_in: 7d
paths:
- ${FRONTEND_FIXTURES_MAPPING_PATH}
.detect-test-base:
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7
needs: []
......@@ -78,17 +96,21 @@ verify-tests-yml:
- install_gitlab_gem
- install_tff_gem
- retrieve_tests_mapping
- retrieve_frontend_fixtures_mapping
- |
if [ -n "$CI_MERGE_REQUEST_IID" ]; then
tooling/bin/find_changes ${CHANGES_FILE};
tooling/bin/find_tests ${CHANGES_FILE} ${MATCHED_TESTS_FILE};
echo "related rspec tests: $(cat $MATCHED_TESTS_FILE)";
tooling/bin/find_changes ${CHANGES_FILE} ${MATCHED_TESTS_FILE} ${FRONTEND_FIXTURES_MAPPING_PATH};
echo "Changed files: $(cat $CHANGES_FILE)";
echo "Related rspec tests: $(cat $MATCHED_TESTS_FILE)";
fi
artifacts:
expire_in: 7d
paths:
- ${CHANGES_FILE}
- ${MATCHED_TESTS_FILE}
- ${FRONTEND_FIXTURES_MAPPING_PATH}
detect-tests:
extends:
......
......@@ -8,7 +8,7 @@
- knapsack/
- rspec_flaky/
- rspec_profiling/
- crystalball/packed-mapping.json.gz
- crystalball/
retrieve-tests-metadata:
extends:
......@@ -27,6 +27,7 @@ update-tests-metadata:
stage: post-test
dependencies:
- retrieve-tests-metadata
- generate-frontend-fixtures-mapping
- setup-test-env
- rspec migration pg12
- rspec-all frontend_fixture
......
......@@ -10,6 +10,7 @@ class JobFinder
pipeline_query: {}.freeze,
job_query: {}.freeze
).freeze
MAX_PIPELINES_TO_ITERATE = 200
def initialize(options)
@project = options.delete(:project)
......@@ -41,8 +42,11 @@ class JobFinder
def find_job_with_artifact
return if artifact_path.nil?
client.pipelines(project, pipeline_query_params).auto_paginate do |pipeline|
client.pipelines(project, pipeline_query_params).paginate_with_limit(MAX_PIPELINES_TO_ITERATE) do |pipeline|
$stderr.puts "Iterating over #{pipeline}" # rubocop:disable Style/StderrPuts
client.pipeline_jobs(project, pipeline.id, job_query_params).auto_paginate do |job|
next if job_name && !found_job_by_name?(job)
return job if found_job_with_artifact?(job) # rubocop:disable Cop/AvoidReturnFromBlocks
end
end
......@@ -53,7 +57,7 @@ class JobFinder
def find_job_with_filtered_pipelines
return if pipeline_query.empty?
client.pipelines(project, pipeline_query_params).auto_paginate do |pipeline|
client.pipelines(project, pipeline_query_params).paginate_with_limit(MAX_PIPELINES_TO_ITERATE) do |pipeline|
client.pipeline_jobs(project, pipeline.id, job_query_params).auto_paginate do |job|
return job if found_job_by_name?(job) # rubocop:disable Cop/AvoidReturnFromBlocks
end
......
......@@ -16,17 +16,27 @@ function retrieve_tests_metadata() {
# always target the canonical project here, so the branch must be hardcoded
local project_path="gitlab-org/gitlab"
local artifact_branch="master"
local username="gitlab-bot"
local job_name="update-tests-metadata"
local test_metadata_job_id
# Ruby
test_metadata_job_id=$(scripts/api/get_job_id.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" -q "status=success" -q "ref=${artifact_branch}" -q "username=gitlab-bot" -Q "scope=success" --job-name "update-tests-metadata")
if [[ ! -f "${KNAPSACK_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 "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"
fi
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}"
test_metadata_job_id=$(scripts/api/get_job_id.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" -q "status=success" -q "ref=${artifact_branch}" -q "username=${username}" -Q "scope=success" --job-name "${job_name}")
if [[ -n "${test_metadata_job_id}" ]]; then
echo "test_metadata_job_id: ${test_metadata_job_id}"
if [[ ! -f "${KNAPSACK_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 "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"
fi
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}"
fi
else
echo "test_metadata_job_id couldn't be found!"
echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"
echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
fi
fi
}
......@@ -61,18 +71,63 @@ function retrieve_tests_mapping() {
# always target the canonical project here, so the branch must be hardcoded
local project_path="gitlab-org/gitlab"
local artifact_branch="master"
local username="gitlab-bot"
local job_name="update-tests-metadata"
local test_metadata_with_mapping_job_id
test_metadata_with_mapping_job_id=$(scripts/api/get_job_id.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" -q "status=success" -q "ref=${artifact_branch}" -q "username=gitlab-bot" -Q "scope=success" --job-name "update-tests-metadata" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz")
test_metadata_with_mapping_job_id=$(scripts/api/get_job_id.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" -q "status=success" -q "ref=${artifact_branch}" -q "username=${username}" -Q "scope=success" --job-name "${job_name}")
if [[ ! -f "${RSPEC_PACKED_TESTS_MAPPING_PATH}" ]]; then
(scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_with_mapping_job_id}" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" && gzip -d "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz") || echo "{}" > "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
if [[ -n "${test_metadata_with_mapping_job_id}" ]]; then
echo "test_metadata_with_mapping_job_id: ${test_metadata_with_mapping_job_id}"
if [[ ! -f "${RSPEC_PACKED_TESTS_MAPPING_PATH}" ]]; then
(scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_with_mapping_job_id}" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" && gzip -d "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz") || echo "{}" > "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
fi
else
echo "test_metadata_with_mapping_job_id couldn't be found!"
echo "{}" > "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
fi
fi
scripts/unpack-test-mapping "${RSPEC_PACKED_TESTS_MAPPING_PATH}" "${RSPEC_TESTS_MAPPING_PATH}"
}
function retrieve_frontend_fixtures_mapping() {
mkdir -p $(dirname "$FRONTEND_FIXTURES_MAPPING_PATH")
if [[ -n "${RETRIEVE_TESTS_METADATA_FROM_PAGES}" ]]; then
if [[ ! -f "${FRONTEND_FIXTURES_MAPPING_PATH}" ]]; then
(curl --location -o "${FRONTEND_FIXTURES_MAPPING_PATH}" "https://gitlab-org.gitlab.io/gitlab/${FRONTEND_FIXTURES_MAPPING_PATH}") || echo "{}" > "${FRONTEND_FIXTURES_MAPPING_PATH}"
fi
else
# ${CI_DEFAULT_BRANCH} might not be master in other forks but we want to
# always target the canonical project here, so the branch must be hardcoded
local project_path="gitlab-org/gitlab"
local artifact_branch="master"
local username="gitlab-bot"
local job_name="generate-frontend-fixtures-mapping"
local test_metadata_with_mapping_job_id
# On the MR that introduces 'generate-frontend-fixtures-mapping', we cannot retrieve the file from a master scheduled pipeline, so we take it from a known MR pipeline
if [[ "${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}" == "339343-execute-related-jests-specs-for-mrs-with-backend-changes" ]]; then
test_metadata_with_mapping_job_id=$(scripts/api/get_job_id.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --pipeline-id "414921396" -Q "scope=success" --job-name "${job_name}")
else
test_metadata_with_mapping_job_id=$(scripts/api/get_job_id.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" -q "ref=${artifact_branch}" -q "username=${username}" -Q "scope=success" --job-name "${job_name}")
fi
if [[ $? -eq 0 ]] && [[ -n "${test_metadata_with_mapping_job_id}" ]]; then
echo "test_metadata_with_mapping_job_id: ${test_metadata_with_mapping_job_id}"
if [[ ! -f "${FRONTEND_FIXTURES_MAPPING_PATH}" ]]; then
(scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_with_mapping_job_id}" --artifact-path "${FRONTEND_FIXTURES_MAPPING_PATH}") || echo "{}" > "${FRONTEND_FIXTURES_MAPPING_PATH}"
fi
else
echo "test_metadata_with_mapping_job_id couldn't be found!"
echo "{}" > "${FRONTEND_FIXTURES_MAPPING_PATH}"
fi
fi
}
function update_tests_mapping() {
if ! crystalball_rspec_data_exists; then
echo "No crystalball rspec data found."
......@@ -113,7 +168,7 @@ function rspec_simple_job() {
export NO_KNAPSACK="1"
bin/rspec -Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}
eval "bin/rspec -Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}"
}
function rspec_db_library_code() {
......@@ -256,3 +311,27 @@ function rspec_matched_foss_tests() {
echo "No impacted FOSS rspec tests to run"
fi
}
function generate_frontend_fixtures_mapping() {
local pattern=""
if [[ -d "ee/" ]]; then
pattern=",ee/"
fi
if [[ -d "jh/" ]]; then
pattern="${pattern},jh/"
fi
if [[ -n "${pattern}" ]]; then
pattern="{${pattern}}"
fi
pattern="${pattern}spec/frontend/fixtures/**/*.rb"
export GENERATE_FRONTEND_FIXTURES_MAPPING="true"
mkdir -p $(dirname "$FRONTEND_FIXTURES_MAPPING_PATH")
rspec_simple_job "--pattern \"${pattern}\""
}
# frozen_string_literal: true
return unless ENV['CI']
return unless ENV['GENERATE_FRONTEND_FIXTURES_MAPPING'] == 'true'
RSpec.configure do |config|
config.before(:suite) do
$fixtures_mapping = Hash.new { |h, k| h[k] = [] } # rubocop:disable Style/GlobalVars
end
config.after(:suite) do
next unless ENV['FRONTEND_FIXTURES_MAPPING_PATH']
File.write(ENV['FRONTEND_FIXTURES_MAPPING_PATH'], $fixtures_mapping.to_json) # rubocop:disable Style/GlobalVars
end
end
......@@ -13,6 +13,12 @@ module JavaScriptFixturesHelpers
included do |base|
base.around do |example|
# Don't actually run the example when we're only interested in the `test file -> JSON frontend fixture` mapping
if ENV['GENERATE_FRONTEND_FIXTURES_MAPPING'] == 'true'
$fixtures_mapping[example.metadata[:file_path].delete_prefix('./')] << File.join(fixture_root_path, example.description) # rubocop:disable Style/GlobalVars
next
end
# pick an arbitrary date from the past, so tests are not time dependent
# Also see spec/frontend/__helpers__/fake_date/jest.js
Timecop.freeze(Time.utc(2015, 7, 3, 10)) { example.run }
......
......@@ -3,19 +3,76 @@
require 'gitlab'
gitlab_token = ENV.fetch('PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE', '')
gitlab_endpoint = ENV.fetch('CI_API_V4_URL')
mr_project_path = ENV.fetch('CI_MERGE_REQUEST_PROJECT_PATH')
mr_iid = ENV.fetch('CI_MERGE_REQUEST_IID')
class FindChanges # rubocop:disable Gitlab/NamespacedClass
def initialize(output_file:, matched_tests_file: nil, frontend_fixtures_mapping_path: nil)
@gitlab_token = ENV.fetch('PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE', '')
@gitlab_endpoint = ENV.fetch('CI_API_V4_URL')
@mr_project_path = ENV.fetch('CI_MERGE_REQUEST_PROJECT_PATH')
@mr_iid = ENV.fetch('CI_MERGE_REQUEST_IID')
@output_file = output_file
@matched_tests_file = matched_tests_file
@frontend_fixtures_mapping_path = frontend_fixtures_mapping_path
end
output_file = ARGV.shift
def execute
add_frontend_fixture_files!
File.write(output_file, file_changes.join(' '))
end
private
def add_frontend_fixture_files?
matched_tests_file && frontend_fixtures_mapping_path
end
def add_frontend_fixture_files!
return unless add_frontend_fixture_files?
# If we have a `test file -> JSON frontend fixture` mapping file, we add the files JSON frontend fixtures
# files to the list of changed files so that Jest can automatically run the dependent tests thanks to --findRelatedTests
test_files.each do |test_file|
file_changes.concat(frontend_fixtures_mapping[test_file]) if frontend_fixtures_mapping.key?(test_file)
end
end
def file_changes
@file_changes ||=
if File.exist?(output_file)
File.read(output_file).split(' ')
else
Gitlab.configure do |config|
config.endpoint = gitlab_endpoint
config.private_token = gitlab_token
end
mr_changes = Gitlab.merge_request_changes(mr_project_path, mr_iid)
Gitlab.configure do |config|
config.endpoint = gitlab_endpoint
config.private_token = gitlab_token
mr_changes.changes.map { |change| change['new_path'] }
end
end
def test_files
return [] if !matched_tests_file || !File.exist?(matched_tests_file)
File.read(matched_tests_file).split(' ')
end
def frontend_fixtures_mapping
return {} if !frontend_fixtures_mapping_path || !File.exist?(frontend_fixtures_mapping_path)
JSON.parse(File.read(frontend_fixtures_mapping_path)) # rubocop:disable Gitlab/Json
end
attr_reader :gitlab_token, :gitlab_endpoint, :mr_project_path, :mr_iid, :output_file, :matched_tests_file, :frontend_fixtures_mapping_path
end
mr_changes = Gitlab.merge_request_changes(mr_project_path, mr_iid)
file_changes = mr_changes.changes.map { |change| change['new_path'] }
output_file = ARGV.shift
raise ArgumentError, "An path to an output file must be given as first argument of #{__FILE__}." if output_file.nil?
matched_tests_file = ARGV.shift
frontend_fixtures_mapping_path = ARGV.shift
File.write(output_file, file_changes.join(' '))
FindChanges
.new(output_file: output_file, matched_tests_file: matched_tests_file, frontend_fixtures_mapping_path: frontend_fixtures_mapping_path)
.execute
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