Commit 4458217a authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch...

Merge branch '228853-reviewer-roulette-default-to-previous-roulette-if-no-timezone-reviewer-maintainer-found' into 'master'

Fallback to random-based roulette when no one is picked otherwise

Closes #228853

See merge request gitlab-org/gitlab!36948
parents ae287984 11af9e44
...@@ -42,23 +42,29 @@ OPTIONAL_REVIEW_TEMPLATE = "%{role} review is optional for %{category}".freeze ...@@ -42,23 +42,29 @@ OPTIONAL_REVIEW_TEMPLATE = "%{role} review is optional for %{category}".freeze
NOT_AVAILABLE_TEMPLATE = 'No %{role} available'.freeze NOT_AVAILABLE_TEMPLATE = 'No %{role} available'.freeze
TIMEZONE_EXPERIMENT = true TIMEZONE_EXPERIMENT = true
def mr_author def note_for_spins_role(spins, role)
roulette.team.find { |person| person.username == gitlab.mr_author } spins.each do |spin|
note = note_for_spin_role(spin, role)
return note if note
end
NOT_AVAILABLE_TEMPLATE % { role: role }
end end
def note_for_category_role(spin, role) def note_for_spin_role(spin, role)
if spin.optional_role == role if spin.optional_role == role
return OPTIONAL_REVIEW_TEMPLATE % { role: role.capitalize, category: helper.label_for_category(spin.category) } return OPTIONAL_REVIEW_TEMPLATE % { role: role.capitalize, category: helper.label_for_category(spin.category) }
end end
spin.public_send(role)&.markdown_name(timezone_experiment: TIMEZONE_EXPERIMENT, author: mr_author) || NOT_AVAILABLE_TEMPLATE % { role: role } # rubocop:disable GitlabSecurity/PublicSend spin.public_send(role)&.markdown_name(timezone_experiment: spin.timezone_experiment, author: roulette.team_mr_author) # rubocop:disable GitlabSecurity/PublicSend
end end
def markdown_row_for_spin(spin) def markdown_row_for_spins(category, spins_array)
reviewer_note = note_for_category_role(spin, :reviewer) reviewer_note = note_for_spins_role(spins_array, :reviewer)
maintainer_note = note_for_category_role(spin, :maintainer) maintainer_note = note_for_spins_role(spins_array, :maintainer)
"| #{helper.label_for_category(spin.category)} | #{reviewer_note} | #{maintainer_note} |" "| #{helper.label_for_category(category)} | #{reviewer_note} | #{maintainer_note} |"
end end
changes = helper.changes_by_category changes = helper.changes_by_category
...@@ -70,26 +76,20 @@ changes.delete(:docs) ...@@ -70,26 +76,20 @@ changes.delete(:docs)
categories = changes.keys - [:unknown] categories = changes.keys - [:unknown]
# Ensure to spin for database reviewer/maintainer when ~database is applied (e.g. to review SQL queries) # Ensure to spin for database reviewer/maintainer when ~database is applied (e.g. to review SQL queries)
categories << :database if gitlab.mr_labels.include?('database') && !categories.include?(:database) categories << :database if helper.gitlab_helper&.mr_labels&.include?('database') && !categories.include?(:database)
if changes.any? if changes.any?
project = helper.project_name project = helper.project_name
branch_name = gitlab.mr_json['source_branch']
markdown(MESSAGE)
roulette_spins = roulette.spin(project, categories, branch_name, timezone_experiment: TIMEZONE_EXPERIMENT)
rows = roulette_spins.map do |spin|
# MR includes QA changes, but also other changes, and author isn't an SET
if spin.category == :qa && categories.size > 1 && mr_author && !mr_author.reviewer?(project, spin.category, [])
spin.optional_role = :maintainer
end
spin.optional_role = :maintainer if spin.category == :test timezone_roulette_spins = roulette.spin(project, categories, timezone_experiment: true)
random_roulette_spins = roulette.spin(project, categories, timezone_experiment: false)
markdown_row_for_spin(spin) rows = timezone_roulette_spins.map do |spin|
fallback_spin = random_roulette_spins.find { |random_roulette_spins| random_roulette_spins.category == spin.category }
markdown_row_for_spins(spin.category, [spin, fallback_spin])
end end
markdown(MESSAGE)
markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty? markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty?
unknown = changes.fetch(:unknown, []) unknown = changes.fetch(:unknown, [])
......
...@@ -53,7 +53,7 @@ module Gitlab ...@@ -53,7 +53,7 @@ module Gitlab
def ee? def ee?
# Support former project name for `dev` and support local Danger run # Support former project name for `dev` and support local Danger run
%w[gitlab gitlab-ee].include?(ENV['CI_PROJECT_NAME']) || Dir.exist?('../../ee') %w[gitlab gitlab-ee].include?(ENV['CI_PROJECT_NAME']) || Dir.exist?(File.expand_path('../../../ee', __dir__))
end end
def gitlab_helper def gitlab_helper
......
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'teammate' require_relative 'teammate'
require_relative 'request_helper'
module Gitlab module Gitlab
module Danger module Danger
...@@ -12,45 +13,49 @@ module Gitlab ...@@ -12,45 +13,49 @@ module Gitlab
database: false database: false
}.freeze }.freeze
Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role) Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role, :timezone_experiment)
def team_mr_author
team.find { |person| person.username == mr_author_username }
end
# Assigns GitLab team members to be reviewer and maintainer # Assigns GitLab team members to be reviewer and maintainer
# for each change category that a Merge Request contains. # for each change category that a Merge Request contains.
# #
# @return [Array<Spin>] # @return [Array<Spin>]
def spin(project, categories, branch_name, timezone_experiment: false) def spin(project, categories, timezone_experiment: false)
team = spins = categories.map do |category|
begin
project_team(project)
rescue => err
warn("Reviewer roulette failed to load team data: #{err.message}")
[]
end
canonical_branch_name = canonical_branch_name(branch_name)
spin_per_category = categories.each_with_object({}) do |category, memo|
including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(category, timezone_experiment) including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(category, timezone_experiment)
memo[category] = spin_for_category(team, project, category, canonical_branch_name, timezone_experiment: including_timezone) spin_for_category(project, category, timezone_experiment: including_timezone)
end end
spin_per_category.map do |category, spin| backend_spin = spins.find { |spin| spin.category == :backend }
case category
spins.each do |spin|
including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(spin.category, timezone_experiment)
case spin.category
when :qa
# MR includes QA changes, but also other changes, and author isn't an SET
if categories.size > 1 && !team_mr_author&.reviewer?(project, spin.category, [])
spin.optional_role = :maintainer
end
when :test when :test
spin.optional_role = :maintainer
if spin.reviewer.nil? if spin.reviewer.nil?
# Fetch an already picked backend reviewer, or pick one otherwise # Fetch an already picked backend reviewer, or pick one otherwise
spin.reviewer = spin_per_category[:backend]&.reviewer || spin_for_category(team, project, :backend, canonical_branch_name).reviewer spin.reviewer = backend_spin&.reviewer || spin_for_category(project, :backend, timezone_experiment: including_timezone).reviewer
end end
when :engineering_productivity when :engineering_productivity
if spin.maintainer.nil? if spin.maintainer.nil?
# Fetch an already picked backend maintainer, or pick one otherwise # Fetch an already picked backend maintainer, or pick one otherwise
spin.maintainer = spin_per_category[:backend]&.maintainer || spin_for_category(team, project, :backend, canonical_branch_name).maintainer spin.maintainer = backend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
end end
end end
spin
end end
spins
end end
# Looks up the current list of GitLab team members and parses it into a # Looks up the current list of GitLab team members and parses it into a
...@@ -73,14 +78,9 @@ module Gitlab ...@@ -73,14 +78,9 @@ module Gitlab
# @return [Array<Teammate>] # @return [Array<Teammate>]
def project_team(project_name) def project_team(project_name)
team.select { |member| member.in_project?(project_name) } team.select { |member| member.in_project?(project_name) }
end rescue => err
warn("Reviewer roulette failed to load team data: #{err.message}")
def canonical_branch_name(branch_name) []
branch_name.gsub(/^[ce]e-|-[ce]e$/, '')
end
def new_random(seed)
Random.new(Digest::MD5.hexdigest(seed).to_i(16))
end end
# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the # Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
...@@ -113,16 +113,35 @@ module Gitlab ...@@ -113,16 +113,35 @@ module Gitlab
# @param [Teammate] person # @param [Teammate] person
# @return [Boolean] # @return [Boolean]
def mr_author?(person) def mr_author?(person)
person.username == gitlab.mr_author person.username == mr_author_username
end
def mr_author_username
helper.gitlab_helper&.mr_author || `whoami`
end
def mr_source_branch
return `git rev-parse --abbrev-ref HEAD` unless helper.gitlab_helper&.mr_json
helper.gitlab_helper.mr_json['source_branch']
end
def mr_labels
helper.gitlab_helper&.mr_labels || []
end
def new_random(seed)
Random.new(Digest::MD5.hexdigest(seed).to_i(16))
end end
def spin_role_for_category(team, role, project, category) def spin_role_for_category(team, role, project, category)
team.select do |member| team.select do |member|
member.public_send("#{role}?", project, category, gitlab.mr_labels) # rubocop:disable GitlabSecurity/PublicSend member.public_send("#{role}?", project, category, mr_labels) # rubocop:disable GitlabSecurity/PublicSend
end end
end end
def spin_for_category(team, project, category, branch_name, timezone_experiment: false) def spin_for_category(project, category, timezone_experiment: false)
team = project_team(project)
reviewers, traintainers, maintainers = reviewers, traintainers, maintainers =
%i[reviewer traintainer maintainer].map do |role| %i[reviewer traintainer maintainer].map do |role|
spin_role_for_category(team, role, project, category) spin_role_for_category(team, role, project, category)
...@@ -132,11 +151,11 @@ module Gitlab ...@@ -132,11 +151,11 @@ module Gitlab
# https://gitlab.com/gitlab-org/gitlab/issues/26723 # https://gitlab.com/gitlab-org/gitlab/issues/26723
# Make traintainers have triple the chance to be picked as a reviewer # Make traintainers have triple the chance to be picked as a reviewer
random = new_random(branch_name) random = new_random(mr_source_branch)
reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random, timezone_experiment: timezone_experiment) reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random, timezone_experiment: timezone_experiment)
maintainer = spin_for_person(maintainers, random: random, timezone_experiment: timezone_experiment) maintainer = spin_for_person(maintainers, random: random, timezone_experiment: timezone_experiment)
Spin.new(category, reviewer, maintainer) Spin.new(category, reviewer, maintainer, false, timezone_experiment)
end end
end end
end end
......
...@@ -3,10 +3,11 @@ ...@@ -3,10 +3,11 @@
module Gitlab module Gitlab
module Danger module Danger
class Teammate class Teammate
attr_reader :username, :name, :role, :projects, :available, :tz_offset_hours attr_reader :options, :username, :name, :role, :projects, :available, :tz_offset_hours
# The options data are produced by https://gitlab.com/gitlab-org/gitlab-roulette/-/blob/master/lib/team_member.rb # The options data are produced by https://gitlab.com/gitlab-org/gitlab-roulette/-/blob/master/lib/team_member.rb
def initialize(options = {}) def initialize(options = {})
@options = options
@username = options['username'] @username = options['username']
@name = options['name'] @name = options['name']
@markdown_name = options['markdown_name'] @markdown_name = options['markdown_name']
...@@ -16,6 +17,16 @@ module Gitlab ...@@ -16,6 +17,16 @@ module Gitlab
@tz_offset_hours = options['tz_offset_hours'] @tz_offset_hours = options['tz_offset_hours']
end end
def to_h
options
end
def ==(other)
return false unless other.respond_to?(:username)
other.username == username
end
def in_project?(name) def in_project?(name)
projects&.has_key?(name) projects&.has_key?(name)
end end
......
...@@ -98,21 +98,21 @@ RSpec.describe Gitlab::Danger::Helper do ...@@ -98,21 +98,21 @@ RSpec.describe Gitlab::Danger::Helper do
it 'delegates to CHANGELOG-EE.md existence if CI_PROJECT_NAME is set to something else' do it 'delegates to CHANGELOG-EE.md existence if CI_PROJECT_NAME is set to something else' do
stub_env('CI_PROJECT_NAME', 'something else') stub_env('CI_PROJECT_NAME', 'something else')
expect(Dir).to receive(:exist?).with('../../ee') { true } expect(Dir).to receive(:exist?).with(File.expand_path('../../../../ee', __dir__)) { true }
is_expected.to be_truthy is_expected.to be_truthy
end end
it 'returns true if ee exists' do it 'returns true if ee exists' do
stub_env('CI_PROJECT_NAME', nil) stub_env('CI_PROJECT_NAME', nil)
expect(Dir).to receive(:exist?).with('../../ee') { true } expect(Dir).to receive(:exist?).with(File.expand_path('../../../../ee', __dir__)) { true }
is_expected.to be_truthy is_expected.to be_truthy
end end
it "returns false if ee doesn't exist" do it "returns false if ee doesn't exist" do
stub_env('CI_PROJECT_NAME', nil) stub_env('CI_PROJECT_NAME', nil)
expect(Dir).to receive(:exist?).with('../../ee') { false } expect(Dir).to receive(:exist?).with(File.expand_path('../../../../ee', __dir__)) { false }
is_expected.to be_falsy is_expected.to be_falsy
end end
......
# frozen_string_literal: true # frozen_string_literal: true
require 'fast_spec_helper'
require 'webmock/rspec' require 'webmock/rspec'
require 'timecop' require 'timecop'
...@@ -11,102 +10,95 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -11,102 +10,95 @@ RSpec.describe Gitlab::Danger::Roulette do
Timecop.freeze(Time.utc(2020, 06, 22, 10)) { example.run } Timecop.freeze(Time.utc(2020, 06, 22, 10)) { example.run }
end end
let(:backend_available) { true }
let(:backend_tz_offset_hours) { 2.0 }
let(:backend_maintainer) do let(:backend_maintainer) do
{ Gitlab::Danger::Teammate.new(
username: 'backend-maintainer', 'username' => 'backend-maintainer',
name: 'Backend maintainer', 'name' => 'Backend maintainer',
role: 'Backend engineer', 'role' => 'Backend engineer',
projects: { 'gitlab' => 'maintainer backend' }, 'projects' => { 'gitlab' => 'maintainer backend' },
available: true, 'available' => backend_available,
tz_offset_hours: 2.0 'tz_offset_hours' => backend_tz_offset_hours
} )
end end
let(:frontend_reviewer) do let(:frontend_reviewer) do
{ Gitlab::Danger::Teammate.new(
username: 'frontend-reviewer', 'username' => 'frontend-reviewer',
name: 'Frontend reviewer', 'name' => 'Frontend reviewer',
role: 'Frontend engineer', 'role' => 'Frontend engineer',
projects: { 'gitlab' => 'reviewer frontend' }, 'projects' => { 'gitlab' => 'reviewer frontend' },
available: true, 'available' => true,
tz_offset_hours: 2.0 'tz_offset_hours' => 2.0
} )
end end
let(:frontend_maintainer) do let(:frontend_maintainer) do
{ Gitlab::Danger::Teammate.new(
username: 'frontend-maintainer', 'username' => 'frontend-maintainer',
name: 'Frontend maintainer', 'name' => 'Frontend maintainer',
role: 'Frontend engineer', 'role' => 'Frontend engineer',
projects: { 'gitlab' => "maintainer frontend" }, 'projects' => { 'gitlab' => "maintainer frontend" },
available: true, 'available' => true,
tz_offset_hours: 2.0 'tz_offset_hours' => 2.0
} )
end end
let(:software_engineer_in_test) do let(:software_engineer_in_test) do
{ Gitlab::Danger::Teammate.new(
username: 'software-engineer-in-test', 'username' => 'software-engineer-in-test',
name: 'Software Engineer in Test', 'name' => 'Software Engineer in Test',
role: 'Software Engineer in Test, Create:Source Code', 'role' => 'Software Engineer in Test, Create:Source Code',
projects: { 'projects' => { 'gitlab' => 'reviewer qa', 'gitlab-qa' => 'maintainer' },
'gitlab' => 'reviewer qa', 'available' => true,
'gitlab-qa' => 'maintainer' 'tz_offset_hours' => 2.0
}, )
available: true,
tz_offset_hours: 2.0
}
end end
let(:engineering_productivity_reviewer) do let(:engineering_productivity_reviewer) do
{ Gitlab::Danger::Teammate.new(
username: 'eng-prod-reviewer', 'username' => 'eng-prod-reviewer',
name: 'EP engineer', 'name' => 'EP engineer',
role: 'Engineering Productivity', 'role' => 'Engineering Productivity',
projects: { 'gitlab' => 'reviewer backend' }, 'projects' => { 'gitlab' => 'reviewer backend' },
available: true, 'available' => true,
tz_offset_hours: 2.0 'tz_offset_hours' => 2.0
} )
end end
let(:teammate_json) do let(:teammate_json) do
[ [
backend_maintainer, backend_maintainer.to_h,
frontend_maintainer, frontend_maintainer.to_h,
frontend_reviewer, frontend_reviewer.to_h,
software_engineer_in_test, software_engineer_in_test.to_h,
engineering_productivity_reviewer engineering_productivity_reviewer.to_h
].to_json ].to_json
end end
subject(:roulette) { Object.new.extend(described_class) } subject(:roulette) { Object.new.extend(described_class) }
def matching_teammate(person) describe 'Spin#==' do
satisfy do |teammate| it 'compares Spin attributes' do
teammate.username == person[:username] && spin1 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, false, false)
teammate.name == person[:name] && spin2 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, false, false)
teammate.role == person[:role] && spin3 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, false, true)
teammate.projects == person[:projects] spin4 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, true, false)
end spin5 = described_class::Spin.new(:backend, frontend_reviewer, backend_maintainer, false, false)
end spin6 = described_class::Spin.new(:backend, backend_maintainer, frontend_maintainer, false, false)
spin7 = described_class::Spin.new(:frontend, frontend_reviewer, frontend_maintainer, false, false)
def matching_spin(category, reviewer: { username: nil }, maintainer: { username: nil }, optional: nil)
satisfy do |spin|
bool = spin.category == category
bool &&= spin.reviewer&.username == reviewer[:username]
bool &&=
if maintainer
spin.maintainer&.username == maintainer[:username]
else
spin.maintainer.nil?
end
bool && spin.optional_role == optional expect(spin1).to eq(spin2)
expect(spin1).not_to eq(spin3)
expect(spin1).not_to eq(spin4)
expect(spin1).not_to eq(spin5)
expect(spin1).not_to eq(spin6)
expect(spin1).not_to eq(spin7)
end end
end end
describe '#spin' do describe '#spin' do
let!(:project) { 'gitlab' } let!(:project) { 'gitlab' }
let!(:branch_name) { 'a-branch' } let!(:mr_source_branch) { 'a-branch' }
let!(:mr_labels) { ['backend', 'devops::create'] } let!(:mr_labels) { ['backend', 'devops::create'] }
let!(:author) { Gitlab::Danger::Teammate.new('username' => 'filipa') } let!(:author) { Gitlab::Danger::Teammate.new('username' => 'johndoe') }
let(:timezone_experiment) { false } let(:timezone_experiment) { false }
let(:spins) do let(:spins) do
# Stub the request at the latest time so that we can modify the raw data, e.g. available fields. # Stub the request at the latest time so that we can modify the raw data, e.g. available fields.
...@@ -114,12 +106,13 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -114,12 +106,13 @@ RSpec.describe Gitlab::Danger::Roulette do
.stub_request(:get, described_class::ROULETTE_DATA_URL) .stub_request(:get, described_class::ROULETTE_DATA_URL)
.to_return(body: teammate_json) .to_return(body: teammate_json)
subject.spin(project, categories, branch_name, timezone_experiment: timezone_experiment) subject.spin(project, categories, timezone_experiment: timezone_experiment)
end end
before do before do
allow(subject).to receive_message_chain(:gitlab, :mr_author).and_return(author.username) allow(subject).to receive(:mr_author_username).and_return(author.username)
allow(subject).to receive_message_chain(:gitlab, :mr_labels).and_return(mr_labels) allow(subject).to receive(:mr_labels).and_return(mr_labels)
allow(subject).to receive(:mr_source_branch).and_return(mr_source_branch)
end end
context 'when timezone_experiment == false' do context 'when timezone_experiment == false' do
...@@ -127,16 +120,16 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -127,16 +120,16 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:categories) { [:backend] } let(:categories) { [:backend] }
it 'assigns backend reviewer and maintainer' do it 'assigns backend reviewer and maintainer' do
expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: backend_maintainer)) expect(spins[0].reviewer).to eq(engineering_productivity_reviewer)
expect(spins[0].maintainer).to eq(backend_maintainer)
expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false)])
end end
context 'when teammate is not available' do context 'when teammate is not available' do
before do let(:backend_available) { false }
backend_maintainer[:available] = false
end
it 'assigns backend reviewer and no maintainer' do it 'assigns backend reviewer and no maintainer' do
expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: nil)) expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, nil, false, false)])
end end
end end
end end
...@@ -145,7 +138,7 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -145,7 +138,7 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:categories) { [:frontend] } let(:categories) { [:frontend] }
it 'assigns frontend reviewer and maintainer' do it 'assigns frontend reviewer and maintainer' do
expect(spins).to contain_exactly(matching_spin(:frontend, reviewer: frontend_reviewer, maintainer: frontend_maintainer)) expect(spins).to eq([described_class::Spin.new(:frontend, frontend_reviewer, frontend_maintainer, false, false)])
end end
end end
...@@ -153,7 +146,7 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -153,7 +146,7 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:categories) { [:qa] } let(:categories) { [:qa] }
it 'assigns QA reviewer' do it 'assigns QA reviewer' do
expect(spins).to contain_exactly(matching_spin(:qa, reviewer: software_engineer_in_test)) expect(spins).to eq([described_class::Spin.new(:qa, software_engineer_in_test, nil, false, false)])
end end
end end
...@@ -161,7 +154,7 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -161,7 +154,7 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:categories) { [:engineering_productivity] } let(:categories) { [:engineering_productivity] }
it 'assigns Engineering Productivity reviewer and fallback to backend maintainer' do it 'assigns Engineering Productivity reviewer and fallback to backend maintainer' do
expect(spins).to contain_exactly(matching_spin(:engineering_productivity, reviewer: engineering_productivity_reviewer, maintainer: backend_maintainer)) expect(spins).to eq([described_class::Spin.new(:engineering_productivity, engineering_productivity_reviewer, backend_maintainer, false, false)])
end end
end end
...@@ -169,7 +162,7 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -169,7 +162,7 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:categories) { [:test] } let(:categories) { [:test] }
it 'assigns corresponding SET' do it 'assigns corresponding SET' do
expect(spins).to contain_exactly(matching_spin(:test, reviewer: software_engineer_in_test)) expect(spins).to eq([described_class::Spin.new(:test, software_engineer_in_test, nil, :maintainer, false)])
end end
end end
end end
...@@ -181,16 +174,14 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -181,16 +174,14 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:categories) { [:backend] } let(:categories) { [:backend] }
it 'assigns backend reviewer and maintainer' do it 'assigns backend reviewer and maintainer' do
expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: backend_maintainer)) expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, true)])
end end
context 'when teammate is not in a good timezone' do context 'when teammate is not in a good timezone' do
before do let(:backend_tz_offset_hours) { 5.0 }
backend_maintainer[:tz_offset_hours] = 5.0
end
it 'assigns backend reviewer and no maintainer' do it 'assigns backend reviewer and no maintainer' do
expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: nil)) expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, nil, false, true)])
end end
end end
end end
...@@ -203,22 +194,33 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -203,22 +194,33 @@ RSpec.describe Gitlab::Danger::Roulette do
end end
it 'assigns backend reviewer and maintainer' do it 'assigns backend reviewer and maintainer' do
expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: backend_maintainer)) expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false)])
end end
context 'when teammate is not in a good timezone' do context 'when teammate is not in a good timezone' do
before do let(:backend_tz_offset_hours) { 5.0 }
backend_maintainer[:tz_offset_hours] = 5.0
end
it 'assigns backend reviewer and maintainer' do it 'assigns backend reviewer and maintainer' do
expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: backend_maintainer)) expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false)])
end end
end end
end end
end end
end end
RSpec::Matchers.define :match_teammates do |expected|
match do |actual|
expected.each do |expected_person|
actual_person_found = actual.find { |actual_person| actual_person.name == expected_person.username }
actual_person_found &&
actual_person_found.name == expected_person.name &&
actual_person_found.role == expected_person.role &&
actual_person_found.projects == expected_person.projects
end
end
end
describe '#team' do describe '#team' do
subject(:team) { roulette.team } subject(:team) { roulette.team }
...@@ -254,15 +256,13 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -254,15 +256,13 @@ RSpec.describe Gitlab::Danger::Roulette do
end end
it 'returns an array of teammates' do it 'returns an array of teammates' do
expected_teammates = [ is_expected.to match_teammates([
matching_teammate(backend_maintainer), backend_maintainer,
matching_teammate(frontend_reviewer), frontend_reviewer,
matching_teammate(frontend_maintainer), frontend_maintainer,
matching_teammate(software_engineer_in_test), software_engineer_in_test,
matching_teammate(engineering_productivity_reviewer) engineering_productivity_reviewer
] ])
is_expected.to contain_exactly(*expected_teammates)
end end
it 'memoizes the result' do it 'memoizes the result' do
...@@ -281,7 +281,9 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -281,7 +281,9 @@ RSpec.describe Gitlab::Danger::Roulette do
end end
it 'filters team by project_name' do it 'filters team by project_name' do
is_expected.to contain_exactly(matching_teammate(software_engineer_in_test)) is_expected.to match_teammates([
software_engineer_in_test
])
end end
end end
...@@ -289,32 +291,32 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -289,32 +291,32 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:person_tz_offset_hours) { 0.0 } let(:person_tz_offset_hours) { 0.0 }
let(:person1) do let(:person1) do
Gitlab::Danger::Teammate.new( Gitlab::Danger::Teammate.new(
'username' => 'rymai', 'username' => 'user1',
'available' => true, 'available' => true,
'tz_offset_hours' => person_tz_offset_hours 'tz_offset_hours' => person_tz_offset_hours
) )
end end
let(:person2) do let(:person2) do
Gitlab::Danger::Teammate.new( Gitlab::Danger::Teammate.new(
'username' => 'godfat', 'username' => 'user2',
'available' => true, 'available' => true,
'tz_offset_hours' => person_tz_offset_hours) 'tz_offset_hours' => person_tz_offset_hours)
end end
let(:author) do let(:author) do
Gitlab::Danger::Teammate.new( Gitlab::Danger::Teammate.new(
'username' => 'filipa', 'username' => 'johndoe',
'available' => true, 'available' => true,
'tz_offset_hours' => 0.0) 'tz_offset_hours' => 0.0)
end end
let(:unavailable) do let(:unavailable) do
Gitlab::Danger::Teammate.new( Gitlab::Danger::Teammate.new(
'username' => 'jacopo-beschi', 'username' => 'janedoe',
'available' => false, 'available' => false,
'tz_offset_hours' => 0.0) 'tz_offset_hours' => 0.0)
end end
before do before do
allow(subject).to receive_message_chain(:gitlab, :mr_author).and_return(author.username) allow(subject).to receive(:mr_author_username).and_return(author.username)
end end
(-4..4).each do |utc_offset| (-4..4).each do |utc_offset|
...@@ -328,7 +330,7 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -328,7 +330,7 @@ RSpec.describe Gitlab::Danger::Roulette do
selected = subject.spin_for_person(persons, random: Random.new, timezone_experiment: timezone_experiment) selected = subject.spin_for_person(persons, random: Random.new, timezone_experiment: timezone_experiment)
expect(selected.username).to be_in(persons.map(&:username)) expect(persons.map(&:username)).to include(selected.username)
end end
end end
end end
...@@ -349,7 +351,7 @@ RSpec.describe Gitlab::Danger::Roulette do ...@@ -349,7 +351,7 @@ RSpec.describe Gitlab::Danger::Roulette do
if timezone_experiment if timezone_experiment
expect(selected).to be_nil expect(selected).to be_nil
else else
expect(selected.username).to be_in(persons.map(&:username)) expect(persons.map(&:username)).to include(selected.username)
end end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
require 'fast_spec_helper'
require 'timecop' require 'timecop'
require 'rspec-parameterized' require 'rspec-parameterized'
...@@ -10,16 +8,16 @@ require 'gitlab/danger/teammate' ...@@ -10,16 +8,16 @@ require 'gitlab/danger/teammate'
RSpec.describe Gitlab::Danger::Teammate do RSpec.describe Gitlab::Danger::Teammate do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
subject { described_class.new(options.stringify_keys) } subject { described_class.new(options) }
let(:tz_offset_hours) { 2.0 } let(:tz_offset_hours) { 2.0 }
let(:options) do let(:options) do
{ {
username: 'luigi', 'username' => 'luigi',
projects: projects, 'projects' => projects,
role: role, 'role' => role,
markdown_name: '[Luigi](https://gitlab.com/luigi) (`@luigi`)', 'markdown_name' => '[Luigi](https://gitlab.com/luigi) (`@luigi`)',
tz_offset_hours: tz_offset_hours 'tz_offset_hours' => tz_offset_hours
} }
end end
let(:capabilities) { ['reviewer backend'] } let(:capabilities) { ['reviewer backend'] }
...@@ -28,6 +26,26 @@ RSpec.describe Gitlab::Danger::Teammate do ...@@ -28,6 +26,26 @@ RSpec.describe Gitlab::Danger::Teammate do
let(:labels) { [] } let(:labels) { [] }
let(:project) { double } let(:project) { double }
describe '#==' do
it 'compares Teammate username' do
joe1 = described_class.new('username' => 'joe', 'projects' => projects)
joe2 = described_class.new('username' => 'joe', 'projects' => [])
jane1 = described_class.new('username' => 'jane', 'projects' => projects)
jane2 = described_class.new('username' => 'jane', 'projects' => [])
expect(joe1).to eq(joe2)
expect(jane1).to eq(jane2)
expect(jane1).not_to eq(nil)
expect(described_class.new('username' => nil)).not_to eq(nil)
end
end
describe '#to_h' do
it 'returns the given options' do
expect(subject.to_h).to eq(options)
end
end
context 'when having multiple capabilities' do context 'when having multiple capabilities' do
let(:capabilities) { ['reviewer backend', 'maintainer frontend', 'trainee_maintainer qa'] } let(:capabilities) { ['reviewer backend', 'maintainer frontend', 'trainee_maintainer qa'] }
...@@ -153,21 +171,21 @@ RSpec.describe Gitlab::Danger::Teammate do ...@@ -153,21 +171,21 @@ RSpec.describe Gitlab::Danger::Teammate do
describe '#markdown_name' do describe '#markdown_name' do
context 'when timezone_experiment == false' do context 'when timezone_experiment == false' do
it 'returns markdown name as-is' do it 'returns markdown name as-is' do
expect(subject.markdown_name).to eq(options[:markdown_name]) expect(subject.markdown_name).to eq(options['markdown_name'])
expect(subject.markdown_name(timezone_experiment: false)).to eq(options[:markdown_name]) expect(subject.markdown_name(timezone_experiment: false)).to eq(options['markdown_name'])
end end
end end
context 'when timezone_experiment == true' do context 'when timezone_experiment == true' do
it 'returns markdown name with timezone info' do it 'returns markdown name with timezone info' do
expect(subject.markdown_name(timezone_experiment: true)).to eq("#{options[:markdown_name]} (UTC+2)") expect(subject.markdown_name(timezone_experiment: true)).to eq("#{options['markdown_name']} (UTC+2)")
end end
context 'when offset is 1.5' do context 'when offset is 1.5' do
let(:tz_offset_hours) { 1.5 } let(:tz_offset_hours) { 1.5 }
it 'returns markdown name with timezone info, not truncated' do it 'returns markdown name with timezone info, not truncated' do
expect(subject.markdown_name(timezone_experiment: true)).to eq("#{options[:markdown_name]} (UTC+1.5)") expect(subject.markdown_name(timezone_experiment: true)).to eq("#{options['markdown_name']} (UTC+1.5)")
end end
end end
...@@ -185,12 +203,12 @@ RSpec.describe Gitlab::Danger::Teammate do ...@@ -185,12 +203,12 @@ RSpec.describe Gitlab::Danger::Teammate do
with_them do with_them do
it 'returns markdown name with timezone info' do it 'returns markdown name with timezone info' do
author = described_class.new(options.merge(username: 'mario', tz_offset_hours: author_offset).stringify_keys) author = described_class.new(options.merge('username' => 'mario', 'tz_offset_hours' => author_offset))
floored_offset_hours = subject.__send__(:floored_offset_hours) floored_offset_hours = subject.__send__(:floored_offset_hours)
utc_offset = floored_offset_hours >= 0 ? "+#{floored_offset_hours}" : floored_offset_hours utc_offset = floored_offset_hours >= 0 ? "+#{floored_offset_hours}" : floored_offset_hours
expect(subject.markdown_name(timezone_experiment: true, author: author)).to eq("#{options[:markdown_name]} (UTC#{utc_offset}, #{diff_text})") expect(subject.markdown_name(timezone_experiment: true, author: author)).to eq("#{options['markdown_name']} (UTC#{utc_offset}, #{diff_text})")
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