2017-08-17 22:00:37 +05:30
|
|
|
require 'spec_helper'
|
|
|
|
|
|
|
|
describe Gitlab::Ci::Trace do
|
|
|
|
let(:build) { create(:ci_build) }
|
|
|
|
let(:trace) { described_class.new(build) }
|
|
|
|
|
|
|
|
describe "associations" do
|
|
|
|
it { expect(trace).to respond_to(:job) }
|
|
|
|
it { expect(trace).to delegate_method(:old_trace).to(:job) }
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#html' do
|
|
|
|
before do
|
|
|
|
trace.set("12\n34")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns formatted html" do
|
|
|
|
expect(trace.html).to eq("12<br>34")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns last line of formatted html" do
|
|
|
|
expect(trace.html(last_lines: 1)).to eq("34")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#raw' do
|
|
|
|
before do
|
|
|
|
trace.set("12\n34")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns raw output" do
|
|
|
|
expect(trace.raw).to eq("12\n34")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns last line of raw output" do
|
|
|
|
expect(trace.raw(last_lines: 1)).to eq("34")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#extract_coverage' do
|
|
|
|
let(:regex) { '\(\d+.\d+\%\) covered' }
|
|
|
|
|
|
|
|
context 'matching coverage' do
|
|
|
|
before do
|
|
|
|
trace.set('Coverage 1033 / 1051 LOC (98.29%) covered')
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns valid coverage" do
|
|
|
|
expect(trace.extract_coverage(regex)).to eq("98.29")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'no coverage' do
|
|
|
|
before do
|
|
|
|
trace.set('No coverage')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returs nil' do
|
|
|
|
expect(trace.extract_coverage(regex)).to be_nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
describe '#extract_sections' do
|
|
|
|
let(:log) { 'No sections' }
|
|
|
|
let(:sections) { trace.extract_sections }
|
|
|
|
|
|
|
|
before do
|
|
|
|
trace.set(log)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'no sections' do
|
|
|
|
it 'returs []' do
|
|
|
|
expect(trace.extract_sections).to eq([])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'multiple sections available' do
|
|
|
|
let(:log) { File.read(expand_fixture_path('trace/trace_with_sections')) }
|
|
|
|
let(:sections_data) do
|
|
|
|
[
|
|
|
|
{ name: 'prepare_script', lines: 2, duration: 3.seconds },
|
|
|
|
{ name: 'get_sources', lines: 4, duration: 1.second },
|
|
|
|
{ name: 'restore_cache', lines: 0, duration: 0.seconds },
|
|
|
|
{ name: 'download_artifacts', lines: 0, duration: 0.seconds },
|
|
|
|
{ name: 'build_script', lines: 2, duration: 1.second },
|
|
|
|
{ name: 'after_script', lines: 0, duration: 0.seconds },
|
|
|
|
{ name: 'archive_cache', lines: 0, duration: 0.seconds },
|
|
|
|
{ name: 'upload_artifacts', lines: 0, duration: 0.seconds }
|
|
|
|
]
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns valid sections" do
|
|
|
|
expect(sections).not_to be_empty
|
|
|
|
expect(sections.size).to eq(sections_data.size),
|
|
|
|
"expected #{sections_data.size} sections, got #{sections.size}"
|
|
|
|
|
|
|
|
buff = StringIO.new(log)
|
|
|
|
sections.each_with_index do |s, i|
|
|
|
|
expected = sections_data[i]
|
|
|
|
|
|
|
|
expect(s[:name]).to eq(expected[:name])
|
|
|
|
expect(s[:date_end] - s[:date_start]).to eq(expected[:duration])
|
|
|
|
|
|
|
|
buff.seek(s[:byte_start], IO::SEEK_SET)
|
|
|
|
length = s[:byte_end] - s[:byte_start]
|
|
|
|
lines = buff.read(length).count("\n")
|
|
|
|
expect(lines).to eq(expected[:lines])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'logs contains "section_start"' do
|
|
|
|
let(:log) { "section_start:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_end:1506417477:a_section\r\033[0K"}
|
|
|
|
|
|
|
|
it "returns only one section" do
|
|
|
|
expect(sections).not_to be_empty
|
|
|
|
expect(sections.size).to eq(1)
|
|
|
|
|
|
|
|
section = sections[0]
|
|
|
|
expect(section[:name]).to eq('a_section')
|
|
|
|
expect(section[:byte_start]).not_to eq(section[:byte_end]), "got an empty section"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'missing section_end' do
|
|
|
|
let(:log) { "section_start:1506417476:a_section\r\033[0KSome logs\nNo section_end\n"}
|
|
|
|
|
|
|
|
it "returns no sections" do
|
|
|
|
expect(sections).to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'missing section_start' do
|
|
|
|
let(:log) { "Some logs\nNo section_start\nsection_end:1506417476:a_section\r\033[0K"}
|
|
|
|
|
|
|
|
it "returns no sections" do
|
|
|
|
expect(sections).to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'inverted section_start section_end' do
|
|
|
|
let(:log) { "section_end:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_start:1506417477:a_section\r\033[0K"}
|
|
|
|
|
|
|
|
it "returns no sections" do
|
|
|
|
expect(sections).to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
describe '#set' do
|
|
|
|
before do
|
|
|
|
trace.set("12")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns trace" do
|
|
|
|
expect(trace.raw).to eq("12")
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'overwrite trace' do
|
|
|
|
before do
|
|
|
|
trace.set("34")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns new trace" do
|
|
|
|
expect(trace.raw).to eq("34")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'runners token' do
|
|
|
|
let(:token) { 'my_secret_token' }
|
|
|
|
|
|
|
|
before do
|
|
|
|
build.project.update(runners_token: token)
|
|
|
|
trace.set(token)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "hides token" do
|
|
|
|
expect(trace.raw).not_to include(token)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'hides build token' do
|
|
|
|
let(:token) { 'my_secret_token' }
|
|
|
|
|
|
|
|
before do
|
|
|
|
build.update(token: token)
|
|
|
|
trace.set(token)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "hides token" do
|
|
|
|
expect(trace.raw).not_to include(token)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#append' do
|
|
|
|
before do
|
|
|
|
trace.set("1234")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns correct trace" do
|
|
|
|
expect(trace.append("56", 4)).to eq(6)
|
|
|
|
expect(trace.raw).to eq("123456")
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'tries to append trace at different offset' do
|
|
|
|
it "fails with append" do
|
|
|
|
expect(trace.append("56", 2)).to eq(-4)
|
|
|
|
expect(trace.raw).to eq("1234")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'runners token' do
|
|
|
|
let(:token) { 'my_secret_token' }
|
|
|
|
|
|
|
|
before do
|
|
|
|
build.project.update(runners_token: token)
|
|
|
|
trace.append(token, 0)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "hides token" do
|
|
|
|
expect(trace.raw).not_to include(token)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'build token' do
|
|
|
|
let(:token) { 'my_secret_token' }
|
|
|
|
|
|
|
|
before do
|
|
|
|
build.update(token: token)
|
|
|
|
trace.append(token, 0)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "hides token" do
|
|
|
|
expect(trace.raw).not_to include(token)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
describe '#read' do
|
|
|
|
shared_examples 'read successfully with IO' do
|
|
|
|
it 'yields with source' do
|
|
|
|
trace.read do |stream|
|
|
|
|
expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
|
|
|
|
expect(stream.stream).to be_a(IO)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples 'read successfully with StringIO' do
|
|
|
|
it 'yields with source' do
|
|
|
|
trace.read do |stream|
|
|
|
|
expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
|
|
|
|
expect(stream.stream).to be_a(StringIO)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples 'failed to read' do
|
|
|
|
it 'yields without source' do
|
|
|
|
trace.read do |stream|
|
|
|
|
expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
|
|
|
|
expect(stream.stream).to be_nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when trace artifact exists' do
|
|
|
|
before do
|
|
|
|
create(:ci_job_artifact, :trace, job: build)
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'read successfully with IO'
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when current_path (with project_id) exists' do
|
|
|
|
before do
|
|
|
|
expect(trace).to receive(:default_path) { expand_fixture_path('trace/sample_trace') }
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'read successfully with IO'
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when current_path (with project_ci_id) exists' do
|
|
|
|
before do
|
|
|
|
expect(trace).to receive(:deprecated_path) { expand_fixture_path('trace/sample_trace') }
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'read successfully with IO'
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when db trace exists' do
|
|
|
|
before do
|
|
|
|
build.send(:write_attribute, :trace, "data")
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'read successfully with StringIO'
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when no sources exist' do
|
|
|
|
it_behaves_like 'failed to read'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
describe 'trace handling' do
|
2018-03-17 18:26:18 +05:30
|
|
|
subject { trace.exist? }
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
context 'trace does not exist' do
|
|
|
|
it { expect(trace.exist?).to be(false) }
|
|
|
|
end
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
context 'when trace artifact exists' do
|
|
|
|
before do
|
|
|
|
create(:ci_job_artifact, :trace, job: build)
|
|
|
|
end
|
|
|
|
|
|
|
|
it { is_expected.to be_truthy }
|
|
|
|
|
|
|
|
context 'when the trace artifact has been erased' do
|
|
|
|
before do
|
|
|
|
trace.erase!
|
|
|
|
end
|
|
|
|
|
|
|
|
it { is_expected.to be_falsy }
|
|
|
|
|
|
|
|
it 'removes associations' do
|
|
|
|
expect(Ci::JobArtifact.exists?(job_id: build.id, file_type: :trace)).to be_falsy
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
context 'new trace path is used' do
|
|
|
|
before do
|
|
|
|
trace.send(:ensure_directory)
|
|
|
|
|
|
|
|
File.open(trace.send(:default_path), "w") do |file|
|
|
|
|
file.write("data")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it "trace exist" do
|
|
|
|
expect(trace.exist?).to be(true)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "can be erased" do
|
|
|
|
trace.erase!
|
|
|
|
expect(trace.exist?).to be(false)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'deprecated path' do
|
|
|
|
let(:path) { trace.send(:deprecated_path) }
|
|
|
|
|
|
|
|
context 'with valid ci_id' do
|
|
|
|
before do
|
|
|
|
build.project.update(ci_id: 1000)
|
|
|
|
|
|
|
|
FileUtils.mkdir_p(File.dirname(path))
|
|
|
|
|
|
|
|
File.open(path, "w") do |file|
|
|
|
|
file.write("data")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it "trace exist" do
|
|
|
|
expect(trace.exist?).to be(true)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "can be erased" do
|
|
|
|
trace.erase!
|
|
|
|
expect(trace.exist?).to be(false)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'without valid ci_id' do
|
|
|
|
it "does not return deprecated path" do
|
|
|
|
expect(path).to be_nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'stored in database' do
|
|
|
|
before do
|
|
|
|
build.send(:write_attribute, :trace, "data")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "trace exist" do
|
|
|
|
expect(trace.exist?).to be(true)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "can be erased" do
|
|
|
|
trace.erase!
|
|
|
|
expect(trace.exist?).to be(false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns database data" do
|
|
|
|
expect(trace.raw).to eq("data")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2018-03-27 19:54:05 +05:30
|
|
|
|
|
|
|
describe '#archive!' do
|
|
|
|
subject { trace.archive! }
|
|
|
|
|
|
|
|
shared_examples 'archive trace file' do
|
|
|
|
it do
|
|
|
|
expect { subject }.to change { Ci::JobArtifact.count }.by(1)
|
|
|
|
|
|
|
|
build.reload
|
|
|
|
expect(build.trace.exist?).to be_truthy
|
|
|
|
expect(build.job_artifacts_trace.file.exists?).to be_truthy
|
|
|
|
expect(build.job_artifacts_trace.file.filename).to eq('job.log')
|
|
|
|
expect(File.exist?(src_path)).to be_falsy
|
|
|
|
expect(src_checksum)
|
|
|
|
.to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
|
|
|
|
expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples 'source trace file stays intact' do |error:|
|
|
|
|
it do
|
|
|
|
expect { subject }.to raise_error(error)
|
|
|
|
|
|
|
|
build.reload
|
|
|
|
expect(build.trace.exist?).to be_truthy
|
|
|
|
expect(build.job_artifacts_trace).to be_nil
|
|
|
|
expect(File.exist?(src_path)).to be_truthy
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples 'archive trace in database' do
|
|
|
|
it do
|
|
|
|
expect { subject }.to change { Ci::JobArtifact.count }.by(1)
|
|
|
|
|
|
|
|
build.reload
|
|
|
|
expect(build.trace.exist?).to be_truthy
|
|
|
|
expect(build.job_artifacts_trace.file.exists?).to be_truthy
|
|
|
|
expect(build.job_artifacts_trace.file.filename).to eq('job.log')
|
|
|
|
expect(build.old_trace).to be_nil
|
|
|
|
expect(src_checksum)
|
|
|
|
.to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
|
|
|
|
expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples 'source trace in database stays intact' do |error:|
|
|
|
|
it do
|
|
|
|
expect { subject }.to raise_error(error)
|
|
|
|
|
|
|
|
build.reload
|
|
|
|
expect(build.trace.exist?).to be_truthy
|
|
|
|
expect(build.job_artifacts_trace).to be_nil
|
|
|
|
expect(build.old_trace).to eq(trace_content)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when job does not have trace artifact' do
|
|
|
|
context 'when trace file stored in default path' do
|
|
|
|
let!(:build) { create(:ci_build, :success, :trace_live) }
|
|
|
|
let!(:src_path) { trace.read { |s| return s.path } }
|
|
|
|
let!(:src_checksum) { Digest::SHA256.file(src_path).hexdigest }
|
|
|
|
|
|
|
|
it_behaves_like 'archive trace file'
|
|
|
|
|
|
|
|
context 'when failed to create clone file' do
|
|
|
|
before do
|
|
|
|
allow(IO).to receive(:copy_stream).and_return(0)
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'source trace file stays intact', error: Gitlab::Ci::Trace::ArchiveError
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when failed to create job artifact record' do
|
|
|
|
before do
|
|
|
|
allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
|
|
|
|
allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
|
|
|
|
.and_return(%w[Error Error])
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'source trace file stays intact', error: ActiveRecord::RecordInvalid
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when trace is stored in database' do
|
|
|
|
let(:build) { create(:ci_build, :success) }
|
|
|
|
let(:trace_content) { 'Sample trace' }
|
|
|
|
let!(:src_checksum) { Digest::SHA256.hexdigest(trace_content) }
|
|
|
|
|
|
|
|
before do
|
|
|
|
build.update_column(:trace, trace_content)
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'archive trace in database'
|
|
|
|
|
|
|
|
context 'when failed to create clone file' do
|
|
|
|
before do
|
|
|
|
allow(IO).to receive(:copy_stream).and_return(0)
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'source trace in database stays intact', error: Gitlab::Ci::Trace::ArchiveError
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when failed to create job artifact record' do
|
|
|
|
before do
|
|
|
|
allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
|
|
|
|
allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
|
|
|
|
.and_return(%w[Error Error])
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'source trace in database stays intact', error: ActiveRecord::RecordInvalid
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when job has trace artifact' do
|
|
|
|
before do
|
|
|
|
create(:ci_job_artifact, :trace, job: build)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not archive' do
|
|
|
|
expect_any_instance_of(described_class).not_to receive(:archive_stream!)
|
|
|
|
expect { subject }.to raise_error('Already archived')
|
|
|
|
expect(build.job_artifacts_trace.file.exists?).to be_truthy
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when job is not finished yet' do
|
|
|
|
let!(:build) { create(:ci_build, :running, :trace_live) }
|
|
|
|
|
|
|
|
it 'does not archive' do
|
|
|
|
expect_any_instance_of(described_class).not_to receive(:archive_stream!)
|
|
|
|
expect { subject }.to raise_error('Job is not finished yet')
|
|
|
|
expect(build.trace.exist?).to be_truthy
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2017-08-17 22:00:37 +05:30
|
|
|
end
|