psychic-runner 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/bin/psychic +1 -1
- data/lib/psychic/cli.rb +8 -1
- data/lib/psychic/output_helper.rb +84 -0
- data/lib/psychic/runner.rb +88 -16
- data/lib/psychic/runner/base_runner.rb +89 -36
- data/lib/psychic/runner/cli.rb +80 -25
- data/lib/psychic/runner/code_helper.rb +131 -0
- data/lib/psychic/runner/code_sample.rb +70 -0
- data/lib/psychic/runner/factories/ruby_factories.rb +16 -0
- data/lib/psychic/runner/{cold/shell_script_runner.rb → factories/shell_script_factories.rb} +4 -4
- data/lib/psychic/runner/{hot_runner.rb → hot_read_task_factory.rb} +4 -3
- data/lib/psychic/runner/magic_task_factory.rb +95 -0
- data/lib/psychic/runner/sample_finder.rb +36 -0
- data/lib/psychic/runner/sample_runner.rb +7 -15
- data/lib/psychic/runner/task_factory_registry.rb +31 -0
- data/lib/psychic/runner/version.rb +1 -1
- data/lib/psychic/shell/mixlib_shellout_executor.rb +3 -1
- data/lib/psychic/task.rb +14 -0
- data/spec/psychic/runner/factories/bundler_detector_spec.rb +90 -0
- data/spec/psychic/runner/{cold → factories}/shell_script_runner_spec.rb +10 -10
- data/spec/psychic/runner/{hot_runner_spec.rb → hot_read_task_factory_spec.rb} +7 -8
- data/spec/psychic/runner/sample_finder_spec.rb +34 -0
- data/spec/psychic/runner_spec.rb +7 -5
- metadata +20 -9
- data/lib/psychic/runner/cold_runner_registry.rb +0 -31
| @@ -0,0 +1,131 @@ | |
| 1 | 
            +
            module Psychic
         | 
| 2 | 
            +
              class Runner
         | 
| 3 | 
            +
                module CodeHelper
         | 
| 4 | 
            +
                  class Highlighter
         | 
| 5 | 
            +
                    def initialize(opts)
         | 
| 6 | 
            +
                      require 'rouge'
         | 
| 7 | 
            +
                      @lexer = Rouge::Lexer.find(opts[:language]) || Rouge::Lexer.guess_by_filename(opts[:filename])
         | 
| 8 | 
            +
                      @formatter = opts[:formatter]
         | 
| 9 | 
            +
                    rescue LoadError # rubocop:disable Lint/HandleExceptions
         | 
| 10 | 
            +
                      # No highlighting support
         | 
| 11 | 
            +
                    end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    def highlight(source)
         | 
| 14 | 
            +
                      if defined?(Rouge)
         | 
| 15 | 
            +
                        Rouge.highlight(source, @lexer, @formatter)
         | 
| 16 | 
            +
                      else
         | 
| 17 | 
            +
                        # No highlighting support
         | 
| 18 | 
            +
                        source
         | 
| 19 | 
            +
                      end
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  class ReStructuredTextHelper
         | 
| 24 | 
            +
                    def self.code_block(source, language)
         | 
| 25 | 
            +
                      buffer = StringIO.new
         | 
| 26 | 
            +
                      buffer.puts ".. code-block:: #{language}"
         | 
| 27 | 
            +
                      indented_source = source.lines.map do|line|
         | 
| 28 | 
            +
                        "  #{line}"
         | 
| 29 | 
            +
                      end.join("\n")
         | 
| 30 | 
            +
                      buffer.puts indented_source
         | 
| 31 | 
            +
                      buffer.string
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                  class MarkdownHelper
         | 
| 35 | 
            +
                    def self.code_block(source, language)
         | 
| 36 | 
            +
                      buffer = StringIO.new
         | 
| 37 | 
            +
                      buffer.puts # I've seen lots of rendering issues without a dividing newline
         | 
