be more colorful - close #41

This commit is contained in:
Josh Holtrop 2017-07-11 13:52:01 -04:00
parent 775363ddbd
commit 65c1bac4f9
7 changed files with 173 additions and 50 deletions

View File

@ -1,3 +1,4 @@
require_relative "rscons/ansi"
require_relative "rscons/build_target"
require_relative "rscons/builder"
require_relative "rscons/cache"
@ -50,6 +51,10 @@ module Rscons
# The number of threads to use when scheduling subprocesses.
attr_accessor :n_threads
# @return [Boolean]
# Whether to output ANSI color escape sequences.
attr_accessor :do_ansi_color
# Remove all generated files.
#
# @return [void]

52
lib/rscons/ansi.rb Normal file
View File

@ -0,0 +1,52 @@
module Rscons
module Ansi
class << self
# Write a message to an IO with ANSI escape codes.
#
# @param io [IO]
# The IO to write to.
# @param message [Array<String, Symbol>]
# Strings to be printed, with Symbols representing ANSI escape codes.
#
# @return [void]
def write(io, *message)
do_color = Rscons.do_ansi_color
if do_color.nil?
do_color = do_ansi?(io)
end
out = ""
message.each do |m|
if m.is_a?(String)
out += m
elsif do_color
case m
when :red
out += "\e[0;31m"
when :cyan
out += "\e[0;36m"
when :reset
out += "\e[0m"
end
end
end
io.write(out)
end
private
# Determine whether to output ANSI color escape codes.
#
# @return [Boolean]
# Whether to output ANSI color escape codes.
def do_ansi?(io)
if RUBY_PLATFORM =~ /mingw/
(ENV["TERM"] == "xterm") && %w[fifo characterSpecial].include?(io.stat.ftype)
else
io.tty?
end
end
end
end
end

View File

@ -39,6 +39,15 @@ module Rscons
Rscons.n_threads = n_threads.to_i
end
opts.on("-r", "--color MODE", "Set color mode (off, auto, force)") do |color_mode|
case color_mode
when "off"
Rscons.do_ansi_color = false
when "force"
Rscons.do_ansi_color = true
end
end
opts.on_tail("--version", "Show version") do
puts "Rscons version #{Rscons::VERSION}"
exit 0

View File

@ -331,7 +331,7 @@ module Rscons
setup_info: job[:setup_info])
unless result
failure = "Failed to build #{job[:target]}"
$stderr.puts failure
Ansi.write($stderr, :red, failure, :reset, "\n")
next
end
end
@ -358,10 +358,10 @@ module Rscons
end
else
unless @echo == :command
$stdout.puts "Failed command was: #{command_to_s(tc.command)}"
print_failed_command(tc.command)
end
failure = "Failed to build #{tc.build_operation[:target]}"
$stderr.puts failure
Ansi.write($stderr, :red, failure, :reset, "\n")
break
end
end
@ -412,16 +412,12 @@ module Rscons
#
# @return [true,false,nil] Return value from Kernel.system().
def execute(short_desc, command, options = {})
if @echo == :command
puts command_to_s(command)
elsif @echo == :short
puts short_desc
end
print_builder_run_message(short_desc, command)
env_args = options[:env] ? [options[:env]] : []
options_args = options[:options] ? [options[:options]] : []
system(*env_args, *Rscons.command_executer, *command, *options_args).tap do |result|
unless result or @echo == :command
$stdout.puts "Failed command was: #{command_to_s(command)}"
print_failed_command(command)
end
end
end
@ -657,7 +653,7 @@ module Rscons
call_build_hooks[:post]
else
unless @echo == :command
$stdout.puts "Failed command was: #{command_to_s(tc.command)}"
print_failed_command(tc.command)
end
end
end
@ -844,7 +840,7 @@ module Rscons
varset_hash = @varset.to_h
varset_hash.keys.sort_by(&:to_s).each do |var|
var_str = var.is_a?(Symbol) ? var.inspect : var
puts "#{var_str} => #{varset_hash[var].inspect}"
Ansi.write($stdout, :cyan, var_str, :reset, " => #{varset_hash[var].inspect}\n")
end
end
@ -857,6 +853,34 @@ module Rscons
@n_threads || Rscons.n_threads
end
# Print the builder run message, depending on the Environment's echo mode.
#
# @param short_description [String]
# Builder short description, printed if the echo mode is :short.
# @param command [Array<String>]
# Builder command, printed if the echo mode is :command.
#
# @return [void]
def print_builder_run_message(short_description, command)
case @echo
when :command
message = command_to_s(command) if command
when :short
message = short_description if short_description
end
Ansi.write($stdout, :cyan, message, :reset, "\n") if message
end
# Print a failed command.
#
# @param command [Array<String>]
# Builder command.
#
# @return [void]
def print_failed_command(command)
Ansi.write($stdout, :red, "Failed command was: #{command_to_s(command)}", :reset, "\n")
end
private
# Add a build target.
@ -889,13 +913,7 @@ module Rscons
#
# @return [void]
def start_threaded_command(tc)
if @echo == :command
puts command_to_s(tc.command)
elsif @echo == :short
if tc.short_description
puts tc.short_description
end
end
print_builder_run_message(tc.short_description, tc.command)
env_args = tc.system_env ? [tc.system_env] : []
options_args = tc.system_options ? [tc.system_options] : []

