Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Use provided stdout/stderr streams #838

Merged
merged 3 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 36 additions & 17 deletions lib/flipper/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ def self.run(argv = ARGV)
# Path to the local Rails application's environment configuration.
DEFAULT_REQUIRE = "./config/environment"

def initialize
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
@program_name = 'flipper'
@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|
command action do |c|
c.banner = "Usage: #{c.program_name} [options] <feature>"
Expand All @@ -40,10 +46,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
Expand All @@ -57,29 +65,29 @@ def initialize
values.each { |value| f.send(action, value) }
end

puts feature_details(f)
ui.info feature_details(f)
end
end
end

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

Expand All @@ -89,7 +97,7 @@ def initialize

# Options available on all commands
on_tail('-h', '--help', 'Print help message') do
puts help
ui.info help
exit
end

Expand All @@ -114,15 +122,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

Expand All @@ -138,7 +146,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

Expand Down Expand Up @@ -170,7 +178,7 @@ def feature_summary(features)
end

colorize("%-#{padding}s" % feature.key, [:BOLD, :WHITE]) + " is #{summary}"
end
end.join("\n")
end

def feature_details(feature)
Expand Down Expand Up @@ -210,17 +218,28 @@ 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 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

Expand Down
67 changes: 21 additions & 46 deletions spec/flipper/cli_spec.rb
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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