busser 0.9.0 → 0.9.2

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/publish.yml +22 -0
  3. data/.release-please-manifest.json +1 -1
  4. data/.rubocop.yml +1 -4
  5. data/CHANGELOG.md +18 -0
  6. data/Gemfile +12 -5
  7. data/README.md +82 -0
  8. data/Rakefile +19 -1
  9. data/busser.gemspec +11 -8
  10. data/features/plugin_create_command.feature +7 -9
  11. data/lib/busser/command/deserialize.rb +4 -0
  12. data/lib/busser/command/plugin_create.rb +46 -3
  13. data/lib/busser/command/plugin_install.rb +40 -4
  14. data/lib/busser/command/plugin_list.rb +16 -4
  15. data/lib/busser/command/setup.rb +37 -3
  16. data/lib/busser/command/suite_cleanup.rb +3 -0
  17. data/lib/busser/command/suite_path.rb +3 -0
  18. data/lib/busser/command/test.rb +38 -1
  19. data/lib/busser/cucumber/hooks.rb +9 -0
  20. data/lib/busser/helpers.rb +24 -0
  21. data/lib/busser/plugin.rb +51 -9
  22. data/lib/busser/rubygems.rb +20 -0
  23. data/lib/busser/runner_plugin/dummy.rb +3 -0
  24. data/lib/busser/runner_plugin.rb +5 -0
  25. data/lib/busser/thor.rb +23 -0
  26. data/lib/busser/ui.rb +63 -0
  27. data/lib/busser/version.rb +2 -1
  28. data/release-please-config.json +3 -1
  29. data/spec/busser/command/deserialize_spec.rb +0 -1
  30. data/spec/busser/command/plugin_create_spec.rb +174 -0
  31. data/spec/busser/command/plugin_install_spec.rb +44 -0
  32. data/spec/busser/command/plugin_list_spec.rb +49 -0
  33. data/spec/busser/command/setup_spec.rb +120 -0
  34. data/spec/busser/command/test_spec.rb +36 -0
  35. data/spec/busser/helpers_spec.rb +10 -10
  36. data/spec/busser/plugin_spec.rb +0 -1
  37. data/spec/busser/ui_spec.rb +56 -10
  38. data/spec/spec_helper.rb +2 -3
  39. data/templates/plugin/Rakefile.erb +4 -24
  40. data/templates/plugin/features_env.rb.erb +4 -4
  41. data/templates/plugin/gemspec.erb +7 -8
  42. data/templates/plugin/github_workflow.yml.erb +26 -0
  43. metadata +13 -60
  44. data/templates/plugin/tailor.erb +0 -4
  45. data/templates/plugin/travis.yml.erb +0 -11
@@ -32,6 +32,10 @@ module Busser
32
32
  desc: "Type of binstub file to create (bourne or bat)",
33
33
  default: "bourne"
34
34
 
35
+ # Creates the Busser root and the binstub that runs Busser with an
36
+ # isolated gem environment.
37
+ #
38
+ # @return [void]
35
39
  def perform
36
40
  banner "Setting up Busser"
37
41
  create_busser_root
@@ -40,11 +44,15 @@ module Busser
40
44
 
41
45
  private
42
46
 
47
+ # @return [void]
43
48
  def create_busser_root
44
49
  info "Creating BUSSER_ROOT in #{root_path}"
45
50
  empty_directory(root_path, verbose: false)
46
51
  end
47
52
 
53
+ # Writes the binstub, in whichever flavour --type asked for.
54
+ #
55
+ # @return [void]
48
56
  def generate_busser_binstub
49
57
  info "Creating busser binstub"
50
58
 
@@ -55,6 +63,9 @@ module Busser
55
63
  end
56
64
  end
57
65
 
66
+ # Writes a Windows batch binstub.
67
+ #
68
+ # @return [void]
58
69
  def generate_busser_binstub_for_bat
59
70
  binstub = root_path + "bin/busser.bat"
60
71
  busser_root = root_path.to_s.gsub("/", "\\")
@@ -79,9 +90,19 @@ module Busser
79
90
  SET "GEM_PATH=#{gem_path}"
80
91
  SET "GEM_CACHE=#{gem_home}\\cache"
81
92
 
