require_relative "../../../base" require "vagrant/util/platform" require Vagrant.source_root.join("plugins/synced_folders/rsync/helper") describe VagrantPlugins::SyncedFolderRSync::RsyncHelper do include_context "unit" let(:iso_env) do # We have to create a Vagrantfile so there is a root path env = isolated_environment env.vagrantfile("") env.create_vagrant_env end let(:guest) { double("guest") } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } subject { described_class } before do allow(machine).to receive(:guest).and_return(guest) # Don't do all the crazy Cygwin stuff allow(Vagrant::Util::Platform).to receive(:cygwin_path) do |path, **opts| path end end describe "#exclude_to_regexp" do let(:path) { "/foo/bar" } it "converts a directory match" do expect(described_class.exclude_to_regexp(path, "foo/")). to eq(/^#{Regexp.escape(path)}\/.*foo\//) end it "converts the start anchor" do expect(described_class.exclude_to_regexp(path, "/foo")). to eq(/^\/foo\/bar\/foo/) end it "converts the **" do expect(described_class.exclude_to_regexp(path, "fo**o")). to eq(/^#{Regexp.escape(path)}\/.*fo.*o/) end it "converts the *" do expect(described_class.exclude_to_regexp(path, "fo*o")). to eq(/^#{Regexp.escape(path)}\/.*fo[^\/]*o/) end end describe "#rsync_single" do let(:result) { Vagrant::Util::Subprocess::Result.new(0, "", "") } let(:ssh_info) {{ private_key_path: [], }} let(:opts) {{ hostpath: "/foo", }} let(:ui) { machine.ui } before do allow(Vagrant::Util::Subprocess).to receive(:execute){ result } allow(guest).to receive(:capability?){ false } end it "doesn't raise an error if it succeeds" do subject.rsync_single(machine, ssh_info, opts) end it "doesn't call cygwin_path on non-Windows" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(false) expect(Vagrant::Util::Platform).not_to receive(:cygwin_path) subject.rsync_single(machine, ssh_info, opts) end it "calls cygwin_path on Windows" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) expect(Vagrant::Util::Platform).to receive(:cygwin_path).and_return("foo") expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[args.length - 3]).to eql("foo/") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "raises an error if the exit code is non-zero" do allow(Vagrant::Util::Subprocess).to receive(:execute) .and_return(Vagrant::Util::Subprocess::Result.new(1, "", "")) expect {subject.rsync_single(machine, ssh_info, opts) }. to raise_error(Vagrant::Errors::RSyncError) end context "host and guest paths" do it "syncs the hostpath to the guest path" do opts[:hostpath] = "/foo" opts[:guestpath] = "/bar" expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expected = Vagrant::Util::Platform.fs_real_path("/foo").to_s expect(args[args.length - 3]).to eql("#{expected}/") expect(args[args.length - 2]).to include("/bar") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "expands the hostpath relative to the root path" do opts[:hostpath] = "foo" opts[:guestpath] = "/bar" hostpath_expanded = File.expand_path(opts[:hostpath], machine.env.root_path) expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[args.length - 3]).to eql("#{hostpath_expanded}/") expect(args[args.length - 2]).to include("/bar") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end end it "executes within the root path" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args.last).to be_kind_of(Hash) opts = args.last expect(opts[:workdir]).to eql(machine.env.root_path.to_s) }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "executes the rsync_pre capability first if it exists" do expect(guest).to receive(:capability?).with(:rsync_pre).and_return(true) expect(guest).to receive(:capability).with(:rsync_pre, opts).ordered expect(Vagrant::Util::Subprocess).to receive(:execute).ordered.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "executes the rsync_post capability after if it exists" do expect(guest).to receive(:capability?).with(:rsync_post).and_return(true) expect(Vagrant::Util::Subprocess).to receive(:execute).ordered.and_return(result) expect(guest).to receive(:capability).with(:rsync_post, opts).ordered subject.rsync_single(machine, ssh_info, opts) end context "with rsync_post capability" do before do allow(guest).to receive(:capability?).with(:rsync_post).and_return(true) allow(Vagrant::Util::Subprocess).to receive(:execute).and_return(result) end it "should raise custom error when capability errors" do expect(guest).to receive(:capability).with(:rsync_post, opts). and_raise(Vagrant::Errors::VagrantError) expect { subject.rsync_single(machine, ssh_info, opts) }. to raise_error(Vagrant::Errors::RSyncPostCommandError) end end context "with rsync_ownership option" do let(:rsync_local_version) { "3.1.1" } let(:rsync_remote_version) { "3.1.1" } let(:rsync_result) { Vagrant::Util::Subprocess::Result.new(0, "", "") } before do expect(Vagrant::Util::Subprocess).to receive(:execute). with("rsync", "--version").and_return(Vagrant::Util::Subprocess::Result.new(0, " version #{rsync_local_version} ", "")) allow(machine.communicate).to receive(:execute).with(/--version/).and_yield(:stdout, " version #{rsync_remote_version} ") allow(Vagrant::Util::Subprocess).to receive(:execute).with("rsync", any_args).and_return(rsync_result) opts[:rsync_ownership] = true end after { subject.reset! } it "should use the rsync --chown flag" do expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args| expect(args.detect{|a| a.include?("--chown")}).to be_truthy rsync_result } subject.rsync_single(machine, ssh_info, opts) end it "should set the chown option to false" do expect(opts.has_key?(:chown)).to eq(false) subject.rsync_single(machine, ssh_info, opts) expect(opts[:chown]).to eq(false) end context "when local rsync version does not support --chown" do let(:rsync_local_version) { "2.0" } it "should not use the --chown flag" do expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args| expect(args.detect{|a| a.include?("--chown")}).to be_falsey rsync_result } subject.rsync_single(machine, ssh_info, opts) end end context "when remote rsync version does not support --chown" do let(:rsync_remote_version) { "2.0" } it "should not use the --chown flag" do expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args| expect(args.detect{|a| a.include?("--chown")}).to be_falsey rsync_result } subject.rsync_single(machine, ssh_info, opts) end end end context "excluding files" do it "excludes files if given as a string" do opts[:exclude] = "foo" expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| index = args.find_index("foo") expect(index).to be > 0 expect(args[index-1]).to eql("--exclude") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "excludes multiple files" do opts[:exclude] = ["foo", "bar"] expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| index = args.find_index("foo") expect(index).to be > 0 expect(args[index-1]).to eql("--exclude") index = args.find_index("bar") expect(index).to be > 0 expect(args[index-1]).to eql("--exclude") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end end context "custom arguments" do it "uses the default arguments if not given" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[1]).to eq("--verbose") expect(args[2]).to eq("--archive") expect(args[3]).to eq("--delete") expected = Vagrant::Util::Platform.fs_real_path("/foo").to_s expect(args[args.length - 3]).to eql("#{expected}/") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "uses the custom arguments if given" do opts[:args] = ["--verbose", "-z"] expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[1]).to eq("--verbose") expect(args[2]).to eq("-z") expected = Vagrant::Util::Platform.fs_real_path("/foo").to_s expect(args[args.length - 3]).to eql("#{expected}/") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end end context "control sockets" do it "creates a tmp dir" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(false) allow(Dir).to receive(:mktmpdir).with("vagrant-rsync-"). and_return("/tmp/vagrant-rsync-12345") expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[9]).to include("ControlPath=/tmp/vagrant-rsync-12345") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end end end describe "#rsync_single with custom ssh_info" do let(:result) { Vagrant::Util::Subprocess::Result.new(0, "", "") } let(:ssh_info) {{ :private_key_path => ['/path/to/key'], :keys_only => true, :verify_host_key => false, }} let(:opts) {{ hostpath: "/foo", }} let(:ui) { machine.ui } before do allow(Vagrant::Util::Subprocess).to receive(:execute){ result } allow(guest).to receive(:capability?){ false } end context "with an IPv6 address" do before { ssh_info[:host] = "fe00::0" } it "formats the address correctly" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args, "@[#{ssh_info[:host]}]:''", instance_of(Hash)) subject.rsync_single(machine, ssh_info, opts) end end context "with an IPv4 address" do before { ssh_info[:host] = "127.0.0.1" } it "formats the address correctly" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args, "@#{ssh_info[:host]}:''", instance_of(Hash)) subject.rsync_single(machine, ssh_info, opts) end end it "includes IdentitiesOnly, StrictHostKeyChecking, and UserKnownHostsFile with defaults" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[9]).to include('IdentitiesOnly') expect(args[9]).to include('StrictHostKeyChecking') expect(args[9]).to include('UserKnownHostsFile') expect(args[9]).to include("-i '/path/to/key'") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "includes StrictHostKeyChecking, and UserKnownHostsFile when verify_host_key is false" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[9]).to include('StrictHostKeyChecking') expect(args[9]).to include('UserKnownHostsFile') }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "includes StrictHostKeyChecking, and UserKnownHostsFile when verify_host_key is :never" do ssh_info[:verify_host_key] = :never expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[9]).to include('StrictHostKeyChecking') expect(args[9]).to include('UserKnownHostsFile') }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "omits IdentitiesOnly with keys_only = false" do ssh_info[:keys_only] = false expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| expect(args[9]).not_to include('IdentitiesOnly') result end subject.rsync_single(machine, ssh_info, opts) end it "omits StrictHostKeyChecking and UserKnownHostsFile with paranoid = true" do ssh_info[:keys_only] = false expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| expect(args[9]).not_to include('StrictHostKeyChecking ') expect(args[9]).not_to include('UserKnownHostsFile ') result end subject.rsync_single(machine, ssh_info, opts) end it "includes custom ssh config when set" do ssh_info[:config] = "/path/to/ssh/config" expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| ssh_config_args = "-F /path/to/ssh/config" expect(args.any?{|a| a.include?(ssh_config_args)}).to be_truthy result end subject.rsync_single(machine, ssh_info, opts) end end describe ".rsync_chown_support?" do let(:local_version) { "3.1.1" } let(:remote_version) { "3.1.1" } before do allow(subject).to receive(:local_rsync_version).and_return(local_version) allow(subject).to receive(:machine_rsync_version).and_return(remote_version) end it "should return when local and remote versions support chown" do expect(subject.rsync_chown_support?(machine)).to be_truthy end context "when local version does not support chown" do let(:local_version) { "2.0" } it "should return false" do expect(subject.rsync_chown_support?(machine)).to be_falsey end end context "when remote version does not support chown" do let(:remote_version) { "2.0" } it "should return false" do expect(subject.rsync_chown_support?(machine)).to be_falsey end end context "when both local and remote versions do not support chown" do let(:local_version) { "2.0" } let(:remote_version) { "2.0" } it "should return false" do expect(subject.rsync_chown_support?(machine)).to be_falsey end end end describe ".machine_rsync_version" do let(:version_output) { <<-EOV rsync version 3.1.3 protocol version 31 Copyright (C) 1996-2018 by Andrew Tridgell, Wayne Davison, and others. Web site: http://rsync.samba.org/ Capabilities: 64-bit files, 64-bit inums, 64-bit timestamps, 64-bit long ints, socketpairs, hardlinks, symlinks, IPv6, batchfiles, inplace, append, ACLs, xattrs, iconv, symtimes, prealloc rsync comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See the GNU General Public Licence for details. EOV } before do allow(machine.communicate).to receive(:execute).with(/--version/). and_yield(:stdout, version_output) allow(guest).to receive(:capability?).and_return(false) end it "should extract the version string" do expect(subject.machine_rsync_version(machine)).to eq("3.1.3") end context "when version output is an unknown format" do let(:version_output) { "unknown" } it "should return nil value" do expect(subject.machine_rsync_version(machine)).to be_nil end end context "with guest rsync_command capability" do let(:rsync_path) { "custom_rsync" } before do allow(guest).to receive(:capability?).with(:rsync_command). and_return(true) allow(guest).to receive(:capability).with(:rsync_command). and_return(rsync_path) end it "should use custom rsync_path" do expect(machine.communicate).to receive(:execute). with("#{rsync_path} --version").and_yield(:stdout, version_output) subject.machine_rsync_version(machine) end end end describe ".local_rsync_version" do let(:version_output) { <<-EOV rsync version 3.1.3 protocol version 31 Copyright (C) 1996-2018 by Andrew Tridgell, Wayne Davison, and others. Web site: http://rsync.samba.org/ Capabilities: 64-bit files, 64-bit inums, 64-bit timestamps, 64-bit long ints, socketpairs, hardlinks, symlinks, IPv6, batchfiles, inplace, append, ACLs, xattrs, iconv, symtimes, prealloc rsync comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See the GNU General Public Licence for details. EOV } let(:result) { Vagrant::Util::Subprocess::Result.new(0, version_output, "") } before do allow(Vagrant::Util::Subprocess).to receive(:execute).with("rsync", "--version"). and_return(result) end after { subject.reset! } it "should extract the version string" do expect(subject.local_rsync_version).to eq("3.1.3") end it "should cache the version lookup" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("rsync", "--version"). and_return(result).once expect(subject.local_rsync_version).to eq("3.1.3") expect(subject.local_rsync_version).to eq("3.1.3") end context "when version output is an unknown format" do let(:version_output) { "unknown" } it "should return nil value" do expect(subject.local_rsync_version).to be_nil end end end end