omnitest-psychic 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.rubocop.yml +5 -0
- data/.rubocop_todo.yml +36 -0
- data/.travis.yml +10 -0
- data/CONTRIBUTING.md +61 -0
- data/Gemfile +23 -0
- data/LICENSE.txt +22 -0
- data/README.md +234 -0
- data/Rakefile +12 -0
- data/appveyor.yml +9 -0
- data/bin/psychic +4 -0
- data/docs/code_samples.md +92 -0
- data/docs/index.md +128 -0
- data/lib/omnitest/output_helper.rb +84 -0
- data/lib/omnitest/psychic.rb +191 -0
- data/lib/omnitest/psychic/cli.rb +171 -0
- data/lib/omnitest/psychic/code2doc.rb +75 -0
- data/lib/omnitest/psychic/code2doc/code_helper.rb +122 -0
- data/lib/omnitest/psychic/code2doc/code_segmenter.rb +170 -0
- data/lib/omnitest/psychic/code2doc/comment_styles.rb +92 -0
- data/lib/omnitest/psychic/command_template.rb +35 -0
- data/lib/omnitest/psychic/error.rb +15 -0
- data/lib/omnitest/psychic/execution/default_strategy.rb +82 -0
- data/lib/omnitest/psychic/execution/env_strategy.rb +12 -0
- data/lib/omnitest/psychic/execution/flag_strategy.rb +14 -0
- data/lib/omnitest/psychic/execution/token_strategy.rb +68 -0
- data/lib/omnitest/psychic/factories/go_factories.rb +14 -0
- data/lib/omnitest/psychic/factories/hot_read_task_factory.rb +54 -0
- data/lib/omnitest/psychic/factories/java_factories.rb +81 -0
- data/lib/omnitest/psychic/factories/powershell_factories.rb +53 -0
- data/lib/omnitest/psychic/factories/ruby_factories.rb +56 -0
- data/lib/omnitest/psychic/factories/shell_factories.rb +89 -0
- data/lib/omnitest/psychic/factories/travis_factories.rb +33 -0
- data/lib/omnitest/psychic/factory_manager.rb +46 -0
- data/lib/omnitest/psychic/file_finder.rb +69 -0
- data/lib/omnitest/psychic/hints.rb +21 -0
- data/lib/omnitest/psychic/magic_task_factory.rb +98 -0
- data/lib/omnitest/psychic/script.rb +146 -0
- data/lib/omnitest/psychic/script_factory.rb +66 -0
- data/lib/omnitest/psychic/script_factory_manager.rb +24 -0
- data/lib/omnitest/psychic/script_runner.rb +45 -0
- data/lib/omnitest/psychic/task.rb +6 -0
- data/lib/omnitest/psychic/task_factory_manager.rb +19 -0
- data/lib/omnitest/psychic/task_runner.rb +30 -0
- data/lib/omnitest/psychic/tokens.rb +51 -0
- data/lib/omnitest/psychic/version.rb +5 -0
- data/lib/omnitest/psychic/workflow.rb +27 -0
- data/lib/omnitest/shell.rb +27 -0
- data/lib/omnitest/shell/buff_shellout_executor.rb +41 -0
- data/lib/omnitest/shell/execution_result.rb +61 -0
- data/lib/omnitest/shell/mixlib_shellout_executor.rb +66 -0
- data/mkdocs.yml +6 -0
- data/omnitest-psychic.gemspec +36 -0
- data/spec/fabricators/shell_fabricator.rb +9 -0
- data/spec/omnitest/psychic/code2doc/code_helper_spec.rb +123 -0
- data/spec/omnitest/psychic/execution/default_strategy_spec.rb +17 -0
- data/spec/omnitest/psychic/execution/env_strategy_spec.rb +17 -0
- data/spec/omnitest/psychic/execution/flag_strategy_spec.rb +26 -0
- data/spec/omnitest/psychic/execution/token_strategy_spec.rb +26 -0
- data/spec/omnitest/psychic/factories/java_factories_spec.rb +35 -0
- data/spec/omnitest/psychic/factories/powershell_factories_spec.rb +59 -0
- data/spec/omnitest/psychic/factories/ruby_factories_spec.rb +91 -0
- data/spec/omnitest/psychic/factories/shell_factories_spec.rb +79 -0
- data/spec/omnitest/psychic/factories/travis_factories_spec.rb +78 -0
- data/spec/omnitest/psychic/hot_read_task_factory_spec.rb +51 -0
- data/spec/omnitest/psychic/script_factory_manager_spec.rb +57 -0
- data/spec/omnitest/psychic/script_spec.rb +55 -0
- data/spec/omnitest/psychic/shell_spec.rb +68 -0
- data/spec/omnitest/psychic/workflow_spec.rb +46 -0
- data/spec/omnitest/psychic_spec.rb +170 -0
- data/spec/spec_helper.rb +52 -0
- metadata +352 -0
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
module Omnitest
|
4
|
+
class Psychic
|
5
|
+
class Script # rubocop:disable Metrics/ClassLength
|
6
|
+
include Code2Doc::CodeHelper
|
7
|
+
include Omnitest::OutputHelper
|
8
|
+
|
9
|
+
attr_reader :psychic
|
10
|
+
|
11
|
+
# @return [String] a name for the script
|
12
|
+
attr_reader :name
|
13
|
+
# @return [Pathname] the location of the script
|
14
|
+
attr_reader :source_file
|
15
|
+
# @return [Hash] options controlling how the script is executed
|
16
|
+
attr_reader :opts
|
17
|
+
# @return [Hash] params key/value pairs to bind to script input
|
18
|
+
attr_accessor :params
|
19
|
+
attr_accessor :arguments
|
20
|
+
attr_accessor :env
|
21
|
+
|
22
|
+
def initialize(psychic, name, source_file, opts = {})
|
23
|
+
fail ArgumentError if psychic.nil?
|
24
|
+
fail ArgumentError if name.nil?
|
25
|
+
fail ArgumentError if source_file.nil?
|
26
|
+
@name = name.to_s
|
27
|
+
@source_file, *@arguments = Shellwords.shellsplit(source_file.to_s)
|
28
|
+
@source_file = Pathname(@source_file)
|
29
|
+
@opts ||= opts
|
30
|
+
@env = opts[:env] || psychic.env
|
31
|
+
@params = opts[:params] ||= psychic.parameters
|
32
|
+
@psychic = psychic
|
33
|
+
end
|
34
|
+
|
35
|
+
def basedir
|
36
|
+
@psychic.basedir
|
37
|
+
end
|
38
|
+
|
39
|
+
def extname
|
40
|
+
source_file.extname
|
41
|
+
end
|
42
|
+
|
43
|
+
def command
|
44
|
+
execution_strategy.command
|
45
|
+
end
|
46
|
+
|
47
|
+
def execute(*extra_args)
|
48
|
+
execution_strategy.execute *extra_args
|
49
|
+
end
|
50
|
+
|
51
|
+
def tokens
|
52
|
+
return [] unless detection_strategy.respond_to? :tokens
|
53
|
+
@tokens ||= detection_strategy.tokens
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s(verbose = false)
|
57
|
+
build_string do
|
58
|
+
status('Script Name', name)
|
59
|
+
display_tokens
|
60
|
+
status('Source File', formatted_file_name)
|
61
|
+
display_source if verbose
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_path
|
66
|
+
# So coercion to Pathname is possible
|
67
|
+
source_file.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
def absolute_source_file
|
71
|
+
return nil if source_file.nil?
|
72
|
+
|
73
|
+
File.expand_path source_file, basedir
|
74
|
+
end
|
75
|
+
|
76
|
+
def detection_strategy
|
77
|
+
@detection_strategy ||= create_detection_strategy
|
78
|
+
end
|
79
|
+
|
80
|
+
def execution_strategy
|
81
|
+
@execution_strategy ||= create_execution_strategy
|
82
|
+
end
|
83
|
+
|
84
|
+
def interactive?
|
85
|
+
@psychic.interactive?
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def display_source
|
91
|
+
return unless source?
|
92
|
+
status 'Source Code'
|
93
|
+
say highlighted_code
|
94
|
+
end
|
95
|
+
|
96
|
+
def display_tokens
|
97
|
+
return status 'Tokens', '(None)' if tokens.empty?
|
98
|
+
|
99
|
+
status 'Tokens'
|
100
|
+
indent do
|
101
|
+
tokens.each do | token |
|
102
|
+
say "- #{token}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def formatted_file_name
|
108
|
+
if source?
|
109
|
+
Omnitest::Core::FileSystem.relativize(absolute_source_file, Dir.pwd)
|
110
|
+
else
|
111
|
+
colorize('<No script>', :red)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def create_execution_strategy
|
116
|
+
strategy = opts[:execution_strategy]
|
117
|
+
case strategy
|
118
|
+
when nil
|
119
|
+
Execution::DefaultStrategy.new self
|
120
|
+
when 'tokens'
|
121
|
+
Execution::TokenStrategy.new self
|
122
|
+
when 'environment_variables'
|
123
|
+
Execution::EnvStrategy.new self
|
124
|
+
when 'flags'
|
125
|
+
Execution::FlagStrategy.new self
|
126
|
+
else
|
127
|
+
# TODO: Need support for custom commands with positional args
|
128
|
+
fail "Unknown binding strategy #{strategy}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def create_detection_strategy
|
133
|
+
strategy = opts[:detection_strategy] || opts[:execution_strategy]
|
134
|
+
case strategy
|
135
|
+
when nil
|
136
|
+
nil
|
137
|
+
when 'tokens'
|
138
|
+
Tokens::RegexpTokenHandler.new(source, /'\{(\w+)\}'/, "'\\1'")
|
139
|
+
else
|
140
|
+
# TODO: Need support for detecting tokens from comments, help commands, etc.
|
141
|
+
fail "Unknown token detection strategy #{strategy}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Omnitest
|
2
|
+
class Psychic
|
3
|
+
class ScriptFactory < FactoryManager
|
4
|
+
include Omnitest::Core::Logger
|
5
|
+
|
6
|
+
TASK_PRIORITY = 5
|
7
|
+
|
8
|
+
attr_reader :priority, :psychic, :run_patterns
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def register_script_factory
|
12
|
+
Omnitest::Psychic::ScriptFactoryManager.register_factory(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
def run_patterns
|
16
|
+
@run_patterns ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def runs(ext, priority = 5)
|
20
|
+
run_patterns[ext] = priority
|
21
|
+
end
|
22
|
+
|
23
|
+
def priority_for_script(script)
|
24
|
+
script_path = Pathname(script)
|
25
|
+
run_patterns.each do | pattern, priority |
|
26
|
+
return priority if script_path.fnmatch(pattern, File::FNM_CASEFOLD)
|
27
|
+
end
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(psychic, opts)
|
33
|
+
@psychic = psychic
|
34
|
+
@opts = opts
|
35
|
+
@logger = psychic.logger
|
36
|
+
end
|
37
|
+
|
38
|
+
def active?
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def cwd
|
43
|
+
psychic.cwd
|
44
|
+
end
|
45
|
+
|
46
|
+
def known_scripts
|
47
|
+
cwd_path = Pathname(cwd)
|
48
|
+
self.class.run_patterns.flat_map do | pattern, _priority |
|
49
|
+
Dir[cwd_path.join(pattern)]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def known_script?(script)
|
54
|
+
known_scripts.include? Pathname(script)
|
55
|
+
end
|
56
|
+
|
57
|
+
def priority_for_script(script)
|
58
|
+
self.class.priority_for_script(script)
|
59
|
+
end
|
60
|
+
|
61
|
+
def script(_script)
|
62
|
+
fail NotImplementedError, 'This should be implemented by subclasses'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Omnitest
|
2
|
+
class Psychic
|
3
|
+
class ScriptFactoryManager < FactoryManager
|
4
|
+
def factories_for(script)
|
5
|
+
capable_factories = active_factories.select do | factory |
|
6
|
+
factory.priority_for_script(script)
|
7
|
+
end
|
8
|
+
|
9
|
+
capable_factories.sort_by do |factory|
|
10
|
+
factory.priority_for_script(script)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def priority_for(script)
|
15
|
+
active_factories.map do | factory |
|
16
|
+
priority = factory.priority_for_script(script) || 0
|
17
|
+
# FIXME: Need to change default log level to info before adding debug logging
|
18
|
+
# logger.debug("#{factory.class} priority for #{script.source_file}: #{priority}")
|
19
|
+
priority
|
20
|
+
end.max
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Omnitest
|
2
|
+
class Psychic
|
3
|
+
module ScriptRunner
|
4
|
+
def script_factory_manager
|
5
|
+
@script_factory_manager ||= ScriptFactoryManager.new(self, opts)
|
6
|
+
end
|
7
|
+
|
8
|
+
def known_scripts
|
9
|
+
@known_scripts ||= hints.scripts.map do | script_name, script_file |
|
10
|
+
Script.new(self, script_name, script_file, opts)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def script(script_name)
|
15
|
+
find_in_known_scripts(script_name) || find_in_basedir(script_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def find_in_known_scripts(script_name)
|
21
|
+
known_scripts.find do |script|
|
22
|
+
script.name.downcase == script_name.downcase
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_in_basedir(script_name) # rubocop:disable Metrics/AbcSize
|
27
|
+
file = FileFinder.find_file_by_alias(script_name, basedir) do | files |
|
28
|
+
candidates = files.group_by do | script_file |
|
29
|
+
# Chooses the file w/ the highest chance of being runnable
|
30
|
+
path = Omnitest::Core::FileSystem.relativize(script_file, cwd)
|
31
|
+
script = Script.new(self, script_name, path, opts)
|
32
|
+
script_factory_manager.priority_for(script) || 0
|
33
|
+
end
|
34
|
+
candidates.empty? ? files.first : candidates[candidates.keys.max].min_by(&:length)
|
35
|
+
end
|
36
|
+
|
37
|
+
return nil if file.nil?
|
38
|
+
|
39
|
+
Script.new(self, script_name, file, opts).tap do | script |
|
40
|
+
@known_scripts << script
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Omnitest
|
2
|
+
class Psychic
|
3
|
+
class TaskFactoryManager < FactoryManager
|
4
|
+
def factories_for(task)
|
5
|
+
capable_factories = active_factories.select do | factory |
|
6
|
+
factory.priority_for_task(task)
|
7
|
+
end
|
8
|
+
|
9
|
+
capable_factories.sort_by do |factory|
|
10
|
+
factory.priority_for_task(task)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def known_tasks
|
15
|
+
active_factories.flat_map(&:known_tasks)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Omnitest
|
2
|
+
class Psychic
|
3
|
+
module TaskRunner
|
4
|
+
# Chooses an appropriate task for the task alias
|
5
|
+
# @param [String] task_alias an alias used to lookup a task
|
6
|
+
# @return [Task] the best match for the task alias
|
7
|
+
def task(task_alias)
|
8
|
+
task_alias = task_alias.to_sym
|
9
|
+
task_factory = task_factory_manager.factories_for(task_alias).last
|
10
|
+
fail TaskNotImplementedError, task_alias if task_factory.nil? || task_factory.priority == 0
|
11
|
+
command = task_factory.task(task_alias)
|
12
|
+
Task.new(self, command)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Lists all known tasks. This will include tasks that have been
|
16
|
+
# manually alased in `psychic.yaml`, well-known tasks for detected
|
17
|
+
# tools, and possibly some dynamically detected tasks for tools that
|
18
|
+
# support task discovery.
|
19
|
+
# @return [Set<Task>] the set of known tasks
|
20
|
+
def known_tasks
|
21
|
+
task_factory_manager.known_tasks
|
22
|
+
end
|
23
|
+
|
24
|
+
# @api private
|
25
|
+
def task_factory_manager
|
26
|
+
@task_factory_manager ||= TaskFactoryManager.new(self, opts)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
autoload :Mustache, 'mustache'
|
2
|
+
|
3
|
+
module Omnitest
|
4
|
+
class Psychic
|
5
|
+
module Tokens
|
6
|
+
class RegexpTokenHandler
|
7
|
+
def initialize(template, token_pattern, token_replacement)
|
8
|
+
@template = template
|
9
|
+
@token_pattern = token_pattern
|
10
|
+
@token_replacement = token_replacement
|
11
|
+
end
|
12
|
+
|
13
|
+
def tokens
|
14
|
+
@template.scan(@token_pattern).flatten.uniq
|
15
|
+
end
|
16
|
+
|
17
|
+
def render(variables = {})
|
18
|
+
@template.gsub(@token_pattern) do
|
19
|
+
full_match = Regexp.last_match[0]
|
20
|
+
key = Regexp.last_match[1]
|
21
|
+
value = variables[key]
|
22
|
+
value = @token_replacement.gsub('\\1', value.to_s) unless @token_replacement.nil?
|
23
|
+
full_match.gsub(@token_pattern, value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class MustacheTokenHandler
|
29
|
+
def initialize(template)
|
30
|
+
@template = Mustache::Template.new(template)
|
31
|
+
end
|
32
|
+
|
33
|
+
def tokens
|
34
|
+
@template.tags
|
35
|
+
end
|
36
|
+
|
37
|
+
def render(variables = {})
|
38
|
+
Mustache.render(@template, variables)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.replace_tokens(template, variables, token_regexp = nil, token_replacement = nil)
|
43
|
+
if token_regexp.nil?
|
44
|
+
MustacheTokenHandler.new(template).render(variables)
|
45
|
+
else
|
46
|
+
RegexpTokenHandler.new(template, token_regexp, token_replacement).render(variables)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Omnitest
|
2
|
+
class Psychic
|
3
|
+
class Workflow
|
4
|
+
attr_reader :commands, :psychic
|
5
|
+
|
6
|
+
def initialize(psychic, name = 'workflow', options = {}, &block)
|
7
|
+
@psychic = psychic
|
8
|
+
@name = name
|
9
|
+
@options = options
|
10
|
+
@commands = []
|
11
|
+
instance_eval &block if block_given?
|
12
|
+
end
|
13
|
+
|
14
|
+
def task(name, *args)
|
15
|
+
@commands << psychic.task(name, *args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def command
|
19
|
+
@commands.map(&:command).join("\n") + "\n"
|
20
|
+
end
|
21
|
+
|
22
|
+
def execute(_params = {}, shell_opts = {}, *extra_args)
|
23
|
+
@psychic.execute(command, shell_opts, *extra_args)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Omnitest
|
2
|
+
module Shell
|
3
|
+
autoload :ExecutionResult, 'omnitest/shell/execution_result'
|
4
|
+
autoload :ExecutionError, 'omnitest/shell/execution_result'
|
5
|
+
autoload :MixlibShellOutExecutor, 'omnitest/shell/mixlib_shellout_executor'
|
6
|
+
autoload :BuffShellOutExecutor, 'omnitest/shell/buff_shellout_executor'
|
7
|
+
|
8
|
+
AVAILABLE_OPTIONS = [
|
9
|
+
# All MixLib::ShellOut options - though we don't use most of these
|
10
|
+
:cwd, :domain, :password, :user, :group, :umask,
|
11
|
+
:timeout, :returns, :live_stream, :live_stdout,
|
12
|
+
:live_stderr, :input, :logger, :log_level, :log_tag, :env
|
13
|
+
]
|
14
|
+
|
15
|
+
attr_writer :shell
|
16
|
+
|
17
|
+
def shell
|
18
|
+
@shell ||= RUBY_PLATFORM == 'java' ? BuffShellOutExecutor.new : MixlibShellOutExecutor.new
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_writer :shell
|
22
|
+
|
23
|
+
def cli
|
24
|
+
@cli ||= Thor::Shell::Base.shell
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|