Commit 912840bf authored by Erick Bajao's avatar Erick Bajao

Track unique number of test cases parsed

Adds a new event `i_testing_test_case_parsed` which is tracked using
HLL Redis Counter to be able to count the unique number of
test cases parsed weekly.

Also, adds new add_pipelined method to HLL.
parent f3715e33
...@@ -2,12 +2,20 @@ ...@@ -2,12 +2,20 @@
module Ci module Ci
class BuildReportResultService class BuildReportResultService
include Gitlab::Utils::UsageData
EVENT_NAME = 'i_testing_test_case_parsed'
def execute(build) def execute(build)
return unless build.has_test_reports? return unless build.has_test_reports?
test_suite = generate_test_suite_report(build)
track_test_cases(build, test_suite)
build.report_results.create!( build.report_results.create!(
project_id: build.project_id, project_id: build.project_id,
data: tests_params(build) data: tests_params(test_suite)
) )
end end
...@@ -17,9 +25,7 @@ module Ci ...@@ -17,9 +25,7 @@ module Ci
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
end end
def tests_params(build) def tests_params(test_suite)
test_suite = generate_test_suite_report(build)
{ {
tests: { tests: {
name: test_suite.name, name: test_suite.name,
...@@ -31,5 +37,20 @@ module Ci ...@@ -31,5 +37,20 @@ module Ci
} }
} }
end end
def track_test_cases(build, test_suite)
return if Feature.disabled?(:track_unique_test_cases_parsed, build.project)
track_usage_event(EVENT_NAME, test_case_hashes(build, test_suite))
end
def test_case_hashes(build, test_suite)
[].tap do |hashes|
test_suite.each_test_case do |test_case|
key = "#{build.project_id}-#{test_suite.name}-#{test_case.key}"
hashes << Digest::SHA256.hexdigest(key)
end
end
end
end end
end end
---
title: Track unique number of test cases parsed
merge_request: 41918
author:
type: added
---
name: track_unique_test_cases_parsed
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41918
rollout_issue_url:
group: group::testing
type: development
default_enabled: false
---
name: usage_data_i_testing_test_case_parsed
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41918
rollout_issue_url:
group: group::testing
type: development
default_enabled: true
...@@ -23,7 +23,7 @@ module Gitlab ...@@ -23,7 +23,7 @@ module Gitlab
@attachment = params.fetch(:attachment, nil) @attachment = params.fetch(:attachment, nil)
@job = params.fetch(:job, nil) @job = params.fetch(:job, nil)
@key = sanitize_key_name("#{classname}_#{name}") @key = hash_key("#{classname}_#{name}")
end end
def has_attachment? def has_attachment?
...@@ -42,8 +42,8 @@ module Gitlab ...@@ -42,8 +42,8 @@ module Gitlab
private private
def sanitize_key_name(key) def hash_key(key)
key.gsub(/[^0-9A-Za-z]/, '-') Digest::SHA256.hexdigest(key)
end end
end end
end end
......
...@@ -12,18 +12,24 @@ module Gitlab ...@@ -12,18 +12,24 @@ module Gitlab
def initialize(name = nil) def initialize(name = nil)
@name = name @name = name
@test_cases = {} @test_cases = {}
@all_test_cases = []
@total_time = 0.0 @total_time = 0.0
@duplicate_cases = []
end end
def add_test_case(test_case) 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_cases[test_case.status][test_case.key] = test_case @test_cases[test_case.status][test_case.key] = test_case
@total_time += test_case.execution_time @total_time += test_case.execution_time
end end
def each_test_case
@test_cases.each do |status, test_cases|
test_cases.values.each do |test_case|
yield test_case
end
end
end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def total_count def total_count
return 0 if suite_error return 0 if suite_error
...@@ -86,10 +92,6 @@ module Gitlab ...@@ -86,10 +92,6 @@ module Gitlab
private private
def existing_key?(test_case)
@test_cases[test_case.status]&.key?(test_case.key)
end
def sort_by_status def sort_by_status
@test_cases = @test_cases.sort_by { |status, _| Gitlab::Ci::Reports::TestCase::STATUS_TYPES.index(status) }.to_h @test_cases = @test_cases.sort_by { |status, _| Gitlab::Ci::Reports::TestCase::STATUS_TYPES.index(status) }.to_h
end end
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module Gitlab module Gitlab
module Redis module Redis
class HLL class HLL
BATCH_SIZE = 300
KEY_REGEX = %r{\A(\w|-|:)*\{\w*\}(\w|-|:)*\z}.freeze KEY_REGEX = %r{\A(\w|-|:)*\{\w*\}(\w|-|:)*\z}.freeze
KeyFormatError = Class.new(StandardError) KeyFormatError = Class.new(StandardError)
...@@ -29,17 +30,24 @@ module Gitlab ...@@ -29,17 +30,24 @@ module Gitlab
# 2020-216-{project_action} # 2020-216-{project_action}
# i_{analytics}_dev_ops_score-2020-32 # i_{analytics}_dev_ops_score-2020-32
def add(key:, value:, expiry:) def add(key:, value:, expiry:)
unless KEY_REGEX.match?(key) validate_key!(key)
raise KeyFormatError.new("Invalid key format. #{key} key should have changeable parts in curly braces. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands")
end
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
redis.multi do |multi| redis.multi do |multi|
multi.pfadd(key, value) Array.wrap(value).each_slice(BATCH_SIZE) { |batch| multi.pfadd(key, batch) }
multi.expire(key, expiry) multi.expire(key, expiry)
end end
end end
end end
private
def validate_key!(key)
return if KEY_REGEX.match?(key)
raise KeyFormatError.new("Invalid key format. #{key} key should have changeable parts in curly braces. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands")
end
end end
end end
end end
...@@ -185,6 +185,11 @@ ...@@ -185,6 +185,11 @@
redis_slot: incident_management redis_slot: incident_management
category: incident_management category: incident_management
aggregation: weekly aggregation: weekly
# Testing category
- name: i_testing_test_case_parsed
category: testing
redis_slot: testing
aggregation: weekly
# Project Management group # Project Management group
- name: g_project_management_issue_title_changed - name: g_project_management_issue_title_changed
category: issues_edit category: issues_edit
......
...@@ -229,6 +229,20 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do ...@@ -229,6 +229,20 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do
end end
end end
describe '#each_test_case' do
before do
test_suite.add_test_case(test_case_success)
test_suite.add_test_case(test_case_failed)
test_suite.add_test_case(test_case_skipped)
test_suite.add_test_case(test_case_error)
end
it 'yields each test case to given block' do
expect { |b| test_suite.each_test_case(&b) }
.to yield_successive_args(test_case_success, test_case_failed, test_case_skipped, test_case_error)
end
end
Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type| Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type|
describe "##{status_type}_count" do describe "##{status_type}_count" do
subject { test_suite.public_send("#{status_type}_count") } subject { test_suite.public_send("#{status_type}_count") }
......
...@@ -39,6 +39,24 @@ RSpec.describe Gitlab::Redis::HLL, :clean_gitlab_redis_shared_state do ...@@ -39,6 +39,24 @@ RSpec.describe Gitlab::Redis::HLL, :clean_gitlab_redis_shared_state do
end end
end end
end end
context 'when adding entries' do
let(:metric) { 'test-{metric}' }
it 'supports single value' do
track_event(metric, 1)
expect(count_unique_events([metric])).to eq(1)
end
it 'supports multiple values' do
stub_const("#{described_class.name}::HLL_BATCH_SIZE", 2)
track_event(metric, [1, 2, 3, 4, 5])
expect(count_unique_events([metric])).to eq(5)
end
end
end end
describe '.count' do describe '.count' do
...@@ -94,13 +112,13 @@ RSpec.describe Gitlab::Redis::HLL, :clean_gitlab_redis_shared_state do ...@@ -94,13 +112,13 @@ RSpec.describe Gitlab::Redis::HLL, :clean_gitlab_redis_shared_state do
expect(unique_counts).to eq(4) expect(unique_counts).to eq(4)
end end
end
def track_event(key, value, expiry = 1.day) def track_event(key, value, expiry = 1.day)
described_class.add(key: key, value: value, expiry: expiry) described_class.add(key: key, value: value, expiry: expiry)
end end
def count_unique_events(keys) def count_unique_events(keys)
described_class.count(keys: keys) described_class.count(keys: keys)
end
end end
end end
...@@ -20,7 +20,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s ...@@ -20,7 +20,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
describe '.categories' do describe '.categories' do
it 'gets all unique category names' do it 'gets all unique category names' do
expect(described_class.categories).to contain_exactly('analytics', 'compliance', 'ide_edit', 'search', 'source_code', 'incident_management', 'issues_edit') expect(described_class.categories).to contain_exactly('analytics', 'compliance', 'ide_edit', 'search', 'source_code', 'incident_management', 'issues_edit', 'testing')
end end
end end
......
...@@ -1178,9 +1178,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do ...@@ -1178,9 +1178,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.redis_hll_counters } subject { described_class.redis_hll_counters }
let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories } let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories }
let(:ineligible_total_categories) { ['source_code'] } let(:ineligible_total_categories) { %w[source_code testing] }
it 'has all know_events' do it 'has all known_events' do
expect(subject).to have_key(:redis_hll_counters) expect(subject).to have_key(:redis_hll_counters)
expect(subject[:redis_hll_counters].keys).to match_array(categories) expect(subject[:redis_hll_counters].keys).to match_array(categories)
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::BuildReportResultService do RSpec.describe Ci::BuildReportResultService do
describe "#execute" do describe '#execute', :clean_gitlab_redis_shared_state do
subject(:build_report_result) { described_class.new.execute(build) } subject(:build_report_result) { described_class.new.execute(build) }
context 'when build is finished' do context 'when build is finished' do
...@@ -17,6 +17,25 @@ RSpec.describe Ci::BuildReportResultService do ...@@ -17,6 +17,25 @@ RSpec.describe Ci::BuildReportResultService do
expect(build_report_result.tests_skipped).to eq(0) expect(build_report_result.tests_skipped).to eq(0)
expect(build_report_result.tests_duration).to eq(0.010284) expect(build_report_result.tests_duration).to eq(0.010284)
expect(Ci::BuildReportResult.count).to eq(1) expect(Ci::BuildReportResult.count).to eq(1)
unique_test_cases_parsed = Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
event_names: described_class::EVENT_NAME,
start_date: 2.weeks.ago,
end_date: 2.weeks.from_now
)
expect(unique_test_cases_parsed).to eq(4)
end
context 'when feature flag for tracking is disabled' do
before do
stub_feature_flags(track_unique_test_cases_parsed: false)
end
it 'creates the report but does not track the event' do
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
expect(build_report_result.tests_name).to eq("test")
expect(Ci::BuildReportResult.count).to eq(1)
end
end end
context 'when data has already been persisted' do context 'when data has already been persisted' 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