From f05cbbf6cfcb90ec5ed61db304b9f09f37dd6d4f Mon Sep 17 00:00:00 2001 From: David Dieulivol <ddieulivol@gitlab.com> Date: Wed, 29 Mar 2023 08:11:51 +0000 Subject: [PATCH] Resolve "test-selection gap: run system specs when some views changed" --- .gitlab/ci/setup.gitlab-ci.yml | 1 + .../view_to_system_specs_mappings_spec.rb | 122 ++++++++++++++++++ tooling/bin/view_to_system_specs_mappings | 9 ++ .../mappings/view_to_system_specs_mappings.rb | 64 +++++++++ 4 files changed, 196 insertions(+) create mode 100644 spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb create mode 100755 tooling/bin/view_to_system_specs_mappings create mode 100644 tooling/lib/tooling/mappings/view_to_system_specs_mappings.rb diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml index 3ebbe1e0fed66..6e24bf946bdbe 100644 --- a/.gitlab/ci/setup.gitlab-ci.yml +++ b/.gitlab/ci/setup.gitlab-ci.yml @@ -124,6 +124,7 @@ detect-tests: tooling/bin/find_tests ${RSPEC_VIEWS_INCLUDING_PARTIALS_PATH} ${RSPEC_MATCHING_TESTS_PATH}; tooling/bin/js_to_system_specs_mappings ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_TESTS_PATH}; tooling/bin/graphql_base_type_mappings ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_TESTS_PATH}; + tooling/bin/view_to_system_specs_mappings ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_TESTS_PATH}; tooling/bin/find_changes ${RSPEC_CHANGED_FILES_PATH} ${RSPEC_MATCHING_TESTS_PATH} ${FRONTEND_FIXTURES_MAPPING_PATH}; filter_rspec_matched_foss_tests ${RSPEC_MATCHING_TESTS_PATH} ${RSPEC_MATCHING_TESTS_FOSS_PATH}; filter_rspec_matched_ee_tests ${RSPEC_MATCHING_TESTS_PATH} ${RSPEC_MATCHING_TESTS_EE_PATH}; diff --git a/spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb new file mode 100644 index 0000000000000..d4f4bbd7f06fb --- /dev/null +++ b/spec/tooling/lib/tooling/mappings/view_to_system_specs_mappings_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'tempfile' +require 'fileutils' +require_relative '../../../../../tooling/lib/tooling/mappings/view_to_system_specs_mappings' + +RSpec.describe Tooling::Mappings::ViewToSystemSpecsMappings, feature_category: :tooling do + attr_accessor :view_base_folder, :changes_file, :output_file + + let(:instance) { described_class.new(changes_file, output_file, view_base_folder: view_base_folder) } + let(:changes_file_content) { "changed_file1 changed_file2" } + let(:output_file_initial_content) { "previously_added_spec.rb" } + + around do |example| + self.changes_file = Tempfile.new('changes') + self.output_file = Tempfile.new('output_file') + + # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/ + # Tempfile.html#class-Tempfile-label-Explicit+close + begin + Dir.mktmpdir do |tmp_views_base_folder| + self.view_base_folder = tmp_views_base_folder + example.run + end + ensure + changes_file.close + output_file.close + changes_file.unlink + output_file.unlink + end + end + + before do + FileUtils.mkdir_p("#{view_base_folder}/app/views/dashboard") + + # We write into the temp files initially, to check how the code modified those files + File.write(changes_file, changes_file_content) + File.write(output_file, output_file_initial_content) + end + + shared_examples 'writes nothing to the output file' do + it 'writes nothing to the output file' do + expect { subject }.not_to change { File.read(changes_file) } + end + end + + describe '#execute' do + subject { instance.execute } + + let(:changed_files) { ["#{view_base_folder}/app/views/dashboard/my_view.html.haml"] } + let(:changes_file_content) { changed_files.join(" ") } + + before do + # We create all of the changed_files, so that they are part of the filtered files + changed_files.each { |changed_file| FileUtils.touch(changed_file) } + end + + context 'when the changed files are not view files' do + let(:changed_files) { ["#{view_base_folder}/app/views/dashboard/my_helper.rb"] } + + it_behaves_like 'writes nothing to the output file' + end + + context 'when the changed files are view files' do + let(:changed_files) { ["#{view_base_folder}/app/views/dashboard/my_view.html.haml"] } + + context 'when the view files do not exist on disk' do + before do + allow(File).to receive(:exist?).with(changed_files.first).and_return(false) + end + + it_behaves_like 'writes nothing to the output file' + end + + context 'when the view files exist on disk' do + context 'when no feature match the view' do + # Nothing in this context, because the spec corresponding to `changed_files` doesn't exist + + it_behaves_like 'writes nothing to the output file' + end + + context 'when there is a feature spec that exactly matches the view' do + let(:expected_feature_spec) { "#{view_base_folder}/spec/features/dashboard/my_view_spec.rb" } + + before do + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).with(expected_feature_spec).and_return(true) + end + + it 'writes that feature spec to the output file' do + expect { subject }.to change { File.read(output_file) } + .from(output_file_initial_content) + .to("#{output_file_initial_content} #{expected_feature_spec}") + end + end + + context 'when there is a feature spec that matches the parent folder of the view' do + let(:expected_feature_specs) do + [ + "#{view_base_folder}/spec/features/dashboard/another_feature_spec.rb", + "#{view_base_folder}/spec/features/dashboard/other_feature_spec.rb" + ] + end + + before do + FileUtils.mkdir_p("#{view_base_folder}/spec/features/dashboard") + + expected_feature_specs.each do |expected_feature_spec| + FileUtils.touch(expected_feature_spec) + end + end + + it 'writes all of the feature specs for the parent folder to the output file' do + expect { subject }.to change { File.read(output_file) } + .from(output_file_initial_content) + .to("#{output_file_initial_content} #{expected_feature_specs.join(' ')}") + end + end + end + end + end +end diff --git a/tooling/bin/view_to_system_specs_mappings b/tooling/bin/view_to_system_specs_mappings new file mode 100755 index 0000000000000..b2e3f2a132ad8 --- /dev/null +++ b/tooling/bin/view_to_system_specs_mappings @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative '../lib/tooling/mappings/view_to_system_specs_mappings' + +changes_file = ARGV.shift +output_file = ARGV.shift + +Tooling::Mappings::ViewToSystemSpecsMappings.new(changes_file, output_file).execute diff --git a/tooling/lib/tooling/mappings/view_to_system_specs_mappings.rb b/tooling/lib/tooling/mappings/view_to_system_specs_mappings.rb new file mode 100644 index 0000000000000..227f2c6e62b24 --- /dev/null +++ b/tooling/lib/tooling/mappings/view_to_system_specs_mappings.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require_relative 'base' +require_relative '../../../../lib/gitlab_edition' + +# Returns system specs files that are related to the Rails views files that were changed in the MR. +module Tooling + module Mappings + class ViewToSystemSpecsMappings < Base + def initialize(changes_file, output_file, view_base_folder: 'app/views') + @output_file = output_file + @changed_files = read_array_from_file(changes_file) + @view_base_folders = folders_for_available_editions(view_base_folder) + end + + def execute + found_system_specs = [] + + filter_files.each do |modified_view_file| + system_specs_exact_match = find_system_specs_exact_match(modified_view_file) + if system_specs_exact_match + found_system_specs << system_specs_exact_match + next + else + system_specs_parent_folder_match = find_system_specs_parent_folder_match(modified_view_file) + found_system_specs += system_specs_parent_folder_match unless system_specs_parent_folder_match.empty? + end + end + + write_array_to_file(output_file, found_system_specs.compact.uniq.sort) + end + + private + + attr_reader :changed_files, :output_file, :view_base_folders + + # Keep the views files that are in the @view_base_folders folder + def filter_files + @_filter_files ||= changed_files.select do |filename| + filename.start_with?(*view_base_folders) && + File.basename(filename).end_with?('.html.haml') && + File.exist?(filename) + end + end + + def find_system_specs_exact_match(view_file) + potential_spec_file = to_feature_spec_folder(view_file).sub('.html.haml', '_spec.rb') + + potential_spec_file if File.exist?(potential_spec_file) + end + + def find_system_specs_parent_folder_match(view_file) + parent_system_specs_folder = File.dirname(to_feature_spec_folder(view_file)) + + Dir["#{parent_system_specs_folder}/**/*_spec.rb"] + end + + # e.g. go from app/views/groups/merge_requests.html.haml to spec/features/groups/merge_requests.html.haml + def to_feature_spec_folder(view_file) + view_file.sub(%r{(ee/|jh/)?app/views}, '\1spec/features') + end + end + end +end -- GitLab