Commit 16e460bf authored by Erick Bajao's avatar Erick Bajao

Include recent failures data in test reports response

This affects the test_reports.json endpoint to include
the `recent_failures` data in each test case entity.
parent 13e00158
...@@ -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
......
...@@ -13,5 +13,16 @@ module Ci ...@@ -13,5 +13,16 @@ module Ci
def get_report(pipeline) def get_report(pipeline)
pipeline&.test_reports pipeline&.test_reports
end end
def build_comparer(base_pipeline, head_pipeline)
base_report = get_report(base_pipeline)
head_report = get_report(head_pipeline)
# We need to load the test failure history for the head report because we display
# this on the MR widget
::Gitlab::Ci::Reports::TestReportFailureHistory.new(head_report, project).load!
comparer_class.new(base_report, head_report)
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 TestReportFailureHistory
include Gitlab::Utils::StrongMemoize
def initialize(report, project)
@report = report
@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
def recent_failures_count
::Ci::TestCaseFailure.recent_failures_count(
project: project,
test_case_keys: failed_test_cases.keys
)
end
def failed_test_cases
strong_memoize(:failed_test_cases) do
report.failed_test_cases
end
end
end
end
end
end
...@@ -52,6 +52,14 @@ module Gitlab ...@@ -52,6 +52,14 @@ module Gitlab
test_suites.values.sum { |suite| suite.public_send("#{status_type}_count") } # rubocop:disable GitlabSecurity/PublicSend test_suites.values.sum { |suite| suite.public_send("#{status_type}_count") } # rubocop:disable GitlabSecurity/PublicSend
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
end end
define_method("#{status_type}_test_cases") do
{}.tap do |hash|
test_suites.values.each do |test_suite|
hash.merge!(test_suite.public_send(status_type)) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end end
end end
end end
......
...@@ -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::TestReportFailureHistory, :aggregate_failures do
include TestReportsHelper
describe '#load!' do
let_it_be(:project) { create(:project) }
let(:test_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:failed_rspec) { create_test_case_rspec_failed }
let(:failed_java) { create_test_case_java_failed }
subject(:load_history) { described_class.new(test_reports, project).load! }
before do
test_reports.get_suite('rspec').add_test_case(failed_rspec)
test_reports.get_suite('java').add_test_case(failed_java)
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
...@@ -185,5 +185,30 @@ RSpec.describe Gitlab::Ci::Reports::TestReports do ...@@ -185,5 +185,30 @@ RSpec.describe Gitlab::Ci::Reports::TestReports do
end end
end end
end end
describe "##{status_type}_test_cases" do
subject { test_reports.public_send("#{status_type}_test_cases") }
context "when #{status_type} test case exists" do
it 'returns the test cases' do
rspec_case = public_send("create_test_case_rspec_#{status_type}")
java_case = public_send("create_test_case_java_#{status_type}")
test_reports.get_suite('rspec').add_test_case(rspec_case)
test_reports.get_suite('junit').add_test_case(java_case)
is_expected.to eq(
rspec_case.key => rspec_case,
java_case.key => java_case
)
end
end
context "when #{status_type} test case do not exist" do
it 'returns nothing' do
is_expected.to eq({})
end
end
end
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
......
...@@ -44,6 +44,33 @@ RSpec.describe Ci::CompareTestReportsService do ...@@ -44,6 +44,33 @@ RSpec.describe Ci::CompareTestReportsService do
expect(subject.dig(:data, 'suites', 0, 'status') ).to eq('error') expect(subject.dig(:data, 'suites', 0, 'status') ).to eq('error')
end end
end end
context 'test failure history' do
let!(:base_pipeline) { nil }
let!(:head_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
let(:recent_failures_per_test_case) do
subject.dig(:data, 'suites', 0, 'new_failures').map { |f| f['recent_failures'] }
end
# Create test case failure records based on the head pipeline build
before do
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 2 failures.
# This service will create 1 test case failure record for each.
Ci::TestCasesService.new.execute(build)
end
it 'loads on the report', :aggregate_failures do
expect(subject[: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' }
])
end
end
end end
describe '#latest?' do describe '#latest?' do
......
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