Speed up file syncing using zip/tgz

This commit is contained in:
Zhongcheng Lao 2019-06-25 08:04:09 +08:00
parent d7456413ff
commit c0102812f4
12 changed files with 1304 additions and 565 deletions

View File

@ -44,7 +44,7 @@ module VagrantPlugins
"rm -f '#{compressed_file}'",
"rm -rf '#{extract_dir}'"
]
cmds.each{ |cmd| comm.execute(cmd) }
cmds.each{ |cmd| comm.execute(cmd, sudo: opts[:sudo] || false) }
true
end
@ -68,7 +68,7 @@ module VagrantPlugins
"rm -f '#{compressed_file}'",
"rm -rf '#{extract_dir}'"
]
cmds.each{ |cmd| comm.execute(cmd) }
cmds.each{ |cmd| comm.execute(cmd, sudo: opts[:sudo] || false) }
true
end
end

View File

@ -46,7 +46,7 @@ module VagrantPlugins
"rm -f '#{compressed_file}'",
"rm -rf '#{extract_dir}'"
]
cmds.each{ |cmd| comm.execute(cmd) }
cmds.each{ |cmd| comm.execute(cmd, sudo: opts[:sudo] || false) }
true
end
@ -70,7 +70,7 @@ module VagrantPlugins
"rm -f '#{compressed_file}'",
"rm -rf '#{extract_dir}'"
]
cmds.each{ |cmd| comm.execute(cmd) }
cmds.each{ |cmd| comm.execute(cmd, sudo: opts[:sudo] || false) }
true
end
@ -78,7 +78,7 @@ module VagrantPlugins
#
# @param [Vagrant::Machine] machine Vagrant guest machine
# @param [array] paths to create on guest
def self.create_directories(machine, dirs)
def self.create_directories(machine, dirs, opts={})
return [] if dirs.empty?
remote_fn = create_tmp_path(machine, {})
@ -93,7 +93,7 @@ module VagrantPlugins
tmp.unlink
end
created_paths = []
machine.communicate.sudo("bash -c 'while IFS= read -r line
machine.communicate.execute("bash -c 'while IFS= read -r line
do
if [ ! -z \"${line}\" ] && [ ! -d \"${line}\" ]; then
if [ -f \"${line}\" ]; then
@ -102,7 +102,7 @@ module VagrantPlugins
mkdir -p -v \"${line}\" || true
fi
done < #{remote_fn}'
") do |type, data|
", sudo: opts[:sudo] || false) do |type, data|
if type == :stdout && /^.*\'(?<dir>.*)\'/ =~ data
created_paths << dir.strip
end

View File

@ -44,7 +44,7 @@ module VagrantPlugins
"rm -f '#{compressed_file}'",
"rm -rf '#{extract_dir}'"
]
cmds.each{ |cmd| comm.execute(cmd) }
cmds.each{ |cmd| comm.execute(cmd, sudo: opts[:sudo] || false) }
true
end
@ -68,7 +68,7 @@ module VagrantPlugins
"rm -f '#{compressed_file}'",
"rm -rf '#{extract_dir}'"
]
cmds.each{ |cmd| comm.execute(cmd) }
cmds.each{ |cmd| comm.execute(cmd, sudo: opts[:sudo] || false) }
true
end
end

View File

@ -66,7 +66,7 @@ module VagrantPlugins
#
# @param [Vagrant::Machine] machine Vagrant guest machine
# @param [array] paths to create on guest
def self.create_directories(machine, dirs)
def self.create_directories(machine, dirs, opts={})
return [] if dirs.empty?
remote_fn = create_tmp_path(machine, {})

View File

@ -1,74 +0,0 @@
require 'find'
require_relative '../helper'
module VagrantPlugins
module HyperV
module Cap
module SyncFolder
def self.sync_folder(machine, data)
is_win_guest = machine.guest.name == :windows
host_path = VagrantPlugins::HyperV::SyncHelper.expand_path(data[:hostpath])
guest_path = data[:guestpath]
win_host_path = Vagrant::Util::Platform.windows_path(
host_path, :disable_unc)
win_guest_path = guest_path.tr '/', '\\'
includes = find_includes(data[:hostpath], data[:exclude])
dir_mappings = {}
file_mappings = {}
platform_guest_path = is_win_guest ? win_guest_path : guest_path
{ dirs: dir_mappings,
files: file_mappings }.map do |sym, mapping|
includes[sym].map do |e|
guest_rel = e.gsub(host_path, '')
guest_rel = guest_rel[1..-1] if guest_rel.start_with? '\\', '/'
guest_rel.tr! '\\', '/'
# make sure the dir names are Windows-style for them to pass to Hyper-V
if guest_rel == ''
win_path = win_host_path
target = platform_guest_path
else
win_path = HyperV::SyncHelper.platform_join(win_host_path, guest_rel)
guest_rel = guest_rel.split("/")[0..-2].join("/") if sym == :files
target = HyperV::SyncHelper.platform_join(platform_guest_path, guest_rel,
is_windows: is_win_guest)
target = target[0..-2] if target.end_with? '\\', '/'
end
mapping[win_path] = target
end
end
machine.guest.capability(:create_directories, dir_mappings.values)
machine.provider.driver.sync_files(machine.id, dir_mappings, file_mappings,
is_win_guest: is_win_guest)
end
protected
def self.find_includes(path, exclude)
expanded_path = HyperV::SyncHelper.expand_path(path)
excludes = HyperV::SyncHelper.expand_excludes(path, exclude)
included_dirs = []
included_files = []
Find.find(expanded_path) do |e|
if VagrantPlugins::HyperV::SyncHelper.directory?(e)
path = File.join e, ''
next if excludes[:dirs].include? path
next if excludes[:dirs].select { |x| path.start_with? x }.any?
included_dirs << e
else
next if excludes[:files].include? e
next if excludes[:dirs].select { |x| e.start_with? x }.any?
included_files << e
end
end
{ dirs: included_dirs,
files: included_files }
end
end
end
end
end

View File

@ -6,6 +6,11 @@ module VagrantPlugins
WINDOWS_SEPARATOR = "\\"
UNIX_SEPARATOR = "/"
# Expands glob-style exclude string
#
# @param [String] path Path to operate on
# @param [String] exclude Array of glob-style exclude strings
# @return [Hash] Excluded directories and files
def self.expand_excludes(path, exclude)
excludes = ['.vagrant/']
excludes += Array(exclude).map(&:to_s) if exclude
@ -29,21 +34,272 @@ module VagrantPlugins
files: excluded_files}
end
def self.find_includes(path, exclude)
expanded_path = expand_path(path)
excludes = expand_excludes(path, exclude)
included_dirs = []
included_files = []
Find.find(expanded_path) do |e|
if directory?(e)
path = File.join e, ''
next if excludes[:dirs].include? path
next if excludes[:dirs].select { |x| path.start_with? x }.any?
included_dirs << e
else
next if excludes[:files].include? e
next if excludes[:dirs].select { |x| e.start_with? x }.any?
included_files << e
end
end
{ dirs: included_dirs,
files: included_files }
end
def self.path_mapping(host_path, guest_path, includes, is_win_guest:)
host_path = expand_path(host_path)
platform_host_path = platform_path host_path, is_windows: !Vagrant::Util::Platform.wsl?
win_host_path = Vagrant::Util::Platform.windows_path(host_path, :disable_unc)
platform_guest_path = platform_path(guest_path, is_windows: is_win_guest)
dir_mappings = { hyperv: {}, platform: {} }
file_mappings = { hyperv: {}, platform: {} }
{ dirs: dir_mappings,
files: file_mappings }.map do |sym, mapping|
includes[sym].map do |e|
guest_rel = e.gsub(host_path, '')
guest_rel = trim_head guest_rel
guest_rel = to_unix_path guest_rel
if guest_rel == ''
file_host_path = win_host_path
file_platform_host_path = platform_host_path
target = platform_guest_path
else
file_host_path = platform_join(win_host_path, guest_rel)
file_platform_host_path = platform_join(platform_host_path, guest_rel,
is_windows: !Vagrant::Util::Platform.wsl?)
guest_rel = guest_rel.split(UNIX_SEPARATOR)[0..-2].join(UNIX_SEPARATOR) if sym == :files
target = platform_join(platform_guest_path, guest_rel, is_windows: is_win_guest)
target = trim_tail target
end
# make sure the dir names are Windows-style for them to pass to Hyper-V
mapping[:hyperv][file_host_path] = target
mapping[:platform][file_platform_host_path] = target
end
end
{ dirs: dir_mappings, files: file_mappings }
end
# Syncs single folder to guest machine
#
# @param [Vagrant::Machine] path Path to operate on
# @param [Hash] ssh_info
# @param [Hash] opts Synced folder details
def self.sync_single(machine, ssh_info, opts)
is_win_guest = machine.guest.name == :windows
host_path = opts[:hostpath]
guest_path = opts[:guestpath]
includes = find_includes(host_path, opts[:exclude])
if opts[:no_compression]
# Copy file to guest directly for disk consumption saving
guest_path_mapping = path_mapping(host_path, guest_path, includes, is_win_guest: is_win_guest)
remove_directory machine, guest_path, is_win_guest: is_win_guest, sudo: true
machine.guest.capability(:create_directories, guest_path_mapping[:dirs][:hyperv].values, sudo: true)
if hyperv_copy? machine
machine.provider.driver.sync_files(machine.id,
guest_path_mapping[:dirs][:hyperv],
guest_path_mapping[:files][:hyperv],
is_win_guest: is_win_guest)
else
guest_path_mapping[:files][:platform].each do |host_path, guest_path|
next unless file_exist? host_path
stat = file_stat host_path
next if stat.symlink?
machine.communicate.upload(host_path, guest_path)
end
end
else
source_items = includes[:files]
type = is_win_guest ? :zip : :tgz
host_path = expand_path(host_path)
source = send("compress_source_#{type}".to_sym, host_path, source_items)
decompress_cap = type == :zip ? :decompress_zip : :decompress_tgz
begin
destination = machine.guest.capability(:create_tmp_path, extension: ".#{type}")
upload_file(machine, source, destination, is_win_guest: is_win_guest)
remove_directory machine, guest_path, is_win_guest: is_win_guest, sudo: true
machine.guest.capability(decompress_cap, destination, platform_path(guest_path, is_windows: is_win_guest),
type: :directory, sudo: true)
ensure
FileUtils.rm_f source if file_exist? source
end
end
end
# Compress path using zip into temporary file
#
# @param [String] path Path to compress
# @return [String] path to compressed file
def self.compress_source_zip(path, source_items)
require "zip"
zipfile = Tempfile.create(%w(vagrant .zip), format_windows_temp)
zipfile.close
c_dir = nil
Zip::File.open(zipfile.path, Zip::File::CREATE) do |zip|
source_items.each do |source_item|
next unless file_exist? source_item
next if directory?(source_item)
stat = file_stat(source_item)
next if stat.symlink?
trim_item = source_item.sub(path, "").sub(%r{^[/\\]}, "")
dirname = File.dirname(trim_item)
begin
zip.get_entry(dirname)
rescue Errno::ENOENT
zip.mkdir dirname if c_dir != dirname
end
c_dir = dirname
zip.get_output_stream(trim_item) do |f|
source_file = File.open(source_item, "rb")
while data = source_file.read(2048)
f.write(data)
end
end
end
end
zipfile.path
end
# Compress path using tar and gzip into temporary file
#
# @param [String] path Path to compress
# @return [String] path to compressed file
def self.compress_source_tgz(path, source_items)
tmp_dir = format_windows_temp
tarfile = Tempfile.create(%w(vagrant .tar), tmp_dir)
tarfile.close
tarfile = File.open(tarfile.path, "wb+")
tgzfile = Tempfile.create(%w(vagrant .tgz), tmp_dir)
tgzfile.close
tgzfile = File.open(tgzfile.path, "wb")
tar = Gem::Package::TarWriter.new(tarfile)
tgz = Zlib::GzipWriter.new(tgzfile)
source_items.each do |item|
next unless file_exist? item
rel_path = item.sub(path, "")
stat = file_stat(item)
item_mode = stat.mode
if directory?(item)
tar.mkdir(rel_path, item_mode)
elsif stat.symlink?
tar.add_symlink(rel_path, File.readlink(item), item_mode)
else
tar.add_file(rel_path, item_mode) do |io|
File.open(item, "rb") do |file|
while bytes = file.read(4096)
io.write(bytes)
end
end
end
end
end
tar.close
tarfile.rewind
while bytes = tarfile.read(4096)
tgz.write bytes
end
tgz.close
tgzfile.close
tarfile.close
File.delete(tarfile.path)
tgzfile.path
end
def self.remove_directory(machine, guestpath, is_win_guest: false, sudo: false)
comm = machine.communicate
if is_win_guest
guestpath = to_windows_path guestpath
cmd = <<-EOH.gsub(/^ {6}/, "")
if (Test-Path(\"#{guestpath}\")) {
Remove-Item -Path \"#{guestpath}\" -Recurse -Force
}
EOH
comm.execute(cmd, shell: :powershell)
else
guestpath = to_unix_path guestpath
if comm.test("test -d '#{guestpath}'")
comm.execute("rm -rf '#{guestpath}'", sudo: sudo)
end
end
end
def self.format_windows_temp
windows_temp = Vagrant::Util::Platform.windows_temp
if Vagrant::Util::Platform.wsl?
process = Vagrant::Util::Subprocess.execute(
"wslpath", "-u", "-a", windows_temp)
windows_temp = process.stdout.chomp if process.exit_code == 0
end
windows_temp
end
def self.upload_file(machine, source, dest, is_win_guest:)
begin
# try Hyper-V guest integration service first as WinRM upload is slower
if hyperv_copy? machine
separator = is_win_guest ? WINDOWS_SEPARATOR: UNIX_SEPARATOR
parts = dest.split(separator)
filename = parts[-1]
dest_dir = parts[0..-2].join(separator)
windows_temp = format_windows_temp
source_copy = platform_join windows_temp, filename, is_windows: !Vagrant::Util::Platform.wsl?
FileUtils.mv source, source_copy
source = source_copy
hyperv_copy machine, source, dest_dir
else
machine.communicate.upload(source, dest)
end
ensure
FileUtils.rm_f source
end
end
def self.hyperv_copy?(machine)
machine.guest.capability?(:hyperv_daemons_running) && machine.guest.capability(:hyperv_daemons_running)
end
def self.hyperv_copy(machine, source, dest_dir)
vm_id = machine.id
ps_cmd = <<-EOH.gsub(/^ {6}/, "")
$machine = Hyper-V\\Get-VM -Id \"#{vm_id}\"
Hyper-V\\Copy-VMFile -VM $machine -SourcePath \"#{source}\" -DestinationPath \"#{dest_dir}\" -CreateFullPath -FileSource Host -Force
EOH
Vagrant::Util::PowerShell.execute_cmd(ps_cmd)
end
def self.platform_join(string, *smth, is_windows: true)
joined = [string, *smth].join is_windows ? WINDOWS_SEPARATOR : UNIX_SEPARATOR
if is_windows
joined.tr! UNIX_SEPARATOR, WINDOWS_SEPARATOR
to_windows_path joined
else
joined.tr! WINDOWS_SEPARATOR, UNIX_SEPARATOR
to_unix_path joined
end
joined
end
def self.sync_single(machine, ssh_info, opts)
opts = opts.dup
opts[:owner] ||= ssh_info[:username]
opts[:group] ||= ssh_info[:username]
machine.provider.capability(:sync_folder, opts)
def self.platform_path(path, is_windows: true)
win_path = to_windows_path path
linux_path = to_unix_path path
is_windows ? win_path : linux_path
end
def self.expand_path(*path)
@ -55,6 +311,32 @@ module VagrantPlugins
# stub for unit test
File.directory? path
end
def self.file_exist?(path)
# stub for unit test
File.exist? path
end
def self.file_stat(path)
# stub for unit test
File.stat path
end
def self.to_windows_path(path)
path.tr UNIX_SEPARATOR, WINDOWS_SEPARATOR
end
def self.to_unix_path(path)
path.tr WINDOWS_SEPARATOR, UNIX_SEPARATOR
end
def self.trim_head(path)
path.start_with?(WINDOWS_SEPARATOR, UNIX_SEPARATOR) ? path[1..-1] : path
end
def self.trim_tail(path)
path.end_with?(WINDOWS_SEPARATOR, UNIX_SEPARATOR) ? path[0..-2] : path
end
end
end
end

View File

@ -27,11 +27,6 @@ module VagrantPlugins
Config
end
provider_capability("hyperv", "sync_folder") do
require_relative "cap/sync_folder"
Cap::SyncFolder
end
provider_capability("hyperv", "public_address") do
require_relative "cap/public_address"
Cap::PublicAddress

View File

@ -45,40 +45,45 @@ describe "VagrantPlugins::GuestBSD::Cap::FileSystem" do
let(:cap) { caps.get(:decompress_tgz) }
let(:comp) { "compressed_file" }
let(:dest) { "path/to/destination" }
let(:opts) { {} }
before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") }
after{ cap.decompress_tgz(machine, comp, dest, opts) }
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
[false, true].each do |sudo_flag|
context "sudo flag: #{sudo_flag}" do
let(:opts) { {sudo: sudo_flag} }
it "should extract file with tar" do
expect(comm).to receive(:execute).with(/tar/)
end
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/)
end
it "should extract file with tar" do
expect(comm).to receive(:execute).with(/tar/, sudo: sudo_flag)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/, sudo: sudo_flag)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/, sudo: sudo_flag)
end
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/, sudo: sudo_flag)
end
context "when type is directory" do
before { opts[:type] = :directory }
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/, sudo: sudo_flag)
end
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/)
context "when type is directory" do
before { opts[:type] = :directory }
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/, sudo: sudo_flag)
end
end
end
end
end
@ -87,40 +92,45 @@ describe "VagrantPlugins::GuestBSD::Cap::FileSystem" do
let(:cap) { caps.get(:decompress_zip) }
let(:comp) { "compressed_file" }
let(:dest) { "path/to/destination" }
let(:opts) { {} }
before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") }
after{ cap.decompress_zip(machine, comp, dest, opts) }
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
[false, true].each do |sudo_flag|
context "sudo flag: #{sudo_flag}" do
let(:opts) { {sudo: sudo_flag} }
it "should extract file with zip" do
expect(comm).to receive(:execute).with(/zip/)
end
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/)
end
it "should extract file with zip" do
expect(comm).to receive(:execute).with(/zip/, sudo: sudo_flag)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/, sudo: sudo_flag)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/, sudo: sudo_flag)
end
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/, sudo: sudo_flag)
end
context "when type is directory" do
before { opts[:type] = :directory }
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/, sudo: sudo_flag)
end
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/)
context "when type is directory" do
before { opts[:type] = :directory }
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/, sudo: sudo_flag)
end
end
end
end
end

