Skip to content

Commit 33cc614

Browse files
authored
test: cover the rspec binary discovery (#86)
These plugins had no unit tests at all -- every check was an end-to-end cucumber feature that boots busser, installs a plugin into a sandbox and runs a suite. That is the right shape for proving the plugin works, but it is slow and it cannot easily probe the edges where regressions actually live, such as exactly which filenames a glob selects. This adds a `spec/` suite using minitest and mocha, matching what test-kitchen/busser already does, and wires `rake unit` in alongside `rake features`. `rake test` runs both. The aim is not coverage; every test here is one that fails if a specific behaviour regresses. Where the logic under test lived inside a script that executes on load, it moved into a small module the script requires, so it can be exercised without running a suite. ## What is covered Finding the rspec executable is the least obvious logic in this plugin. The machine under test may have several Ruby installations and several gem paths, and the point of the search is to avoid shelling out to whatever `rspec` is on PATH -- which may belong to a different Ruby than the one serverspec was installed into. It had no coverage at all. It moved out of the runner script into `Busser::Serverspec::RspecBinary`, with the running Ruby's bindir injectable so the tests do not depend on what the host Ruby happens to have installed next to it. Seven tests: the Ruby bindir is searched before any gem path; gem paths get `bin` appended; an empty gem path list is handled; the first match wins; a gem path without rspec is skipped; **a file that exists but is not executable is skipped** (otherwise it would be handed to Rake as `rspec_path` and the run would die on a permission error instead of falling back to PATH); and nothing found returns `nil` rather than a bogus path, which is what lets the caller leave `rspec_path` unset. Verified they bite: dropping the executable check fails one. Signed-off-by: Tim Smith <tsmith84@proton.me>
1 parent 5488e76 commit 33cc614

6 files changed

Lines changed: 146 additions & 18 deletions

File tree

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ group :test do
1010
gem "aruba", ">= 2.4"
1111
gem "base64", ">= 0.3" # cucumber needs it; not a default gem on Ruby 4.0
1212
gem "cucumber", ">= 11.1"
13+
gem "minitest", ">= 6.0"
14+
gem "mocha", ">= 2.7"
1315
gem "rake", ">= 13.4"
1416
gem "serverspec", ">= 2.43"
1517
end

Rakefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
require "bundler/gem_tasks"
2+
require "rake/testtask"
3+
Rake::TestTask.new(:unit) do |t|
4+
t.libs.push "lib"
5+
t.test_files = FileList["spec/**/*_spec.rb"]
6+
t.verbose = true
7+
end
8+
29
require "cucumber/rake/task"
310

411
Cucumber::Rake::Task.new(:features) do |t|
512
t.cucumber_opts = ["features", "--format progress", "--fail-fast"]
613
end
714

815
desc "Run all test suites"
9-
task test: [:features]
16+
task test: %i{unit features}
1017

1118
task default: [:test]
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
require "rbconfig" unless defined?(RbConfig)
15+
16+
module Busser
17+
module Serverspec
18+
# Locates the rspec executable to run the suite with.
19+
#
20+
# The machine under test may have several Ruby installations and several
21+
# gem paths, and RSpec::Core::RakeTask would otherwise just shell out to
22+
# whatever `rspec` is on PATH -- which may belong to a different Ruby than
23+
# the one Busser installed serverspec into.
24+
#
25+
# This lives apart from runner.rb because that file is a script: requiring
26+
# it runs the suite.
27+
module RspecBinary
28+
module_function
29+
30+
# Directories that might hold an rspec executable, most specific first.
31+
#
32+
# @param gem_paths [Array<String>] gem paths to search, defaulting to the
33+
# current RubyGems configuration
34+
# @param ruby_bindir [String] the running Ruby's bindir; injectable so
35+
# tests do not depend on what the host Ruby happens to have installed
36+
# @return [Array<String>] candidate bin directories
37+
def candidate_bindirs(gem_paths = Gem.paths.path, ruby_bindir = RbConfig::CONFIG["bindir"])
38+
[ruby_bindir] + gem_paths.map { |p| File.join(p, "bin") }
39+
end
40+
41+
# The first candidate that exists and is executable.
42+
#
43+
# @param gem_paths [Array<String>] gem paths to search
44+
# @param ruby_bindir [String] the running Ruby's bindir
45+
# @return [String, nil] path to rspec, or nil to let the caller fall back
46+
# to whatever is on PATH
47+
def find(gem_paths = Gem.paths.path, ruby_bindir = RbConfig::CONFIG["bindir"])
48+
candidate_bindirs(gem_paths, ruby_bindir)
49+
.map { |dir| File.join(dir, "rspec") }
50+
.find { |bin| File.exist?(bin) && File.executable?(bin) }
51+
end
52+
end
53+
end
54+
end

lib/busser/serverspec/runner.rb

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,12 @@
2020
require "rspec/core/rake_task"
2121
require "rbconfig" unless defined?(RbConfig)
2222

23+
require_relative "rspec_binary"
24+
2325
base_path = File.expand_path(ARGV.shift)
2426

2527
RSpec::Core::RakeTask.new(:spec) do |t|
26-
candidate_bindirs = []
27-
# Current Ruby's default bindir
28-
candidate_bindirs << RbConfig::CONFIG["bindir"]
29-
# Search all Gem paths bindirs
30-
candidate_bindirs << Gem.paths.path.map do |gem_path|
31-
File.join(gem_path, "bin")
32-
end
33-
34-
candidate_rspec_bins = candidate_bindirs.flatten.map do |bin_dir|
35-
File.join(bin_dir, "rspec")
36-
end
37-
38-
rspec_bin = candidate_rspec_bins.find do |candidate_rspec_bin|
39-
FileTest.exist?(candidate_rspec_bin) &&
40-
FileTest.executable?(candidate_rspec_bin)
41-
end
42-
28+
rspec_bin = Busser::Serverspec::RspecBinary.find
4329
t.rspec_path = rspec_bin if rspec_bin
4430
t.rspec_opts = [
4531
"--color",
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
require_relative "../../spec_helper"
2+
3+
require "fileutils"
4+
require "tmpdir"
5+
require "busser/serverspec/rspec_binary"
6+
7+
describe Busser::Serverspec::RspecBinary do
8+
def bindir_with_rspec(root, executable: true)
9+
bin = File.join(root, "bin")
10+
FileUtils.mkdir_p(bin)
11+
path = File.join(bin, "rspec")
12+
File.write(path, "#!/bin/sh\n")
13+
File.chmod(executable ? 0o755 : 0o644, path)
14+
path
15+
end
16+
17+
describe ".candidate_bindirs" do
18+
it "looks at the running Ruby's bindir before any gem path" do
19+
dirs = Busser::Serverspec::RspecBinary.candidate_bindirs(["/gems/a"])
20+
_(dirs.first).must_equal RbConfig::CONFIG["bindir"]
21+
end
22+
23+
it "appends bin to each gem path" do
24+
dirs = Busser::Serverspec::RspecBinary.candidate_bindirs(["/gems/a", "/gems/b"])
25+
_(dirs.last(2)).must_equal ["/gems/a/bin", "/gems/b/bin"]
26+
end
27+
28+
it "copes with no gem paths at all" do
29+
_(Busser::Serverspec::RspecBinary.candidate_bindirs([]))
30+
.must_equal [RbConfig::CONFIG["bindir"]]
31+
end
32+
end
33+
34+
describe ".find" do
35+
# An empty stand-in for the running Ruby's bindir, so these do not depend on
36+
# whether the host Ruby happens to have an rspec next to it.
37+
let(:no_ruby_bin) { Dir.mktmpdir }
38+
after { FileUtils.remove_entry(no_ruby_bin) if File.directory?(no_ruby_bin) }
39+
it "returns the rspec in the first gem path that has one" do
40+
Dir.mktmpdir do |a|
41+
Dir.mktmpdir do |b|
42+
bindir_with_rspec(a)
43+
bindir_with_rspec(b)
44+
_(Busser::Serverspec::RspecBinary.find([a, b], no_ruby_bin)).must_equal File.join(a, "bin", "rspec")
45+
end
46+
end
47+
end
48+
49+
it "skips a gem path with no rspec" do
50+
Dir.mktmpdir do |empty|
51+
Dir.mktmpdir do |real|
52+
bindir_with_rspec(real)
53+
_(Busser::Serverspec::RspecBinary.find([empty, real], no_ruby_bin))
54+
.must_equal File.join(real, "bin", "rspec")
55+
end
56+
end
57+
end
58+
59+
# A non-executable file here would be handed to Rake as rspec_path and the
60+
# run would die with a permission error rather than falling back to PATH.
61+
it "skips a file that exists but is not executable" do
62+
Dir.mktmpdir do |a|
63+
Dir.mktmpdir do |b|
64+
bindir_with_rspec(a, executable: false)
65+
bindir_with_rspec(b)
66+
_(Busser::Serverspec::RspecBinary.find([a, b], no_ruby_bin)).must_equal File.join(b, "bin", "rspec")
67+
end
68+
end
69+
end
70+
71+
# nil is meaningful: the caller leaves rspec_path unset and Rake falls back
72+
# to whatever rspec is on PATH.
73+
it "returns nil when nothing is found, rather than a bogus path" do
74+
Dir.mktmpdir { |dir| _(Busser::Serverspec::RspecBinary.find([dir], no_ruby_bin)).must_be_nil }
75+
end
76+
end
77+
end

spec/spec_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
require "minitest/autorun"
2+
require "mocha/minitest"

0 commit comments

Comments
 (0)