| 38 | 
            +
                      buffer.puts "```#{language}"
         | 
| 39 | 
            +
                      buffer.puts source
         | 
| 40 | 
            +
                      buffer.puts '```'
         | 
| 41 | 
            +
                      buffer.puts # Put a dividing newline after as well, to be safe...
         | 
| 42 | 
            +
                      buffer.string
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  def absolute_source_file
         | 
| 47 | 
            +
                    return nil if source_file.nil?
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    if basedir
         | 
| 50 | 
            +
                      File.expand_path source_file, basedir
         | 
| 51 | 
            +
                    else
         | 
| 52 | 
            +
                      source_file
         | 
| 53 | 
            +
                    end
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def source
         | 
| 57 | 
            +
                    File.read absolute_source_file
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  def source?
         | 
| 61 | 
            +
                    !absolute_source_file.nil?
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  def language
         | 
| 65 | 
            +
                    @language ||= detect_language
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  def detect_language
         | 
| 69 | 
            +
                    File.extname(source_file)
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def highlighted_code(formatter = 'terminal256')
         | 
| 73 | 
            +
                    Highlighter.new(language: language, filename: absolute_source_file,
         | 
| 74 | 
            +
                      formatter: formatter).highlight(source)
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  def code_block(source_code, language, opts = { format: :markdown })
         | 
| 78 | 
            +
                    case opts[:format]
         | 
| 79 | 
            +
                    when :rst
         | 
| 80 | 
            +
                      ReStructuredTextHelper.code_block source_code, language
         | 
| 81 | 
            +
                    when :markdown
         | 
| 82 | 
            +
                      MarkdownHelper.code_block source_code, language
         | 
| 83 | 
            +
                    else
         | 
| 84 | 
            +
                      fail IllegalArgumentError, "Unknown format: #{format}"
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  # TODO: Segmentation support...
         | 
| 89 | 
            +
                  # def initialize(*args)
         | 
| 90 | 
            +
                  #   @segmenter = Polytrix::Documentation::CodeSegmenter.new
         | 
| 91 | 
            +
                  #   super
         | 
| 92 | 
            +
                  # end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  # # Loses proper indentation on comments
         | 
| 95 | 
            +
                  # def snippet_after(matcher)
         | 
| 96 | 
            +
                  #   segments = @segmenter.segment(source)
         | 
| 97 | 
            +
                  #   buffer = StringIO.new
         | 
| 98 | 
            +
                  #   segment = segments.find do |s|
         | 
| 99 | 
            +
                  #     doc_segment_content = s.first.join
         | 
| 100 | 
            +
                  #     doc_segment_content.match matcher
         | 
| 101 | 
            +
                  #   end
         | 
| 102 | 
            +
                  #   buffer.print segment[1].join "\n" if segment # return code segment
         | 
| 103 | 
            +
                  #   buffer.string
         | 
| 104 | 
            +
                  # end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                  # def snippet_between(before_matcher, after_matcher)
         | 
| 107 | 
            +
                  #   segments = @segmenter.segment(source)
         | 
| 108 | 
            +
                  #   start_segment = find_segment_index segments, before_matcher
         | 
| 109 | 
            +
                  #   end_segment   = find_segment_index segments, after_matcher
         | 
| 110 | 
            +
                  #   buffer = StringIO.new
         | 
| 111 | 
            +
                  #   if start_segment && end_segment
         | 
| 112 | 
            +
                  #     segments[start_segment...end_segment].each do |segment|
         | 
| 113 | 
            +
                  #       buffer.puts @segmenter.comment(segment[0]) unless segment == segments[start_segment]
         | 
| 114 | 
            +
                  #       buffer.puts segment[1].join
         | 
| 115 | 
            +
                  #     end
         | 
| 116 | 
            +
                  #   end
         | 
| 117 | 
            +
                  #   buffer.puts "\n"
         | 