View File

@ -45,40 +45,45 @@ describe "VagrantPlugins::GuestLinux::Cap::FileSystem" do
let(:cap) { caps.get(:decompress_tgz) }
let(:comp) { "compressed_file" }
let(:dest) { "path/to/destination" }
let(:opts) { {} }
before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") }
after{ cap.decompress_tgz(machine, comp, dest, opts) }
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
[false, true].each do |sudo_flag|
context "sudo flag: #{sudo_flag}" do
let(:opts) { {sudo: sudo_flag} }
it "should extract file with tar" do
expect(comm).to receive(:execute).with(/tar/)
end
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/)
end
it "should extract file with tar" do
expect(comm).to receive(:execute).with(/tar/, sudo: sudo_flag)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/, sudo: sudo_flag)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/, sudo: sudo_flag)
end
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/, sudo: sudo_flag)
end
context "when type is directory" do
before { opts[:type] = :directory }
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/, sudo: sudo_flag)
end
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/)
context "when type is directory" do
before { opts[:type] = :directory }
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/, sudo: sudo_flag)
end
end
end
end
end
@ -87,40 +92,45 @@ describe "VagrantPlugins::GuestLinux::Cap::FileSystem" do
let(:cap) { caps.get(:decompress_zip) }
let(:comp) { "compressed_file" }
let(:dest) { "path/to/destination" }
let(:opts) { {} }
before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") }
after{ cap.decompress_zip(machine, comp, dest, opts) }
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
[false, true].each do |sudo_flag|
context "sudo flag: #{sudo_flag}" do
let(:opts) { {sudo: sudo_flag} }
it "should extract file with zip" do
expect(comm).to receive(:execute).with(/zip/)
end
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/)
end
it "should extract file with zip" do
expect(comm).to receive(:execute).with(/zip/, sudo: sudo_flag)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/, sudo: sudo_flag)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/, sudo: sudo_flag)
end
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/, sudo: sudo_flag)
end
context "when type is directory" do
before { opts[:type] = :directory }
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/, sudo: sudo_flag)
end
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/)
context "when type is directory" do
before { opts[:type] = :directory }
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/, sudo: sudo_flag)
end
end
end
end
end
@ -130,66 +140,73 @@ describe "VagrantPlugins::GuestLinux::Cap::FileSystem" do
let(:dirs) { %w(dir1 dir2) }
before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") }
after { expect(cap.create_directories(machine, dirs)).to eql(dirs) }
context "passes directories to be create" do
let(:temp_file) do
double("temp_file").tap do |temp_file|
allow(temp_file).to receive(:binmode)
allow(temp_file).to receive(:close)
allow(temp_file).to receive(:path).and_return("temp_path")
allow(temp_file).to receive(:unlink)
end
end
let(:sudo_block) do
Proc.new do |arg, &proc|
lines = arg.split("\n")
expect(lines[lines.length - 2]).to match(/TMP_DIR/)
dirs.each do |dir|
proc.call :stdout, "mkdir: created directory '#{dir}'\n"
[false, true].each do |sudo_flag|
context "sudo flag: #{sudo_flag}" do
let(:opts) { {sudo: sudo_flag} }
after { expect(cap.create_directories(machine, dirs, opts)).to eql(dirs) }
context "passes directories to be create" do
let(:temp_file) do
double("temp_file").tap do |temp_file|
allow(temp_file).to receive(:binmode)
allow(temp_file).to receive(:close)
allow(temp_file).to receive(:path).and_return("temp_path")
allow(temp_file).to receive(:unlink)
end
end
let(:exec_block) do
Proc.new do |arg, &proc|
lines = arg.split("\n")
expect(lines[lines.length - 2]).to match(/TMP_DIR/)
dirs.each do |dir|
proc.call :stdout, "mkdir: created directory '#{dir}'\n"
end
end
end
before do
allow(Tempfile).to receive(:new).and_return(temp_file)
allow(temp_file).to receive(:write)
allow(temp_file).to receive(:close)
allow(comm).to receive(:upload)
allow(comm).to receive(:execute, &exec_block)
end
it "creates temporary file on guest" do
expect(cap).to receive(:create_tmp_path)
end
it "creates a temporary file to write dir list" do
expect(Tempfile).to receive(:new).and_return(temp_file)
end
it "writes dir list to a local temporary file" do
expect(temp_file).to receive(:write).with(dirs.join("\n") + "\n")
end
it "uploads the local temporary file with dir list to guest" do
expect(comm).to receive(:upload).with("temp_path", "TMP_DIR")
end
it "executes bash script to create directories on guest" do
expect(comm).to receive(:execute, &exec_block).with(/bash -c .*/, sudo: sudo_flag)
end
end
end
before do
allow(Tempfile).to receive(:new).and_return(temp_file)
allow(temp_file).to receive(:write)
allow(temp_file).to receive(:close)
allow(comm).to receive(:upload)
allow(comm).to receive(:sudo, &sudo_block)
end
context "passes empty dir list" do
let(:dirs) { [] }
it "creates temporary file on guest" do
expect(cap).to receive(:create_tmp_path)
end
after { expect(cap.create_directories(machine, dirs, opts)).to eql([]) }
it "creates a temporary file to write dir list" do
expect(Tempfile).to receive(:new).and_return(temp_file)
end
it "writes dir list to a local temporary file" do
expect(temp_file).to receive(:write).with(dirs.join("\n") + "\n")
end
it "uploads the local temporary file with dir list to guest" do
expect(comm).to receive(:upload).with("temp_path", "TMP_DIR")
end
it "executes bash script to create directories on guest" do
expect(comm).to receive(:sudo, &sudo_block)
end
end
context "passes empty dir list" do
let(:dirs) { [] }
after { expect(cap.create_directories(machine, dirs)).to eql([]) }
it "does nothing" do
expect(cap).to receive(:create_tmp_path).never
expect(Tempfile).to receive(:new).never
expect(comm).to receive(:upload).never
expect(comm).to receive(:sudo).never
it "does nothing" do
expect(cap).to receive(:create_tmp_path).never
expect(Tempfile).to receive(:new).never
expect(comm).to receive(:upload).never
expect(comm).to receive(:execute).never
end
end
end
end
end

