require 'spec_helper'
require 'rspec/core/formatters/base_text_formatter'

describe RSpec::Core::Formatters::BaseTextFormatter do
  let(:output) { StringIO.new }
  let(:formatter) { RSpec::Core::Formatters::BaseTextFormatter.new(output) }

  context "when closing the formatter", :isolated_directory => true do
    it 'does not close an already closed output stream' do
      output = File.new("./output_to_close", "w")
      formatter = described_class.new(output)
      output.close

      expect { formatter.close }.not_to raise_error
    end
  end

  describe "#summary_line" do
    it "with 0s outputs pluralized (excluding pending)" do
      expect(formatter.summary_line(0,0,0)).to eq("0 examples, 0 failures")
    end

    it "with 1s outputs singular (including pending)" do
      expect(formatter.summary_line(1,1,1)).to eq("1 example, 1 failure, 1 pending")
    end

    it "with 2s outputs pluralized (including pending)" do
      expect(formatter.summary_line(2,2,2)).to eq("2 examples, 2 failures, 2 pending")
    end
  end

  describe "#dump_commands_to_rerun_failed_examples" do
    it "includes command to re-run each failed example" do
      group = RSpec::Core::ExampleGroup.describe("example group") do
        it("fails") { fail }
      end
      line = __LINE__ - 2
      group.run(formatter)
      formatter.dump_commands_to_rerun_failed_examples
      expect(output.string).to include("rspec #{RSpec::Core::Metadata::relative_path("#{__FILE__}:#{line}")} # example group fails")
    end
  end

  describe "#dump_failures" do
    let(:group) { RSpec::Core::ExampleGroup.describe("group name") }

    before { RSpec.configuration.stub(:color_enabled?) { false } }

    def run_all_and_dump_failures
      group.run(formatter)
      formatter.dump_failures
    end

    it "preserves formatting" do
      group.example("example name") { expect("this").to eq("that") }

      run_all_and_dump_failures

      expect(output.string).to match(/group name example name/m)
      expect(output.string).to match(/(\s+)expected: \"that\"\n\1     got: \"this\"/m)
    end

    context "with an exception without a message" do
      it "does not throw NoMethodError" do
        exception_without_message = Exception.new()
        exception_without_message.stub(:message) { nil }
        group.example("example name") { raise exception_without_message }
        expect { run_all_and_dump_failures }.not_to raise_error
      end

      it "preserves ancestry" do
        example = group.example("example name") { raise "something" }
        run_all_and_dump_failures
        expect(example.example_group.parent_groups.size).to eq 1
      end
    end

    context "with an exception that has an exception instance as its message" do
      it "does not raise NoMethodError" do
        gonzo_exception = RuntimeError.new
        gonzo_exception.stub(:message) { gonzo_exception }
        group.example("example name") { raise gonzo_exception }
        expect { run_all_and_dump_failures }.not_to raise_error
      end
    end

    context "with an instance of an anonymous exception class" do
      it "substitutes '(anonymous error class)' for the missing class name" do
        exception = Class.new(StandardError).new
        group.example("example name") { raise exception }
        run_all_and_dump_failures
        expect(output.string).to include('(anonymous error class)')
      end
    end

    context "with an exception class other than RSpec" do
      it "does not show the error class" do
        group.example("example name") { raise NameError.new('foo') }
        run_all_and_dump_failures
        expect(output.string).to match(/NameError/m)
      end
    end

    context "with a failed expectation (rspec-expectations)" do
      it "does not show the error class" do
        group.example("example name") { expect("this").to eq("that") }
        run_all_and_dump_failures
        expect(output.string).not_to match(/RSpec/m)
      end
    end

    context "with a failed message expectation (rspec-mocks)" do
      it "does not show the error class" do
        group.example("example name") { "this".should_receive("that") }
        run_all_and_dump_failures
        expect(output.string).not_to match(/RSpec/m)
      end
    end

    context 'for #share_examples_for' do
      it 'outputs the name and location' do

        group.share_examples_for 'foo bar' do
          it("example name") { expect("this").to eq("that") }
        end

        line = __LINE__.next
        group.it_should_behave_like('foo bar')

        run_all_and_dump_failures

        expect(output.string).to include(
          'Shared Example Group: "foo bar" called from ' +
            "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{line}"
        )
      end

      context 'that contains nested example groups' do
        it 'outputs the name and location' do
          group.share_examples_for 'foo bar' do
            describe 'nested group' do
              it("example name") { expect("this").to eq("that") }
            end
          end

          line = __LINE__.next
          group.it_should_behave_like('foo bar')

          run_all_and_dump_failures

          expect(output.string).to include(
            'Shared Example Group: "foo bar" called from ' +
              "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{line}"
          )
        end
      end
    end

    context 'for #share_as' do
      before { allow(RSpec).to receive(:deprecate) }

      it 'outputs the name and location' do

        group.share_as :FooBar do
          it("example name") { expect("this").to eq("that") }
        end

        line = __LINE__.next
        group.send(:include, FooBar)

        run_all_and_dump_failures

        expect(output.string).to include(
          'Shared Example Group: "FooBar" called from ' +
            "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{line}"
        )
      end

      context 'that contains nested example groups' do
        it 'outputs the name and location' do

          group.share_as :NestedFoo do
            describe 'nested group' do
              describe 'hell' do
                it("example name") { expect("this").to eq("that") }
              end
            end
          end

          line = __LINE__.next
          group.send(:include, NestedFoo)

          run_all_and_dump_failures

          expect(output.string).to include(
            'Shared Example Group: "NestedFoo" called from ' +
              "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{line}"
          )
        end
      end
    end
  end

  describe "#dump_pending" do
    let(:group) { RSpec::Core::ExampleGroup.describe("group name") }

    before { RSpec.configuration.stub(:color_enabled?) { false } }

    def run_all_and_dump_pending
      group.run(formatter)
      formatter.dump_pending
    end

    context "with show_failures_in_pending_blocks setting enabled" do
      before { RSpec.configuration.instance_variable_set(:@show_failures_in_pending_blocks, true) }

      it "preserves formatting" do
        group.example("example name") { pending { expect("this").to eq("that") } }

        run_all_and_dump_pending

        expect(output.string).to match(/group name example name/m)
        expect(output.string).to match(/(\s+)expected: \"that\"\n\1     got: \"this\"/m)
      end

      context "with an exception without a message" do
        it "does not throw NoMethodError" do
          exception_without_message = Exception.new()
          exception_without_message.stub(:message) { nil }
          group.example("example name") { pending { raise exception_without_message } }
          expect { run_all_and_dump_pending }.not_to raise_error
        end
      end

      context "with an exception class other than RSpec" do
        it "does not show the error class" do
          group.example("example name") { pending { raise NameError.new('foo') } }
          run_all_and_dump_pending
          expect(output.string).to match(/NameError/m)
        end
      end

      context "with a failed expectation (rspec-expectations)" do
        it "does not show the error class" do
          group.example("example name") { pending { expect("this").to eq("that") } }
          run_all_and_dump_pending
          expect(output.string).not_to match(/RSpec/m)
        end
      end

      context "with a failed message expectation (rspec-mocks)" do
        it "does not show the error class" do
          group.example("example name") { pending { "this".should_receive("that") } }
          run_all_and_dump_pending
          expect(output.string).not_to match(/RSpec/m)
        end
      end

      context 'for #share_examples_for' do
        it 'outputs the name and location' do

          group.share_examples_for 'foo bar' do
            it("example name") { pending { expect("this").to eq("that") } }
          end

          line = __LINE__.next
          group.it_should_behave_like('foo bar')

          run_all_and_dump_pending

          expect(output.string).to include(
            'Shared Example Group: "foo bar" called from ' +
            "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{line}"
          )
        end

        context 'that contains nested example groups' do
          it 'outputs the name and location' do
            group.share_examples_for 'foo bar' do
              describe 'nested group' do
                it("example name") { pending { expect("this").to eq("that") } }
              end
            end

            line = __LINE__.next
            group.it_should_behave_like('foo bar')

            run_all_and_dump_pending

            expect(output.string).to include(
              'Shared Example Group: "foo bar" called from ' +
              "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{line}"
            )
          end
        end
      end

      context 'for #share_as' do
        before { allow(RSpec).to receive(:deprecate) }

        it 'outputs the name and location' do

          group.share_as :FooBar2 do
            it("example name") { pending { expect("this").to eq("that") } }
          end

          line = __LINE__.next
          group.send(:include, FooBar2)

          run_all_and_dump_pending

          expect(output.string).to include(
            'Shared Example Group: "FooBar2" called from ' +
            "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{line}"
          )
        end

        context 'that contains nested example groups' do
          it 'outputs the name and location' do

            group.share_as :NestedFoo2 do
              describe 'nested group' do
                describe 'hell' do
                  it("example name") { pending { expect("this").to eq("that") } }
                end
              end
            end

            line = __LINE__.next
            group.send(:include, NestedFoo2)

            run_all_and_dump_pending

            expect(output.string).to include(
              'Shared Example Group: "NestedFoo2" called from ' +
              "./spec/rspec/core/formatters/base_text_formatter_spec.rb:#{line}"
            )
          end
        end
      end
    end

    context "with show_failures_in_pending_blocks setting disabled" do
      before { RSpec.configuration.stub(:show_failures_in_pending_blocks?) { false } }

      it "does not output the failure information" do
        group.example("example name") { pending { expect("this").to eq("that") } }
        run_all_and_dump_pending
        expect(output.string).not_to match(/(\s+)expected: \"that\"\n\1     got: \"this\"/m)
      end
    end
  end

  describe "#dump_profile_slowest_examples" do
    example_line_number = nil

    before do
      group = RSpec::Core::ExampleGroup.describe("group") do
        # Use a sleep so there is some measurable time, to ensure
        # the reported percent is 100%, not 0%.
        example("example") { sleep 0.001 }
        example_line_number = __LINE__ - 1
      end
      group.run(double('reporter').as_null_object)

      formatter.stub(:examples) { group.examples }
      RSpec.configuration.stub(:profile_examples) { 10 }
    end

    it "names the example" do
      formatter.dump_profile_slowest_examples
      expect(output.string).to match(/group example/m)
    end

    it "prints the time" do
      formatter.dump_profile_slowest_examples
      expect(output.string).to match(/0(\.\d+)? seconds/)
    end

    it "prints the path" do
      formatter.dump_profile_slowest_examples
      filename = __FILE__.split(File::SEPARATOR).last

      expect(output.string).to match(/#{filename}\:#{example_line_number}/)
    end

    it "prints the percentage taken from the total runtime" do
      formatter.dump_profile_slowest_examples
      expect(output.string).to match(/, 100.0% of total time\):/)
    end
  end

  describe "#dump_profile_slowest_example_groups" do
    let(:group) do 
      RSpec::Core::ExampleGroup.describe("slow group") do
        # Use a sleep so there is some measurable time, to ensure
        # the reported percent is 100%, not 0%.
        example("example") { sleep 0.01 }
      end 
    end
    let(:rpt) { double('reporter').as_null_object }

    before do
      group.run(rpt)
      RSpec.configuration.stub(:profile_examples) { 10 }
    end

    context "with one example group" do
      before { formatter.stub(:examples) { group.examples } }

      it "doesn't profile a single example group" do
        formatter.dump_profile_slowest_example_groups
        expect(output.string).not_to match(/slowest example groups/)
      end
    end

    context "with multiple example groups" do
      before do
        group2 = RSpec::Core::ExampleGroup.describe("fast group") do
          example("example 1") { sleep 0.004 }
          example("example 2") { sleep 0.007 }
        end
        group2.run(rpt)

        formatter.stub(:examples) { group.examples + group2.examples }
      end

      it "prints the slowest example groups" do
        formatter.dump_profile_slowest_example_groups
        expect(output.string).to match(/slowest example groups/)
      end

      it "prints the time" do
        formatter.dump_profile_slowest_example_groups
        expect(output.string).to match(/0(\.\d+)? seconds/)
      end

      it "ranks the example groups by average time" do
        formatter.dump_profile_slowest_example_groups
        expect(output.string).to match(/slow group(.*)fast group/m)
      end
    end

    it "depends on parent_groups to get the top level example group" do
      ex = ""
      group.describe("group 2") do 
        describe "group 3" do
          ex = example("nested example 1") 
        end
      end

      expect(ex.example_group.parent_groups.last).to eq(group)
    end
  end

  describe "custom_colors" do
    it "uses the custom success color" do
      RSpec.configure do |config|
        config.color = true
        config.tty = true
        config.success_color = :cyan
      end
      formatter.dump_summary(0,1,0,0)
      expect(output.string).to include("\e[36m")
    end
  end

  describe "#colorize" do
    before do
      allow(RSpec.configuration).to receive(:color_enabled?) { true }
    end

    it "accepts a VT100 integer code and formats the text with it" do
       expect(formatter.colorize('abc', 32)).to eq "\e[32mabc\e[0m"
    end

    it "accepts a symbol as a color parameter and translates it to the correct integer code, then formats the text with it" do
       expect(formatter.colorize('abc', :green)).to eq "\e[32mabc\e[0m"
    end

    it "accepts a non-default color symbol as a parameter and translates it to the correct integer code, then formats the text with it" do
       expect(formatter.colorize('abc', :cyan)).to eq "\e[36mabc\e[0m"
    end
  end

  describe "#const_missing name" do

    it "warns of deprecation for VT100_COLORS" do
      expect_deprecation_with_call_site __FILE__, __LINE__ + 1
      expect(described_class::VT100_COLORS).to eq RSpec::Core::Formatters::ConsoleCodes::VT100_CODES
    end

    it "warns of deprecation for VT100_COLOR_CODES" do
      expect_deprecation_with_call_site __FILE__, __LINE__ + 1
      expect(described_class::VT100_COLOR_CODES).to eq RSpec::Core::Formatters::ConsoleCodes::VT100_CODES.to_set
    end

    it "behaves normally for other constants" do
      expect { described_class::NoSuchConst }.to raise_error(NameError)
    end
  end

  RSpec::Core::Formatters::ConsoleCodes::VT100_CODES.each do |name, number|
    next if name == :black || name == :bold

    describe "##{name}" do
      before do
        allow(RSpec.configuration).to receive(:color_enabled?) { true }
        allow(RSpec).to receive(:deprecate)
      end

      it "prints the text using the color code for #{name}" do
        expect(formatter.send(name, "text")).to eq("\e[#{number}mtext\e[0m")
      end

      it "prints a deprecation warning" do
        expect(RSpec).to receive(:deprecate) {|*args|
          expect(args.first).to match(/#{name}/)
        }
        formatter.send(name, "text")
      end
    end
  end
end
