diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 993ea58..698f748 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -57,3 +57,22 @@ jobs: use-verbose-mode: "yes" check-modified-files-only: "yes" base-branch: "main" + + test: + name: Unit Test with Ruby + runs-on: ubuntu-latest + needs: [yamllint, cookstyle] + strategy: + fail-fast: false + matrix: + ruby: ["3.4"] + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Rake Spec + run: bundle exec rake spec diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..1600554 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--require spec_helper +--color +--format progress diff --git a/Gemfile b/Gemfile index f1f1c3a..c82267a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ # # Author:: Noah Kantrowitz # -# Copyright 2014, Noah Kantrowitz +# Copyright:: 2014, Noah Kantrowitz # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,6 @@ # limitations under the License. # -source 'http://rubygems.org' +source "https://rubygems.org" gemspec diff --git a/README.md b/README.md index 1bb03f5..2551459 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Quick Start Run `chef gem install kitchen-sync` and then set your transport to `sftp`: -``` +```yaml transport: name: sftp ``` @@ -19,7 +19,8 @@ transport: Available Transfer Methods -------------------------- -### `sftp` +`sftp` +------ The default mode uses SFTP for file transfers, as well as a helper script to avoid recopying files that are already present on the test host. If SFTP is @@ -28,19 +29,21 @@ disabled, this will automatically fall back to the SCP mode. By default this will use the Chef omnibus Ruby, you can customize the path to Ruby via `ruby_path`: -``` +```yaml transport: name: sftp ruby_path: /usr/bin/ruby ``` -### `rsync` +`rsync` +------- -The Rsync mode is based on the work done by [Mikhail Bautin](https://github.com/test-kitchen/test-kitchen/pull/359). -This is the fastest mode, but it does have a few downsides. The biggest is that -you must be using `ssh-agent` and have an identity loaded for it to use. It also -requires that rsync be available on the remote side. Consider this implementation -more experimental than `sftp` at this time. +The Rsync mode is based on the work done by [Mikhail +Bautin](https://github.com/test-kitchen/test-kitchen/pull/359). This is the +fastest mode, but it does have a few downsides. The biggest is that you must be +using `ssh-agent` and have an identity loaded for it to use. It also requires +that rsync be available on the remote side. Consider this implementation more +experimental than `sftp` at this time. Windows Guests -------------- @@ -52,10 +55,10 @@ Upgrading from 1.x ------------------ As of version 2.0, kitchen-sync uses Test Kitchen's modular transport system -rather than monkey patch overrides. To upgrade, remove the `<% require 'kitchen-sync' %>` -from your `.kitchen.yml` and add the transport configuration mentioned above. -The `$KITCHEN_SYNC_MODE` environment variable is no longer needed as configuration -can happen in the normal Yaml file. +rather than monkey patch overrides. To upgrade, remove the `<% require +'kitchen-sync' %>` from your `.kitchen.yml` and add the transport configuration +mentioned above. The `$KITCHEN_SYNC_MODE` environment variable is no longer +needed as configuration can happen in the normal Yaml file. License ------- @@ -66,7 +69,9 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +```text +http://www.apache.org/licenses/LICENSE-2.0 +``` Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/Rakefile b/Rakefile index e3da77c..2dab3fc 100644 --- a/Rakefile +++ b/Rakefile @@ -2,7 +2,7 @@ # # Author:: Noah Kantrowitz # -# Copyright 2014, Noah Kantrowitz +# Copyright:: 2014, Noah Kantrowitz # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,17 +17,29 @@ # limitations under the License. # -require 'bundler/gem_tasks' +require "bundler/gem_tasks" begin - require 'cookstyle' - require 'rubocop/rake_task' + require "rspec/core/rake_task" + RSpec::Core::RakeTask.new(:spec) +rescue LoadError + puts "rspec is not available. (sudo) gem install rspec to enable the spec task." +end + +begin + require "cookstyle/chefstyle" + require "rubocop/rake_task" RuboCop::RakeTask.new(:style) do |task| - task.options << '--chefstyle' - task.options << '--display-cop-names' + task.options += ["--display-cop-names", "--no-color"] end rescue LoadError - puts 'cookstyle is not available. (sudo) gem install cookstyle to enable the style task.' + puts "cookstyle/chefstyle is not available. (sudo) gem install cookstyle to enable the style task." end -task default: [:style] +desc "Run all tests (alias for spec)" +task test: [:spec] + +desc "Run unit tests (alias for spec)" +task unit: [:spec] + +task default: %i{spec style} diff --git a/kitchen-sync.gemspec b/kitchen-sync.gemspec index af08e49..0ebe6a1 100644 --- a/kitchen-sync.gemspec +++ b/kitchen-sync.gemspec @@ -1,27 +1,27 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) +require "English" +lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'kitchen-sync/version' +require "kitchen-sync/version" Gem::Specification.new do |spec| - spec.name = 'kitchen-sync' + spec.name = "kitchen-sync" spec.version = KitchenSync::VERSION - spec.authors = ['Noah Kantrowitz'] - spec.email = ['noah@coderanger.net'] - spec.description = %q{Improved file transfers for for test-kitchen} + spec.authors = ["Noah Kantrowitz"] + spec.email = ["noah@coderanger.net"] + spec.description = "Improved file transfers for for test-kitchen" spec.summary = spec.description - spec.homepage = 'https://github.com/coderanger/kitchen-sync' - spec.license = 'Apache 2.0' + spec.homepage = "https://github.com/coderanger/kitchen-sync" + spec.license = "Apache 2.0" - spec.files = `git ls-files`.split($/) + spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) spec.executables = [] - spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) - spec.require_paths = ['lib'] + spec.require_paths = ["lib"] - spec.add_dependency 'test-kitchen', '>= 1.0.0' - spec.add_dependency 'net-sftp' + spec.add_dependency "test-kitchen", ">= 1.0.0" + spec.add_dependency "net-sftp" - spec.add_development_dependency 'bundler' - spec.add_development_dependency 'rake' - spec.add_development_dependency 'cookstyle' + spec.add_development_dependency "bundler" + spec.add_development_dependency "rake" + spec.add_development_dependency "rspec", "~> 3.0" + spec.add_development_dependency "cookstyle" end diff --git a/lib/kitchen-sync.rb b/lib/kitchen-sync.rb index 077a801..c2e2fe8 100644 --- a/lib/kitchen-sync.rb +++ b/lib/kitchen-sync.rb @@ -1,5 +1,5 @@ # -# Copyright 2014-2016, Noah Kantrowitz +# Copyright:: 2014-2016, Noah Kantrowitz # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ # limitations under the License. # - class KitchenSync - autoload :VERSION, 'kitchen-sync/version' + autoload :VERSION, "kitchen-sync/version" end diff --git a/lib/kitchen-sync/checksums.rb b/lib/kitchen-sync/checksums.rb index 793ac61..e980bd3 100644 --- a/lib/kitchen-sync/checksums.rb +++ b/lib/kitchen-sync/checksums.rb @@ -1,7 +1,7 @@ # # Author:: Noah Kantrowitz # -# Copyright 2014, Noah Kantrowitz +# Copyright:: 2014, Noah Kantrowitz # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,14 +16,14 @@ # limitations under the License. # -require 'json' -require 'digest/sha1' +require "json" unless defined?(JSON) +require "digest/sha1" unless defined?(Digest::SHA1) glob_path = base = ARGV.first -glob_path = File.join(glob_path, '**', '*') if File.directory?(glob_path) +glob_path = File.join(glob_path, "**", "*") if File.directory?(glob_path) d = Digest::SHA1.new STDOUT.write( - Dir.glob(glob_path, File::FNM_PATHNAME | File::FNM_DOTMATCH).inject({}) do |memo, path| + Dir.glob(glob_path, File::FNM_PATHNAME | File::FNM_DOTMATCH).each_with_object({}) do |path, memo| rel_path = path[base.length..-1] if File.file?(path) && File.readable?(path) d.reset @@ -31,6 +31,5 @@ elsif File.directory?(path) memo[rel_path] = true end - memo end.to_json ) diff --git a/lib/kitchen-sync/core_ext.rb b/lib/kitchen-sync/core_ext.rb index 12b37b6..42507ec 100644 --- a/lib/kitchen-sync/core_ext.rb +++ b/lib/kitchen-sync/core_ext.rb @@ -1,5 +1,5 @@ # -# Copyright 2014-2016, Noah Kantrowitz +# Copyright:: 2014-2016, Noah Kantrowitz # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,18 +19,16 @@ module Kitchen # Monkey patch to prevent the deletion of everything module Provisioner class ChefBase < Base - old_init_command = instance_method(:init_command) define_method(:init_command) do if (defined?(Kitchen::Transport::Sftp) && instance.transport.is_a?(Kitchen::Transport::Sftp)) || \ - (defined?(Kitchen::Transport::Rsync) && instance.transport.is_a?(Kitchen::Transport::Rsync)) - "mkdir -p #{config[:root_path]}" + (defined?(Kitchen::Transport::Rsync) && instance.transport.is_a?(Kitchen::Transport::Rsync)) + "mkdir -p #{config[:root_path]}" else - old_init_command.bind(self).() + old_init_command.bind(self).call end end - end end end diff --git a/lib/kitchen-sync/version.rb b/lib/kitchen-sync/version.rb index cd21412..5a53376 100644 --- a/lib/kitchen-sync/version.rb +++ b/lib/kitchen-sync/version.rb @@ -1,5 +1,5 @@ # -# Copyright 2014-2016, Noah Kantrowitz +# Copyright:: 2014-2016, Noah Kantrowitz # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ # limitations under the License. # - class KitchenSync - VERSION = '2.2.2.pre' + VERSION = "2.2.2.pre".freeze end diff --git a/lib/kitchen/transport/rsync.rb b/lib/kitchen/transport/rsync.rb index 4862dbd..59afdc9 100644 --- a/lib/kitchen/transport/rsync.rb +++ b/lib/kitchen/transport/rsync.rb @@ -1,5 +1,5 @@ # -# Copyright 2014-2016, Noah Kantrowitz +# Copyright:: 2014-2016, Noah Kantrowitz # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,12 +14,13 @@ # limitations under the License. # -require 'base64' +require "English" +require "base64" unless defined?(Base64) -require 'kitchen/transport/ssh' -require 'net/ssh' +require "kitchen/transport/ssh" +require "net/ssh" unless defined?(Net::SSH) -require 'kitchen-sync/core_ext' +require "kitchen-sync/core_ext" module Kitchen module Transport @@ -49,46 +50,48 @@ def create_new_connection(options, &block) class Connection < Ssh::Connection def upload(locals, remote) - if @rsync_failed || !File.exists?('/usr/bin/rsync') - logger.debug('[rsync] Rsync already failed or not installed, not trying it') + if @rsync_failed || !File.exist?("/usr/bin/rsync") + logger.debug("[rsync] Rsync already failed or not installed, not trying it") return super end locals = Array(locals) # We only try to sync folders for now and ignore the cache folder # because we don't want to --delete that. - rsync_candidates = locals.select {|path| File.directory?(path) && File.basename(path) != 'cache' } - ssh_command = "ssh #{ssh_args.join(' ')}" + rsync_candidates = locals.select { |path| File.directory?(path) && File.basename(path) != "cache" } + ssh_command = "ssh #{ssh_args.join(" ")}" copy_identity - rsync_cmd = "/usr/bin/rsync -e '#{ssh_command}' -az#{logger.level == :debug ? 'vv' : ''} --delete #{rsync_candidates.join(' ')} #{@session.options[:user]}@#{@session.host}:#{remote}" + rsync_cmd = "/usr/bin/rsync -e '#{ssh_command}' -az#{logger.level == :debug ? "vv" : ""} --delete #{rsync_candidates.join(" ")} #{@session.options[:user]}@#{@session.host}:#{remote}" logger.debug("[rsync] Running rsync command: #{rsync_cmd}") ret = [] time = Benchmark.realtime do ret << system(rsync_cmd) end - logger.info("[rsync] Time taken to upload #{rsync_candidates.join(';')} to #{self}:#{remote}: %.2f sec" % time) + logger.info(format("[rsync] Time taken to upload #{rsync_candidates.join(";")} to #{self}:#{remote}: %.2f sec", time)) unless ret.first - logger.warn("[rsync] rsync exited with status #{$?.exitstatus}, using SCP instead") + logger.warn("[rsync] rsync exited with status #{$CHILD_STATUS.exitstatus}, using SCP instead") @rsync_failed = true end # Fall back to SCP remaining = if @rsync_failed - locals - else - locals - rsync_candidates - end - logger.debug("[rsync] Using fallback to upload #{remaining.join(';')}") + locals + else + locals - rsync_candidates + end + logger.debug("[rsync] Using fallback to upload #{remaining.join(";")}") super(remaining, remote) unless remaining.empty? end # Copy your SSH identity, creating a new one if needed def copy_identity return if @copied_identity + identities = Net::SSH::Authentication::Agent.connect.identities - raise 'No SSH identities found. Please run ssh-add.' if identities.empty? + raise "No SSH identities found. Please run ssh-add." if identities.empty? + key = identities.first - enc_key = Base64.encode64(key.to_blob).gsub("\n", '') + enc_key = Base64.encode64(key.to_blob).gsub("\n", "") identitiy = "ssh-rsa #{enc_key} #{key.comment}" @session.exec! <<-EOT test -e ~/.ssh || mkdir ~/.ssh @@ -102,16 +105,15 @@ def copy_identity end def ssh_args - args = %W{ -o UserKnownHostsFile=/dev/null } - args += %W{ -o StrictHostKeyChecking=no } - args += %W{ -o IdentitiesOnly=yes } if @options[:keys] + args = %w{ -o UserKnownHostsFile=/dev/null } + args += %w{ -o StrictHostKeyChecking=no } + args += %w{ -o IdentitiesOnly=yes } if @options[:keys] args += %W{ -o LogLevel=#{@logger.debug? ? "VERBOSE" : "ERROR"} } args += %W{ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} } if @options.key? :forward_agent Array(@options[:keys]).each { |ssh_key| args += %W{ -i #{ssh_key}} } args += %W{ -p #{@session.options[:port]}} end end - end end end diff --git a/lib/kitchen/transport/sftp.rb b/lib/kitchen/transport/sftp.rb index 44b3803..18e2dc8 100644 --- a/lib/kitchen/transport/sftp.rb +++ b/lib/kitchen/transport/sftp.rb @@ -1,5 +1,5 @@ # -# Copyright 2014-2016, Noah Kantrowitz +# Copyright:: 2014-2016, Noah Kantrowitz # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,25 +14,24 @@ # limitations under the License. # -require 'benchmark' -require 'digest/sha1' -require 'json' +require "benchmark" unless defined?(Benchmark) +require "digest/sha1" unless defined?(Digest::SHA1) +require "json" unless defined?(JSON) -require 'kitchen/transport/ssh' -require 'net/sftp' - -require 'kitchen-sync/core_ext' +require "kitchen/transport/ssh" +require "net/sftp" +require "kitchen-sync/core_ext" module Kitchen module Transport class Sftp < Ssh - CHECKSUMS_PATH = File.expand_path('../../../kitchen-sync/checksums.rb', __FILE__) + CHECKSUMS_PATH = File.expand_path("../../kitchen-sync/checksums.rb", __dir__) CHECKSUMS_HASH = Digest::SHA1.file(CHECKSUMS_PATH) - CHECKSUMS_REMOTE_PATH = "/tmp/checksums-#{CHECKSUMS_HASH}.rb" # This won't work on Windows targets + CHECKSUMS_REMOTE_PATH = "/tmp/checksums-#{CHECKSUMS_HASH}.rb".freeze # This won't work on Windows targets MAX_TRANSFERS = 64 - default_config :ruby_path, '/opt/chef/embedded/bin/ruby' + default_config :ruby_path, "/opt/chef/embedded/bin/ruby" def finalize_config!(instance) super.tap do @@ -94,13 +93,12 @@ def upload(locals, remote) full_remote = File.join(remote, File.basename(local)) options = { recursive: File.directory?(local), - purge: File.basename(local) != 'cache', + purge: File.basename(local) != "cache", } - recursive = File.directory?(local) time = Benchmark.realtime do sftp_upload!(local, full_remote, options) end - logger.info("[SFTP] Time taken to upload #{local} to #{self}:#{full_remote}: %.2f sec" % time) + logger.info(format("[SFTP] Time taken to upload #{local} to #{self}:#{full_remote}: %.2f sec", time)) end end @@ -137,11 +135,9 @@ def execute_with_exit_code(command) exit_code = nil closed = false session.open_channel do |channel| - channel.request_pty channel.exec(command) do |_ch, _success| - channel.on_data do |_ch, data| logger << data end @@ -154,7 +150,7 @@ def execute_with_exit_code(command) exit_code = data.read_long end - channel.on_close do |ch| # This block is new. + channel.on_close do |_ch| # This block is new. closed = true end end @@ -179,6 +175,7 @@ def sftp_session def safe_stat(path) stat = sftp_session.lstat!(path) raise "#{path} is a symlink, possible security threat, bailing out" if stat.symlink? + true rescue Net::SFTP::StatusException false @@ -190,6 +187,7 @@ def safe_stat(path) def copy_checksums_script! # Fast path because upload itself is called multiple times. return if @checksums_copied + # Only try to transfer the script if it isn't present. a stat takes about # 1/3rd the time of the transfer, so worst case here is still okay. sftp_session.upload!(CHECKSUMS_PATH, CHECKSUMS_REMOTE_PATH) unless safe_stat(CHECKSUMS_REMOTE_PATH) @@ -198,13 +196,14 @@ def copy_checksums_script! def files_to_upload(checksums, local, recursive) glob_path = if recursive - File.join(local, '**', '*') - else - local - end + File.join(local, "**", "*") + else + local + end pending = [] Dir.glob(glob_path, File::FNM_PATHNAME | File::FNM_DOTMATCH).each do |path| next unless File.file?(path) + rel_path = path[local.length..-1] remote_hash = checksums.delete(rel_path) pending << rel_path unless remote_hash && remote_hash == Digest::SHA1.file(path).hexdigest @@ -213,17 +212,17 @@ def files_to_upload(checksums, local, recursive) end def upload_file(checksums, local, remote, rel_path) - parts = rel_path.split('/') + parts = rel_path.split("/") parts.pop # Drop the filename since we are only checking dirs parts_to_check = [] until parts.empty? parts_to_check << parts.shift - path_to_check = parts_to_check.join('/') - unless checksums[path_to_check] - logger.debug("[SFTP] Creating directory #{remote}#{path_to_check}") - add_xfer(sftp_session.mkdir("#{remote}#{path_to_check}")) - checksums[path_to_check] = true - end + path_to_check = parts_to_check.join("/") + next if checksums[path_to_check] + + logger.debug("[SFTP] Creating directory #{remote}#{path_to_check}") + add_xfer(sftp_session.mkdir("#{remote}#{path_to_check}")) + checksums[path_to_check] = true end logger.debug("[SFTP] Uploading #{local}#{rel_path} to #{remote}#{rel_path}") add_xfer(sftp_session.upload("#{local}#{rel_path}", "#{remote}#{rel_path}")) @@ -248,16 +247,13 @@ def add_xfer(xfer) sftp_loop end - def sftp_loop(n_xfers=MAX_TRANSFERS) + def sftp_loop(n_xfers = MAX_TRANSFERS) sftp_session.loop do - sftp_xfers.delete_if {|x| !(x.is_a?(Net::SFTP::Request) ? x.pending? : x.active?) } # Purge any completed operations, which has two different APIs for some reason + sftp_xfers.delete_if { |x| !(x.is_a?(Net::SFTP::Request) ? x.pending? : x.active?) } # Purge any completed operations, which has two different APIs for some reason sftp_xfers.length > n_xfers # Run until we have fewer than max end end - - end - end end end diff --git a/spec/kitchen-sync/checksums_spec.rb b/spec/kitchen-sync/checksums_spec.rb new file mode 100644 index 0000000..40f845c --- /dev/null +++ b/spec/kitchen-sync/checksums_spec.rb @@ -0,0 +1,72 @@ +require "spec_helper" +require "json" +require "digest/sha1" + +describe "kitchen-sync/checksums.rb" do + let(:script_path) { File.expand_path("../../lib/kitchen-sync/checksums.rb", __dir__) } + + def run_checksums(target) + output = IO.popen([RbConfig.ruby, script_path, target], &:read) + raise "checksums.rb failed: #{output}" unless $CHILD_STATUS.success? + + JSON.parse(output) + end + + around(:each) do |example| + Dir.mktmpdir("kitchen-sync-checksums") do |dir| + @tmpdir = dir + example.run + end + end + + it "exists at the expected path" do + expect(File).to exist(script_path) + end + + context "with a single file target" do + it "returns the file digest keyed by empty path" do + file = File.join(@tmpdir, "single.txt") + File.write(file, "hello world") + expected = Digest::SHA1.hexdigest("hello world") + + result = run_checksums(file) + expect(result).to eq("" => expected) + end + end + + context "with a directory target" do + before do + File.write(File.join(@tmpdir, "a.txt"), "alpha") + File.write(File.join(@tmpdir, "b.txt"), "beta") + FileUtils.mkdir_p(File.join(@tmpdir, "sub")) + File.write(File.join(@tmpdir, "sub", "c.txt"), "gamma") + end + + it "returns a hash with sha1 digests for files and true for directories" do + result = run_checksums(@tmpdir) + + expect(result["/a.txt"]).to eq(Digest::SHA1.hexdigest("alpha")) + expect(result["/b.txt"]).to eq(Digest::SHA1.hexdigest("beta")) + expect(result["/sub/c.txt"]).to eq(Digest::SHA1.hexdigest("gamma")) + expect(result["/sub"]).to eq(true) + end + + it "uses paths relative to the target directory" do + result = run_checksums(@tmpdir) + result.each_key do |key| + expect(key).to start_with("/").or eq("") + expect(key).not_to include(@tmpdir) + end + end + end + + context "with an empty directory" do + it "returns an empty mapping for the contents" do + result = run_checksums(@tmpdir) + # Keys (if any) should only refer to dot entries within the target. + result.each_value do |value| + expect(value).to(satisfy { |v| v == true || v.is_a?(String) }) + end + end + end +end diff --git a/spec/kitchen-sync/core_ext_spec.rb b/spec/kitchen-sync/core_ext_spec.rb new file mode 100644 index 0000000..84cf37e --- /dev/null +++ b/spec/kitchen-sync/core_ext_spec.rb @@ -0,0 +1,53 @@ +require "spec_helper" +require "kitchen" + +# kitchen-sync's core_ext.rb monkey-patches Kitchen::Provisioner::ChefBase, a +# class that lives in downstream gems (e.g. kitchen-chef). Define a minimal +# stand-in here so we can exercise the patch in isolation. +module Kitchen + module Provisioner + class ChefBase < Base + def init_command + "ORIGINAL INIT COMMAND for #{config[:root_path]}" + end + end + end +end + +require "kitchen-sync/core_ext" +require "kitchen/transport/sftp" +require "kitchen/transport/rsync" + +describe "kitchen-sync/core_ext" do + let(:provisioner) do + Kitchen::Provisioner::ChefBase.new(root_path: "/opt/kitchen").tap do |p| + p.instance_variable_set(:@instance, instance) + end + end + + let(:instance) { double("Kitchen::Instance", transport: transport) } + + context "when the transport is Kitchen::Transport::Sftp" do + let(:transport) { Kitchen::Transport::Sftp.new } + + it "returns a simple mkdir for the root_path" do + expect(provisioner.init_command).to eq("mkdir -p /opt/kitchen") + end + end + + context "when the transport is Kitchen::Transport::Rsync" do + let(:transport) { Kitchen::Transport::Rsync.new } + + it "returns a simple mkdir for the root_path" do + expect(provisioner.init_command).to eq("mkdir -p /opt/kitchen") + end + end + + context "when the transport is something else" do + let(:transport) { Kitchen::Transport::Ssh.new } + + it "delegates to the original init_command implementation" do + expect(provisioner.init_command).to eq("ORIGINAL INIT COMMAND for /opt/kitchen") + end + end +end diff --git a/spec/kitchen-sync/version_spec.rb b/spec/kitchen-sync/version_spec.rb new file mode 100644 index 0000000..fd13f0b --- /dev/null +++ b/spec/kitchen-sync/version_spec.rb @@ -0,0 +1,23 @@ +require "spec_helper" +require "kitchen-sync" + +describe KitchenSync do + describe "VERSION" do + it "is defined" do + expect(KitchenSync::VERSION).not_to be_nil + end + + it "is a non-empty string" do + expect(KitchenSync::VERSION).to be_a(String) + expect(KitchenSync::VERSION).not_to be_empty + end + + it "is frozen" do + expect(KitchenSync::VERSION).to be_frozen + end + + it "looks like a semantic version" do + expect(KitchenSync::VERSION).to match(/\A\d+\.\d+\.\d+/) + end + end +end diff --git a/spec/kitchen/transport/rsync_spec.rb b/spec/kitchen/transport/rsync_spec.rb new file mode 100644 index 0000000..b422937 --- /dev/null +++ b/spec/kitchen/transport/rsync_spec.rb @@ -0,0 +1,15 @@ +require "spec_helper" +require "kitchen" +require "kitchen/transport/rsync" + +describe Kitchen::Transport::Rsync do + subject(:transport) { described_class.new } + + it "is a subclass of Kitchen::Transport::Ssh" do + expect(described_class.ancestors).to include(Kitchen::Transport::Ssh) + end + + it "defines a Connection class that inherits from Ssh::Connection" do + expect(described_class::Connection.ancestors).to include(Kitchen::Transport::Ssh::Connection) + end +end diff --git a/spec/kitchen/transport/sftp_spec.rb b/spec/kitchen/transport/sftp_spec.rb new file mode 100644 index 0000000..17e3777 --- /dev/null +++ b/spec/kitchen/transport/sftp_spec.rb @@ -0,0 +1,39 @@ +require "spec_helper" +require "kitchen" +require "kitchen/transport/sftp" + +describe Kitchen::Transport::Sftp do + subject(:transport) { described_class.new } + + it "is a subclass of Kitchen::Transport::Ssh" do + expect(described_class.ancestors).to include(Kitchen::Transport::Ssh) + end + + it "defines a default ruby_path config" do + expect(transport[:ruby_path]).to eq("/opt/chef/embedded/bin/ruby") + end + + describe "CHECKSUMS_PATH" do + it "points at the bundled checksums.rb script" do + expect(File).to exist(described_class::CHECKSUMS_PATH) + expect(described_class::CHECKSUMS_PATH).to end_with("kitchen-sync/checksums.rb") + end + end + + describe "CHECKSUMS_REMOTE_PATH" do + it "embeds the checksums script SHA1 in the remote path" do + expect(described_class::CHECKSUMS_REMOTE_PATH).to match(%r{\A/tmp/checksums-[0-9a-f]{40}\.rb\z}) + end + + it "is frozen" do + expect(described_class::CHECKSUMS_REMOTE_PATH).to be_frozen + end + end + + describe "MAX_TRANSFERS" do + it "is a positive integer" do + expect(described_class::MAX_TRANSFERS).to be_a(Integer) + expect(described_class::MAX_TRANSFERS).to be > 0 + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..7d1cf3b --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,24 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# + +require "rspec" +require "tmpdir" +require "fileutils" + +$LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + config.order = :random + Kernel.srand config.seed +end