View File

@ -45,40 +45,45 @@ describe "VagrantPlugins::GuestSolaris::Cap::FileSystem" do
let(:cap) { caps.get(:decompress_tgz) }
let(:comp) { "compressed_file" }
let(:dest) { "path/to/destination" }
let(:opts) { {} }
before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") }
after{ cap.decompress_tgz(machine, comp, dest, opts) }
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
[false, true].each do |sudo_flag|
context "sudo flag: #{sudo_flag}" do
let(:opts) { {sudo: sudo_flag} }
it "should extract file with tar" do
expect(comm).to receive(:execute).with(/tar/)
end
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/)
end
it "should extract file with tar" do
expect(comm).to receive(:execute).with(/tar/, sudo: sudo_flag)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/, sudo: sudo_flag)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/, sudo: sudo_flag)
end
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/, sudo: sudo_flag)
end
context "when type is directory" do
before { opts[:type] = :directory }
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/, sudo: sudo_flag)
end
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/)
context "when type is directory" do
before { opts[:type] = :directory }
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/, sudo: sudo_flag)
end
end
end
end
end
@ -87,40 +92,45 @@ describe "VagrantPlugins::GuestSolaris::Cap::FileSystem" do
let(:cap) { caps.get(:decompress_zip) }
let(:comp) { "compressed_file" }
let(:dest) { "path/to/destination" }
let(:opts) { {} }
before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") }
after{ cap.decompress_zip(machine, comp, dest, opts) }
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
[false, true].each do |sudo_flag|
context "sudo flag: #{sudo_flag}" do
let(:opts) { {sudo: sudo_flag} }
it "should extract file with zip" do
expect(comm).to receive(:execute).with(/zip/)
end
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/)
end
it "should extract file with zip" do
expect(comm).to receive(:execute).with(/zip/, sudo: sudo_flag)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/, sudo: sudo_flag)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/, sudo: sudo_flag)
end
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/, sudo: sudo_flag)
end
context "when type is directory" do
before { opts[:type] = :directory }
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/, sudo: sudo_flag)
end
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/)
context "when type is directory" do
before { opts[:type] = :directory }
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/, sudo: sudo_flag)
end
end
end
end
end

