Files
ubicloud/spec/lib/rodish_spec.rb
Jeremy Evans c2303cbe48 Refactor Rodish
Make Rodish::Processor a module, and make Rodish.processor take a
module/class that is extended with Rodish::Processor.
Rodish::Processor#process no longer takes a context keyword
argument, and instead creates a new instance of the class it is
included using the remaining arguments.

This allows for more intuitive behavior.  The before and run
blocks inside the "on" blocks are now in instance scope of the
receiver of the "on" method.
2025-02-19 10:25:42 -08:00

375 lines
13 KiB
Ruby

# frozen_string_literal: true
RSpec.describe Rodish do
let(:base_app) do
c = Class.new(Array)
described_class.processor(c) do
options "example [options] [subcommand [subcommand_options] [...]]" do
on("-v", "top verbose output")
on("--version", "show program version") { halt "0.0.0" }
on("--help", "show program help") { halt to_s }
end
# Rubocop incorrectly believes these before blocks are related to rspec examples,
# when they are rodish hooks. Rubocop's auto correction breaks this Rodish processor
# unless this cop is disabled.
# rubocop:disable RSpec/ScatteredSetup
before do
push :top
end
on "a" do
before do
push :before_a
end
options "example a [options] [subcommand [subcommand_options] [...]]", key: :a do
on("-v", "a verbose output")
end
on "b" do
before do
push :before_b
end
options "example a b [options] arg [...]", key: :b do
on("-v", "b verbose output")
end
args(1...)
run do |args, opts|
push [:b, args, opts]
end
end
args 2, invalid_args_message: "accepts: x y"
run do |x, y|
push [:a, x, y]
end
end
is "c" do
push :c
end
is "d", args: 1 do |d|
push [:d, d]
end
on "e" do
on "f" do
run do
push :f
end
end
end
on "g" do
post_options "example g arg [options] [subcommand [subcommand_options] [...]]", key: :g do
on("-v", "g verbose output")
on("-k", "--key=foo", "set key")
end
args(2...)
is "j" do
push :j
end
run_is "h" do |opts|
push [:h, opts.dig(:g, :v), opts.dig(:g, :key)]
end
run_on "i" do
before do |_, opts|
push opts.dig(:g, :v)
push opts.dig(:g, :key)
end
# rubocop:enable RSpec/ScatteredSetup
is "k" do
push :k
end
run do
push :i
end
end
run do |(x, *argv), opts, command|
push [:g, x]
command.run(self, opts, argv)
end
end
on "l" do
skip_option_parsing "example l"
args(0...)
run do |argv|
push [:l, argv]
end
end
run do
push :empty
end
end
c
end
[true, false].each do |frozen|
context "when #{"not " unless frozen}frozen" do
let(:app) do
app = base_app
app.freeze if frozen
app
end
it "executes expected command code in expected order" do
expect(app.process([])).to eq [:top, :empty]
expect(app.process(%w[a b 1])).to eq [:top, :before_a, :before_b, [:b, %w[1], {a: {}, b: {}}]]
expect(app.process(%w[a b 1 2])).to eq [:top, :before_a, :before_b, [:b, %w[1 2], {a: {}, b: {}}]]
expect(app.process(%w[a 3 4])).to eq [:top, :before_a, [:a, "3", "4"]]
expect(app.process(%w[c])).to eq [:top, :c]
expect(app.process(%w[d 5])).to eq [:top, [:d, "5"]]
expect(app.process(%w[e f])).to eq [:top, :f]
end
it "supports run_on/run_is for subcommands dispatched to during run" do
expect(app.process(%w[g j])).to eq [:top, :j]
expect(app.process(%w[g 1 h])).to eq [:top, [:g, "1"], [:h, nil, nil]]
expect(app.process(%w[g 1 i])).to eq [:top, [:g, "1"], nil, nil, :i]
expect(app.process(%w[g 1 i k])).to eq [:top, [:g, "1"], nil, nil, :k]
end
it "supports post options for commands, parsed before subcommand dispatching" do
expect(app.process(%w[g 1 -v h])).to eq [:top, [:g, "1"], [:h, true, nil]]
expect(app.process(%w[g 1 -v i])).to eq [:top, [:g, "1"], true, nil, :i]
expect(app.process(%w[g 1 -v i k])).to eq [:top, [:g, "1"], true, nil, :k]
expect(app.process(%w[g 1 -k 2 h])).to eq [:top, [:g, "1"], [:h, nil, "2"]]
expect(app.process(%w[g 1 -k 2 i])).to eq [:top, [:g, "1"], nil, "2", :i]
expect(app.process(%w[g 1 -k 2 i k])).to eq [:top, [:g, "1"], nil, "2", :k]
end
it "supports skipping option parsing" do
expect(app.process(%w[l -A 1 b])).to eq [:top, [:l, %w[-A 1 b]]]
end
it "handles invalid subcommands dispatched to during run" do
expect { app.process(%w[g 1 l]) }.to raise_error(Rodish::CommandFailure, "invalid post subcommand: l")
end
it "handles options at any level they are defined" do
expect(app.process(%w[-v a b -v 1 2])).to eq [:top, :before_a, :before_b, [:b, %w[1 2], {a: {}, b: {v: true}, v: true}]]
expect(app.process(%w[a -v b 1 2])).to eq [:top, :before_a, :before_b, [:b, %w[1 2], {a: {v: true}, b: {}}]]
end
it "raises CommandFailure when there a command has no command block or subcommands" do
app = described_class.processor(Class.new) {}
expect { app.process([]) }.to raise_error(Rodish::CommandFailure, "program bug, no run block or subcommands defined for command")
app = described_class.processor(Class.new) do
on("f") {}
end
expect { app.process(%w[f]) }.to raise_error(Rodish::CommandFailure, "program bug, no run block or subcommands defined for f subcommand")
end
it "raises CommandFailure for unexpected post options" do
expect { app.process(%w[g 1 -b h]) }.to raise_error(Rodish::CommandFailure, "invalid option: -b")
end
it "raises CommandExit for blocks that use halt" do
expect { app.process(%w[--version]) }.to raise_error(Rodish::CommandExit, "0.0.0")
expect { app.process(%w[--help]) }.to raise_error(Rodish::CommandExit, <<~USAGE)
Usage: example [options] [subcommand [subcommand_options] [...]]
Options:
-v top verbose output
--version show program version
--help show program help
Subcommands: a c d e g l
USAGE
end
it "has options_text return nil if there are no option parsers for the command" do
expect(app.command.subcommand("d").options_text).to be_nil
end
it "can get usages for all options" do
usages = app.usages
expect(usages.length).to eq 5
expect(usages[""]).to eq <<~USAGE
Usage: example [options] [subcommand [subcommand_options] [...]]
Options:
-v top verbose output
--version show program version
--help show program help
Subcommands: a c d e g l
USAGE
expect(usages["a"]).to eq <<~USAGE
Usage: example a [options] [subcommand [subcommand_options] [...]]
Options:
-v a verbose output
Subcommands: b
USAGE
expect(usages["a b"]).to eq <<~USAGE
Usage: example a b [options] arg [...]
Options:
-v b verbose output
USAGE
expect(usages["g"]).to eq <<~USAGE
Usage: example g arg [options] [subcommand [subcommand_options] [...]]
Options:
-v g verbose output
-k, --key=foo set key
Subcommands: h i
USAGE
expect(usages["l"]).to eq "Usage: example l\n"
end
next if frozen
describe "failure handling" do
let(:res) { [] }
before do
res = self.res
app.define_singleton_method(:new) { res.clear }
end
it "raises CommandFailure for unexpected number of arguments without executing code" do
expect { app.process(%w[6]) }.to raise_error(Rodish::CommandFailure, "invalid number of arguments for command (accepts: 0, given: 1)")
expect(res).to be_empty
expect { app.process(%w[a b]) }.to raise_error(Rodish::CommandFailure, "invalid number of arguments for a b subcommand (accepts: 1..., given: 0)")
expect(res).to eq [:top, :before_a]
expect { app.process(%w[a]) }.to raise_error(Rodish::CommandFailure, "invalid arguments for a subcommand (accepts: x y)")
expect(res).to eq [:top]
expect { app.process(%w[a 1]) }.to raise_error(Rodish::CommandFailure, "invalid arguments for a subcommand (accepts: x y)")
expect(res).to eq [:top]
expect { app.process(%w[a 1 2 3]) }.to raise_error(Rodish::CommandFailure, "invalid arguments for a subcommand (accepts: x y)")
expect(res).to eq [:top]
expect { app.process(%w[c 1]) }.to raise_error(Rodish::CommandFailure, "invalid number of arguments for c subcommand (accepts: 0, given: 1)")
expect(res).to eq [:top]
expect { app.process(%w[d]) }.to raise_error(Rodish::CommandFailure, "invalid number of arguments for d subcommand (accepts: 1, given: 0)")
expect(res).to eq [:top]
expect { app.process(%w[d 1 2]) }.to raise_error(Rodish::CommandFailure, "invalid number of arguments for d subcommand (accepts: 1, given: 2)")
expect(res).to eq [:top]
end
it "raises CommandFailure for missing subcommand" do
expect { app.process(%w[e]) }.to raise_error(Rodish::CommandFailure, "no subcommand provided")
expect(res).to eq [:top]
end
it "raises CommandFailure for invalid subcommand" do
expect { app.process(%w[e g]) }.to raise_error(Rodish::CommandFailure, "invalid subcommand: g")
expect(res).to eq [:top]
app = described_class.processor(Class.new) do
on("f") {}
end
expect { app.process(%w[g]) }.to raise_error(Rodish::CommandFailure, "invalid subcommand: g")
end
it "raises CommandFailure for unexpected options" do
expect { app.process(%w[-d]) }.to raise_error(Rodish::CommandFailure, "invalid option: -d")
expect(res).to be_empty
expect { app.process(%w[a -d]) }.to raise_error(Rodish::CommandFailure, "invalid option: -d")
expect(res).to eq [:top]
expect { app.process(%w[a b -d]) }.to raise_error(Rodish::CommandFailure, "invalid option: -d")
expect(res).to eq [:top, :before_a]
expect { app.process(%w[d -d 1 2]) }.to raise_error(Rodish::CommandFailure, "invalid option: -d")
expect(res).to eq [:top]
end
it "supports adding subcommands after initialization" do
expect { app.process(%w[z]) }.to raise_error(Rodish::CommandFailure, "invalid number of arguments for command (accepts: 0, given: 1)")
expect(res).to be_empty
app.on("z") do
args 1
run do |arg|
push [:z, arg]
end
end
app.process(%w[z h])
expect(res).to eq [:top, [:z, "h"]]
app.on("z", "y") do
run do
push :y
end
end
app.process(%w[z y])
expect(res).to eq [:top, :y]
app.is("z", "y", "x", args: 1) do |arg|
push [:x, arg]
end
app.process(%w[z y x j])
expect(res).to eq [:top, [:x, "j"]]
end
it "supports autoloading" do
main = TOPLEVEL_BINDING.receiver
main.instance_variable_set(:@ExampleRodish, app)
app.on("k") do
autoload_subcommand_dir("spec/lib/rodish-example")
autoload_post_subcommand_dir("spec/lib/rodish-example-post")
args(2...)
run do |(x, *argv), opts, command|
push [:k, x]
command.run(self, opts, argv)
end
end
app.process(%w[k m])
expect(res).to eq [:top, :m]
app.process(%w[k 1 o])
expect(res).to eq [:top, [:k, "1"], :o]
expect { app.process(%w[k n]) }.to raise_error(Rodish::CommandFailure, "program bug, autoload of subcommand n failed")
ensure
main.remove_instance_variable(:@ExampleRodish)
end
end
it "uses separate lines for more than 6 subcommands" do
subcommands = %w[a b c d e f g]
app.on("z") do
options "example z subcommand"
subcommands.each do |cmd|
is(cmd) {}
end
end
expect(app.command.subcommand("z").options_text).to eq <<~USAGE
Usage: example z subcommand
Subcommands:
a
b
c
d
e
f
g
USAGE
end
end
end
end