82
- REM Unset RUBYOPT, we don't want this bleeding into our runtime.
93
+ REM Keep the calling Ruby environment out of this one. Clearing
94
+ REM RUBYOPT is no longer enough: RubyGems requires bundler/setup
95
+ REM whenever BUNDLER_SETUP is set, and bundler then points GEM_HOME
96
+ REM at the calling project's bundle, undoing the isolation above.
83
97
  SET RUBYOPT=
84
98
  SET GEMRC=
99
+ SET RUBYLIB=
100
+ SET BUNDLER_SETUP=
101
+ SET BUNDLER_VERSION=
102
+ SET BUNDLE_BIN_PATH=
103
+ SET BUNDLE_GEMFILE=
104
+ SET BUNDLE_LOCKFILE=
105
+ SET BUNDLE_PATH=
85
106
 
86
107
  REM Call the actual Busser bin with our arguments
87
108
  "#{ruby_bin}" "#{gem_bindir}\\busser" %*
@@ -95,6 +116,9 @@ module Busser
95
116
  end
96
117
  end
97
118
 
119
+ # Writes a Bourne shell binstub, executable.
120
+ #
121
+ # @return [void]
98
122
  def generate_busser_binstub_for_bourne
99
123
  binstub = root_path + "bin/busser"
100
124
 
@@ -120,8 +144,12 @@ module Busser
120
144
  GEM_PATH="#{gem_path}"; export GEM_PATH
121
145
  GEM_CACHE="#{gem_home}/cache"; export GEM_CACHE
122
146
 
123
- # Unset RUBYOPT, we don't want this bleeding into our runtime.
124
- unset RUBYOPT GEMRC
147
+ # Keep the calling Ruby environment out of this one. Unsetting
148
+ # RUBYOPT is no longer enough: RubyGems requires bundler/setup
149
+ # whenever BUNDLER_SETUP is set, and bundler then points GEM_HOME
150
+ # at the calling project's bundle, undoing the isolation above.
151
+ unset RUBYOPT GEMRC RUBYLIB BUNDLER_SETUP BUNDLER_VERSION
152
+ unset BUNDLE_BIN_PATH BUNDLE_GEMFILE BUNDLE_LOCKFILE BUNDLE_PATH
125
153
 
126
154
  # Call the actual Busser bin with our arguments
127
155
  exec "#{ruby_bin}" "#{gem_bindir}/busser" "$@"
@@ -130,6 +158,8 @@ module Busser
130
158
  chmod(binstub, 0755, verbose: false)
131
159
  end
132
160
 
161
+ # @return [String] path to the Ruby that is running Busser, in the path
162
+ # style the target binstub needs
133
163
  def ruby_bin
134
164
  result = if (bindir = RbConfig::CONFIG["bindir"])
135
165
  File.join(bindir, "ruby")
@@ -140,18 +170,22 @@ module Busser
140
170
  result
141
171
  end
142
172
 
173
+ # @return [String] the isolated gem home the binstub exports
143
174
  def gem_home
144
175
  Gem.paths.home.dup.tap { |p| p.gsub!("/", "\\") if bat? }
145
176
  end
146
177
 
178
+ # @return [String] the gem path the binstub exports
147
179
  def gem_path
148
180
  Gem.paths.path.join(":").dup.tap { |p| p.gsub!("/", "\\") if bat? }
149
181
  end
150
182
 
183
+ # @return [String] directory holding the real busser executable
151
184
  def gem_bindir
152
185
  Gem.bindir.dup.tap { |p| p.gsub!("/", "\\") if bat? }
153
186
  end
154
187
 
188
+ # @return [Boolean] true when generating a Windows batch binstub
155
189
  def bat?
156
190
  options[:type] == "bat"
157
191
  end
@@ -28,6 +28,9 @@ module Busser
28
28
  #
29
29
  class SuiteCleanup < Busser::Thor::BaseGroup
30
30
 
31
+ # Removes the suites directory and everything in it.
32
+ #
33
+ # @return [void]
31
34
  def cleanup
32
35
  if suite_path.directory?
33
36
  Pathname.glob(suite_path + "*").each do |dir|
@@ -29,6 +29,9 @@ module Busser
29
29
 
30
30
  argument :suite_name, required: false
31
31
 
32
+ # Prints where suites live, or where one named suite lives.
33
+ #
34
+ # @return [void]
32
35
  def path