| 118 | 
            +
                  #   buffer.string
         | 
| 119 | 
            +
                  # end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  # private
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  # def find_segment_index(segments, matcher)
         | 
| 124 | 
            +
                  #   segments.find_index do |s|
         | 
| 125 | 
            +
                  #     doc_segment_content = s.first.join
         | 
| 126 | 
            +
                  #     doc_segment_content.match matcher
         | 
| 127 | 
            +
                  #   end
         | 
| 128 | 
            +
                  # end
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
              end
         | 
| 131 | 
            +
            end
         | 
| @@ -0,0 +1,70 @@ | |
| 1 | 
            +
            require 'psychic/runner/code_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Psychic
         | 
| 4 | 
            +
              class Runner
         | 
| 5 | 
            +
                class CodeSample < Struct.new(:name, :source_file, :basedir)
         | 
| 6 | 
            +
                  include CodeHelper
         | 
| 7 | 
            +
                  include Psychic::OutputHelper
         | 
| 8 | 
            +
                  # property :name
         | 
| 9 | 
            +
                  # property :basedir
         | 
| 10 | 
            +
                  # property :source_file
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def token_handler
         | 
| 13 | 
            +
                    # Default token pattern/replacement (used by php-opencloud) should be configurable
         | 
| 14 | 
            +
                    @token_handler ||= RegexpTokenHandler.new(source, /'\{(\w+)\}'/, "'\\1'")
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def command(runner)
         | 
| 18 | 
            +
                    command = runner.task_for(:run_sample)
         | 
| 19 | 
            +
                    # FIXME: Shouldn't this be relative to runner's cwd?
         | 
| 20 | 
            +
                    # command ||= Psychic::Util.relativize(source_file, runner.cwd)
         | 
| 21 | 
            +
                    command ||= "./#{source_file}"
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    command_params = { sample: name, sample_file: source_file }
         | 
| 24 | 
            +
                    command_params.merge!(@parameters) unless @parameters.nil?
         | 
| 25 | 
            +
                    Psychic::Util.replace_tokens(command, command_params)
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def to_s(verbose = false)
         | 
| 29 | 
            +
                    build_string do
         | 
| 30 | 
            +
                      status('Sample Name', name)
         | 
| 31 | 
            +
                      display_tokens
         | 
| 32 | 
            +
                      status('Source File', formatted_file_name)
         | 
| 33 | 
            +
                      display_source if verbose
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def to_path
         | 
| 38 | 
            +
                    # So coercion to Pathname is possible
         | 
| 39 | 
            +
                    source_file.to_s
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  private
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  def display_source
         | 
| 45 | 
            +
                    return unless source?
         | 
| 46 | 
            +
                    status 'Source Code'
         | 
| 47 | 
            +
                    say highlighted_code
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def display_tokens
         | 
| 51 | 
            +
                    return status 'Tokens', '(None)' if token_handler.tokens.empty?
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    status 'Tokens'
         | 
| 54 | 
            +
                    indent do
         | 
| 55 | 
            +
                      token_handler.tokens.each do | token |
         | 
| 56 | 
            +
                        say "- #{token}"
         | 
| 57 | 
            +
                      end
         | 
| 58 | 
            +
                    end
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  def formatted_file_name
         | 
| 62 | 
            +
                    if source?
         | 
| 63 | 
            +
                      Psychic::Util.relativize(absolute_source_file, Dir.pwd)
         | 
| 64 | 
            +
                    else
         | 
| 65 | 
            +
                      colorize('<No code sample>', :red)
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
              end
         | 
| 70 | 
            +
            end
         | 
| @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            module Psychic
         | 
| 2 | 
            +
              class Runner
         | 
| 3 | 
            +
                module Factories
         | 
| 4 | 
            +
                  class BundlerTaskFactory < MagicTaskFactory
         | 
| 5 | 
            +
                    magic_file 'Gemfile'
         | 
