busser 0.8.0 → 0.9.1

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 (63) 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 +43 -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_create_command.feature +7 -9
  19. data/features/plugin_install_command.feature +1 -1
  20. data/features/support/env.rb +4 -16
  21. data/lib/busser/cli.rb +6 -7
  22. data/lib/busser/command/deserialize.rb +16 -11
  23. data/lib/busser/command/plugin.rb +4 -5
  24. data/lib/busser/command/plugin_create.rb +40 -27
  25. data/lib/busser/command/plugin_install.rb +15 -16
  26. data/lib/busser/command/plugin_list.rb +3 -4
  27. data/lib/busser/command/setup.rb +34 -21
  28. data/lib/busser/command/suite.rb +3 -4
  29. data/lib/busser/command/suite_cleanup.rb +3 -3
  30. data/lib/busser/command/suite_path.rb +2 -3
  31. data/lib/busser/command/test.rb +4 -4
  32. data/lib/busser/cucumber/hooks.rb +1 -9
  33. data/lib/busser/cucumber.rb +69 -49
  34. data/lib/busser/helpers.rb +4 -5
  35. data/lib/busser/plugin.rb +12 -6
  36. data/lib/busser/rubygems.rb +25 -11
  37. data/lib/busser/runner_plugin/dummy.rb +2 -3
  38. data/lib/busser/runner_plugin.rb +1 -2
  39. data/lib/busser/thor.rb +3 -4
  40. data/lib/busser/ui.rb +23 -5
  41. data/lib/busser/version.rb +1 -2
  42. data/lib/busser.rb +3 -4
  43. data/release-please-config.json +13 -0
  44. data/renovate.json +8 -0
  45. data/spec/busser/command/deserialize_spec.rb +110 -0
  46. data/spec/busser/command/plugin_create_spec.rb +175 -0
  47. data/spec/busser/command/setup_spec.rb +121 -0
  48. data/spec/busser/helpers_spec.rb +10 -10
  49. data/spec/busser/plugin_spec.rb +133 -0
  50. data/spec/busser/ui_spec.rb +84 -32
  51. data/spec/spec_helper.rb +1 -7
  52. data/templates/plugin/Rakefile.erb +4 -24
  53. data/templates/plugin/features_env.rb.erb +4 -4
  54. data/templates/plugin/gemspec.erb +7 -8
  55. data/templates/plugin/github_workflow.yml.erb +26 -0
  56. metadata +29 -88
  57. data/.cane +0 -0
  58. data/.simplecov +0 -10
  59. data/.travis.yml +0 -52
  60. data/Guardfile +0 -18
  61. data/appveyor.yml +0 -36
  62. data/templates/plugin/tailor.erb +0 -4
  63. data/templates/plugin/travis.yml.erb +0 -11
@@ -1,123 +1,143 @@
1
1
  begin
2
- require 'aruba/cucumber'
2
+ require "aruba/cucumber"
3
3
  rescue LoadError
4
4
  abort "The aruba gem must be in your development dependencies"
5
5
  end
6
6
 
7
- require 'busser/cucumber/hooks'
7
+ require "busser/cucumber/hooks"
8
8
 
9
- require 'tmpdir' unless defined?(Dir.mktmpdir)
10
- require 'pathname' unless defined?(Pathname)
9
+ require "fileutils" unless defined?(FileUtils)
10
+ require "tmpdir" unless defined?(Dir.mktmpdir)
11
+ require "pathname" unless defined?(Pathname)
11
12
 
12
13
  Given(/^a BUSSER_ROOT of "(.*?)"$/) do |busser_root|
13
- backup_envvar('BUSSER_ROOT')
14
+ backup_envvar("BUSSER_ROOT")
14
15
 
15
- ENV['BUSSER_ROOT'] = busser_root
16
+ ENV["BUSSER_ROOT"] = busser_root
16
17
  end
17
18
 
18
19
  Given(/^a test BUSSER_ROOT directory named "(.*?)"$/) do |name|
19
- backup_envvar('BUSSER_ROOT')
20
+ backup_envvar("BUSSER_ROOT")
20
21
 
21
22
  busser_root = Pathname.new(Dir.mktmpdir(name))
22
23
  (busser_root + "suites").mkpath
23
- ENV['BUSSER_ROOT'] = busser_root.to_s
24
+ ENV["BUSSER_ROOT"] = busser_root.to_s
25
+ # aruba 2.x runs commands with its own environment, so set it there too
26
+ set_environment_variable("BUSSER_ROOT", busser_root.to_s)
24
27
  @busser_root_dirs << busser_root
25
28
  end
26
29
 
27
30
  Given(/^I delete the BUSSER_ROOT directory$/) do
28
- FileUtils.rm_rf(ENV['BUSSER_ROOT'])
31
+ FileUtils.rm_rf(ENV["BUSSER_ROOT"])
29
32
  end
