Project

General

Profile

Download (8.92 KB) Statistics
| Branch: | Tag: | Revision:
require 'thread'
require 'highline'

module ForemanMaintain
class Reporter
class CLIReporter < Reporter
# Simple spinner able to keep updating current line
class Spinner
def initialize(reporter, interval = 0.1)
@reporter = reporter
@mutex = Mutex.new
@active = false
@interval = interval
@spinner_index = 0
@spinner_chars = %w[| / - \\]
@current_line = ''
@puts_needed = false
start_spinner
end

def update(line)
@mutex.synchronize do
@current_line = line
print_current_line
end
end

def active?
@mutex.synchronize { @active }
end

def activate
@mutex.synchronize { @active = true }
spin
end

def deactivate
return unless active?
@mutex.synchronize do
@active = false
end
end

private

def start_spinner
@thread = Thread.new do
loop do
spin
sleep @interval
end
end
end

def spin
@mutex.synchronize do
return unless @active
print_current_line
@spinner_index = (@spinner_index + 1) % @spinner_chars.size
end
end

def print_current_line
@reporter.clear_line
line = "#{@spinner_chars[@spinner_index]} #{@current_line}"
@reporter.print(line)
end
end

def initialize(stdout = STDOUT, stdin = STDIN, options = {})
@stdout = stdout
@stdin = stdin
options.validate_options!(:assumeyes)
@assumeyes = options.fetch(:assumeyes, false)
@hl = HighLine.new(@stdin, @stdout)
@max_length = 80
@line_char = '-'
@cell_char = '|'
@spinner = Spinner.new(self)
@last_line = ''
end

def before_scenario_starts(scenario)
puts "Running #{scenario.description || scenario.class}"
hline('=')
end

def before_execution_starts(execution)
puts(execution_info(execution, ''))
end

def print(string)
new_line_if_needed
@stdout.print(string)
@stdout.flush
record_last_line(string)
end

def puts(string)
# we don't print the new line right away, as we want to be able to put
# the status label at the end of the last line, if possible.
# Therefore, we just mark that we need to print the new line next time
# we are printing something.
new_line_if_needed
@stdout.print(string)
@stdout.flush
@new_line_next_time = true
record_last_line(string)
end

def ask(message, options = {})
new_line_if_needed
options.validate_options!(:password)
# the answer is confirmed by ENTER which will emit a new line
@new_line_next_time = false
@last_line = ''
# add space at the end as otherwise highline would add new line there :/
message = "#{message} " unless message =~ /\s\Z/
answer = @hl.ask(message) { |q| q.echo = false if options[:password] }
answer.to_s.chomp.downcase if answer
end

def new_line_if_needed
if @new_line_next_time
@stdout.print("\n")
@stdout.flush
@new_line_next_time = false
end
end

def with_spinner(message)
new_line_if_needed
@spinner.activate
@spinner.update(message)
yield @spinner
ensure
@spinner.deactivate
@new_line_next_time = true
end

def after_execution_finishes(execution)
puts_status(execution.status)
puts(execution.output) unless execution.output.empty?
if execution.status == :already_run
puts(<<-MESSAGE.strip_heredoc)
The step was skipped as it was already run and it is marked
as run_once. Use --force to enforce the execution.
MESSAGE
end
hline
new_line_if_needed
end

def after_scenario_finishes(scenario)
scenario_failure_message(scenario)
puts "\n"
end

def clear_line
print "\r" + ' ' * @max_length + "\r"
end

def assumeyes?
@assumeyes
end

def single_step_decision(step)
answer = ask_decision("Continue with step [#{step.runtime_message}]?")
if answer == :yes
step
else
answer
end
end

def multiple_steps_decision(steps)
puts 'There are multiple steps to proceed:'
steps.each_with_index do |step, index|
puts "#{index + 1}) #{step.runtime_message}"
end
ask_to_select('Select step to continue', steps, &:runtime_message)
end

def ask_decision(message)
if assumeyes?
print("#{message} (assuming yes)")
return :yes
end
until_valid_decision do
filter_decision(ask("#{message}, [y(yes), n(no), q(quit)]"))
end
ensure
clear_line
end

def filter_decision(answer)
decision = nil
DECISION_MAPPER.each do |options, decision_label|
decision = decision_label if options.include?(answer)
end
decision
end

# rubocop:disable Metrics/MethodLength,Metrics/AbcSize
def ask_to_select(message, steps)
if assumeyes?
puts('(assuming first option)')
return steps.first
end
until_valid_decision do
answer = ask("#{message}, [n(next), q(quit)]")
if answer =~ /^\d+$/ && (answer.to_i - 1) < steps.size
steps[answer.to_i - 1]
else
decision = filter_decision(answer)
if decision == :yes
steps.first
else
decision
end
end
end
ensure
clear_line
end

# loop over the block until it returns some non-false value
def until_valid_decision
decision = nil
decision = yield until decision
decision
end

def execution_info(execution, text)
prefix = "#{execution.name}:"
"#{prefix} #{text}"
end

def puts_status(status)
label_offset = 10
padding = @max_length - @last_line.to_s.size - label_offset
if padding < 0
new_line_if_needed
padding = @max_length - label_offset
end
@stdout.print(' ' * padding + status_label(status))
@new_line_next_time = true
end

def status_label(status)
mapping = { :success => { :label => '[OK]', :color => :green },
:fail => { :label => '[FAIL]', :color => :red },
:running => { :label => '[RUNNING]', :color => :blue },
:skipped => { :label => '[SKIPPED]', :color => :yellow },
:already_run => { :label => '[ALREADY RUN]', :color => :yellow },
:warning => { :label => '[WARNING]', :color => :yellow } }
properties = mapping[status]
@hl.color(properties[:label], properties[:color], :bold)
end

def hline(line_char = @line_char)
puts line_char * @max_length
end

def record_last_line(string)
@last_line = string.lines.to_a.last
end

private

def scenario_failure_message(scenario)
return if scenario.passed?
message = []
message << <<-MESSAGE.strip_heredoc
Scenario [#{scenario.description}] failed.
MESSAGE
recommend = []
steps_with_error = scenario.steps_with_error(:whitelisted => false)
unless steps_with_error.empty?
message << format(<<-MESSAGE.strip_heredoc, format_steps(steps_with_error, "\n", 2))
The following steps ended up in failing state:

%s
MESSAGE
whitelist_labels = steps_with_error.map(&:label_dashed).join(',')
recommend << format(<<-MESSAGE.strip_heredoc, whitelist_labels)
Resolve the failed steps and rerun
the command. In case the failures are false positives,
use --whitelist="%s"
MESSAGE
end

steps_with_warning = scenario.steps_with_warning(:whitelisted => false)
unless steps_with_warning.empty?
message << format(<<-MESSAGE.strip_heredoc, format_steps(steps_with_warning, "\n", 2))
The following steps ended up in warning state:

%s
MESSAGE

recommend << <<-MESSAGE.strip_heredoc
The steps in warning state itself might not mean there is an error,
but it should be reviews to ensure the behavior is expected
MESSAGE
end
puts((message + recommend).join("\n"))
end

def format_steps(steps, join_with = ', ', indent = 0)
steps.map { |s| "#{' ' * indent}[#{s.label_dashed}]" }.join(join_with)
end
end
end
end
    (1-1/1)