| 6 | 
            +
                    magic_file '.bundle/config'
         | 
| 7 | 
            +
                    magic_env_var 'BUNDLE_GEMFILE'
         | 
| 8 | 
            +
                    register_task_factory
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    task :bootstrap do
         | 
| 11 | 
            +
                      'bundle install'
         | 
| 12 | 
            +
                    end
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
            end
         | 
| @@ -1,11 +1,11 @@ | |
| 1 1 | 
             
            module Psychic
         | 
| 2 2 | 
             
              class Runner
         | 
| 3 | 
            -
                module  | 
| 4 | 
            -
                  class  | 
| 3 | 
            +
                module Factories
         | 
| 4 | 
            +
                  class ShellScriptTaskFactory < MagicTaskFactory
         | 
| 5 5 | 
             
                    include BaseRunner
         | 
| 6 6 | 
             
                    EXTENSIONS = ['.sh', '']
         | 
| 7 7 | 
             
                    magic_file 'scripts/*'
         | 
| 8 | 
            -
                     | 
| 8 | 
            +
                    register_task_factory
         | 
| 9 9 |  | 
| 10 10 | 
             
                    def initialize(opts)
         | 
| 11 11 | 
             
                      super
         | 
| @@ -14,7 +14,7 @@ module Psychic | |
| 14 14 | 
             
                      end
         | 
| 15 15 | 
             
                    end
         | 
| 16 16 |  | 
| 17 | 
            -
                    def  | 
| 17 | 
            +
                    def task_for(task_name)
         | 
| 18 18 | 
             
                      task = task_name.to_s
         | 
| 19 19 | 
             
                      script = Dir["#{@cwd}/scripts/#{task}{.sh,}"].first
         | 
| 20 20 | 
             
                      if script
         | 
| @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            module Psychic
         | 
| 2 2 | 
             
              class Runner
         | 
| 3 | 
            -
                class  | 
| 3 | 
            +
                class HotReadTaskFactory
         | 
| 4 4 | 
             
                  include BaseRunner
         | 
| 5 5 | 
             
                  def initialize(opts = {})
         | 
| 6 6 | 
             
                    super
         | 
| @@ -8,8 +8,9 @@ module Psychic | |
| 8 8 | 
             
                    @known_tasks = @tasks.keys
         | 
| 9 9 | 
             
                  end
         | 
| 10 10 |  | 
| 11 | 
            -
                  def  | 
| 12 | 
            -
                    @tasks[task_name]
         | 
| 11 | 
            +
                  def task_for(task_name)
         | 
| 12 | 
            +
                    return @tasks[task_name.to_s] if @tasks.include? task_name.to_s
         | 
| 13 | 
            +
                    super
         | 
| 13 14 | 
             
                  end
         | 
| 14 15 | 
             
                end
         | 
| 15 16 | 
             
              end
         | 
| @@ -0,0 +1,95 @@ | |
| 1 | 
            +
            module Psychic
         | 
| 2 | 
            +
              class Runner
         | 
| 3 | 
            +
                class MagicTaskFactory
         | 
| 4 | 
            +
                  include Psychic::Logger
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  attr_reader :known_tasks, :tasks, :cwd, :env, :hints
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  class << self
         | 
| 9 | 
            +
                    def register_task_factory
         | 
| 10 | 
            +
                      Psychic::Runner::TaskFactoryRegistry.register(self)
         | 
| 11 | 
            +
                    end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    def magic_file_patterns
         | 
| 14 | 
            +
                      @magic_file_patterns ||= []
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    def magic_file(pattern) # rubocop:disable Style/TrivialAccessors
         | 
| 18 | 
            +
                      magic_file_patterns << pattern
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    def magic_env_vars
         | 
| 22 | 
            +
                      @magic_env_vars ||= []
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    def magic_env_var(var)
         | 
| 26 | 
            +
                      magic_env_vars << var
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                    def known_tasks
         | 