30
33
 
31
34
  Given(/^a suite directory named "(.*?)"$/) do |name|
32
- FileUtils.mkdir_p(File.join(ENV['BUSSER_ROOT'], "suites", name))
35
+ FileUtils.mkdir_p(File.join(ENV["BUSSER_ROOT"], "suites", name))
33
36
  end
34
37
 
35
38
  Given(/^a file in suite "(.*?)" named "(.*?)" with:$/) do |suite, file, content|
36
- file_name = File.join(ENV['BUSSER_ROOT'], "suites", suite, file)
37
- write_file(file_name, content)
39
+ # BUSSER_ROOT is outside aruba's working directory, and aruba 1.0+ refuses
40
+ # absolute paths in its file helpers, so write it directly.
41
+ file_name = File.join(ENV["BUSSER_ROOT"], "suites", suite, file)
42
+ FileUtils.mkdir_p(File.dirname(file_name))
43
+ File.write(file_name, content)
38
44
  end
39
45
 
40
46
  Given(/^a sandboxed GEM_HOME directory named "(.*?)"$/) do |name|
41
- backup_envvar('GEM_HOME')
42
- backup_envvar('GEM_PATH')
47
+ backup_envvar("GEM_HOME")
48
+ backup_envvar("GEM_PATH")
43
49
 
44
50
  gem_home = Pathname.new(Dir.mktmpdir(name))
45
- ENV['GEM_HOME'] = gem_home.to_s
46
- ENV['GEM_PATH'] = [gem_home.to_s, ENV['GEM_PATH']].join(':')
51
+ ENV["GEM_HOME"] = gem_home.to_s
52
+ ENV["GEM_PATH"] = [gem_home.to_s, ENV["GEM_PATH"]].join(":")
53
+ set_environment_variable("GEM_HOME", ENV["GEM_HOME"])
54
+ set_environment_variable("GEM_PATH", ENV["GEM_PATH"])
47
55
  @busser_root_dirs << gem_home
48
56
  end
49
57
 
50
58
  Given(/^a non bundler environment$/) do
51
- %w[BUNDLER_EDITOR BUNDLE_BIN_PATH BUNDLE_GEMFILE RUBYOPT].each do |key|
59
+ # Where bundler is currently serving gems from: vendor/bundle/ruby/<abi>
60
+ # when the bundle is vendored, the system gem directory otherwise. Busser's
61
+ # own runtime dependencies live there, so the child still needs it on
62
+ # GEM_PATH once bundler is gone. Read it while RubyGems still points at it.
63
+ bundled_gem_dir = Gem.dir
64
+
65
+ # RubyGems auto-requires bundler/setup whenever BUNDLER_SETUP is set, so
66
+ # leaving that behind lets bundler re-activate inside the child, walk up to
67
+ # this repository's Gemfile and .bundle/config, and reset GEM_HOME to the
68
+ # bundle -- plugins would install there instead of the sandbox.
69
+ %w{
70
+ BUNDLER_EDITOR BUNDLER_SETUP BUNDLER_VERSION
71
+ BUNDLE_BIN_PATH BUNDLE_GEMFILE BUNDLE_LOCKFILE BUNDLE_PATH
72
+ RUBYLIB RUBYOPT
73
+ }.each do |key|
52
74
  backup_envvar(key)
53
75
  ENV.delete(key)
76
+ delete_environment_variable(key)
54
77
  end
78
+
79
+ ENV["GEM_PATH"] =
80
+ [ENV["GEM_PATH"], bundled_gem_dir].compact.reject(&:empty?).join(":")
81
+ set_environment_variable("GEM_PATH", ENV["GEM_PATH"])
55
82
  end
56
83
 
57
84
  Then(/^the suite directory named "(.*?)" should exist$/) do |name|
58
- directory = File.join(ENV['BUSSER_ROOT'], "suites", name)
59
- check_directory_presence([directory], true)
85
+ directory = File.join(ENV["BUSSER_ROOT"], "suites", name)
86
+ expect(Dir).to exist(directory)
60
87
  end
61
88
 
62
89
  Then(/^the suite directory named "(.*?)" should not exist$/) do |name|
63
- directory = File.join(ENV['BUSSER_ROOT'], "suites", name)
64
- check_directory_presence([directory], false)
90
+ directory = File.join(ENV["BUSSER_ROOT"], "suites", name)
91
+ expect(Dir).to_not exist(directory)
65
92
  end
66
93
 
67
94
  Then(/^the suite file "(.*?)" should contain exactly:$/) do |file, content|
68
- file_name = File.join(ENV['BUSSER_ROOT'], "suites", file)
69
- check_file_content(file_name, content)
95
+ file_name = File.join(ENV["BUSSER_ROOT"], "suites", file)
96
+ expect(File.read(file_name)).to eq(content)
70
97
  end
