busser 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +32 -0
  3. data/.github/workflows/pr-title.yml +28 -0
  4. data/.github/workflows/publish.yml +45 -0
  5. data/.gitignore +0 -1
  6. data/.markdownlint.yaml +6 -0
  7. data/.release-please-manifest.json +3 -0
  8. data/.rubocop.yml +9 -0
  9. data/.yamllint +6 -0
  10. data/CHANGELOG.md +35 -0
  11. data/CONTRIBUTING.md +103 -0
  12. data/Gemfile +10 -6
  13. data/README.md +77 -19
  14. data/Rakefile +6 -23
  15. data/bin/busser +2 -4
  16. data/busser.gemspec +12 -15
  17. data/features/deserialize_command.feature +4 -1
  18. data/features/plugin_install_command.feature +1 -1
  19. data/features/support/env.rb +4 -16
  20. data/lib/busser/cli.rb +6 -7
  21. data/lib/busser/command/deserialize.rb +16 -11
  22. data/lib/busser/command/plugin.rb +4 -5
  23. data/lib/busser/command/plugin_create.rb +23 -24
  24. data/lib/busser/command/plugin_install.rb +15 -16
  25. data/lib/busser/command/plugin_list.rb +3 -4
  26. data/lib/busser/command/setup.rb +17 -18
  27. data/lib/busser/command/suite.rb +3 -4
  28. data/lib/busser/command/suite_cleanup.rb +3 -3
  29. data/lib/busser/command/suite_path.rb +2 -3
  30. data/lib/busser/command/test.rb +4 -4
  31. data/lib/busser/cucumber/hooks.rb +1 -9
  32. data/lib/busser/cucumber.rb +69 -49
  33. data/lib/busser/helpers.rb +4 -5
  34. data/lib/busser/plugin.rb +12 -6
  35. data/lib/busser/rubygems.rb +25 -11
  36. data/lib/busser/runner_plugin/dummy.rb +2 -3
  37. data/lib/busser/runner_plugin.rb +1 -2
  38. data/lib/busser/thor.rb +3 -4
  39. data/lib/busser/ui.rb +15 -5
  40. data/lib/busser/version.rb +1 -2
  41. data/lib/busser.rb +3 -4
  42. data/release-please-config.json +13 -0
  43. data/renovate.json +8 -0
  44. data/spec/busser/command/deserialize_spec.rb +110 -0
  45. data/spec/busser/helpers_spec.rb +10 -10
  46. data/spec/busser/plugin_spec.rb +133 -0
  47. data/spec/busser/ui_spec.rb +35 -29
  48. data/spec/spec_helper.rb +1 -7
  49. metadata +26 -86
  50. data/.cane +0 -0
  51. data/.simplecov +0 -10
  52. data/.travis.yml +0 -52
  53. data/Guardfile +0 -18
  54. data/appveyor.yml +0 -36
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
3
  #
@@ -16,9 +15,9 @@
16
15
  # See the License for the specific language governing permissions and
17
16
  # limitations under the License.
18
17
 
19
- require 'pathname' unless defined?(Pathname)
18
+ require "pathname" unless defined?(Pathname)
20
19
 
21
- require 'busser/rubygems'
20
+ require "busser/rubygems"
22
21
 
23
22
  module Busser
24
23
 
@@ -43,12 +42,12 @@ module Busser
43
42
  end
44
43
 
45
44
  def root_path
46
- Pathname.new(ENV['BUSSER_ROOT'] || "/opt/busser")
45
+ Pathname.new(ENV["BUSSER_ROOT"] || "/opt/busser")
47
46
  end
48
47
 
49
48
  def chef_apply(config = {}, &block)
50
49
  warn "Apologies, but Busser no longer supports the chef_apply helper," +
51
- " so the contents of this block will not be exectued. Please refactor" +
50
+ " so the contents of this block will not be executed. Please refactor" +
52
51
  " your code to use Thor actions, shell out commands or another" +
53
52
  " strategy"
54
53
  end
data/lib/busser/plugin.rb CHANGED
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
3
  #
@@ -35,16 +34,22 @@ module Busser
35
34
 
36
35
  def runner_plugins(plugin_names = nil)
37
36
  if plugin_names
38
- Array(plugin_names).map { |plugin| runner_plugin(plugin) }
37
+ Array(plugin_names).map { |plugin| runner_plugin(plugin) }.uniq
39
38
  else