33
36
  say suite_path(suite_name).to_s
34
37
  end
@@ -15,11 +15,14 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
 
18
+ require "shellwords" unless defined?(Shellwords)
19
+
18
20
  require "busser/thor"
19
21
  require "busser/plugin"
20
22
 
21
23
  module Busser
22
24
 
25
+ # Namespace for Busser's subcommands.
23
26
  module Command
24
27
 
25
28
  # Test command.
@@ -30,6 +33,10 @@ module Busser
30
33
 
31
34
  argument :plugins, type: :array, required: false
32
35
 
36
+ # Runs each requested plugin's suite, or every installed plugin's suite
37
+ # when none were named.
38
+ #
39
+ # @return [void]
33
40
  def perform
34
41
  Busser::Plugin.runner_plugins(plugins).each do |runner_path|
35
42
  runner = File.basename(runner_path)
@@ -44,26 +51,56 @@ module Busser
44
51
  end
45
52
  end
46
53
 
54
+ # Builds the command that runs a suite's prepare.sh.
55
+ #
56
+ # The path is quoted. It is rooted at BUSSER_ROOT, which the caller
57
+ # chooses, so an unquoted path containing a space would be split by the
58
+ # shell and sh would be handed a fragment instead of the script.
59
+ #
60
+ # @param script [String, Pathname] path to the prepare.sh
61
+ # @return [String] the command to run
62
+ def self.prepare_sh_command(script)
63
+ "/bin/sh #{Shellwords.escape(script.to_s)}"
64
+ end
65
+
47
66
  private
48
67
 
68
+ # The dummy runner exists for Busser's own tests, so it only runs when
69
+ # asked for by name.
70
+ #
71
+ # @param runner [String] short plugin name
72
+ # @return [Boolean] true if this runner should be passed over
49
73
  def skip_runner?(runner)
50
74
  runner == "dummy" && ! Array(plugins).include?("dummy")
51
75
  end
52
76
 
77
+ # Runs whatever preparation the suite ships before its tests.
78
+ #
79
+ # @param runner [String] short plugin name
80
+ # @return [void]
53
81
  def prepare_suite(runner)
54
82
  run_prepare_sh(runner)
55
83
  run_prepare_recipe(runner)
56
84
  end
57
85
 
86
+ # Runs the suite's prepare.sh, if it has one.
87
+ #
88
+ # @param runner [String] short plugin name
89
+ # @return [void]
58
90
  def run_prepare_sh(runner)
59
91
  prepare_sh_script = suite_path(runner).join("prepare.sh")
60
92
 
61
93
  if prepare_sh_script.exist?
62
94
  banner "Preparing #{runner} suite with #{prepare_sh_script}"
63
- run!("/bin/sh #{prepare_sh_script}")
95
+ run!(self.class.prepare_sh_command(prepare_sh_script))
64
96
  end
65
97
  end
66
98
 
99
+ # Warns if the suite still ships the prepare_recipe.rb that Busser
100
+ # dropped support for, rather than ignoring it silently.
101
+ #
102
+ # @param runner [String] short plugin name
103
+ # @return [void]
67
104
  def run_prepare_recipe(runner)
68
105
  prepare_recipe = suite_path(runner).join("prepare_recipe.rb")
69
106
 
@@ -14,10 +14,19 @@ After do
14
14
  end
15
15
  end
16
16
 
17
+ # Stashes an environment variable so a scenario can change it and have the
18
+ # original put back afterwards.
19
+ #
20
+ # @param key [String] the variable name
21
+ # @return [String, nil] the stashed value
17
22
  def backup_envvar(key)
18
23
  ENV["_CUKE_#{key}"] = ENV[key]
19
24
  end
20
25
 
26
+ # Puts back a variable stashed by {backup_envvar}.
27
+ #
28
+ # @param key [String] the variable name
29
+ # @return [String, nil] the restored value
21
30
  def restore_envvar(key)
22
31
  ENV[key] = ENV.delete("_CUKE_#{key}")
23
32
  end
@@ -29,22 +29,41 @@ module Busser
29
29
 
30
30
  module_function
31
31
 
32
+ # Path to the suites directory, or to one suite inside it.
33
+ #
34
+ # @param name [String, nil] a suite name, or nil for the containing
35
+ # directory
36
+ # @return [Pathname] absolute path beneath the Busser root
32
37
  def suite_path(name = nil)