| 30 | 
            +
                      @known_tasks ||= []
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    def tasks
         | 
| 34 | 
            +
                      @tasks ||= {}
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    def task(name, &block)
         | 
| 38 | 
            +
                      name = name.to_s
         | 
| 39 | 
            +
                      tasks[name] = block
         | 
| 40 | 
            +
                      known_tasks << name
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  def initialize(opts = {})
         | 
| 45 | 
            +
                    @opts = opts
         | 
| 46 | 
            +
                    init_attr(:cwd) { Dir.pwd }
         | 
| 47 | 
            +
                    init_attr(:known_tasks) { self.class.known_tasks }
         | 
| 48 | 
            +
                    init_attr(:tasks) { self.class.tasks }
         | 
| 49 | 
            +
                    init_attr(:logger) { new_logger }
         | 
| 50 | 
            +
                    init_attr(:env) { ENV.to_hash }
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def task_for(task_name)
         | 
| 54 | 
            +
                    tasks[task_name] if tasks.include? task_name
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  def build_task(task_name, *_args)
         | 
| 58 | 
            +
                    task_name = task_name.to_s
         | 
| 59 | 
            +
                    task = task_for(task_name)
         | 
| 60 | 
            +
                    task = task.call if task.respond_to? :call
         | 
| 61 | 
            +
                    fail Psychic::Runner::TaskNotImplementedError, task_name if task.nil?
         | 
| 62 | 
            +
                    task
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def active?
         | 
| 66 | 
            +
                    self.class.magic_file_patterns.each do | pattern |
         | 
| 67 | 
            +
                      return true unless Dir["#{@cwd}/#{pattern}"].empty?
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
                    self.class.magic_env_vars.each do | var |
         | 
| 70 | 
            +
                      return true if ENV[var]
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
                    false
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  def known_task?(task_name)
         | 
| 76 | 
            +
                    known_tasks.include?(task_name.to_s)
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  private
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def init_attr(var)
         | 
| 82 | 
            +
                    var_name = "@#{var}"
         | 
| 83 | 
            +
                    var_value = @opts[var]
         | 
| 84 | 
            +
                    var_value = yield if var_value.nil? && block_given?
         | 
| 85 | 
            +
                    instance_variable_set(var_name, var_value)
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  def init_attrs(*vars)
         | 
| 89 | 
            +
                    vars.each do | var |
         | 
| 90 | 
            +
                      init_attr var
         | 
| 91 | 
            +
                    end
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
              end
         | 
| 95 | 
            +
            end
         | 
| @@ -0,0 +1,36 @@ | |
| 1 | 
            +
            module Psychic
         | 
| 2 | 
            +
              class Runner
         | 
| 3 | 
            +
                class SampleFinder
         | 
| 4 | 
            +
                  attr_accessor :hints
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def initialize(search_dir = Dir.pwd, hints = nil)
         | 
| 7 | 
            +
                    @search_dir = search_dir
         | 
| 8 | 
            +
                    @hints = hints || {}
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def known_samples
         | 
| 12 | 
            +
                    hints.map do | name, file |
         | 
| 13 | 
            +
                      CodeSample.new(name, file, @search_dir)
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def find_sample(name)
         | 
| 18 | 
            +
                    file = find_in_hints(name) || Psychic::Util.find_file_by_alias(name, @search_dir)
         | 
| 19 | 
            +
                    CodeSample.new(name, file, @search_dir)
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  # Find multiple samples by a regex or glob pattern
         | 
| 23 | 
            +
                  # def find_samples(pattern)
         | 
| 24 | 
            +
                  # end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  private
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def find_in_hints(name)
         | 
| 29 | 
            +
                    hints.each do |k, v|
         | 
| 30 | 
            +
                      return v if k.downcase == name.downcase
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
                    nil
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
            end
         | 
| @@ -2,23 +2,14 @@ module Psychic | |
| 2 2 | 
             
              class Runner
         | 
