polytrix 0.1.0.pre → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|