33
38
  path = root_path + "suites"
34
39
  path += name if name
35
40
  path.expand_path
36
41
  end
37
42
 
43
+ # Path to the vendor directory, or to one vendored product inside it.
44
+ #
45
+ # @param product [String, nil] a product name, or nil for the containing
46
+ # directory
47
+ # @return [Pathname] absolute path beneath the Busser root
38
48
  def vendor_path(product = nil)
39
49
  path = root_path + "vendor"
40
50
  path += product if product
41
51
  path.expand_path
42
52
  end
43
53
 
54
+ # The Busser root, where plugins, suites and vendored products live.
55
+ #
56
+ # @return [Pathname] BUSSER_ROOT if set, otherwise /opt/busser
44
57
  def root_path
45
58
  Pathname.new(ENV["BUSSER_ROOT"] || "/opt/busser")
46
59
  end
47
60
 
61
+ # No longer supported. Kept so a plugin still calling it warns rather
62
+ # than dying with a NoMethodError halfway through a test run.
63
+ #
64
+ # @param config [Hash] ignored
65
+ # @return [void]
66
+ # @deprecated Shell out or use a Thor action instead.
48
67
  def chef_apply(config = {}, &block)
49
68
  warn "Apologies, but Busser no longer supports the chef_apply helper," +
50
69
  " so the contents of this block will not be executed. Please refactor" +
@@ -52,6 +71,11 @@ module Busser
52
71
  " strategy"
53
72
  end
54
73
 
74
+ # Installs a gem into the Busser root's gem home.
75
+ #
76
+ # @param gem [String] the gem name
77
+ # @param version [String, nil] a requirement string, or nil for any version
78
+ # @return [Gem::Specification, nil] the spec that was installed
55
79
  def install_gem(gem, version = nil)
56
80
  Busser::RubyGems.install_gem(gem, version)
57
81
  end
data/lib/busser/plugin.rb CHANGED
@@ -17,6 +17,7 @@
17
17
 
18
18
  module Busser
19
19
 
20
+ # Namespace that runner plugins define their classes inside.
20
21
  module RunnerPlugin
21
22
  end
22
23
 
@@ -28,10 +29,17 @@ module Busser
28
29
 
29
30
  module_function
30
31
 
32
+ # @param plugin_name [String] short plugin name, such as "bash"
33
+ # @return [String] the path to require for that plugin
31
34
  def runner_plugin(plugin_name)
32
35
  "busser/runner_plugin/#{plugin_name}"
33
36
  end
34
37
 
38
+ # Require paths for the named plugins, or for every installed plugin.
39
+ #
40
+ # @param plugin_names [Array<String>, String, nil] plugin names, or nil for
41
+ # everything installed
42
+ # @return [Array<String>] require paths, without duplicates
35
43
  def runner_plugins(plugin_names = nil)
36
44
  if plugin_names
37
45
  Array(plugin_names).map { |plugin| runner_plugin(plugin) }.uniq
@@ -46,33 +54,67 @@ module Busser
46
54
  # duplicates made `busser test` run a suite twice and `busser plugin list`
47
55
  # print it twice. The path is what gets required, and require resolves to
48
56
  # the active version, so collapsing the repeats is safe.
57
+ # Require paths for every runner plugin installed on this machine.
58
+ #
59
+ # @return [Array<String>] require paths, without duplicates
49
60
  def all_runner_plugins
50
61
  Gem.find_files("busser/runner_plugin/*.rb").map do |file|
51
62
  "busser/runner_plugin/#{File.basename(file).sub(/\.rb$/, "")}"
52
63
  end.uniq
53
64
  end
54
65
 
66
+ # Requires a plugin, exiting with a readable message rather than a
67
+ # backtrace if it cannot be loaded.
68
+ #
69
+ # @param plugin_path [String] the path to require
70
+ # @return [void]
55
71
  def require!(plugin_path)
56
72
  require plugin_path
57
73
  rescue LoadError => e
58
74
  Busser::UI.die "Could not load #{plugin_path} (#{e.class}: #{e.message})"
59
75
  end
60
76
 
77
+ # @param klass [String, Symbol] a constant name inside
78
+ # {Busser::RunnerPlugin}
79
+ # @return [Class] the runner plugin class
61
80
  def runner_class(klass)
