lib/polytrix/cli.rb in polytrix-0.1.0.pre vs lib/polytrix/cli.rb in polytrix-0.1.0
- old
+ new
@@ -1,158 +1,250 @@
-require 'polytrix'
require 'thor'
+require 'polytrix'
+require 'polytrix/command'
+
module Polytrix
- module CLI
- autoload :Add, 'polytrix/cli/add'
- autoload :Report, 'polytrix/cli/report'
+ class CLI < Thor # rubocop:disable ClassLength
+ # Common module to load and invoke a CLI-implementation agnostic command.
+ module PerformCommand
+ # Perform a scenario subcommand.
+ #
+ # @param task [String] action to take, usually corresponding to the
+ # subcommand name
+ # @param command [String] command class to create and invoke]
+ # @param args [Array] remainder arguments from processed ARGV
+ # (default: `nil`)
+ # @param additional_options [Hash] additional configuration needed to
+ # set up the command class (default: `{}`)
+ def perform(task, command, args = nil, additional_options = {})
+ require "polytrix/command/#{command}"
- class Base < Thor
- include Polytrix::Core::FileSystemHelper
+ command_options = {
+ action: task,
+ help: -> { help(task) },
+ test_dir: @test_dir,
+ shell: shell
+ }.merge(additional_options)
- def self.config_options
- # I had trouble with class_option and subclasses...
- method_option :manifest, type: 'string', default: 'polytrix_tests.yml', desc: 'The Polytrix test manifest file'
- method_option :config, type: 'string', default: 'polytrix.rb', desc: 'The Polytrix config file'
+ str_const = Thor::Util.camel_case(command)
+ klass = ::Polytrix::Command.const_get(str_const)
+ klass.new(args, options, command_options).call
+ rescue ArgumentError => e
+ abort e.message
end
+ end
- def self.log_options
- method_option :quiet, type: :boolean, default: false, desc: 'Do not print log messages'
- end
+ include Logging
+ include PerformCommand
- def self.doc_options
- method_option :target_dir, type: :string, default: 'docs'
- method_option :lang, enum: Polytrix::Documentation::CommentStyles::COMMENT_STYLES.keys, desc: 'Source language (auto-detected if not specified)'
- method_option :format, enum: %w(md rst), default: 'md'
- end
+ # The maximum number of concurrent instances that can run--which is a bit
+ # high
+ MAX_CONCURRENCY = 9999
- def self.sdk_options
- method_option :sdk, type: 'string', desc: 'An implementor name or directory', default: '.'
- end
+ # Constructs a new instance.
+ def initialize(*args)
+ super
+ $stdout.sync = true
+ # Polytrix.logger = Polytrix.default_file_logger
+ end
- protected
+ desc 'list [INSTANCE|REGEXP|all]', 'Lists one or more scenarios'
+ method_option :bare,
+ aliases: '-b',
+ type: :boolean,
+ desc: 'List the name of each scenario only, one per line'
+ method_option :log_level,
+ aliases: '-l',
+ desc: 'Set the log level (debug, info, warn, error, fatal)'
+ method_option :manifest,
+ aliases: '-m',
+ desc: 'The Polytrix test manifest file location',
+ default: 'polytrix.yml'
+ method_option :test_dir,
+ aliases: '-t',
+ desc: 'The Polytrix test directory',
+ default: 'tests/polytrix'
+ method_option :solo,
+ desc: 'Enable solo mode - Polytrix will auto-configure a single implementor and its scenarios'
+ # , default: 'polytrix.yml'
+ method_option :solo_glob,
+ desc: 'The globbing pattern to find code samples in solo mode'
+ def list(*args)
+ update_config!
+ perform('list', 'list', args, options)
+ end
- def find_sdks(sdks)
- sdks.map do |sdk|
- implementor = Polytrix.implementors.find { |i| i.name == sdk }
- abort "SDK #{sdk} not found" if implementor.nil?
- implementor
- end
+ {
+ clone: "Change scenario state to cloned. " \
+ "Clone the code sample from git",
+ bootstrap: "Change scenario state to bootstraped. " \
+ "Running bootstrap scripts for the implementor",
+ exec: "Change instance state to executed. " \
+ "Execute the code sample and capture the results.",
+ verify: "Change instance state to verified. " \
+ "Assert that the captured results match the expectations for the scenario.",
+ destroy: "Change scenario state to destroyed. " \
+ "Delete all information for one or more scenarios"
+ }.each do |action, short_desc|
+ desc(
+ "#{action} [INSTANCE|REGEXP|all]",
+ short_desc
+ )
+ long_desc <<-DESC
+ The scenario states are in order: cloned, bootstrapped, executed, verified.
+ Change one or more scenarios from the current state to the #{action} state. Actions for all
+ intermediate states will be executed.
+ DESC
+ method_option :concurrency,
+ aliases: '-c',
+ type: :numeric,
+ lazy_default: MAX_CONCURRENCY,
+ desc: <<-DESC.gsub(/^\s+/, '').gsub(/\n/, ' ')
+ Run a #{action} against all matching instances concurrently. Only N
+ instances will run at the same time if a number is given.
+ DESC
+ method_option :log_level,
+ aliases: '-l',
+ desc: 'Set the log level (debug, info, warn, error, fatal)'
+ method_option :manifest,
+ aliases: '-m',
+ desc: 'The Polytrix test manifest file location',
+ default: 'polytrix.yml'
+ method_option :test_dir,
+ aliases: '-t',
+ desc: 'The Polytrix test directory',
+ default: 'tests/polytrix'
+ method_option :solo,
+ desc: 'Enable solo mode - Polytrix will auto-configure a single implementor and its scenarios'
+ method_option :solo_glob,
+ desc: 'The globbing pattern to find code samples in solo mode'
+ define_method(action) do |*args|
+ update_config!
+ action_options = options.dup
+ action_options['on'] = :implementor if [:clone, :bootstrap].include? action
+ perform(action, 'action', args, action_options)
end
+ end
- def pick_implementor(sdk)
- Polytrix.implementors.find { |i| i.name == sdk } || Polytrix.configuration.implementor(sdk)
- end
+ desc 'test [INSTANCE|REGEXP|all]',
+ 'Test (clone, bootstrap, exec, and verify) one or more scenarios'
+ long_desc <<-DESC
+ The scenario states are in order: cloned, bootstrapped, executed, verified.
+ Test changes the state of one or more scenarios executes
+ the actions for each state up to verify.
+ DESC
+ method_option :concurrency,
+ aliases: '-c',
+ type: :numeric,
+ lazy_default: MAX_CONCURRENCY,
+ desc: <<-DESC.gsub(/^\s+/, '').gsub(/\n/, ' ')
+ Run a test against all matching instances concurrently. Only N
+ instances will run at the same time if a number is given.
+ DESC
+ method_option :log_level,
+ aliases: '-l',
+ desc: 'Set the log level (debug, info, warn, error, fatal)'
+ method_option :manifest,
+ aliases: '-m',
+ desc: 'The Polytrix test manifest file location',
+ default: 'polytrix.yml'
+ method_option :test_dir,
+ aliases: '-t',
+ desc: 'The Polytrix test directory',
+ default: 'tests/polytrix'
+ method_option :solo,
+ desc: 'Enable solo mode - Polytrix will auto-configure a single implementor and its scenarios'
+ # , default: 'polytrix.yml'
+ method_option :solo_glob,
+ desc: 'The globbing pattern to find code samples in solo mode'
+ def test(*args)
+ update_config!
+ action_options = options.dup
+ perform('test', 'test', args, action_options)
+ end
- def debug(msg)
- say("polytrix::debug: #{msg}", :cyan) if debugging?
- end
+ desc 'code2doc [INSTANCE|REGEXP|all]',
+ 'Generates documenation from sample code for one or more scenarios'
+ long_desc <<-DESC
+ This task will convert annotated sample code to documentation. Markdown or
+ reStructureText are supported.
+ DESC
+ method_option :log_level,
+ aliases: '-l',
+ desc: 'Set the log level (debug, info, warn, error, fatal)'
+ method_option :manifest,
+ aliases: '-m',
+ desc: 'The Polytrix test manifest file location',
+ default: 'polytrix.yml'
+ method_option :solo,
+ desc: 'Enable solo mode - Polytrix will auto-configure a single implementor and its scenarios'
+ # , default: 'polytrix.yml'
+ method_option :solo_glob,
+ desc: 'The globbing pattern to find code samples in solo mode'
+ method_option :format,
+ aliases: '-f',
+ enum: %w(md rst),
+ default: 'md',
+ desc: 'Target documentation format'
+ method_option :target_dir,
+ aliases: '-d',
+ default: 'docs/',
+ desc: 'The target directory where documentation for generated documentation.'
+ def code2doc(*args)
+ update_config!
+ action_options = options.dup
+ perform('code2doc', 'action', args, action_options)
+ end
- def debugging?
- !ENV['POLYTRIX_DEBUG'].nil?
- end
-
- def setup
- manifest_file = File.expand_path options[:manifest]
- config_file = File.expand_path options[:config]
- if File.exists? manifest_file
- debug "Loading manifest file: #{manifest_file}"
- Polytrix.configuration.test_manifest = manifest_file if File.exists? manifest_file
- end
- if File.exists? config_file
- debug "Loading Polytrix config: #{config_file}"
- require_relative config_file
- end
- end
+ desc 'version', "Print Polytrix's version information"
+ def version
+ puts "Polytrix version #{Polytrix::VERSION}"
end
+ map %w[-v --version] => :version
- class Main < Base
- include Polytrix::Documentation::Helpers::CodeHelper
+ # register Polytrix::Generator::Init, "init",
+ # "init", "Adds some configuration to your cookbook so Polytrix can rock"
+ # long_desc <<-D, :for => "init"
+ # Init will add Test Polytrix support to an existing project for
+ # convergence integration testing. A default .polytrix.yml file (which is
+ # intended to be customized) is created in the project's root directory
+ # and one or more gems will be added to the project's Gemfile.
+ # D
+ # tasks["init"].options = Polytrix::Generator::Init.class_options
- # register Add, :add, 'add', 'Add implementors or code samples'
- # register Report, :report, 'report', 'Generate test reports'
- desc 'add', 'Add implementors or code samples'
- subcommand 'add', Add
+ private
- desc 'report', 'Generate test reports'
- subcommand 'report', Report
+ # Ensure the any failing commands exit non-zero.
+ #
+ # @return [true] you die always on failure
+ # @api private
+ def self.exit_on_failure?
+ true
+ end
- desc 'code2doc FILES', 'Converts annotated code to Markdown or reStructuredText'
- doc_options
- def code2doc(*files)
- if files.empty?
- help('code2doc')
- abort 'No FILES were specified, check usage above'
- end
+ # @return [Logger] the common logger
+ # @api private
+ def logger
+ Polytrix.logger
+ end
- files.each do |file|
- target_file_name = File.basename(file, File.extname(file)) + ".#{options[:format]}"
- target_file = File.join(options[:target_dir], target_file_name)
- say_status 'polytrix:code2doc', "Converting #{file} to #{target_file}", !quiet?
- doc = Polytrix::DocumentationGenerator.new.code2doc(file, options[:lang])
- FileUtils.mkdir_p File.dirname(target_file)
- File.write(target_file, doc)
- end
- rescue Polytrix::Documentation::CommentStyles::UnknownStyleError => e
- abort "Unknown file extension: #{e.extension}, please use --lang to set the language manually"
- end
+ # Update and finalize options for logging, concurrency, and other concerns.
+ #
+ # @api private
+ def update_config!
+ end
- desc 'exec', 'Executes code sample(s), using the SDK settings if provided'
- method_option :code2doc, type: :boolean, desc: 'Convert successfully executed code samples to documentation using the code2doc command'
- doc_options
- sdk_options
- config_options
- def exec(*files)
- setup
- if files.empty?
- help('exec')
- abort 'No FILES were specified, check usage above'
- end
+ # If auto_init option is active, invoke the init generator.
+ #
+ # @api private
+ def ensure_initialized
+ end
- exec_options = {
- # default_implementor: pick_implementor(options[:sdk])
- }
-
- files.each do | file |
- say_status 'polytrix:exec', "Running #{file}..."
- results = Polytrix.exec(file, exec_options)
- display_results results
- code2doc(file) if options[:code2doc]
- end
- end
-
- desc 'bootstrap [SDKs]', 'Bootstraps the SDK by installing dependencies'
- config_options
- def bootstrap(*sdks)
- setup
- Polytrix.bootstrap(*sdks)
- rescue ArgumentError => e
- abort e.message
- end
-
- desc 'test [SDKs]', 'Runs and tests the code samples'
- method_option :rspec_options, format: 'string', desc: 'Extra options to pass to rspec'
- config_options
- def test(*sdks)
- setup
- implementors = find_sdks(sdks)
- Polytrix.configuration.rspec_options = options[:rspec_options]
- Polytrix.run_tests(implementors)
- end
-
- protected
-
- def quiet?
- options[:quiet] || false
- end
-
- def display_results(challenge)
- short_name = challenge.name
- exit_code = challenge.result.execution_result.exitstatus
- color = exit_code == 0 ? :green : :red
- stderr = challenge.result.execution_result.stderr
- say_status "polytrix:exec[#{short_name}][stderr]", stderr, !quiet? unless stderr.empty?
- say_status "polytrix:exec[#{short_name}]", "Finished with exec code: #{challenge.result.execution_result.exitstatus}", color unless quiet?
- end
+ def duration(total)
+ total = 0 if total.nil?
+ minutes = (total / 60).to_i
+ seconds = (total - (minutes * 60))
+ format('(%dm%.2fs)', minutes, seconds)
end
end
end