lib/polytrix/util.rb in polytrix-0.1.2 vs lib/polytrix/util.rb in polytrix-0.1.3

- old
+ new

@@ -1,22 +1,9 @@ # -*- encoding: utf-8 -*- # -# Author:: Fletcher Nichol (<fnichol@nichol.ca>) -# -# Copyright (C) 2012, 2013, 2014, Fletcher Nichol -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Much of this code has been adapted from Fletcher Nichol (<fnichol@nichol.ca>) +# work on test-kitchen. module Polytrix # Stateless utility methods used in different contexts. Essentially a mini # PassiveSupport library. module Util @@ -57,18 +44,16 @@ # @param obj [Object] the hash to be processed. While intended for # hashes, this method safely processes arbitrary objects # @return [Object] a converted hash with all keys as symbols def self.symbolized_hash(obj) if obj.is_a?(Hash) - obj.reduce({}) do |h, (k, v)| + obj.each_with_object({}) do |(k, v), h| h[k.to_sym] = symbolized_hash(v) - h end elsif obj.is_a?(Array) - obj.reduce([]) do |a, e| + obj.each_with_object([]) do |e, a| a << symbolized_hash(e) - a end else obj end end @@ -80,18 +65,16 @@ # @param obj [Object] the hash to be processed. While intended for # hashes, this method safely processes arbitrary objects # @return [Object] a converted hash with all keys as strings def self.stringified_hash(obj) if obj.is_a?(Hash) - obj.reduce({}) do |h, (k, v)| + obj.each_with_object({}) do |(k, v), h| h[k.to_s] = stringified_hash(v) - h end elsif obj.is_a?(Array) - obj.reduce([]) do |a, e| + obj.each_with_object([]) do |e, a| a << stringified_hash(e) - a end else obj end end @@ -105,51 +88,163 @@ minutes = (total / 60).to_i seconds = (total - (minutes * 60)) format('(%dm%.2fs)', minutes, seconds) end - # Generates a command (or series of commands) wrapped so that it can be - # invoked on a remote instance or locally. - # - # This method uses the Bourne shell (/bin/sh) to maximize the chance of - # cross platform portability on Unixlike systems. - # - # @param [String] the command - # @return [String] a wrapped command string - def self.wrap_command(cmd) - cmd = 'false' if cmd.nil? - cmd = 'true' if cmd.to_s.empty? - cmd = cmd.sub(/\n\Z/, '') if cmd =~ /\n\Z/ + module String + module ClassMethods + def slugify(*labels) + labels.map do |label| + label.downcase.gsub(/[\.\s-]/, '_') + end.join('-') + end - "sh -c '\n#{cmd}\n'" + def ansi2html(text) + HTML.from_ansi(text) + end + + def escape_html(text) + HTML.escape_html(text) + end + alias_method :h, :escape_html + + def highlight(source, opts = {}) + return nil if source.nil? + + opts[:language] ||= 'ruby' + opts[:formatter] ||= 'terminal256' + Highlight.new(opts).highlight(source) + end + end + + def self.included(base) + base.extend(ClassMethods) + end + + include ClassMethods end - # Modifes the given string to strip leading whitespace on each line, the - # amount which is calculated by using the first line of text. - # - # @example - # - # string = <<-STRING - # a - # b - # c - # STRING - # Util.outdent!(string) # => "a\n b\nc\n" - # - # @param string [String] the string that will be modified - # @return [String] the modified string - def self.outdent!(string) - string.gsub!(/^ {#{string.index(/[^ ]/)}}/, '') + class Highlight + def initialize(opts) + @lexer = Rouge::Lexer.find(opts[:language]) || Rouge::Lexer.guess_by_filename(opts[:filename]) + @formatter = opts[:formatter] + end + + def highlight(source) + Rouge.highlight(source, @lexer, @formatter) + end end - # Returns a set of Bourne Shell (AKA /bin/sh) compatible helper - # functions. This function is usually called inline in a string that - # will be executed remotely on a test instance. - # - # @return [String] a string representation of useful helper functions - def self.shell_helpers - IO.read(File.join( - File.dirname(__FILE__), %w[.. .. support download_helpers.sh] - )) + class HTML + ANSICODES = { + '1' => 'bold', + '4' => 'underline', + '30' => 'black', + '31' => 'red', + '32' => 'green', + '33' => 'yellow', + '34' => 'blue', + '35' => 'magenta', + '36' => 'cyan', + '37' => 'white', + '40' => 'bg-black', + '41' => 'bg-red', + '42' => 'bg-green', + '43' => 'bg-yellow', + '44' => 'bg-blue', + '45' => 'bg-magenta', + '46' => 'bg-cyan', + '47' => 'bg-white' + } + + def self.from_ansi(text) + ansi = StringScanner.new(text) + html = StringIO.new + until ansi.eos? + if ansi.scan(/\e\[0?m/) + html.print(%(</span>)) + elsif ansi.scan(/\e\[0?(\d+)m/) + # use class instead of style? + style = ANSICODES[ansi[1]] || 'text-reset' + html.print(%(<span class="#{style}">)) + else + html.print(ansi.scan(/./m)) + end + end + html.string + end + + # From Rack + + ESCAPE_HTML = { + '&' => '&amp;', + '<' => '&lt;', + '>' => '&gt;', + "'" => '&#x27;', + '"' => '&quot;', + '/' => '&#x2F;' + } + ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys) + + # Escape ampersands, brackets and quotes to their HTML/XML entities. + def self.escape_html(string) + string.to_s.gsub(ESCAPE_HTML_PATTERN) { |c| ESCAPE_HTML[c] } + end + end + + module FileSystem + include Polytrix::Logging + include Polytrix::Util::String + + # Finds a file by loosely matching the file name to a scenario name + def find_file(search_path, scenario_name, ignored_patterns = nil) + ignored_patterns ||= read_gitignore(search_path) + glob_string = "#{search_path}/**/*#{slugify(scenario_name)}.*" + potential_files = Dir.glob(glob_string, File::FNM_CASEFOLD) + potential_files.concat Dir.glob(glob_string.gsub('_', '-'), File::FNM_CASEFOLD) + potential_files.concat Dir.glob(glob_string.gsub('_', ''), File::FNM_CASEFOLD) + + # Filter out ignored filesFind the first file, not including generated files + files = potential_files.select do |f| + !ignored? ignored_patterns, search_path, f + end + + # Select the shortest path, likely the best match + file = files.min_by(&:length) + + fail Errno::ENOENT, "No file was found for #{scenario_name} within #{search_path}" if file.nil? + Pathname.new file + end + + def relativize(file, base_path) + absolute_file = File.absolute_path(file) + absolute_base_path = File.absolute_path(base_path) + Pathname.new(absolute_file).relative_path_from Pathname.new(absolute_base_path) + end + + private + + # @api private + def read_gitignore(dir) + gitignore_file = "#{dir}/.gitignore" + File.read(gitignore_file) + rescue + '' + end + + # @api private + def ignored?(ignored_patterns, base_path, target_file) + # Trying to match the git ignore rules but there's some discrepencies. + ignored_patterns.split.find do |pattern| + # if git ignores a folder, we should ignore all files it contains + pattern = "#{pattern}**" if pattern[-1] == '/' + started_with_slash = pattern.start_with? '/' + + pattern.gsub!(/\A\//, '') # remove leading slashes since we're searching from root + file = relativize(target_file, base_path) + ignored = file.fnmatch? pattern + ignored || (file.fnmatch? "**/#{pattern}" unless started_with_slash) + end + end end end end