diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-03-21 15:08:46 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-03-21 15:08:46 +0300 |
commit | 7f521d27811b472c43203ed3d1bde4460a617f89 (patch) | |
tree | 47f1a10b776991e86c6db002bc6e03e83acc356a /spec/tooling/lib | |
parent | 83e3316a189d3b709b23af30647b5f9ea5377bac (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/tooling/lib')
-rw-r--r-- | spec/tooling/lib/tooling/kubernetes_client_spec.rb | 483 | ||||
-rw-r--r-- | spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb | 274 |
2 files changed, 559 insertions, 198 deletions
diff --git a/spec/tooling/lib/tooling/kubernetes_client_spec.rb b/spec/tooling/lib/tooling/kubernetes_client_spec.rb index 50d33182a42..20eb78c2f4f 100644 --- a/spec/tooling/lib/tooling/kubernetes_client_spec.rb +++ b/spec/tooling/lib/tooling/kubernetes_client_spec.rb @@ -1,286 +1,373 @@ # frozen_string_literal: true +require 'time' require_relative '../../../../tooling/lib/tooling/kubernetes_client' RSpec.describe Tooling::KubernetesClient do - let(:namespace) { 'review-apps' } - let(:release_name) { 'my-release' } - let(:pod_for_release) { "pod-my-release-abcd" } - let(:raw_resource_names_str) { "NAME\nfoo\n#{pod_for_release}\nbar" } - let(:raw_resource_names) { raw_resource_names_str.lines.map(&:strip) } - - subject { described_class.new(namespace: namespace) } + let(:instance) { described_class.new } + let(:one_day_ago) { Time.now - 3600 * 24 * 1 } + let(:two_days_ago) { Time.now - 3600 * 24 * 2 } + let(:three_days_ago) { Time.now - 3600 * 24 * 3 } + + before do + # Global mock to ensure that no kubectl commands are run by accident in a test. + allow(instance).to receive(:run_command) + end - describe 'RESOURCE_LIST' do - it 'returns the correct list of resources separated by commas' do - expect(described_class::RESOURCE_LIST).to eq('ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa,crd') + describe '#cleanup_pvcs_by_created_at' do + let(:pvc_1_created_at) { three_days_ago } + let(:pvc_2_created_at) { three_days_ago } + let(:pvc_1_namespace) { 'review-first-review-app' } + let(:pvc_2_namespace) { 'review-second-review-app' } + let(:kubectl_pvcs_json) do + <<~JSON + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "creationTimestamp": "#{pvc_1_created_at.utc.iso8601}", + "name": "pvc1", + "namespace": "#{pvc_1_namespace}" + } + }, + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "creationTimestamp": "#{pvc_2_created_at.utc.iso8601}", + "name": "pvc2", + "namespace": "#{pvc_2_namespace}" + } + } + ] + } + JSON end - end - describe '#cleanup_by_release' do + subject { instance.cleanup_pvcs_by_created_at(created_before: two_days_ago) } + before do - allow(subject).to receive(:raw_resource_names).and_return(raw_resource_names) + allow(instance).to receive(:run_command).with( + "kubectl get pvc --all-namespaces --sort-by='{.metadata.creationTimestamp}' -o json" + ).and_return(kubectl_pvcs_json) end - shared_examples 'a kubectl command to delete resources' do - let(:wait) { true } - let(:release_names_in_command) { release_name.respond_to?(:join) ? %(-l 'release in (#{release_name.join(', ')})') : %(-l release="#{release_name}") } - - specify do - expect(Gitlab::Popen).to receive(:popen_with_detail) - .with(["kubectl delete #{described_class::RESOURCE_LIST} " + - %(--namespace "#{namespace}" --now --ignore-not-found --wait=#{wait} #{release_names_in_command})]) - .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true))) + context 'when no pvcs are stale' do + let(:pvc_1_created_at) { one_day_ago } + let(:pvc_2_created_at) { one_day_ago } - expect(Gitlab::Popen).to receive(:popen_with_detail) - .with([%(kubectl delete --namespace "#{namespace}" --ignore-not-found #{pod_for_release})]) - .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true))) + it 'does not delete any PVC' do + expect(instance).not_to receive(:run_command).with(/kubectl delete pvc/) - # We're not verifying the output here, just silencing it - expect { subject.cleanup_by_release(release_name: release_name) }.to output.to_stdout + subject end end - it 'raises an error if the Kubernetes command fails' do - expect(Gitlab::Popen).to receive(:popen_with_detail) - .with(["kubectl delete #{described_class::RESOURCE_LIST} " + - %(--namespace "#{namespace}" --now --ignore-not-found --wait=true -l release="#{release_name}")]) - .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false))) + context 'when some pvcs are stale' do + let(:pvc_1_created_at) { three_days_ago } + let(:pvc_2_created_at) { three_days_ago } - expect { subject.cleanup_by_release(release_name: release_name) }.to raise_error(described_class::CommandFailedError) - end + context 'when some pvcs are not in a review app namespaces' do + let(:pvc_1_namespace) { 'review-my-review-app' } + let(:pvc_2_namespace) { 'review-apps' } # This is not a review apps namespace, so we should not delete PVCs inside it - it_behaves_like 'a kubectl command to delete resources' + it 'deletes the stale pvcs inside of review-apps namespaces only' do + expect(instance).to receive(:run_command).with("kubectl delete pvc --namespace=#{pvc_1_namespace} --now --ignore-not-found pvc1") + expect(instance).not_to receive(:run_command).with(/kubectl delete pvc --namespace=#{pvc_2_namespace}/) - context 'with multiple releases' do - let(:release_name) { %w[my-release my-release-2] } + subject + end + end - it_behaves_like 'a kubectl command to delete resources' - end + context 'when all pvcs are in review-apps namespaces' do + let(:pvc_1_namespace) { 'review-my-review-app' } + let(:pvc_2_namespace) { 'review-another-review-app' } - context 'with `wait: false`' do - let(:wait) { false } + it 'deletes all of the stale pvcs' do + expect(instance).to receive(:run_command).with("kubectl delete pvc --namespace=#{pvc_1_namespace} --now --ignore-not-found pvc1") + expect(instance).to receive(:run_command).with("kubectl delete pvc --namespace=#{pvc_2_namespace} --now --ignore-not-found pvc2") - it_behaves_like 'a kubectl command to delete resources' + subject + end + end end end - describe '#cleanup_by_created_at' do - let(:two_days_ago) { Time.now - 3600 * 24 * 2 } - let(:resource_type) { 'pvc' } - let(:resource_names) { [pod_for_release] } + describe '#cleanup_namespaces_by_created_at' do + let(:namespace_1_created_at) { three_days_ago } + let(:namespace_2_created_at) { three_days_ago } + let(:namespace_1_name) { 'review-first-review-app' } + let(:namespace_2_name) { 'review-second-review-app' } + let(:kubectl_namespaces_json) do + <<~JSON + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "namespace", + "metadata": { + "creationTimestamp": "#{namespace_1_created_at.utc.iso8601}", + "name": "#{namespace_1_name}" + } + }, + { + "apiVersion": "v1", + "kind": "namespace", + "metadata": { + "creationTimestamp": "#{namespace_2_created_at.utc.iso8601}", + "name": "#{namespace_2_name}" + } + } + ] + } + JSON + end + + subject { instance.cleanup_namespaces_by_created_at(created_before: two_days_ago) } before do - allow(subject).to receive(:resource_names_created_before).with(resource_type: resource_type, created_before: two_days_ago).and_return(resource_names) + allow(instance).to receive(:run_command).with( + "kubectl get namespace --all-namespaces --sort-by='{.metadata.creationTimestamp}' -o json" + ).and_return(kubectl_namespaces_json) end - shared_examples 'a kubectl command to delete resources by older than given creation time' do - let(:wait) { true } - let(:release_names_in_command) { resource_names.join(' ') } + context 'when no namespaces are stale' do + let(:namespace_1_created_at) { one_day_ago } + let(:namespace_2_created_at) { one_day_ago } - specify do - expect(Gitlab::Popen).to receive(:popen_with_detail) - .with(["kubectl delete #{resource_type} ".squeeze(' ') + - %(--namespace "#{namespace}" --now --ignore-not-found --wait=#{wait} #{release_names_in_command})]) - .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true))) + it 'does not delete any namespace' do + expect(instance).not_to receive(:run_command).with(/kubectl delete namespace/) - # We're not verifying the output here, just silencing it - expect { subject.cleanup_by_created_at(resource_type: resource_type, created_before: two_days_ago) }.to output.to_stdout + subject end end - it 'raises an error if the Kubernetes command fails' do - expect(Gitlab::Popen).to receive(:popen_with_detail) - .with(["kubectl delete #{resource_type} " + - %(--namespace "#{namespace}" --now --ignore-not-found --wait=true #{pod_for_release})]) - .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false))) + context 'when some namespaces are stale' do + let(:namespace_1_created_at) { three_days_ago } + let(:namespace_2_created_at) { three_days_ago } - expect { subject.cleanup_by_created_at(resource_type: resource_type, created_before: two_days_ago) }.to raise_error(described_class::CommandFailedError) - end + context 'when some namespaces are not review app namespaces' do + let(:namespace_1_name) { 'review-my-review-app' } + let(:namespace_2_name) { 'review-apps' } # This is not a review apps namespace, so we should not try to delete it - it_behaves_like 'a kubectl command to delete resources by older than given creation time' + it 'only deletes the review app namespaces' do + expect(instance).to receive(:run_command).with("kubectl delete namespace --now --ignore-not-found #{namespace_1_name}") - context 'with multiple resource names' do - let(:resource_names) { %w[pod-1 pod-2] } + subject + end + end - it_behaves_like 'a kubectl command to delete resources by older than given creation time' - end + context 'when all namespaces are review app namespaces' do + let(:namespace_1_name) { 'review-my-review-app' } + let(:namespace_2_name) { 'review-another-review-app' } - context 'with `wait: false`' do - let(:wait) { false } + it 'deletes all of the stale namespaces' do + expect(instance).to receive(:run_command).with("kubectl delete namespace --now --ignore-not-found #{namespace_1_name} #{namespace_2_name}") - it_behaves_like 'a kubectl command to delete resources by older than given creation time' + subject + end + end end + end - context 'with no resource_type given' do - let(:resource_type) { nil } + describe '#delete_pvc' do + let(:pvc_name) { 'my-pvc' } - it_behaves_like 'a kubectl command to delete resources by older than given creation time' - end + subject { instance.delete_pvc(pvc_name, pvc_namespace) } + + context 'when the namespace is not a review app namespace' do + let(:pvc_namespace) { 'not-a-review-app-namespace' } - context 'with multiple resource_type given' do - let(:resource_type) { 'pvc,service' } + it 'does not delete the pvc' do + expect(instance).not_to receive(:run_command).with(/kubectl delete pvc/) - it_behaves_like 'a kubectl command to delete resources by older than given creation time' + subject + end end - context 'with no resources found' do - let(:resource_names) { [] } + context 'when the namespace is a review app namespace' do + let(:pvc_namespace) { 'review-apple-test' } - it 'does not call #delete_by_exact_names' do - expect(subject).not_to receive(:delete_by_exact_names) + it 'deletes the pvc' do + expect(instance).to receive(:run_command).with("kubectl delete pvc --namespace=#{pvc_namespace} --now --ignore-not-found #{pvc_name}") - subject.cleanup_by_created_at(resource_type: resource_type, created_before: two_days_ago) + subject end end end - describe '#cleanup_review_app_namespaces' do - let(:two_days_ago) { Time.now - 3600 * 24 * 2 } - let(:namespaces) { %w[review-abc-123 review-xyz-789] } + describe '#delete_namespaces' do + subject { instance.delete_namespaces(namespaces) } - subject { described_class.new(namespace: nil) } + context 'when at least one namespace is not a review app namespace' do + let(:namespaces) { %w[review-ns-1 default] } - before do - allow(subject).to receive(:review_app_namespaces_created_before).with(created_before: two_days_ago).and_return(namespaces) + it 'does not delete any namespace' do + expect(instance).not_to receive(:run_command).with(/kubectl delete namespace/) + + subject + end end - shared_examples 'a kubectl command to delete namespaces older than given creation time' do - let(:wait) { true } + context 'when all namespaces are review app namespaces' do + let(:namespaces) { %w[review-ns-1 review-ns-2] } - specify do - expect(Gitlab::Popen).to receive(:popen_with_detail) - .with(["kubectl delete namespace " + - %(--now --ignore-not-found --wait=#{wait} #{namespaces.join(' ')})]) - .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true))) + it 'deletes the namespaces' do + expect(instance).to receive(:run_command).with("kubectl delete namespace --now --ignore-not-found #{namespaces.join(' ')}") - # We're not verifying the output here, just silencing it - expect { subject.cleanup_review_app_namespaces(created_before: two_days_ago) }.to output.to_stdout + subject end end + end - it_behaves_like 'a kubectl command to delete namespaces older than given creation time' - - it 'raises an error if the Kubernetes command fails' do - expect(Gitlab::Popen).to receive(:popen_with_detail) - .with(["kubectl delete namespace " + - %(--now --ignore-not-found --wait=true #{namespaces.join(' ')})]) - .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false))) - - expect { subject.cleanup_review_app_namespaces(created_before: two_days_ago) }.to raise_error(described_class::CommandFailedError) + describe '#pvcs_created_before' do + subject { instance.pvcs_created_before(created_before: two_days_ago) } + + let(:pvc_1_created_at) { three_days_ago } + let(:pvc_2_created_at) { three_days_ago } + let(:pvc_1_namespace) { 'review-first-review-app' } + let(:pvc_2_namespace) { 'review-second-review-app' } + let(:kubectl_pvcs_json) do + <<~JSON + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "creationTimestamp": "#{pvc_1_created_at.utc.iso8601}", + "name": "pvc1", + "namespace": "#{pvc_1_namespace}" + } + }, + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "creationTimestamp": "#{pvc_2_created_at.utc.iso8601}", + "name": "pvc2", + "namespace": "#{pvc_2_namespace}" + } + } + ] + } + JSON end - context 'with no namespaces found' do - let(:namespaces) { [] } + it 'calls #resource_created_before with the correct parameters' do + expect(instance).to receive(:resource_created_before).with(resource_type: 'pvc', created_before: two_days_ago) - it 'does not call #delete_namespaces_by_exact_names' do - expect(subject).not_to receive(:delete_namespaces_by_exact_names) + subject + end - subject.cleanup_review_app_namespaces(created_before: two_days_ago) - end + it 'returns a hash with two keys' do + allow(instance).to receive(:run_command).with( + "kubectl get pvc --all-namespaces --sort-by='{.metadata.creationTimestamp}' -o json" + ).and_return(kubectl_pvcs_json) + + expect(subject).to match_array([ + { + resource_name: 'pvc1', + namespace: 'review-first-review-app' + }, + { + resource_name: 'pvc2', + namespace: 'review-second-review-app' + } + ]) end end - describe '#raw_resource_names' do - it 'calls kubectl to retrieve the resource names' do - expect(Gitlab::Popen).to receive(:popen_with_detail) - .with(["kubectl get #{described_class::RESOURCE_LIST} " + - %(--namespace "#{namespace}" -o name)]) - .and_return(Gitlab::Popen::Result.new([], raw_resource_names_str, '', double(success?: true))) - - expect(subject.__send__(:raw_resource_names)).to eq(raw_resource_names) + describe '#namespaces_created_before' do + subject { instance.namespaces_created_before(created_before: two_days_ago) } + + let(:namespace_1_created_at) { three_days_ago } + let(:namespace_2_created_at) { three_days_ago } + let(:namespace_1_name) { 'review-first-review-app' } + let(:namespace_2_name) { 'review-second-review-app' } + let(:kubectl_namespaces_json) do + <<~JSON + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "namespace", + "metadata": { + "creationTimestamp": "#{namespace_1_created_at.utc.iso8601}", + "name": "#{namespace_1_name}" + } + }, + { + "apiVersion": "v1", + "kind": "namespace", + "metadata": { + "creationTimestamp": "#{namespace_2_created_at.utc.iso8601}", + "name": "#{namespace_2_name}" + } + } + ] + } + JSON end - end - describe '#resource_names_created_before' do - let(:three_days_ago) { Time.now - 3600 * 24 * 3 } - let(:two_days_ago) { Time.now - 3600 * 24 * 2 } - let(:pvc_created_three_days_ago) { 'pvc-created-three-days-ago' } - let(:resource_type) { 'pvc' } - let(:raw_resources) do - { - items: [ - { - apiVersion: "v1", - kind: "PersistentVolumeClaim", - metadata: { - creationTimestamp: three_days_ago, - name: pvc_created_three_days_ago - } - }, - { - apiVersion: "v1", - kind: "PersistentVolumeClaim", - metadata: { - creationTimestamp: Time.now, - name: 'another-pvc' - } - } - ] - }.to_json + it 'calls #resource_created_before with the correct parameters' do + expect(instance).to receive(:resource_created_before).with(resource_type: 'namespace', created_before: two_days_ago) + + subject end - shared_examples 'a kubectl command to retrieve resource names sorted by creationTimestamp' do - specify do - expect(Gitlab::Popen).to receive(:popen_with_detail) - .with(["kubectl get #{resource_type} ".squeeze(' ') + - %(--namespace "#{namespace}" ) + - "--sort-by='{.metadata.creationTimestamp}' -o json"]) - .and_return(Gitlab::Popen::Result.new([], raw_resources, '', double(success?: true))) + it 'returns an array of namespaces' do + allow(instance).to receive(:run_command).with( + "kubectl get namespace --all-namespaces --sort-by='{.metadata.creationTimestamp}' -o json" + ).and_return(kubectl_namespaces_json) - expect(subject.__send__(:resource_names_created_before, resource_type: resource_type, created_before: two_days_ago)).to contain_exactly(pvc_created_three_days_ago) - end + expect(subject).to match_array(%w[review-first-review-app review-second-review-app]) end + end - it_behaves_like 'a kubectl command to retrieve resource names sorted by creationTimestamp' + describe '#run_command' do + subject { instance.run_command(command) } - context 'with no resource_type given' do - let(:resource_type) { nil } + before do + # We undo the global mock just for this method + allow(instance).to receive(:run_command).and_call_original - it_behaves_like 'a kubectl command to retrieve resource names sorted by creationTimestamp' + # Mock stdout + allow(instance).to receive(:puts) end - context 'with multiple resource_type given' do - let(:resource_type) { 'pvc,service' } + context 'when executing a successful command' do + let(:command) { 'true' } # https://linux.die.net/man/1/true - it_behaves_like 'a kubectl command to retrieve resource names sorted by creationTimestamp' - end - end + it 'displays the name of the command to stdout' do + expect(instance).to receive(:puts).with("Running command: `#{command}`") + + subject + end - describe '#review_app_namespaces_created_before' do - let(:three_days_ago) { Time.now - 3600 * 24 * 3 } - let(:two_days_ago) { Time.now - 3600 * 24 * 2 } - let(:namespace_created_three_days_ago) { 'review-ns-created-three-days-ago' } - let(:resource_type) { 'namespace' } - let(:raw_resources) do - { - items: [ - { - apiVersion: "v1", - kind: "Namespace", - metadata: { - creationTimestamp: three_days_ago, - name: namespace_created_three_days_ago - } - }, - { - apiVersion: "v1", - kind: "Namespace", - metadata: { - creationTimestamp: Time.now, - name: 'another-namespace' - } - } - ] - }.to_json + it 'does not raise an error' do + expect { subject }.not_to raise_error + end end - specify do - expect(Gitlab::Popen).to receive(:popen_with_detail) - .with(["kubectl get namespace --sort-by='{.metadata.creationTimestamp}' -o json"]) - .and_return(Gitlab::Popen::Result.new([], raw_resources, '', double(success?: true))) + context 'when executing an unsuccessful command' do + let(:command) { 'false' } # https://linux.die.net/man/1/false + + it 'displays the name of the command to stdout' do + expect(instance).to receive(:puts).with("Running command: `#{command}`") - expect(subject.__send__(:review_app_namespaces_created_before, created_before: two_days_ago)).to eq([namespace_created_three_days_ago]) + expect { subject }.to raise_error(described_class::CommandFailedError) + end + + it 'raises an error' do + expect { subject }.to raise_error(described_class::CommandFailedError) + end end end end diff --git a/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb new file mode 100644 index 00000000000..69dddb0ae3d --- /dev/null +++ b/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb @@ -0,0 +1,274 @@ +# frozen_string_literal: true + +require 'tempfile' +require 'fileutils' +require_relative '../../../../../tooling/lib/tooling/mappings/partial_to_views_mappings' + +RSpec.describe Tooling::Mappings::PartialToViewsMappings, 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_content) { "previously_added_view.html.haml" } + + 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 + # 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_content) + end + + describe '#execute' do + subject { instance.execute } + + let(:changed_files) { ["#{view_base_folder}/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 + + it 'does not modify the content of the input file' do + expect { subject }.not_to change { File.read(changes_file) } + end + + context 'when no partials were modified' do + it 'empties the output file' do + expect { subject }.to change { File.read(output_file) }.from(output_file_content).to('') + end + end + + context 'when some partials were modified' do + let(:changed_files) do + [ + "#{view_base_folder}/my_view.html.haml", + "#{view_base_folder}/_my_partial.html.haml" + ] + end + + before do + # We create a red-herring partial to have a more convincing test suite + FileUtils.touch("#{view_base_folder}/_another_partial.html.haml") + end + + context 'when the partials are not included in any views' do + before do + File.write("#{view_base_folder}/my_view.html.haml", "render 'another_partial'") + end + + it 'empties the output file' do + expect { subject }.to change { File.read(output_file) }.from(output_file_content).to('') + end + end + + context 'when the partials are included in views' do + before do + File.write("#{view_base_folder}/my_view.html.haml", "render 'my_partial'") + end + + it 'writes the view including the partial to the output' do + expect { subject }.to change { File.read(output_file) } + .from(output_file_content) + .to("#{view_base_folder}/my_view.html.haml") + end + end + end + end + + describe '#filter_files' do + subject { instance.filter_files } + + let(:changes_file_content) { file_path } + + context 'when the file does not exist on disk' do + let(:file_path) { "#{view_base_folder}/_index.html.erb" } + + it 'returns an empty array' do + expect(subject).to be_empty + end + end + + context 'when the file exists on disk' do + before do + File.write(file_path, "I am a partial!") + end + + context 'when the file is not in the view base folders' do + let(:file_path) { "/tmp/_index.html.haml" } + + it 'returns an empty array' do + expect(subject).to be_empty + end + end + + context 'when the filename does not start with an underscore' do + let(:file_path) { "#{view_base_folder}/index.html.haml" } + + it 'returns an empty array' do + expect(subject).to be_empty + end + end + + context 'when the filename does not have the correct extension' do + let(:file_path) { "#{view_base_folder}/_index.html.erb" } + + it 'returns an empty array' do + expect(subject).to be_empty + end + end + + context 'when the file is a partial' do + let(:file_path) { "#{view_base_folder}/_index.html.haml" } + + it 'returns the file' do + expect(subject).to match_array(file_path) + end + end + end + end + + describe '#extract_partial_keyword' do + subject { instance.extract_partial_keyword('ee/app/views/shared/_new_project_item_vue_select.html.haml') } + + it 'returns the correct partial keyword' do + expect(subject).to eq('new_project_item_vue_select') + end + end + + describe '#view_includes_modified_partial?' do + subject { instance.view_includes_modified_partial?(view_file, included_partial_name) } + + context 'when the included partial name is relative to the view file' do + let(:view_file) { "#{view_base_folder}/components/my_view.html.haml" } + let(:included_partial_name) { 'subfolder/relative_partial' } + + before do + FileUtils.mkdir_p("#{view_base_folder}/components/subfolder") + File.write(changes_file_content, "I am a partial!") + end + + context 'when the partial is not part of the changed files' do + let(:changes_file_content) { "#{view_base_folder}/components/subfolder/_not_the_partial.html.haml" } + + it 'returns false' do + expect(subject).to be_falsey + end + end + + context 'when the partial is part of the changed files' do + let(:changes_file_content) { "#{view_base_folder}/components/subfolder/_relative_partial.html.haml" } + + it 'returns true' do + expect(subject).to be_truthy + end + end + end + + context 'when the included partial name is relative to the base views folder' do + let(:view_file) { "#{view_base_folder}/components/my_view.html.haml" } + let(:included_partial_name) { 'shared/absolute_partial' } + + before do + FileUtils.mkdir_p("#{view_base_folder}/components") + FileUtils.mkdir_p("#{view_base_folder}/shared") + File.write(changes_file_content, "I am a partial!") + end + + context 'when the partial is not part of the changed files' do + let(:changes_file_content) { "#{view_base_folder}/shared/not_the_partial" } + + it 'returns false' do + expect(subject).to be_falsey + end + end + + context 'when the partial is part of the changed files' do + let(:changes_file_content) { "#{view_base_folder}/shared/_absolute_partial.html.haml" } + + it 'returns true' do + expect(subject).to be_truthy + end + end + end + end + + describe '#reconstruct_partial_filename' do + subject { instance.reconstruct_partial_filename(partial_name) } + + context 'when the partial does not contain a path' do + let(:partial_name) { 'sidebar' } + + it 'returns the correct filename' do + expect(subject).to eq('_sidebar.html.haml') + end + end + + context 'when the partial contains a path' do + let(:partial_name) { 'shared/components/sidebar' } + + it 'returns the correct filename' do + expect(subject).to eq('shared/components/_sidebar.html.haml') + end + end + end + + describe '#find_pattern_in_file' do + let(:subject) { instance.find_pattern_in_file(file.path, /pattern/) } + let(:file) { Tempfile.new('find_pattern_in_file') } + + before do + file.write(file_content) + file.close + end + + context 'when the file contains the pattern' do + let(:file_content) do + <<~FILE + Beginning of file + + pattern + pattern + pattern + + End of file + FILE + end + + it 'returns the pattern once' do + expect(subject).to match_array(%w[pattern]) + end + end + + context 'when the file does not contain the pattern' do + let(:file_content) do + <<~FILE + Beginning of file + End of file + FILE + end + + it 'returns an empty array' do + expect(subject).to match_array([]) + end + end + end +end |