62
81
  Busser::RunnerPlugin.const_get(klass)
63
82
  end
64
83
 
84
+ # Finds the gemspec a plugin was loaded from.
85
+ #
86
+ # A plugin being developed locally is not an installed gem, so the local
87
+ # gemspec is preferred when the plugin resolves inside the working tree.
88
+ #
89
+ # @param plugin_path [String] the plugin's require path
90
+ # @return [Gem::Specification] the spec the plugin belongs to
65
91
  def gem_from_path(plugin_path)
66
- local_gem_path = "#{File.expand_path(plugin_path, $LOAD_PATH.first)}"
67
- local_gemspec = File.join(
68
- File.dirname($LOAD_PATH.first), "busser.gemspec"
69
- )
92
+ # Ask RubyGems first. Loading a gemspec *evaluates* it, and these
93
+ # gemspecs shell out to `git ls-files` to build their file list -- so
94
+ # asking the working tree first printed "fatal: not a git repository"
95
+ # into the middle of `busser plugin list` for every installed plugin.
96
+ # find_by_path answers from the installed specs without running anything.
97
+ Gem::Specification.find_by_path(plugin_path) ||
98
+ local_gemspec_for(plugin_path)
99
+ end
70
100
 
71
- if ! Dir.glob("#{local_gem_path}#{Gem.suffix_pattern}").empty?
72
- Gem::Specification.load(File.expand_path(local_gemspec))
73
- else
74
- Gem::Specification.find_by_path(plugin_path)
75
- end
101
+ # Falls back to a gemspec in the working tree, which is how a plugin being
102
+ # developed locally -- and so not installed as a gem -- is resolved.
103
+ #
104
+ # @param plugin_path [String] the plugin's require path
105
+ # @return [Gem::Specification, nil] the spec, or nil if there is no local
106
+ # gemspec to read
107
+ def local_gemspec_for(plugin_path)
108
+ root = $LOAD_PATH.first
109
+ return nil if root.nil?
110
+
111
+ local_gem_path = File.expand_path(plugin_path, root)
112
+ return nil if Dir.glob("#{local_gem_path}#{Gem.suffix_pattern}").empty?
113
+
114
+ local_gemspec = File.join(File.dirname(root), "busser.gemspec")
115
+ return nil unless File.exist?(local_gemspec)
116
+
117
+ Gem::Specification.load(local_gemspec)
76
118
  end
77
119
  end
78
120
  end
@@ -27,11 +27,24 @@ module Busser
27
27
 
28
28
  module_function
29
29
 
30
+ # @param name [String] the gem name
31
+ # @param version [String, nil] a requirement string, or nil for any version
32
+ # @return [Boolean] true if a matching gem is already available
30
33
  def gem_installed?(name, version)
31
34
  version = Gem::Requirement.default unless version
32
35
  ! Gem::Dependency.new(name, version).matching_specs.empty?
33
36
  end
34
37
 
38
+ # Installs a gem into GEM_HOME.
39
+ #
40
+ # RubyGems under bundler points Gem.dir at the bundle, which is not where
41
+ # Busser plugins belong, so GEM_HOME is applied explicitly before the
42
+ # install and the freshly installed spec is put on the load path by hand.
43
+ #
44
+ # @param gem_name [String] the gem name
45
+ # @param version [String, nil] a requirement string, or nil for any version
46
+ # @return [Gem::Specification, nil] the installed spec, or nil if the gem
47
+ # was not among those installed
35
48
  def install_gem(gem_name, version)
36
49
  version = Gem::Requirement.default unless version
37
50
 
@@ -54,6 +67,8 @@ module Busser
54
67
  spec
55
68
  end
56
69
 
70
+ # @return [Hash] options handed to RubyGems' dependency installer, with
71
+ # the install directory pinned to GEM_HOME
57
72
  def rbg_options
58
73
  @rbg_options ||= Gem::DependencyInstaller::DEFAULT_OPTIONS.merge(
59
74
  suggest_alternate: false,
@@ -67,6 +82,11 @@ module Busser
67
82
  )
68
83
  end
69
84
 
85
+ # Runs a block with RubyGems' own progress output suppressed, unless the
86
+ # user asked for verbose output.
87
+ #
88
+ # @yield the block to run quietly
89
+ # @return [Object] whatever the block returned
70
90
  def silence_gem_ui
