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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +7 -2
  4. data/.travis.yml +2 -1
  5. data/Gemfile +1 -0
  6. data/README.md +190 -98
  7. data/Rakefile +8 -6
  8. data/bin/polytrix +2 -1
  9. data/docs/samples/code2doc/java/HelloWorld.md +4 -0
  10. data/docs/samples/code2doc/java/Quine.md +2 -0
  11. data/docs/samples/code2doc/python/hello_world.md +2 -0
  12. data/docs/samples/code2doc/python/quine.md +2 -0
  13. data/docs/samples/code2doc/ruby/hello_world.md +4 -0
  14. data/features/bootstrapping.feature +36 -0
  15. data/features/cloning.feature +34 -0
  16. data/features/execution.feature +2 -16
  17. data/features/fixtures/configs/empty.yml +12 -1
  18. data/features/fixtures/configs/hello_world.yml +11 -1
  19. data/features/fixtures/spec/polytrix_spec.rb +1 -4
  20. data/features/solo.feature +12 -0
  21. data/features/states.feature +40 -0
  22. data/features/step_definitions/sdk_steps.rb +11 -1
  23. data/features/support/env.rb +2 -1
  24. data/lib/polytrix/challenge.rb +211 -13
  25. data/lib/polytrix/challenge_result.rb +9 -0
  26. data/lib/polytrix/challenge_runner.rb +4 -11
  27. data/lib/polytrix/challenges.rb +16 -0
  28. data/lib/polytrix/cli/report.rb +0 -4
  29. data/lib/polytrix/cli.rb +229 -137
  30. data/lib/polytrix/color.rb +40 -0
  31. data/lib/polytrix/command/action.rb +26 -0
  32. data/lib/polytrix/command/list.rb +53 -0
  33. data/lib/polytrix/command/rundoc.rb +27 -0
  34. data/lib/polytrix/command/test.rb +24 -0
  35. data/lib/polytrix/command.rb +209 -0
  36. data/lib/polytrix/configuration.rb +30 -40
  37. data/lib/polytrix/core/file_system_helper.rb +2 -5
  38. data/lib/polytrix/core/hashie.rb +14 -0
  39. data/lib/polytrix/core/implementor.rb +52 -12
  40. data/lib/polytrix/core/manifest_section.rb +4 -0
  41. data/lib/polytrix/core/string_helpers.rb +15 -0
  42. data/lib/polytrix/documentation/helpers/code_helper.rb +3 -1
  43. data/lib/polytrix/error.rb +209 -0
  44. data/lib/polytrix/logger.rb +365 -8
  45. data/lib/polytrix/logging.rb +34 -0
  46. data/lib/polytrix/manifest.rb +40 -26
  47. data/lib/polytrix/result.rb +1 -0
  48. data/lib/polytrix/rspec.rb +7 -5
  49. data/lib/polytrix/runners/buff_shellout_executor.rb +19 -0
  50. data/lib/polytrix/runners/executor.rb +32 -0
  51. data/lib/polytrix/runners/mixlib_shellout_executor.rb +83 -0
  52. data/lib/polytrix/state_file.rb +60 -0
  53. data/lib/polytrix/util.rb +155 -0
  54. data/lib/polytrix/validation.rb +1 -1
  55. data/lib/polytrix/validator.rb +9 -5
  56. data/lib/polytrix/version.rb +1 -1
  57. data/lib/polytrix.rb +55 -33
  58. data/polytrix.gemspec +4 -2
  59. data/polytrix.rb +0 -5
  60. data/{polytrix_tests.yml → polytrix.yml} +5 -0
  61. data/samples/default_bootstrap.rb +0 -7
  62. data/samples/polytrix.rb +0 -9
  63. data/samples/{polytrix_tests.yml → polytrix.yml} +11 -0
  64. data/samples/polytrix_cli.sh +1 -1
  65. data/spec/fabricators/implementor_fabricator.rb +20 -0
  66. data/spec/fabricators/manifest_fabricator.rb +4 -1
  67. data/spec/fixtures/{polytrix_tests.yml → polytrix.yml} +10 -0
  68. data/spec/polytrix/challenge_runner_spec.rb +3 -2
  69. data/spec/polytrix/challenge_spec.rb +5 -4
  70. data/spec/polytrix/cli_spec.rb +23 -26
  71. data/spec/polytrix/configuration_spec.rb +4 -43
  72. data/spec/polytrix/documentation/helpers/code_helper_spec.rb +1 -1
  73. data/spec/polytrix/documentation_generator_spec.rb +2 -0
  74. data/spec/polytrix/implementor_spec.rb +44 -2
  75. data/spec/polytrix/manifest_spec.rb +7 -4
  76. data/spec/polytrix_spec.rb +9 -11
  77. data/spec/thor_spy.rb +2 -0
  78. metadata +66 -16
  79. data/features/fixtures/spec/polytrix_merge.rb +0 -5
  80. data/features/reporting.feature +0 -140
  81. data/lib/polytrix/executor.rb +0 -89
  82. 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 < Hashie::Dash
24
- include Hashie::Extensions::Coercion
25
-
20
+ class Configuration < Polytrix::ManifestSection
26
21
  property :dry_run, default: false
27
- property :log_level, default: 'info'
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 logger
38
- @logger ||= ::Logger.new($stdout).tap do |logger|
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 test_manifest=(yaml_file)
56
- @test_manifest = Manifest.from_yaml yaml_file
39
+ def manifest
40
+ @manifest ||= load_manifest('polytrix.yml')
57
41
  end
58
42
 
59
- def implementor(metadata)
60
- if metadata.is_a? Hash # load from data
61
- Implementor.new(metadata).tap do |implementor|
62
- implementors << implementor
63
- end
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::Logger
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 < Hashie::Dash
11
- include Polytrix::Logger
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 Hashie::Extensions::Coercion
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(data)
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
- fail FeatureNotImplementedError, "#{name} is not setup" unless File.directory? challenge_data[:basedir]
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
- rescue Polytrix::Core::FileSystemHelper::FileNotFound
42
- raise FeatureNotImplementedError, challenge_data[:name]
79
+ end
80
+
81
+ def cloned?
82
+ File.directory? basedir
43
83
  end
44
84
  end
45
85
  end
@@ -0,0 +1,4 @@
1
+ module Polytrix
2
+ class ManifestSection < Polytrix::Dash
3
+ end
4
+ end
@@ -0,0 +1,15 @@
1
+ module Polytrix
2
+ module StringHelpers
3
+ module ClassMethods
4
+ def slugify(*string)
5
+ string.join('-').downcase.gsub(' ', '_')
6
+ end
7
+ end
8
+
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ include ClassMethods
14
+ end
15
+ 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 source_file
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