40
39
  all_runner_plugins
41
40
  end
42
41
  end
43
42
 
43
+ # Gem.find_files searches every installed gem version, not just the
44
+ # newest, so a machine holding two versions of a plugin gem reports that
45
+ # plugin twice. Callers treat each entry as one suite to run, so the
46
+ # duplicates made `busser test` run a suite twice and `busser plugin list`
47
+ # print it twice. The path is what gets required, and require resolves to
48
+ # the active version, so collapsing the repeats is safe.
44
49
  def all_runner_plugins
45
- Gem.find_files('busser/runner_plugin/*.rb').map do |file|
46
- "busser/runner_plugin/#{File.basename(file).sub(/\.rb$/, '')}"
47
- end
50
+ Gem.find_files("busser/runner_plugin/*.rb").map do |file|
51
+ "busser/runner_plugin/#{File.basename(file).sub(/\.rb$/, "")}"
52
+ end.uniq
48
53
  end
49
54
 
50
55
  def require!(plugin_path)
@@ -60,7 +65,8 @@ module Busser
60
65
  def gem_from_path(plugin_path)
61
66
  local_gem_path = "#{File.expand_path(plugin_path, $LOAD_PATH.first)}"
62
67
  local_gemspec = File.join(
63
- File.dirname($LOAD_PATH.first), "busser.gemspec")
68
+ File.dirname($LOAD_PATH.first), "busser.gemspec"
69
+ )
64
70
 
65
71
  if ! Dir.glob("#{local_gem_path}#{Gem.suffix_pattern}").empty?
66
72
  Gem::Specification.load(File.expand_path(local_gemspec))
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
3
  #
@@ -16,7 +15,7 @@
16
15
  # See the License for the specific language governing permissions and
17
16
  # limitations under the License.
18
17
 
19
- require 'rubygems/dependency_installer'
18
+ require "rubygems/dependency_installer"
20
19
 
21
20
  module Busser
22
21
 
@@ -33,29 +32,44 @@ module Busser
33
32
  ! Gem::Dependency.new(name, version).matching_specs.empty?
34
33
  end
35
34
 
36
- def install_gem(gem, version)
35
+ def install_gem(gem_name, version)
37
36
  version = Gem::Requirement.default unless version
38
37
 
38
+ # A bundler-managed environment can leave Gem.dir pointing at the bundle,
39
+ # which is not where busser plugins belong. Point RubyGems at GEM_HOME.
40
+ gem_home = ENV.fetch("GEM_HOME", nil)
41
+ Gem.use_paths(gem_home, Gem.path) if gem_home && Gem.dir != gem_home
42
+
39
43
  inst = Gem::DependencyInstaller.new(rbg_options)
40
- specs = inst.install(gem, Gem::Requirement.create(version))
44
+ specs = inst.install(gem_name, Gem::Requirement.create(version))
41
45
 
42
46
  Gem.clear_paths
43
- specs.find { |s| s.name == gem }
47
+ spec = specs.find { |s| s.name == gem_name }
48
+ # Modern RubyGems does not put a freshly installed gem on the load path.
49
+ # Add it directly rather than activating, so installing a second version
50
+ # in the same process does not raise a Gem::LoadError conflict.
51
+ spec&.full_require_paths&.each do |path|
52
+ $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path)
53
+ end
54
+ spec
44
55
  end
45
56
 
46
57
  def rbg_options
47
58
  @rbg_options ||= Gem::DependencyInstaller::DEFAULT_OPTIONS.merge(
48
- :suggest_alternate => false,
49
- :version => Gem::Requirement.default,
50
- :without_groups => [],
51
- :minimal_deps => true,
52
- :http_proxy => ENV.fetch("http_proxy", ENV.fetch("HTTP_PROXY", nil))
59
+ suggest_alternate: false,
60
+ version: Gem::Requirement.default,
61
+ without_groups: [],
62
+ minimal_deps: true,
63
+ # Install into GEM_HOME explicitly. Under bundler Gem.dir points at the
64
+ # bundle path, which is not where busser plugins belong.
65
+ install_dir: ENV.fetch("GEM_HOME", nil),
66
+ http_proxy: ENV.fetch("http_proxy", ENV.fetch("HTTP_PROXY", nil))
53
67
  )
54
68
  end
55
69
 
56
70
  def silence_gem_ui
