diff --git a/features/cli/dry_run.feature b/features/cli/dry_run.feature new file mode 100644 index 0000000..708b296 --- /dev/null +++ b/features/cli/dry_run.feature @@ -0,0 +1,11 @@ +Feature: CLI dry run option + + Scenario: perfoms a trial run without applying actions + Given a recipe with: + """ + task :say_hello do + echo 'hello' + end + """ + When I successfully execute the recipe with option -n + Then the output must not contain "hello" diff --git a/features/cli/usage.feature b/features/cli/usage.feature index 4f677eb..2ad288b 100644 --- a/features/cli/usage.feature +++ b/features/cli/usage.feature @@ -5,5 +5,5 @@ Feature: CLI usage Then the exit status must be 64 And the output must contain exactly: """ - Usage: producer recipe_file + Usage: producer [-v] [-n] recipe_file """ diff --git a/features/cli/verbose.feature b/features/cli/verbose.feature new file mode 100644 index 0000000..0287e22 --- /dev/null +++ b/features/cli/verbose.feature @@ -0,0 +1,42 @@ +Feature: CLI verbose option + + Scenario: prints tasks name + Given a recipe with: + """ + task :say_hello do + end + """ + When I successfully execute the recipe with option -v + Then the output must match /Task:.+say_hello/ + + Scenario: prints whether condition is met + Given a recipe with: + """ + task :task_ok do + condition { true } + end + task :task_ko do + condition { false } + end + """ + When I successfully execute the recipe with option -v + Then the output must match /task_ok.+ condition: met.*task_ko.* condition: NOT met/ + + Scenario: prints actions info + Given a recipe with: + """ + task :say_hello do + echo 'hello message' + end + """ + When I successfully execute the recipe with option -v + Then the output must match /say_hello.+ action: echo/ + + Scenario: formats message with our simple logger + Given a recipe with: + """ + task :say_hello do + end + """ + When I successfully execute the recipe with option -v + Then the output must match /\ATask:.+say_hello.*\n.*condition/ diff --git a/features/steps/recipe_steps.rb b/features/steps/recipe_steps.rb index 6303ab1..3c0fb21 100644 --- a/features/steps/recipe_steps.rb +++ b/features/steps/recipe_steps.rb @@ -11,6 +11,11 @@ When /^I successfully execute the recipe$/ do assert_exit_status 0 end +When /^I successfully execute the recipe with option (-\w)$/ do |option| + run_simple "producer #{option} recipe.rb", false + assert_exit_status 0 +end + When /^I execute the recipe interactively$/ do run_interactive 'producer recipe.rb' end diff --git a/lib/producer/core.rb b/lib/producer/core.rb index 98fb8da..c32070c 100644 --- a/lib/producer/core.rb +++ b/lib/producer/core.rb @@ -28,6 +28,7 @@ require 'producer/core/condition' require 'producer/core/condition/dsl' require 'producer/core/env' require 'producer/core/errors' +require 'producer/core/logger_formatter' require 'producer/core/prompter' require 'producer/core/recipe' require 'producer/core/recipe/dsl' diff --git a/lib/producer/core/action.rb b/lib/producer/core/action.rb index bd5916a..7fbb6ad 100644 --- a/lib/producer/core/action.rb +++ b/lib/producer/core/action.rb @@ -11,6 +11,14 @@ module Producer @env = env @arguments = args end + + def name + self.class.name.split('::').last.downcase + end + + def to_s + name + end end end end diff --git a/lib/producer/core/actions/echo.rb b/lib/producer/core/actions/echo.rb index 4547179..92ff802 100644 --- a/lib/producer/core/actions/echo.rb +++ b/lib/producer/core/actions/echo.rb @@ -2,6 +2,10 @@ module Producer module Core module Actions class Echo < Action + def name + 'echo' + end + def apply output.puts arguments.first end diff --git a/lib/producer/core/actions/file_append.rb b/lib/producer/core/actions/file_append.rb index c78362f..830196e 100644 --- a/lib/producer/core/actions/file_append.rb +++ b/lib/producer/core/actions/file_append.rb @@ -2,6 +2,10 @@ module Producer module Core module Actions class FileAppend < Action + def name + 'file_append' + end + def apply fs.file_write path, combined_content end diff --git a/lib/producer/core/actions/file_replace_content.rb b/lib/producer/core/actions/file_replace_content.rb index 00fcdf0..7342f3a 100644 --- a/lib/producer/core/actions/file_replace_content.rb +++ b/lib/producer/core/actions/file_replace_content.rb @@ -2,6 +2,10 @@ module Producer module Core module Actions class FileReplaceContent < Action + def name + 'file_replace_content' + end + def apply fs.file_write path, replaced_content end diff --git a/lib/producer/core/actions/file_writer.rb b/lib/producer/core/actions/file_writer.rb index 2fea259..966b969 100644 --- a/lib/producer/core/actions/file_writer.rb +++ b/lib/producer/core/actions/file_writer.rb @@ -2,6 +2,10 @@ module Producer module Core module Actions class FileWriter < Action + def name + 'file_write' + end + def apply case arguments.size when 2 diff --git a/lib/producer/core/actions/mkdir.rb b/lib/producer/core/actions/mkdir.rb index fad206e..ccb7baa 100644 --- a/lib/producer/core/actions/mkdir.rb +++ b/lib/producer/core/actions/mkdir.rb @@ -2,6 +2,10 @@ module Producer module Core module Actions class Mkdir < Action + def name + 'mkdir' + end + def apply case arguments.size when 1 diff --git a/lib/producer/core/actions/shell_command.rb b/lib/producer/core/actions/shell_command.rb index 718ca86..3ff2599 100644 --- a/lib/producer/core/actions/shell_command.rb +++ b/lib/producer/core/actions/shell_command.rb @@ -2,6 +2,10 @@ module Producer module Core module Actions class ShellCommand < Action + def name + 'sh' + end + def apply remote.execute(arguments.first, output) end diff --git a/lib/producer/core/cli.rb b/lib/producer/core/cli.rb index 8646f31..b7c5653 100644 --- a/lib/producer/core/cli.rb +++ b/lib/producer/core/cli.rb @@ -3,7 +3,7 @@ module Producer class CLI ArgumentError = Class.new(::ArgumentError) - USAGE = "Usage: #{File.basename $0} recipe_file" + USAGE = "Usage: #{File.basename $0} [-v] [-n] recipe_file" EX_USAGE = 64 @@ -11,6 +11,7 @@ module Producer def run!(arguments, output: $stderr) begin cli = new(arguments) + cli.parse_arguments! rescue ArgumentError output.puts USAGE exit EX_USAGE @@ -19,21 +20,41 @@ module Producer end end - attr_reader :arguments, :stdout, :recipe + attr_reader :arguments, :stdout, :env, :recipe - def initialize(arguments, stdout: $stdout) - raise ArgumentError unless arguments.any? - @arguments = arguments + def initialize(args, stdout: $stdout) + @arguments = args @stdout = stdout + @env = Env.new(output: stdout) end - def run(worker: Worker.new) + def parse_arguments! + @arguments = arguments.inject([]) do |m, e| + case e + when '-v' + env.log_level = Logger::INFO + when '-n' + env.dry_run = true + else + m << e + end + m + end + + raise ArgumentError unless arguments.any? + end + + def run(worker: build_worker) load_recipe worker.process recipe.tasks end def load_recipe - @recipe = Recipe.evaluate_from_file(@arguments.first, Env.new) + @recipe = Recipe.evaluate_from_file(@arguments.first, env) + end + + def build_worker + Worker.new(env) end end end diff --git a/lib/producer/core/env.rb b/lib/producer/core/env.rb index b2e3e68..99a7f78 100644 --- a/lib/producer/core/env.rb +++ b/lib/producer/core/env.rb @@ -1,15 +1,15 @@ module Producer module Core class Env - attr_reader :input, :output, :registry - attr_accessor :target + attr_reader :input, :output, :registry, :logger + attr_accessor :target, :dry_run def initialize(input: $stdin, output: $stdout, remote: nil, registry: {}) @input = input @output = output @registry = registry @remote = remote - @target = nil + @dry_run = false end def remote @@ -23,6 +23,31 @@ module Producer def []=(key, value) @registry[key] = value end + + def logger + @logger ||= begin + logger = Logger.new(output) + logger.level = Logger::ERROR + logger.formatter = LoggerFormatter.new + logger + end + end + + def log(message) + logger.info message + end + + def log_level + logger.level + end + + def log_level=(level) + logger.level = level + end + + def dry_run? + @dry_run + end end end end diff --git a/lib/producer/core/logger_formatter.rb b/lib/producer/core/logger_formatter.rb new file mode 100644 index 0000000..657165c --- /dev/null +++ b/lib/producer/core/logger_formatter.rb @@ -0,0 +1,9 @@ +module Producer + module Core + class LoggerFormatter < Logger::Formatter + def call(severity, datetime, progname, message) + message + "\n" + end + end + end +end diff --git a/lib/producer/core/task.rb b/lib/producer/core/task.rb index b6e1b74..18ff815 100644 --- a/lib/producer/core/task.rb +++ b/lib/producer/core/task.rb @@ -17,6 +17,10 @@ module Producer @condition = condition end + def to_s + name.to_s + end + def condition_met? !!@condition end diff --git a/lib/producer/core/worker.rb b/lib/producer/core/worker.rb index 0bc46b2..d57fe0e 100644 --- a/lib/producer/core/worker.rb +++ b/lib/producer/core/worker.rb @@ -1,12 +1,27 @@ module Producer module Core class Worker + attr_accessor :env + + def initialize(env) + @env = env + end + def process(tasks) tasks.each { |t| process_task t } end def process_task(task) - task.actions.each(&:apply) if task.condition_met? + env.log "Task: #{task} applying" + if task.condition_met? + env.log ' condition: met' + task.actions.each do |e| + env.log " action: #{e} applying" + e.apply unless env.dry_run? + end + else + env.log ' condition: NOT met' + end end end end diff --git a/spec/producer/core/action_spec.rb b/spec/producer/core/action_spec.rb index 884c4ad..47d637c 100644 --- a/spec/producer/core/action_spec.rb +++ b/spec/producer/core/action_spec.rb @@ -3,5 +3,13 @@ require 'spec_helper' module Producer::Core describe Action do it_behaves_like 'action' + + describe '#name' do + subject(:action) { described_class.new(double 'env') } + + it 'infers action name from class name' do + expect(action.name).to eq 'action' + end + end end end diff --git a/spec/producer/core/cli_spec.rb b/spec/producer/core/cli_spec.rb index fde4729..e5a324a 100644 --- a/spec/producer/core/cli_spec.rb +++ b/spec/producer/core/cli_spec.rb @@ -6,29 +6,41 @@ module Producer::Core include FixturesHelpers let(:recipe_file) { fixture_path_for 'recipes/some_recipe.rb' } - let(:arguments) { [recipe_file] } + let(:options) { [] } + let(:arguments) { [*options, recipe_file] } let(:stdout) { StringIO.new } subject(:cli) { CLI.new(arguments, stdout: stdout) } describe '.run!' do + let(:cli) { double('cli').as_null_object } let(:output) { StringIO.new } subject(:run) { described_class.run! arguments, output: output } it 'builds a new CLI with given arguments' do - expect(CLI).to receive(:new).with(arguments).and_call_original + expect(described_class) + .to receive(:new).with(arguments).and_call_original run end it 'runs the CLI' do - cli = double 'cli' - allow(CLI).to receive(:new) { cli } + allow(described_class).to receive(:new) { cli } expect(cli).to receive :run run end - context 'when recipe argument is missing' do - let(:arguments) { [] } + it 'parses CLI arguments' do + allow(described_class).to receive(:new) { cli } + expect(cli).to receive :parse_arguments! + run + end + + context 'when an argument error is raised' do + before do + allow(described_class).to receive(:new) { cli } + allow(cli).to receive(:parse_arguments!) + .and_raise described_class::ArgumentError + end it 'exits with a return status of 64' do expect { run }.to raise_error(SystemExit) { |e| @@ -44,17 +56,15 @@ module Producer::Core end describe '#initialize' do - subject(:cli) { CLI.new(arguments) } - - it 'assigns $stdout as the default standard output' do - expect(cli.stdout).to be $stdout + it 'assigns the env with CLI output' do + expect(cli.env.output).to be stdout end - context 'without arguments' do - let(:arguments) { [] } + context 'without options' do + subject(:cli) { described_class.new(arguments) } - it 'raises our ArgumentError exception' do - expect { cli }.to raise_error described_class::ArgumentError + it 'assigns $stdout as the default standard output' do + expect(cli.stdout).to be $stdout end end end @@ -71,6 +81,40 @@ module Producer::Core end end + describe '#parse_arguments!' do + context 'with options' do + let(:options) { %w[-v] } + + before { cli.parse_arguments! } + + it 'removes options from arguments' do + expect(cli.arguments).to eq [recipe_file] + end + + context 'verbose' do + it 'sets env logger level to INFO' do + expect(cli.env.log_level).to eq Logger::INFO + end + end + + context 'dry run' do + let(:options) { %w[-n] } + + it 'enables env dry run' do + expect(cli.env).to be_dry_run + end + end + end + + context 'without arguments' do + let(:arguments) { [] } + + it 'raises the argument error exception' do + expect { cli.parse_arguments! }.to raise_error described_class::ArgumentError + end + end + end + describe '#run' do it 'loads the recipe' do cli.run @@ -84,10 +128,16 @@ module Producer::Core end end + describe '#env' do + it 'returns an env' do + expect(cli.env).to be_an Env + end + end + describe '#load_recipe' do - it 'evaluates the recipe file with an env' do + it 'evaluates the recipe file with the CLI env' do expect(Recipe) - .to receive(:evaluate_from_file).with(recipe_file, kind_of(Env)) + .to receive(:evaluate_from_file).with(recipe_file, cli.env) cli.load_recipe end @@ -96,5 +146,15 @@ module Producer::Core expect(cli.recipe.tasks.count).to be 2 end end + + describe '#build_worker' do + it 'returns a worker' do + expect(cli.build_worker).to be_a Worker + end + + it 'assigns the CLI env' do + expect(cli.build_worker.env).to eq cli.env + end + end end end diff --git a/spec/producer/core/env_spec.rb b/spec/producer/core/env_spec.rb index 275f388..fbee8fe 100644 --- a/spec/producer/core/env_spec.rb +++ b/spec/producer/core/env_spec.rb @@ -2,17 +2,14 @@ require 'spec_helper' module Producer::Core describe Env do - subject(:env) { Env.new } + let(:output) { StringIO.new } + subject(:env) { Env.new(output: output) } describe '#initialize' do it 'assigns $stdin as the default output' do expect(env.input).to be $stdin end - it 'assigns $stdout as the default output' do - expect(env.output).to be $stdout - end - it 'assigns no default target' do expect(env.target).not_to be end @@ -21,6 +18,18 @@ module Producer::Core expect(env.registry).to be_empty end + it 'assigns dry run as false' do + expect(env.dry_run).to be false + end + + context 'when output is not given as argument' do + subject(:env) { Env.new } + + it 'assigns $stdout as the default output' do + expect(env.output).to be $stdout + end + end + context 'when input is given as argument' do let(:input) { double 'input' } subject(:env) { described_class.new(input: input) } @@ -31,7 +40,6 @@ module Producer::Core end context 'when output is given as argument' do - let(:output) { double 'output' } subject(:env) { described_class.new(output: output) } it 'assigns the given output' do @@ -58,6 +66,25 @@ module Producer::Core end end + describe '#logger' do + it 'returns a logger' do + expect(env.logger).to be_a Logger + end + + it 'uses env output' do + env.logger.error 'some message' + expect(output.string).to include 'some message' + end + + it 'has a log level of ERROR' do + expect(env.log_level).to eq Logger::ERROR + end + + it 'uses our formatter' do + expect(env.logger.formatter).to be_a LoggerFormatter + end + end + describe '#remote' do it 'builds a Remote with the current target' do env.target = 'some_hostname.example' @@ -90,5 +117,33 @@ module Producer::Core expect(env[:some_key]).to eq :some_value end end + + describe '#log' do + it 'logs an info message through the assigned logger' do + expect(env.logger).to receive(:info).with 'message' + env.log 'message' + end + end + + describe '#log_level' do + it 'returns the logger level' do + expect(env.log_level).to eq env.logger.level + end + end + + describe '#log_level=' do + it 'sets the logger level' do + env.log_level = Logger::DEBUG + expect(env.logger.level).to eq Logger::DEBUG + end + end + + describe '#dry_run?' do + before { env.dry_run = true } + + it 'returns true when dry run is enabled' do + expect(env.dry_run?).to be true + end + end end end diff --git a/spec/producer/core/logger_formatter_spec.rb b/spec/producer/core/logger_formatter_spec.rb new file mode 100644 index 0000000..110dc6b --- /dev/null +++ b/spec/producer/core/logger_formatter_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +module Producer::Core + describe LoggerFormatter do + describe '#call' do + let(:severity) { double 'severity' } + let(:datetime) { double 'datetime' } + let(:progname) { double 'progname' } + let(:message) { 'some message' } + + subject { described_class.new.call(severity, datetime, progname, message) } + + it 'returns the given message with a line separator' do + expect(subject).to eq "#{message}\n" + end + end + end +end diff --git a/spec/producer/core/task_spec.rb b/spec/producer/core/task_spec.rb index 7d0259c..3d2236c 100644 --- a/spec/producer/core/task_spec.rb +++ b/spec/producer/core/task_spec.rb @@ -61,6 +61,12 @@ module Producer::Core end end + describe '#to_s' do + it 'includes the task name' do + expect(task.to_s).to include name.to_s + end + end + describe '#condition_met?' do context 'when condition is truthy' do let(:condition) { Condition.new([], true) } diff --git a/spec/producer/core/worker_spec.rb b/spec/producer/core/worker_spec.rb index de38bb2..8ea8aa1 100644 --- a/spec/producer/core/worker_spec.rb +++ b/spec/producer/core/worker_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' module Producer::Core describe Worker do - subject(:worker) { described_class.new } + let(:env) { double 'env', log: nil, dry_run?: false } + subject(:worker) { described_class.new(env) } describe '#process' do it 'processes each task' do @@ -12,23 +13,59 @@ module Producer::Core end describe '#process_task' do - let(:action) { double 'action' } - let(:task) { double('task', actions: [action]).as_null_object } + let(:action) { double('action', to_s: 'echo').as_null_object } + let(:task_name) { 'some_task' } + let(:task) { Task.new(task_name, [action]) } + + it 'logs task info' do + expect(env).to receive(:log).with /\ATask: #{task_name}/ + worker.process_task task + end context 'when task condition is met' do it 'applies the actions' do expect(action).to receive :apply worker.process_task task end + + it 'logs condition info' do + expect(env).to receive(:log).with(' condition: met') + worker.process_task task + end + + it 'logs action info' do + expect(env).to receive(:log).with /\A action: echo/ + worker.process_task task + end + + context 'when dry run is enabled' do + before { allow(env).to receive(:dry_run?) { true } } + + it 'does not apply the actions' do + expect(action).not_to receive :apply + worker.process_task task + end + end end context 'when task condition is not met' do - before { allow(task).to receive(:condition_met?) { false } } + let(:task) { Task.new(task_name, [action], false) } it 'does not apply the actions' do expect(action).not_to receive :apply worker.process_task task end + + it 'logs condition info' do + expect(env).to receive(:log).with(' condition: NOT met') + worker.process_task task + end + end + end + + describe '#env' do + it 'returns the assigned env' do + expect(worker.env).to be env end end end diff --git a/spec/support/shared_action.rb b/spec/support/shared_action.rb index c8c6224..d6d8539 100644 --- a/spec/support/shared_action.rb +++ b/spec/support/shared_action.rb @@ -40,5 +40,17 @@ module Producer::Core expect(action.fs).to be env.remote.fs end end + + describe '#name' do + it 'returns a word' do + expect(action.name).to match /\A\w+\z/ + end + end + + describe '#to_s' do + it 'returns a word' do + expect(action.to_s).to eq action.name + end + end end end