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