View File

@ -769,6 +769,29 @@ EOF
expect(lines(result.stdout)).to eq ["165"]
end
context "colored output" do
it "does not output in color with --color=off" do
test_dir("simple")
result = run_test(rscons_args: %w[--color=off])
expect(result.stderr).to eq ""
expect(result.stdout).to_not match(/\e\[/)
end
it "displays output in color with --color=force" do
test_dir("simple")
result = run_test(rscons_args: %w[--color=force])
expect(result.stderr).to eq ""
expect(result.stdout).to match(/\e\[/)
File.open("simple.c", "wb") do |fh|
fh.write("foobar")
end
result = run_test(rscons_args: %w[--color=force])
expect(result.stderr).to match(/\e\[/)
end
end
context "backward compatibility" do
it "allows a builder to call Environment#run_builder in a non-threaded manner" do
test_dir("simple")
@ -791,6 +814,21 @@ EOF
]
end
it "prints the failed build command for a non-threaded builder" do
test_dir("simple")
File.open("simple.c", "wb") do |fh|
fh.write(<<EOF)
void one()
{
}
EOF
end
result = run_test(rsconsfile: "build_sources.rb")
expect(result.status).to_not eq 0
expect(result.stderr).to match /main/
expect(result.stdout).to match /Failed command was: gcc/
end
it "prints the failed build command for a threaded builder when called via Environment#run_builder without delayed execution" do
test_dir("simple")
File.open("simple.c", "wb") do |fh|

33
spec/rscons/ansi_spec.rb Normal file
View File

@ -0,0 +1,33 @@
module Rscons
describe Ansi do
describe ".do_ansi?" do
context "on Windows" do
it "returns true when IO is a fifo on an xterm" do
stub_const("RUBY_PLATFORM", "mingw")
expect(ENV).to receive(:[]).with("TERM").and_return("xterm")
io = double
expect(io).to receive(:stat).and_return(Struct.new(:ftype).new("fifo"))
expect(Ansi.__send__(:do_ansi?, io)).to be_truthy
end
it "returns false when TERM is not set appropriately" do
stub_const("RUBY_PLATFORM", "mingw")
expect(ENV).to receive(:[]).with("TERM").and_return(nil)
io = double
expect(Ansi.__send__(:do_ansi?, io)).to be_falsey
end
end
context "on POSIX" do
it "returns true when IO is a TTY" do
stub_const("RUBY_PLATFORM", "linux")
io = double
expect(io).to receive(:tty?).and_return(true)
expect(Ansi.__send__(:do_ansi?, io)).to be_truthy
end
end
end
end
end

View File

@ -190,38 +190,6 @@ module Rscons
end
end
describe "#execute" do
context "with echo: :short" do
context "with no errors" do
it "prints the short description and executes the command" do
env = Environment.new(echo: :short)
expect(env).to receive(:puts).with("short desc")
expect(env).to receive(:system).with(*Rscons.command_executer, "a", "command").and_return(true)
env.execute("short desc", ["a", "command"])
end
end
context "with errors" do
it "prints the short description, executes the command, and prints the failed command line" do
env = Environment.new(echo: :short)
expect(env).to receive(:puts).with("short desc")
expect(env).to receive(:system).with(*Rscons.command_executer, "a", "command").and_return(false)
expect($stdout).to receive(:puts).with("Failed command was: a command")
env.execute("short desc", ["a", "command"])
end
end
end
context "with echo: :command" do
it "prints the command executed and executes the command" do
env = Environment.new(echo: :command)
expect(env).to receive(:puts).with("a command '--arg=val with spaces'")
expect(env).to receive(:system).with({modified: :environment}, *Rscons.command_executer, "a", "command", "--arg=val with spaces", {opt: :val}).and_return(false)
env.execute("short desc", ["a", "command", "--arg=val with spaces"], env: {modified: :environment}, options: {opt: :val})
end
end
end
describe "#method_missing" do
it "calls the original method missing when the target method is not a known builder" do
env = Environment.new