71
98
 
72
99
  Then(/^the vendor directory named "(.*?)" should exist$/) do |name|
73
- directory = File.join(ENV['BUSSER_ROOT'], "vendor", name)
74
- check_directory_presence([directory], true)
100
+ directory = File.join(ENV["BUSSER_ROOT"], "vendor", name)
101
+ expect(Dir).to exist(directory)
75
102
  end
76
103
 
77
104
  Then(/^the vendor directory named "(.*?)" should not exist$/) do |name|
78
- directory = File.join(ENV['BUSSER_ROOT'], "vendor", name)
79
- check_directory_presence([directory], false)
105
+ directory = File.join(ENV["BUSSER_ROOT"], "vendor", name)
106
+ expect(Dir).to_not exist(directory)
80
107
  end
81
108
 
82
109
  Then(/^the vendor file "(.*?)" should contain "(.*?)"$/) do |file, content|
83
- file_name = File.join(ENV['BUSSER_ROOT'], "vendor", file)
84
- check_file_content(file_name, content, true)
110
+ file_name = File.join(ENV["BUSSER_ROOT"], "vendor", file)
111
+ expect(File.read(file_name)).to include(content)
85
112
  end
86
113
 
114
+ # Check the sandboxed GEM_HOME directly. Shelling out to `gem list` depends on
115
+ # the child process inheriting the sandbox and a bundler-free environment,
116
+ # which aruba 2.x no longer arranges for us.
87
117
  Then(/^a gem named "(.*?)" is installed with version "(.*?)"$/) do |name, ver|
88
- unbundlerize do
89
- run_simple(unescape("gem list #{name} --version #{ver} -i"), true, nil)
90
- end
118
+ specs = Dir.glob(File.join(ENV["GEM_HOME"], "specifications", "#{name}-#{ver}.gemspec"))
119
+ expect(specs).to_not be_empty
91
120
  end
92
121
 
93
122
  Then(/^a gem named "(.*?)" is installed$/) do |name|
94
- unbundlerize do
95
- run_simple(unescape("gem list #{name} -i"), true, nil)
96
- end
123
+ specs = Dir.glob(File.join(ENV["GEM_HOME"], "specifications", "#{name}-*.gemspec"))
124
+ expect(specs).to_not be_empty
97
125
  end
98
126
 
99
127
  Then(/^the BUSSER_ROOT directory should exist$/) do
100
- check_directory_presence([ENV['BUSSER_ROOT']], true)
128
+ expect(Dir).to exist(ENV["BUSSER_ROOT"])
101
129
  end
102
130
 
103
131
  Then(/^a busser binstub file should contain:$/) do |partial_content|
104
- file = File.join(ENV['BUSSER_ROOT'], %w{bin busser})
105
- check_file_content(file, Regexp.new(Regexp.escape(partial_content)), true)
132
+ file = File.join(ENV["BUSSER_ROOT"], %w{bin busser})
133
+ expect(File.read(file)).to include(partial_content)
106
134
  end
107
135
 
108
136
  Then(/^a bat busser binstub file should contain:$/) do |partial_content|
109
- file = File.join(ENV['BUSSER_ROOT'], %w{bin busser.bat})
110
- check_file_content(file, Regexp.new(Regexp.escape(partial_content)), true)
111
- end
112
-
113
- Then(/^the file "(.*?)" should have permissions "(.*?)"$/) do |file, perms|
114
- in_current_directory do
115
- file_perms = sprintf("%o", File.stat(file).mode)
116
- file_perms = file_perms[2, 4]
117
- expect(file_perms).to eq(perms)
118
- end
137
+ file = File.join(ENV["BUSSER_ROOT"], %w{bin busser.bat})
138
+ expect(File.read(file)).to include(partial_content)
119
139
  end
120
140
 
121
141
  Then(/^pry me$/) do
122
- require 'pry' ; binding.pry
142
+ require "pry"; binding.pry # rubocop:disable Lint/Debugger
123
143
  end
@@ -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
 
@@ -86,6 +96,14 @@ module Busser
86
96
  )
87
97
  elsif status.success?
88
98
  true
99
+ elsif status.exitstatus.nil?
100
+ # A process killed by a signal has no exit status, and Kernel#exit
101
+ # rejects nil, so reporting it as an exit code raised a TypeError and
102
+ # took Busser down with a stack trace rather than a diagnosis. Signals
103
+ # are how the out of memory killer stops a test run, which is the same
104
+ # situation the nil status branch above was written for.
105
+ signal = status.termsig
106
+ die("#{type} [#{cmd}] was terminated by signal #{signal}", 128 + signal)
89
107
  else
90
108
  code = status.exitstatus
91
109
  die("#{type} [#{cmd}] exit code was #{code}", code)
@@ -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.1".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