psychic-runner 0.0.7 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|