View File

@ -1,260 +0,0 @@
require_relative "../../../../base"
require Vagrant.source_root.join("plugins/providers/hyperv/cap/sync_folder")
describe VagrantPlugins::HyperV::Cap::SyncFolder 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(:driver) { double("driver") }
let(:provider) do
double("provider").tap do |provider|
allow(provider).to receive(:driver).and_return(driver)
end
end
let(:vm_id) { 'vm_id' }
let(:machine) do
iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m|
allow(m).to receive(:guest).and_return(guest)
allow(m).to receive(:provider).and_return(provider)
allow(m).to receive(:id).and_return(vm_id)
end
end
let(:hostpath) { 'vagrant' }
let(:exclude) { [".git/"] }
let(:expanded_hostpaths) do
{ Windows: 'C:\vagrant', WSL: "/vagrant" }
end
let(:guestpaths) do
{ windows: 'C:\vagrant', linux: "/vagrant" }
end
let(:paths) do
{ Windows: %w[C:\vagrant C:\vagrant\.vagrant C:\vagrant\.git C:\vagrant\test],
WSL: %w[/vagrant /vagrant/.vagrant /vagrant/.git /vagrant/test] }
end
let(:separators) do
{ Windows: '\\', WSL: "/" }
end
let(:shared_folder) do
{ hostpath: 'vagrant',
owner: "vagrant",
group: "vagrant",
exclude: [".git/"] }
end
def random_name
(0...8).map { ('a'..'z').to_a[rand(26)] }.join
end
def generate_random_file(files, path, separator, is_directory: true)
prefix = is_directory ? "dir" : "file"
fn = [path, "#{prefix}_#{random_name}"].join(separator)
files << fn
allow(VagrantPlugins::HyperV::SyncHelper).to receive(:directory?).with(fn).and_return(is_directory)
fn
end
def generate_test_data(paths, separator)
files = []
excludes = { dirs: [], files: [] }
includes = { dirs: [], files: [] }
paths.map do |dir|
files << dir
excluded = dir.end_with?('.vagrant', '.git')
allow(VagrantPlugins::HyperV::SyncHelper).to receive(:directory?).with(dir).and_return(true)
(0..10).map do
fn = generate_random_file(files, dir, separator, is_directory: false)
if excluded
excludes[:files] << fn
else
includes[:files] << fn
end
end
sub_dir = generate_random_file(files, dir, separator, is_directory: true)
if excluded
excludes[:dirs] << dir
excludes[:dirs] << sub_dir
else
includes[:dirs] << dir
includes[:dirs] << sub_dir
end
(0..10).map do
fn = generate_random_file(files, sub_dir, separator, is_directory: false)
if excluded
excludes[:files] << fn
else
includes[:files] << fn
end
end
end
{ files: files,
excludes: excludes,
includes: includes }
end
def convert_path(mapping, path, host_type, guest_type, is_file: true)
win_path = path.gsub "/vagrant", 'C:\vagrant'
win_path.tr! "/", '\\'
linux_path = path.gsub 'C:\vagrant', "/vagrant"
linux_path.tr! '\\', "/"
dir_win_path = is_file ? win_path.split("\\")[0..-2].join("\\") : win_path
dir_win_path = dir_win_path[0..-2] if dir_win_path.end_with? '\\', '/'
dir_linux_path = is_file ? linux_path.split("/")[0..-2].join("/") : linux_path
dir_linux_path = dir_linux_path[0..-2] if dir_linux_path.end_with? '\\', '/'
mapping[win_path] =
if host_type == :WSL
if guest_type == :linux
dir_linux_path
else
dir_win_path
end
else
# windows
if guest_type == :linux
dir_linux_path
else
dir_win_path
end
end
end
describe "#sync_folder" do
%i[windows linux].map do |guest_type|
context "syncs folders to #{guest_type} guest" do
%i[Windows WSL].map do |host_type|
context "in #{host_type} environment" do
let(:host_type) { host_type }
let(:guest_type) { guest_type }
let(:separator) { separators[host_type] }
let(:input_paths) { paths[host_type] }
let(:expanded_hostpath) { expanded_hostpaths[host_type] }
let(:expanded_hostpath_windows) { expanded_hostpaths[:Windows] }
let(:guestpath) { guestpaths[guest_type] }
let(:test_data) { generate_test_data input_paths, separator }
let(:includes) { test_data[:includes] }
let(:dir_mappings) do
mappings = {}
includes[:dirs].map do |dir|
convert_path(mappings, dir, host_type, guest_type, is_file: false)
end
mappings
end
let(:files_mappings) do
mappings = {}
includes[:files].map do |file|
convert_path(mappings, file, host_type, guest_type, is_file: true)
end
mappings
end
let(:opts) do
shared_folder.dup.tap do |opts|
opts[:guestpath] = guestpath
end
end
before do
allow(guest).to receive(:name).and_return(guest_type)
allow(VagrantPlugins::HyperV::SyncHelper).to receive(:expand_path).
with(hostpath).and_return(expanded_hostpath)
allow(Vagrant::Util::Platform).to receive(:windows_path).
with(expanded_hostpath, :disable_unc).and_return(expanded_hostpath_windows)
allow(described_class).to receive(:find_includes).
with(hostpath, exclude).and_return(includes)
allow(guest).to receive(:capability).with(:create_directories, dir_mappings.values)
allow(driver).to receive(:sync_files).
with(vm_id, dir_mappings, files_mappings, is_win_guest: guest_type == :windows)
end
after { expect(described_class.sync_folder(machine, opts)) }
it "expands host path to full path" do
expect(VagrantPlugins::HyperV::SyncHelper).to receive(:expand_path).
with(hostpath).and_return(expanded_hostpath)
end
it "formats expanded full path to windows path" do
expect(Vagrant::Util::Platform).to receive(:windows_path).
with(expanded_hostpath, :disable_unc).and_return(expanded_hostpath_windows)
end
it "finds all files included in transfer" do
expect(described_class).to receive(:find_includes).
with(hostpath, exclude).and_return(includes)
end
it "calls create_directories to make directories" do
expect(guest).to receive(:capability).with(:create_directories, dir_mappings.values)
end
it "calls driver #sync_files to sync files" do
expect(driver).to receive(:sync_files).
with(vm_id, dir_mappings, files_mappings, is_win_guest: guest_type == :windows)
end
end
end
end
end
end
describe "#find_includes" do
%i[windows linux].map do |guest_type|
context "#{guest_type} guest" do
%i[Windows WSL].map do |host_type|
context "in #{host_type} environment" do
let(:host_type) { host_type }
let(:separator) { separators[host_type] }
let(:input_paths) { paths[host_type] }
let(:expanded_hostpath) { expanded_hostpaths[host_type] }
let(:test_data) { generate_test_data input_paths, separator }
let(:test_files) { test_data[:files] }
let(:test_includes) { test_data[:includes] }
let(:test_excludes) { test_data[:excludes] }
before do
allow(VagrantPlugins::HyperV::SyncHelper).to receive(:expand_path).
with(hostpath).and_return(expanded_hostpath)
allow(VagrantPlugins::HyperV::SyncHelper).to receive(:expand_excludes).
with(hostpath, exclude).and_return(test_excludes)
allow(Find).to receive(:find).with(expanded_hostpath) do |arg, &proc|
test_files.map do |file|
proc.call file
end
end
end
after do
expect(described_class.send(:find_includes, hostpath, exclude)).to eq(test_includes)
end
it "expands host path to full path" do
allow(VagrantPlugins::HyperV::SyncHelper).to receive(:expand_path).
with(hostpath).and_return(expanded_hostpath)
end
it "expands excluded files and directories for exclusion" do
allow(VagrantPlugins::HyperV::SyncHelper).to receive(:expand_excludes).
with(hostpath, exclude).and_return(test_excludes)
end
it "locates all files in expanded host path" do
expect(Find).to receive(:find).with(expanded_hostpath)
end
end
end
end
end
end
end