| 3 3 | 
             
                module SampleRunner
         | 
| 4 4 | 
             
                  def find_sample(code_sample)
         | 
| 5 | 
            -
                     | 
| 5 | 
            +
                    @sample_finder.find_sample(code_sample)
         | 
| 6 6 | 
             
                  end
         | 
| 7 7 |  | 
| 8 | 
            -
                  def run_sample( | 
| 9 | 
            -
                     | 
| 10 | 
            -
                    absolute_sample_file =  | 
| 8 | 
            +
                  def run_sample(code_sample_name, *args)
         | 
| 9 | 
            +
                    code_sample = find_sample(code_sample_name)
         | 
| 10 | 
            +
                    absolute_sample_file = code_sample.absolute_source_file
         | 
| 11 11 | 
             
                    process_parameters(absolute_sample_file)
         | 
| 12 | 
            -
                    command | 
| 13 | 
            -
                    if command
         | 
| 14 | 
            -
                      execute(command, *args)
         | 
| 15 | 
            -
                    else
         | 
| 16 | 
            -
                      run_sample_file(sample_file)
         | 
| 17 | 
            -
                    end
         | 
| 18 | 
            -
                  end
         | 
| 19 | 
            -
             | 
| 20 | 
            -
                  def run_sample_file(sample_file, *args)
         | 
| 21 | 
            -
                    execute("./#{sample_file}", *args) # Assuming Bash, but should detect Windows and use PowerShell
         | 
| 12 | 
            +
                    execute(code_sample.command(self), *args)
         | 
| 22 13 | 
             
                  end
         | 
| 23 14 |  | 
| 24 15 | 
             
                  def process_parameters(sample_file)
         | 
| @@ -52,7 +43,7 @@ module Psychic | |
| 52 43 | 
             
                  end
         | 
| 53 44 |  | 
| 54 45 | 
             
                  def build_command(code_sample, sample_file)
         | 
| 55 | 
            -
                    command =  | 
| 46 | 
            +
                    command = task_for(:run_sample)
         | 
| 56 47 | 
             
                    return nil if command.nil?
         | 
| 57 48 |  | 
| 58 49 | 
             
                    command_params = { sample: code_sample, sample_file: sample_file }
         | 
| @@ -80,6 +71,7 @@ module Psychic | |
| 80 71 | 
             
                  end
         | 
| 81 72 |  | 
| 82 73 | 
             
                  def prompt(key)
         | 
| 74 | 
            +
                    value = @parameters[key]
         | 
| 83 75 | 
             
                    if value
         | 
| 84 76 | 
             
                      return value unless @interactive_mode == 'always'
         | 
| 85 77 | 
             
                      new_value = @cli.ask "Please set a value for #{key} (or enter to confirm #{value.inspect}): "
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            module Psychic
         | 
| 2 | 
            +
              class Runner
         | 
| 3 | 
            +
                class TaskFactoryRegistry
         | 
| 4 | 
            +
                  include Psychic::Logger
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  BUILT_IN_DIR = File.expand_path('../factories', __FILE__)
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  class << self
         | 
| 9 | 
            +
                    def autoload_task_factories!
         | 
| 10 | 
            +
                      # Load built-in task factories
         | 
| 11 | 
            +
                      Dir["#{BUILT_IN_DIR}/*.rb"].each do |task_factory_file|
         | 
| 12 | 
            +
                        require task_factory_file
         | 
| 13 | 
            +
                      end
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    def task_factory_classes
         | 
| 17 | 
            +
                      @task_factory_classes ||= Set.new
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    def register(klass)
         | 
| 21 | 
            +
                      task_factory_classes.add klass
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    def active_task_factories(opts)
         | 
| 25 | 
            +
                      task_factories = task_factory_classes.map { |k| k.new(opts) }
         | 
| 26 | 
            +
                      task_factories.select(&:active?)
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         |