57
71
  interaction = Gem::DefaultUserInteraction.ui
58
- if !Gem.configuration.really_verbose
72
+ unless Gem.configuration.really_verbose
59
73
  Gem::DefaultUserInteraction.ui = Gem::SilentUI.new
60
74
  end
61
75
  yield
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
3
  #
@@ -16,7 +15,7 @@
16
15
  # See the License for the specific language governing permissions and
17
16
  # limitations under the License.
18
17
 
19
- require 'busser/runner_plugin'
18
+ require "busser/runner_plugin"
20
19
 
21
20
  # Dummy runner plugin for Busser.
22
21
  #
@@ -37,7 +36,7 @@ class Busser::RunnerPlugin::Dummy < Busser::RunnerPlugin::Base
37
36
 
38
37
  def test
39
38
  banner "[dummy] Running"
40
- if File.exists?(File.join(suite_path("dummy"), "foobar.txt"))
39
+ if File.exist?(File.join(suite_path("dummy"), "foobar.txt"))
41
40
  info "[dummy] The postinstall script has been called"
42
41
  else
43
42
  warn "[dummy] The postinstall script was not called"
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
3
  #
@@ -16,7 +15,7 @@
16
15
  # See the License for the specific language governing permissions and
17
16
  # limitations under the License.
18
17
 
19
- require 'busser/thor'
18
+ require "busser/thor"
20
19
 
21
20
  module Busser
22
21
 
data/lib/busser/thor.rb CHANGED
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
3
  #
@@ -16,10 +15,10 @@
16
15
  # See the License for the specific language governing permissions and
17
16
  # limitations under the License.
18
17
 
19
- require 'thor' unless defined?(Thor)
18
+ require "thor" unless defined?(Thor)
20
19
 
21
- require 'busser/helpers'
22
- require 'busser/ui'
20
+ require "busser/helpers"
21
+ require "busser/ui"
23
22
 
24
23
  module Busser
25
24
 
data/lib/busser/ui.rb CHANGED
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
3
  #
@@ -16,7 +15,7 @@
16
15
  # See the License for the specific language governing permissions and
17
16
  # limitations under the License.
18
17
 
19
- require 'thor/shell'
18
+ require "thor/shell"
20
19
 
21
20
  module Busser
22
21
 
@@ -28,6 +27,16 @@ module Busser
28
27
 
29
28
  module_function
30
29
 
30
+ # Thor::Shell's delegated methods are not mixed into this module, so
31
+ # provide the two output primitives it relies on.
32
+ def say(msg)
33
+ $stdout.puts(msg)
34
+ end
35
+
36
+ def error(msg)
37
+ $stderr.puts(msg)
38
+ end
39
+
31
40
  def banner(msg)
32
41
  say("-----> #{msg}")
33
42
  end
@@ -45,7 +54,7 @@ module Busser
45
54
  end
46
55
 
47
56
  def run!(cmd, config = {})
48
- config = { :capture => false, :verbose => false }.merge(config)
57
+ config = { capture: false, verbose: false }.merge(config)
49
58
 
50
59
  handle_command("Command", cmd) do
51
60
  run(cmd, config)
@@ -53,7 +62,7 @@ module Busser
53
62
  end
54
63
 
55
64
  def run_ruby_script!(cmd, config = {})
56
- config = { :capture => false, :verbose => false }.merge(config)
65
+ config = { capture: false, verbose: false }.merge(config)
57
66
 
58
67
  handle_command("Ruby Script", cmd) do
59
68
  run_ruby_script(cmd, config)
@@ -75,7 +84,8 @@ module Busser
75
84
  rescue => e
76
85
  fatal(
77
86
  "#{type} [#{cmd}] raised an exception: #{e.message}\n" +
78
- e.backtrace.join("\n"))
87
+ e.backtrace.join("\n")
88
+ )
79
89
  raise
80
90
  end
81
91
 
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
3
  #
@@ -17,5 +16,5 @@
17
16
  # limitations under the License.
18
17
 
19
18
  module Busser
20
- VERSION = "0.8.0"
19
+ VERSION = "0.9.0".freeze
21
20
  end
data/lib/busser.rb CHANGED
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
3
  #
@@ -16,9 +15,9 @@
16
15
  # See the License for the specific language governing permissions and
17
16
  # limitations under the License.
18
17
 
19
- require 'pathname' unless defined?(Pathname)
18
+ require "pathname" unless defined?(Pathname)
20
19
 
