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

Refactor formatter/ANSIColor #1589

Merged
merged 22 commits into from
Dec 1, 2021
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
50abbc7
Add a spec with custom colors
aurelien-reeves Nov 25, 2021
6ea0d8d
Refactor formatter/ANSIcolor to avoid usage of 'eval'
aurelien-reeves Nov 25, 2021
588b9f9
Fix rubocop offense
aurelien-reeves Nov 25, 2021
c44f12e
Fix broken spec
aurelien-reeves Nov 25, 2021
4af774e
Remove code duplication
aurelien-reeves Nov 25, 2021
c0ddd73
Use class << self and group together Formatter::ANSIColor class methods
aurelien-reeves Nov 29, 2021
5045ce7
Remove the use of the 'genki-ruby-terminfo' gem
aurelien-reeves Nov 29, 2021
8801450
Refactor definition of Cucumber ANSIColor aliases
aurelien-reeves Nov 29, 2021
73a0a3f
Rename 'customize_colors' to 'apply_custom_colors'
aurelien-reeves Nov 29, 2021
55f79ff
Refactor Term::ANSIColor with more modern coding styles
aurelien-reeves Nov 29, 2021
e9a30f9
Make the 'apply_styles' method private
aurelien-reeves Nov 30, 2021
3197a6d
Use 'define_method' rather than 'eval' in term::ansicolor
aurelien-reeves Nov 30, 2021
6dca37f
Use 'text' rather than 'string' to name some variables in **/ansicolo…
aurelien-reeves Nov 30, 2021
0dc54f4
Fix block usage with define_method
aurelien-reeves Nov 30, 2021
38ed8c8
Add some documentation to Cucumber::Term::ANSIColor
aurelien-reeves Dec 1, 2021
a669555
Add moe doc to Formatter::ANSIColor
aurelien-reeves Dec 1, 2021
9cb1640
Update CHANGELOG.md
aurelien-reeves Dec 1, 2021
a8d0ef8
Remove useless 'module_function' as we reopen self later
aurelien-reeves Dec 1, 2021
8dc4cf6
Properly reset colorurs to default in ANSIColor spec
aurelien-reeves Dec 1, 2021
86ddc10
Use #end_with? rather than a regexp
aurelien-reeves Dec 1, 2021
36e7656
Update lib/cucumber/formatter/ansicolor.rb
aurelien-reeves Dec 1, 2021
03daec7
Update lib/cucumber/formatter/ansicolor.rb
aurelien-reeves Dec 1, 2021
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo
- Do not serialize Messages::Hook#tag_expression if it is empty.
([PR#1579](https://github.com/cucumber/cucumber-ruby/pull/1579))

- Removed usage of `eval` in `Cucumber::Term::ANSIColor` and `Cucumber::Formatter::ANSIColor`.
([PR#1589](https://github.com/cucumber/cucumber-ruby/pull/1589)
[Issue#1583](https://github.com/cucumber/cucumber-ruby/issues/1583))

### Changed

### Removed
Expand Down
127 changes: 62 additions & 65 deletions lib/cucumber/formatter/ansicolor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,44 @@

module Cucumber
module Formatter
# Defines aliases for coloured output. You don't invoke any methods from this
# module directly, but you can change the output colours by defining
# a <tt>CUCUMBER_COLORS</tt> variable in your shell, very much like how you can
# tweak the familiar POSIX command <tt>ls</tt> with
# <a href="http://mipsisrisc.com/rambling/2008/06/27/lscolorsls_colors-now-with-linux-support/">$LSCOLORS/$LS_COLORS</a>
# This modules allows to format cucumber related outputs using ANSI escape sequences.
aurelien-reeves marked this conversation as resolved.
Show resolved Hide resolved
#
# For example, it provides a `passed` method which returns the string with
# the ANSI escape sequence to format it green per default.
#
# To use this, include or extend it in your class.
#
# Example:
#
# require 'cucumber/formatter/ansicolor'
#
# class MyFormatter
# extend Cucumber::Term::ANSIColor
#
# def on_test_step_finished(event)
# $stdout.puts undefined(event.test_step) if event.result.undefined?
# $stdout.puts passed(event.test_step) if event.result.passed?
# end
# end
#
# This modules also allows the user to customize the format of cucumber outputs
aurelien-reeves marked this conversation as resolved.
Show resolved Hide resolved
# using environment variables.
#
# For instance, if your shell has a black background and a green font (like the
# "Homebrew" settings for OS X' Terminal.app), you may want to override passed
# steps to be white instead of green.
#
# Example:
#
# export CUCUMBER_COLORS="passed=white,bold:passed_param=white,bold,underline"
#
# The colours that you can change are:
#
# * <tt>undefined</tt> - defaults to <tt>yellow</tt>
# * <tt>pending</tt> - defaults to <tt>yellow</tt>
# * <tt>pending_param</tt> - defaults to <tt>yellow,bold</tt>
# * <tt>flaky</tt> - defaults to <tt>yellow</tt>
# * <tt>flaky_param</tt> - defaults to <tt>yellow,bold</tt>
# * <tt>failed</tt> - defaults to <tt>red</tt>
# * <tt>failed_param</tt> - defaults to <tt>red,bold</tt>
# * <tt>passed</tt> - defaults to <tt>green</tt>
Expand All @@ -29,26 +56,18 @@ module Formatter
# * <tt>comment</tt> - defaults to <tt>grey</tt>
# * <tt>tag</tt> - defaults to <tt>cyan</tt>
#
# For instance, if your shell has a black background and a green font (like the
# "Homebrew" settings for OS X' Terminal.app), you may want to override passed
# steps to be white instead of green.
#
# Although not listed, you can also use <tt>grey</tt>.
#
# Examples: (On Windows, use SET instead of export.)
#
# export CUCUMBER_COLORS="passed=white"
# export CUCUMBER_COLORS="passed=white,bold:passed_param=white,bold,underline"
#
# To see what colours and effects are available, just run this in your shell:
#
# ruby -e "require 'rubygems'; require 'term/ansicolor'; puts Cucumber::Term::ANSIColor.attributes"
# ruby -e "require 'rubygems'; require 'cucumber/term/ansicolor'; puts Cucumber::Term::ANSIColor.attributes"
#
module ANSIColor
include Cucumber::Term::ANSIColor

# :stopdoc:
ALIASES = Hash.new do |h, k|
"#{h[Regexp.last_match(1)]},bold" if k.to_s =~ /(.*)_param/
next unless k.to_s =~ /(.*)_param/
aurelien-reeves marked this conversation as resolved.
Show resolved Hide resolved

"#{h[Regexp.last_match(1)]},bold"
end.merge(
'undefined' => 'yellow',
'pending' => 'yellow',
Expand All @@ -60,15 +79,22 @@ module ANSIColor
'comment' => 'grey',
'tag' => 'cyan'
)
# :startdoc:

if ENV['CUCUMBER_COLORS'] # Example: export CUCUMBER_COLORS="passed=red:failed=yellow"
ENV['CUCUMBER_COLORS'].split(':').each do |pair|
# Apply the custom color scheme
#
# example:
#
# apply_custom_colors('passed=white')
def apply_custom_colors(colors)
colors.split(':').each do |pair|
a = pair.split('=')
ALIASES[a[0]] = a[1]
end
end
apply_custom_colors(ENV['CUCUMBER_COLORS']) if ENV['CUCUMBER_COLORS']

# Eval to define the color-named methods required by Term::ANSIColor.
# Define the color-named methods required by Term::ANSIColor.
#
# Examples:
#
Expand All @@ -80,56 +106,18 @@ module ANSIColor
# red(bold(string, &proc)) + red
# end
ALIASES.each_key do |method_name|
next if method_name =~ /.*_param/
next if method_name.end_with?('_param')

code = <<-COLOR
def #{method_name}(string=nil, &proc)
#{"#{ALIASES[method_name].split(',').join('(')}(string, &proc#{')' * ALIASES[method_name].split(',').length}"}
aurelien-reeves marked this conversation as resolved.
Show resolved Hide resolved
end
# This resets the colour to the non-param colour
def #{method_name}_param(string=nil, &proc)
#{"#{ALIASES["#{method_name}_param"].split(',').join('(')}(string, &proc#{')' * ALIASES["#{method_name}_param"].split(',').length}"} + #{ALIASES[method_name].split(',').join(' + ')}
end
COLOR
eval(code) # rubocop:disable Security/Eval
end

def self.define_grey # :nodoc:
gem 'genki-ruby-terminfo'
require 'terminfo'
case TermInfo.default_object.tigetnum('colors')
when 0
raise "Your terminal doesn't support colours."
when 1
::Cucumber::Term::ANSIColor.coloring = false
alias_method :grey, :white
when 2..8
alias_method :grey, :white # rubocop:disable Lint/DuplicateMethods
else
define_real_grey
end
rescue Exception => e # rubocop:disable Lint/RescueException
# rubocop:disable Style/ClassEqualityComparison
if e.class.name == 'TermInfo::TermInfoError'
$stderr.puts '*** WARNING ***'
$stderr.puts "You have the genki-ruby-terminfo gem installed, but you haven't set your TERM variable."
$stderr.puts 'Try setting it to TERM=xterm-256color to get grey colour in output.'
$stderr.puts "\n"
alias_method :grey, :white
else
define_real_grey
define_method(method_name) do |text = nil, &proc|
apply_styles(ALIASES[method_name], text, &proc)
end
# rubocop:enable Style/ClassEqualityComparison
end

def self.define_real_grey # :nodoc:
define_method :grey do |string|
::Cucumber::Term::ANSIColor.coloring? ? "\e[90m#{string}\e[0m" : string
define_method("#{method_name}_param") do |text = nil, &proc|
apply_styles(ALIASES["#{method_name}_param"], text, &proc) + apply_styles(ALIASES[method_name])
end
end

define_grey

# :stopdoc:
def cukes(n)
('(::) ' * n).strip
end
Expand All @@ -145,6 +133,15 @@ def red_cukes(n)
def yellow_cukes(n)
blink(yellow(cukes(n)))
end
# :startdoc:

private

def apply_styles(styles, text = nil, &proc)
styles.split(',').reverse.reduce(text) do |result, method_name|
send(method_name, result, &proc)
end
end
end
end
end
123 changes: 73 additions & 50 deletions lib/cucumber/term/ansicolor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,28 @@

module Cucumber
module Term
# The ANSIColor module can be used for namespacing and mixed into your own
# classes.
# This module allows to colorize text using ANSI escape sequences.
#
# Include the module in your class and use its methods to colorize text.
#
# Example:
#
# require 'cucumber/term/ansicolor'
#
# class MyFormatter
# include Cucumber::Term::ANSIColor
#
# def initialize(config)
# $stdout.puts yellow("Initializing formatter")
# $stdout.puts green("Coloring is active \o/") if Cucumber::Term::ANSIColor.coloring?
# $stdout.puts grey("Feature path:") + blue(bold(config.feature_dirs))
# end
# end
#
# To see what colours and effects are available, just run this in your shell:
#
# ruby -e "require 'rubygems'; require 'cucumber/term/ansicolor'; puts Cucumber::Term::ANSIColor.attributes"
#
module ANSIColor
# :stopdoc:
ATTRIBUTES = [
Expand All @@ -27,6 +47,7 @@ module ANSIColor
[:magenta, 35],
[:cyan, 36],
[:white, 37],
[:grey, 90],
[:on_black, 40],
[:on_red, 41],
[:on_green, 42],
Expand All @@ -40,73 +61,75 @@ module ANSIColor
ATTRIBUTE_NAMES = ATTRIBUTES.transpose.first
# :startdoc:

# Returns true, if the coloring function of this module
# is switched on, false otherwise.
def self.coloring?
@coloring
end

# Turns the coloring on or off globally, so you can easily do
# this for example:
# Cucumber::Term::ANSIColor::coloring = STDOUT.isatty
def self.coloring=(val)
@coloring = val
end
self.coloring = true

# rubocop:disable Security/Eval
ATTRIBUTES.each do |c, v|
eval <<-END_EVAL, binding, __FILE__, __LINE__ + 1
def #{c}(string = nil)
result = String.new
result << "\e[#{v}m" if Cucumber::Term::ANSIColor.coloring?
if block_given?
result << yield
elsif string
result << string
elsif respond_to?(:to_str)
result << to_str
else
return result #only switch on
end
result << "\e[0m" if Cucumber::Term::ANSIColor.coloring?
result
end
END_EVAL
end
# rubocop:enable Security/Eval

# Regular expression that is used to scan for ANSI-sequences while
# uncoloring strings.
COLORED_REGEXP = /\e\[(?:[34][0-7]|[0-9])?m/

def self.included(klass)
return unless klass == String
@coloring = true

class << self
# Turns the coloring on or off globally, so you can easily do
# this for example:
# Cucumber::Term::ANSIColor::coloring = $stdout.isatty
attr_accessor :coloring

# Returns true, if the coloring function of this module
# is switched on, false otherwise.
alias coloring? :coloring

def included(klass)
return unless klass == String

ATTRIBUTES.delete(:clear)
ATTRIBUTE_NAMES.delete(:clear)
end
end

ATTRIBUTES.delete(:clear)
ATTRIBUTE_NAMES.delete(:clear)
ATTRIBUTES.each do |color_name, color_code|
define_method(color_name) do |text = nil, &block|
if block
colorize(block.call, color_code)
elsif text
colorize(text, color_code)
elsif respond_to?(:to_str)
colorize(to_str, color_code)
else
colorize(nil, color_code) # switch coloration on
end
end
end

# Returns an uncolored version of the string, that is all
# Returns an uncolored version of the string
# ANSI-sequences are stripped from the string.
def uncolored(string = nil)
def uncolored(text = nil)
if block_given?
yield.gsub(COLORED_REGEXP, '')
elsif string
string.gsub(COLORED_REGEXP, '')
uncolorize(yield)
elsif text
uncolorize(text)
elsif respond_to?(:to_str)
to_str.gsub(COLORED_REGEXP, '')
uncolorize(to_str)
else
''
end
end

module_function

# Returns an array of all Cucumber::Term::ANSIColor attributes as symbols.
def attributes
ATTRIBUTE_NAMES
end

private

def colorize(text, color_code)
return String.new(text || '') unless Cucumber::Term::ANSIColor.coloring?
return "\e[#{color_code}m" unless text

"\e[#{color_code}m#{text}\e[0m"
end

def uncolorize(string)
string.gsub(COLORED_REGEXP, '')
end
end
end
end
22 changes: 22 additions & 0 deletions spec/cucumber/formatter/ansicolor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ module Formatter

expect(passed('foo')).to eq 'foo'
end

it 'works with a block' do
expect(passed { 'foo' }).to eq "\e[32mfoo\e[0m"
end

context 'with custom color scheme' do
before do
apply_custom_colors('passed=red,bold')
end

after do
reset_colours_to_default
end

it 'works with custom colors' do
expect(passed('foo')).to eq "\e[31m\e[1mfoo\e[0m\e[0m"
end

def reset_colours_to_default
apply_custom_colors('passed=green')
end
end
end
end
end