From b586c7ff11d8cc70e4387f7372aa943c574f5eb2 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Fri, 9 Feb 2024 10:19:00 -0500 Subject: [PATCH 1/3] Use Bundler::UI for output, allow redirecting stdout, stderr streams --- lib/flipper/cli.rb | 51 ++++++++++++++++++++---------- spec/flipper/cli_spec.rb | 67 +++++++++++++--------------------------- 2 files changed, 56 insertions(+), 62 deletions(-) diff --git a/lib/flipper/cli.rb b/lib/flipper/cli.rb index ba1938098..2a7ba8870 100644 --- a/lib/flipper/cli.rb +++ b/lib/flipper/cli.rb @@ -9,7 +9,7 @@ def self.run(argv = ARGV) # Path to the local Rails application's environment configuration. DEFAULT_REQUIRE = "./config/environment" - def initialize + def initialize(stdout: $stdout, stderr: $stderr) super # Program is always flipper, no matter how it's invoked @@ -17,6 +17,8 @@ def initialize @require = ENV.fetch("FLIPPER_REQUIRE", DEFAULT_REQUIRE) @commands = {} + shell.redirect(stdout: stdout, stderr: stderr) + %w[enable disable].each do |action| command action do |c| c.banner = "Usage: #{c.program_name} [options] " @@ -40,10 +42,12 @@ def initialize begin values << Flipper::Expression.build(JSON.parse(expression)) rescue JSON::ParserError => e - warn "JSON parse error: #{e.message}" + ui.error "JSON parse error #{e.message}" + ui.trace(e) exit 1 rescue ArgumentError => e - warn "Invalid expression: #{e.message}" + ui.error "Invalid expression: #{e.message}" + ui.trace(e) exit 1 end end @@ -57,7 +61,7 @@ def initialize values.each { |value| f.send(action, value) } end - puts feature_details(f) + ui.info feature_details(f) end end end @@ -65,21 +69,21 @@ def initialize command 'list' do |c| c.description = "List defined features" c.action do - puts feature_summary(Flipper.features) + ui.info feature_summary(Flipper.features) end end command 'show' do |c| c.description = "Show a defined feature" c.action do |feature| - puts feature_details(Flipper.feature(feature)) + ui.info feature_details(Flipper.feature(feature)) end end command 'help' do |c| c.load_environment = false c.action do |command = nil| - puts command ? @commands[command].help : help + ui.info command ? @commands[command].help : help end end @@ -89,7 +93,7 @@ def initialize # Options available on all commands on_tail('-h', '--help', 'Print help message') do - puts help + ui.info help exit end @@ -114,15 +118,15 @@ def run(argv) load_environment! if @commands[command].load_environment @commands[command].run(args) else - puts help + ui.info help if command - warn "Unknown command: #{command}" + ui.error "Unknown command: #{command}" exit 1 end end rescue OptionParser::InvalidOption => e - warn e.message + ui.error e.message exit 1 end @@ -138,7 +142,7 @@ def load_environment! # Ensure all of flipper gets loaded if it hasn't already. require 'flipper' rescue LoadError => e - warn e.message + ui.error e.message exit 1 end @@ -210,17 +214,32 @@ def pluralize(count, singular, plural) end def colorize(text, colors) - if defined?(Bundler) - Bundler.ui.add_color(text, *colors) - else - text + ui.add_color(text, *colors) + end + + def ui + @ui ||= Bundler::UI::Shell.new.tap do |ui| + ui.shell = shell end end + def shell + @shell ||= Bundler::Thor::Base.shell.new.extend(ShellOutput) + end + def indent(text, spaces) text.gsub(/^/, " " * spaces) end + # Redirect the shell's output to the given stdout and stderr streams + module ShellOutput + attr_reader :stdout, :stderr + + def redirect(stdout: $stdout, stderr: $stderr) + @stdout, @stderr = stdout, stderr + end + end + class Command < OptionParser attr_accessor :description, :load_environment diff --git a/spec/flipper/cli_spec.rb b/spec/flipper/cli_spec.rb index 6fbb8caa3..7be999b61 100644 --- a/spec/flipper/cli_spec.rb +++ b/spec/flipper/cli_spec.rb @@ -1,13 +1,33 @@ require "flipper/cli" RSpec.describe Flipper::CLI do + let(:stdout) { StringIO.new } + let(:stderr) { StringIO.new } + let(:cli) { Flipper::CLI.new(stdout: stdout, stderr: stderr) } + + before do + # Prentend stdout/stderr a TTY to test colorization + allow(stdout).to receive(:tty?).and_return(true) + allow(stderr).to receive(:tty?).and_return(true) + end + # Infer the command from the description subject(:argv) do descriptions = self.class.parent_groups.map {|g| g.metadata[:description_args] }.reverse.flatten.drop(1) descriptions.map { |arg| Shellwords.split(arg) }.flatten end - subject { run argv } + subject do + status = 0 + + begin + cli.run(argv) + rescue SystemExit => e + status = e.status + end + + OpenStruct.new(status: status, stdout: stdout.string, stderr: stderr.string) + end before do ENV["FLIPPER_REQUIRE"] = "./spec/fixtures/environment" @@ -141,49 +161,4 @@ it { should have_attributes(status: 0, stdout: /enabled.*admins/m) } end end - - context "bundler is not installed" do - let(:argv) { "list" } - - around do |example| - original_bundler = Bundler - begin - Object.send(:remove_const, :Bundler) - example.run - ensure - Object.const_set(:Bundler, original_bundler) - end - end - - it "should not raise an error" do - Flipper.enable(:enabled_feature) - Flipper.enable_group(:enabled_groups, :admins) - Flipper.add(:disabled_feature) - - expect(subject).to have_attributes(status: 0, stdout: /enabled_feature.*enabled_groups.*disabled_feature/m) - end - end - - def run(argv) - original_stdout = $stdout - original_stderr = $stderr - - $stdout = StringIO.new - $stderr = StringIO.new - status = 0 - - # Prentend this a TTY so we can test colorization - allow($stdout).to receive(:tty?).and_return(true) - - begin - Flipper::CLI.run(argv) - rescue SystemExit => e - status = e.status - end - - OpenStruct.new(status: status, stdout: $stdout.string, stderr: $stderr.string) - ensure - $stdout = original_stdout - $stderr = original_stderr - end end From b74df9edbb1c98ed951fe20b41d2b0c755dd1dfb Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Fri, 9 Feb 2024 10:41:42 -0500 Subject: [PATCH 2/3] Join feature summary on newline --- lib/flipper/cli.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/flipper/cli.rb b/lib/flipper/cli.rb index 2a7ba8870..282abea7a 100644 --- a/lib/flipper/cli.rb +++ b/lib/flipper/cli.rb @@ -174,7 +174,7 @@ def feature_summary(features) end colorize("%-#{padding}s" % feature.key, [:BOLD, :WHITE]) + " is #{summary}" - end + end.join("\n") end def feature_details(feature) From ef9a631858c582b53b6d0ab7bcc557e0c477a685 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Fri, 9 Feb 2024 11:52:25 -0500 Subject: [PATCH 3/3] Initialize with a shell --- lib/flipper/cli.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/flipper/cli.rb b/lib/flipper/cli.rb index 282abea7a..c27520ccf 100644 --- a/lib/flipper/cli.rb +++ b/lib/flipper/cli.rb @@ -9,7 +9,9 @@ def self.run(argv = ARGV) # Path to the local Rails application's environment configuration. DEFAULT_REQUIRE = "./config/environment" - def initialize(stdout: $stdout, stderr: $stderr) + attr_accessor :shell + + def initialize(stdout: $stdout, stderr: $stderr, shell: Bundler::Thor::Base.shell.new) super # Program is always flipper, no matter how it's invoked @@ -17,6 +19,8 @@ def initialize(stdout: $stdout, stderr: $stderr) @require = ENV.fetch("FLIPPER_REQUIRE", DEFAULT_REQUIRE) @commands = {} + # Extend whatever shell to support output redirection + @shell = shell.extend(ShellOutput) shell.redirect(stdout: stdout, stderr: stderr) %w[enable disable].each do |action| @@ -223,10 +227,6 @@ def ui end end - def shell - @shell ||= Bundler::Thor::Base.shell.new.extend(ShellOutput) - end - def indent(text, spaces) text.gsub(/^/, " " * spaces) end