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.
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