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
|