21
- require 'busser/version'
20
+ require "busser/version"
22
21
 
23
22
  # Busser - Runs tests for projects in Test Kitchen.
24
23
  #
@@ -30,6 +29,6 @@ module Busser
30
29
  #
31
30
  # @return [Pathname] root path of gem
32
31
  def self.source_root
33
- @source_root ||= Pathname.new(File.expand_path('../../', __FILE__))
32
+ @source_root ||= Pathname.new(File.expand_path("..", __dir__))
34
33
  end
35
34
  end
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "bootstrap-sha": "9ff44ee27bd8863c0a197d9976c25623c8c05fc0",
4
+ "packages": {
5
+ ".": {
6
+ "package-name": "busser",
7
+ "changelog-path": "CHANGELOG.md",
8
+ "release-type": "ruby",
9
+ "include-component-in-tag": false,
10
+ "version-file": "lib/busser/version.rb"
11
+ }
12
+ }
13
+ }
data/renovate.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:recommended",
5
+ "helpers:pinGitHubActionDigests",
6
+ ":semanticCommits"
7
+ ]
8
+ }
@@ -0,0 +1,110 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require_relative "../../spec_helper"
16
+
17
+ require "base64"
18
+ require "digest/md5"
19
+ require "tmpdir"
20
+
21
+ require "busser/command/deserialize"
22
+
23
+ # Deserialize is how Test Kitchen streams a file onto the instance under test,
24
+ # so the thing worth pinning down is what ends up on disk.
25
+ describe Busser::Command::Deserialize do
26
+
27
+ CONTENTS = "Hello there.\n".freeze
28
+
29
+ before do
30
+ @tmpdir = Dir.mktmpdir("busser-deserialize")
31
+ @file = File.join(@tmpdir, "nested", "decoded.txt")
32
+ end
33
+
34
+ after do
35
+ FileUtils.rm_rf(@tmpdir)
36
+ end
37
+
38
+ describe "with a matching digest" do
39
+
40
+ it "writes the decoded contents" do
41
+ deserialize(md5sum: Digest::MD5.hexdigest(CONTENTS))
42
+
43
+ _(File.read(@file)).must_equal CONTENTS
44
+ end
45
+
46
+ it "creates any missing parent directories" do
47
+ deserialize(md5sum: Digest::MD5.hexdigest(CONTENTS))
48
+
49
+ _(File.directory?(File.dirname(@file))).must_equal true
50
+ end
51
+
52
+ it "applies the requested permissions" do
53
+ deserialize(md5sum: Digest::MD5.hexdigest(CONTENTS), perms: "0755")
54
+
55
+ _(format("%o", File.stat(@file).mode & 0o777)).must_equal "755"
56
+ end
57
+
58
+ it "writes binary content unchanged" do
59
+ binary = "\x00\x01\x02\xFF".dup.force_encoding("ASCII-8BIT")
60
+
61
+ deserialize(contents: binary, md5sum: Digest::MD5.hexdigest(binary))
62
+
63
+ _(File.binread(@file)).must_equal binary
64
+ end
65
+ end
66
+
67
+ describe "with a mismatching digest" do
68
+
69
+ it "fails the command" do
70
+ _ { deserialize(md5sum: "not-the-right-digest") }.must_raise SystemExit
71
+ end
72
+
73
+ # The digest used to be checked only after the write, so a corrupted or
74
+ # truncated stream still landed on disk, with its requested permissions,
75
+ # for a later command to pick up and run.
76
+ it "leaves no file behind" do
77
+ _ { deserialize(md5sum: "not-the-right-digest") }.must_raise SystemExit
78
+
79
+ _(File.exist?(@file)).must_equal false
80
+ end
81
+
82
+ it "does not overwrite a file that is already there" do
83
+ FileUtils.mkdir_p(File.dirname(@file))
84
+ File.write(@file, "original contents")
85
+
86
+ _ { deserialize(md5sum: "not-the-right-digest") }.must_raise SystemExit
87
+
88
+ _(File.read(@file)).must_equal "original contents"
89
+ end
90
+ end
91
+
92
+ def deserialize(md5sum:, contents: CONTENTS, perms: "0644")
93
+ STDIN.stubs(:read).returns(Base64.encode64(contents))
94
+
95
+ command = Busser::Command::Deserialize.new([], {
96
+ "destination" => @file,
97
+ "md5sum" => md5sum,
98
+ "perms" => perms,
99
+ })
100
+ capture_stderr { command.perform }
101
+ end
102
+
103
+ def capture_stderr
104
+ original = $stderr
105
+ $stderr = StringIO.new
106
+ yield
107
+ ensure
108
+ $stderr = original
109
+ end
110
+ end
@@ -9,17 +9,17 @@ describe Busser::Helpers do
9
9
  describe ".suite_path" do
