Commit d4290995 authored by Shinya Maeda's avatar Shinya Maeda

Add tests for Trace::Stream

parent b94c84e5
...@@ -24,7 +24,7 @@ module Ci ...@@ -24,7 +24,7 @@ module Ci
raw_data raw_data
else else
raise 'Unsupported data store' raise 'Unsupported data store'
end end&.force_encoding(Encoding::BINARY)
end end
def set_data(value) def set_data(value)
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Trace::Stream do describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
set(:job) { create(:ci_build, :running) }
before do
stub_feature_flags(ci_enable_live_trace: true)
end
describe 'delegates' do describe 'delegates' do
subject { described_class.new { nil } } subject { described_class.new { nil } }
...@@ -15,333 +21,465 @@ describe Gitlab::Ci::Trace::Stream do ...@@ -15,333 +21,465 @@ describe Gitlab::Ci::Trace::Stream do
end end
describe '#limit' do describe '#limit' do
let(:stream) do shared_examples_for 'limits' do
described_class.new do it 'if size is larger we start from beginning' do
StringIO.new((1..8).to_a.join("\n")) stream.limit(20)
expect(stream.tell).to eq(0)
end end
end
it 'if size is larger we start from beginning' do it 'if size is smaller we start from the end' do
stream.limit(20) stream.limit(2)
expect(stream.tell).to eq(0) expect(stream.raw).to eq("8")
end end
it 'if size is smaller we start from the end' do context 'when the trace contains ANSI sequence and Unicode' do
stream.limit(2) let(:stream) do
described_class.new do
File.open(expand_fixture_path('trace/ansi-sequence-and-unicode'))
end
end
expect(stream.raw).to eq("8") it 'forwards to the next linefeed, case 1' do
end stream.limit(7)
context 'when the trace contains ANSI sequence and Unicode' do result = stream.raw
let(:stream) do
described_class.new do expect(result).to eq('')
File.open(expand_fixture_path('trace/ansi-sequence-and-unicode')) expect(result.encoding).to eq(Encoding.default_external)
end end
end
it 'forwards to the next linefeed, case 1' do it 'forwards to the next linefeed, case 2' do
stream.limit(7) stream.limit(29)
result = stream.raw result = stream.raw
expect(result).to eq('') expect(result).to eq("\e[01;32m許功蓋\e[0m\n")
expect(result.encoding).to eq(Encoding.default_external) expect(result.encoding).to eq(Encoding.default_external)
end end
it 'forwards to the next linefeed, case 2' do # See https://gitlab.com/gitlab-org/gitlab-ce/issues/30796
stream.limit(29) it 'reads in binary, output as Encoding.default_external' do
stream.limit(52)
result = stream.raw result = stream.html
expect(result).to eq("\e[01;32m許功蓋\e[0m\n") expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>")
expect(result.encoding).to eq(Encoding.default_external) expect(result.encoding).to eq(Encoding.default_external)
end
end end
end
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/30796 context 'when stream is StringIO' do
it 'reads in binary, output as Encoding.default_external' do let(:stream) do
stream.limit(52) described_class.new do
StringIO.new((1..8).to_a.join("\n"))
end
end
result = stream.html it_behaves_like 'limits'
end
expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>") context 'when stream is ChunkedIO' do
expect(result.encoding).to eq(Encoding.default_external) let(:stream) do
described_class.new do
Gitlab::Ci::Trace::ChunkedIO.new(job).tap do |chunked_io|
chunked_io.write((1..8).to_a.join("\n"))
chunked_io.seek(0, IO::SEEK_SET)
end
end
end end
it_behaves_like 'limits'
end end
end end
describe '#append' do describe '#append' do
let(:tempfile) { Tempfile.new } shared_examples_for 'appends' do
it "truncates and append content" do
stream.append("89", 4)
stream.seek(0)
let(:stream) do expect(stream.size).to eq(6)
described_class.new do expect(stream.raw).to eq("123489")
tempfile.write("12345678")
tempfile.rewind
tempfile
end end
end
after do it 'appends in binary mode' do
tempfile.unlink '😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset|
end stream.append(byte, offset)
end
it "truncates and append content" do stream.seek(0)
stream.append("89", 4)
stream.seek(0)
expect(stream.size).to eq(6) expect(stream.size).to eq(4)
expect(stream.raw).to eq("123489") expect(stream.raw).to eq('😺')
end
end end
it 'appends in binary mode' do context 'when stream is StringIO' do
'😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset| let(:tempfile) { Tempfile.new }
stream.append(byte, offset)
let(:stream) do
described_class.new do
tempfile.write("12345678")
tempfile.rewind
tempfile
end
end end
stream.seek(0) after do
tempfile.unlink
end
expect(stream.size).to eq(4) it_behaves_like 'appends'
expect(stream.raw).to eq('😺') end
context 'when stream is ChunkedIO' do
let(:stream) do
described_class.new do
Gitlab::Ci::Trace::ChunkedIO.new(job).tap do |chunked_io|
chunked_io.write('12345678')
chunked_io.seek(0, IO::SEEK_SET)
end
end
end
it_behaves_like 'appends'
end end
end end
describe '#set' do describe '#set' do
let(:stream) do shared_examples_for 'sets' do
described_class.new do before do
StringIO.new("12345678") 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
end end
before do context 'when stream is StringIO' do
stream.set("8901") let(:stream) do
described_class.new do
StringIO.new("12345678")
end
end
it_behaves_like 'sets'
end end
it "overwrite content" do context 'when stream is ChunkedIO' do
stream.seek(0) let(:stream) do
described_class.new do
Gitlab::Ci::Trace::ChunkedIO.new(job).tap do |chunked_io|
chunked_io.write('12345678')
chunked_io.seek(0, IO::SEEK_SET)
end
end
end
expect(stream.size).to eq(4) it_behaves_like 'sets'
expect(stream.raw).to eq("8901")
end end
end end
describe '#raw' do describe '#raw' do
let(:path) { __FILE__ } shared_examples_for 'sets' do
let(:lines) { File.readlines(path) } it 'returns all contents if last_lines is not specified' do
let(:stream) do result = stream.raw
described_class.new do
File.open(path) expect(result).to eq(lines.join)
expect(result.encoding).to eq(Encoding.default_external)
end end
end
it 'returns all contents if last_lines is not specified' do context 'limit max lines' do
result = stream.raw before do
# specifying BUFFER_SIZE forces to seek backwards
allow(described_class).to receive(:BUFFER_SIZE)
.and_return(2)
end
expect(result).to eq(lines.join) it 'returns last few lines' do
expect(result.encoding).to eq(Encoding.default_external) result = stream.raw(last_lines: 2)
end
context 'limit max lines' do expect(result).to eq(lines.last(2).join)
before do expect(result.encoding).to eq(Encoding.default_external)
# specifying BUFFER_SIZE forces to seek backwards end
allow(described_class).to receive(:BUFFER_SIZE)
.and_return(2)
end
it 'returns last few lines' do it 'returns everything if trying to get too many lines' do
result = stream.raw(last_lines: 2) result = stream.raw(last_lines: lines.size * 2)
expect(result).to eq(lines.last(2).join) expect(result).to eq(lines.join)
expect(result.encoding).to eq(Encoding.default_external) expect(result.encoding).to eq(Encoding.default_external)
end
end end
end
it 'returns everything if trying to get too many lines' do let(:path) { __FILE__ }
result = stream.raw(last_lines: lines.size * 2) let(:lines) { File.readlines(path) }
expect(result).to eq(lines.join) context 'when stream is File' do
expect(result.encoding).to eq(Encoding.default_external) let(:stream) do
described_class.new do
File.open(path)
end
end end
end end
context 'when stream is ChunkedIO' do
let(:stream) do
described_class.new do
Gitlab::Ci::Trace::ChunkedIO.new(job).tap do |chunked_io|
chunked_io.write(File.binread(path))
chunked_io.seek(0, IO::SEEK_SET)
end
end
end
it_behaves_like 'sets'
end
end end
describe '#html_with_state' do describe '#html_with_state' do
let(:stream) do shared_examples_for 'html_with_states' do
described_class.new do it 'returns html content with state' do
StringIO.new("1234") result = stream.html_with_state
expect(result.html).to eq("1234")
end end
end
it 'returns html content with state' do context 'follow-up state' do
result = stream.html_with_state let!(:last_result) { stream.html_with_state }
expect(result.html).to eq("1234") before do
end stream.append("5678", 4)
stream.seek(0)
end
context 'follow-up state' do it "returns appended trace" do
let!(:last_result) { stream.html_with_state } result = stream.html_with_state(last_result.state)
before do expect(result.append).to be_truthy
stream.append("5678", 4) expect(result.html).to eq("5678")
stream.seek(0) end
end
end
context 'when stream is StringIO' do
let(:stream) do
described_class.new do
StringIO.new("1234")
end
end end
it "returns appended trace" do it_behaves_like 'html_with_states'
result = stream.html_with_state(last_result.state) end
expect(result.append).to be_truthy context 'when stream is ChunkedIO' do
expect(result.html).to eq("5678") let(:stream) do
described_class.new do
Gitlab::Ci::Trace::ChunkedIO.new(job).tap do |chunked_io|
chunked_io.write("1234")
chunked_io.seek(0, IO::SEEK_SET)
end
end
end end
it_behaves_like 'html_with_states'
end end
end end
describe '#html' do describe '#html' do
let(:stream) do shared_examples_for 'htmls' do
described_class.new do it "returns html" do
StringIO.new("12\n34\n56") expect(stream.html).to eq("12<br>34<br>56")
end
it "returns html for last line only" do
expect(stream.html(last_lines: 1)).to eq("56")
end end
end end
it "returns html" do context 'when stream is StringIO' do
expect(stream.html).to eq("12<br>34<br>56") let(:stream) do
described_class.new do
StringIO.new("12\n34\n56")
end
end
it_behaves_like 'htmls'
end end
it "returns html for last line only" do context 'when stream is ChunkedIO' do
expect(stream.html(last_lines: 1)).to eq("56") let(:stream) do
described_class.new do
Gitlab::Ci::Trace::ChunkedIO.new(job).tap do |chunked_io|
chunked_io.write("12\n34\n56")
chunked_io.seek(0, IO::SEEK_SET)
end
end
end
it_behaves_like 'htmls'
end end
end end
describe '#extract_coverage' do describe '#extract_coverage' do
let(:stream) do shared_examples_for 'extract_coverages' do
described_class.new do context 'valid content & regex' do
StringIO.new(data) let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
end let(:regex) { '\(\d+.\d+\%\) covered' }
end
subject { stream.extract_coverage(regex) } it { is_expected.to eq("98.29") }
end
context 'valid content & regex' do context 'valid content & bad regex' do
let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
let(:regex) { '\(\d+.\d+\%\) covered' } let(:regex) { 'very covered' }
it { is_expected.to eq("98.29") } it { is_expected.to be_nil }
end end
context 'valid content & bad regex' do context 'no coverage content & regex' do
let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' } let(:data) { 'No coverage for today :sad:' }
let(:regex) { 'very covered' } let(:regex) { '\(\d+.\d+\%\) covered' }
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
context 'no coverage content & regex' do context 'multiple results in content & regex' do
let(:data) { 'No coverage for today :sad:' } let(:data) do
let(:regex) { '\(\d+.\d+\%\) covered' } <<~HEREDOC
(98.39%) covered
(98.29%) covered
HEREDOC
end
it { is_expected.to be_nil } let(:regex) { '\(\d+.\d+\%\) covered' }
end
context 'multiple results in content & regex' do it 'returns the last matched coverage' do
let(:data) do is_expected.to eq("98.29")
<<~HEREDOC end
(98.39%) covered
(98.29%) covered
HEREDOC
end end
let(:regex) { '\(\d+.\d+\%\) covered' } 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' }
it 'returns the last matched coverage' do before do
is_expected.to eq("98.29") stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
end
it { is_expected.to eq("98.29") }
end end
end
context 'when BUFFER_SIZE is smaller than stream.size' do context 'when regex is multi-byte char' do
let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' } let(:data) { '95.0 ゴッドファット\n' }
let(:regex) { '\(\d+.\d+\%\) covered' } let(:regex) { '\d+\.\d+ ゴッドファット' }
before do before do
stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5) stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
end
it { is_expected.to eq('95.0') }
end end
it { is_expected.to eq("98.29") } context 'when BUFFER_SIZE is equal to stream.size' do
end let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
let(:regex) { '\(\d+.\d+\%\) covered' }
context 'when regex is multi-byte char' do before do
let(:data) { '95.0 ゴッドファット\n' } stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', data.length)
let(:regex) { '\d+\.\d+ ゴッドファット' } end
before do it { is_expected.to eq("98.29") }
stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
end end
it { is_expected.to eq('95.0') } context 'using a regex capture' do
end let(:data) { 'TOTAL 9926 3489 65%' }
let(:regex) { 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)' }
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 it { is_expected.to eq("65") }
stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', data.length)
end end
it { is_expected.to eq("98.29") } context 'malicious regexp' do
end let(:data) { malicious_text }
let(:regex) { malicious_regexp }
context 'using a regex capture' do include_examples 'malicious regexp'
let(:data) { 'TOTAL 9926 3489 65%' } end
let(:regex) { 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)' }
it { is_expected.to eq("65") } context 'multi-line data with rooted regexp' do
end let(:data) { "\n65%\n" }
let(:regex) { '^(\d+)\%$' }
context 'malicious regexp' do it { is_expected.to eq('65') }
let(:data) { malicious_text } end
let(:regex) { malicious_regexp }
include_examples 'malicious regexp' context 'long line' do
end let(:data) { 'a' * 80000 + '100%' + 'a' * 80000 }
let(:regex) { '\d+\%' }
context 'multi-line data with rooted regexp' do it { is_expected.to eq('100') }
let(:data) { "\n65%\n" } end
let(:regex) { '^(\d+)\%$' }
it { is_expected.to eq('65') } context 'many lines' do
end let(:data) { "foo\n" * 80000 + "100%\n" + "foo\n" * 80000 }
let(:regex) { '\d+\%' }
context 'long line' do it { is_expected.to eq('100') }
let(:data) { 'a' * 80000 + '100%' + 'a' * 80000 } end
let(:regex) { '\d+\%' }
it { is_expected.to eq('100') } context 'empty regex' do
end let(:data) { 'foo' }
let(:regex) { '' }
context 'many lines' do it 'skips processing' do
let(:data) { "foo\n" * 80000 + "100%\n" + "foo\n" * 80000 } expect(stream).not_to receive(:read)
let(:regex) { '\d+\%' }
it { is_expected.to eq('100') } is_expected.to be_nil
end end
end
context 'empty regex' do context 'nil regex' do
let(:data) { 'foo' } let(:data) { 'foo' }
let(:regex) { '' } let(:regex) { nil }
it 'skips processing' do it 'skips processing' do
expect(stream).not_to receive(:read) expect(stream).not_to receive(:read)
is_expected.to be_nil is_expected.to be_nil
end
end end
end end
context 'nil regex' do subject { stream.extract_coverage(regex) }
let(:data) { 'foo' }
let(:regex) { nil }
it 'skips processing' do context 'when stream is StringIO' do
expect(stream).not_to receive(:read) let(:stream) do
described_class.new do
StringIO.new(data)
end
end
it_behaves_like 'extract_coverages'
end
is_expected.to be_nil context 'when stream is ChunkedIO' do
let(:stream) do
described_class.new do
Gitlab::Ci::Trace::ChunkedIO.new(job).tap do |chunked_io|
chunked_io.write(data)
chunked_io.seek(0, IO::SEEK_SET)
end
end
end end
it_behaves_like 'extract_coverages'
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment