omnitest-core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: []