omnitest-core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bdb34e38c505f97708ebeea946cf17c9fd6c4c8e
4
+ data.tar.gz: 57c68bab3e68e34e8e11d28778521b0f168cafe4
5
+ SHA512:
6
+ metadata.gz: 57c9056f0e7b5dc6d09404c8ebc019989b415b998d6015850fe798cdf3596188781a5c5bba8d93f6d311914ee787efe526d11716809fc3ae5bcce8d536f95b85
7
+ data.tar.gz: f6accd7f908e093e1426af222caf8a153e6474be2abe4abf8a0e9fe32cc6dcbc4bcf043352249c86b67ed8299653200414755772658052a2089f0ad08ced2c9f
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rubocop.yml ADDED
@@ -0,0 +1 @@
1
+ inherit_from: .rubocop_todo.yml
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,30 @@
1
+ # This configuration was generated by `rubocop --auto-gen-config`
2
+ # on 2015-01-20 16:57:55 -0500 using RuboCop version 0.27.0.
3
+ # The point is for the user to remove these configuration records
4
+ # one by one as the offenses are removed from the code base.
5
+ # Note that changes in the inspected code, or installation of new
6
+ # versions of RuboCop, may require this file to be generated again.
7
+
8
+ # Offense count: 1
9
+ Metrics/AbcSize:
10
+ Max: 18
11
+
12
+ # Offense count: 10
13
+ # Configuration parameters: AllowURI, URISchemes.
14
+ Metrics/LineLength:
15
+ Max: 105
16
+
17
+ # Offense count: 4
18
+ # Configuration parameters: CountComments.
19
+ Metrics/MethodLength:
20
+ Max: 13
21
+
22
+ # Offense count: 14
23
+ Style/Documentation:
24
+ Enabled: false
25
+
26
+ # Offense count: 2
27
+ # Configuration parameters: MaxSlashes.
28
+ Style/RegexpLiteral:
29
+ Enabled: false
30
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in omnitest-core.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Max Lincoln
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ # Omnitest::Core
2
+
3
+ Shared code for omnitest projects.
4
+ See [omnitest](https://github.com/omnitest/omnitest)
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rubocop/rake_task'
3
+ require 'rake/notes/rake_task'
4
+ require 'rspec/core/rake_task'
5
+
6
+ task default: [:spec, :rubocop, :notes]
7
+
8
+ RSpec::Core::RakeTask.new('spec')
9
+ RuboCop::RakeTask.new(:rubocop) do |task|
10
+ # abort rake on failure
11
+ task.fail_on_error = true
12
+ end
@@ -0,0 +1,56 @@
1
+ require 'omnitest/core/version'
2
+ require 'logger'
3
+ require 'cause'
4
+ require 'thor'
5
+ require 'pathname'
6
+ require 'omnitest/errors'
7
+
8
+ module Omnitest
9
+ module Core
10
+ autoload :Configurable, 'omnitest/core/configurable'
11
+ autoload :Util, 'omnitest/core/util'
12
+ autoload :FileSystem, 'omnitest/core/file_system'
13
+ autoload :CLI, 'omnitest/core/cli'
14
+ autoload :Logger, 'omnitest/core/logger'
15
+ autoload :Logging, 'omnitest/core/logging'
16
+ autoload :DefaultLogger, 'omnitest/core/logging'
17
+ autoload :StdoutLogger, 'omnitest/core/logging'
18
+ autoload :LogdevLogger, 'omnitest/core/logging'
19
+ autoload :Color, 'omnitest/core/color'
20
+ autoload :Dash, 'omnitest/core/hashie'
21
+ autoload :Mash, 'omnitest/core/hashie'
22
+ end
23
+
24
+ include Omnitest::Core::Logger
25
+ include Omnitest::Core::Logging
26
+
27
+ class << self
28
+ # @return [Logger] the common Omnitest logger
29
+ attr_accessor :logger
30
+
31
+ # @return [Mutex] a common mutex for global coordination
32
+ attr_accessor :mutex
33
+
34
+ def basedir
35
+ @basedir ||= Dir.pwd
36
+ end
37
+
38
+ def logger
39
+ @logger ||= Core::StdoutLogger.new($stdout)
40
+ end
41
+
42
+ # Determine the default log level from an environment variable, if it is
43
+ # set.
44
+ #
45
+ # @return [Integer,nil] a log level or nil if not set
46
+ # @api private
47
+ def env_log
48
+ level = ENV['CROSSTEST_LOG'] && ENV['CROSSTEST_LOG'].downcase.to_sym
49
+ level = Util.to_logger_level(level) unless level.nil?
50
+ level
51
+ end
52
+
53
+ # Default log level verbosity
54
+ DEFAULT_LOG_LEVEL = :info
55
+ end
56
+ end
@@ -0,0 +1,70 @@
1
+ require 'English'
2
+ require 'thor'
3
+
4
+ module Omnitest
5
+ module Core
6
+ class CLI < Thor
7
+ # Common module to load and invoke a CLI-implementation agnostic command.
8
+ module PerformCommand
9
+ attr_reader :action
10
+
11
+ # Perform a scenario subcommand.
12
+ #
13
+ # @param task [String] action to take, usually corresponding to the
14
+ # subcommand name
15
+ # @param command [String] command class to create and invoke]
16
+ # @param args [Array] remainder arguments from processed ARGV
17
+ # (default: `nil`)
18
+ # @param additional_options [Hash] additional configuration needed to
19
+ # set up the command class (default: `{}`)
20
+ def perform(task, command, args = nil, additional_options = {})
21
+ require "omnitest/command/#{command}"
22
+
23
+ command_options = {
24
+ help: -> { help(task) },
25
+ test_dir: @test_dir,
26
+ shell: shell
27
+ }.merge(additional_options)
28
+
29
+ str_const = Thor::Util.camel_case(command)
30
+ klass = ::Omnitest::Command.const_get(str_const)
31
+ klass.new(task, args, options, command_options).call
32
+ end
33
+ end
34
+
35
+ include Core::Logging
36
+ include PerformCommand
37
+
38
+ class << self
39
+ # Override Thor's start to strip extra_args from ARGV before it's processed
40
+ attr_accessor :extra_args
41
+
42
+ def start(given_args = ARGV, config = {})
43
+ if given_args && (split_pos = given_args.index('--'))
44
+ @extra_args = given_args.slice(split_pos + 1, given_args.length).map do | arg |
45
+ # Restore quotes
46
+ arg.match(/\=/) ? restore_quotes(arg) : arg
47
+ end
48
+ given_args = given_args.slice(0, split_pos)
49
+ end
50
+ super given_args, config
51
+ end
52
+
53
+ private
54
+
55
+ def restore_quotes(arg)
56
+ lhs, rhs = arg.split('=')
57
+ lhs = "\"#{lhs}\"" if lhs.match(/\s/)
58
+ rhs = "\"#{rhs}\"" if rhs.match(/\s/)
59
+ [lhs, rhs].join('=')
60
+ end
61
+ end
62
+
63
+ no_commands do
64
+ def extra_args
65
+ self.class.extra_args
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,42 @@
1
+ module Omnitest
2
+ module Core
3
+ module Color
4
+ ANSI = {
5
+ reset: 0, black: 30, red: 31, green: 32, yellow: 33,
6
+ blue: 34, magenta: 35, cyan: 36, white: 37,
7
+ bright_black: 90, bright_red: 91, bright_green: 92,
8
+ bright_yellow: 93, bright_blue: 94, bright_magenta: 95,
9
+ bright_cyan: 96, bright_white: 97
10
+ }.freeze
11
+
12
+ COLORS = %w(
13
+ cyan yellow green magenta blue bright_cyan bright_yellow
14
+ bright_green bright_magenta bright_blue
15
+ ).freeze
16
+
17
+ # Returns an ansi escaped string representing a color control sequence.
18
+ #
19
+ # @param name [Symbol] a valid color representation, taken from
20
+ # Omnitest::Color::ANSI
21
+ # @return [String] an ansi escaped string if the color is valid and an
22
+ # empty string otherwise
23
+ def self.escape(name)
24
+ return '' if name.nil?
25
+ return '' unless ANSI[name]
26
+ "\e[#{ANSI[name]}m"
27
+ end
28
+
29
+ # Returns a colorized ansi escaped string with the given color.
30
+ #
31
+ # @param str [String] a string to colorize
32
+ # @param name [Symbol] a valid color representation, taken from
33
+ # Omnitest::Color::ANSI
34
+ # @return [String] an ansi escaped string if the color is valid and an
35
+ # unescaped string otherwise
36
+ def self.colorize(str, name)
37
+ color = escape(name)
38
+ color.empty? ? str : "#{color}#{str}#{escape(:reset)}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ module Omnitest
2
+ module Core
3
+ module Configurable
4
+ # @see Omnitest::Configuration
5
+ def configuration
6
+ fail "configuration doesn't take a block, use configure" if block_given?
7
+ @configuration ||= const_get('Configuration').new
8
+ end
9
+
10
+ # @see Omnitest::Configuration
11
+ def configure
12
+ yield(configuration)
13
+ end
14
+
15
+ def reset
16
+ configuration.clear
17
+ @configuration = nil
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,60 @@
1
+ module Omnitest
2
+ module Core
3
+ module FileSystem
4
+ include Util::String
5
+
6
+ class << self
7
+ # Finds a file by loosely matching the file name to a scenario name
8
+ def find_file(search_path, scenario_name, ignored_patterns = nil)
9
+ ignored_patterns ||= read_gitignore(search_path)
10
+ glob_string = "#{search_path}/**/*#{slugify(scenario_name)}.*"
11
+ potential_files = Dir.glob(glob_string, File::FNM_CASEFOLD)
12
+ potential_files.concat Dir.glob(glob_string.gsub('_', '-'), File::FNM_CASEFOLD)
13
+ potential_files.concat Dir.glob(glob_string.gsub('_', ''), File::FNM_CASEFOLD)
14
+
15
+ # Filter out ignored filesFind the first file, not including generated files
16
+ files = potential_files.select do |f|
17
+ !ignored? ignored_patterns, search_path, f
18
+ end
19
+
20
+ # Select the shortest path, likely the best match
21
+ file = files.min_by(&:length)
22
+
23
+ fail Errno::ENOENT, "No file was found for #{scenario_name} within #{search_path}" if file.nil?
24
+ Pathname.new file
25
+ end
26
+
27
+ def relativize(file, base_path)
28
+ absolute_file = File.absolute_path(file)
29
+ absolute_base_path = File.absolute_path(base_path)
30
+ Pathname.new(absolute_file).relative_path_from Pathname.new(absolute_base_path)
31
+ end
32
+
33
+ private
34
+
35
+ # @api private
36
+ def read_gitignore(dir)
37
+ gitignore_file = "#{dir}/.gitignore"
38
+ File.read(gitignore_file)
39
+ rescue
40
+ ''
41
+ end
42
+
43
+ # @api private
44
+ def ignored?(ignored_patterns, base_path, target_file)
45
+ # Trying to match the git ignore rules but there's some discrepencies.
46
+ ignored_patterns.split.find do |pattern|
47
+ # if git ignores a folder, we should ignore all files it contains
48
+ pattern = "#{pattern}**" if pattern[-1] == '/'
49
+ started_with_slash = pattern.start_with? '/'
50
+
51
+ pattern.gsub!(/\A\//, '') # remove leading slashes since we're searching from root
52
+ file = relativize(target_file, base_path)
53
+ ignored = file.fnmatch? pattern
54
+ ignored || (file.fnmatch? "**/#{pattern}" unless started_with_slash)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,51 @@
1
+ require 'hashie'
2
+ require 'erb'
3
+
4
+ module Omnitest
5
+ module Core
6
+ class Dash < Hashie::Dash
7
+ include Hashie::Extensions::Coercion
8
+
9
+ def initialize(hash = {})
10
+ super Omnitest::Core::Util.symbolized_hash(hash)
11
+ end
12
+
13
+ # @api private
14
+ # @!macro [attach] field
15
+ # @!attribute [rw] $1
16
+ # Attribute $1. $3
17
+ # @return [$2]
18
+ # Defines a typed attribute on a class.
19
+ def self.field(name, type, opts = {})
20
+ property name, opts
21
+ coerce_key name, type
22
+ end
23
+
24
+ # @api private
25
+ # @!macro [attach] required_field
26
+ # @!attribute [rw] $1
27
+ # **Required** Attribute $1. $3
28
+ # @return [$2]
29
+ # Defines a required, typed attribute on a class.
30
+ def self.required_field(name, type, opts = {})
31
+ opts[:required] = true
32
+ field(name, type, opts)
33
+ end
34
+
35
+ module Loadable
36
+ include Core::DefaultLogger
37
+ def from_yaml(yaml_file)
38
+ logger.debug "Loading #{yaml_file}"
39
+ raw_content = File.read(yaml_file)
40
+ processed_content = ERB.new(raw_content).result
41
+ data = YAML.load processed_content
42
+ new data
43
+ end
44
+ end
45
+ end
46
+
47
+ class Mash < Hashie::Mash
48
+ include Hashie::Extensions::Coercion
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,42 @@
1
+ require 'logger'
2
+
3
+ module Omnitest
4
+ module Core
5
+ module Logger
6
+ def logger
7
+ @logger ||= new_logger
8
+ end
9
+
10
+ def new_logger(io = $stdout, level = :debug)
11
+ stdout_logger(io).tap do | logger |
12
+ logger.level = Util.to_logger_level(level)
13
+ end
14
+ end
15
+
16
+ # Construct a new standard out logger.
17
+ #
18
+ # @param stdout [IO] the IO object that represents stdout (or similar)
19
+ # @param color [Symbol] color to use when outputing messages
20
+ # @return [StdoutLogger] a new logger
21
+ # @api private
22
+ def stdout_logger(stdout, color = nil)
23
+ logger = Omnitest::Core::StdoutLogger.new(stdout)
24
+ # if Omnitest.tty?
25
+ if stdout.tty? && color
26
+ logger.formatter = proc do |_severity, _datetime, _progname, msg|
27
+ Core::Color.colorize("#{msg}", color).concat("\n")
28
+ end
29
+ else
30
+ logger.formatter = proc do |_severity, _datetime, _progname, msg|
31
+ msg.concat("\n")
32
+ end
33
+ end
34
+ logger
35
+ end
36
+
37
+ def log_level=(level)
38
+ logger.level = level
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,133 @@
1
+ module Omnitest
2
+ module Core
3
+ module DefaultLogger
4
+ module ClassMethods
5
+ def logger
6
+ @logger ||= default_logger
7
+ end
8
+
9
+ def default_logger
10
+ if Omnitest.respond_to? :configuration
11
+ Omnitest.configuration.default_logger
12
+ else
13
+ ::Logger.new(STDOUT)
14
+ end
15
+ end
16
+ end
17
+
18
+ def self.included(base)
19
+ base.extend(ClassMethods)
20
+ end
21
+
22
+ include ClassMethods
23
+ end
24
+
25
+ module Logging
26
+ class << self
27
+ private
28
+
29
+ def logger_method(meth)
30
+ define_method(meth) do |*args|
31
+ logger.public_send(meth, *args)
32
+ end
33
+ end
34
+ end
35
+
36
+ logger_method :banner
37
+ logger_method :debug
38
+ logger_method :info
39
+ logger_method :warn
40
+ logger_method :error
41
+ logger_method :fatal
42
+ end
43
+
44
+ # Internal class which adds a #banner method call that displays the
45
+ # message with a callout arrow.
46
+ class LogdevLogger < ::Logger
47
+ alias_method :super_info, :info
48
+
49
+ # Dump one or more messages to info.
50
+ #
51
+ # @param msg [String] a message
52
+ def <<(msg)
53
+ @buffer ||= ''
54
+ lines, _, remainder = msg.rpartition("\n")
55
+ if lines.empty?
56
+ @buffer << remainder
57
+ else
58
+ lines.insert(0, @buffer)
59
+ lines.split("\n").each { |l| format_line(l.chomp) }
60
+ @buffer = ''
61
+ end
62
+ end
63
+
64
+ # Log a banner message.
65
+ #
66
+ # @param msg [String] a message
67
+ def banner(msg = nil, &block)
68
+ super_info("-----> #{msg}", &block)
69
+ end
70
+
71
+ private
72
+
73
+ # Reformat a line if it already contains log formatting.
74
+ #
75
+ # @param line [String] a message line
76
+ # @api private
77
+ def format_line(line)
78
+ case line
79
+ when /^-----> / then banner(line.gsub(/^[ >-]{6} /, ''))
80
+ when /^>>>>>> / then error(line.gsub(/^[ >-]{6} /, ''))
81
+ when /^ / then info(line.gsub(/^[ >-]{6} /, ''))
82
+ else info(line)
83
+ end
84
+ end
85
+ end
86
+
87
+ # Internal class which reformats logging methods for display as console
88
+ # output.
89
+ class StdoutLogger < LogdevLogger
90
+ # Log a debug message
91
+ #
92
+ # @param msg [String] a message
93
+ def debug(msg = nil, &block)
94
+ super("D #{msg}", &block)
95
+ end
96
+
97
+ # Log an info message
98
+ #
99
+ # @param msg [String] a message
100
+ def info(msg = nil, &block)
101
+ super(" #{msg}", &block)
102
+ end
103
+
104
+ # Log a warn message
105
+ #
106
+ # @param msg [String] a message
107
+ def warn(msg = nil, &block)
108
+ super("$$$$$$ #{msg}", &block)
109
+ end
110
+
111
+ # Log an error message
112
+ #
113
+ # @param msg [String] a message
114
+ def error(msg = nil, &block)
115
+ super(">>>>>> #{msg}", &block)
116
+ end
117
+
118
+ # Log a fatal message
119
+ #
120
+ # @param msg [String] a message
121
+ def fatal(msg = nil, &block)
122
+ super("!!!!!! #{msg}", &block)
123
+ end
124
+
125
+ # Log an unknown message
126
+ #
127
+ # @param msg [String] a message
128
+ def unknown(msg = nil, &block)
129
+ super("?????? #{msg}", &block)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,204 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Much of this code has been adapted from Fletcher Nichol (<fnichol@nichol.ca>)
4
+ # work on test-kitchen.
5
+
6
+ module Omnitest
7
+ module Core
8
+ # Stateless utility methods used in different contexts. Essentially a mini
9
+ # PassiveSupport library.
10
+ module Util
11
+ module Hashable
12
+ def to_hash
13
+ instance_variables.each_with_object({}) do |var, hash|
14
+ hash[var.to_s.delete('@')] = instance_variable_get(var)
15
+ end
16
+ end
17
+ end
18
+
19
+ # Returns the standard library Logger level constants for a given symbol
20
+ # representation.
21
+ #
22
+ # @param symbol [Symbol] symbol representation of a logger level (:debug,
23
+ # :info, :warn, :error, :fatal)
24
+ # @return [Integer] Logger::Severity constant value or nil if input is not
25
+ # valid
26
+ def self.to_logger_level(symbol)
27
+ return nil unless [:debug, :info, :warn, :error, :fatal].include?(symbol)
28
+
29
+ ::Logger.const_get(symbol.to_s.upcase)
30
+ end
31
+
32
+ # Returns the symbol represenation of a logging levels for a given
33
+ # standard library Logger::Severity constant.
34
+ #
35
+ # @param const [Integer] Logger::Severity constant value for a logging
36
+ # level (Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::ERROR,
37
+ # Logger::FATAL)
38
+ # @return [Symbol] symbol representation of the logging level
39
+ def self.from_logger_level(const)
40
+ case const
41
+ when ::Logger::DEBUG then :debug
42
+ when ::Logger::INFO then :info
43
+ when ::Logger::WARN then :warn
44
+ when ::Logger::ERROR then :error
45
+ else :fatal
46
+ end
47
+ end
48
+
49
+ # Returns a new Hash with all key values coerced to symbols. All keys
50
+ # within a Hash are coerced by calling #to_sym and hashes within arrays
51
+ # and other hashes are traversed.
52
+ #
53
+ # @param obj [Object] the hash to be processed. While intended for
54
+ # hashes, this method safely processes arbitrary objects
55
+ # @return [Object] a converted hash with all keys as symbols
56
+ def self.symbolized_hash(obj)
57
+ if obj.is_a?(Hash)
58
+ obj.each_with_object({}) do |(k, v), h|
59
+ h[k.to_sym] = symbolized_hash(v)
60
+ end
61
+ elsif obj.is_a?(Array)
62
+ obj.each_with_object([]) do |e, a|
63
+ a << symbolized_hash(e)
64
+ end
65
+ else
66
+ obj
67
+ end
68
+ end
69
+
70
+ # Returns a new Hash with all key values coerced to strings. All keys
71
+ # within a Hash are coerced by calling #to_s and hashes with arrays
72
+ # and other hashes are traversed.
73
+ #
74
+ # @param obj [Object] the hash to be processed. While intended for
75
+ # hashes, this method safely processes arbitrary objects
76
+ # @return [Object] a converted hash with all keys as strings
77
+ def self.stringified_hash(obj)
78
+ if obj.is_a?(Hash)
79
+ obj.each_with_object({}) do |(k, v), h|
80
+ h[k.to_s] = stringified_hash(v)
81
+ end
82
+ elsif obj.is_a?(Array)
83
+ obj.each_with_object([]) do |e, a|
84
+ a << stringified_hash(e)
85
+ end
86
+ else
87
+ obj
88
+ end
89
+ end
90
+
91
+ # Returns a formatted string representing a duration in seconds.
92
+ #
93
+ # @param total [Integer] the total number of seconds
94
+ # @return [String] a formatted string of the form (XmYY.00s)
95
+ def self.duration(total)
96
+ total = 0 if total.nil?
97
+ minutes = (total / 60).to_i
98
+ seconds = (total - (minutes * 60))
99
+ format('(%dm%.2fs)', minutes, seconds)
100
+ end
101
+
102
+ module String
103
+ module ClassMethods
104
+ def slugify(*labels)
105
+ labels.map do |label|
106
+ label.downcase.gsub(/[\.\s-]/, '_')
107
+ end.join('-')
108
+ end
109
+
110
+ def ansi2html(text)
111
+ HTML.from_ansi(text)
112
+ end
113
+
114
+ def escape_html(text)
115
+ HTML.escape_html(text)
116
+ end
117
+ alias_method :h, :escape_html
118
+
119
+ def highlight(source, opts = {})
120
+ return nil if source.nil?
121
+
122
+ opts[:language] ||= 'ruby'
123
+ opts[:formatter] ||= 'terminal256'
124
+ Highlight.new(opts).highlight(source)
125
+ end
126
+ end
127
+
128
+ def self.included(base)
129
+ base.extend(ClassMethods)
130
+ end
131
+
132
+ include ClassMethods
133
+ end
134
+
135
+ class Highlight
136
+ def initialize(opts)
137
+ @lexer = Rouge::Lexer.find(opts[:language]) || Rouge::Lexer.guess_by_filename(opts[:filename])
138
+ @formatter = opts[:formatter]
139
+ end
140
+
141
+ def highlight(source)
142
+ Rouge.highlight(source, @lexer, @formatter)
143
+ end
144
+ end
145
+
146
+ class HTML
147
+ ANSICODES = {
148
+ '1' => 'bold',
149
+ '4' => 'underline',
150
+ '30' => 'black',
151
+ '31' => 'red',
152
+ '32' => 'green',
153
+ '33' => 'yellow',
154
+ '34' => 'blue',
155
+ '35' => 'magenta',
156
+ '36' => 'cyan',
157
+ '37' => 'white',
158
+ '40' => 'bg-black',
159
+ '41' => 'bg-red',
160
+ '42' => 'bg-green',
161
+ '43' => 'bg-yellow',
162
+ '44' => 'bg-blue',
163
+ '45' => 'bg-magenta',
164
+ '46' => 'bg-cyan',
165
+ '47' => 'bg-white'
166
+ }
167
+
168
+ def self.from_ansi(text)
169
+ ansi = StringScanner.new(text)
170
+ html = StringIO.new
171
+ until ansi.eos?
172
+ if ansi.scan(/\e\[0?m/)
173
+ html.print(%(</span>))
174
+ elsif ansi.scan(/\e\[0?(\d+)m/)
175
+ # use class instead of style?
176
+ style = ANSICODES[ansi[1]] || 'text-reset'
177
+ html.print(%(<span class="#{style}">))
178
+ else
179
+ html.print(ansi.scan(/./m))
180
+ end
181
+ end
182
+ html.string
183
+ end
184
+
185
+ # From Rack
186
+
187
+ ESCAPE_HTML = {
188
+ '&' => '&amp;',
189
+ '<' => '&lt;',
190
+ '>' => '&gt;',
191
+ "'" => '&#x27;',
192
+ '"' => '&quot;',
193
+ '/' => '&#x2F;'
194
+ }
195
+ ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
196
+
197
+ # Escape ampersands, brackets and quotes to their HTML/XML entities.
198
+ def self.escape_html(string)
199
+ string.to_s.gsub(ESCAPE_HTML_PATTERN) { |c| ESCAPE_HTML[c] }
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,5 @@
1
+ module Omnitest
2
+ module Core
3
+ VERSION = '0.0.1'
4
+ end
5
+ end
@@ -0,0 +1,244 @@
1
+ require 'English'
2
+
3
+ module Omnitest
4
+ # All Omnitest 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: Omnitest::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: Omnitest::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
+ module ErrorSource
61
+ def error_source
62
+ if backtrace_locations
63
+ source_from_backtrace(backtrace_locations)
64
+ elsif original && original.backtrace_locations
65
+ source_from_backtrace(original.backtrace_locations)
66
+ end
67
+ end
68
+
69
+ def source_from_backtrace(backtrace_locations)
70
+ error_location = backtrace_locations.delete_if { |l| l.absolute_path =~ /gems\/rspec-/ }.first
71
+ error_source = File.read(error_location.absolute_path)
72
+ error_lineno = error_location.lineno - 1 # lineno counts from 1
73
+ get_dedented_block(error_source, error_lineno)
74
+ end
75
+
76
+ def get_dedented_block(source_text, target_lineno)
77
+ block = []
78
+ lines = source_text.lines
79
+ target_indent = lines[target_lineno][/\A */].size
80
+ lines[0...target_lineno].reverse.each do |line|
81
+ indent = line[/\A */].size
82
+ block.prepend line
83
+ break if indent < target_indent
84
+ end
85
+ lines[target_lineno..lines.size].each do |line|
86
+ indent = line[/\A */].size
87
+ block.push line
88
+ break if indent < target_indent
89
+ end
90
+ block.join
91
+ end
92
+ end
93
+
94
+ # Base exception class from which all Omnitest exceptions derive. This class
95
+ # nests an exception when this class is re-raised from a rescue block.
96
+ class StandardError < ::StandardError
97
+ include Error
98
+
99
+ # @return [::StandardError] the original (wrapped) exception
100
+ attr_reader :original
101
+
102
+ # Creates a new StandardError exception which optionally wraps an original
103
+ # exception if given or detected by checking the `$!` global variable.
104
+ #
105
+ # @param msg [String] exception message
106
+ # @param original [::StandardError] an original exception which will be
107
+ # wrapped (default: `$ERROR_INFO`)
108
+ def initialize(msg, original = $ERROR_INFO)
109
+ super(msg)
110
+ @original = original
111
+ end
112
+ end
113
+
114
+ # Base exception class for all exceptions that are caused by user input
115
+ # errors.
116
+ class UserError < StandardError; end
117
+
118
+ # Base exception class for all exceptions that are caused by incorrect use
119
+ # of an API.
120
+ class ClientError < StandardError; end
121
+
122
+ # Base exception class for exceptions that are caused by external library
123
+ # failures which may be temporary.
124
+ class TransientFailure < StandardError; end
125
+
126
+ # Exception class for any exceptions raised when performing an scenario
127
+ # action.
128
+ class ActionFailed < TransientFailure; end
129
+
130
+ class ExecutionError < TransientFailure
131
+ attr_accessor :execution_result
132
+ end
133
+
134
+ class << self
135
+ # Yields to a code block in order to consistently emit a useful crash/error
136
+ # message and exit appropriately. There are two primary failure conditions:
137
+ # an expected scenario failure, and any other unexpected failures.
138
+ #
139
+ # **Note** This method may call `Kernel.exit` so may not return if the
140
+ # yielded code block raises an exception.
141
+ #
142
+ # ## Scenario Failure
143
+ #
144
+ # This is an expected failure scenario which could happen if an scenario
145
+ # couldn't be created, a Chef run didn't successfully converge, a
146
+ # post-convergence test suite failed, etc. In other words, you can count on
147
+ # encountering these failures all the time--this is Omnitest's worldview:
148
+ # crash early and often. In this case a cleanly formatted exception is
149
+ # written to `STDERR` and the exception message is written to
150
+ # the common Omnitest file logger.
151
+ #
152
+ # ## Unexpected Failure
153
+ #
154
+ # All other forms of `Omnitest::Error` exceptions are considered unexpected
155
+ # or unplanned exceptions, typically from user configuration errors, driver
156
+ # or provisioner coding issues or bugs, or internal code issues. Given
157
+ # a stable release of Omnitest and a solid set of drivers and provisioners,
158
+ # the most likely cause of this is user configuration error originating in
159
+ # the `.omnitest.yaml` setup. For this reason, the exception is written to
160
+ # `STDERR`, a full formatted exception trace is written to the common
161
+ # Omnitest file logger, and a message is displayed on `STDERR` to the user
162
+ # informing them to check the log files and check their configuration with
163
+ # the `omnitest diagnose` subcommand.
164
+ #
165
+ # @raise [SystemExit] if an exception is raised in the yielded block
166
+ def with_friendly_errors
167
+ yield
168
+ rescue Omnitest::Skeptic::ScenarioFailure => e
169
+ Omnitest.mutex.synchronize do
170
+ handle_scenario_failure(e)
171
+ end
172
+ exit 10
173
+ rescue Omnitest::Error => e
174
+ Omnitest.mutex.synchronize do
175
+ handle_error(e)
176
+ end
177
+ exit 20
178
+ end
179
+
180
+ # Handles an scenario failure exception.
181
+ #
182
+ # @param e [StandardError] an exception to handle
183
+ # @see Omnitest.with_friendly_errors
184
+ # @api private
185
+ def handle_scenario_failure(e)
186
+ stderr_log(e.message.split(/\s{2,}/))
187
+ stderr_log(Error.formatted_exception(e.original))
188
+ file_log(:error, e.message.split(/\s{2,}/).first)
189
+ debug_log(Error.formatted_trace(e))
190
+ end
191
+
192
+ alias_method :handle_validation_failure, :handle_scenario_failure
193
+
194
+ # Handles an unexpected failure exception.
195
+ #
196
+ # @param e [StandardError] an exception to handle
197
+ # @see Omnitest.with_friendly_errors
198
+ # @api private
199
+ def handle_error(e)
200
+ stderr_log(Error.formatted_exception(e))
201
+ stderr_log('Please see .omnitest/logs/omnitest.log for more details')
202
+ # stderr_log("Also try running `omnitest diagnose --all` for configuration\n")
203
+ file_log(:error, Error.formatted_trace(e))
204
+ end
205
+
206
+ private
207
+
208
+ # Writes an array of lines to the common Omnitest logger's file device at the
209
+ # given severity level. If the Omnitest logger is set to debug severity, then
210
+ # the array of lines will also be written to the console output.
211
+ #
212
+ # @param level [Symbol,String] the desired log level
213
+ # @param lines [Array<String>] an array of strings to log
214
+ # @api private
215
+ def file_log(level, lines)
216
+ Array(lines).each do |line|
217
+ if Omnitest.logger.debug?
218
+ Omnitest.logger.debug(line)
219
+ else
220
+ Omnitest.logger.logdev && Omnitest.logger.logdev.public_send(level, line)
221
+ end
222
+ end
223
+ end
224
+
225
+ # Writes an array of lines to the `STDERR` device.
226
+ #
227
+ # @param lines [Array<String>] an array of strings to log
228
+ # @api private
229
+ def stderr_log(lines)
230
+ Array(lines).each do |line|
231
+ $stderr.puts(Core::Color.colorize(">>>>>> #{line}", :red))
232
+ end
233
+ end
234
+
235
+ # Writes an array of lines to the common Omnitest debugger with debug
236
+ # severity.
237
+ #
238
+ # @param lines [Array<String>] an array of strings to log
239
+ # @api private
240
+ def debug_log(lines)
241
+ Array(lines).each { |line| Omnitest.logger.debug(line) }
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'omnitest/core/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'omnitest-core'
8
+ spec.version = Omnitest::Core::VERSION
9
+ spec.authors = ['Max Lincoln']
10
+ spec.email = ['max@devopsy.com']
11
+ spec.summary = 'Shared code for omnitest projects.'
12
+ spec.homepage = ''
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_dependency 'thor', '~> 0.19'
21
+ spec.add_dependency 'cause', '~> 0.1'
22
+ spec.add_dependency 'rouge', '~> 1.7'
23
+ spec.add_dependency 'hashie', '~> 3.0'
24
+ spec.add_development_dependency 'bundler', '~> 1.5'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'rake-notes'
27
+ spec.add_development_dependency 'simplecov'
28
+ spec.add_development_dependency 'rspec', '~> 3.0'
29
+ spec.add_development_dependency 'rubocop', '~> 0.18', '<= 0.27'
30
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.2'
31
+ spec.add_development_dependency 'aruba'
32
+ end
metadata ADDED
@@ -0,0 +1,237 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omnitest-core
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Max Lincoln
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.19'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.19'
27
+ - !ruby/object:Gem::Dependency
28
+ name: cause
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rouge
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.7'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: hashie
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.5'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake-notes
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.18'
146
+ - - "<="
147
+ - !ruby/object:Gem::Version
148
+ version: '0.27'
149
+ type: :development
150
+ prerelease: false
151
+ version_requirements: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - "~>"
154
+ - !ruby/object:Gem::Version
155
+ version: '0.18'
156
+ - - "<="
157
+ - !ruby/object:Gem::Version
158
+ version: '0.27'
159
+ - !ruby/object:Gem::Dependency
160
+ name: rubocop-rspec
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '1.2'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '1.2'
173
+ - !ruby/object:Gem::Dependency
174
+ name: aruba
175
+ requirement: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ type: :development
181
+ prerelease: false
182
+ version_requirements: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ description:
188
+ email:
189
+ - max@devopsy.com
190
+ executables: []
191
+ extensions: []
192
+ extra_rdoc_files: []
193
+ files:
194
+ - ".gitignore"
195
+ - ".rubocop.yml"
196
+ - ".rubocop_todo.yml"
197
+ - Gemfile
198
+ - LICENSE.txt
199
+ - README.md
200
+ - Rakefile
201
+ - lib/omnitest/core.rb
202
+ - lib/omnitest/core/cli.rb
203
+ - lib/omnitest/core/color.rb
204
+ - lib/omnitest/core/configurable.rb
205
+ - lib/omnitest/core/file_system.rb
206
+ - lib/omnitest/core/hashie.rb
207
+ - lib/omnitest/core/logger.rb
208
+ - lib/omnitest/core/logging.rb
209
+ - lib/omnitest/core/util.rb
210
+ - lib/omnitest/core/version.rb
211
+ - lib/omnitest/errors.rb
212
+ - omnitest-core.gemspec
213
+ homepage: ''
214
+ licenses:
215
+ - MIT
216
+ metadata: {}
217
+ post_install_message:
218
+ rdoc_options: []
219
+ require_paths:
220
+ - lib
221
+ required_ruby_version: !ruby/object:Gem::Requirement
222
+ requirements:
223
+ - - ">="
224
+ - !ruby/object:Gem::Version
225
+ version: '0'
226
+ required_rubygems_version: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - ">="
229
+ - !ruby/object:Gem::Version
230
+ version: '0'
231
+ requirements: []
232
+ rubyforge_project:
233
+ rubygems_version: 2.4.2
234
+ signing_key:
235
+ specification_version: 4
236
+ summary: Shared code for omnitest projects.
237
+ test_files: []