require 'spec_helper' describe Gitlab::Ci::Trace::Stream do describe 'delegates' do subject { described_class.new { nil } } it { is_expected.to delegate_method(:close).to(:stream) } it { is_expected.to delegate_method(:tell).to(:stream) } it { is_expected.to delegate_method(:seek).to(:stream) } it { is_expected.to delegate_method(:size).to(:stream) } it { is_expected.to delegate_method(:path).to(:stream) } it { is_expected.to delegate_method(:truncate).to(:stream) } it { is_expected.to delegate_method(:valid?).to(:stream).as(:present?) } it { is_expected.to delegate_method(:file?).to(:path).as(:present?) } end describe '#limit' do let(:stream) do described_class.new do StringIO.new((1..8).to_a.join("\n")) end end it 'if size is larger we start from beginning' do stream.limit(20) expect(stream.tell).to eq(0) end it 'if size is smaller we start from the end' do stream.limit(2) expect(stream.raw).to eq("8") end context 'when the trace contains ANSI sequence and Unicode' do let(:stream) do described_class.new do File.open(expand_fixture_path('trace/ansi-sequence-and-unicode')) end end it 'forwards to the next linefeed, case 1' do stream.limit(7) result = stream.raw expect(result).to eq('') expect(result.encoding).to eq(Encoding.default_external) end it 'forwards to the next linefeed, case 2' do stream.limit(29) result = stream.raw expect(result).to eq("\e[01;32m許功蓋\e[0m\n") expect(result.encoding).to eq(Encoding.default_external) end # See https://gitlab.com/gitlab-org/gitlab-ce/issues/30796 it 'reads in binary, output as Encoding.default_external' do stream.limit(52) result = stream.html expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ
許功蓋
") expect(result.encoding).to eq(Encoding.default_external) end end end describe '#append' do let(:tempfile) { Tempfile.new } let(:stream) do described_class.new do tempfile.write("12345678") tempfile.rewind tempfile end end after do tempfile.unlink end it "truncates and append content" do stream.append("89", 4) stream.seek(0) expect(stream.size).to eq(6) expect(stream.raw).to eq("123489") end it 'appends in binary mode' do '😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset| stream.append(byte, offset) end stream.seek(0) expect(stream.size).to eq(4) expect(stream.raw).to eq('😺') end end describe '#set' do let(:stream) do described_class.new do StringIO.new("12345678") end end before do stream.set("8901") end it "overwrite content" do stream.seek(0) expect(stream.size).to eq(4) expect(stream.raw).to eq("8901") end end describe '#raw' do let(:path) { __FILE__ } let(:lines) { File.readlines(path) } let(:stream) do described_class.new do File.open(path) end end it 'returns all contents if last_lines is not specified' do result = stream.raw expect(result).to eq(lines.join) expect(result.encoding).to eq(Encoding.default_external) end context 'limit max lines' do before do # specifying BUFFER_SIZE forces to seek backwards allow(described_class).to receive(:BUFFER_SIZE) .and_return(2) end it 'returns last few lines' do result = stream.raw(last_lines: 2) expect(result).to eq(lines.last(2).join) expect(result.encoding).to eq(Encoding.default_external) end it 'returns everything if trying to get too many lines' do result = stream.raw(last_lines: lines.size * 2) expect(result).to eq(lines.join) expect(result.encoding).to eq(Encoding.default_external) end end end describe '#html_with_state' do let(:stream) do described_class.new do StringIO.new("1234") end end it 'returns html content with state' do result = stream.html_with_state expect(result.html).to eq("1234") end context 'follow-up state' do let!(:last_result) { stream.html_with_state } before do stream.append("5678", 4) stream.seek(0) end it "returns appended trace" do result = stream.html_with_state(last_result.state) expect(result.append).to be_truthy expect(result.html).to eq("5678") end end end describe '#html' do let(:stream) do described_class.new do StringIO.new("12\n34\n56") end end it "returns html" do expect(stream.html).to eq("12
34
56") end it "returns html for last line only" do expect(stream.html(last_lines: 1)).to eq("56") end end describe '#extract_coverage' do let(:stream) do described_class.new do StringIO.new(data) end end subject { stream.extract_coverage(regex) } context 'valid content & regex' do let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } let(:regex) { '\(\d+.\d+\%\) covered' } it { is_expected.to eq("98.29") } end context 'valid content & bad regex' do let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' } let(:regex) { 'very covered' } it { is_expected.to be_nil } end context 'no coverage content & regex' do let(:data) { 'No coverage for today :sad:' } let(:regex) { '\(\d+.\d+\%\) covered' } it { is_expected.to be_nil } end context 'multiple results in content & regex' do let(:data) do <<~HEREDOC (98.39%) covered (98.29%) covered HEREDOC end let(:regex) { '\(\d+.\d+\%\) covered' } it 'returns the last matched coverage' do is_expected.to eq("98.29") end end context 'when BUFFER_SIZE is smaller than stream.size' do let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' } let(:regex) { '\(\d+.\d+\%\) covered' } before do stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5) end it { is_expected.to eq("98.29") } end context 'when regex is multi-byte char' do let(:data) { '95.0 ゴッドファット\n' } let(:regex) { '\d+\.\d+ ゴッドファット' } before do stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5) end it { is_expected.to eq('95.0') } end context 'when BUFFER_SIZE is equal to stream.size' do let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' } let(:regex) { '\(\d+.\d+\%\) covered' } before do stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', data.length) end it { is_expected.to eq("98.29") } end context 'using a regex capture' do let(:data) { 'TOTAL 9926 3489 65%' } let(:regex) { 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)' } it { is_expected.to eq("65") } end context 'malicious regexp' do let(:data) { malicious_text } let(:regex) { malicious_regexp } include_examples 'malicious regexp' end context 'multi-line data with rooted regexp' do let(:data) { "\n65%\n" } let(:regex) { '^(\d+)\%$' } it { is_expected.to eq('65') } end context 'long line' do let(:data) { 'a' * 80000 + '100%' + 'a' * 80000 } let(:regex) { '\d+\%' } it { is_expected.to eq('100') } end context 'many lines' do let(:data) { "foo\n" * 80000 + "100%\n" + "foo\n" * 80000 } let(:regex) { '\d+\%' } it { is_expected.to eq('100') } end context 'empty regex' do let(:data) { 'foo' } let(:regex) { '' } it 'skips processing' do expect(stream).not_to receive(:read) is_expected.to be_nil end end context 'nil regex' do let(:data) { 'foo' } let(:regex) { nil } it 'skips processing' do expect(stream).not_to receive(:read) is_expected.to be_nil end end end end