10
10
 
11
11
  it "returns a Pathname" do
12
- suite_path.must_be_kind_of Pathname
12
+ _(suite_path).must_be_kind_of Pathname
13
13
  end
14
14
 
15
15
  describe "with a default root path" do
16
16
 
17
17
  it "returns a base path if no suite name is given" do
18
- suite_path.to_s.must_match %r{/opt/busser/suites$}
18
+ _(suite_path.to_s).must_match %r{/opt/busser/suites$}
19
19
  end
20
20
 
21
21
  it "returns a suite path given a suite name" do
22
- suite_path("fuzzy").to_s.must_match %r{/opt/busser/suites/fuzzy$}
22
+ _(suite_path("fuzzy").to_s).must_match %r{/opt/busser/suites/fuzzy$}
23
23
  end
24
24
  end
25
25
 
@@ -30,12 +30,12 @@ describe Busser::Helpers do
30
30
 
31
31
  it "returns a base path if no suite name is given" do
32
32
  ENV['BUSSER_ROOT'] = "/path/to/busser"
33
- suite_path.to_s.must_match %r{/path/to/busser/suites$}
33
+ _(suite_path.to_s).must_match %r{/path/to/busser/suites$}
34
34
  end
35
35
 
36
36
  it "returns a suite path given a suite name" do
37
37
  ENV['BUSSER_ROOT'] = "/path/to/busser"
38
- suite_path("fuzzy").to_s.must_match %r{/path/to/busser/suites/fuzzy$}
38
+ _(suite_path("fuzzy").to_s).must_match %r{/path/to/busser/suites/fuzzy$}
39
39
  end
40
40
  end
41
41
  end
@@ -43,17 +43,17 @@ describe Busser::Helpers do
43
43
  describe ".vendor_path" do
44
44
 
45
45
  it "returns a Pathname" do
46
- vendor_path.must_be_kind_of Pathname
46
+ _(vendor_path).must_be_kind_of Pathname
47
47
  end
48
48
 
49
49
  describe "with a default root path" do
50
50
 
51
51
  it "returns a base path if no product name is given" do
52
- vendor_path.to_s.must_match %r{/opt/busser/vendor$}
52
+ _(vendor_path.to_s).must_match %r{/opt/busser/vendor$}
53
53
  end
54
54
 
55
55
  it "returns a vendor path given a product name" do
56
- vendor_path("supreme").to_s.must_match %r{/opt/busser/vendor/supreme$}
56
+ _(vendor_path("supreme").to_s).must_match %r{/opt/busser/vendor/supreme$}
57
57
  end
58
58
  end
59
59
 
@@ -64,12 +64,12 @@ describe Busser::Helpers do
64
64
 
65
65
  it "returns a base path if no product name is given" do
66
66
  ENV['BUSSER_ROOT'] = "/path/to/busser"
67
- vendor_path.to_s.must_match %r{/path/to/busser/vendor$}
67
+ _(vendor_path.to_s).must_match %r{/path/to/busser/vendor$}
68
68
  end
69
69
 
70
70
  it "returns a suite path given a product name" do
71
71
  ENV['BUSSER_ROOT'] = "/path/to/busser"
72
- vendor_path("maximal").to_s.must_match \
72
+ _(vendor_path("maximal").to_s).must_match \
73
73
  %r{/path/to/busser/vendor/maximal$}
74
74
  end
75
75
  end
