summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorYorick Peterse <yorickpeterse@gmail.com>2019-01-21 17:19:22 +0100
committerYorick Peterse <yorickpeterse@gmail.com>2019-02-04 16:19:53 +0100
commit814d9c82e32008f51ee0b0e97a52e58335951508 (patch)
treeb43f5197bc6fbd1807b1226bb12b213ecabb77cc /spec
parent2b0f4df0217b4a4aee53f964610d66ceedb68dca (diff)
downloadgitlab-ce-build-annotations.tar.gz
WIP: Add support for CI build annotationsbuild-annotations
Diffstat (limited to 'spec')
-rw-r--r--spec/lib/gitlab/ci/parsers/build_annotation_spec.rb213
-rw-r--r--spec/models/ci/build_annotation_spec.rb84
-rw-r--r--spec/models/ci/build_spec.rb45
-rw-r--r--spec/services/ci/create_build_annotations_service_spec.rb118
-rw-r--r--spec/workers/ci/create_build_annotations_worker_spec.rb37
5 files changed, 497 insertions, 0 deletions
diff --git a/spec/lib/gitlab/ci/parsers/build_annotation_spec.rb b/spec/lib/gitlab/ci/parsers/build_annotation_spec.rb
new file mode 100644
index 00000000000..1b2c262f096
--- /dev/null
+++ b/spec/lib/gitlab/ci/parsers/build_annotation_spec.rb
@@ -0,0 +1,213 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Parsers::BuildAnnotation do
+ let(:parser) { described_class.new }
+
+ describe '#parse!' do
+ context 'when the annotations artifact is too large' do
+ it 'raises a ParserError' do
+ artifact = double(:artifact, job_id: 1, size: 20.megabytes)
+
+ expect { parser.parse!(artifact) }
+ .to raise_error(described_class::ParserError)
+ end
+ end
+
+ context 'when the annotations artifact is small enough' do
+ it 'parses the JSON and returns the build annotations' do
+ json = JSON.dump([
+ {
+ 'severity' => 'info',
+ 'summary' => 'summary here',
+ 'description' => 'description here',
+ 'line_number' => 14,
+ 'file_path' => 'README.md'
+ }
+ ])
+
+ artifact = double(:artifact, job_id: 1, size: 4)
+
+ allow(artifact)
+ .to receive(:each_blob)
+ .and_yield(json)
+
+ annotations = parser.parse!(artifact)
+
+ expect(annotations).to eq([
+ {
+ build_id: 1,
+ severity: Ci::BuildAnnotation.severities[:info],
+ summary: 'summary here',
+ description: 'description here',
+ line_number: 14,
+ file_path: 'README.md'
+ }
+ ])
+ end
+ end
+ end
+
+ describe '#parse_json' do
+ context 'when the JSON object contains too many nested objects' do
+ it 'raises a ParserError' do
+ json = JSON.dump([[[[[[{}]]]]]])
+
+ expect { parser.parse_json(json) }
+ .to raise_error(described_class::ParserError)
+ end
+ end
+
+ context 'when the JSON contains too many entries' do
+ it 'raises a ParserError' do
+ json = JSON.dump(Array.new(described_class::MAX_ENTRIES + 1) { {} })
+
+ expect { parser.parse_json(json) }
+ .to raise_error(described_class::ParserError)
+ end
+ end
+
+ context 'when the JSON is valid' do
+ it 'returns the annotations' do
+ json = JSON.dump([{}])
+
+ expect(parser.parse_json(json)).to eq([{}])
+ end
+ end
+ end
+
+ describe '#build_annotation_rows' do
+ context 'when one of the entries is not a JSON object' do
+ it 'raises a ParserError' do
+ artifact = double(:artifact)
+
+ expect { parser.build_annotation_rows(artifact, [10]) }
+ .to raise_error(described_class::ParserError)
+ end
+ end
+
+ context 'when one of the JSON objects is invalid' do
+ it 'raises a ParserError' do
+ artifact = double(:artifact, job_id: 1)
+
+ expect { parser.build_annotation_rows(artifact, [{}]) }
+ .to raise_error(described_class::ParserError)
+ end
+ end
+
+ context 'when the JSON objects are valid' do
+ it 'returns the database attributes' do
+ artifact = double(:artifact, job_id: 1)
+ annotations = [
+ {
+ 'severity' => 'info',
+ 'summary' => 'summary here',
+ 'description' => 'description here',
+ 'line_number' => 14,
+ 'file_path' => 'README.md'
+ }
+ ]
+
+ attributes = parser.build_annotation_rows(artifact, annotations)
+
+ expect(attributes).to eq([
+ {
+ build_id: 1,
+ severity: Ci::BuildAnnotation.severities[:info],
+ summary: 'summary here',
+ description: 'description here',
+ line_number: 14,
+ file_path: 'README.md'
+ }
+ ])
+ end
+ end
+ end
+
+ describe '#validate_database_attributes!' do
+ context 'when the attributes are invalid' do
+ it 'raises a ParserError' do
+ annotation = {
+ 'severity' => 'info',
+ 'summary' => 'summary here',
+ 'description' => 'description here',
+ 'line_number' => 14
+ }
+
+ attributes = {
+ build_id: 1,
+ severity: Ci::BuildAnnotation.severities[:info],
+ summary: 'summary here',
+ description: 'description here',
+ line_number: 14
+ }
+
+ expect { parser.validate_database_attributes!(annotation, attributes) }
+ .to raise_error(described_class::ParserError)
+ end
+ end
+
+ context 'when the attributes are valid' do
+ it 'returns nil' do
+ annotation = {
+ 'severity' => 'info',
+ 'summary' => 'summary here',
+ 'description' => 'description here'
+ }
+
+ attributes = {
+ build_id: 1,
+ severity: Ci::BuildAnnotation.severities[:info],
+ summary: 'summary here',
+ description: 'description here'
+ }
+
+ expect(parser.validate_database_attributes!(annotation, attributes))
+ .to be_nil
+ end
+ end
+
+ it 'does not execute any SQL queries' do
+ annotation = {
+ 'severity' => 'info',
+ 'summary' => 'summary here',
+ 'description' => 'description here',
+ 'line_number' => 14,
+ 'file_path' => 'README.md'
+ }
+
+ attributes = {
+ build_id: 1,
+ severity: Ci::BuildAnnotation.severities[:info],
+ summary: 'summary here',
+ description: 'description here',
+ line_number: 14,
+ file_path: 'README.md'
+ }
+
+ # It's possible that in a rare case this test is the very first test to
+ # run. This means the schema for `Ci::BuildAnnotation` might not be loaded
+ # yet, which would cause the below assertion to fail.
+ #
+ # To prevent this from happening, we ensure the schema is always present
+ # before testing the number of queries executed during validation.
+ Ci::BuildAnnotation.columns
+
+ queries = ActiveRecord::QueryRecorder
+ .new { parser.validate_database_attributes!(annotation, attributes) }
+ .count
+
+ expect(queries).to be_zero
+ end
+ end
+
+ describe '#parser_error!' do
+ it 'raises a ParserError' do
+ expect { parser.parser_error!({}, 'foo') }.to raise_error(
+ described_class::ParserError,
+ 'The annotation {} is invalid: foo'
+ )
+ end
+ end
+end
diff --git a/spec/models/ci/build_annotation_spec.rb b/spec/models/ci/build_annotation_spec.rb
new file mode 100644
index 00000000000..7b716be48ab
--- /dev/null
+++ b/spec/models/ci/build_annotation_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::BuildAnnotation do
+ def annotation(*args)
+ described_class.new(*args).tap do |annotation|
+ annotation.validate
+ end
+ end
+
+ describe 'when validating the severity' do
+ it 'produces an error if it is missing' do
+ expect(annotation.errors[:severity]).not_to be_empty
+ end
+ end
+
+ describe 'when validating the CI build ID' do
+ it 'produces an error if it is missing' do
+ expect(annotation.errors[:build_id]).not_to be_empty
+ end
+ end
+
+ describe 'when validating the summary' do
+ it 'produces an error if it is missing' do
+ expect(annotation.errors[:summary]).not_to be_empty
+ end
+
+ it 'produces an error if it exceeds the maximum length' do
+ expect(annotation(summary: 'a' * 1024).errors[:summary]).not_to be_empty
+ end
+
+ it 'does not produce an error if it is valid' do
+ expect(annotation(summary: 'a').errors[:summary]).to be_empty
+ end
+ end
+
+ describe 'when validating the line number' do
+ it 'produces an error if it is zero' do
+ expect(annotation(line_number: 0).errors[:line_number]).not_to be_empty
+ end
+
+ it 'produces an error if it is negative' do
+ expect(annotation(line_number: -1).errors[:line_number]).not_to be_empty
+ end
+
+ it 'produces an error if it is too great' do
+ expect(annotation(line_number: 40_000).errors[:line_number])
+ .not_to be_empty
+ end
+
+ it 'produces an error if the file path is not present' do
+ expect(annotation(line_number: 1).errors[:file_path]).not_to be_empty
+ end
+
+ it 'does not produce an error if it is valid' do
+ row = annotation(line_number: 1, file_path: 'foo.rb')
+
+ expect(row.errors[:line_number]).to be_empty
+ expect(row.errors[:file_path]).to be_empty
+ end
+
+ it 'does not produce an error if it and the file path are not given' do
+ row = annotation
+
+ expect(row.errors[:line_number]).to be_empty
+ expect(row.errors[:file_path]).to be_empty
+ end
+ end
+
+ describe 'when validating the file path' do
+ it 'produces an error if the line number is not present' do
+ expect(annotation(file_path: 'foo.rb').errors[:line_number])
+ .not_to be_empty
+ end
+
+ it 'does not produce an error if it is valid' do
+ row = annotation(line_number: 1, file_path: 'foo.rb')
+
+ expect(row.errors[:file_path]).to be_empty
+ expect(row.errors[:line_number]).to be_empty
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 8a1bbb26e57..0550b456c21 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -3602,4 +3602,49 @@ describe Ci::Build do
end
end
end
+
+ describe '#first_build_annotation_artifact' do
+ context 'when there are no build annotations' do
+ it 'returns nil' do
+ build = create(:ci_build)
+
+ expect(build.first_build_annotation_artifact).to be_nil
+ end
+ end
+
+ context 'when there is one build annotation artifact' do
+ it 'returns the artifact' do
+ build = create(:ci_build)
+ artifact = create(
+ :ci_job_artifact,
+ job: build,
+ file_type: :annotation,
+ file_format: :gzip
+ )
+
+ expect(build.first_build_annotation_artifact).to eq(artifact)
+ end
+ end
+
+ context 'when there are multiple build annotation artifacts' do
+ it 'returns the oldest artifact' do
+ build = create(:ci_build)
+ artifact1 = create(
+ :ci_job_artifact,
+ job: build,
+ file_type: :annotation,
+ file_format: :gzip
+ )
+
+ create(
+ :ci_job_artifact,
+ job: build,
+ file_type: :annotation,
+ file_format: :gzip
+ )
+
+ expect(build.first_build_annotation_artifact).to eq(artifact1)
+ end
+ end
+ end
end
diff --git a/spec/services/ci/create_build_annotations_service_spec.rb b/spec/services/ci/create_build_annotations_service_spec.rb
new file mode 100644
index 00000000000..032ee25745d
--- /dev/null
+++ b/spec/services/ci/create_build_annotations_service_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::CreateBuildAnnotationsService do
+ describe '#execute' do
+ let(:ci_build) { create(:ci_build) }
+ let(:service) { described_class.new(ci_build) }
+
+ context 'when there is no annotations artifact' do
+ it 'does nothing' do
+ service.execute
+
+ expect(Ci::BuildAnnotation.count).to be_zero
+ end
+ end
+
+ context 'when there is an annotation artifact' do
+ it 'imports the artifact' do
+ artifact = double(:artifact, job_id: ci_build.id, size: 1)
+ json = JSON.dump([{ severity: 'warning', summary: 'hello' }])
+
+ allow(artifact)
+ .to receive(:each_blob)
+ .and_yield(json)
+
+ allow(ci_build)
+ .to receive(:first_build_annotation_artifact)
+ .and_return(artifact)
+
+ service.execute
+
+ expect(Ci::BuildAnnotation.count).to eq(1)
+ end
+ end
+ end
+
+ describe '#parse_annotations' do
+ let(:service) { described_class.new(double(:build, id: 1)) }
+
+ context 'when the annotations are valid' do
+ it 'returns the database rows to insert' do
+ artifact = double(:artifact, job_id: 1, size: 1)
+ json = JSON.dump([{ severity: 'warning', summary: 'hello' }])
+
+ allow(artifact)
+ .to receive(:each_blob)
+ .and_yield(json)
+
+ expect(service.parse_annotations(artifact)).to eq([
+ {
+ build_id: 1,
+ severity: Ci::BuildAnnotation.severities[:warning],
+ summary: 'hello',
+ description: nil,
+ line_number: nil,
+ file_path: nil
+ }
+ ])
+ end
+ end
+
+ context 'when the annotations are not valid' do
+ it 'returns an error row to insert' do
+ artifact = double(:artifact, job_id: 1, size: 1)
+ json = JSON.dump([{ summary: 'hello' }])
+
+ allow(artifact)
+ .to receive(:each_blob)
+ .and_yield(json)
+
+ annotations = service.parse_annotations(artifact)
+
+ expect(annotations.length).to eq(1)
+
+ expect(annotations[0][:severity])
+ .to eq(Ci::BuildAnnotation.severities[:error])
+
+ expect(annotations[0][:summary]).to be_an_instance_of(String)
+ end
+ end
+ end
+
+ describe '#insert_annotations' do
+ it 'inserts the build annotations into the database' do
+ build = create(:ci_build)
+ row = {
+ build_id: build.id,
+ severity: Ci::BuildAnnotation.severities[:warning],
+ summary: 'hello',
+ description: nil,
+ line_number: nil,
+ file_path: nil
+ }
+
+ service = described_class.new(build)
+
+ service.insert_annotations([row])
+
+ expect(Ci::BuildAnnotation.count).to eq(1)
+ end
+ end
+
+ describe '#build_error_annotation' do
+ it 'returns an annotation to use when an error was produced' do
+ build = create(:ci_build)
+ service = described_class.new(build)
+
+ expect(service.build_error_annotation('foo')).to eq([
+ {
+ build_id: build.id,
+ severity: Ci::BuildAnnotation.severities[:error],
+ summary: 'foo'
+ }
+ ])
+ end
+ end
+end
diff --git a/spec/workers/ci/create_build_annotations_worker_spec.rb b/spec/workers/ci/create_build_annotations_worker_spec.rb
new file mode 100644
index 00000000000..63ffba12c6f
--- /dev/null
+++ b/spec/workers/ci/create_build_annotations_worker_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::CreateBuildAnnotationsWorker do
+ describe '#perform' do
+ context 'when the CI build does not exist' do
+ it 'does nothing' do
+ expect(Ci::CreateBuildAnnotationsService)
+ .not_to receive(:new)
+
+ described_class.new.perform(-1)
+ end
+ end
+
+ context 'when the CI build exists' do
+ it 'creates the build annotations for the build' do
+ build = create(:ci_build)
+ worker = described_class.new
+ service = instance_spy(Ci::CreateBuildAnnotationsService)
+
+ allow(Ci::CreateBuildAnnotationsService)
+ .to receive(:new)
+ .with(build)
+ .and_return(service)
+
+ allow(service)
+ .to receive(:execute)
+
+ worker.perform(build.id)
+
+ expect(service)
+ .to have_received(:execute)
+ end
+ end
+ end
+end