Commit e5168c8c authored by Erick Bajao's avatar Erick Bajao

Store daily code coverage

Hooks into PipelineSuccessWorker to run a service
that will store the daily code coverage values.
parent 479fa52d
......@@ -174,6 +174,8 @@ module Ci
pipeline: Ci::Pipeline::PROJECT_ROUTE_AND_NAMESPACE_ROUTE)
end
scope :with_coverage, -> { where.not(coverage: nil) }
acts_as_taggable
add_authentication_token_field :token, encrypted: :optional
......
# frozen_string_literal: true
class DailyCodeCoverage < ApplicationRecord
validates :project_id, presence: true, uniqueness: { scope: [:ref, :name, :date], case_sensitive: false }
validates :last_pipeline_id, presence: true
validates :ref, presence: true
validates :name, presence: true
validates :coverage, presence: true
validates :date, presence: true
validate :newer_pipeline
private
def newer_pipeline
return if new_record?
return unless last_pipeline_id_changed?
old_pipeline_id, new_pipeline_id = last_pipeline_id_change
return if new_pipeline_id > old_pipeline_id
errors.add(:last_pipeline_id, 'new pipeline ID must be newer than the existing one')
end
end
# frozen_string_literal: true
module Ci
class DailyCodeCoverageService
def execute(pipeline)
pipeline.builds.with_coverage.each do |build|
daily_coverage = daily_coverage_for(pipeline, build)
daily_coverage.with_lock do
daily_coverage.coverage = build.coverage
daily_coverage.last_pipeline_id = pipeline.id
daily_coverage.save
end
end
end
private
def daily_coverage_for(pipeline, build)
# rubocop: disable CodeReuse/ActiveRecord
DailyCodeCoverage.find_or_initialize_by(
project_id: pipeline.project_id,
ref: pipeline.ref,
name: build.name,
date: pipeline.created_at.to_date
)
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
......@@ -605,6 +605,13 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :name: pipeline_background:daily_code_coverage
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :default
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :name: pipeline_cache:expire_job_cache
:feature_category: :continuous_integration
:has_external_dependencies:
......
# frozen_string_literal: true
class DailyCodeCoverageWorker
include ApplicationWorker
include PipelineBackgroundQueue
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
Ci::DailyCodeCoverageService.new.execute(pipeline)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
......@@ -7,7 +7,11 @@ class PipelineSuccessWorker # rubocop:disable Scalability/IdempotentWorker
queue_namespace :pipeline_processing
urgency :high
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
# no-op
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
DailyCodeCoverageWorker.perform_async(pipeline.id)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
---
title: Store daily code coverages into daily_code_coverages table
merge_request: 24695
author:
type: added
# frozen_string_literal: true
class CreateDailyCodeCoverages < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :daily_code_coverages do |t|
t.date :date, null: false
t.integer :project_id, null: false
t.integer :last_pipeline_id, null: false
t.float :coverage, null: false
t.string :ref, null: false # rubocop:disable Migration/AddLimitToStringColumns
t.string :name, null: false # rubocop:disable Migration/AddLimitToStringColumns
t.index [:project_id, :ref, :name, :date], name: 'index_daily_code_coverage_unique_columns', unique: true, order: { date: :desc }
t.foreign_key :projects, on_delete: :cascade
t.foreign_key :ci_pipelines, column: :last_pipeline_id, on_delete: :cascade
end
end
end
......@@ -1327,6 +1327,16 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
t.float "percentage_service_desk_issues", default: 0.0, null: false
end
create_table "daily_code_coverages", force: :cascade do |t|
t.date "date", null: false
t.integer "project_id", null: false
t.integer "last_pipeline_id", null: false
t.float "coverage", null: false
t.string "ref", null: false
t.string "name", null: false
t.index ["project_id", "ref", "name", "date"], name: "index_daily_code_coverage_unique_columns", unique: true, order: { date: :desc }
end
create_table "dependency_proxy_blobs", id: :serial, force: :cascade do |t|
t.integer "group_id", null: false
t.datetime_with_timezone "created_at", null: false
......@@ -4832,6 +4842,8 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
add_foreign_key "commit_user_mentions", "notes", on_delete: :cascade
add_foreign_key "container_expiration_policies", "projects", on_delete: :cascade
add_foreign_key "container_repositories", "projects"
add_foreign_key "daily_code_coverages", "ci_pipelines", column: "last_pipeline_id", on_delete: :cascade
add_foreign_key "daily_code_coverages", "projects", on_delete: :cascade
add_foreign_key "dependency_proxy_blobs", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "dependency_proxy_group_settings", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
......
# frozen_string_literal: true
FactoryBot.define do
factory :daily_code_coverage do
ref { 'test_branch' }
name { 'test_coverage_job' }
coverage { 77 }
date { Time.zone.now.to_date }
after(:build) do |dcc|
pipeline = create(:ci_pipeline)
dcc.project_id = pipeline.project_id unless dcc.project_id
dcc.last_pipeline_id = pipeline.id unless dcc.last_pipeline_id
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DailyCodeCoverage do
describe 'validation' do
subject { described_class.new }
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:last_pipeline_id) }
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:coverage) }
it { is_expected.to validate_presence_of(:date) }
context 'uniqueness' do
before do
create(:daily_code_coverage)
end
it { is_expected.to validate_uniqueness_of(:project_id).scoped_to([:ref, :name, :date]) }
end
context 'ensuring newer pipeline' do
context 'on new records' do
subject { build(:daily_code_coverage, last_pipeline_id: 1) }
it { is_expected.to be_valid }
end
context 'on existing records' do
subject { create(:daily_code_coverage, last_pipeline_id: 12) }
context 'and new pipeline ID is older' do
before do
subject.last_pipeline_id = 10
end
it { is_expected.not_to be_valid }
end
context 'and new pipeline ID is newer' do
before do
subject.last_pipeline_id = 15
end
it { is_expected.to be_valid }
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::DailyCodeCoverageService, '#execute' do
let!(:pipeline) { create(:ci_pipeline, created_at: '2020-02-06 00:01:10') }
let!(:rspec_job) { create(:ci_build, pipeline: pipeline, name: 'rspec', coverage: 80) }
let!(:karma_job) { create(:ci_build, pipeline: pipeline, name: 'karma', coverage: 90) }
let!(:extra_job) { create(:ci_build, pipeline: pipeline, name: 'extra', coverage: nil) }
it 'creates daily code coverage record for each job in the pipeline that has coverage value' do
described_class.new.execute(pipeline)
DailyCodeCoverage.find_by(name: 'rspec').tap do |coverage|
expect(coverage).to have_attributes(
project_id: pipeline.project.id,
last_pipeline_id: pipeline.id,
ref: pipeline.ref,
name: rspec_job.name,
coverage: rspec_job.coverage,
date: pipeline.created_at.to_date
)
end
DailyCodeCoverage.find_by(name: 'karma').tap do |coverage|
expect(coverage).to have_attributes(
project_id: pipeline.project.id,
last_pipeline_id: pipeline.id,
ref: pipeline.ref,
name: karma_job.name,
coverage: karma_job.coverage,
date: pipeline.created_at.to_date
)
end
expect(DailyCodeCoverage.find_by(name: 'extra')).to be_nil
end
context 'when there is an existing daily code coverage for the matching date, project, ref, and name' do
let!(:new_pipeline) do
create(
:ci_pipeline,
project: pipeline.project,
ref: pipeline.ref,
created_at: '2020-02-06 00:02:20'
)
end
let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: 'rspec', coverage: 84) }
let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: 'karma', coverage: 92) }
before do
# Create the existing daily code coverage records
described_class.new.execute(pipeline)
end
it "updates the existing record's coverage value and last_pipeline_id" do
rspec_coverage = DailyCodeCoverage.find_by(name: 'rspec')
karma_coverage = DailyCodeCoverage.find_by(name: 'karma')
# Bump up the coverage values
described_class.new.execute(new_pipeline)
rspec_coverage.reload
karma_coverage.reload
expect(rspec_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id,
coverage: new_rspec_job.coverage
)
expect(karma_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id,
coverage: new_karma_job.coverage
)
end
end
context 'when the ID of the given pipeline is older than the last_pipeline_id' do
let!(:new_pipeline) do
create(
:ci_pipeline,
project: pipeline.project,
ref: pipeline.ref,
created_at: '2020-02-06 00:02:20'
)
end
let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: 'rspec', coverage: 84) }
let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: 'karma', coverage: 92) }
before do
# Create the existing daily code coverage records
# but in this case, for the newer pipeline first.
described_class.new.execute(new_pipeline)
end
it 'does not update the existing daily code coverage records' do
rspec_coverage = DailyCodeCoverage.find_by(name: 'rspec')
karma_coverage = DailyCodeCoverage.find_by(name: 'karma')
# Run another one but for the older pipeline.
# This simulates the scenario wherein the success worker
# of an older pipeline, for some network hiccup, was delayed
# and only got executed right after the newer pipeline's success worker.
# In this case, we don't want to bump the coverage value with an older one.
described_class.new.execute(pipeline)
rspec_coverage.reload
karma_coverage.reload
expect(rspec_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id,
coverage: new_rspec_job.coverage
)
expect(karma_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id,
coverage: new_karma_job.coverage
)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe DailyCodeCoverageWorker do
describe '#perform' do
let!(:pipeline) { create(:ci_pipeline) }
subject { described_class.new.perform(pipeline_id) }
context 'when pipeline is found' do
let(:pipeline_id) { pipeline.id }
it 'executes service' do
expect_any_instance_of(Ci::DailyCodeCoverageService)
.to receive(:execute).with(pipeline)
subject
end
end
context 'when pipeline is not found' do
let(:pipeline_id) { 123 }
it 'does not execute service' do
expect_any_instance_of(Ci::DailyCodeCoverageService)
.not_to receive(:execute)
expect { subject }
.not_to raise_error
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe PipelineSuccessWorker do
describe '#perform' do
context 'when pipeline exists' do
let(:pipeline) { create(:ci_pipeline) }
it 'asynchronously executes the daily code coverage worker' do
expect(DailyCodeCoverageWorker)
.to receive(:perform_async).with(pipeline.id)
described_class.new.perform(pipeline.id)
end
end
context 'when pipeline does not exist' do
it 'does not raise exception' do
expect { described_class.new.perform(123) }
.not_to raise_error
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