From e58eaec414ea3f94071c218efb1b64cb26de6519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Wed, 24 Nov 2021 22:59:07 +0000 Subject: [PATCH] Automatically detect Jest tests to run upon backend changes --- .gitlab-ci.yml | 1 + .gitlab/ci/rules.gitlab-ci.yml | 10 ++ .gitlab/ci/setup.gitlab-ci.yml | 24 +++- .gitlab/ci/test-metadata.gitlab-ci.yml | 3 +- scripts/api/get_job_id.rb | 8 +- scripts/rspec_helpers.sh | 103 ++++++++++++++++-- spec/support/frontend_fixtures.rb | 16 +++ .../helpers/javascript_fixtures_helpers.rb | 6 + tooling/bin/find_changes | 79 ++++++++++++-- 9 files changed, 223 insertions(+), 27 deletions(-) create mode 100644 spec/support/frontend_fixtures.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e810edd813397..2ea4eb0de6561 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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" diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 0b4f7ca1dd9fe..efc65431d5076 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -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 diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml index 4c674f3893936..1eb3bd2ea4174 100644 --- a/.gitlab/ci/setup.gitlab-ci.yml +++ b/.gitlab/ci/setup.gitlab-ci.yml @@ -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: diff --git a/.gitlab/ci/test-metadata.gitlab-ci.yml b/.gitlab/ci/test-metadata.gitlab-ci.yml index 2d96fb6d4b07c..d0d45cb929438 100644 --- a/.gitlab/ci/test-metadata.gitlab-ci.yml +++ b/.gitlab/ci/test-metadata.gitlab-ci.yml @@ -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 diff --git a/scripts/api/get_job_id.rb b/scripts/api/get_job_id.rb index 166c919895109..c32299706ba27 100755 --- a/scripts/api/get_job_id.rb +++ b/scripts/api/get_job_id.rb @@ -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 @@ def execute 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 @@ def find_job_with_artifact 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 diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh index 8714f1c0060bd..70a63695f5eb6 100644 --- a/scripts/rspec_helpers.sh +++ b/scripts/rspec_helpers.sh @@ -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}\"" +} diff --git a/spec/support/frontend_fixtures.rb b/spec/support/frontend_fixtures.rb new file mode 100644 index 0000000000000..5587d9059dd01 --- /dev/null +++ b/spec/support/frontend_fixtures.rb @@ -0,0 +1,16 @@ +# 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 diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb index fb909008f1229..84cd0181533b5 100644 --- a/spec/support/helpers/javascript_fixtures_helpers.rb +++ b/spec/support/helpers/javascript_fixtures_helpers.rb @@ -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 } diff --git a/tooling/bin/find_changes b/tooling/bin/find_changes index 20df085879aee..c6b8bafbd8526 100755 --- a/tooling/bin/find_changes +++ b/tooling/bin/find_changes @@ -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 -- GitLab