polytrix 0.1.0.pre → 0.1.0
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/.gitignore +1 -0
- data/.rubocop.yml +7 -2
- data/.travis.yml +2 -1
- data/Gemfile +1 -0
- data/README.md +190 -98
- data/Rakefile +8 -6
- data/bin/polytrix +2 -1
- data/docs/samples/code2doc/java/HelloWorld.md +4 -0
- data/docs/samples/code2doc/java/Quine.md +2 -0
- data/docs/samples/code2doc/python/hello_world.md +2 -0
- data/docs/samples/code2doc/python/quine.md +2 -0
- data/docs/samples/code2doc/ruby/hello_world.md +4 -0
- data/features/bootstrapping.feature +36 -0
- data/features/cloning.feature +34 -0
- data/features/execution.feature +2 -16
- data/features/fixtures/configs/empty.yml +12 -1
- data/features/fixtures/configs/hello_world.yml +11 -1
- data/features/fixtures/spec/polytrix_spec.rb +1 -4
- data/features/solo.feature +12 -0
- data/features/states.feature +40 -0
- data/features/step_definitions/sdk_steps.rb +11 -1
- data/features/support/env.rb +2 -1
- data/lib/polytrix/challenge.rb +211 -13
- data/lib/polytrix/challenge_result.rb +9 -0
- data/lib/polytrix/challenge_runner.rb +4 -11
- data/lib/polytrix/challenges.rb +16 -0
- data/lib/polytrix/cli/report.rb +0 -4
- data/lib/polytrix/cli.rb +229 -137
- data/lib/polytrix/color.rb +40 -0
- data/lib/polytrix/command/action.rb +26 -0
- data/lib/polytrix/command/list.rb +53 -0
- data/lib/polytrix/command/rundoc.rb +27 -0
- data/lib/polytrix/command/test.rb +24 -0
- data/lib/polytrix/command.rb +209 -0
- data/lib/polytrix/configuration.rb +30 -40
- data/lib/polytrix/core/file_system_helper.rb +2 -5
- data/lib/polytrix/core/hashie.rb +14 -0
- data/lib/polytrix/core/implementor.rb +52 -12
- data/lib/polytrix/core/manifest_section.rb +4 -0
- data/lib/polytrix/core/string_helpers.rb +15 -0
- data/lib/polytrix/documentation/helpers/code_helper.rb +3 -1
- data/lib/polytrix/error.rb +209 -0
- data/lib/polytrix/logger.rb +365 -8
- data/lib/polytrix/logging.rb +34 -0
- data/lib/polytrix/manifest.rb +40 -26
- data/lib/polytrix/result.rb +1 -0
- data/lib/polytrix/rspec.rb +7 -5
- data/lib/polytrix/runners/buff_shellout_executor.rb +19 -0
- data/lib/polytrix/runners/executor.rb +32 -0
- data/lib/polytrix/runners/mixlib_shellout_executor.rb +83 -0
- data/lib/polytrix/state_file.rb +60 -0
- data/lib/polytrix/util.rb +155 -0
- data/lib/polytrix/validation.rb +1 -1
- data/lib/polytrix/validator.rb +9 -5
- data/lib/polytrix/version.rb +1 -1
- data/lib/polytrix.rb +55 -33
- data/polytrix.gemspec +4 -2
- data/polytrix.rb +0 -5
- data/{polytrix_tests.yml → polytrix.yml} +5 -0
- data/samples/default_bootstrap.rb +0 -7
- data/samples/polytrix.rb +0 -9
- data/samples/{polytrix_tests.yml → polytrix.yml} +11 -0
- data/samples/polytrix_cli.sh +1 -1
- data/spec/fabricators/implementor_fabricator.rb +20 -0
- data/spec/fabricators/manifest_fabricator.rb +4 -1
- data/spec/fixtures/{polytrix_tests.yml → polytrix.yml} +10 -0
- data/spec/polytrix/challenge_runner_spec.rb +3 -2
- data/spec/polytrix/challenge_spec.rb +5 -4
- data/spec/polytrix/cli_spec.rb +23 -26
- data/spec/polytrix/configuration_spec.rb +4 -43
- data/spec/polytrix/documentation/helpers/code_helper_spec.rb +1 -1
- data/spec/polytrix/documentation_generator_spec.rb +2 -0
- data/spec/polytrix/implementor_spec.rb +44 -2
- data/spec/polytrix/manifest_spec.rb +7 -4
- data/spec/polytrix_spec.rb +9 -11
- data/spec/thor_spy.rb +2 -0
- metadata +66 -16
- data/features/fixtures/spec/polytrix_merge.rb +0 -5
- data/features/reporting.feature +0 -140
- data/lib/polytrix/executor.rb +0 -89
- data/samples/sdks/custom/polytrix.yml +0 -2
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Polytrix
|
4
|
+
module Command
|
5
|
+
class Base
|
6
|
+
include Polytrix::DefaultLogger
|
7
|
+
include Polytrix::Logging
|
8
|
+
include Polytrix::Core::FileSystemHelper
|
9
|
+
|
10
|
+
# Need standard executor...
|
11
|
+
SUPPORTED_EXTENSIONS = %w(py rb js)
|
12
|
+
|
13
|
+
# Contstructs a new Command object.
|
14
|
+
#
|
15
|
+
# @param cmd_args [Array] remainder of the arguments from processed ARGV
|
16
|
+
# @param cmd_options [Hash] hash of Thor options
|
17
|
+
# @param options [Hash] configuration options
|
18
|
+
# @option options [String] :action action to take, usually corresponding
|
19
|
+
# to the subcommand name (default: `nil`)
|
20
|
+
# @option options [proc] :help a callable that displays help for the
|
21
|
+
# command
|
22
|
+
# @option options [Config] :test_dir a Config object (default: `nil`)
|
23
|
+
# @option options [Loader] :loader a Loader object (default: `nil`)
|
24
|
+
# @option options [String] :shell a Thor shell object
|
25
|
+
def initialize(cmd_args, cmd_options, options = {})
|
26
|
+
@args = cmd_args
|
27
|
+
@options = cmd_options
|
28
|
+
@action = options.fetch(:action, nil)
|
29
|
+
@help = options.fetch(:help, -> { 'No help provided' })
|
30
|
+
@manifest_file = options.fetch('manifest', nil)
|
31
|
+
@test_dir = options.fetch('test_dir', nil)
|
32
|
+
@loader = options.fetch(:loader, nil)
|
33
|
+
@shell = options.fetch(:shell)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# @return [Array] remainder of the arguments from processed ARGV
|
39
|
+
# @api private
|
40
|
+
attr_reader :args
|
41
|
+
|
42
|
+
# @return [Hash] hash of Thor options
|
43
|
+
# @api private
|
44
|
+
attr_reader :options
|
45
|
+
|
46
|
+
# @return [proc] a callable that displays help for the command
|
47
|
+
# @api private
|
48
|
+
attr_reader :help
|
49
|
+
|
50
|
+
# @return [Config] a Config object
|
51
|
+
# @api private
|
52
|
+
attr_reader :test_dir
|
53
|
+
|
54
|
+
# @return [Thor::Shell] a Thor shell object
|
55
|
+
# @api private
|
56
|
+
attr_reader :shell
|
57
|
+
|
58
|
+
# @return [String] the action to perform
|
59
|
+
# @api private
|
60
|
+
attr_reader :action
|
61
|
+
|
62
|
+
def setup
|
63
|
+
manifest_file = File.expand_path @manifest_file
|
64
|
+
if File.exists? manifest_file
|
65
|
+
logger.debug "Loading manifest file: #{manifest_file}"
|
66
|
+
Polytrix.configuration.manifest = @manifest_file
|
67
|
+
elsif @options.solo
|
68
|
+
solo_setup
|
69
|
+
else
|
70
|
+
fail StandardError, "No manifest found at #{manifest_file} and not using --solo mode"
|
71
|
+
end
|
72
|
+
|
73
|
+
Polytrix.configuration.documentation_dir = options[:target_dir]
|
74
|
+
Polytrix.configuration.documentation_format = options[:format]
|
75
|
+
|
76
|
+
manifest.build_challenges
|
77
|
+
|
78
|
+
test_dir = @test_dir.nil? ? nil : File.expand_path(@test_dir)
|
79
|
+
if test_dir && File.directory?(test_dir)
|
80
|
+
$LOAD_PATH.unshift test_dir
|
81
|
+
Dir["#{test_dir}/**/*.rb"].each do | file_to_require |
|
82
|
+
require relativize(file_to_require, test_dir).to_s.gsub('.rb', '')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def solo_setup
|
88
|
+
suites = {}
|
89
|
+
solo_basedir = @options.solo
|
90
|
+
solo_glob = @options.fetch(:solo_glob, "**/*.{#{SUPPORTED_EXTENSIONS.join(',')}}")
|
91
|
+
Dir[File.join(solo_basedir, solo_glob)].each do | code_sample |
|
92
|
+
code_sample = Pathname.new(code_sample)
|
93
|
+
suite_name = relativize(code_sample.dirname, solo_basedir).to_s
|
94
|
+
scenario_name = code_sample.basename(code_sample.extname).to_s
|
95
|
+
suite = suites[suite_name] ||= Polytrix::Manifest::Suite.new(samples: [])
|
96
|
+
suite.samples << scenario_name
|
97
|
+
end
|
98
|
+
@manifest = Polytrix.configuration.manifest = Polytrix::Manifest.new(
|
99
|
+
implementors: {
|
100
|
+
File.basename(solo_basedir) => {
|
101
|
+
basedir: solo_basedir
|
102
|
+
}
|
103
|
+
},
|
104
|
+
suites: suites
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
def manifest
|
109
|
+
@manifest ||= Polytrix.configuration.manifest
|
110
|
+
@manifest
|
111
|
+
end
|
112
|
+
|
113
|
+
# Emit an error message, display contextual help and then exit with a
|
114
|
+
# non-zero exit code.
|
115
|
+
#
|
116
|
+
# **Note** This method calls exit and will not return.
|
117
|
+
#
|
118
|
+
# @param msg [String] error message
|
119
|
+
# @api private
|
120
|
+
def die(msg)
|
121
|
+
logger.error "\n#{msg}\n\n"
|
122
|
+
help.call
|
123
|
+
exit 1
|
124
|
+
end
|
125
|
+
|
126
|
+
# @return [Array<Scenario>] an array of scenarios
|
127
|
+
# @raise [SystemExit] if no scenario are returned
|
128
|
+
# @api private
|
129
|
+
def all_scenarios
|
130
|
+
result = manifest.challenges.values
|
131
|
+
|
132
|
+
if result.empty?
|
133
|
+
die 'No scenarios defined'
|
134
|
+
else
|
135
|
+
result
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Return an array on scenarios whos name matches the regular expression.
|
140
|
+
#
|
141
|
+
# @param regexp [Regexp] a regular expression matching on instance names
|
142
|
+
# @return [Array<Instance>] an array of scenarios
|
143
|
+
# @raise [SystemExit] if no scenarios are returned or the regular
|
144
|
+
# expression is invalid
|
145
|
+
# @api private
|
146
|
+
def filtered_scenarios(regexp)
|
147
|
+
result = begin
|
148
|
+
manifest.challenges.get(regexp) ||
|
149
|
+
manifest.challenges.get_all(/#{regexp}/)
|
150
|
+
rescue RegexpError => e
|
151
|
+
die "Invalid Ruby regular expression, " \
|
152
|
+
"you may need to single quote the argument. " \
|
153
|
+
"Please try again or consult http://rubular.com/ (#{e.message})"
|
154
|
+
end
|
155
|
+
result = [result] unless result.is_a? Array
|
156
|
+
|
157
|
+
if result.empty?
|
158
|
+
die "No scenarios for regex `#{regexp}', try running `polytrix list'"
|
159
|
+
else
|
160
|
+
result
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Return an array on scenarios whos name matches the regular expression,
|
165
|
+
# the full instance name, or the `"all"` literal.
|
166
|
+
#
|
167
|
+
# @param arg [String] an instance name, a regular expression, the literal
|
168
|
+
# `"all"`, or `nil`
|
169
|
+
# @return [Array<Instance>] an array of scenarios
|
170
|
+
# @api private
|
171
|
+
def parse_subcommand(arg = nil)
|
172
|
+
arg ||= 'all'
|
173
|
+
arg == 'all' ? all_scenarios : filtered_scenarios(arg)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Common module to execute a Polytrix action such as create, converge, etc.
|
178
|
+
module RunAction
|
179
|
+
# Run an instance action (create, converge, setup, verify, destroy) on
|
180
|
+
# a collection of scenarios. The instance actions will take place in a
|
181
|
+
# seperate thread of execution which may or may not be running
|
182
|
+
# concurrently.
|
183
|
+
#
|
184
|
+
# @param action [String] action to perform
|
185
|
+
# @param scenarios [Array<Instance>] an array of scenarios
|
186
|
+
def run_action(action, scenarios, *args)
|
187
|
+
concurrency = 1
|
188
|
+
if options[:concurrency]
|
189
|
+
concurrency = options[:concurrency] || scenarios.size
|
190
|
+
concurrency = scenarios.size if concurrency > scenarios.size
|
191
|
+
end
|
192
|
+
|
193
|
+
queue = Queue.new
|
194
|
+
scenarios.each { |i| queue << i }
|
195
|
+
concurrency.times { queue << nil }
|
196
|
+
|
197
|
+
threads = []
|
198
|
+
concurrency.times do
|
199
|
+
threads << Thread.new do
|
200
|
+
while (instance = queue.pop)
|
201
|
+
instance.public_send(action, *args)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
threads.map { |i| i.join }
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -1,7 +1,4 @@
|
|
1
1
|
require 'middleware'
|
2
|
-
require 'logger'
|
3
|
-
require 'hashie/dash'
|
4
|
-
require 'hashie/extensions/coercion'
|
5
2
|
|
6
3
|
module Polytrix
|
7
4
|
RESOURCES_DIR = File.expand_path '../../../resources', __FILE__
|
@@ -20,60 +17,40 @@ module Polytrix
|
|
20
17
|
end
|
21
18
|
end
|
22
19
|
|
23
|
-
class Configuration <
|
24
|
-
include Hashie::Extensions::Coercion
|
25
|
-
|
20
|
+
class Configuration < Polytrix::ManifestSection
|
26
21
|
property :dry_run, default: false
|
27
|
-
property :
|
22
|
+
property :log_root, default: '.polytrix/logs'
|
23
|
+
property :log_level, default: :info
|
28
24
|
property :middleware, default: Polytrix::Runners::Middleware::STANDARD_MIDDLEWARE
|
29
25
|
property :implementors, default: []
|
30
26
|
# coerce_key :implementors, Polytrix::Implementor
|
31
27
|
property :suppress_output, default: false
|
32
28
|
property :default_doc_template
|
33
29
|
property :template_dir, default: "#{RESOURCES_DIR}"
|
30
|
+
property :documentation_dir, default: 'docs/'
|
31
|
+
property :documentation_format, default: 'md'
|
34
32
|
# Extra options for rspec
|
35
33
|
property :rspec_options, default: ''
|
36
34
|
|
37
|
-
def
|
38
|
-
@
|
39
|
-
levels = {
|
40
|
-
'fatal' => ::Logger::FATAL,
|
41
|
-
'error' => ::Logger::ERROR,
|
42
|
-
'warn' => ::Logger::WARN,
|
43
|
-
'info' => ::Logger::INFO,
|
44
|
-
'debug' => ::Logger::DEBUG
|
45
|
-
}
|
46
|
-
fail "Unknown log level: #{log_level}" unless levels.keys.include? log_level
|
47
|
-
logger.level = levels[log_level]
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def test_manifest
|
52
|
-
@test_manifest ||= Manifest.from_yaml 'polytrix_tests.yml'
|
35
|
+
def default_logger
|
36
|
+
@default_logger ||= Logger.new(stdout: $stdout, level: env_log)
|
53
37
|
end
|
54
38
|
|
55
|
-
def
|
56
|
-
@
|
39
|
+
def manifest
|
40
|
+
@manifest ||= load_manifest('polytrix.yml')
|
57
41
|
end
|
58
42
|
|
59
|
-
def
|
60
|
-
if
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
else # load from filesystem
|
65
|
-
folder = metadata
|
66
|
-
fail ArgumentError, "#{folder} is not a directory" unless File.directory? folder
|
67
|
-
settings_file = File.expand_path('polytrix.yml', folder)
|
68
|
-
if File.exist? settings_file
|
69
|
-
settings = YAML.load(File.read(settings_file))
|
70
|
-
Polytrix.configuration.implementor(settings.merge(basedir: folder))
|
71
|
-
else
|
72
|
-
Polytrix.configuration.implementor name: File.basename(folder), basedir: folder
|
73
|
-
end
|
43
|
+
def manifest=(manifest_data)
|
44
|
+
if manifest_data.is_a? Manifest
|
45
|
+
@manifest = manifest_data
|
46
|
+
else
|
47
|
+
@manifest = Manifest.from_yaml manifest_data
|
74
48
|
end
|
49
|
+
@manifest
|
75
50
|
end
|
76
51
|
|
52
|
+
alias_method :load_manifest, :manifest=
|
53
|
+
|
77
54
|
# The callback used to validate code samples that
|
78
55
|
# don't have a custom validator. The default
|
79
56
|
# checks that the sample code runs successfully.
|
@@ -88,5 +65,18 @@ module Polytrix
|
|
88
65
|
end
|
89
66
|
|
90
67
|
attr_writer :default_validator_callback
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Determine the default log level from an environment variable, if it is
|
72
|
+
# set.
|
73
|
+
#
|
74
|
+
# @return [Integer,nil] a log level or nil if not set
|
75
|
+
# @api private
|
76
|
+
def env_log
|
77
|
+
level = ENV['POLYTRIX_LOG'] && ENV['POLYTRIX_LOG'].downcase.to_sym
|
78
|
+
level = Polytrix::Util.to_logger_level(level) unless level.nil?
|
79
|
+
level
|
80
|
+
end
|
91
81
|
end
|
92
82
|
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
module Polytrix
|
2
2
|
module Core
|
3
3
|
module FileSystemHelper
|
4
|
-
include Polytrix::
|
4
|
+
include Polytrix::Logging
|
5
|
+
include Polytrix::StringHelpers
|
5
6
|
class FileNotFound < StandardError; end
|
6
7
|
|
7
8
|
# Finds a file by loosely matching the file name to a scenario name
|
@@ -20,10 +21,6 @@ module Polytrix
|
|
20
21
|
Pathname.new file
|
21
22
|
end
|
22
23
|
|
23
|
-
def slugify(path)
|
24
|
-
path.downcase.gsub(' ', '_')
|
25
|
-
end
|
26
|
-
|
27
24
|
def recursive_parent_search(path, file_name = nil, &block)
|
28
25
|
if block_given?
|
29
26
|
obj = yield path
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'hashie/dash'
|
2
|
+
require 'hashie/mash'
|
3
|
+
require 'hashie/extensions/coercion'
|
4
|
+
|
5
|
+
module Polytrix
|
6
|
+
class Dash < Hashie::Dash
|
7
|
+
include Hashie::Extensions::Coercion
|
8
|
+
|
9
|
+
def initialize(hash = {})
|
10
|
+
mash = Hashie::Mash.new(hash)
|
11
|
+
super mash.to_hash(symbolize_keys: true)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -7,39 +7,79 @@ module Polytrix
|
|
7
7
|
super "Feature #{feature} is not implemented"
|
8
8
|
end
|
9
9
|
end
|
10
|
-
class Implementor <
|
11
|
-
|
10
|
+
class Implementor < Polytrix::ManifestSection
|
11
|
+
class GitOptions < Polytrix::ManifestSection
|
12
|
+
property :repo, required: true
|
13
|
+
property :branch
|
14
|
+
property :to
|
15
|
+
|
16
|
+
def initialize(data)
|
17
|
+
data = { repo: data } if data.is_a? String
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
include Polytrix::Logging
|
12
23
|
include Polytrix::Core::FileSystemHelper
|
13
|
-
include
|
14
|
-
include Polytrix::Executor
|
24
|
+
include Polytrix::Runners::Executor
|
15
25
|
property :name
|
16
26
|
property :basedir, required: true
|
17
27
|
property :language
|
18
28
|
coerce_key :basedir, Pathname
|
29
|
+
property :git
|
30
|
+
coerce_key :git, GitOptions
|
19
31
|
|
20
32
|
def initialize(data)
|
21
|
-
data = Hashie::Mash.new data
|
22
|
-
data[:name] ||= File.basename data[:basedir]
|
23
33
|
data[:basedir] = File.absolute_path(data[:basedir])
|
24
|
-
super
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
def logger
|
38
|
+
@logger ||= Polytrix::Logger.new_logger(self)
|
39
|
+
end
|
40
|
+
|
41
|
+
def clone
|
42
|
+
# Logging.mdc['implementor'] = name
|
43
|
+
if git.nil? || git.repo.nil?
|
44
|
+
logger.info 'Skipping clone because there are no git options'
|
45
|
+
return
|
46
|
+
end
|
47
|
+
branch = git.branch ||= 'master'
|
48
|
+
target_dir = git.to ||= basedir
|
49
|
+
if File.exists? target_dir
|
50
|
+
logger.info "Skipping clone because #{target_dir} already exists"
|
51
|
+
else
|
52
|
+
clone_cmd = "git clone #{git.repo} -b #{branch} #{target_dir}"
|
53
|
+
logger.info "Cloning: #{clone_cmd}"
|
54
|
+
execute clone_cmd
|
55
|
+
end
|
25
56
|
end
|
26
57
|
|
27
58
|
def bootstrap
|
59
|
+
# Logging.mdc['implementor'] = name
|
60
|
+
banner "Bootstrapping #{name}"
|
61
|
+
fail "Implementor #{name} has not been cloned" unless cloned?
|
62
|
+
|
28
63
|
execute('./scripts/bootstrap', cwd: basedir, prefix: name)
|
29
64
|
rescue Errno::ENOENT
|
30
65
|
logger.warn "Skipping bootstrapping for #{name}, no script/bootstrap exists"
|
31
66
|
end
|
32
67
|
|
33
68
|
def build_challenge(challenge_data)
|
34
|
-
challenge_data[:source_file] ||= find_file basedir, challenge_data[:name]
|
35
69
|
challenge_data[:basedir] ||= basedir
|
36
|
-
challenge_data[:source_file] = relativize(challenge_data[:source_file], challenge_data[:basedir])
|
37
70
|
challenge_data[:implementor] ||= self
|
38
71
|
challenge_data[:suite] ||= ''
|
39
|
-
|
72
|
+
begin
|
73
|
+
challenge_data[:source_file] ||= find_file basedir, challenge_data[:name]
|
74
|
+
challenge_data[:source_file] = relativize(challenge_data[:source_file], challenge_data[:basedir])
|
75
|
+
rescue Polytrix::Core::FileSystemHelper::FileNotFound
|
76
|
+
challenge_data[:source_file] = nil
|
77
|
+
end
|
40
78
|
Challenge.new challenge_data
|
41
|
-
|
42
|
-
|
79
|
+
end
|
80
|
+
|
81
|
+
def cloned?
|
82
|
+
File.directory? basedir
|
43
83
|
end
|
44
84
|
end
|
45
85
|
end
|
@@ -18,9 +18,11 @@ module Polytrix
|
|
18
18
|
class MarkdownHelper
|
19
19
|
def self.code_block(source, language)
|
20
20
|
buffer = StringIO.new
|
21
|
+
buffer.puts # I've seen lots of rendering issues without a dividing newline
|
21
22
|
buffer.puts "```#{language}"
|
22
23
|
buffer.puts source
|
23
24
|
buffer.puts '```'
|
25
|
+
buffer.puts # Put a dividing newline after as well, to be safe...
|
24
26
|
buffer.string
|
25
27
|
end
|
26
28
|
end
|
@@ -30,7 +32,7 @@ module Polytrix
|
|
30
32
|
end
|
31
33
|
|
32
34
|
def source
|
33
|
-
File.read
|
35
|
+
File.read absolute_source_file
|
34
36
|
end
|
35
37
|
|
36
38
|
def code_block(source_code, language, opts = { format: :markdown })
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'English'
|
2
|
+
|
3
|
+
module Polytrix
|
4
|
+
# All Polytrix errors and exceptions.
|
5
|
+
module Error
|
6
|
+
# Creates an array of strings, representing a formatted exception,
|
7
|
+
# containing backtrace and nested exception info as necessary, that can
|
8
|
+
# be viewed by a human.
|
9
|
+
#
|
10
|
+
# For example:
|
11
|
+
#
|
12
|
+
# ------Exception-------
|
13
|
+
# Class: Polytrix::StandardError
|
14
|
+
# Message: Failure starting the party
|
15
|
+
# ---Nested Exception---
|
16
|
+
# Class: IOError
|
17
|
+
# Message: not enough directories for a party
|
18
|
+
# ------Backtrace-------
|
19
|
+
# nil
|
20
|
+
# ----------------------
|
21
|
+
#
|
22
|
+
# @param exception [::StandardError] an exception
|
23
|
+
# @return [Array<String>] a formatted message
|
24
|
+
def self.formatted_trace(exception)
|
25
|
+
arr = formatted_exception(exception).dup
|
26
|
+
last = arr.pop
|
27
|
+
if exception.respond_to?(:original) && exception.original
|
28
|
+
arr += formatted_exception(exception.original, 'Nested Exception')
|
29
|
+
last = arr.pop
|
30
|
+
end
|
31
|
+
arr += ['Backtrace'.center(22, '-'), exception.backtrace, last].flatten
|
32
|
+
arr
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates an array of strings, representing a formatted exception that
|
36
|
+
# can be viewed by a human. Thanks to MiniTest for the inspiration
|
37
|
+
# upon which this output has been designed.
|
38
|
+
#
|
39
|
+
# For example:
|
40
|
+
#
|
41
|
+
# ------Exception-------
|
42
|
+
# Class: Polytrix::StandardError
|
43
|
+
# Message: I have failed you
|
44
|
+
# ----------------------
|
45
|
+
#
|
46
|
+
# @param exception [::StandardError] an exception
|
47
|
+
# @param title [String] a custom title for the message
|
48
|
+
# (default: `"Exception"`)
|
49
|
+
# @return [Array<String>] a formatted message
|
50
|
+
def self.formatted_exception(exception, title = 'Exception')
|
51
|
+
[
|
52
|
+
title.center(22, '-'),
|
53
|
+
"Class: #{exception.class}",
|
54
|
+
"Message: #{exception.message}",
|
55
|
+
''.center(22, '-')
|
56
|
+
]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Base exception class from which all Polytrix exceptions derive. This class
|
61
|
+
# nests an exception when this class is re-raised from a rescue block.
|
62
|
+
class StandardError < ::StandardError
|
63
|
+
include Error
|
64
|
+
|
65
|
+
# @return [::StandardError] the original (wrapped) exception
|
66
|
+
attr_reader :original
|
67
|
+
|
68
|
+
# Creates a new StandardError exception which optionally wraps an original
|
69
|
+
# exception if given or detected by checking the `$!` global variable.
|
70
|
+
#
|
71
|
+
# @param msg [String] exception message
|
72
|
+
# @param original [::StandardError] an original exception which will be
|
73
|
+
# wrapped (default: `$ERROR_INFO`)
|
74
|
+
def initialize(msg, original = $ERROR_INFO)
|
75
|
+
super(msg)
|
76
|
+
@original = original
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Base exception class for all exceptions that are caused by user input
|
81
|
+
# errors.
|
82
|
+
class UserError < StandardError; end
|
83
|
+
|
84
|
+
# Base exception class for all exceptions that are caused by incorrect use
|
85
|
+
# of an API.
|
86
|
+
class ClientError < StandardError; end
|
87
|
+
|
88
|
+
# Base exception class for exceptions that are caused by external library
|
89
|
+
# failures which may be temporary.
|
90
|
+
class TransientFailure < StandardError; end
|
91
|
+
|
92
|
+
# Exception class for any exceptions raised when performing an challenge
|
93
|
+
# action.
|
94
|
+
class ActionFailed < TransientFailure; end
|
95
|
+
|
96
|
+
# Exception class capturing what caused an challenge to die.
|
97
|
+
class ChallengeFailure < TransientFailure; end
|
98
|
+
|
99
|
+
class ExecutionError < TransientFailure
|
100
|
+
attr_accessor :execution_result
|
101
|
+
end
|
102
|
+
|
103
|
+
# Yields to a code block in order to consistently emit a useful crash/error
|
104
|
+
# message and exit appropriately. There are two primary failure conditions:
|
105
|
+
# an expected challenge failure, and any other unexpected failures.
|
106
|
+
#
|
107
|
+
# **Note** This method may call `Kernel.exit` so may not return if the
|
108
|
+
# yielded code block raises an exception.
|
109
|
+
#
|
110
|
+
# ## Challenge Failure
|
111
|
+
#
|
112
|
+
# This is an expected failure scenario which could happen if an challenge
|
113
|
+
# couldn't be created, a Chef run didn't successfully converge, a
|
114
|
+
# post-convergence test suite failed, etc. In other words, you can count on
|
115
|
+
# encountering these failures all the time--this is Polytrix's worldview:
|
116
|
+
# crash early and often. In this case a cleanly formatted exception is
|
117
|
+
# written to `STDERR` and the exception message is written to
|
118
|
+
# the common Polytrix file logger.
|
119
|
+
#
|
120
|
+
# ## Unexpected Failure
|
121
|
+
#
|
122
|
+
# All other forms of `Polytrix::Error` exceptions are considered unexpected
|
123
|
+
# or unplanned exceptions, typically from user configuration errors, driver
|
124
|
+
# or provisioner coding issues or bugs, or internal code issues. Given
|
125
|
+
# a stable release of Polytrix and a solid set of drivers and provisioners,
|
126
|
+
# the most likely cause of this is user configuration error originating in
|
127
|
+
# the `.polytrix.yml` setup. For this reason, the exception is written to
|
128
|
+
# `STDERR`, a full formatted exception trace is written to the common
|
129
|
+
# Polytrix file logger, and a message is displayed on `STDERR` to the user
|
130
|
+
# informing them to check the log files and check their configuration with
|
131
|
+
# the `polytrix diagnose` subcommand.
|
132
|
+
#
|
133
|
+
# @raise [SystemExit] if an exception is raised in the yielded block
|
134
|
+
def self.with_friendly_errors
|
135
|
+
yield
|
136
|
+
rescue Polytrix::ChallengeFailure => e
|
137
|
+
Polytrix.mutex.synchronize do
|
138
|
+
handle_challenge_failure(e)
|
139
|
+
end
|
140
|
+
exit 10
|
141
|
+
rescue Polytrix::Error => e
|
142
|
+
Polytrix.mutex.synchronize do
|
143
|
+
handle_error(e)
|
144
|
+
end
|
145
|
+
exit 20
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
# Writes an array of lines to the common Polytrix logger's file device at the
|
151
|
+
# given severity level. If the Polytrix logger is set to debug severity, then
|
152
|
+
# the array of lines will also be written to the console output.
|
153
|
+
#
|
154
|
+
# @param level [Symbol,String] the desired log level
|
155
|
+
# @param lines [Array<String>] an array of strings to log
|
156
|
+
# @api private
|
157
|
+
def self.file_log(level, lines)
|
158
|
+
Array(lines).each do |line|
|
159
|
+
if Polytrix.logger.debug?
|
160
|
+
Polytrix.logger.debug(line)
|
161
|
+
else
|
162
|
+
Polytrix.logger.logdev && Polytrix.logger.logdev.public_send(level, line)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Writes an array of lines to the `STDERR` device.
|
168
|
+
#
|
169
|
+
# @param lines [Array<String>] an array of strings to log
|
170
|
+
# @api private
|
171
|
+
def self.stderr_log(lines)
|
172
|
+
Array(lines).each do |line|
|
173
|
+
$stderr.puts(Color.colorize(">>>>>> #{line}", :red))
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Writes an array of lines to the common Polytrix debugger with debug
|
178
|
+
# severity.
|
179
|
+
#
|
180
|
+
# @param lines [Array<String>] an array of strings to log
|
181
|
+
# @api private
|
182
|
+
def self.debug_log(lines)
|
183
|
+
Array(lines).each { |line| Polytrix.logger.debug(line) }
|
184
|
+
end
|
185
|
+
|
186
|
+
# Handles an challenge failure exception.
|
187
|
+
#
|
188
|
+
# @param e [StandardError] an exception to handle
|
189
|
+
# @see Polytrix.with_friendly_errors
|
190
|
+
# @api private
|
191
|
+
def self.handle_challenge_failure(e)
|
192
|
+
stderr_log(e.message.split(/\s{2,}/))
|
193
|
+
stderr_log(Error.formatted_exception(e.original))
|
194
|
+
file_log(:error, e.message.split(/\s{2,}/).first)
|
195
|
+
debug_log(Error.formatted_trace(e))
|
196
|
+
end
|
197
|
+
|
198
|
+
# Handles an unexpected failure exception.
|
199
|
+
#
|
200
|
+
# @param e [StandardError] an exception to handle
|
201
|
+
# @see Polytrix.with_friendly_errors
|
202
|
+
# @api private
|
203
|
+
def self.handle_error(e)
|
204
|
+
stderr_log(Error.formatted_exception(e))
|
205
|
+
stderr_log('Please see .polytrix/logs/polytrix.log for more details')
|
206
|
+
# stderr_log("Also try running `polytrix diagnose --all` for configuration\n")
|
207
|
+
file_log(:error, Error.formatted_trace(e))
|
208
|
+
end
|
209
|
+
end
|