71
91
  interaction = Gem::DefaultUserInteraction.ui
72
92
  unless Gem.configuration.really_verbose
@@ -34,6 +34,9 @@ class Busser::RunnerPlugin::Dummy < Busser::RunnerPlugin::Base
34
34
  create_file("#{dummy_path}/foobar.txt", "The Dummy Driver.")
35
35
  end
36
36
 
37
+ # Prints a line so Busser's own suite can prove a plugin ran.
38
+ #
39
+ # @return [void]
37
40
  def test
38
41
  banner "[dummy] Running"
39
42
  if File.exist?(File.join(suite_path("dummy"), "foobar.txt"))
@@ -27,6 +27,11 @@ module Busser
27
27
  #
28
28
  class Base < Busser::Thor::BaseGroup
29
29
 
30
+ # Declares work to run once, when Busser installs this plugin -- which
31
+ # is where a plugin installs the test framework it drives.
32
+ #
33
+ # @yield the postinstall body, evaluated on the plugin instance
34
+ # @return [void]
30
35
  def self.postinstall(&block)
31
36
  (class << self; self; end).send(:define_method, :run_postinstall) do
32
37
  klass = Class.new(Busser::Thor::BaseGroup) do
data/lib/busser/thor.rb CHANGED
@@ -22,6 +22,7 @@ require "busser/ui"
22
22
 
23
23
  module Busser
24
24
 
25
+ # Thor base classes preloaded with Busser's helpers and output methods.
25
26
  module Thor
26
27
 
27
28
  # Base class for all Thor subclasses which includes useful mixins.
@@ -33,6 +34,17 @@ module Busser
33
34
  include Helpers
34
35
  include UI
35
36
  include ::Thor::Actions
37
+
38
+ # Thor exits 0 when a command fails unless this is true, and warns on
39
+ # every failure that it is not set. Busser is a test runner: a failure
40
+ # has to be a non-zero exit or CI reports a red suite as green. Thor 2
41
+ # makes this the default; setting it now silences the deprecation on
42
+ # every failing run and pins the behaviour we already rely on.
43
+ #
44
+ # @return [true]
45
+ def self.exit_on_failure?
46
+ true
47
+ end
36
48
  end
37
49
 
38
50
  # Base class for all Thor Group subclasses which includes useful mixins.
@@ -44,6 +56,17 @@ module Busser
44
56
  include Helpers
45
57
  include UI
46
58
  include ::Thor::Actions
59
+
60
+ # Thor exits 0 when a command fails unless this is true, and warns on
61
+ # every failure that it is not set. Busser is a test runner: a failure
62
+ # has to be a non-zero exit or CI reports a red suite as green. Thor 2
63
+ # makes this the default; setting it now silences the deprecation on
64
+ # every failing run and pins the behaviour we already rely on.
65
+ #
66
+ # @return [true]
67
+ def self.exit_on_failure?
68
+ true
69
+ end
47
70
  end
48
71
  end
49
72
  end
data/lib/busser/ui.rb CHANGED
@@ -29,30 +29,56 @@ module Busser
29
29
 
30
30
  # Thor::Shell's delegated methods are not mixed into this module, so
31
31
  # provide the two output primitives it relies on.
32
+ # @param msg [String] text to write to stdout
33
+ # @return [void]
32
34
  def say(msg)
33
35
  $stdout.puts(msg)
34
36
  end
35
37
 
38
+ # @param msg [String] text to write to stderr
39
+ # @return [void]
36
40
  def error(msg)
37
41
  $stderr.puts(msg)
38
42
  end
39
43
 
44
+ # Announces a step, at the top level of the output.
45
+ #
46
+ # @param msg [String] the message
47
+ # @return [void]
40
48
  def banner(msg)
41
49
  say("-----> #{msg}")
42
50
  end
43
51
 
52
+ # Reports detail beneath a banner.
53
+ #
54
+ # @param msg [String] the message
55
+ # @return [void]
44
56
  def info(msg)
45
57
  say(" #{msg}")
46
58
  end
47
59
 
60
+ # Reports something the user should notice but which is not fatal.
61
+ #
62
+ # @param msg [String] the message
63
+ # @return [void]
48
64
  def warn(msg)
