diff --git a/app/services/tournament_group_match_starter.rb b/app/services/tournament_group_match_starter.rb index 0fdd7d5..a6575c8 100644 --- a/app/services/tournament_group_match_starter.rb +++ b/app/services/tournament_group_match_starter.rb @@ -3,17 +3,11 @@ class TournamentGroupMatchStarter class << self def upcoming_matches(tournament) - group_stage = tournament.group_stage - return [] if group_stage.nil? - - next_matches = group_stage.groups.filter_map do |group| - group.matches.not_started.order(:position).first - end - lowest_position = next_matches.map(&:position).min + group_matches = not_started_group_matches(tournament) + lowest_position = group_matches.minimum(:position) return [] if lowest_position.nil? - next_matches.select { |match| match.position == lowest_position } - .sort_by { |match| [match.position, match.group.number, match.id] } + selected_lowest_matches(group_matches, lowest_position) end def start_next!(tournament, logger: Rails.logger) @@ -30,6 +24,25 @@ class TournamentGroupMatchStarter private + def not_started_group_matches(tournament) + group_stage = tournament.group_stage + return Match.none if group_stage.nil? + + group_ids = group_stage.groups.ids + return Match.none if group_ids.empty? + + Match.where(group_id: group_ids).not_started + end + + def selected_lowest_matches(group_matches, lowest_position) + group_matches.where(position: lowest_position) + .includes(:group) + .group_by(&:group_id) + .values + .map { |matches| matches.min_by(&:id) } + .sort_by { |match| [match.position, match.group.number, match.id] } + end + def recalculate_group_scores!(matches, logger:) matches.map(&:group).uniq.each do |group| next if UpdateGroupsGroupScoresAndSave.call(group:).success? diff --git a/spec/e2e/http/api_surface_spec.rb b/spec/e2e/http/api_surface_spec.rb index b63963c..4938578 100644 --- a/spec/e2e/http/api_surface_spec.rb +++ b/spec/e2e/http/api_surface_spec.rb @@ -412,43 +412,54 @@ RSpec.describe 'Backend API surface HTTP E2E' do .to eq([first_match.fetch(:position)]) end - it 'starts next group matches at the lowest pending position and returns started matches' do + it 'starts only a single lagging group before all groups can advance over HTTP' do tournament = create_group_stage_tournament(client: owner_client, public: false, name_prefix: 'Start Next Groups') - groups = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == -1 } - .fetch(:groups) - .sort_by { |group| group.fetch(:number) } - matches_by_group = groups.to_h do |group| - [group.fetch(:id), group.fetch(:matches).sort_by { |match| match.fetch(:position) }] - end + groups = group_stage_groups(tournament) + matches_by_group = indexed_group_matches(groups) + start_group_positions!(matches_by_group:, group: groups[0], positions: [0, 1]) + start_group_positions!(matches_by_group:, group: groups[1], positions: [0]) + start_group_positions!(matches_by_group:, group: groups[2], positions: [0, 1]) + start_group_positions!(matches_by_group:, group: groups[3], positions: [0, 1]) - first_start = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/start_next_group_matches") - expect(first_start[:status]).to eq(200) - expect(first_start[:json].map { |match| match.dig(:group, :id) }) - .to match_array(groups.map { |group| group.fetch(:id) }) - expect(first_start[:json].map { |match| match.fetch(:position) }.uniq).to eq([0]) - - matches_by_group.fetch(groups.first.fetch(:id))[1..3].each do |match| - start_group_match!(match_id: match.fetch(:id)) - end - groups.drop(1).each do |group| - matches_by_group.fetch(group.fetch(:id))[1..2].each do |match| - start_group_match!(match_id: match.fetch(:id)) - end - end - - expected_match_ids = groups.drop(1).map do |group| - matches_by_group.fetch(group.fetch(:id)).fetch(3).fetch(:id) - end + first_expected_ids = match_ids_for_positions(matches_by_group:, pairs: [[groups[1], 1]]) + second_expected_ids = match_ids_for_positions( + matches_by_group:, + pairs: groups.map { |group| [group, 2] } + ) upcoming = owner_client.get("/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' }) - catch_up_start = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/start_next_group_matches") + first_start = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/start_next_group_matches") + second_start = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/start_next_group_matches") - expect(upcoming[:status]).to eq(200) - expect(upcoming[:json].map { |match| match.fetch(:id) }).to match_array(expected_match_ids) + expect_match_ids(upcoming, first_expected_ids) + expect_started_match_ids(first_start, first_expected_ids) + expect_started_match_ids(second_start, second_expected_ids) + end - expect(catch_up_start[:status]).to eq(200) - expect(catch_up_start[:json].map { |match| match.fetch(:id) }).to match_array(expected_match_ids) - expect(catch_up_start[:json].map { |match| match.fetch(:position) }.uniq).to eq([3]) - expect(catch_up_start[:json].map { |match| match.fetch(:state) }.uniq).to eq(['in_progress']) + it 'starts catch-up waves before the next position is started everywhere over HTTP' do + tournament = create_group_stage_tournament(client: owner_client, public: false, name_prefix: 'Start Catch Up Waves') + groups = group_stage_groups(tournament) + matches_by_group = indexed_group_matches(groups) + start_group_positions!(matches_by_group:, group: groups[0], positions: [0, 1, 2]) + start_group_positions!(matches_by_group:, group: groups[1], positions: [0]) + start_group_positions!(matches_by_group:, group: groups[2], positions: [0, 1, 2]) + start_group_positions!(matches_by_group:, group: groups[3], positions: [0, 1]) + + first_expected_ids = match_ids_for_positions(matches_by_group:, pairs: [[groups[1], 1]]) + second_expected_ids = match_ids_for_positions( + matches_by_group:, + pairs: [[groups[1], 2], [groups[3], 2]] + ) + third_expected_ids = match_ids_for_positions( + matches_by_group:, + pairs: groups.map { |group| [group, 3] } + ) + first_start = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/start_next_group_matches") + second_start = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/start_next_group_matches") + third_start = owner_client.patch("/tournaments/#{tournament.fetch(:id)}/start_next_group_matches") + + expect_started_match_ids(first_start, first_expected_ids) + expect_started_match_ids(second_start, second_expected_ids) + expect_started_match_ids(third_start, third_expected_ids) end def login_client(email:, password:) @@ -505,6 +516,40 @@ RSpec.describe 'Backend API surface HTTP E2E' do fetch_tournament(client: client, tournament_id: response.dig(:json, :id)) end + def group_stage_groups(tournament) + tournament.fetch(:stages).find { |stage| stage.fetch(:level) == -1 } + .fetch(:groups) + .sort_by { |group| group.fetch(:number) } + end + + def indexed_group_matches(groups) + groups.to_h do |group| + [group.fetch(:id), group.fetch(:matches).sort_by { |match| match.fetch(:position) }] + end + end + + def start_group_positions!(matches_by_group:, group:, positions:) + positions.each do |position| + start_group_match!(match_id: matches_by_group.fetch(group.fetch(:id)).fetch(position).fetch(:id)) + end + end + + def match_ids_for_positions(matches_by_group:, pairs:) + pairs.map do |group, position| + matches_by_group.fetch(group.fetch(:id)).fetch(position).fetch(:id) + end + end + + def expect_match_ids(response, expected_ids) + expect(response[:status]).to eq(200) + expect(response[:json].map { |match| match.fetch(:id) }).to match_array(expected_ids) + end + + def expect_started_match_ids(response, expected_ids) + expect_match_ids(response, expected_ids) + expect(response[:json].map { |match| match.fetch(:state) }.uniq).to eq(['in_progress']) + end + def create_playoff_tournament(client:, public:, name_prefix:) payload = { name: "#{name_prefix} #{unique_suffix}", diff --git a/spec/services/tournament_group_match_starter_spec.rb b/spec/services/tournament_group_match_starter_spec.rb index 77f2b24..1c0f662 100644 --- a/spec/services/tournament_group_match_starter_spec.rb +++ b/spec/services/tournament_group_match_starter_spec.rb @@ -7,29 +7,50 @@ RSpec.describe TournamentGroupMatchStarter do let(:groups) { tournament.group_stage.groups.sort_by(&:number) } before do - set_group_match_states(groups[0], %i[finished finished finished in_progress]) - groups.drop(1).each do |group| - set_group_match_states(group, %i[finished finished in_progress not_started]) - end + groups.each { |group| set_group_match_states(group, Array.new(4, :not_started)) } end describe '.upcoming_matches' do it 'returns only groups whose next unstarted match is at the lowest pending position' do + start_group_positions(groups[0], [0, 1, 2]) + start_group_positions(groups[1], [0, 1]) + start_group_positions(groups[2], [0, 1]) + start_group_positions(groups[3], [0, 1]) + matches = described_class.upcoming_matches(tournament) expect(matches.map(&:group)).to match_array(groups.drop(1)) - expect(matches.map(&:position).uniq).to eq([3]) + expect(matches.map(&:position).uniq).to eq([2]) end end describe '.start_next!' do - it 'starts selected catch-up matches and returns them' do - started_matches = described_class.start_next!(tournament, logger: Logger.new(nil)) + it 'starts only a single lagging group before all groups can advance' do + start_group_positions(groups[0], [0, 1]) + start_group_positions(groups[1], [0]) + start_group_positions(groups[2], [0, 1]) + start_group_positions(groups[3], [0, 1]) - expect(started_matches.map(&:group)).to match_array(groups.drop(1)) - expect(started_matches.map(&:position).uniq).to eq([3]) - expect(started_matches.map { |match| match.reload.state }.uniq).to eq(['in_progress']) - expect(groups.first.matches.find_by!(position: 3).reload).to be_in_progress + first_start = described_class.start_next!(tournament, logger: Logger.new(nil)) + second_start = described_class.start_next!(tournament, logger: Logger.new(nil)) + + expect_group_positions(first_start, [[groups[1], 1]]) + expect_group_positions(second_start, groups.map { |group| [group, 2] }) + end + + it 'starts catch-up waves before the next position is started everywhere' do + start_group_positions(groups[0], [0, 1, 2]) + start_group_positions(groups[1], [0]) + start_group_positions(groups[2], [0, 1, 2]) + start_group_positions(groups[3], [0, 1]) + + first_start = described_class.start_next!(tournament, logger: Logger.new(nil)) + second_start = described_class.start_next!(tournament, logger: Logger.new(nil)) + third_start = described_class.start_next!(tournament, logger: Logger.new(nil)) + + expect_group_positions(first_start, [[groups[1], 1]]) + expect_group_positions(second_start, [[groups[1], 2], [groups[3], 2]]) + expect_group_positions(third_start, groups.map { |group| [group, 3] }) end end @@ -38,4 +59,15 @@ RSpec.describe TournamentGroupMatchStarter do match.update!(position: index, state: states.fetch(index)) end end + + def start_group_positions(group, positions) + positions.each do |position| + group.matches.find_by!(position:).update!(state: :in_progress) + end + end + + def expect_group_positions(matches, expected_pairs) + expect(matches.map { |match| [match.group, match.position] }).to match_array(expected_pairs) + expect(matches.map { |match| match.reload.state }.uniq).to eq(['in_progress']) + end end