busser 0.9.1 → 0.9.3

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/integration.yml +55 -0
  3. data/.github/workflows/publish.yml +22 -0
  4. data/.gitignore +1 -0
  5. data/.release-please-manifest.json +1 -1
  6. data/.rubocop.yml +1 -4
  7. data/CHANGELOG.md +19 -0
  8. data/Gemfile +12 -5
  9. data/README.md +82 -0
  10. data/Rakefile +19 -1
  11. data/busser.gemspec +11 -8
  12. data/kitchen-preinstall.sh +55 -0
  13. data/kitchen.yml +27 -0
  14. data/lib/busser/command/deserialize.rb +4 -0
  15. data/lib/busser/command/plugin_create.rb +50 -0
  16. data/lib/busser/command/plugin_install.rb +40 -4
  17. data/lib/busser/command/plugin_list.rb +16 -4
  18. data/lib/busser/command/setup.rb +27 -1
  19. data/lib/busser/command/suite_cleanup.rb +3 -0
  20. data/lib/busser/command/suite_path.rb +3 -0
  21. data/lib/busser/command/test.rb +38 -1
  22. data/lib/busser/cucumber/hooks.rb +9 -0
  23. data/lib/busser/helpers.rb +24 -0
  24. data/lib/busser/plugin.rb +51 -9
  25. data/lib/busser/rubygems.rb +20 -0
  26. data/lib/busser/runner_plugin/dummy.rb +3 -0
  27. data/lib/busser/runner_plugin.rb +5 -0
  28. data/lib/busser/thor.rb +23 -0
  29. data/lib/busser/ui.rb +55 -0
  30. data/lib/busser/version.rb +2 -1
  31. data/release-please-config.json +3 -1
  32. data/renovate.json +1 -0
  33. data/spec/busser/command/deserialize_spec.rb +0 -1
  34. data/spec/busser/command/plugin_create_spec.rb +79 -7
  35. data/spec/busser/command/plugin_install_spec.rb +44 -0
  36. data/spec/busser/command/plugin_list_spec.rb +49 -0
  37. data/spec/busser/command/setup_spec.rb +21 -1
  38. data/spec/busser/command/test_spec.rb +36 -0
  39. data/spec/busser/helpers_spec.rb +10 -10
  40. data/spec/busser/plugin_spec.rb +0 -1
  41. data/spec/busser/ui_spec.rb +7 -7
  42. data/spec/spec_helper.rb +2 -3
  43. data/templates/plugin/Gemfile.erb +7 -0
  44. data/templates/plugin/Rakefile.erb +5 -1
  45. data/templates/plugin/features_env.rb.erb +3 -8
  46. data/templates/plugin/gemspec.erb +2 -6
  47. data/templates/plugin/github_workflow.yml.erb +1 -1
  48. data/test/integration/default/bash/smoke_test.sh +12 -0
  49. metadata +15 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c0be7ee63d2b1d111438e2039187a5283308daae897a27d6028c08d9cefa13d
4
- data.tar.gz: 49e9d09408a9cc284cfb9b43f8daeac1f55eda961ad66697dbefe8741ce2976a
3
+ metadata.gz: 22ea0046cc1c5cb2f4473f9315c506ee3ebd4b22d7cf35de392060ae02816ae9
4
+ data.tar.gz: a42365f1541af1364e6eb82d06049a2910fc5fbceb63cf137e6aaad5243f1d64
5
5
  SHA512:
6
- metadata.gz: 71b525573141abe2bf35be2559189a811328e608eaef9a1ead8390595b7d500e76344ef7d29c624028c42d3e7a288a23c41e2e4a09d288734101ee9de2975d35
7
- data.tar.gz: 4d769038c5032cfefdc0b50217f77e6950d2121162c4a07bdbd7d3a7a4b1b794d35a7d501b94ad5621c7628e942cd8d4f56c90d87fa01d0e7aab722e0961fd54
6
+ metadata.gz: 4fe454f0801dff2a7dac3677a73f98055bf2368ccaa503a1fb641997cf0210482bfd9ff456f853e4e4a8a7e40c4291e8c67f4f9dfc65ce574596fd8be1123cbd
7
+ data.tar.gz: 3138b112b0ac5a405361553b0a20e9ea43fbbf5df16fbeb6be0b7cb325811e6c494e8d44998af2dc15c6fc163b7568ec681c3a8edd7dddff79018ade13eb46c1
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: "Test Kitchen"
3
+
4
+ "on":
5
+ push:
6
+ branches: [main]
7
+ pull_request:
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ concurrency:
14
+ group: ${{ github.workflow }}-${{ github.ref }}
15
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
16
+
17
+ jobs:
18
+ kitchen:
19
+ name: "kitchen verify on Ruby ${{ matrix.ruby }}"
20
+ runs-on: ubuntu-latest
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ # The ends of the supported range. The unit and feature suites already
25
+ # cover everything in between; this job is about the Test Kitchen
26
+ # integration, not Ruby compatibility.
27
+ ruby: ["3.2", "4.0"]
28
+ env:
29
+ # Outside the checkout on purpose. The runners require "bundler/setup",
30
+ # which walks up from the suite looking for a Gemfile -- so a Busser root
31
+ # inside the workspace finds this project's Gemfile and tries to
32
+ # materialize that bundle inside the isolated gem home, which fails. A
33
+ # real Busser root is /opt/busser, never inside the project.
34
+ BUSSER_ROOT: /tmp/busser-kitchen
35
+ steps:
36
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
37
+
38
+ # Deliberately no bundler-cache, and nothing below runs under `bundle
39
+ # exec`. The verifier shells out to `gem list` to decide what to install,
40
+ # and bundler in the environment makes that report the bundle's gems
41
+ # rather than the Busser root's -- so it skips installing busser and the
42
+ # run dies on a path that was never created. Test Kitchen is not run from
43
+ # inside a project's bundle in real use either.
44
+ - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
45
+ with:
46
+ ruby-version: ${{ matrix.ruby }}
47
+
48
+ - name: Install Test Kitchen
49
+ run: gem install test-kitchen --no-document --version "~> 4.1"
50
+
51
+ - name: Install this working tree into the Busser root
52
+ run: ./kitchen-preinstall.sh
53
+
54
+ - name: kitchen verify
55
+ run: kitchen verify
@@ -43,3 +43,25 @@ jobs:
43
43
  uses: actionshub/publish-gem-to-rubygems@f9126f7a2d36a4fd31a13e78fde5fbdcc4b7b251 # v2.0.6
44
44
  with:
45
45
  token: ${{ secrets.RUBYGEMS_API_KEY }}