View File

@ -1,3 +1,6 @@
require 'find'
require 'zip'
require_relative "../../../base"
require Vagrant.source_root.join("plugins/providers/hyperv/helper")
@ -5,6 +8,14 @@ require Vagrant.source_root.join("plugins/providers/hyperv/helper")
describe VagrantPlugins::HyperV::SyncHelper do
subject { described_class }
let(:vm_id) { "vm_id" }
let(:guest) { double("guest") }
let(:comm) { double("comm") }
let(:machine) { double("machine", provider: provider, guest: guest, id: vm_id, communicate: comm) }
let(:provider) { double("provider", driver: driver) }
let(:driver) { double("driver") }
let(:separators) { { Windows: '\\', WSL: "/" } }
def random_name
(0...8).map { ('a'..'z').to_a[rand(26)] }.join
end
@ -13,10 +24,82 @@ describe VagrantPlugins::HyperV::SyncHelper do
prefix = is_directory ? "dir" : "file"
fn = [path, "#{prefix}_#{random_name}"].join(separator)
files << fn
allow(VagrantPlugins::HyperV::SyncHelper).to receive(:directory?).with(fn).and_return(is_directory)
allow(subject).to receive(:directory?).with(fn).and_return(is_directory)
fn
end
def generate_test_data(paths, separator)
files = []
excludes = { dirs: [], files: [] }
includes = { dirs: [], files: [] }
paths.map do |dir|
files << dir
excluded = dir.end_with?('.vagrant', '.git')
allow(VagrantPlugins::HyperV::SyncHelper).to receive(:directory?).with(dir).and_return(true)
(0..10).map do
fn = generate_random_file(files, dir, separator, is_directory: false)
if excluded
excludes[:files] << fn
else
includes[:files] << fn
end
end
sub_dir = generate_random_file(files, dir, separator, is_directory: true)
if excluded
excludes[:dirs] << dir
excludes[:dirs] << sub_dir
else
includes[:dirs] << dir
includes[:dirs] << sub_dir
end
(0..10).map do
fn = generate_random_file(files, sub_dir, separator, is_directory: false)
if excluded
excludes[:files] << fn
else
includes[:files] << fn
end
end
end
{ files: files,
excludes: excludes,
includes: includes }
end
def convert_path(mapping, path, host_type, guest_type, is_file: true)
win_path = path.gsub "/vagrant", 'C:\vagrant'
win_path.tr! "/", '\\'
linux_path = path.gsub 'C:\vagrant', "/vagrant"
linux_path.tr! '\\', "/"
dir_win_path = is_file ? win_path.split("\\")[0..-2].join("\\") : win_path
dir_win_path = dir_win_path[0..-2] if dir_win_path.end_with? '\\', '/'
dir_linux_path = is_file ? linux_path.split("/")[0..-2].join("/") : linux_path
dir_linux_path = dir_linux_path[0..-2] if dir_linux_path.end_with? '\\', '/'
guest_path =
if host_type == :WSL
if guest_type == :linux
dir_linux_path
else
dir_win_path
end
else
# windows
if guest_type == :linux
dir_linux_path
else
dir_win_path
end
end
mapping[:hyperv][win_path] = guest_path
mapping[:platform][host_type == :Windows ? win_path : linux_path] = guest_path
end
describe "#expand_excludes" do
let(:hostpath) { 'vagrant' }
let(:expanded_hostpaths) do
@ -24,7 +107,6 @@ describe VagrantPlugins::HyperV::SyncHelper do
end
let(:exclude) { [".git/"] }
let(:exclude_dirs) { %w[.vagrant/ .git/] }
let(:separators) { { Windows: '\\', WSL: "/" } }
%i[Windows WSL].map do |host_type|
context "in #{host_type} environment" do
@ -74,6 +156,618 @@ describe VagrantPlugins::HyperV::SyncHelper do
end
end
describe "#sync_single" do
let(:hostpath) { 'vagrant' }
let(:expanded_hostpaths) do
{ Windows: 'C:\vagrant', WSL: "/vagrant" }
end
let(:guestpaths) do
{ windows: 'C:\vagrant', linux: "/vagrant" }
end
let(:remote_guestdirs) do
{ windows: 'C:\Windows\tmp', linux: "/tmp" }
end
let(:paths) do
{ Windows: %w[C:\vagrant C:\vagrant\.vagrant C:\vagrant\.git C:\vagrant\test],
WSL: %w[/vagrant /vagrant/.vagrant /vagrant/.git /vagrant/test] }
end
let(:exclude) { [".git/"] }
let(:ssh_info) { { username: "vagrant" } }
let(:no_compression) { false }
%i[windows linux].map do |guest_type|
context "#{guest_type} guest" do
let(:guest_type) { guest_type }
let(:is_win_guest) { guest_type == :windows }
let(:guestpath) { guestpaths[guest_type] }
let(:remote_guestdir) { remote_guestdirs[guest_type] }
before { allow(guest).to receive(:name).and_return(guest_type) }
%i[Windows WSL].map do |host_type|
let(:host_type) { host_type }
let(:separator) { separators[host_type] }
let(:input_paths) { paths[host_type] }
let(:expanded_hostpath) { expanded_hostpaths[host_type] }
let(:test_data) { generate_test_data input_paths, separator }
let(:test_includes) { test_data[:includes] }
let(:folder_opts) do
h = { hostpath: hostpath,
guestpath: guestpath,
exclude: exclude }
h = !no_compression ? h : h.dup.merge({no_compression: no_compression})
h
end
before do
allow(subject).to receive(:expand_path).
with(hostpath).and_return(expanded_hostpath)
end
after { subject.sync_single(machine, ssh_info, folder_opts) }
context "in #{host_type} environment" do
before do
allow(subject).to receive(:find_includes).with(hostpath, exclude).and_return(test_includes)
end
context "with no compression" do
let(:no_compression) { true }
let(:dir_mappings) do
mappings = { hyperv: {}, platform: {} }
test_includes[:dirs].map do |dir|
convert_path(mappings, dir, host_type, guest_type, is_file: false)
end
mappings
end
let(:files_mappings) do
mappings = { hyperv: {}, platform: {} }
test_includes[:files].map do |file|
convert_path(mappings, file, host_type, guest_type, is_file: true)
end
mappings
end
before do
allow(subject).to receive(:path_mapping).
with(hostpath, guestpath, test_includes, is_win_guest: is_win_guest).
and_return({dirs: dir_mappings, files: files_mappings})
allow(subject).to receive(:remove_directory).
with(machine, guestpath, is_win_guest: is_win_guest, sudo: true)
allow(guest).to receive(:capability).
with(:create_directories, dir_mappings[:hyperv].values, sudo: true)
allow(subject).to receive(:hyperv_copy?).with(machine).and_return(true)
allow(driver).to receive(:sync_files).
with(machine.id, dir_mappings[:hyperv], files_mappings[:hyperv], is_win_guest: is_win_guest)
end
context "copy with Hyper-V daemons" do
it "calls driver#sync_files to sync all files at once" do
expect(driver).to receive(:sync_files).
with(machine.id, dir_mappings[:hyperv], files_mappings[:hyperv], is_win_guest: is_win_guest)
end
end
context "copy with WinRM" do
let(:stat) { double("stat", symlink?: false)}
before do
allow(subject).to receive(:hyperv_copy?).with(machine).and_return(false)
allow(subject).to receive(:file_exist?).and_return(true)
allow(subject).to receive(:file_stat).and_return(stat)
allow(comm).to receive(:upload)
end
it "calls WinRM to upload files" do
files_mappings[:platform].each do |host_path, guest_path|
expect(subject).to receive(:file_exist?).ordered.with(host_path).and_return(true)
expect(subject).to receive(:file_stat).ordered.with(host_path).and_return(stat)
expect(comm).to receive(:upload).ordered.with(host_path, guest_path)
end
end
end
it "removes destination dir and creates directory structure on guest" do
expect(subject).to receive(:remove_directory).
with(machine, guestpath, is_win_guest: is_win_guest, sudo: true)
expect(guest).to receive(:capability).
with(:create_directories, dir_mappings[:hyperv].values, sudo: true)
end
end
context "with compression" do
let(:compression_type) { guest_type == :windows ? :zip : :tgz }
let(:remote_guestpath) { [remote_guestdir, "remote_#{compression_type}"].join separator }
let(:archive_name) { [expanded_hostpath, "vagrant_tmp.#{compression_type}"].join separator }
before do
allow(subject).to receive(:compress_source_zip).
with(expanded_hostpath, test_includes[:files]).and_return(archive_name)
allow(subject).to receive(:compress_source_tgz).
with(expanded_hostpath, test_includes[:files]).and_return(archive_name)
allow(guest).to receive(:capability).
with(:create_tmp_path, extension: ".#{compression_type}").and_return(remote_guestpath)
allow(subject).to receive(:upload_file).
with(machine, archive_name, remote_guestpath, is_win_guest: is_win_guest)
allow(subject).to receive(:remove_directory).
with(machine, guestpath, is_win_guest: is_win_guest, sudo: true)
allow(guest).to receive(:capability).
with("decompress_#{compression_type}".to_sym, remote_guestpath, guestpath, type: :directory, sudo: true)
allow(subject).to receive(:file_exist?).with(archive_name).and_return(true)
allow(FileUtils).to receive(:rm_f).with(archive_name)
end
it "compresses the host directory to archive" do
expect(subject).to receive("compress_source_#{compression_type}".to_sym).
with(expanded_hostpath, test_includes[:files]).and_return(archive_name)
end
it "creates temporary path on guest" do
expect(guest).to receive(:capability).
with(:create_tmp_path, extension: ".#{compression_type}").and_return(remote_guestpath)
end
it "uploads archive file to temporary path on guest" do
allow(subject).to receive(:upload_file).
with(machine, archive_name, remote_guestpath, is_win_guest: is_win_guest)
end
it "removes destination dir and decompresses archive file at temporary path on guest" do
expect(subject).to receive(:remove_directory).
with(machine, guestpath, is_win_guest: is_win_guest, sudo: true)
expect(guest).to receive(:capability).
with("decompress_#{compression_type}".to_sym, remote_guestpath, guestpath, type: :directory, sudo: true)
end
it "removes temporary archive file" do
expect(FileUtils).to receive(:rm_f).with(archive_name)
end
end
end
end
end
end
end
describe "#find_includes" do
let(:hostpath) { 'vagrant' }
let(:expanded_hostpaths) do
{ Windows: 'C:\vagrant', WSL: "/vagrant" }
end
let(:exclude) { [".git/"] }
let(:paths) do
{ Windows: %w[C:\vagrant C:\vagrant\.vagrant C:\vagrant\.git C:\vagrant\test],
WSL: %w[/vagrant /vagrant/.vagrant /vagrant/.git /vagrant/test] }
end
%i[windows linux].map do |guest_type|
context "#{guest_type} guest" do
%i[Windows WSL].map do |host_type|
context "in #{host_type} environment" do
let(:host_type) { host_type }
let(:separator) { separators[host_type] }
let(:input_paths) { paths[host_type] }
let(:expanded_hostpath) { expanded_hostpaths[host_type] }
let(:test_data) { generate_test_data input_paths, separator }
let(:test_files) { test_data[:files] }
let(:test_includes) { test_data[:includes] }
let(:test_excludes) { test_data[:excludes] }
before do
allow(subject).to receive(:expand_path).
with(hostpath).and_return(expanded_hostpath)
allow(subject).to receive(:expand_excludes).
with(hostpath, exclude).and_return(test_excludes)
allow(Find).to receive(:find).with(expanded_hostpath) do |arg, &proc|
test_files.map do |file|
proc.call file
end
end
end
after do
expect(described_class.send(:find_includes, hostpath, exclude)).to eq(test_includes)
end
it "expands host path to full path" do
allow(subject).to receive(:expand_path).
with(hostpath).and_return(expanded_hostpath)
end
it "expands excluded files and directories for exclusion" do
allow(subject).to receive(:expand_excludes).
with(hostpath, exclude).and_return(test_excludes)
end
it "locates all files in expanded host path" do
expect(Find).to receive(:find).with(expanded_hostpath)
end
end
end
end
end
end
describe "#path_mapping" do
let(:hostpath) { 'vagrant' }
let(:expanded_hostpaths) do
{ Windows: 'C:\vagrant', WSL: "/vagrant" }
end
let(:guestpaths) do
{ windows: 'C:\vagrant', linux: "/vagrant" }
end
let(:paths) do
{ Windows: %w[C:\vagrant C:\vagrant\.vagrant C:\vagrant\.git C:\vagrant\test],
WSL: %w[/vagrant /vagrant/.vagrant /vagrant/.git /vagrant/test] }
end
%i[windows linux].map do |guest_type|
context "maps dirs and files for copy on #{guest_type} guest" do
let(:guest_type) { guest_type }
let(:is_win_guest) { guest_type == :windows }
%i[Windows WSL].map do |host_type|
context "in #{host_type} environment" do
let(:is_windows) { host_type == :Windows }
let(:host_type) { host_type }
let(:separator) { separators[host_type] }
let(:input_paths) { paths[host_type] }
let(:expanded_hostpath) { expanded_hostpaths[host_type] }
let(:expanded_hostpath_windows) { expanded_hostpaths[:Windows] }
let(:guestpath) { guestpaths[guest_type] }
let(:test_data) { generate_test_data input_paths, separator }
let(:includes) { test_data[:includes] }
let(:dir_mappings) do
mappings = { hyperv: {}, platform: {} }
includes[:dirs].map do |dir|
convert_path(mappings, dir, host_type, guest_type, is_file: false)
end
mappings
end
let(:files_mappings) do
mappings = { hyperv: {}, platform: {} }
includes[:files].map do |file|
convert_path(mappings, file, host_type, guest_type, is_file: true)
end
mappings
end
let(:opts) do
shared_folder.dup.tap do |opts|
opts[:guestpath] = guestpath
end
end
before do
allow(subject).to receive(:expand_path).
with(hostpath).and_return(expanded_hostpath)
allow(Vagrant::Util::Platform).to receive(:wsl?).and_return(!is_windows)
allow(Vagrant::Util::Platform).to receive(:windows_path).
with(expanded_hostpath, :disable_unc).and_return(expanded_hostpath_windows)
end
after { expect(subject.path_mapping(hostpath, guestpath, includes, is_win_guest: is_win_guest)).to eq({ dirs: dir_mappings, files: files_mappings }) }
it "expands host path to full path" do
expect(subject).to receive(:expand_path).
with(hostpath).and_return(expanded_hostpath)
end
it "formats expanded full path to windows path" do
expect(Vagrant::Util::Platform).to receive(:windows_path).
with(expanded_hostpath, :disable_unc).and_return(expanded_hostpath_windows)
end
end
end
end
end
end
describe "#compress_source_zip" do
let(:windows_temps) { { Windows: 'C:\Windows\tmp', WSL: "/mnt/c/Windows/tmp" } }
let(:paths) do
{ Windows: 'C:\vagrant', WSL: "/vagrant" }
end
let(:dir_items) { { Windows: 'C:\vagrant\dir1', WSL: '/vagrant/dir1' } }
let(:source_items) { { Windows: %w(C:\vagrant\file1 C:\vagrant\file2 C:\vagrant\dir1 C:\vagrant\dir1\file2),
WSL: %w(/vagrant/file1 /vagrant/file2 /vagrant/dir1 /vagrant/dir1/file2) } }
%i[Windows WSL].map do |host_type|
context "in #{host_type} environment" do
let(:is_windows) { host_type == :Windows }
let(:host_type) { host_type }
let(:separator) { separators[host_type] }
let(:windows_temp) { windows_temps[host_type] }
let(:path) { paths[host_type] }
let(:dir_item) { dir_items[host_type] }
let(:platform_source_items) { source_items[host_type] }
let(:zip_path) { [windows_temp, "vagrant_test_tmp.zip"].join separator }
let(:zip_file) { double("zip_file", path: zip_path) }
let(:zip) { double("zip") }
let(:stat) { double("stat", symlink?: false)}
let(:zip_output_stream) { double("zip_output_stream") }
let(:source_file) { double("source_file") }
let(:content) { "ABC" }
before do
allow(subject).to receive(:format_windows_temp).and_return(windows_temp)
allow(Tempfile).to receive(:create).and_return(zip_file)
allow(zip_file).to receive(:close)
allow(Zip::File).to receive(:open).with(zip_path, Zip::File::CREATE).and_yield(zip)
allow(subject).to receive(:file_exist?).and_return(true)
allow(subject).to receive(:file_stat).and_return(stat)
allow(subject).to receive(:directory?).and_return(false)
allow(subject).to receive(:directory?).with(dir_item).and_return(true)
allow(zip).to receive(:get_entry).with(/[\.|dir?]/).and_raise(Errno::ENOENT)
allow(zip).to receive(:mkdir).with(/[\.|dir?]/)
allow(zip).to receive(:get_output_stream).with(/.*file?/).and_yield(zip_output_stream)
allow(File).to receive(:open).with(/.*/, "rb").and_return(source_file)
allow(source_file).to receive(:read).with(2048).and_return(content, nil)
allow(zip_output_stream).to receive(:write).with(content)
end
after { expect(subject.compress_source_zip(path, platform_source_items)).to eq(zip_path) }
it "creates a temporary file for writing" do
expect(subject).to receive(:format_windows_temp).and_return(windows_temp)
expect(Tempfile).to receive(:create).and_return(zip_file)
expect(Zip::File).to receive(:open).with(zip_path, Zip::File::CREATE).and_yield(zip)
end
it "skips directory" do
expect(File).to receive(:open).with(dir_item, "rb").never
end
it "writes file content to zip archive" do
expect(File).to receive(:open).with(/.*/, "rb").and_return(source_file)
expect(source_file).to receive(:read).with(2048).and_return(content, nil)
expect(zip_output_stream).to receive(:write).with(content)
end
it "processes all files" do
allow(zip).to receive(:get_output_stream).with(/.*file?/).exactly(platform_source_items.length - 1).times
end
end
end
end
describe "#compress_source_tgz" do
let(:windows_temps) { { Windows: 'C:\Windows\tmp', WSL: "/mnt/c/Windows/tmp" } }
let(:paths) do
{ Windows: 'C:\vagrant', WSL: "/vagrant" }
end
let(:dir_items) { { Windows: 'C:\vagrant\dir1', WSL: '/vagrant/dir1' } }
let(:symlink_items) { { Windows: 'C:\vagrant\file2', WSL: '/vagrant/file2' } }
let(:source_items) { { Windows: %w(C:\vagrant\file1 C:\vagrant\file2 C:\vagrant\dir1 C:\vagrant\dir1\file2),
WSL: %w(/vagrant/file1 /vagrant/file2 /vagrant/dir1 /vagrant/dir1/file2) } }
%i[Windows WSL].map do |host_type|
context "in #{host_type} environment" do
let(:is_windows) { host_type == :Windows }
let(:host_type) { host_type }
let(:separator) { separators[host_type] }
let(:windows_temp) { windows_temps[host_type] }
let(:path) { paths[host_type] }
let(:dir_item) { dir_items[host_type] }
let(:symlink_item) { symlink_items[host_type] }
let(:platform_source_items) { source_items[host_type] }
let(:tar_path) { [windows_temp, "vagrant_test_tmp.tar"].join separator }
let(:tgz_path) { [windows_temp, "vagrant_test_tmp.tgz"].join separator }
let(:tar_file) { double("tar_file", path: tar_path) }
let(:tgz_file) { double("tgz_file", path: tgz_path) }
let(:tar) { double("tar") }
let(:tgz) { double("tgz") }
let(:file_mode) { 0744 }
let(:stat) { double("stat", symlink?: false, mode: file_mode)}
let(:stat_symlink) { double("stat_symlink", symlink?: true, mode: file_mode)}
let(:tar_io) { double("tar_io") }
let(:source_file) { double("source_file") }
let(:content) { "ABC" }
before do
allow(subject).to receive(:format_windows_temp).and_return(windows_temp)
allow(Tempfile).to receive(:create).and_return(tar_file, tgz_file)
allow(File).to receive(:open).with(tar_path, "wb+").and_return(tar_file)
allow(File).to receive(:open).with(tgz_path, "wb").and_return(tgz_file)
allow(Gem::Package::TarWriter).to receive(:new).with(tar_file).and_return(tar)
allow(Zlib::GzipWriter).to receive(:new).with(tgz_file).and_return(tgz)
allow(File).to receive(:delete).with(tar_path)
allow(tar_file).to receive(:close)
allow(tar_file).to receive(:read)
allow(tar_file).to receive(:rewind)
allow(tgz_file).to receive(:close)
allow(tar).to receive(:mkdir)
allow(tar).to receive(:add_symlink)
allow(tar).to receive(:add_file).and_yield(tar_io)
allow(tar).to receive(:close)
allow(tgz).to receive(:mkdir)
allow(tgz).to receive(:write)
allow(tgz).to receive(:close)
allow(subject).to receive(:file_exist?).and_return(true)
allow(subject).to receive(:file_stat).and_return(stat)
allow(subject).to receive(:directory?).and_return(false)
allow(subject).to receive(:directory?).with(dir_item).and_return(true)
allow(File).to receive(:open).with(/.*/, "rb").and_yield(source_file)
allow(source_file).to receive(:read).and_return(content, nil)
allow(tar_io).to receive(:write)
end
after { expect(subject.compress_source_tgz(path, platform_source_items)).to eq(tgz_path) }
it "creates temporary tar/tgz for writing" do
expect(subject).to receive(:format_windows_temp).and_return(windows_temp)
expect(Tempfile).to receive(:create).and_return(tar_file, tgz_file)
expect(File).to receive(:open).with(tar_path, "wb+").and_return(tar_file)
expect(File).to receive(:open).with(tgz_path, "wb").and_return(tgz_file)
expect(Gem::Package::TarWriter).to receive(:new).with(tar_file).and_return(tar)
expect(Zlib::GzipWriter).to receive(:new).with(tgz_file).and_return(tgz)
end
it "creates directories in tar" do
expect(tar).to receive(:mkdir).with(dir_item.sub(path, ""), file_mode)
end
it "writes file content to tar archive" do
expect(File).to receive(:open).with(/.*/, "rb").and_yield(source_file)
expect(source_file).to receive(:read).and_return(content, nil)
expect(tar_io).to receive(:write).with(content)
end
it "deletes tar file eventually" do
expect(File).to receive(:delete).with(tar_path)
end
it "processes all files" do
expect(tar).to receive(:add_file).with(/.*file?/, file_mode).exactly(platform_source_items.length - 1).times
end
it "does not treat symlink as normal file" do
allow(subject).to receive(:file_stat).with(symlink_item).and_return(stat_symlink)
expect(File).to receive(:readlink).with(symlink_item).and_return("/target")
expect(tar).to receive(:add_symlink).with(/.*file?/, "/target", file_mode)
end
end
end
end
describe "#remove_directory" do
let(:windows_path) { 'C:\Windows\Temp' }
let(:unix_path) { "C:/Windows/Temp" }
[true, false].each do |sudo_flag|
context "sudo flag: #{sudo_flag}" do
it "calls powershell script to remove directory" do
allow(subject).to receive(:to_windows_path).with(unix_path).and_return(windows_path)
expect(comm).to receive(:execute).with(/.*if \(Test-Path\(\"C:\\Windows\\Temp\"\).*\n.*Remove-Item -Path \"C:\\Windows\\Temp\".*/, shell: :powershell)
subject.remove_directory machine, unix_path, is_win_guest: true, sudo: sudo_flag
end
it "calls linux command to remove directory forcibly" do
allow(subject).to receive(:to_unix_path).with(windows_path).and_return(unix_path)
expect(comm).to receive(:test).with("test -d '#{unix_path}'").and_return(true)
expect(comm).to receive(:execute).with("rm -rf '#{unix_path}'", sudo: sudo_flag)
subject.remove_directory machine, windows_path, is_win_guest: false, sudo: sudo_flag
end
it "does not call rm when directory does not exist" do
allow(subject).to receive(:to_unix_path).with(windows_path).and_return(unix_path)
expect(comm).to receive(:test).with("test -d '#{unix_path}'").and_return(false)
expect(comm).to receive(:execute).never
subject.remove_directory machine, windows_path, is_win_guest: false, sudo: sudo_flag
end
end
end
end
describe "#format_windows_temp" do
let(:windows_temp) { 'C:\Windows\tmp' }
let(:unix_temp) { "/mnt/c/Windows/tmp" }
before { allow(Vagrant::Util::Platform).to receive(:windows_temp).and_return(windows_temp) }
it "returns Windows style temporary directory" do
allow(Vagrant::Util::Platform).to receive(:wsl?).and_return(false)
expect(subject.format_windows_temp).to eq(windows_temp)
end
it "returns Unix style temporary directory in WSL" do
allow(Vagrant::Util::Platform).to receive(:wsl?).and_return(true)
expect(Vagrant::Util::Subprocess).to receive(:execute).
with("wslpath", "-u", "-a", windows_temp).and_return(double(stdout: unix_temp, exit_code: 0))
expect(subject.format_windows_temp).to eq(unix_temp)
end
end
describe "#upload_file" do
let(:sources) { { Windows: 'C:\vagrant\file1.zip', WSL: "/vagrant/file1.zip" } }
let(:new_sources) { { Windows: 'C:\Windows\tmp\file2.zip', WSL: "/mnt/c/Windows/tmp/file2.zip" } }
let(:windows_temps) { { Windows: 'C:\Windows\tmp', WSL: "/mnt/c/Windows/tmp" } }
let(:dests) { { windows: 'C:\vagrant\file2.zip', linux: "/vagrant/file2.zip" } }
let(:dest_dirs) { { windows: 'C:\vagrant', linux: "/vagrant" } }
%i[windows linux].map do |guest_type|
context "#{guest_type} guest" do
let(:guest_type) { guest_type }
let(:dest) { dests[guest_type] }
let(:dest_dir) { dest_dirs[guest_type] }
%i[Windows WSL].map do |host_type|
context "in #{host_type} environment" do
let(:host_type) { host_type }
let(:is_windows) { host_type == :Windows }
let(:source) { sources[host_type] }
let(:new_source) { new_sources[host_type] }
context "uploads file by Hyper-V daemons when applicable" do
let(:windows_temp) { windows_temps[host_type] }
before do
allow(subject).to receive(:hyperv_copy?).with(machine).and_return(true)
allow(subject).to receive(:format_windows_temp).and_return(windows_temp)
allow(Vagrant::Util::Platform).to receive(:wsl?).and_return(!is_windows)
allow(FileUtils).to receive(:rm_f)
allow(FileUtils).to receive(:mv)
allow(subject).to receive(:hyperv_copy)
end
after { subject.upload_file machine, source, dest, is_win_guest: guest_type == :windows }
if host_type != :Windows
it "moves the source file to new path with the destination filename" do
expect(FileUtils).to receive(:mv).with(source, new_source)
expect(FileUtils).to receive(:rm_f).with(new_source)
end
end
it "calls Hyper-V cmdlet to copy file" do
expect(subject).to receive(:hyperv_copy).with(machine, new_source, dest_dir)
end
end
it "uploads file by WinRM when Hyper-V daemons are not applicable" do
allow(subject).to receive(:hyperv_copy?).with(machine).and_return(false)
expect(comm).to receive(:upload).with(source, dest)
expect(subject).to receive(:hyperv_copy).never
subject.upload_file machine, source, dest, is_win_guest: guest_type == :windows
end
end
end
end
end
end
describe "#hyperv_copy?" do
before do
allow(guest).to receive(:capability?)
allow(guest).to receive(:capability)
end
it "does not leverage Hyper-V daemons when guest does not support Hyper-V daemons" do
allow(guest).to receive(:capability?).with(:hyperv_daemons_running).and_return(false)
expect(guest).to receive(:capability).never
end
it "checks whether Hyper-V daemons are running" do
allow(guest).to receive(:capability?).with(:hyperv_daemons_running).and_return(true)
allow(guest).to receive(:capability).with(:hyperv_daemons_running).and_return(true)
expect(subject.hyperv_copy?(machine)).to eq(true)
end
end
describe "#hyperv_copy" do
let(:source) { 'C:\Windows\test' }
let(:dest_dir) { "/vagrant" }
it "calls Copy-VMFile cmdlet to copy file to guest" do
expect(Vagrant::Util::PowerShell).to receive(:execute_cmd).with(/.*Hyper-V\\Get-VM -Id \"vm_id\"\n.*Hyper-V\\Copy-VMFile -VM \$machine -SourcePath \"C:\\Windows\\test\" -DestinationPath \"#{dest_dir}\".*/)
subject.hyperv_copy(machine, source, dest_dir)
end
end
describe "#platform_join" do
it "produces Windows-style path" do
expect(subject.platform_join("C:", "vagrant", ".vagrant", "", is_windows: true)).to eq("C:\\vagrant\\.vagrant\\")
@ -84,23 +778,88 @@ describe VagrantPlugins::HyperV::SyncHelper do
end
end
describe "#sync_single" do
let(:machine) { double("machine", provider: provider) }
let(:provider) { double("provider") }
let(:ssh_info) { { username: "vagrant" } }
let(:folder_opts) do
{ hostpath: 'vagrant',
guestpath: 'C:\vagrant',
owner: "vagrant",
group: "vagrant",
exclude: [".git/"] }
describe "#platform_path" do
let(:windows_path) { 'C:\Windows\Temp' }
let(:unix_path) { "C:/Windows/Temp" }
it "returns Windows style path in Windows" do
expect(subject.platform_path(unix_path, is_windows: true)).to eq(windows_path)
end
after { subject.sync_single(machine, ssh_info, folder_opts) }
it "returns Unix style path in WSL" do
expect(subject.platform_path(windows_path, is_windows: false)).to eq(unix_path)
end
end
it "calls provider capability to sync single folder" do
expect(provider).to receive(:capability).
with(:sync_folder, folder_opts)
describe "#to_windows_path" do
let(:windows_path) { 'C:\Windows\Temp' }
let(:unix_path) { "C:/Windows/Temp" }
it "converts path with unix separator to Windows" do
expect(subject.to_windows_path(unix_path)).to eq(windows_path)
end
it "keeps the original input for Windows path" do
expect(subject.to_windows_path(windows_path)).to eq(windows_path)
end
end
describe "#to_unix_path" do
let(:windows_path) { '\usr\bin\test' }
let(:unix_path) { "/usr/bin/test" }
it "converts path with Windows separator to Unix" do
expect(subject.to_unix_path(windows_path)).to eq(unix_path)
end
it "keeps the original input for Unix path" do
expect(subject.to_unix_path(unix_path)).to eq(unix_path)
end
end
describe "#trim_head" do
let(:windows_path_no_heading) { 'usr\bin\test' }
let(:unix_path_no_heading) { "usr/bin/test" }
let(:windows_path_with_heading) { '\usr\bin\test' }
let(:unix_path_with_heading) { "/usr/bin/test" }
it "keeps Windows path with no heading" do
expect(subject.trim_head(windows_path_no_heading)).to eq(windows_path_no_heading)
end
it "keeps Unix path with no heading" do
expect(subject.trim_head(unix_path_no_heading)).to eq(unix_path_no_heading)
end
it "removes heading separator from Windows path" do
expect(subject.trim_head(windows_path_with_heading)).to eq(windows_path_no_heading)
end
it "removes heading separator from Unix path" do
expect(subject.trim_head(unix_path_with_heading)).to eq(unix_path_no_heading)
end
end
describe "#trim_tail" do
let(:windows_path_no_tailing) { '\usr\bin\test' }
let(:unix_path_no_tailing) { "/usr/bin/test" }
let(:windows_path_with_tailing) { '\usr\bin\test\\' }
let(:unix_path_with_tailing) { "/usr/bin/test/" }
it "keeps Windows path with no tailing" do
expect(subject.trim_tail(windows_path_no_tailing)).to eq(windows_path_no_tailing)
end
it "keeps Unix path with no tailing" do
expect(subject.trim_tail(unix_path_no_tailing)).to eq(unix_path_no_tailing)
end
it "removes tailing separator from Windows path" do
expect(subject.trim_tail(windows_path_with_tailing)).to eq(windows_path_no_tailing)
end
it "removes tailing separator from Unix path" do
expect(subject.trim_tail(unix_path_with_tailing)).to eq(unix_path_no_tailing)
end
end
end