Commit 65e57ef3 authored by Tiger Watson's avatar Tiger Watson

Merge branch 'eb-test-failure-history-mvc-api' into 'master'

Include recent failures data in test reports response

See merge request gitlab-org/gitlab!45187
parents 11e5fcac 930d857e
...@@ -4,9 +4,26 @@ module Ci ...@@ -4,9 +4,26 @@ module Ci
class TestCaseFailure < ApplicationRecord class TestCaseFailure < ApplicationRecord
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
REPORT_WINDOW = 14.days
validates :test_case, :build, :failed_at, presence: true validates :test_case, :build, :failed_at, presence: true
belongs_to :test_case, class_name: "Ci::TestCase", foreign_key: :test_case_id belongs_to :test_case, class_name: "Ci::TestCase", foreign_key: :test_case_id
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
def self.recent_failures_count(project:, test_case_keys:, date_range: REPORT_WINDOW.ago..Time.current)
joins(:test_case)
.where(
ci_test_cases: {
project_id: project.id,
key_hash: test_case_keys
},
ci_test_case_failures: {
failed_at: date_range
}
)
.group(:key_hash)
.count('ci_test_case_failures.id')
end
end end
end end
...@@ -10,6 +10,7 @@ class TestCaseEntity < Grape::Entity ...@@ -10,6 +10,7 @@ class TestCaseEntity < Grape::Entity
expose :execution_time expose :execution_time
expose :system_output expose :system_output
expose :stack_trace expose :stack_trace
expose :recent_failures
expose :attachment_url, if: -> (*) { can_read_screenshots? } do |test_case| expose :attachment_url, if: -> (*) { can_read_screenshots? } do |test_case|
expose_url(test_case.attachment_url) expose_url(test_case.attachment_url)
end end
......
# frozen_string_literal: true # frozen_string_literal: true
class TestSuiteComparerEntity < Grape::Entity class TestSuiteComparerEntity < Grape::Entity
DEFAULT_MAX_TESTS = 100
DEFAULT_MIN_TESTS = 10
expose :name expose :name
expose :total_status, as: :status expose :total_status, as: :status
...@@ -14,39 +11,27 @@ class TestSuiteComparerEntity < Grape::Entity ...@@ -14,39 +11,27 @@ class TestSuiteComparerEntity < Grape::Entity
expose :error_count, as: :errored expose :error_count, as: :errored
end end
# rubocop: disable CodeReuse/ActiveRecord
expose :new_failures, using: TestCaseEntity do |suite| expose :new_failures, using: TestCaseEntity do |suite|
suite.new_failures.take(max_tests) suite.limited_tests.new_failures
end end
expose :existing_failures, using: TestCaseEntity do |suite| expose :existing_failures, using: TestCaseEntity do |suite|
suite.existing_failures.take( suite.limited_tests.existing_failures
max_tests(suite.new_failures))
end end
expose :resolved_failures, using: TestCaseEntity do |suite| expose :resolved_failures, using: TestCaseEntity do |suite|
suite.resolved_failures.take( suite.limited_tests.resolved_failures
max_tests(suite.new_failures, suite.existing_failures))
end end
expose :new_errors, using: TestCaseEntity do |suite| expose :new_errors, using: TestCaseEntity do |suite|
suite.new_errors.take(max_tests) suite.limited_tests.new_errors
end end
expose :existing_errors, using: TestCaseEntity do |suite| expose :existing_errors, using: TestCaseEntity do |suite|
suite.existing_errors.take( suite.limited_tests.existing_errors
max_tests(suite.new_errors))
end end
expose :resolved_errors, using: TestCaseEntity do |suite| expose :resolved_errors, using: TestCaseEntity do |suite|
suite.resolved_errors.take( suite.limited_tests.resolved_errors
max_tests(suite.new_errors, suite.existing_errors))
end
private
def max_tests(*used)
[DEFAULT_MAX_TESTS - used.map(&:count).sum, DEFAULT_MIN_TESTS].max
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
...@@ -8,7 +8,9 @@ module Ci ...@@ -8,7 +8,9 @@ module Ci
# issue: https://gitlab.com/gitlab-org/gitlab/issues/34224 # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
class CompareReportsBaseService < ::BaseService class CompareReportsBaseService < ::BaseService
def execute(base_pipeline, head_pipeline) def execute(base_pipeline, head_pipeline)
comparer = build_comparer(base_pipeline, head_pipeline) base_report = get_report(base_pipeline)
head_report = get_report(head_pipeline)
comparer = build_comparer(base_report, head_report)
{ {
status: :parsed, status: :parsed,
...@@ -31,8 +33,8 @@ module Ci ...@@ -31,8 +33,8 @@ module Ci
protected protected
def build_comparer(base_pipeline, head_pipeline) def build_comparer(base_report, head_report)
comparer_class.new(get_report(base_pipeline), get_report(head_pipeline)) comparer_class.new(base_report, head_report)
end end
private private
......
...@@ -13,5 +13,19 @@ module Ci ...@@ -13,5 +13,19 @@ module Ci
def get_report(pipeline) def get_report(pipeline)
pipeline&.test_reports pipeline&.test_reports
end end
def build_comparer(base_report, head_report)
# We need to load the test failure history on the test comparer because we display
# this on the MR widget
super.tap do |test_reports_comparer|
::Gitlab::Ci::Reports::TestFailureHistory.new(failed_test_cases(test_reports_comparer), project).load!
end
end
def failed_test_cases(test_reports_comparer)
test_reports_comparer.suite_comparers.flat_map do |suite_comparer|
suite_comparer.limited_tests.new_failures + suite_comparer.limited_tests.existing_failures
end
end
end end
end end
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
STATUS_ERROR = 'error' STATUS_ERROR = 'error'
STATUS_TYPES = [STATUS_ERROR, STATUS_FAILED, STATUS_SUCCESS, STATUS_SKIPPED].freeze STATUS_TYPES = [STATUS_ERROR, STATUS_FAILED, STATUS_SUCCESS, STATUS_SKIPPED].freeze
attr_reader :suite_name, :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key, :attachment, :job attr_reader :suite_name, :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key, :attachment, :job, :recent_failures
def initialize(params) def initialize(params)
@suite_name = params.fetch(:suite_name) @suite_name = params.fetch(:suite_name)
...@@ -24,9 +24,15 @@ module Gitlab ...@@ -24,9 +24,15 @@ module Gitlab
@attachment = params.fetch(:attachment, nil) @attachment = params.fetch(:attachment, nil)
@job = params.fetch(:job, nil) @job = params.fetch(:job, nil)
@recent_failures = nil
@key = hash_key("#{suite_name}_#{classname}_#{name}") @key = hash_key("#{suite_name}_#{classname}_#{name}")
end end
def set_recent_failures(count, base_branch)
@recent_failures = { count: count, base_branch: base_branch }
end
def has_attachment? def has_attachment?
attachment.present? attachment.present?
end end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
class TestFailureHistory
include Gitlab::Utils::StrongMemoize
def initialize(failed_test_cases, project)
@failed_test_cases = build_map(failed_test_cases)
@project = project
end
def load!
return unless Feature.enabled?(:test_failure_history, project)
recent_failures_count.each do |key_hash, count|
failed_test_cases[key_hash].set_recent_failures(count, project.default_branch_or_master)
end
end
private
attr_reader :report, :project, :failed_test_cases
def recent_failures_count
::Ci::TestCaseFailure.recent_failures_count(
project: project,
test_case_keys: failed_test_cases.keys
)
end
def build_map(test_cases)
{}.tap do |hash|
test_cases.each do |test_case|
hash[test_case.key] = test_case
end
end
end
end
end
end
end
...@@ -6,6 +6,9 @@ module Gitlab ...@@ -6,6 +6,9 @@ module Gitlab
class TestSuiteComparer class TestSuiteComparer
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
DEFAULT_MAX_TESTS = 100
DEFAULT_MIN_TESTS = 10
attr_reader :name, :base_suite, :head_suite attr_reader :name, :base_suite, :head_suite
def initialize(name, base_suite, head_suite) def initialize(name, base_suite, head_suite)
...@@ -81,6 +84,29 @@ module Gitlab ...@@ -81,6 +84,29 @@ module Gitlab
def error_count def error_count
new_errors.count + existing_errors.count new_errors.count + existing_errors.count
end end
# This is used to limit the presented test cases but does not affect
# total count of tests in the summary
def limited_tests
strong_memoize(:limited_tests) do
# rubocop: disable CodeReuse/ActiveRecord
OpenStruct.new(
new_failures: new_failures.take(max_tests),
existing_failures: existing_failures.take(max_tests(new_failures)),
resolved_failures: resolved_failures.take(max_tests(new_failures, existing_failures)),
new_errors: new_errors.take(max_tests),
existing_errors: existing_errors.take(max_tests(new_errors)),
resolved_errors: resolved_errors.take(max_tests(new_errors, existing_errors))
)
# rubocop: enable CodeReuse/ActiveRecord
end
end
private
def max_tests(*used)
[DEFAULT_MAX_TESTS - used.map(&:count).sum, DEFAULT_MIN_TESTS].max
end
end end
end end
end end
......
...@@ -338,6 +338,12 @@ FactoryBot.define do ...@@ -338,6 +338,12 @@ FactoryBot.define do
end end
end end
trait :test_reports_with_three_failures do
after(:build) do |build|
build.job_artifacts << create(:ci_job_artifact, :junit_with_three_failures, job: build)
end
end
trait :accessibility_reports do trait :accessibility_reports do
after(:build) do |build| after(:build) do |build|
build.job_artifacts << create(:ci_job_artifact, :accessibility, job: build) build.job_artifacts << create(:ci_job_artifact, :accessibility, job: build)
......
...@@ -149,6 +149,16 @@ FactoryBot.define do ...@@ -149,6 +149,16 @@ FactoryBot.define do
end end
end end
trait :junit_with_three_failures 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_failures.xml.gz'), 'application/x-gzip')
end
end
trait :accessibility do trait :accessibility do
file_type { :accessibility } file_type { :accessibility }
file_format { :raw } file_format { :raw }
......
...@@ -121,6 +121,14 @@ FactoryBot.define do ...@@ -121,6 +121,14 @@ FactoryBot.define do
end end
end end
trait :with_test_reports_with_three_failures do
status { :success }
after(:build) do |pipeline, _evaluator|
pipeline.builds << build(:ci_build, :test_reports_with_three_failures, pipeline: pipeline, project: pipeline.project)
end
end
trait :with_accessibility_reports do trait :with_accessibility_reports do
status { :success } status { :success }
......
...@@ -558,8 +558,9 @@ RSpec.describe 'Merge request > User sees merge widget', :js do ...@@ -558,8 +558,9 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end end
before do before do
allow_any_instance_of(TestSuiteComparerEntity) stub_const("Gitlab::Ci::Reports::TestSuiteComparer::DEFAULT_MAX_TESTS", 2)
.to receive(:max_tests).and_return(2) stub_const("Gitlab::Ci::Reports::TestSuiteComparer::DEFAULT_MIN_TESTS", 1)
allow_any_instance_of(MergeRequest) allow_any_instance_of(MergeRequest)
.to receive(:has_test_reports?).and_return(true) .to receive(:has_test_reports?).and_return(true)
allow_any_instance_of(MergeRequest) allow_any_instance_of(MergeRequest)
......
...@@ -12,7 +12,13 @@ ...@@ -12,7 +12,13 @@
"execution_time": { "type": "float" }, "execution_time": { "type": "float" },
"system_output": { "type": ["string", "null"] }, "system_output": { "type": ["string", "null"] },
"stack_trace": { "type": ["string", "null"] }, "stack_trace": { "type": ["string", "null"] },
"attachment_url": { "type": ["string", "null"] } "attachment_url": { "type": ["string", "null"] },
"recent_failures": {
"oneOf": [
{ "type": "null" },
{ "$ref": "test_case/recent_failures.json" }
]
}
}, },
"additionalProperties": false "additionalProperties": false
} }
{
"type": "object",
"required": [
"count",
"base_branch"
],
"properties": {
"count": { "type": "integer" },
"base_branch": { "type": "string" }
},
"additionalProperties": false
}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::TestCase do RSpec.describe Gitlab::Ci::Reports::TestCase, :aggregate_failures do
describe '#initialize' do describe '#initialize' do
let(:test_case) { described_class.new(params) } let(:test_case) { described_class.new(params) }
...@@ -82,4 +82,17 @@ RSpec.describe Gitlab::Ci::Reports::TestCase do ...@@ -82,4 +82,17 @@ RSpec.describe Gitlab::Ci::Reports::TestCase do
end end
end end
end end
describe '#set_recent_failures' do
it 'sets the recent_failures information' do
test_case = build(:report_test_case)
test_case.set_recent_failures(1, 'master')
expect(test_case.recent_failures).to eq(
count: 1,
base_branch: 'master'
)
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::TestFailureHistory, :aggregate_failures do
include TestReportsHelper
describe '#load!' do
let_it_be(:project) { create(:project) }
let(:failed_rspec) { create_test_case_rspec_failed }
let(:failed_java) { create_test_case_java_failed }
subject(:load_history) { described_class.new([failed_rspec, failed_java], project).load! }
before do
allow(Ci::TestCaseFailure)
.to receive(:recent_failures_count)
.with(project: project, test_case_keys: [failed_rspec.key, failed_java.key])
.and_return(
failed_rspec.key => 2,
failed_java.key => 1
)
end
it 'sets the recent failures for each matching failed test case in all test suites' do
load_history
expect(failed_rspec.recent_failures).to eq(count: 2, base_branch: 'master')
expect(failed_java.recent_failures).to eq(count: 1, base_branch: 'master')
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(test_failure_history: false)
end
it 'does not set recent failures' do
load_history
expect(failed_rspec.recent_failures).to be_nil
expect(failed_java.recent_failures).to be_nil
end
end
end
end
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer, :aggregate_failures do
include TestReportsHelper include TestReportsHelper
let(:comparer) { described_class.new(name, base_suite, head_suite) } let(:comparer) { described_class.new(name, base_suite, head_suite) }
let(:name) { 'rpsec' } let(:name) { 'rspec' }
let(:base_suite) { Gitlab::Ci::Reports::TestSuite.new(name) } let(:base_suite) { Gitlab::Ci::Reports::TestSuite.new(name) }
let(:head_suite) { Gitlab::Ci::Reports::TestSuite.new(name) } let(:head_suite) { Gitlab::Ci::Reports::TestSuite.new(name) }
let(:test_case_success) { create_test_case_java_success } let(:test_case_success) { create_test_case_java_success }
...@@ -16,7 +16,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do ...@@ -16,7 +16,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do
describe '#new_failures' do describe '#new_failures' do
subject { comparer.new_failures } subject { comparer.new_failures }
context 'when head sutie has a newly failed test case which does not exist in base' do context 'when head suite has a newly failed test case which does not exist in base' do
before do before do
base_suite.add_test_case(test_case_success) base_suite.add_test_case(test_case_success)
head_suite.add_test_case(test_case_failed) head_suite.add_test_case(test_case_failed)
...@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do ...@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do
end end
end end
context 'when head sutie still has a failed test case which failed in base' do context 'when head suite still has a failed test case which failed in base' do
before do before do
base_suite.add_test_case(test_case_failed) base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_failed) head_suite.add_test_case(test_case_failed)
...@@ -38,7 +38,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do ...@@ -38,7 +38,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do
end end
end end
context 'when head sutie has a success test case which failed in base' do context 'when head suite has a success test case which failed in base' do
before do before do
base_suite.add_test_case(test_case_failed) base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_success) head_suite.add_test_case(test_case_success)
...@@ -53,7 +53,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do ...@@ -53,7 +53,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do
describe '#existing_failures' do describe '#existing_failures' do
subject { comparer.existing_failures } subject { comparer.existing_failures }
context 'when head sutie has a newly failed test case which does not exist in base' do context 'when head suite has a newly failed test case which does not exist in base' do
before do before do
base_suite.add_test_case(test_case_success) base_suite.add_test_case(test_case_success)
head_suite.add_test_case(test_case_failed) head_suite.add_test_case(test_case_failed)
...@@ -64,7 +64,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do ...@@ -64,7 +64,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do
end end
end end
context 'when head sutie still has a failed test case which failed in base' do context 'when head suite still has a failed test case which failed in base' do
before do before do
base_suite.add_test_case(test_case_failed) base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_failed) head_suite.add_test_case(test_case_failed)
...@@ -75,7 +75,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do ...@@ -75,7 +75,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do
end end
end end
context 'when head sutie has a success test case which failed in base' do context 'when head suite has a success test case which failed in base' do
before do before do
base_suite.add_test_case(test_case_failed) base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_success) head_suite.add_test_case(test_case_success)
...@@ -90,7 +90,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do ...@@ -90,7 +90,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do
describe '#resolved_failures' do describe '#resolved_failures' do
subject { comparer.resolved_failures } subject { comparer.resolved_failures }
context 'when head sutie has a newly failed test case which does not exist in base' do context 'when head suite has a newly failed test case which does not exist in base' do
before do before do
base_suite.add_test_case(test_case_success) base_suite.add_test_case(test_case_success)
head_suite.add_test_case(test_case_failed) head_suite.add_test_case(test_case_failed)
...@@ -105,7 +105,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do ...@@ -105,7 +105,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do
end end
end end
context 'when head sutie still has a failed test case which failed in base' do context 'when head suite still has a failed test case which failed in base' do
before do before do
base_suite.add_test_case(test_case_failed) base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_failed) head_suite.add_test_case(test_case_failed)
...@@ -120,7 +120,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do ...@@ -120,7 +120,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do
end end
end end
context 'when head sutie has a success test case which failed in base' do context 'when head suite has a success test case which failed in base' do
before do before do
base_suite.add_test_case(test_case_failed) base_suite.add_test_case(test_case_failed)
head_suite.add_test_case(test_case_success) head_suite.add_test_case(test_case_success)
...@@ -347,4 +347,128 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do ...@@ -347,4 +347,128 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteComparer do
end end
end end
end end
describe '#limited_tests' do
subject(:limited_tests) { comparer.limited_tests }
context 'limits amount of tests returned' do
before do
stub_const("#{described_class}::DEFAULT_MAX_TESTS", 2)
stub_const("#{described_class}::DEFAULT_MIN_TESTS", 1)
end
context 'prefers new over existing and resolved' do
before do
3.times { add_new_failure }
3.times { add_new_error }
3.times { add_existing_failure }
3.times { add_existing_error }
3.times { add_resolved_failure }
3.times { add_resolved_error }
end
it 'returns 2 of each new category, and 1 of each resolved and existing' do
expect(limited_tests.new_failures.count).to eq(2)
expect(limited_tests.new_errors.count).to eq(2)
expect(limited_tests.existing_failures.count).to eq(1)
expect(limited_tests.existing_errors.count).to eq(1)
expect(limited_tests.resolved_failures.count).to eq(1)
expect(limited_tests.resolved_errors.count).to eq(1)
end
it 'does not affect the overall count' do
expect(summary).to include(total: 18, resolved: 6, failed: 6, errored: 6)
end
end
context 'prefers existing over resolved' do
before do
3.times { add_existing_failure }
3.times { add_existing_error }
3.times { add_resolved_failure }
3.times { add_resolved_error }
end
it 'returns 2 of each existing category, and 1 of each resolved' do
expect(limited_tests.new_failures.count).to eq(0)
expect(limited_tests.new_errors.count).to eq(0)
expect(limited_tests.existing_failures.count).to eq(2)
expect(limited_tests.existing_errors.count).to eq(2)
expect(limited_tests.resolved_failures.count).to eq(1)
expect(limited_tests.resolved_errors.count).to eq(1)
end
it 'does not affect the overall count' do
expect(summary).to include(total: 12, resolved: 6, failed: 3, errored: 3)
end
end
context 'limits amount of resolved' do
before do
3.times { add_resolved_failure }
3.times { add_resolved_error }
end
it 'returns 2 of each resolved category' do
expect(limited_tests.new_failures.count).to eq(0)
expect(limited_tests.new_errors.count).to eq(0)
expect(limited_tests.existing_failures.count).to eq(0)
expect(limited_tests.existing_errors.count).to eq(0)
expect(limited_tests.resolved_failures.count).to eq(2)
expect(limited_tests.resolved_errors.count).to eq(2)
end
it 'does not affect the overall count' do
expect(summary).to include(total: 6, resolved: 6, failed: 0, errored: 0)
end
end
end
def summary
{
total: comparer.total_count,
resolved: comparer.resolved_count,
failed: comparer.failed_count,
errored: comparer.error_count
}
end
def add_new_failure
failed_case = create_test_case_rspec_failed(SecureRandom.hex)
head_suite.add_test_case(failed_case)
end
def add_new_error
error_case = create_test_case_rspec_error(SecureRandom.hex)
head_suite.add_test_case(error_case)
end
def add_existing_failure
failed_case = create_test_case_rspec_failed(SecureRandom.hex)
base_suite.add_test_case(failed_case)
head_suite.add_test_case(failed_case)
end
def add_existing_error
error_case = create_test_case_rspec_error(SecureRandom.hex)
base_suite.add_test_case(error_case)
head_suite.add_test_case(error_case)
end
def add_resolved_failure
case_name = SecureRandom.hex
failed_case = create_test_case_java_failed(case_name)
success_case = create_test_case_java_success(case_name)
base_suite.add_test_case(failed_case)
head_suite.add_test_case(success_case)
end
def add_resolved_error
case_name = SecureRandom.hex
error_case = create_test_case_java_error(case_name)
success_case = create_test_case_java_success(case_name)
base_suite.add_test_case(error_case)
head_suite.add_test_case(success_case)
end
end
end end
...@@ -15,4 +15,59 @@ RSpec.describe Ci::TestCaseFailure do ...@@ -15,4 +15,59 @@ RSpec.describe Ci::TestCaseFailure do
it { is_expected.to validate_presence_of(:build) } it { is_expected.to validate_presence_of(:build) }
it { is_expected.to validate_presence_of(:failed_at) } it { is_expected.to validate_presence_of(:failed_at) }
end end
describe '.recent_failures_count' do
let_it_be(:project) { create(:project) }
subject(:recent_failures) do
described_class.recent_failures_count(
project: project,
test_case_keys: test_case_keys
)
end
context 'when test case failures are within the date range and are for the test case keys' do
let(:tc_1) { create(:ci_test_case, project: project) }
let(:tc_2) { create(:ci_test_case, project: project) }
let(:test_case_keys) { [tc_1.key_hash, tc_2.key_hash] }
before do
create_list(:ci_test_case_failure, 3, test_case: tc_1, failed_at: 1.day.ago)
create_list(:ci_test_case_failure, 2, test_case: tc_2, failed_at: 3.days.ago)
end
it 'returns the number of failures for each test case key hash for the past 14 days by default' do
expect(recent_failures).to eq(
tc_1.key_hash => 3,
tc_2.key_hash => 2
)
end
end
context 'when test case failures are within the date range but are not for the test case keys' do
let(:tc) { create(:ci_test_case, project: project) }
let(:test_case_keys) { ['some-other-key-hash'] }
before do
create(:ci_test_case_failure, test_case: tc, failed_at: 1.day.ago)
end
it 'excludes them from the count' do
expect(recent_failures[tc.key_hash]).to be_nil
end
end
context 'when test case failures are not within the date range but are for the test case keys' do
let(:tc) { create(:ci_test_case, project: project) }
let(:test_case_keys) { [tc.key_hash] }
before do
create(:ci_test_case_failure, test_case: tc, failed_at: 15.days.ago)
end
it 'excludes them from the count' do
expect(recent_failures[tc.key_hash]).to be_nil
end
end
end
end end
...@@ -27,12 +27,17 @@ RSpec.describe TestCaseEntity do ...@@ -27,12 +27,17 @@ RSpec.describe TestCaseEntity do
context 'when test case is failed' do context 'when test case is failed' do
let(:test_case) { create_test_case_rspec_failed } let(:test_case) { create_test_case_rspec_failed }
before do
test_case.set_recent_failures(3, 'master')
end
it 'contains correct test case details' do it 'contains correct test case details' do
expect(subject[:status]).to eq('failed') expect(subject[:status]).to eq('failed')
expect(subject[:name]).to eq('Test#sum when a is 1 and b is 3 returns summary') expect(subject[:name]).to eq('Test#sum when a is 1 and b is 3 returns summary')
expect(subject[:classname]).to eq('spec.test_spec') expect(subject[:classname]).to eq('spec.test_spec')
expect(subject[:file]).to eq('./spec/test_spec.rb') expect(subject[:file]).to eq('./spec/test_spec.rb')
expect(subject[:execution_time]).to eq(2.22) expect(subject[:execution_time]).to eq(2.22)
expect(subject[:recent_failures]).to eq({ count: 3, base_branch: 'master' })
end end
end end
......
...@@ -100,109 +100,5 @@ RSpec.describe TestSuiteComparerEntity do ...@@ -100,109 +100,5 @@ RSpec.describe TestSuiteComparerEntity do
expect(subject[:existing_failures]).to be_empty expect(subject[:existing_failures]).to be_empty
end end
end end
context 'limits amount of tests returned' do
before do
stub_const('TestSuiteComparerEntity::DEFAULT_MAX_TESTS', 2)
stub_const('TestSuiteComparerEntity::DEFAULT_MIN_TESTS', 1)
end
context 'prefers new over existing and resolved' do
before do
3.times { add_new_failure }
3.times { add_new_error }
3.times { add_existing_failure }
3.times { add_existing_error }
3.times { add_resolved_failure }
3.times { add_resolved_error }
end
it 'returns 2 of each new category, and 1 of each resolved and existing' do
expect(subject[:summary]).to include(total: 18, resolved: 6, failed: 6, errored: 6)
expect(subject[:new_failures].count).to eq(2)
expect(subject[:new_errors].count).to eq(2)
expect(subject[:existing_failures].count).to eq(1)
expect(subject[:existing_errors].count).to eq(1)
expect(subject[:resolved_failures].count).to eq(1)
expect(subject[:resolved_errors].count).to eq(1)
end
end
context 'prefers existing over resolved' do
before do
3.times { add_existing_failure }
3.times { add_existing_error }
3.times { add_resolved_failure }
3.times { add_resolved_error }
end
it 'returns 2 of each existing category, and 1 of each resolved' do
expect(subject[:summary]).to include(total: 12, resolved: 6, failed: 3, errored: 3)
expect(subject[:new_failures].count).to eq(0)
expect(subject[:new_errors].count).to eq(0)
expect(subject[:existing_failures].count).to eq(2)
expect(subject[:existing_errors].count).to eq(2)
expect(subject[:resolved_failures].count).to eq(1)
expect(subject[:resolved_errors].count).to eq(1)
end
end
context 'limits amount of resolved' do
before do
3.times { add_resolved_failure }
3.times { add_resolved_error }
end
it 'returns 2 of each resolved category' do
expect(subject[:summary]).to include(total: 6, resolved: 6, failed: 0, errored: 0)
expect(subject[:new_failures].count).to eq(0)
expect(subject[:new_errors].count).to eq(0)
expect(subject[:existing_failures].count).to eq(0)
expect(subject[:existing_errors].count).to eq(0)
expect(subject[:resolved_failures].count).to eq(2)
expect(subject[:resolved_errors].count).to eq(2)
end
end
private
def add_new_failure
failed_case = create_test_case_rspec_failed(SecureRandom.hex)
head_suite.add_test_case(failed_case)
end
def add_new_error
error_case = create_test_case_rspec_error(SecureRandom.hex)
head_suite.add_test_case(error_case)
end
def add_existing_failure
failed_case = create_test_case_rspec_failed(SecureRandom.hex)
base_suite.add_test_case(failed_case)
head_suite.add_test_case(failed_case)
end
def add_existing_error
error_case = create_test_case_rspec_error(SecureRandom.hex)
base_suite.add_test_case(error_case)
head_suite.add_test_case(error_case)
end
def add_resolved_failure
case_name = SecureRandom.hex
failed_case = create_test_case_java_failed(case_name)
success_case = create_test_case_java_success(case_name)
base_suite.add_test_case(failed_case)
head_suite.add_test_case(success_case)
end
def add_resolved_error
case_name = SecureRandom.hex
error_case = create_test_case_java_error(case_name)
success_case = create_test_case_java_success(case_name)
base_suite.add_test_case(error_case)
head_suite.add_test_case(success_case)
end
end
end end
end end
...@@ -7,15 +7,15 @@ RSpec.describe Ci::CompareTestReportsService do ...@@ -7,15 +7,15 @@ RSpec.describe Ci::CompareTestReportsService do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
describe '#execute' do describe '#execute' do
subject { service.execute(base_pipeline, head_pipeline) } subject(:comparison) { service.execute(base_pipeline, head_pipeline) }
context 'when head pipeline has test reports' do context 'when head pipeline has test reports' do
let!(:base_pipeline) { nil } let!(:base_pipeline) { nil }
let!(:head_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 it 'returns status and data' do
expect(subject[:status]).to eq(:parsed) expect(comparison[:status]).to eq(:parsed)
expect(subject[:data]).to match_schema('entities/test_reports_comparer') expect(comparison[:data]).to match_schema('entities/test_reports_comparer')
end end
end end
...@@ -24,8 +24,8 @@ RSpec.describe Ci::CompareTestReportsService do ...@@ -24,8 +24,8 @@ RSpec.describe Ci::CompareTestReportsService do
let!(:head_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 it 'returns status and data' do
expect(subject[:status]).to eq(:parsed) expect(comparison[:status]).to eq(:parsed)
expect(subject[:data]).to match_schema('entities/test_reports_comparer') expect(comparison[:data]).to match_schema('entities/test_reports_comparer')
end end
end end
...@@ -39,9 +39,44 @@ RSpec.describe Ci::CompareTestReportsService do ...@@ -39,9 +39,44 @@ RSpec.describe Ci::CompareTestReportsService do
end end
it 'returns a parsed TestReports success status and failure on the individual suite' do it 'returns a parsed TestReports success status and failure on the individual suite' do
expect(subject[:status]).to eq(:parsed) expect(comparison[:status]).to eq(:parsed)
expect(subject.dig(:data, 'status')).to eq('success') expect(comparison.dig(:data, 'status')).to eq('success')
expect(subject.dig(:data, 'suites', 0, 'status') ).to eq('error') expect(comparison.dig(:data, 'suites', 0, 'status') ).to eq('error')
end
end
context 'test failure history' do
let!(:base_pipeline) { nil }
let!(:head_pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project) }
let(:new_failures) do
comparison.dig(:data, 'suites', 0, 'new_failures')
end
let(:recent_failures_per_test_case) do
new_failures.map { |f| f['recent_failures'] }
end
# Create test case failure records based on the head pipeline build
before do
stub_const("Gitlab::Ci::Reports::TestSuiteComparer::DEFAULT_MAX_TESTS", 2)
stub_const("Gitlab::Ci::Reports::TestSuiteComparer::DEFAULT_MIN_TESTS", 1)
build = head_pipeline.builds.last
build.update_column(:finished_at, 1.day.ago) # Just to be sure we are included in the report window
# The JUnit fixture for the given build has 3 failures.
# This service will create 1 test case failure record for each.
Ci::TestCasesService.new.execute(build)
end
it 'loads recent failures on limited test cases to avoid building up a huge DB query', :aggregate_failures do
expect(comparison[:data]).to match_schema('entities/test_reports_comparer')
expect(recent_failures_per_test_case).to eq([
{ 'count' => 1, 'base_branch' => 'master' },
{ 'count' => 1, 'base_branch' => 'master' }
])
expect(new_failures.count).to eq(2)
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment