Commit 99c033f2 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'artifact-format-v2-with-parser' into 'master'

Parse junit.xml.gz and calculate the difference between head and base

See merge request gitlab-org/gitlab-ce!20576
parents 53ecd2e1 fafd1764
......@@ -99,6 +99,23 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
}
end
def test_reports
result = @merge_request.compare_test_reports
Gitlab::PollingInterval.set_header(response, interval: 10_000)
case result[:status]
when :parsing
render json: '', status: :no_content
when :parsed
render json: result[:data].to_json, status: :ok
when :error
render json: { status_reason: result[:status_reason] }, status: :bad_request
else
render json: { status_reason: 'Unknown error' }, status: :internal_server_error
end
end
def edit
define_edit_vars
end
......
......@@ -71,6 +71,11 @@ module Ci
where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
end
scope :with_test_reports, ->() do
includes(:job_artifacts_junit) # Prevent N+1 problem when iterating each ci_job_artifact row
.where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').test_reports)
end
scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
......@@ -629,8 +634,24 @@ module Ci
running? && runner_session_url.present?
end
def collect_test_reports!(test_reports)
test_reports.get_suite(group_name).tap do |test_suite|
each_test_report do |file_type, blob|
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, test_suite)
end
end
end
private
def each_test_report
Ci::JobArtifact::TEST_REPORT_FILE_TYPES.each do |file_type|
public_send("job_artifacts_#{file_type}").each_blob do |blob| # rubocop:disable GitlabSecurity/PublicSend
yield file_type, blob
end
end
end
def update_artifacts_size
self.artifacts_size = legacy_artifacts_file&.size
end
......
......@@ -6,6 +6,8 @@ module Ci
include ObjectStorage::BackgroundMove
extend Gitlab::Ci::Model
NotSupportedAdapterError = Class.new(StandardError)
TEST_REPORT_FILE_TYPES = %w[junit].freeze
DEFAULT_FILE_NAMES = { junit: 'junit.xml' }.freeze
TYPE_AND_FORMAT_PAIRS = { archive: :zip, metadata: :gzip, trace: :raw, junit: :gzip }.freeze
......@@ -46,6 +48,10 @@ module Ci
gzip: 3
}
FILE_FORMAT_ADAPTERS = {
gzip: Gitlab::Ci::Build::Artifacts::GzipFileAdapter
}.freeze
def valid_file_format?
unless TYPE_AND_FORMAT_PAIRS[self.file_type&.to_sym] == self.file_format&.to_sym
errors.add(:file_format, 'Invalid file format with specified file type')
......@@ -77,8 +83,22 @@ module Ci
end
end
def each_blob(&blk)
unless file_format_adapter_class
raise NotSupportedAdapterError, 'This file format requires a dedicated adapter'
end
file.open do |stream|
file_format_adapter_class.new(stream).each_blob(&blk)
end
end
private
def file_format_adapter_class
FILE_FORMAT_ADAPTERS[file_format.to_sym]
end
def set_size
self.size = file.size
end
......
......@@ -605,6 +605,18 @@ module Ci
@latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a
end
def has_test_reports?
complete? && builds.with_test_reports.any?
end
def test_reports
Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
builds.with_test_reports.each do |build|
build.collect_test_reports!(test_reports)
end
end
end
private
def ci_yaml_from_repo
......
......@@ -13,6 +13,11 @@ class MergeRequest < ActiveRecord::Base
include ThrottledTouch
include Gitlab::Utils::StrongMemoize
include LabelEventable
include ReactiveCaching
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 1.hour
self.reactive_cache_lifetime = 1.hour
ignore_column :locked_at,
:ref_fetched,
......@@ -1012,6 +1017,30 @@ class MergeRequest < ActiveRecord::Base
.order(id: :desc)
end
def has_test_reports?
actual_head_pipeline&.has_test_reports?
end
def compare_test_reports
unless has_test_reports?
return { status: :error, status_reason: 'This merge request does not have test reports' }
end
with_reactive_cache(
:compare_test_results,
base_pipeline&.iid,
actual_head_pipeline.iid) { |data| data } || { status: :parsing }
end
def calculate_reactive_cache(identifier, *args)
case identifier.to_sym
when :compare_test_results
Ci::CompareTestReportsService.new(project).execute(*args)
else
raise NotImplementedError, "Unknown identifier: #{identifier}"
end
end
def all_commits
# MySQL doesn't support LIMIT in a subquery.
diffs_relation = if Gitlab::Database.postgresql?
......@@ -1124,6 +1153,12 @@ class MergeRequest < ActiveRecord::Base
true
end
def base_pipeline
@base_pipeline ||= project.pipelines
.order(id: :desc)
.find_by(sha: diff_base_sha)
end
def discussions_rendered_on_frontend?
true
end
......
......@@ -231,6 +231,12 @@ class MergeRequestWidgetEntity < IssuableEntity
end
end
expose :test_reports_path do |merge_request|
if merge_request.has_test_reports?
test_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
end
end
private
delegate :current_user, to: :request
......
class TestCaseEntity < Grape::Entity
expose :status
expose :name
expose :execution_time
expose :system_output
expose :stack_trace
end
class TestReportsComparerEntity < Grape::Entity
expose :total_status, as: :status
expose :summary do
expose :total_count, as: :total
expose :resolved_count, as: :resolved
expose :failed_count, as: :failed
end
expose :suite_comparers, as: :suites, using: TestSuiteComparerEntity
end
class TestReportsComparerSerializer < BaseSerializer
entity TestReportsComparerEntity
end
class TestSuiteComparerEntity < Grape::Entity
expose :name
expose :total_status, as: :status
expose :summary do
expose :total_count, as: :total
expose :resolved_count, as: :resolved
expose :failed_count, as: :failed
end
expose :new_failures, using: TestCaseEntity
expose :resolved_failures, using: TestCaseEntity
expose :existing_failures, using: TestCaseEntity
end
# frozen_string_literal: true
module Ci
class CompareTestReportsService < ::BaseService
def execute(base_pipeline_iid, head_pipeline_iid)
base_pipeline = project.pipelines.find_by_iid(base_pipeline_iid) if base_pipeline_iid
head_pipeline = project.pipelines.find_by_iid(head_pipeline_iid)
begin
comparer = Gitlab::Ci::Reports::TestReportsComparer
.new(base_pipeline&.test_reports, head_pipeline.test_reports)
{
status: :parsed,
data: TestReportsComparerSerializer
.new(project: project)
.represent(comparer).as_json
}
rescue => e
{ status: :error, status_reason: e.message }
end
end
end
end
---
title: JUnit XML Test Summary In MR widget
merge_request: 20576
author:
type: added
......@@ -109,6 +109,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :assign_related_issues
get :discussions, format: :json
post :rebase
get :test_reports
scope constraints: { format: nil }, action: :show do
get :commits, defaults: { tab: 'commits' }
......
module Gitlab
module Ci
module Build
module Artifacts
class GzipFileAdapter
attr_reader :stream
InvalidStreamError = Class.new(StandardError)
def initialize(stream)
raise InvalidStreamError, "Stream is required" unless stream
@stream = stream
end
def each_blob
stream.seek(0)
until stream.eof?
gzip(stream) do |gz|
yield gz.read, gz.orig_name
unused = gz.unused&.length.to_i
# pos has already reached to EOF at the moment
# We rewind the pos to the top of unused files
# to read next gzip stream, to support multistream archives
# https://golang.org/src/compress/gzip/gunzip.go#L117
stream.seek(-unused, IO::SEEK_CUR)
end
end
end
private
def gzip(stream, &block)
gz = Zlib::GzipReader.new(stream)
yield(gz)
rescue Zlib::Error => e
raise InvalidStreamError, e.message
ensure
gz&.finish
end
end
end
end
end
end
module Gitlab
module Ci
module Parsers
def self.fabricate!(file_type)
"Gitlab::Ci::Parsers::#{file_type.classify}".constantize.new
end
end
end
end
module Gitlab
module Ci
module Parsers
class Junit
attr_reader :data
JunitParserError = Class.new(StandardError)
def parse!(xml_data, test_suite)
@data = Hash.from_xml(xml_data)
each_suite do |testcases|
testcases.each do |testcase|
test_case = create_test_case(testcase)
test_suite.add_test_case(test_case)
end
end
rescue REXML::ParseException => e
raise JunitParserError, "XML parsing failed: #{e.message}"
rescue => e
raise JunitParserError, "JUnit parsing failed: #{e.message}"
end
private
def each_suite
testsuites.each do |testsuite|
yield testcases(testsuite)
end
end
def testsuites
if data['testsuites']
data['testsuites']['testsuite']
else
[data['testsuite']]
end
end
def testcases(testsuite)
if testsuite['testcase'].is_a?(Array)
testsuite['testcase']
else
[testsuite['testcase']]
end
end
def create_test_case(data)
if data['failure']
status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED
system_output = data['failure']
else
status = ::Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS
system_output = nil
end
::Gitlab::Ci::Reports::TestCase.new(
classname: data['classname'],
name: data['name'],
file: data['file'],
execution_time: data['time'],
status: status,
system_output: system_output
)
end
end
end
end
end
module Gitlab
module Ci
module Reports
class TestCase
STATUS_SUCCESS = 'success'.freeze
STATUS_FAILED = 'failed'.freeze
STATUS_SKIPPED = 'skipped'.freeze
STATUS_ERROR = 'error'.freeze
STATUS_TYPES = [STATUS_SUCCESS, STATUS_FAILED, STATUS_SKIPPED, STATUS_ERROR].freeze
attr_reader :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key
def initialize(name:, classname:, execution_time:, status:, file: nil, system_output: nil, stack_trace: nil)
@name = name
@classname = classname
@file = file
@execution_time = execution_time.to_f
@status = status
@system_output = system_output
@stack_trace = stack_trace
@key = sanitize_key_name("#{classname}_#{name}")
end
private
def sanitize_key_name(key)
key.gsub(/[^0-9A-Za-z]/, '-')
end
end
end
end
end
module Gitlab
module Ci
module Reports
class TestReports
attr_reader :test_suites
def initialize
@test_suites = {}
end
def get_suite(suite_name)
test_suites[suite_name] ||= TestSuite.new(suite_name)
end
def total_time
test_suites.values.sum(&:total_time)
end
def total_count
test_suites.values.sum(&:total_count)
end
def total_status
if failed_count > 0 || error_count > 0
TestCase::STATUS_FAILED
else
TestCase::STATUS_SUCCESS
end
end
TestCase::STATUS_TYPES.each do |status_type|
define_method("#{status_type}_count") do
test_suites.values.sum { |suite| suite.public_send("#{status_type}_count") } # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end
end
end
module Gitlab
module Ci
module Reports
class TestReportsComparer
include Gitlab::Utils::StrongMemoize
attr_reader :base_reports, :head_reports
def initialize(base_reports, head_reports)
@base_reports = base_reports || TestReports.new
@head_reports = head_reports
end
def suite_comparers
strong_memoize(:suite_comparers) do
head_reports.test_suites.map do |name, test_suite|
TestSuiteComparer.new(name, base_reports.get_suite(name), test_suite)
end
end
end
def total_status
if suite_comparers.any? { |suite| suite.total_status == TestCase::STATUS_FAILED }
TestCase::STATUS_FAILED
else
TestCase::STATUS_SUCCESS
end
end
%w(total_count resolved_count failed_count).each do |method|
define_method(method) do
suite_comparers.sum { |suite| suite.public_send(method) } # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end
end
end
module Gitlab
module Ci
module Reports
class TestSuite
attr_reader :name
attr_reader :test_cases
attr_reader :total_time
def initialize(name = nil)
@name = name
@test_cases = {}
@total_time = 0.0
@duplicate_cases = []
end
def add_test_case(test_case)
@duplicate_cases << test_case if existing_key?(test_case)
@test_cases[test_case.status] ||= {}
@test_cases[test_case.status][test_case.key] = test_case
@total_time += test_case.execution_time
end
def total_count
test_cases.values.sum(&:count)
end
def total_status
if failed_count > 0 || error_count > 0
TestCase::STATUS_FAILED
else
TestCase::STATUS_SUCCESS
end
end
TestCase::STATUS_TYPES.each do |status_type|
define_method("#{status_type}") do
test_cases[status_type] || {}
end
define_method("#{status_type}_count") do
test_cases[status_type]&.length.to_i
end
end
private
def existing_key?(test_case)
@test_cases[test_case.status]&.key?(test_case.key)
end
end
end
end
end
module Gitlab
module Ci
module Reports
class TestSuiteComparer
include Gitlab::Utils::StrongMemoize
attr_reader :name, :base_suite, :head_suite
def initialize(name, base_suite, head_suite)
@name = name
@base_suite = base_suite || TestSuite.new
@head_suite = head_suite
end
def new_failures
strong_memoize(:new_failures) do
head_suite.failed.reject do |key, _|
base_suite.failed.include?(key)
end.values
end
end
def existing_failures
strong_memoize(:existing_failures) do
head_suite.failed.select do |key, _|
base_suite.failed.include?(key)
end.values
end
end
def resolved_failures
strong_memoize(:resolved_failures) do
head_suite.success.select do |key, _|
base_suite.failed.include?(key)
end.values
end
end
def total_count
head_suite.total_count
end
def total_status
head_suite.total_status
end
def resolved_count
resolved_failures.count
end
def failed_count
new_failures.count + existing_failures.count
end
end
end
end
end
......@@ -580,6 +580,64 @@ describe Projects::MergeRequestsController do
end
end
describe 'GET test_reports' do
subject do
get :test_reports,
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid,
format: :json
end
before do
allow_any_instance_of(MergeRequest)
.to receive(:compare_test_reports).and_return(comparison_status)
end
context 'when comparison is being processed' do
let(:comparison_status) { { status: :parsing } }
it 'returns 204 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'when comparison is done' do
let(:comparison_status) { { status: :parsed, data: { summary: 1 } } }
it 'returns 200 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({ 'summary' => 1 })
end
end
context 'when user created corrupted test reports' do
let(:comparison_status) { { status: :error, status_reason: 'Failed to parse test reports' } }
it 'returns 400 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({ 'status_reason' => 'Failed to parse test reports' })
end
end
context 'when something went wrong on our system' do
let(:comparison_status) { {} }
it 'returns 500 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:internal_server_error)
expect(json_response).to eq({ 'status_reason' => 'Unknown error' })
end
end
end
describe 'POST remove_wip' do
before do
merge_request.title = merge_request.wip_title
......
......@@ -188,9 +188,8 @@ FactoryBot.define do
end
trait :test_reports do
after(:create) do |build|
create(:ci_job_artifact, :junit, job: build)
build.reload
after(:build) do |build|
build.job_artifacts << create(:ci_job_artifact, :junit, job: build)
end
end
......
......@@ -50,7 +50,37 @@ FactoryBot.define do
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/junit.xml.gz'), 'application/x-gzip')
Rails.root.join('spec/fixtures/junit/junit.xml.gz'), 'application/x-gzip')
end
end
trait :junit_with_ant do
file_type :junit
file_format :gzip
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/junit/junit_ant.xml.gz'), 'application/x-gzip')
end
end
trait :junit_with_three_testsuites do
file_type :junit
file_format :gzip
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/junit/junit_with_three_testsuites.xml.gz'), 'application/x-gzip')
end
end
trait :junit_with_corrupted_data do
file_type :junit
file_format :gzip
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/junit/junit_with_corrupted_data.xml.gz'), 'application/x-gzip')
end
end
......
......@@ -58,6 +58,10 @@ FactoryBot.define do
status :success
end
trait :running do
status :running
end
trait :failed do
status :failed
end
......@@ -65,6 +69,14 @@ FactoryBot.define do
trait :protected do
protected true
end
trait :with_test_reports do
status :success
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ci_build, :test_reports, pipeline: pipeline, project: pipeline.project)
end
end
end
end
end
......@@ -89,6 +89,18 @@ FactoryBot.define do
end
end
trait :with_test_reports do
after(:build) do |merge_request|
merge_request.head_pipeline = build(
:ci_pipeline,
:success,
:with_test_reports,
project: merge_request.source_project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha)
end
end
after(:build) do |merge_request|
target_project = merge_request.target_project
source_project = merge_request.source_project
......
......@@ -117,7 +117,8 @@
"rebase_in_progress": { "type": "boolean" },
"can_push_to_source_branch": { "type": "boolean" },
"rebase_path": { "type": ["string", "null"] },
"squash": { "type": "boolean" }
"squash": { "type": "boolean" },
"test_reports_path": { "type": ["string", "null"] }
},
"additionalProperties": false
}
{
"type": "object",
"required" : [
"status",
"name"
],
"properties": {
"status": { "type": "string" },
"name": { "type": "string" },
"execution_time": { "type": "float" },
"system_output": { "type": ["string", "null"] },
"stack_trace": { "type": ["string", "null"] }
},
"additionalProperties": false
}
{
"type": "object",
"required" : [
"status",
"summary",
"suites"
],
"properties": {
"status": { "type": "string" },
"summary": {
"type": "object",
"properties": {
"total": { "type": "integer" },
"resolved": { "type": "integer" },
"failed": { "type": "integer" }
},
"required": [
"total",
"resolved",
"failed"
]
},
"suites": { "type": "array", "items": { "$ref": "test_suite_comparer.json" } }
},
"additionalProperties": false
}
{
"type": "object",
"required": [
"name",
"status",
"summary",
"new_failures",
"resolved_failures",
"existing_failures"
],
"properties": {
"name": { "type": "string" },
"status": { "type": "string" },
"summary": {
"type": "object",
"properties": {
"total": { "type": "integer" },
"resolved": { "type": "integer" },
"failed": { "type": "integer" }
},
"required": [
"total",
"resolved",
"failed"
]
},
"new_failures": { "type": "array", "items": { "$ref": "test_case.json" } },
"resolved_failures": { "type": "array", "items": { "$ref": "test_case.json" } },
"existing_failures": { "type": "array", "items": { "$ref": "test_case.json" } }
},
"additionalProperties": false
}
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="rspec" tests="4" skipped="0" failures="2" errors="0" time="0.011289" timestamp="2018-07-17T10:48:13+00:00" hostname="runner-400e3f62-project-15-concurrent-0">
<properties>
<property name="seed" value="404"/>
</properties>
<testcase classname="spec.test_spec" name="Test#sum when a is 1 and b is 2 returns summary" file="./spec/test_spec.rb" time="0.009292"><failure message="
expected: 3
got: -1
(compared using ==)
" type="RSpec::Expectations::ExpectationNotMetError">Failure/Error: is_expected.to eq(3)
expected: 3
got: -1
(compared using ==)
./spec/test_spec.rb:12:in `block (4 levels) in &lt;top (required)&gt;&apos;</failure></testcase>
<testcase classname="spec.test_spec" name="Test#sum when a is 100 and b is 200 returns summary" file="./spec/test_spec.rb" time="0.000180"><failure message="
expected: 300
got: -100
(compared using ==)
" type="RSpec::Expectations::ExpectationNotMetError">Failure/Error: is_expected.to eq(300)
expected: 300
got: -100
(compared using ==)
./spec/test_spec.rb:21:in `block (4 levels) in &lt;top (required)&gt;&apos;</failure></testcase>
<testcase classname="spec.test_spec" name="Test#subtract when a is 1 and b is 2 raises an error" file="./spec/test_spec.rb" time="0.000748"></testcase>
<testcase classname="spec.test_spec" name="Test#subtract when a is 2 and b is 1 returns correct result" file="./spec/test_spec.rb" time="0.000064"></testcase>
</testsuite>
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="rspec" tests="2" skipped="0" failures="0" errors="0" time="0.001670" timestamp="2018-07-30T10:02:37+00:00" hostname="runner-7661726c-project-14-concurrent-0">
<properties>
<property name="seed" value="52549"/>
</properties>
<testcase classname="spec.hash_scan_spec" name="HashScan#scan when argument is hash returns the value" file="./spec/hash_scan_spec.rb" time="0.000287"></testcase>
<testcase classname="spec.hash_scan_spec" name="HashScan#scan when argument is not hash raises and error" file="./spec/hash_scan_spec.rb" time="0.000686"></testcase>
</testsuite>
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="rspec" tests="2" skipped="0" failures="0" errors="0" time="0.001691" timestamp="2018-07-30T10:02:37+00:00" hostname="runner-7661726c-project-14-concurrent-0">
<properties>
<property name="seed" value="8528"/>
</properties>
<testcase classname="spec.string_helper_spec" name="StringHelper#concatenate when a is git and b is lab returns summary" file="./spec/string_helper_spec.rb" time="0.000287"></testcase>
<testcase classname="spec.string_helper_spec" name="StringHelper#concatenate when a is git and b is 200 raises an error" file="./spec/string_helper_spec.rb" time="0.000706"></testcase>
</testsuite>
require 'spec_helper'
describe Gitlab::Ci::Build::Artifacts::GzipFileAdapter do
describe '#initialize' do
context 'when stream is passed' do
let(:stream) { File.open(expand_fixture_path('junit/junit.xml.gz'), 'rb') }
it 'initialized' do
expect { described_class.new(stream) }.not_to raise_error
end
end
context 'when stream is not passed' do
let(:stream) { nil }
it 'raises an error' do
expect { described_class.new(stream) }.to raise_error(described_class::InvalidStreamError)
end
end
end
describe '#each_blob' do
let(:adapter) { described_class.new(stream) }
context 'when stream is gzip file' do
context 'when gzip file contains one file' do
let(:stream) { File.open(expand_fixture_path('junit/junit.xml.gz'), 'rb') }
it 'iterates content and file_name' do
expect { |b| adapter.each_blob(&b) }
.to yield_with_args(fixture_file('junit/junit.xml'), 'rspec.xml')
end
end
context 'when gzip file contains three files' do
let(:stream) { File.open(expand_fixture_path('junit/junit_with_three_testsuites.xml.gz'), 'rb') }
it 'iterates content and file_name' do
expect { |b| adapter.each_blob(&b) }
.to yield_successive_args(
[fixture_file('junit/junit_with_three_testsuites_1.xml'), 'rspec-3.xml'],
[fixture_file('junit/junit_with_three_testsuites_2.xml'), 'rspec-1.xml'],
[fixture_file('junit/junit_with_three_testsuites_3.xml'), 'rspec-2.xml'])
end
end
end
context 'when stream is zip file' do
let(:stream) { File.open(expand_fixture_path('ci_build_artifacts.zip'), 'rb') }
it 'raises an error' do
expect { |b| adapter.each_blob(&b) }.to raise_error(described_class::InvalidStreamError)
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Parsers::Junit do
describe '#parse!' do
subject { described_class.new.parse!(junit, test_suite) }
let(:test_suite) { Gitlab::Ci::Reports::TestSuite.new('rspec') }
let(:test_cases) { flattened_test_cases(test_suite) }
context 'when data is JUnit style XML' do
context 'when there are no test cases' do
let(:junit) do
<<-EOF.strip_heredoc
<testsuite></testsuite>
EOF
end
it 'raises an error and does not add any test cases' do
expect { subject }.to raise_error(described_class::JunitParserError)
expect(test_cases.count).to eq(0)
end
end
context 'when there is a test case' do
let(:junit) do
<<-EOF.strip_heredoc
<testsuite>
<testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
</testsuite>
EOF
end
it 'parses XML and adds a test case to a suite' do
expect { subject }.not_to raise_error
expect(test_cases[0].classname).to eq('Calculator')
expect(test_cases[0].name).to eq('sumTest1')
expect(test_cases[0].execution_time).to eq(0.01)
end
end
context 'when there are two test cases' do
let(:junit) do
<<-EOF.strip_heredoc
<testsuite>
<testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
<testcase classname='Calculator' name='sumTest2' time='0.02'></testcase>
</testsuite>
EOF
end
it 'parses XML and adds test cases to a suite' do
expect { subject }.not_to raise_error
expect(test_cases[0].classname).to eq('Calculator')
expect(test_cases[0].name).to eq('sumTest1')
expect(test_cases[0].execution_time).to eq(0.01)
expect(test_cases[1].classname).to eq('Calculator')
expect(test_cases[1].name).to eq('sumTest2')
expect(test_cases[1].execution_time).to eq(0.02)
end
end
context 'when there are two test suites' do
let(:junit) do
<<-EOF.strip_heredoc
<testsuites>
<testsuite>
<testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
<testcase classname='Calculator' name='sumTest2' time='0.02'></testcase>
</testsuite>
<testsuite>
<testcase classname='Statemachine' name='happy path' time='100'></testcase>
<testcase classname='Statemachine' name='unhappy path' time='200'></testcase>
</testsuite>
</testsuites>
EOF
end
it 'parses XML and adds test cases to a suite' do
expect { subject }.not_to raise_error
expect(test_cases[0].classname).to eq('Calculator')
expect(test_cases[0].name).to eq('sumTest1')
expect(test_cases[0].execution_time).to eq(0.01)
expect(test_cases[1].classname).to eq('Calculator')
expect(test_cases[1].name).to eq('sumTest2')
expect(test_cases[1].execution_time).to eq(0.02)
expect(test_cases[2].classname).to eq('Statemachine')
expect(test_cases[2].name).to eq('happy path')
expect(test_cases[2].execution_time).to eq(100)
expect(test_cases[3].classname).to eq('Statemachine')
expect(test_cases[3].name).to eq('unhappy path')
expect(test_cases[3].execution_time).to eq(200)
end
end
end
context 'when data is not JUnit style XML' do
let(:junit) { { testsuite: 'abc' }.to_json }
it 'raises an error' do
expect { subject }.to raise_error(described_class::JunitParserError)
end
end
private
def flattened_test_cases(test_suite)
test_suite.test_cases.map do |status, value|
value.map do |key, test_case|
test_case
end
end.flatten
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Parsers do
describe '.fabricate!' do
subject { described_class.fabricate!(file_type) }
context 'when file_type exists' do
let(:file_type) { 'junit' }
it 'fabricates the class' do
is_expected.to be_a(described_class::Junit)
end
end
context 'when file_type does not exist' do
let(:file_type) { 'undefined' }
it 'raises an error' do
expect { subject }.to raise_error(NameError)
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Reports::TestCase do
describe '#initialize' do
let(:test_case) { described_class.new(**params)}
context 'when both classname and name are given' do
context 'when test case is passed' do
let(:params) do
{
name: 'test-1',
classname: 'trace',
file: 'spec/trace_spec.rb',
execution_time: 1.23,
status: described_class::STATUS_SUCCESS,
system_output: nil
}
end
it 'initializes an instance' do
expect { test_case }.not_to raise_error
expect(test_case.name).to eq('test-1')
expect(test_case.classname).to eq('trace')
expect(test_case.file).to eq('spec/trace_spec.rb')
expect(test_case.execution_time).to eq(1.23)
expect(test_case.status).to eq(described_class::STATUS_SUCCESS)
expect(test_case.system_output).to be_nil
end
end
context 'when test case is failed' do
let(:params) do
{
name: 'test-1',
classname: 'trace',
file: 'spec/trace_spec.rb',
execution_time: 1.23,
status: described_class::STATUS_FAILED,
system_output: "Failure/Error: is_expected.to eq(300) expected: 300 got: -100"
}
end
it 'initializes an instance' do
expect { test_case }.not_to raise_error
expect(test_case.name).to eq('test-1')
expect(test_case.classname).to eq('trace')
expect(test_case.file).to eq('spec/trace_spec.rb')
expect(test_case.execution_time).to eq(1.23)
expect(test_case.status).to eq(described_class::STATUS_FAILED)
expect(test_case.system_output)
.to eq('Failure/Error: is_expected.to eq(300) expected: 300 got: -100')
end
end
end
context 'when classname is missing' do
let(:params) do
{
name: 'test-1',
file: 'spec/trace_spec.rb',
execution_time: 1.23,
status: described_class::STATUS_SUCCESS,
system_output: nil
}
end
it 'raises an error' do
expect { test_case }.to raise_error(ArgumentError)
end
end
context 'when name is missing' do
let(:params) do
{
classname: 'trace',
file: 'spec/trace_spec.rb',
execution_time: 1.23,
status: described_class::STATUS_SUCCESS,
system_output: nil
}
end
it 'raises an error' do
expect { test_case }.to raise_error(ArgumentError)
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Reports::TestReportsComparer do
include TestReportsHelper
let(:comparer) { described_class.new(base_reports, head_reports) }
let(:base_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:head_reports) { Gitlab::Ci::Reports::TestReports.new }
describe '#suite_comparers' do
subject { comparer.suite_comparers }
context 'when head and base reports include two test suites' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_success)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
it 'returns test suite comparers with specified values' do
expect(subject[0]).to be_a(Gitlab::Ci::Reports::TestSuiteComparer)
expect(subject[0].name).to eq('rspec')
expect(subject[0].head_suite).to eq(head_reports.get_suite('rspec'))
expect(subject[0].base_suite).to eq(base_reports.get_suite('rspec'))
expect(subject[1]).to be_a(Gitlab::Ci::Reports::TestSuiteComparer)
expect(subject[1].name).to eq('junit')
expect(subject[1].head_suite).to eq(head_reports.get_suite('junit'))
expect(subject[1].base_suite).to eq(base_reports.get_suite('junit'))
end
end
end
describe '#total_status' do
subject { comparer.total_status }
context 'when all tests cases are success in head suites' do
before do
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
it 'returns the total status' do
is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
end
context 'when there is a failed test case in head suites' do
before do
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
end
it 'returns the total status in head suite' do
is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED)
end
end
end
describe '#total_count' do
subject { comparer.total_count }
before do
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
end
it 'returns the total test counts in head suites' do
is_expected.to eq(2)
end
end
describe '#resolved_count' do
subject { comparer.resolved_count }
context 'when there is a resolved test case in head suites' do
let(:create_test_case_java_resolved) do
create_test_case_java_failed.tap do |test_case|
test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
end
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved)
end
it 'returns the correct count' do
is_expected.to eq(1)
end
end
context 'when there are no resolved test cases in head suites' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
end
it 'returns the correct count' do
is_expected.to eq(0)
end
end
end
describe '#failed_count' do
subject { comparer.failed_count }
context 'when there is a failed test case in head suites' do
before do
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
end
it 'returns the correct count' do
is_expected.to eq(1)
end
end
context 'when there are no failed test cases in head suites' do
before do
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_rspec_success)
end
it 'returns the correct count' do
is_expected.to eq(0)
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Reports::TestReports do
include TestReportsHelper
let(:test_reports) { described_class.new }
describe '#get_suite' do
subject { test_reports.get_suite(suite_name) }
context 'when suite name is rspec' do
let(:suite_name) { 'rspec' }
it { expect(subject.name).to eq('rspec') }
it 'initializes a new test suite and returns it' do
expect(Gitlab::Ci::Reports::TestSuite).to receive(:new).and_call_original
is_expected.to be_a(Gitlab::Ci::Reports::TestSuite)
end
context 'when suite name is already allocated' do
before do
subject
end
it 'does not initialize a new test suite' do
expect(Gitlab::Ci::Reports::TestSuite).not_to receive(:new)
is_expected.to be_a(Gitlab::Ci::Reports::TestSuite)
end
end
end
end
describe '#total_time' do
subject { test_reports.total_time }
before do
test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
test_reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
it 'returns the total time' do
is_expected.to eq(6.66)
end
end
describe '#total_count' do
subject { test_reports.total_count }
before do
test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
test_reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
it 'returns the total count' do
is_expected.to eq(2)
end
end
describe '#total_status' do
subject { test_reports.total_status }
context 'when all test cases succeeded' do
before do
test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
test_reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
it 'returns correct total status' do
is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
end
context 'when there is a failed test case' do
before do
test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
test_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
end
it 'returns correct total status' do
is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED)
end
end
context 'when there is a skipped test case' do
before do
test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
test_reports.get_suite('junit').add_test_case(create_test_case_java_skipped)
end
it 'returns correct total status' do
is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
end
context 'when there is an error test case' do
before do
test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
test_reports.get_suite('junit').add_test_case(create_test_case_java_error)
end
it 'returns correct total status' do
is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED)
end
end
end
Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type|
describe "##{status_type}_count" do
subject { test_reports.public_send("#{status_type}_count") }
context "when #{status_type} test case exists" do
before do
test_reports.get_suite('rspec').add_test_case(public_send("create_test_case_rspec_#{status_type}"))
test_reports.get_suite('junit').add_test_case(public_send("create_test_case_java_#{status_type}"))
end
it 'returns the count' do
is_expected.to eq(2)
end
end
context "when #{status_type} test case do not exist" do
it 'returns nothing' do
is_expected.to be(0)
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Reports::TestSuiteComparer do
include TestReportsHelper
let(:comparer) { described_class.new(name, base_suite, head_suite) }
let(:name) { 'rpsec' }
let(:base_suite) { Gitlab::Ci::Reports::TestSuite.new(name) }
let(:head_suite) { Gitlab::Ci::Reports::TestSuite.new(name) }
let(:test_case_success) { create_test_case_rspec_success }
let(:test_case_failed) { create_test_case_rspec_failed }
let(:test_case_resolved) do
create_test_case_rspec_failed.tap do |test_case|
test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
end
describe '#new_failures' do
subject { comparer.new_failures }
context 'when head sutie has a newly failed test case which does not exist in base' do
before do
base_suite.add_test_case(test_case_success)
head_suite.add_test_case(test_case_failed)
end
it 'returns the failed test case' do
is_expected.to eq([test_case_failed])
end
end
context 'when head sutie still has a failed test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_failed)
end
it 'does not return the failed test case' do
is_expected.to be_empty
end
end
context 'when head sutie has a success test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_resolved)
end
it 'does not return the failed test case' do
is_expected.to be_empty
end
end
end
describe '#existing_failures' do
subject { comparer.existing_failures }
context 'when head sutie has a newly failed test case which does not exist in base' do
before do
base_suite.add_test_case(test_case_success)
head_suite.add_test_case(test_case_failed)
end
it 'returns the failed test case' do
is_expected.to be_empty
end
end
context 'when head sutie still has a failed test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_failed)
end
it 'does not return the failed test case' do
is_expected.to eq([test_case_failed])
end
end
context 'when head sutie has a success test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_resolved)
end
it 'does not return the failed test case' do
is_expected.to be_empty
end
end
end
describe '#resolved_failures' do
subject { comparer.resolved_failures }
context 'when head sutie has a newly failed test case which does not exist in base' do
before do
base_suite.add_test_case(test_case_success)
head_suite.add_test_case(test_case_failed)
end
it 'returns the failed test case' do
is_expected.to be_empty
end
it 'returns the correct resolved count' do
expect(comparer.resolved_count).to eq(0)
end
end
context 'when head sutie still has a failed test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_failed)
end
it 'does not return the failed test case' do
is_expected.to be_empty
end
it 'returns the correct resolved count' do
expect(comparer.resolved_count).to eq(0)
end
end
context 'when head sutie has a success test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_resolved)
end
it 'does not return the resolved test case' do
is_expected.to eq([test_case_resolved])
end
it 'returns the correct resolved count' do
expect(comparer.resolved_count).to eq(1)
end
end
end
describe '#total_count' do
subject { comparer.total_count }
before do
head_suite.add_test_case(test_case_success)
end
it 'returns the total test counts in head suite' do
is_expected.to eq(1)
end
end
describe '#failed_count' do
subject { comparer.failed_count }
context 'when there are a new failure and an existing failure' do
let(:test_case_1_success) { create_test_case_rspec_success }
let(:test_case_2_failed) { create_test_case_rspec_failed }
let(:test_case_1_failed) do
create_test_case_rspec_success.tap do |test_case|
test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_FAILED)
end
end
before do
base_suite.add_test_case(test_case_1_success)
base_suite.add_test_case(test_case_2_failed)
head_suite.add_test_case(test_case_1_failed)
head_suite.add_test_case(test_case_2_failed)
end
it 'returns the correct count' do
is_expected.to eq(2)
end
end
context 'when there is a new failure' do
before do
base_suite.add_test_case(test_case_success)
head_suite.add_test_case(test_case_failed)
end
it 'returns the correct count' do
is_expected.to eq(1)
end
end
context 'when there is an existing failure' do
before do
base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_failed)
end
it 'returns the correct count' do
is_expected.to eq(1)
end
end
end
describe '#total_status' do
subject { comparer.total_status }
context 'when all test cases in head suite are success' do
before do
head_suite.add_test_case(test_case_success)
end
it 'returns the total status in head suite' do
is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
end
context 'when there is a failed test case in head suite' do
before do
head_suite.add_test_case(test_case_failed)
end
it 'returns the total status in head suite' do
is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED)
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Reports::TestSuite do
include TestReportsHelper
let(:test_suite) { described_class.new('Rspec') }
let(:test_case_success) { create_test_case_rspec_success }
let(:test_case_failed) { create_test_case_rspec_failed }
let(:test_case_skipped) { create_test_case_rspec_skipped }
let(:test_case_error) { create_test_case_rspec_error }
it { expect(test_suite.name).to eq('Rspec') }
describe '#add_test_case' do
context 'when status of the test case is success' do
it 'stores data correctly' do
test_suite.add_test_case(test_case_success)
expect(test_suite.test_cases[test_case_success.status][test_case_success.key])
.to eq(test_case_success)
expect(test_suite.total_time).to eq(1.11)
end
end
context 'when status of the test case is failed' do
it 'stores data correctly' do
test_suite.add_test_case(test_case_failed)
expect(test_suite.test_cases[test_case_failed.status][test_case_failed.key])
.to eq(test_case_failed)
expect(test_suite.total_time).to eq(2.22)
end
end
context 'when two test cases are added' do
it 'sums up total time' do
test_suite.add_test_case(test_case_success)
test_suite.add_test_case(test_case_failed)
expect(test_suite.total_time).to eq(3.33)
end
end
end
describe '#total_count' do
subject { test_suite.total_count }
before do
test_suite.add_test_case(test_case_success)
test_suite.add_test_case(test_case_failed)
end
it { is_expected.to eq(2) }
end
describe '#total_status' do
subject { test_suite.total_status }
context 'when all test cases succeeded' do
before do
test_suite.add_test_case(test_case_success)
end
it { is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) }
end
context 'when a test case failed' do
before do
test_suite.add_test_case(test_case_success)
test_suite.add_test_case(test_case_failed)
end
it { is_expected.to eq(Gitlab::Ci::Reports::TestCase::STATUS_FAILED) }
end
end
Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type|
describe "##{status_type}" do
subject { test_suite.public_send("#{status_type}") }
context "when #{status_type} test case exists" do
before do
test_suite.add_test_case(public_send("test_case_#{status_type}"))
end
it 'returns all success test cases' do
is_expected.to eq( { public_send("test_case_#{status_type}").key => public_send("test_case_#{status_type}") })
end
end
context "when #{status_type} test case do not exist" do
it 'returns nothing' do
is_expected.to be_empty
end
end
end
end
Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type|
describe "##{status_type}_count" do
subject { test_suite.public_send("#{status_type}_count") }
context "when #{status_type} test case exists" do
before do
test_suite.add_test_case(public_send("test_case_#{status_type}"))
end
it 'returns the count' do
is_expected.to eq(1)
end
end
context "when #{status_type} test case do not exist" do
it 'returns nothing' do
is_expected.to be(0)
end
end
end
end
end
......@@ -151,6 +151,42 @@ describe Ci::Build do
end
end
describe '.with_test_reports' do
subject { described_class.with_test_reports }
context 'when build has a test report' do
let!(:build) { create(:ci_build, :success, :test_reports) }
it 'selects the build' do
is_expected.to eq([build])
end
end
context 'when build does not have test reports' do
let!(:build) { create(:ci_build, :success, :trace_artifact) }
it 'does not select the build' do
is_expected.to be_empty
end
end
context 'when there are multiple builds with test reports' do
let!(:builds) { create_list(:ci_build, 5, :success, :test_reports) }
it 'does not execute a query for selecting job artifact one by one' do
recorded = ActiveRecord::QueryRecorder.new do
subject.each do |build|
Ci::JobArtifact::TEST_REPORT_FILE_TYPES.each do |file_type|
build.public_send("job_artifacts_#{file_type}").file.exists?
end
end
end
expect(recorded.count).to eq(2)
end
end
end
describe '#actionize' do
context 'when build is a created' do
before do
......@@ -2760,6 +2796,60 @@ describe Ci::Build do
end
end
describe '#collect_test_reports!' do
subject { build.collect_test_reports!(test_reports) }
let(:test_reports) { Gitlab::Ci::Reports::TestReports.new }
it { expect(test_reports.get_suite(build.name).total_count).to eq(0) }
context 'when build has a test report' do
context 'when there is a JUnit test report from rspec test suite' do
before do
create(:ci_job_artifact, :junit, job: build, project: build.project)
end
it 'parses blobs and add the results to the test suite' do
expect { subject }.not_to raise_error
expect(test_reports.get_suite(build.name).total_count).to eq(4)
expect(test_reports.get_suite(build.name).success_count).to be(2)
expect(test_reports.get_suite(build.name).failed_count).to be(2)
end
end
context 'when there is a JUnit test report from java ant test suite' do
before do
create(:ci_job_artifact, :junit_with_ant, job: build, project: build.project)
end
it 'parses blobs and add the results to the test suite' do
expect { subject }.not_to raise_error
expect(test_reports.get_suite(build.name).total_count).to eq(3)
expect(test_reports.get_suite(build.name).success_count).to be(3)
expect(test_reports.get_suite(build.name).failed_count).to be(0)
end
end
context 'when there is a corrupted JUnit test report' do
before do
create(:ci_job_artifact, :junit_with_corrupted_data, job: build, project: build.project)
end
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Ci::Parsers::Junit::JunitParserError)
end
end
end
context 'when build does not have test reports' do
it 'raises an error' do
expect { subject }.to raise_error(NoMethodError)
end
end
end
describe '#artifacts_metadata_entry' do
set(:build) { create(:ci_build, project: project) }
let(:path) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' }
......
......@@ -147,6 +147,34 @@ describe Ci::JobArtifact do
end
end
describe '#each_blob' do
context 'when file format is gzip' do
context 'when gzip file contains one file' do
let(:artifact) { build(:ci_job_artifact, :junit) }
it 'iterates blob once' do
expect { |b| artifact.each_blob(&b) }.to yield_control.once
end
end
context 'when gzip file contains three files' do
let(:artifact) { build(:ci_job_artifact, :junit_with_three_testsuites) }
it 'iterates blob three times' do
expect { |b| artifact.each_blob(&b) }.to yield_control.exactly(3).times
end
end
end
context 'when there are no adapters for the file format' do
let(:artifact) { build(:ci_job_artifact, :junit, file_format: :zip) }
it 'raises an error' do
expect { |b| artifact.each_blob(&b) }.to raise_error(described_class::NotSupportedAdapterError)
end
end
end
describe '#expire_in' do
subject { artifact.expire_in }
......
......@@ -1851,6 +1851,62 @@ describe Ci::Pipeline, :mailer do
end
end
describe '#has_test_reports?' do
subject { pipeline.has_test_reports? }
context 'when pipeline has builds with test reports' do
before do
create(:ci_build, pipeline: pipeline, project: project).tap do |build|
create(:ci_job_artifact, :junit, job: build, project: build.project)
end
end
context 'when pipeline status is running' do
let(:pipeline) { create(:ci_pipeline, :running, project: project) }
it { is_expected.to be_falsey }
end
context 'when pipeline status is success' do
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { is_expected.to be_truthy }
end
end
context 'when pipeline does not have builds with test reports' do
it { is_expected.to be_falsey }
end
end
describe '#test_reports' do
subject { pipeline.test_reports }
context 'when pipeline has multiple builds with test reports' do
before do
create(:ci_build, :success, name: 'rspec', pipeline: pipeline, project: project).tap do |build|
create(:ci_job_artifact, :junit, job: build, project: build.project)
end
create(:ci_build, :success, name: 'java', pipeline: pipeline, project: project).tap do |build|
create(:ci_job_artifact, :junit_with_ant, job: build, project: build.project)
end
end
it 'returns test reports with collected data' do
expect(subject.total_count).to be(7)
expect(subject.success_count).to be(5)
expect(subject.failed_count).to be(2)
end
end
context 'when pipeline does not have any builds with test reports' do
it 'returns empty test reports' do
expect(subject.total_count).to be(0)
end
end
end
describe '#total_size' do
let!(:build_job1) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
let!(:build_job2) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
......
......@@ -3,6 +3,7 @@ require 'spec_helper'
describe MergeRequest do
include RepoHelpers
include ProjectForksHelper
include ReactiveCachingHelpers
subject { create(:merge_request) }
......@@ -1079,6 +1080,86 @@ describe MergeRequest do
end
end
describe '#has_test_reports?' do
subject { merge_request.has_test_reports? }
let(:project) { create(:project, :repository) }
context 'when head pipeline has test reports' do
let(:merge_request) { create(:merge_request, :with_test_reports, source_project: project) }
it { is_expected.to be_truthy }
end
context 'when head pipeline does not have test reports' do
let(:merge_request) { create(:merge_request, source_project: project) }
it { is_expected.to be_falsey }
end
end
describe '#compare_test_reports' do
subject { merge_request.compare_test_reports }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:base_pipeline) do
create(:ci_pipeline,
:with_test_reports,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha)
end
before do
merge_request.update!(head_pipeline_id: head_pipeline.id)
end
context 'when head pipeline has test reports' do
let!(:head_pipeline) do
create(:ci_pipeline,
:with_test_reports,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha)
end
context 'when reactive cache worker is parsing asynchronously' do
it 'returns status' do
expect(subject[:status]).to eq(:parsing)
end
end
context 'when reactive cache worker is inline' do
before do
synchronous_reactive_cache(merge_request)
end
it 'returns status and data' do
expect_any_instance_of(Ci::CompareTestReportsService)
.to receive(:execute).with(base_pipeline.iid, head_pipeline.iid)
subject
end
end
end
context 'when head pipeline does not have test reports' do
let!(:head_pipeline) do
create(:ci_pipeline,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha)
end
it 'returns status and error message' do
expect(subject[:status]).to eq(:error)
expect(subject[:status_reason]).to eq('This merge request does not have test reports')
end
end
end
describe '#all_commit_shas' do
context 'when merge request is persisted' do
let(:all_commit_shas) do
......@@ -2010,6 +2091,26 @@ describe MergeRequest do
end
end
describe '#base_pipeline' do
let(:pipeline_arguments) do
{
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha
}
end
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:first_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) }
let!(:last_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) }
it 'returns latest pipeline' do
expect(merge_request.base_pipeline).to eq(last_pipeline)
end
end
describe '#has_commits?' do
before do
allow(subject.merge_request_diff).to receive(:commits_count)
......
......@@ -1449,7 +1449,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
context 'when artifact_type is junit' do
context 'when artifact_format is gzip' do
let(:file_upload) { fixture_file_upload('spec/fixtures/junit.xml.gz') }
let(:file_upload) { fixture_file_upload('spec/fixtures/junit/junit.xml.gz') }
let(:params) { { artifact_type: :junit, artifact_format: :gzip } }
it 'stores junit test report' do
......@@ -1461,7 +1461,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
context 'when artifact_format is raw' do
let(:file_upload) { fixture_file_upload('spec/fixtures/junit.xml.gz') }
let(:file_upload) { fixture_file_upload('spec/fixtures/junit/junit.xml.gz') }
let(:params) { { artifact_type: :junit, artifact_format: :raw } }
it 'returns an error' do
......
require 'spec_helper'
describe TestCaseEntity do
include TestReportsHelper
let(:entity) { described_class.new(test_case) }
describe '#as_json' do
subject { entity.as_json }
context 'when test case is success' do
let(:test_case) { create_test_case_rspec_success }
it 'contains correct test case details' do
expect(subject[:status]).to eq('success')
expect(subject[:name]).to eq('Test#sum when a is 1 and b is 3 returns summary')
expect(subject[:execution_time]).to eq(1.11)
end
end
context 'when test case is failed' do
let(:test_case) { create_test_case_rspec_failed }
it 'contains correct test case details' do
expect(subject[:status]).to eq('failed')
expect(subject[:name]).to eq('Test#sum when a is 2 and b is 2 returns summary')
expect(subject[:execution_time]).to eq(2.22)
end
end
end
end
require 'spec_helper'
describe TestReportsComparerEntity do
include TestReportsHelper
let(:entity) { described_class.new(comparer) }
let(:comparer) { Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports) }
let(:base_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:head_reports) { Gitlab::Ci::Reports::TestReports.new }
describe '#as_json' do
subject { entity.as_json }
context 'when head and base reports include two test suites' do
context 'when the status of head report is success' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_success)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
it 'contains correct compared test reports details' do
expect(subject[:status]).to eq('success')
expect(subject[:summary]).to include(total: 2, resolved: 0, failed: 0)
expect(subject[:suites].first[:name]).to eq('rspec')
expect(subject[:suites].first[:status]).to eq('success')
expect(subject[:suites].second[:name]).to eq('junit')
expect(subject[:suites].second[:status]).to eq('success')
end
end
context 'when the status of head report is failed' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_success)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
end
it 'contains correct compared test reports details' do
expect(subject[:status]).to eq('failed')
expect(subject[:summary]).to include(total: 2, resolved: 0, failed: 1)
expect(subject[:suites].first[:name]).to eq('rspec')
expect(subject[:suites].first[:status]).to eq('success')
expect(subject[:suites].second[:name]).to eq('junit')
expect(subject[:suites].second[:status]).to eq('failed')
end
end
context 'when the status of head report is resolved' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved)
end
let(:create_test_case_java_resolved) do
create_test_case_java_failed.tap do |test_case|
test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
end
it 'contains correct compared test reports details' do
expect(subject[:status]).to eq('success')
expect(subject[:summary]).to include(total: 2, resolved: 1, failed: 0)
expect(subject[:suites].first[:name]).to eq('rspec')
expect(subject[:suites].first[:status]).to eq('success')
expect(subject[:suites].second[:name]).to eq('junit')
expect(subject[:suites].second[:status]).to eq('success')
end
end
end
end
end
require 'spec_helper'
describe TestReportsComparerSerializer do
include TestReportsHelper
let(:project) { double(:project) }
let(:serializer) { described_class.new(project: project).represent(comparer) }
let(:comparer) { Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports) }
let(:base_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:head_reports) { Gitlab::Ci::Reports::TestReports.new }
describe '#to_json' do
subject { serializer.to_json }
context 'when head and base reports include two test suites' do
context 'when the status of head report is success' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_success)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
it 'matches the schema' do
expect(subject).to match_schema('entities/test_reports_comparer')
end
end
context 'when the status of head report is failed' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_success)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
end
it 'matches the schema' do
expect(subject).to match_schema('entities/test_reports_comparer')
end
end
context 'when the status of head report is resolved' do
before do
base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
base_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved)
end
let(:create_test_case_java_resolved) do
create_test_case_java_failed.tap do |test_case|
test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
end
it 'matches the schema' do
expect(subject).to match_schema('entities/test_reports_comparer')
end
end
end
end
end
require 'spec_helper'
describe TestSuiteComparerEntity do
include TestReportsHelper
let(:entity) { described_class.new(comparer) }
let(:comparer) { Gitlab::Ci::Reports::TestSuiteComparer.new(name, base_suite, head_suite) }
let(:name) { 'rpsec' }
let(:base_suite) { Gitlab::Ci::Reports::TestSuite.new(name) }
let(:head_suite) { Gitlab::Ci::Reports::TestSuite.new(name) }
let(:test_case_success) { create_test_case_rspec_success }
let(:test_case_failed) { create_test_case_rspec_failed }
let(:test_case_resolved) do
create_test_case_rspec_failed.tap do |test_case|
test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
end
describe '#as_json' do
subject { entity.as_json }
context 'when head sutie has a newly failed test case which does not exist in base' do
before do
base_suite.add_test_case(test_case_success)
head_suite.add_test_case(test_case_failed)
end
it 'contains correct compared test suite details' do
expect(subject[:name]).to eq(name)
expect(subject[:status]).to eq('failed')
expect(subject[:summary]).to include(total: 1, resolved: 0, failed: 1)
subject[:new_failures].first.tap do |new_failure|
expect(new_failure[:status]).to eq(test_case_failed.status)
expect(new_failure[:name]).to eq(test_case_failed.name)
expect(new_failure[:execution_time]).to eq(test_case_failed.execution_time)
expect(new_failure[:system_output]).to eq(test_case_failed.system_output)
end
expect(subject[:resolved_failures]).to be_empty
expect(subject[:existing_failures]).to be_empty
end
end
context 'when head sutie still has a failed test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_failed)
end
it 'contains correct compared test suite details' do
expect(subject[:name]).to eq(name)
expect(subject[:status]).to eq('failed')
expect(subject[:summary]).to include(total: 1, resolved: 0, failed: 1)
expect(subject[:new_failures]).to be_empty
expect(subject[:resolved_failures]).to be_empty
subject[:existing_failures].first.tap do |existing_failure|
expect(existing_failure[:status]).to eq(test_case_failed.status)
expect(existing_failure[:name]).to eq(test_case_failed.name)
expect(existing_failure[:execution_time]).to eq(test_case_failed.execution_time)
expect(existing_failure[:system_output]).to eq(test_case_failed.system_output)
end
end
end
context 'when head sutie has a success test case which failed in base' do
before do
base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_resolved)
end
it 'contains correct compared test suite details' do
expect(subject[:name]).to eq(name)
expect(subject[:status]).to eq('success')
expect(subject[:summary]).to include(total: 1, resolved: 1, failed: 0)
expect(subject[:new_failures]).to be_empty
subject[:resolved_failures].first.tap do |resolved_failure|
expect(resolved_failure[:status]).to eq(test_case_resolved.status)
expect(resolved_failure[:name]).to eq(test_case_resolved.name)
expect(resolved_failure[:execution_time]).to eq(test_case_resolved.execution_time)
expect(resolved_failure[:system_output]).to eq(test_case_resolved.system_output)
end
expect(subject[:existing_failures]).to be_empty
end
end
end
end
require 'spec_helper'
describe Ci::CompareTestReportsService do
let(:service) { described_class.new(project) }
let(:project) { create(:project, :repository) }
describe '#execute' do
subject { service.execute(base_pipeline&.iid, head_pipeline.iid) }
context 'when head pipeline has test reports' do
let!(:base_pipeline) { nil }
let!(:head_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
it 'returns status and data' do
expect(subject[:status]).to eq(:parsed)
expect(subject[:data]).to match_schema('entities/test_reports_comparer')
end
end
context 'when base and head pipelines have test reports' do
let!(:base_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
let!(:head_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
it 'returns status and data' do
expect(subject[:status]).to eq(:parsed)
expect(subject[:data]).to match_schema('entities/test_reports_comparer')
end
end
context 'when head pipeline has corrupted test reports' do
let!(:base_pipeline) { nil }
let!(:head_pipeline) { create(:ci_pipeline, project: project) }
before do
build = create(:ci_build, pipeline: head_pipeline, project: head_pipeline.project)
create(:ci_job_artifact, :junit_with_corrupted_data, job: build, project: project)
end
it 'returns status and error message' do
expect(subject[:status]).to eq(:error)
expect(subject[:status_reason]).to include('XML parsing failed')
end
end
end
end
......@@ -14,8 +14,8 @@ module ReactiveCachingHelpers
end
def synchronous_reactive_cache(subject)
allow(service).to receive(:with_reactive_cache) do |*args, &block|
block.call(service.calculate_reactive_cache(*args))
allow(subject).to receive(:with_reactive_cache) do |*args, &block|
block.call(subject.calculate_reactive_cache(*args))
end
end
......
module TestReportsHelper
def create_test_case_rspec_success
Gitlab::Ci::Reports::TestCase.new(
name: 'Test#sum when a is 1 and b is 3 returns summary',
classname: 'spec.test_spec',
file: './spec/test_spec.rb',
execution_time: 1.11,
status: Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
def create_test_case_rspec_failed
Gitlab::Ci::Reports::TestCase.new(
name: 'Test#sum when a is 2 and b is 2 returns summary',
classname: 'spec.test_spec',
file: './spec/test_spec.rb',
execution_time: 2.22,
system_output: sample_rspec_failed_message,
status: Gitlab::Ci::Reports::TestCase::STATUS_FAILED)
end
def create_test_case_rspec_skipped
Gitlab::Ci::Reports::TestCase.new(
name: 'Test#sum when a is 3 and b is 3 returns summary',
classname: 'spec.test_spec',
file: './spec/test_spec.rb',
execution_time: 3.33,
status: Gitlab::Ci::Reports::TestCase::STATUS_SKIPPED)
end
def create_test_case_rspec_error
Gitlab::Ci::Reports::TestCase.new(
name: 'Test#sum when a is 4 and b is 4 returns summary',
classname: 'spec.test_spec',
file: './spec/test_spec.rb',
execution_time: 4.44,
status: Gitlab::Ci::Reports::TestCase::STATUS_ERROR)
end
def sample_rspec_failed_message
<<-EOF.strip_heredoc
Failure/Error: is_expected.to eq(3)
expected: 3
got: -1
(compared using ==)
./spec/test_spec.rb:12:in `block (4 levels) in &lt;top (required)&gt;&apos;
EOF
end
def create_test_case_java_success
Gitlab::Ci::Reports::TestCase.new(
name: 'addTest',
classname: 'CalculatorTest',
execution_time: 5.55,
status: Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
def create_test_case_java_failed
Gitlab::Ci::Reports::TestCase.new(
name: 'subtractTest',
classname: 'CalculatorTest',
execution_time: 6.66,
system_output: sample_java_failed_message,
status: Gitlab::Ci::Reports::TestCase::STATUS_FAILED)
end
def create_test_case_java_skipped
Gitlab::Ci::Reports::TestCase.new(
name: 'multiplyTest',
classname: 'CalculatorTest',
execution_time: 7.77,
status: Gitlab::Ci::Reports::TestCase::STATUS_SKIPPED)
end
def create_test_case_java_error
Gitlab::Ci::Reports::TestCase.new(
name: 'divideTest',
classname: 'CalculatorTest',
execution_time: 8.88,
status: Gitlab::Ci::Reports::TestCase::STATUS_ERROR)
end
def sample_java_failed_message
<<-EOF.strip_heredoc
junit.framework.AssertionFailedError: expected:&lt;1&gt; but was:&lt;3&gt;
at CalculatorTest.subtractExpression(Unknown Source)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
EOF
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