46
+
47
+ # actionshub/publish-gem-to-rubygems logs a rejected push and still exits
48
+ # 0, so a release that never reached RubyGems shows up as a green job.
49
+ # Ask RubyGems directly instead of trusting the step. The v2 endpoint is
50
+ # 404 until the exact version is indexed, so -f turns "not there" into a
51
+ # non-zero exit rather than a substring match on `gem list` output.
52
+ - name: Verify the release reached RubyGems
53
+ if: ${{ steps.release.outputs.release_created }}
54
+ env:
55
+ VERSION: ${{ steps.release.outputs.version }}
56
+ run: |
57
+ set -euo pipefail
58
+ name="$(basename "$(ls ./*.gemspec)" .gemspec)"
59
+ for _ in $(seq 1 12); do
60
+ if curl -fsS -o /dev/null "https://rubygems.org/api/v2/rubygems/${name}/versions/${VERSION}.json"; then
61
+ echo "${name} ${VERSION} is on RubyGems"
62
+ exit 0
63
+ fi
64
+ sleep 15
65
+ done
66
+ echo "::error::${name} ${VERSION} never reached RubyGems. A permissions failure in the push step above does not fail that step -- read its log."
67
+ exit 1
data/.gitignore CHANGED
@@ -14,3 +14,4 @@ spec/reports
14
14
  test/tmp
15
15
  test/version_tmp
16
16
  tmp
17
+ .kitchen/
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.9.1"
2
+ ".": "0.9.3"
3
3
  }
data/.rubocop.yml CHANGED
@@ -3,7 +3,4 @@ require:
3
3
  - cookstyle/chefstyle
4
4
 
5
5
  AllCops:
6
- TargetRubyVersion: 3.1
7
- Exclude:
8
- - "vendor/**/*"
9
- - "spec/**/*"
6
+ TargetRubyVersion: 3.2
data/CHANGELOG.md CHANGED
@@ -33,6 +33,25 @@ main reason to take this version.
33
33
  * Rewritten README, and development documentation moved to
34
34
  CONTRIBUTING.md ([#63](https://github.com/test-kitchen/busser/pull/63))
35
35
 
36
+ ## [0.9.3](https://github.com/test-kitchen/busser/compare/v0.9.2...v0.9.3) (2026-08-29)
37
+
38
+
39
+ ### Bug Fixes
40
+
41
+ * bring the generated plugin scaffold up to date ([#81](https://github.com/test-kitchen/busser/issues/81)) ([f67b8d0](https://github.com/test-kitchen/busser/commit/f67b8d0979d924b2cb653dace74a80fec03a3da5))
42
+ * reject plugin names that cannot be a Ruby constant ([#80](https://github.com/test-kitchen/busser/issues/80)) ([bd7ac17](https://github.com/test-kitchen/busser/commit/bd7ac1749afe458e35328ebb7b57c64634ad1acc))
43
+ * use the Windows path separator in the bat binstub ([#79](https://github.com/test-kitchen/busser/issues/79)) ([1eb0cbf](https://github.com/test-kitchen/busser/commit/1eb0cbf744ce68fc3c4126bf2eec73f784f422e5))
44
+
45
+ ## [0.9.2](https://github.com/test-kitchen/busser/compare/v0.9.1...v0.9.2) (2026-08-28)
46
+
47
+
48
+ ### Bug Fixes
49
+
50
+ * quote the prepare.sh path before handing it to a shell ([#73](https://github.com/test-kitchen/busser/issues/73)) ([00a24a4](https://github.com/test-kitchen/busser/commit/00a24a4d4c286f50ac698b2cda9e9600b1c54322))
51
+ * restore SSL verification if a plugin postinstall raises ([#72](https://github.com/test-kitchen/busser/issues/72)) ([7f26adb](https://github.com/test-kitchen/busser/commit/7f26adbca1682bd152afb8fc15d90feb55c15d8e))
52
+ * stop `busser plugin list` printing git errors and a phantom plugin ([#77](https://github.com/test-kitchen/busser/issues/77)) ([7a99ebd](https://github.com/test-kitchen/busser/commit/7a99ebd8453ddc836bc0abf0af0f639f07b8d554))
53
+ * tell Thor to exit non-zero when a command fails ([#76](https://github.com/test-kitchen/busser/issues/76)) ([218ba31](https://github.com/test-kitchen/busser/commit/218ba31b812db55cba2479ba6345d305743cb0c1))
54
+
36
55
  ## [0.9.1](https://github.com/test-kitchen/busser/compare/v0.9.0...v0.9.1) (2026-08-28)
37
56
 
38
57
 
data/Gemfile CHANGED
@@ -1,13 +1,20 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gemspec development_group: :test
3
+ gemspec
4
+
4
5
  group :cookstyle do
5
- gem "cookstyle"
6
+ gem "cookstyle", ">= 9.0"
6
7
  end
7
8
 
8
9
  group :test do
9
- gem "minitest", ">= 6.0"
10
- gem "base64" # cucumber needs it; not a default gem on Ruby 4.0
10
+ gem "aruba", ">= 2.4"
11
+ gem "base64", ">= 0.3" # cucumber needs it; not a default gem on Ruby 4.0
11
12
  gem "cucumber", ">= 11.1"
12
- gem "rake"
13
+ gem "minitest", ">= 6.0"
14
+ gem "mocha", ">= 2.7"
15
+ gem "rake", ">= 13.4"
16
+ end
17
+
18
+ group :development do
19
+ gem "yard", ">= 0.9.37"
13
20
  end
data/README.md CHANGED
@@ -87,6 +87,88 @@ busser test
87
87
  busser test bash minitest
88
88
  ```
89
89
 
90
+ ## Using Busser with Test Kitchen
91
+
92
+ This is how most people meet Busser, and it needs no `busser` commands of your
93
+ own. Select the verifier in `kitchen.yml`:
94
+
95
+ ```yaml
96
+ verifier:
97
+ name: busser
98
+
99
+ suites:
100
+ - name: default
101
+ ```
102
+
103
+ Then put tests in a directory named after the plugin that should run them,
104
+ inside the suite:
105
+
106
+ ```text
107
+ test/integration/default/bash/smoke_test.sh
108
+ ```
109
+
110
+ `kitchen verify` installs Busser and the matching plugin on the instance, then
111
+ runs the suite. Which plugin runs is decided by that directory name alone --
112
+ `bash/` is picked up by busser-bash, `minitest/` by busser-minitest, and so on.
113
+ There is nothing else to configure.
114
+
115
+ ## A worked example, without Test Kitchen
116
+
117
+ To see Busser on its own, set a `BUSSER_ROOT` you can write to:
118
+
119
+ ```bash
120
+ export BUSSER_ROOT=/tmp/busser
121
+ busser setup
122
+ busser plugin install busser-bash
123
+ mkdir -p "$BUSSER_ROOT/suites/bash"
124
+
125
+ cat > "$BUSSER_ROOT/suites/bash/smoke_test.sh" <<'SH'
126
+ #!/usr/bin/env bash
127
+ set -euo pipefail
128
+ echo hello
129
+ SH
130
+
131
+ busser test bash
132
+ ```
133
+
134
+ A passing run looks like this, and exits `0`:
135
+
136
+ ```text
137
+ -----> Running bash test suite
138
+ -----> [bash] smoke_test.sh
139
+ hello
140
+ ```
141
+
142
+ A failing one names the command and exits non-zero:
143
+
144
+ ```text
145
+ -----> Running bash test suite
146
+ -----> [bash] smoke_test.sh
147
+ !!!!!! Command [bash /tmp/busser/suites/bash/smoke_test.sh] exit code was 1
148
+ ```
149
+
150
+ ## When nothing runs
151
+
152
+ A suite whose files do not match what the plugin looks for produces this, and
153
+ **exits `0`**:
154
+
155
+ ```text
156
+ -----> Running bash test suite
157
+ ```
158
+
159
+ No tests ran, and nothing said so. If a suite looks like it is being skipped,
160
+ work through these in order:
161
+
162
+ 1. **Is the plugin installed?** `busser plugin list` shows what is available.
163
+ Without the plugin, `busser test` has no runner for that directory.
164
+ 2. **Is the directory named after the plugin?** Tests for busser-bash live in
165
+ `bash/`, not `tests/` or `scripts/`.
166
+ 3. **Do the filenames match?** Each plugin globs for its own pattern -- for
167
+ example busser-bash takes `*_test.sh` and `*_spec.bash` but ignores
168
+ `mytest.sh`. Each plugin's README states its pattern.
169
+ 4. **Is `BUSSER_ROOT` what you think?** `busser suite path` prints where suites
170
+ are actually being looked for.
171
+
90
172
  ## Contributing
91
173
 
92
174
  Bug reports and pull requests are welcome. See
data/Rakefile CHANGED
@@ -9,7 +9,25 @@ Rake::TestTask.new(:unit) do |t|
9
9
  end
10
10
 
11
11
  Cucumber::Rake::Task.new(:features) do |t|
12
- t.cucumber_opts = ["features", "-x", "--format progress", "--no-color", "-b"]
12
+ t.cucumber_opts = ["features", "--format progress", "--fail-fast"]
13
+ end
14
+
15
+ # yard lives in the :development group, which CI omits when it runs the tests.
16
+ # Requiring it unconditionally would make `rake test` fail there, so the real
17
+ # task is only defined when yard is installed and a stub explains its absence
18
+ # otherwise. Nothing in CI gates on documentation.
19
+ begin
20
+ require "yard"
21
+
22
+ YARD::Rake::YardocTask.new(:doc) do |t|
23
+ t.files = ["lib/**/*.rb"]
24
+ t.options = ["--output-dir", "doc", "--markup", "markdown"]
25
+ end
26
+ rescue LoadError
27
+ desc "Generate YARD documentation (install the development group first)"
28
+ task :doc do
29
+ abort "yard is not available. Run `bundle install --with development` first."
30
+ end
13
31
  end
14
32
 
15
33
  desc "Run all test suites"
data/busser.gemspec CHANGED
@@ -7,25 +7,28 @@ Gem::Specification.new do |spec|
7
7
  spec.version = Busser::VERSION
8
8
  spec.authors = ["Fletcher Nichol"]
9
9
  spec.email = ["fnichol@nichol.ca"]
10
- spec.description = %q{Busser - Runs tests for projects in Test Kitchen}
10
+ spec.description = "Busser - Runs tests for projects in Test Kitchen"
11
11
  spec.summary = spec.description
12
12
  spec.homepage = "https://github.com/test-kitchen/busser"
13
13
  spec.license = "Apache-2.0"
14
14
 
15
- spec.files = `git ls-files`.split($/)
15
+ spec.required_ruby_version = ">= 3.2"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
16
18
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
19
  spec.require_paths = ["lib"]
18
20
 
19
- spec.required_ruby_version = ">= 3.2"
21
+ spec.metadata = {
22
+ "bug_tracker_uri" => "#{spec.homepage}/issues",
23
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
24
+ "documentation_uri" => "#{spec.homepage}/blob/main/README.md",
25
+ "homepage_uri" => spec.homepage,
26
+ "source_code_uri" => spec.homepage,
27
+ }
20
28
 
21
29
  # thor 1.1.0 references DidYouMean::SPELL_CHECKERS, removed in Ruby 3.1,
22
30
  # so the CLI crashed on startup on every supported Ruby
23
31
  spec.add_dependency "thor", ">= 1.1"
24
32
  # base64 is no longer a default gem; deserialize needs it at runtime
25
33
  spec.add_dependency "base64"
26
-
27
- spec.add_development_dependency "aruba", ">= 2.0"
28
- spec.add_development_dependency "minitest"
29
- spec.add_development_dependency "mocha"
30
- spec.add_development_dependency "rake"
31
34
  end
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # Installs this working tree into the Busser root the Test Kitchen verifier
3
+ # will use, so `kitchen verify` exercises the code on this branch rather than
4
+ # the last release from RubyGems.
5
+ #
6
+ # The verifier's install script skips its own install when `gem list` already
7
+ # shows what it wants, so putting things in place first is all that is needed.
8
+ # Two consequences to handle:
9
+ #
10
+ # * It also has to install busser itself. That check is `grep "^busser"` with
11
+ # no anchor at the end, so a plugin named busser-* satisfies it and busser
12
+ # would otherwise never be installed at all.
13
+ # * Skipping `busser plugin install` also skips the plugin's postinstall,
14
+ # which is where a plugin installs the test framework it drives. So run the
15
+ # postinstall here, which is what the verifier would have done.
16
+ set -euo pipefail
17
+
18
+ BUSSER_ROOT="${BUSSER_ROOT:-/tmp/busser-kitchen}"
19
+ gemspec="$(ls ./*.gemspec)"
20
+ gem_name="$(basename "${gemspec}" .gemspec)"
21
+
22
+ install_opts=(
23
+ --install-dir "${BUSSER_ROOT}/gems"
24
+ --bindir "${BUSSER_ROOT}/bin"
25
+ --no-document
26
+ )
27
+
28
+ mkdir -p "${BUSSER_ROOT}/gems"
29
+
30
+ gem build "${gemspec}" --output "${BUSSER_ROOT}/${gem_name}.gem" >/dev/null
31
+
32
+ if [ "${gem_name}" = "busser" ]; then
33
+ # No --local: a plugin's runtime dependencies are things its runner needs on
34
+ # the machine under test, and they have to be fetched into the Busser root
35
+ # like everything else.
36
+ gem install "${BUSSER_ROOT}/${gem_name}.gem" "${install_opts[@]}" >/dev/null
37
+ else
38
+ gem install busser "${install_opts[@]}" >/dev/null
39
+ # Not --ignore-dependencies: a plugin's runtime dependencies are things its
40
+ # runner needs on the machine under test, and they have to be in the Busser
41
+ # root like everything else.
42
+ # No --local: a plugin's runtime dependencies are things its runner needs on
43
+ # the machine under test, and they have to be fetched into the Busser root
44
+ # like everything else.
45
+ gem install "${BUSSER_ROOT}/${gem_name}.gem" "${install_opts[@]}" >/dev/null
46
+
47
+ # The same environment the verifier exports, so the framework this plugin
48
+ # installs lands in the Busser root rather than the ambient gem home.
49
+ BUSSER_ROOT="${BUSSER_ROOT}" \
50
+ GEM_HOME="${BUSSER_ROOT}/gems" \
51
+ GEM_PATH="${BUSSER_ROOT}/gems" \
52
+ "${BUSSER_ROOT}/bin/busser" plugin install "${gem_name}" --force-postinstall
53
+ fi
54
+
55
+ echo "pre-installed ${gem_name} from the working tree into ${BUSSER_ROOT}"
data/kitchen.yml ADDED
@@ -0,0 +1,27 @@
1
+ ---
2
+ # Runs Busser the way Test Kitchen actually runs it -- real verifier,
3
+ # real busser, real suite transfer -- but against the machine running the tests
4
+ # rather than a VM or container, so it works unchanged in CI.
5
+ driver:
6
+ name: exec
7
+
8
+ provisioner:
9
+ name: dummy
10
+
11
+ verifier:
12
+ name: busser
13
+ # The verifier defaults to Chef's omnibus Ruby, which is not what is running
14
+ # here, and to sudo, which is not needed for a path under the workspace.
15
+ ruby_bindir: <%= RbConfig::CONFIG["bindir"] %>
16
+ sudo: false
17
+ root_path: <%= ENV.fetch("BUSSER_ROOT", "/tmp/busser-kitchen") %>
18
+
19
+ platforms:
20
+ - name: local
21
+
22
+ suites:
23
+ # busser-bash is the exercise: a real plugin, installed by the verifier from
24
+ # RubyGems, driven by the busser built from this working tree. The internal
25
+ # dummy runner would prove the chain runs but can never fail, so it would not
26
+ # notice a regression.
27
+ - name: default
@@ -38,6 +38,10 @@ module Busser
38
38
 
39
39
  class_option :perms, desc: "Unix permissions on destination file"
40
40
 
41
+ # Writes a file that was streamed in over stdin, checking it arrived
42
+ # intact before applying its permissions.
43
+ #
44
+ # @return [void]
41
45
  def perform
42
46
  file = File.expand_path(options[:destination])
43
47
  contents = Base64.decode64(STDIN.read)
@@ -36,7 +36,25 @@ module Busser
36
36
  class_option :license, aliases: "-l", default: "apachev2",
37
37
  desc: "License type for gem (apachev2, mit, lgplv3, reserved)"
38
38
 
39
+ # Generates a new runner plugin project.
40
+ #
41
+ # @return [void]
42
+ # A plugin name becomes three things: a require path, a directory name and
43
+ # a Ruby constant. Only the last is fussy, and Thor's camel_case only
44
+ # folds underscores -- so "my-junit" produced `module My-junit`, which is
45
+ # not parseable Ruby. The generator ran to completion and wrote a project
46
+ # that could not be loaded at all.
47
+ NAME_PATTERN = /\A[a-z][a-z0-9_]*\z/
48
+
39
49
  def create
50
+ unless NAME_PATTERN.match?(name)
51
+ raise ::Thor::Error,
52
+ "'#{name}' is not a usable plugin name. A name becomes a Ruby " \
53
+ "constant, so it must start with a lowercase letter and contain " \
54
+ "only lowercase letters, digits and underscores. Try " \
55
+ "'#{suggested_name}'."
56
+ end
57
+
40
58
  self.class.source_root(Busser.source_root.join("templates", "plugin"))
41
59
 
42
60
  create_core_files
@@ -47,6 +65,9 @@ module Busser
47
65
 
48
66
  private
49
67
 
68
+ # Writes the gemspec, Gemfile, Rakefile, README and licence.
69
+ #
70
+ # @return [void]
50
71
  def create_core_files
51
72
  empty_directory(target_dir)
52
73
 
@@ -60,6 +81,9 @@ module Busser
60
81
  create_template("github_workflow.yml.erb", ".github/workflows/test.yml")
61
82
  end
62
83
 
84
+ # Writes lib/, the version file and the runner plugin class.
85
+ #
86
+ # @return [void]
63
87
  def create_source_files
64
88
  empty_directory(File.join(target_dir, "lib/busser", name))
65
89
  empty_directory(File.join(target_dir, "lib/busser/runner_plugin"))
@@ -74,6 +98,9 @@ module Busser
74
98
  )
75
99
  end
76
100
 
101
+ # Writes a starter cucumber suite for the new plugin.
102
+ #
103
+ # @return [void]
77
104
  def create_features_files
78
105
  empty_directory(File.join(target_dir, "features/support"))
79
106
 
@@ -95,6 +122,10 @@ module Busser
95
122
  )
96
123
  end
97
124
 
125
+ # Runs git init in the generated project, so the gemspec's
126
+ # `git ls-files` has something to read.
127
+ #
128
+ # @return [void]
98
129
  def initialize_git
99
130
  inside(target_dir) do
100
131
  run("git init")
@@ -102,14 +133,27 @@ module Busser
102
133
  end
103
134
  end
104
135
 
136
+ # @param erb [String] template path, relative to the templates directory
137
+ # @param dest [String] destination path, relative to the target directory
138
+ # @return [void]
105
139
  def create_template(erb, dest)
106
140
  template(erb, File.join(target_dir, dest), config)
107
141
  end
108
142
 
143
+ # @return [String] directory the new plugin is generated into
144
+ # @return [String] the given name reshaped into something usable, for the
145
+ # error message
146
+ def suggested_name
147
+ name.to_s.downcase.gsub(/[^a-z0-9_]+/, "_").sub(/\A[^a-z]+/, "").squeeze("_")
148
+ end
149
+
109
150
  def target_dir
110
151
  File.join(Dir.pwd, "busser-#{name}")
111
152
  end
112
153
 
154
+ # Values the templates interpolate.
155
+ #
156
+ # @return [Hash] the template binding
113
157
  def config
114
158
  @config ||= begin
115
159
  type_klass_name = "#{::Thor::Util.camel_case(options[:type])}Plugin"
@@ -132,16 +176,19 @@ module Busser
132
176
  end
133
177
  end
134
178
 
179
+ # @return [String] the author name, from git config where available
135
180
  def author
136
181
  git_user_name = `git config user.name`.chomp
137
182
  git_user_name.empty? ? "TODO: Write your name" : git_user_name
138
183
  end
139
184
 
185
+ # @return [String] the author email, from git config where available
140
186
  def email
141
187
  git_user_email = `git config user.email`.chomp
142
188
  git_user_email.empty? ? "TODO: Write your email" : git_user_email
143
189
  end
144
190
 
191
+ # @return [String] full licence text for the chosen licence
145
192
  def license_string
146
193
  case options[:license]
147
194
  when "mit" then "MIT"
@@ -168,6 +215,8 @@ module Busser
168
215
  end
169
216
  end
170
217
 
218
+ # @return [String] the filename the licence is written to, which differs
219
+ # by licence
171
220
  def license_filename
172
221
  case options[:license]
173
222
  when "mit" then "LICENSE.txt"
@@ -178,6 +227,7 @@ module Busser
178
227
  end
179
228
  end
180
229
 
230
+ # @return [String] the licence header comment prepended to source files
181
231
  def license_comment
182
232
  @license_comment ||= IO.read(File.join(target_dir, license_filename))
183
233
  .gsub(/^/, "# ").gsub(/\s+$/, "")
@@ -40,6 +40,9 @@ module Busser
40
40
  class_option :verbose, type: :boolean, default: false,
41
41
  desc: "Set a more verbose output"
42
42
 
43
+ # Installs each requested plugin.
44
+ #
45
+ # @return [void]
43
46
  def install_all
44
47
  if options[:verbose]
45
48
  Gem.configuration.verbose = 2 if options[:verbose]
@@ -53,6 +56,10 @@ module Busser
53
56
 
54
57
  private
55
58
 
59
+ # Installs one plugin and runs its postinstall.
60
+ #
61
+ # @param plugin [String] gem name, optionally suffixed with @version
62
+ # @return [void]
56
63
  def install(plugin)
57
64
  gem_name, version = plugin.split("@")
58
65
  name = gem_name.sub(/^busser-/, "")
@@ -65,6 +72,12 @@ module Busser
65
72
  end
66
73
  end
67
74
 
75
+ # Installs the plugin gem unless it is already available.
76
+ #
77
+ # @param gem [String] the gem name
78
+ # @param version [String, nil] a version requirement, or nil for any
79
+ # @param name [String] short plugin name, for the message
80
+ # @return [Boolean] true if the gem was newly installed
68
81
  def install_plugin_gem(gem, version, name)
69
82
  if internal_plugin?(name) || gem_installed?(gem, version)
70
83
  info "Plugin #{name} already installed"
@@ -76,10 +89,17 @@ module Busser
76
89
  end
77
90
  end
78
91
 
92
+ # @param name [String] short plugin name
93
+ # @return [void]
79
94
  def load_plugin(name)
80
95
  Busser::Plugin.require!(Busser::Plugin.runner_plugin(name))
81
96
  end
82
97
 
98
+ # Runs the plugin's postinstall block, which is where a plugin installs
99
+ # the test framework it drives.
100
+ #
101
+ # @param name [String] short plugin name
102
+ # @return [void]
83
103
  def run_postinstall(name)
84
104
  klass = Busser::Plugin.runner_class(::Thor::Util.camel_case(name))
85
105
  if klass.respond_to?(:run_postinstall)
@@ -100,13 +120,29 @@ module Busser
100
120
  #
101
121
  # Please use with extreme caution.
102
122
  #
123
+ # The restore is in an ensure block. Without one, a postinstall that
124
+ # raised left VERIFY_PEER set to VERIFY_NONE for the rest of the
125
+ # process -- and `busser plugin install` takes a list, so every gem
126
+ # downloaded for every later plugin in that same run would have had its
127
+ # certificate unchecked.
128
+ #
129
+ # @yield the block to run with peer verification dropped
130
+ # @return [void]
103
131
  def drop_ssl_verify_peer
104
132
  before = OpenSSL::SSL::VERIFY_PEER
133
+ set_ssl_verify_peer(OpenSSL::SSL::VERIFY_NONE)
134
+ begin
135
+ yield
136
+ ensure
137
+ set_ssl_verify_peer(before)
138
+ end
139
+ end
140
+
141
+ # @param value [Integer] the OpenSSL verify mode to install
142
+ # @return [void]
143
+ def set_ssl_verify_peer(value)
105
144
  OpenSSL::SSL.send(:remove_const, "VERIFY_PEER")
106
- OpenSSL::SSL.const_set("VERIFY_PEER", OpenSSL::SSL::VERIFY_NONE)
107
- yield
108
- OpenSSL::SSL.send(:remove_const, "VERIFY_PEER")
109
- OpenSSL::SSL.const_set("VERIFY_PEER", before)
145
+ OpenSSL::SSL.const_set("VERIFY_PEER", value)
110
146
  end
111
147
  end
112
148
  end
@@ -28,6 +28,9 @@ module Busser
28
28
  #
29
29
  class PluginList < Busser::Thor::BaseGroup
30
30
 
31
+ # Prints the installed plugins and their versions.
32
+ #
33
+ # @return [void]
31
34
  def list
32
35
  if plugin_data.empty?
33
36
  say "No plugins installed yet"
@@ -38,11 +41,20 @@ module Busser
38
41
 
39
42
  private
40
43
 
44
+ # The dummy runner is a fixture shipped inside busser for its own tests,
45
+ # not something anyone installed. `busser test` already passes over it,
46
+ # so listing it as an installed plugin was inconsistent as well as
47
+ # confusing.
48
+ #
49
+ # @return [Array<Array(String, String)>] each installed plugin's short
50
+ # name and version
41
51
  def plugin_data
42
- @plugin_data ||= Busser::Plugin.runner_plugins.map do |path|
43
- spec = Busser::Plugin.gem_from_path(path)
44
- [File.basename(path), (spec && spec.version)]
45
- end
52
+ @plugin_data ||= Busser::Plugin.runner_plugins
53
+ .reject { |path| File.basename(path) == "dummy" }
54
+ .map do |path|
55
+ spec = Busser::Plugin.gem_from_path(path)
56
+ [File.basename(path), (spec && spec.version)]
57
+ end
46
58
  end
47
59
  end
48
60
  end