lib/shindo.rb



require 'rubygems'
require 'annals'

module Shindo

  def self.tests(header = nil, &block)
    STDOUT.sync = true
    Shindo::Tests.new(header, &block)
  end

  class Tests

    attr_accessor :backtrace

    def initialize(header, tags = [], &block)
      @afters     = []
      @annals     = Annals.new
      @befores    = []
      @description_stack = []
      @if_tagged      = Thread.current[:tags].select {|tag| tag.match(/^\+/)}
      @unless_tagged  = Thread.current[:tags].select {|tag| tag.match(/^\-/)}.map {|tag| tag[1..-1]}
      @indent     = 1
      @success    = true
      @tag_stack  = []
      Thread.current[:reload] = false
      print("\n")
      tests(header, &block)
      print("\n")
      if @success
        Thread.current[:success] = false
      else
        Thread.current[:success] = false
      end
    end

    def after(&block)
      @afters[-1].push(block)
    end

    def before(&block)
      @befores[-1].push(block)
    end

    def full_description
      "#{@description_stack.compact.join(' ')}#{full_tags}"
    end

    def full_tags
      unless @tag_stack.flatten.empty?
        " [#{@tag_stack.flatten.join(', ')}]"
      end
    end

    def green_line(content)
      print_line(content, "\e[32m")
    end

    def indent(&block)
      @indent += 1
      yield
      @indent -= 1
    end

    def indentation
      '  ' * @indent
    end

    def print_line(content, color = nil)
      if color && STDOUT.tty?
        content = "#{color}#{content}\e[0m"
      end
      print("#{indentation}#{content}\n")
    end

    def prompt(&block)
      print("#{indentation}Action? [c,i,q,r,t,#,?]? ")
      choice = STDIN.gets.strip
      print("\n")
      case choice
      when 'c'
        return
      when 'i'
        print_line('Starting interactive session...')
        if @irb.nil?
          require 'irb'
          ARGV.clear # Avoid passing args to IRB
          IRB.setup(nil)
          @irb = IRB::Irb.new(nil)
          IRB.conf[:MAIN_CONTEXT] = @irb.context
          IRB.conf[:PROMPT][:TREST] = {}
        end
        for key, value in IRB.conf[:PROMPT][:SIMPLE]
          IRB.conf[:PROMPT][:TREST][key] = "#{indentation}#{value}"
        end
        @irb.context.prompt_mode = :TREST
        @irb.context.workspace = IRB::WorkSpace.new(block.binding)
        begin
          @irb.eval_input
        rescue SystemExit
        end
      when 'q'
        Thread.current[:success] = false
        Thread.exit
      when 'r'
        print("Reloading...\n")
        Thread.current[:reload] = true
        Thread.exit
      when 't'
        indent {
          if @annals.lines.empty?
            print_line('no backtrace available')
          else
            index = 1
            for line in @annals.lines
              print_line("#{' ' * (2 - index.to_s.length)}#{index}  #{line}")
              index += 1
            end
          end
        }
        print("\n")
      when '?'
        print_line('c - ignore this error and continue')
        print_line('i - interactive mode')
        print_line('q - quit Shindo')
        print_line('r - reload and run the tests again')
        print_line('t - display backtrace')
        print_line('# - enter a number of a backtrace line to see its context')
        print_line('? - display help')
      when /\d/
        index = choice.to_i - 1
        if backtrace.lines[index]
          indent {
            print_line("#{@annals.lines[index]}: ")
            indent {
              print("\n")
              current_line = @annals.buffer[index]
              File.open(current_line[:file], 'r') do |file|
                data = file.readlines
                current = current_line[:line]
                min     = [0, current - (@annals.max / 2)].max
                max     = [current + (@annals.max / 2), data.length].min
                min.upto(current - 1) do |line|
                  print_line("#{line}  #{data[line].rstrip}")
                end
                yellow_line("#{current}  #{data[current].rstrip}")
                (current + 1).upto(max - 1) do |line|
                  print_line("#{line}  #{data[line].rstrip}")
                end
              end
              print("\n")
            }
          }
        else
          red_line("#{choice} is not a valid backtrace line, please try again.")
        end
      else
        red_line("#{choice} is not a valid choice, please try again.")
      end
      red_line("- #{full_description}")
      prompt(&block)
    end

    def red_line(content)
      print_line(content, "\e[31m")
    end

    def tests(description, tags = [], &block)
      @tag_stack.push([*tags])
      @befores.push([])
      @afters.push([])

      print_line(description || 'Shindo.tests')
      if block_given?
        indent { instance_eval(&block) }
      end

      @afters.pop
      @befores.pop
      @tag_stack.pop
    end

    def test(description, tags = [], &block)
      @description_stack.push(description)
      @tag_stack.push([*tags])

      # if the test includes tags and discludes ^tags, evaluate it
      if (@if_tagged.empty? || !(@if_tagged & @tag_stack.flatten).empty?) &&
          (@unless_tagged.empty? || (@unless_tagged & @tag_stack.flatten).empty?)
        if block_given?
          for before in @befores.flatten.compact
            before.call
          end

          @annals.start
          begin
            success = instance_eval(&block)
            @annals.stop
          rescue => error
            success = false
            file, line, method = error.backtrace.first.split(':')
            if method
              method << "in #{method[4...-1]} " # get method from "in `foo'"
            else
              method = ''
            end
            method << "! #{error.message} (#{error.class})"
            @annals.stop
            @annals.unshift(:file => file, :line => line.to_i, :method => method)
          end
          @success = @success && success
          if success
            green_line("+ #{full_description}")
          else
            red_line("- #{full_description}")
            if STDOUT.tty?
              prompt(&block)
            end
          end

          for after in @afters.flatten.compact
            after.call
          end
        else
          yellow_line("* #{full_description}")
        end
      else
        print_line("_ #{full_description}")
      end

      @tag_stack.pop
      @description_stack.pop
    end

    def yellow_line(content)
      print_line(content, "\e[33m")
    end

  end

end