@@ -0,0 +1,133 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require_relative "../spec_helper"
16
+
17
+ require "busser/plugin"
18
+ require "busser/ui"
19
+
20
+ describe Busser::Plugin do
21
+
22
+ describe ".runner_plugin" do
23
+
24
+ it "builds the require path for a plugin name" do
25
+ _(Busser::Plugin.runner_plugin("bash"))
26
+ .must_equal "busser/runner_plugin/bash"
27
+ end
28
+ end
29
+
30
+ describe ".runner_plugins" do
31
+
32
+ it "maps each given name to its require path" do
33
+ _(Busser::Plugin.runner_plugins(%w{bash minitest})).must_equal [
34
+ "busser/runner_plugin/bash",
35
+ "busser/runner_plugin/minitest",
36
+ ]
37
+ end
38
+
39
+ it "accepts a single name that is not in an array" do
40
+ _(Busser::Plugin.runner_plugins("bash"))
41
+ .must_equal ["busser/runner_plugin/bash"]
42
+ end
43
+
44
+ # Callers run one suite per entry, so a repeated name would run that
45
+ # suite twice.
46
+ it "returns a name given more than once only once" do
47
+ _(Busser::Plugin.runner_plugins(%w{bash bash}))
48
+ .must_equal ["busser/runner_plugin/bash"]
49
+ end
50
+
51
+ it "falls back to every installed plugin when given no names" do
52
+ Busser::Plugin.expects(:all_runner_plugins).returns(["stubbed"])
53
+
54
+ _(Busser::Plugin.runner_plugins).must_equal ["stubbed"]
55
+ end
56
+ end
57
+
58
+ describe ".all_runner_plugins" do
59
+
60
+ it "turns each found file into a require path" do
61
+ stub_found_files %w{
62
+ /gems/busser-bash-0.1.1/lib/busser/runner_plugin/bash.rb
63
+ /gems/busser-serverspec-0.5.0/lib/busser/runner_plugin/serverspec.rb
64
+ }
65
+
66
+ _(Busser::Plugin.all_runner_plugins).must_equal [
67
+ "busser/runner_plugin/bash",
68
+ "busser/runner_plugin/serverspec",
69
+ ]
70
+ end
71
+
72
+ # Gem.find_files searches every installed version of every gem. A machine
73
+ # that has installed a plugin more than once therefore reports it once per
74
+ # version, which made `busser test` run that suite repeatedly and
75
+ # `busser plugin list` print it repeatedly.
76
+ it "reports a plugin once when several versions of its gem are installed" do
77
+ stub_found_files %w{
78
+ /gems/busser-bash-0.1.1/lib/busser/runner_plugin/bash.rb
79
+ /gems/busser-bash-0.1.0/lib/busser/runner_plugin/bash.rb
80
+ }
81
+
82
+ _(Busser::Plugin.all_runner_plugins)
83
+ .must_equal ["busser/runner_plugin/bash"]
84
+ end
85
+
86
+ it "keeps the first occurrence, which is the newest version found" do
87
+ stub_found_files %w{
88
+ /gems/busser-bash-0.1.1/lib/busser/runner_plugin/bash.rb
89
+ /gems/busser-bash-0.1.0/lib/busser/runner_plugin/bash.rb
90
+ /gems/busser-serverspec-0.5.0/lib/busser/runner_plugin/serverspec.rb
91
+ }
92
+
93
+ _(Busser::Plugin.all_runner_plugins).must_equal [
94
+ "busser/runner_plugin/bash",
95
+ "busser/runner_plugin/serverspec",
96
+ ]
97
+ end
98
+
99
+ it "returns nothing when no plugins are installed" do
100
+ stub_found_files []
101
+
102
+ _(Busser::Plugin.all_runner_plugins).must_equal []
103
+ end
104
+
105
+ def stub_found_files(files)
106
+ Gem.expects(:find_files)
107
+ .with("busser/runner_plugin/*.rb")
108
+ .returns(files)
109
+ end
110
+ end
111
+
112
+ describe ".require!" do
113
+
114
+ it "requires the given path" do
115
+ Busser::Plugin.expects(:require).with("busser/runner_plugin/bash")
116
+
117
+ Busser::Plugin.require!("busser/runner_plugin/bash")
118
+ end
119
+
120
+ # A plugin gem that is listed but not loadable is a common failure on a
121
+ # test instance, and the message is the only clue the user gets.
122
+ it "dies with the offending path when the plugin cannot be loaded" do
123
+ Busser::Plugin.stubs(:require).raises(LoadError, "no such file")
124
+ Busser::UI.expects(:die).with do |message|
125
+ message.include?("busser/runner_plugin/nope") &&
126
+ message.include?("LoadError") &&
127
+ message.include?("no such file")
128
+ end
129
+
130
+ Busser::Plugin.require!("busser/runner_plugin/nope")
131
+ end
132
+ end
133
+ end