49
65
  say(">>>>>> #{msg}")
50
66
  end
51
67
 
68
+ # Reports a failure, on stderr.
69
+ #
70
+ # @param msg [String] the message
71
+ # @return [void]
52
72
  def fatal(msg)
53
73
  error("!!!!!! #{msg}")
54
74
  end
55
75
 
76
+ # Runs a shell command, exiting with a diagnosis if it fails.
77
+ #
78
+ # @param cmd [String] the command line
79
+ # @param config [Hash] options passed through to Thor's runner
80
+ # @return [true] if the command succeeded
81
+ # @see #handle_command
56
82
  def run!(cmd, config = {})
57
83
  config = { capture: false, verbose: false }.merge(config)
58
84
 
@@ -61,6 +87,13 @@ module Busser
61
87
  end
62
88
  end
63
89
 
90
+ # Runs a Ruby script with the current interpreter, exiting with a
91
+ # diagnosis if it fails.
92
+ #
93
+ # @param cmd [String] the script and its arguments
94
+ # @param config [Hash] options passed through to Thor's runner
95
+ # @return [true] if the script succeeded
96
+ # @see #handle_command
64
97
  def run_ruby_script!(cmd, config = {})
65
98
  config = { capture: false, verbose: false }.merge(config)
66
99
 
@@ -69,15 +102,37 @@ module Busser
69
102
  end
70
103
  end
71
104
 
105
+ # Reports a failure and exits.
106
+ #
107
+ # @param msg [String] the message
108
+ # @param exitstatus [Integer] the status to exit with
109
+ # @return [void] does not return
72
110
  def die(msg, exitstatus = 1)
73
111
  fatal(msg)
74
112
  exit(exitstatus)
75
113
  end
76
114
 
115
+ # Wrapped so tests can stub it.
116
+ #
117
+ # @return [Process::Status, nil] the status of the last child process
77
118
  def status
78
119
  $?
79
120
  end
80
121
 
122
+ # Runs a block and turns however the child process ended into a readable
123
+ # message and a matching exit status.
124
+ #
125
+ # Three endings are distinguished because they mean different things to
126
+ # someone reading CI output: a nil status usually means the machine ran out
127
+ # of memory before the child could report; a signal means something killed
128
+ # the run, typically the out of memory killer; and an exit code means the
129
+ # command itself failed.
130
+ #
131
+ # @param type [String] what was run, for the message
132
+ # @param cmd [String] the command line, for the message
133
+ # @yield runs the command
134
+ # @return [true] if the command succeeded
135
+ # @raise [StandardError] re-raised if the block itself raised
81
136
  def handle_command(type, cmd)
82
137
  begin
83
138
  yield
@@ -96,6 +151,14 @@ module Busser
96
151
  )
97
152
  elsif status.success?
98
153
  true
154
+ elsif status.exitstatus.nil?
155
+ # A process killed by a signal has no exit status, and Kernel#exit
156
+ # rejects nil, so reporting it as an exit code raised a TypeError and
157
+ # took Busser down with a stack trace rather than a diagnosis. Signals
158
+ # are how the out of memory killer stops a test run, which is the same
159
+ # situation the nil status branch above was written for.
160
+ signal = status.termsig
161
+ die("#{type} [#{cmd}] was terminated by signal #{signal}", 128 + signal)
99
162
  else
100
163
  code = status.exitstatus
101
164
  die("#{type} [#{cmd}] exit code was #{code}", code)
@@ -16,5 +16,6 @@
16
16
  # limitations under the License.
17
17
 
18
18
  module Busser
19
- VERSION = "0.9.0".freeze
19
+ # Version string for Busser
20
+ VERSION = "0.9.2".freeze
20
21
  end
@@ -7,7 +7,9 @@
7
7
  "changelog-path": "CHANGELOG.md",
8
8
  "release-type": "ruby",
9
9
  "include-component-in-tag": false,
10
- "version-file": "lib/busser/version.rb"
10
+ "version-file": "lib/busser/version.rb",
11
+ "bump-minor-pre-major": true,
12
+ "bump-patch-for-minor-pre-major": true
11
13
  }
12
14
  }
13
15
  }
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  #
3
2
  # Licensed under the Apache License, Version 2.0 (the "License");
4
3
  # you may not use this file except in compliance with the License.