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
@@ -0,0 +1,175 @@
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 "tmpdir"
18
+
19
+ require "busser/command/plugin_create"
20
+
21
+ # The generated project is what a plugin author starts from, so the checks
22
+ # worth having are the ones a new project would trip over on day one.
23
+ describe Busser::Command::PluginCreate do
24
+
25
+ before do
26
+ @tmpdir = Dir.mktmpdir("busser-plugin-create")
27
+ @pwd = Dir.pwd
28
+ Dir.chdir(@tmpdir)
29
+ end
30
+
31
+ after do
32
+ Dir.chdir(@pwd)
33
+ FileUtils.rm_rf(@tmpdir)
34
+ end
35
+
36
+ describe "the generated gemspec" do
37
+
38
+ it "loads" do
39
+ generate
40
+
41
+ _(gemspec.name).must_equal "busser-demo"
42
+ end
43
+
44
+ # RubyGems validates spec.license against the SPDX list and warns on
45
+ # anything else. The generator used to emit "Apache 2.0", the human
46
+ # readable name rather than the identifier.
47
+ it "declares a license RubyGems recognises" do
48
+ generate
49
+
50
+ _(Gem::Licenses.match?(gemspec.license)).must_equal true
51
+ end
52
+
53
+ {
54
+ "apachev2" => "Apache-2.0",
55
+ "mit" => "MIT",
56
+ "lgplv3" => "LGPL-3.0-only",
57
+ }.each do |option, identifier|
58
+ it "maps --license #{option} to #{identifier}" do
59
+ generate(license: option)
60
+
61
+ _(gemspec.license).must_equal identifier
62
+ end
63
+ end
64
+
65
+ # "All rights reserved" has no SPDX identifier, so declaring nothing is
66
+ # the only correct option.
67
+ it "declares no license for an all rights reserved plugin" do
68
+ generate(license: "reserved")
69
+
70
+ _(gemspec.license).must_be_nil
71
+ end
72
+
73
+ # bundler ~> 1.3 cannot be satisfied by any bundler in use today, so a
74
+ # freshly generated plugin failed to resolve before running anything.
75
+ it "does not constrain bundler" do
76
+ generate
77
+
78
+ _(dev_dependencies).wont_include "bundler"
79
+ end
80
+
81
+ # cane, tailor and countloc have been unmaintained for about a decade.
82
+ %w{cane tailor countloc}.each do |gem_name|
83
+ it "does not depend on #{gem_name}" do
84
+ generate
85
+
86
+ _(dev_dependencies).wont_include gem_name
87
+ end
88
+ end
89
+
90
+ it "depends on what the generated Rakefile needs" do
91
+ generate
92
+
93
+ _(dev_dependencies).must_include "rake"
94
+ _(dev_dependencies).must_include "cucumber"
95
+ _(dev_dependencies).must_include "aruba"
96
+ end
97
+
98
+ it "states a supported Ruby version" do
99
+ generate
100
+
101
+ requirement = gemspec.required_ruby_version
102
+
103
+ _(requirement.satisfied_by?(Gem::Version.new("3.2.0"))).must_equal true
104
+ _(requirement.satisfied_by?(Gem::Version.new("2.7.0"))).must_equal false
105
+ end
106
+
107
+ def gemspec
108
+ Gem::Specification.load(File.join(plugin_dir, "busser-demo.gemspec"))
109
+ end
110
+
111
+ def dev_dependencies
112
+ gemspec.development_dependencies.map(&:name)
113
+ end
114
+ end
115
+
116
+ describe "the generated project" do
117
+
118
+ it "ships a CI workflow rather than a Travis config" do
119
+ generate
120
+
121
+ _(File.exist?(File.join(plugin_dir, ".github/workflows/test.yml")))
122
+ .must_equal true
123
+ _(File.exist?(File.join(plugin_dir, ".travis.yml"))).must_equal false
124
+ end
125
+
126
+ # Both linters are long dead, and the Rakefile's default task depended on
127
+ # them, so `rake` failed on a freshly generated plugin.
128
+ it "ships no dead linter configuration" do
129
+ generate
130
+
131
+ _(File.exist?(File.join(plugin_dir, ".tailor"))).must_equal false
132
+ _(File.exist?(File.join(plugin_dir, ".cane"))).must_equal false
133
+ end
134
+
135
+ it "has a Rakefile that only references tasks it defines" do
136
+ generate
137
+
138
+ rakefile = File.read(File.join(plugin_dir, "Rakefile"))
139
+
140
+ _(rakefile).wont_match(/cane|tailor|countloc/)
141
+ _(rakefile).must_match(/Cucumber::Rake::Task/)
142
+ end
143
+
144
+ # aruba 1.0 dropped @aruba_timeout_seconds, and setting it on aruba 2 is
145
+ # silently ignored, so the generated timeout did nothing at all.
146
+ it "configures aruba through the supported API" do
147
+ generate
148
+
149
+ env = File.read(File.join(plugin_dir, "features/support/env.rb"))
150
+
151
+ _(env).wont_match(/@aruba_timeout_seconds/)
152
+ _(env).must_match(/Aruba\.configure/)
153
+ end
154
+ end
155
+
156
+ def generate(license: "apachev2")
157
+ command = Busser::Command::PluginCreate.new(
158
+ ["demo"], { "type" => "runner", "license" => license }
159
+ )
160
+ command.stubs(:initialize_git)
161
+ capture_stdout { command.create }
162
+ end
163
+
164
+ def plugin_dir
165
+ File.join(@tmpdir, "busser-demo")
166
+ end
167
+
168
+ def capture_stdout
169
+ original = $stdout
170
+ $stdout = StringIO.new
171
+ yield
172
+ ensure
173
+ $stdout = original
174
+ end
175
+ end
@@ -0,0 +1,121 @@
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 "tmpdir"
18
+
19
+ require "busser/command/setup"
20
+
21
+ # The binstub exists to give Busser an isolated gem environment on the machine
22
+ # under test, so what it does to the environment before exec is the contract
23
+ # worth pinning down.
24
+ describe Busser::Command::Setup do
25
+
26
+ # Anything left set here lets the calling Ruby environment reach into
27
+ # Busser's. RUBYOPT was once the only way in; RubyGems now requires
28
+ # bundler/setup whenever BUNDLER_SETUP is present, and bundler then resets
29
+ # GEM_HOME to the calling project's bundle.
30
+ LEAKY_VARIABLES = %w{
31
+ RUBYOPT GEMRC RUBYLIB
32
+ BUNDLER_SETUP BUNDLER_VERSION
33
+ BUNDLE_BIN_PATH BUNDLE_GEMFILE BUNDLE_LOCKFILE BUNDLE_PATH
34
+ }.freeze
35
+
36
+ before do
37
+ @tmpdir = Dir.mktmpdir("busser-setup")
38
+ @original_root = ENV["BUSSER_ROOT"]
39
+ ENV["BUSSER_ROOT"] = @tmpdir
40
+ end
41
+
42
+ after do
43
+ ENV["BUSSER_ROOT"] = @original_root
44
+ FileUtils.rm_rf(@tmpdir)
45
+ end
46
+
47
+ describe "the bourne binstub" do
48
+
49
+ it "is created and executable" do
50
+ run_setup
51
+
52
+ _(File.exist?(binstub)).must_equal true
53
+ _(File.stat(binstub).mode & 0o111).wont_equal 0
54
+ end
55
+
56
+ it "pins BUSSER_ROOT and the gem paths" do
57
+ run_setup
58
+
59
+ _(binstub_body).must_match(/BUSSER_ROOT="#{Regexp.escape(@tmpdir)}"/)
60
+ _(binstub_body).must_match(/^GEM_HOME=/)
61
+ _(binstub_body).must_match(/^GEM_PATH=/)
62
+ end
63
+
64
+ LEAKY_VARIABLES.each do |name|
65
+ it "unsets #{name} before exec" do
66
+ run_setup
67
+
68
+ _(unset_variables).must_include name
69
+ end
70
+ end
71
+
72
+ it "unsets everything before handing off to ruby" do
73
+ run_setup
74
+
75
+ unset_line = binstub_body.index("unset ")
76
+ exec_line = binstub_body.index("exec ")
77
+
78
+ _(unset_line).must_be :<, exec_line
79
+ end
80
+
81
+ def binstub
82
+ File.join(@tmpdir, "bin", "busser")
83
+ end
84
+
85
+ def unset_variables
86
+ binstub_body.scan(/^unset (.+)$/).flatten.join(" ").split(/\s+/)
87
+ end
88
+ end
89
+
90
+ describe "the bat binstub" do
91
+
92
+ LEAKY_VARIABLES.each do |name|
93
+ it "clears #{name} before exec" do
94
+ run_setup(type: "bat")
95
+
96
+ _(binstub_body).must_match(/^SET #{name}=\s*$/)
97
+ end
98
+ end
99
+
100
+ def binstub
101
+ File.join(@tmpdir, "bin", "busser.bat")
102
+ end
103
+ end
104
+
105
+ def run_setup(type: "bourne")
106
+ command = Busser::Command::Setup.new([], { "type" => type })
107
+ capture_stdout { command.perform }
108
+ end
109
+
110
+ def binstub_body
111
+ File.read(binstub)
112
+ end
113
+
114
+ def capture_stdout
115
+ original = $stdout
116
+ $stdout = StringIO.new
117
+ yield
118
+ ensure
119
+ $stdout = original
120
+ end
121
+ 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