From 8e3325bbfca72d0f2b5b29c0bbe31edcdf515ceb Mon Sep 17 00:00:00 2001 From: Malaber <32635600+Malaber@users.noreply.github.com> Date: Wed, 12 Jun 2019 09:39:39 +0200 Subject: [PATCH 01/11] Simplify concatenation of empty stages --- app/services/playoff_stage_service.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/services/playoff_stage_service.rb b/app/services/playoff_stage_service.rb index 38ccabc..60b44a6 100644 --- a/app/services/playoff_stage_service.rb +++ b/app/services/playoff_stage_service.rb @@ -15,9 +15,7 @@ class PlayoffStageService playoffs << initial_stage # empty stages are the stages, the tournament is filled with to have the matches ready for later empty_stages = generate_stages_with_empty_matches(stage_count - 1) - empty_stages.each do |stage| - playoffs << stage - end + playoffs.concat empty_stages playoffs end From 36db03293e62cfe12d81489f625d1c3c9870ab04 Mon Sep 17 00:00:00 2001 From: Malaber <32635600+Malaber@users.noreply.github.com> Date: Wed, 12 Jun 2019 20:39:09 +0200 Subject: [PATCH 02/11] Return a "winner" for :single_team matches --- app/models/match.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/match.rb b/app/models/match.rb index 7891276..6e15b10 100644 --- a/app/models/match.rb +++ b/app/models/match.rb @@ -28,6 +28,8 @@ class Match < ApplicationRecord end def winner + return teams.first if single_team? + finished? ? current_leading_team : nil end From 5f378f27b20585477fbb7d315def54167c69a0f0 Mon Sep 17 00:00:00 2001 From: Malaber <32635600+Malaber@users.noreply.github.com> Date: Wed, 12 Jun 2019 20:40:02 +0200 Subject: [PATCH 03/11] Correct position for :single_team matches --- app/services/match_service.rb | 8 +++++--- spec/services/match_service_spec.rb | 11 +++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/services/match_service.rb b/app/services/match_service.rb index adf2995..9d3cb50 100644 --- a/app/services/match_service.rb +++ b/app/services/match_service.rb @@ -45,12 +45,14 @@ class MatchService end # the start point is to compensate for all the teams that are already within a "normal" match - startpoint = matches.size + i = team_offset = matches.size until matches.size >= needed_games # while we do not have enough matches in general we need to fill the array with "single team" matches - i = matches.size + startpoint - match = Match.new state: :single_team, position: i, match_scores: [MatchScore.create(team: teams[i])] + match = Match.new state: :single_team, + position: i, + match_scores: [MatchScore.create(team: teams[i + team_offset])] matches << match + i += 1 end matches end diff --git a/spec/services/match_service_spec.rb b/spec/services/match_service_spec.rb index 21ada52..726fc55 100644 --- a/spec/services/match_service_spec.rb +++ b/spec/services/match_service_spec.rb @@ -117,5 +117,16 @@ RSpec.describe MatchService do it 'raises an exception for for 0 teams' do expect { MatchService.generate_matches([]) }. to raise_error 'Cannot generate Matches without teams' end + + it 'generates matches with consecutive positions' do + MatchService.generate_matches(create_list(:team, 7)).sort_by(&:position).each_with_index do |match, i| + expect(match.position).to eq(i) + end + end + + it 'places all given teams into the matches exactly once' do + teams = create_list(:team, 11) + expect(MatchService.generate_matches(teams).map(&:teams).flatten).to match_array(teams) + end end end From 62f3ccba311e9a5279eec3986ff74d4fd4fdc6b4 Mon Sep 17 00:00:00 2001 From: Malaber <32635600+Malaber@users.noreply.github.com> Date: Thu, 13 Jun 2019 13:14:42 +0200 Subject: [PATCH 04/11] Rename method test --- spec/services/playoff_stage_service_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/services/playoff_stage_service_spec.rb b/spec/services/playoff_stage_service_spec.rb index 91e3149..99466bd 100644 --- a/spec/services/playoff_stage_service_spec.rb +++ b/spec/services/playoff_stage_service_spec.rb @@ -51,7 +51,7 @@ RSpec.describe PlayoffStageService do end end - describe 'generates playoff stages for' do + describe '#generate_playoff_stages' do [ { team_size: 1, expected_amount_of_playoff_stages: 1 }, { team_size: 2, expected_amount_of_playoff_stages: 1 }, @@ -66,7 +66,7 @@ RSpec.describe PlayoffStageService do { team_size: 64, expected_amount_of_playoff_stages: 6 }, { team_size: 111, expected_amount_of_playoff_stages: 7 } ].each do |parameters| - it "#{parameters[:team_size]} teams" do + it "generates playoff stages for #{parameters[:team_size]} teams" do amount_of_teams = parameters[:team_size] expected_amount_of_playoff_stages = parameters[:expected_amount_of_playoff_stages] teams = build_list(:team, amount_of_teams) From 98319e962515e373514922228828501192b05729 Mon Sep 17 00:00:00 2001 From: Malaber <32635600+Malaber@users.noreply.github.com> Date: Wed, 12 Jun 2019 20:41:06 +0200 Subject: [PATCH 05/11] Assign :intermediate_stage to first stage if single_team matches present --- app/models/stage.rb | 2 +- app/services/playoff_stage_service.rb | 1 + spec/services/playoff_stage_service_spec.rb | 32 +++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/models/stage.rb b/app/models/stage.rb index 1f36b21..f29d661 100644 --- a/app/models/stage.rb +++ b/app/models/stage.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Stage < ApplicationRecord - enum state: %i[playoff_stage in_progress finished] + enum state: %i[playoff_stage intermediate_stage in_progress finished] belongs_to :tournament has_many :matches, dependent: :destroy diff --git a/app/services/playoff_stage_service.rb b/app/services/playoff_stage_service.rb index 60b44a6..3ee2e1f 100644 --- a/app/services/playoff_stage_service.rb +++ b/app/services/playoff_stage_service.rb @@ -12,6 +12,7 @@ class PlayoffStageService # initial_matches are the matches in the first stage; this is the only stage filled with teams from the start on initial_matches = MatchService.generate_matches(teams) initial_stage = Stage.new level: stage_count - 1, matches: initial_matches + initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil? playoffs << initial_stage # empty stages are the stages, the tournament is filled with to have the matches ready for later empty_stages = generate_stages_with_empty_matches(stage_count - 1) diff --git a/spec/services/playoff_stage_service_spec.rb b/spec/services/playoff_stage_service_spec.rb index 99466bd..97fdfb7 100644 --- a/spec/services/playoff_stage_service_spec.rb +++ b/spec/services/playoff_stage_service_spec.rb @@ -79,6 +79,38 @@ RSpec.describe PlayoffStageService do end end end + + describe 'number of teams isn\'t a power of two' do + let(:generated_stages) do + PlayoffStageService.generate_playoff_stages(create_list(:team, 12)) + end + + let(:intermediate_stage) do + generated_stages.max_by(&:level) + end + + it 'generates an intermediate stage at the top level' do + expect(intermediate_stage.state).to eq('intermediate_stage') + end + + it 'generates normal playoff_stage state stages elsewhere' do + (generated_stages - [intermediate_stage]).each do |stage| + expect(stage.state).to eq('playoff_stage') + end + end + end + + describe 'number of teams is a power of two' do + let(:generated_stages) do + PlayoffStageService.generate_playoff_stages(create_list(:team, 16)) + end + + it 'generates only normal playoff_stage state stages' do + generated_stages.each do |stage| + expect(stage.state).to eq('playoff_stage') + end + end + end end describe '#populate_match_below' do From c9e1e153df0449c5942f768df419b3b98404dab5 Mon Sep 17 00:00:00 2001 From: Malaber <32635600+Malaber@users.noreply.github.com> Date: Wed, 12 Jun 2019 21:53:36 +0200 Subject: [PATCH 06/11] Add interactor to advance all teams stuck in single team matches After playoffs are created, before saving the stage this interactor goes to the intermediate stage, looks for all the matches that are :single_team matches and populates the match below with their respective "winner". --- app/interactors/add_playoffs_to_tournament.rb | 3 ++- .../advance_teams_in_intermediate_stage.rb | 14 ++++++++++++++ .../add_playoffs_to_tournament_and_save.rb | 2 +- ...ms_in_intermediate_stage_interactor_spec.rb | 18 ++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 app/interactors/advance_teams_in_intermediate_stage.rb create mode 100644 spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb diff --git a/app/interactors/add_playoffs_to_tournament.rb b/app/interactors/add_playoffs_to_tournament.rb index 9efc2ec..59753b0 100644 --- a/app/interactors/add_playoffs_to_tournament.rb +++ b/app/interactors/add_playoffs_to_tournament.rb @@ -12,7 +12,8 @@ class AddPlayoffsToTournament else tournament.stages.concat playoff_stages end - context.object_to_save = tournament + context.intermediate_stage = tournament.stages.find(&:intermediate_stage?) + context.object_to_save = [tournament] else context.fail! end diff --git a/app/interactors/advance_teams_in_intermediate_stage.rb b/app/interactors/advance_teams_in_intermediate_stage.rb new file mode 100644 index 0000000..cf060b6 --- /dev/null +++ b/app/interactors/advance_teams_in_intermediate_stage.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class AdvanceTeamsInIntermediateStage + include Interactor + + def call + intermediate_stage = context.intermediate_stage + return if intermediate_stage.nil? + + intermediate_stage.matches.select { |m| m.state == 'single_team' } + .each { |match| PopulateMatchBelowAndSave.call(match: match) } + context.object_to_save << intermediate_stage + end +end diff --git a/app/organizers/add_playoffs_to_tournament_and_save.rb b/app/organizers/add_playoffs_to_tournament_and_save.rb index 766add6..e2e5c49 100644 --- a/app/organizers/add_playoffs_to_tournament_and_save.rb +++ b/app/organizers/add_playoffs_to_tournament_and_save.rb @@ -3,5 +3,5 @@ class AddPlayoffsToTournamentAndSave include Interactor::Organizer - organize AddPlayoffsToTournament, SaveApplicationRecordObject + organize AddPlayoffsToTournament, AdvanceTeamsInIntermediateStage, SaveApplicationRecordObject end diff --git a/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb b/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb new file mode 100644 index 0000000..cdb13ce --- /dev/null +++ b/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.describe AdvanceTeamsInIntermediateStage do + context 'intermediate_stage is nil' do + let(:context) do + AdvanceTeamsInIntermediateStage.call(intermediate_stage: nil) + end + + it 'succeeds' do + expect(context).to be_a_success + end + + it 'doesn\'t call PopulateMatchBelow' do + expect(PopulateMatchBelowAndSave).not_to receive(:call) + context + end + end +end From e0da9ff7b5f9f92dd30f1730377678ace0d5c361 Mon Sep 17 00:00:00 2001 From: Malaber <32635600+Malaber@users.noreply.github.com> Date: Thu, 13 Jun 2019 00:19:49 +0200 Subject: [PATCH 07/11] Assign empty array if object_to_save is nil before pushing values to it https://stackoverflow.com/questions/12163625/create-or-append-to-array-in-ruby --- app/interactors/add_group_stage_to_tournament.rb | 2 +- app/interactors/add_playoffs_to_tournament.rb | 2 +- app/interactors/advance_teams_in_intermediate_stage.rb | 2 +- app/interactors/populate_match_below.rb | 2 +- app/interactors/update_groups_group_scores.rb | 2 +- spec/interactors/populate_match_below_interactor_spec.rb | 2 +- spec/interactors/update_groups_group_scores_interactor_spec.rb | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/interactors/add_group_stage_to_tournament.rb b/app/interactors/add_group_stage_to_tournament.rb index 23e81de..e9866c2 100644 --- a/app/interactors/add_group_stage_to_tournament.rb +++ b/app/interactors/add_group_stage_to_tournament.rb @@ -13,7 +13,7 @@ class AddGroupStageToTournament tournament.instant_finalists_amount, tournament.intermediate_round_participants_amount = TournamentService.calculate_default_amount_of_teams_advancing(tournament.playoff_teams_amount, group_stage.groups.size) - context.object_to_save = tournament + (context.object_to_save ||= []) << tournament rescue StandardError context.fail! end diff --git a/app/interactors/add_playoffs_to_tournament.rb b/app/interactors/add_playoffs_to_tournament.rb index 59753b0..ce530cb 100644 --- a/app/interactors/add_playoffs_to_tournament.rb +++ b/app/interactors/add_playoffs_to_tournament.rb @@ -13,7 +13,7 @@ class AddPlayoffsToTournament tournament.stages.concat playoff_stages end context.intermediate_stage = tournament.stages.find(&:intermediate_stage?) - context.object_to_save = [tournament] + (context.object_to_save ||= []) << tournament else context.fail! end diff --git a/app/interactors/advance_teams_in_intermediate_stage.rb b/app/interactors/advance_teams_in_intermediate_stage.rb index cf060b6..a85ba63 100644 --- a/app/interactors/advance_teams_in_intermediate_stage.rb +++ b/app/interactors/advance_teams_in_intermediate_stage.rb @@ -9,6 +9,6 @@ class AdvanceTeamsInIntermediateStage intermediate_stage.matches.select { |m| m.state == 'single_team' } .each { |match| PopulateMatchBelowAndSave.call(match: match) } - context.object_to_save << intermediate_stage + (context.object_to_save ||= []) << intermediate_stage end end diff --git a/app/interactors/populate_match_below.rb b/app/interactors/populate_match_below.rb index 96a58c3..e224639 100644 --- a/app/interactors/populate_match_below.rb +++ b/app/interactors/populate_match_below.rb @@ -7,7 +7,7 @@ class PopulateMatchBelow match = context.match begin objects_to_save = PlayoffStageService.populate_match_below(match) - context.object_to_save = objects_to_save + (context.object_to_save ||= []) << objects_to_save rescue StandardError context.fail! end diff --git a/app/interactors/update_groups_group_scores.rb b/app/interactors/update_groups_group_scores.rb index 3ea49e2..fbb05fc 100644 --- a/app/interactors/update_groups_group_scores.rb +++ b/app/interactors/update_groups_group_scores.rb @@ -4,7 +4,7 @@ class UpdateGroupsGroupScores include Interactor def call - context.object_to_save = GroupStageService.update_group_scores(context.group) + (context.object_to_save ||= []) << GroupStageService.update_group_scores(context.group) rescue StandardError context.fail! end diff --git a/spec/interactors/populate_match_below_interactor_spec.rb b/spec/interactors/populate_match_below_interactor_spec.rb index 22ef725..2201c91 100644 --- a/spec/interactors/populate_match_below_interactor_spec.rb +++ b/spec/interactors/populate_match_below_interactor_spec.rb @@ -21,7 +21,7 @@ RSpec.describe PopulateMatchBelow, type: :interactor do end it 'provides the objects to save' do - expect(context.object_to_save).to match_array(@objects_to_save) + expect(context.object_to_save.flatten).to match_array(@objects_to_save.flatten) end end diff --git a/spec/interactors/update_groups_group_scores_interactor_spec.rb b/spec/interactors/update_groups_group_scores_interactor_spec.rb index e664ffc..bc9a3d9 100644 --- a/spec/interactors/update_groups_group_scores_interactor_spec.rb +++ b/spec/interactors/update_groups_group_scores_interactor_spec.rb @@ -22,7 +22,7 @@ RSpec.describe UpdateGroupsGroupScores, type: :interactor do end it 'provides the objects to save' do - expect(context.object_to_save).to eq(@group_scores) + expect(context.object_to_save.flatten).to eq(@group_scores) end end From f35ba5c745c4c92679e18ed9331b5081b9c30cd3 Mon Sep 17 00:00:00 2001 From: Malaber <32635600+Malaber@users.noreply.github.com> Date: Thu, 13 Jun 2019 00:43:26 +0200 Subject: [PATCH 08/11] Create factory for :single_team_match --- spec/factories/matches.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/factories/matches.rb b/spec/factories/matches.rb index 5c8a42e..cd0543f 100644 --- a/spec/factories/matches.rb +++ b/spec/factories/matches.rb @@ -14,6 +14,13 @@ FactoryBot.define do state { :in_progress } end + factory :single_team_match do + state { :single_team } + after(:create) do |match| + match.match_scores = [create(:match_score, points: 0)] + end + end + factory :empty_prepared_playoff_match do state { :not_ready } end From 213867a827122f49c0fe12927edc2d099f613a94 Mon Sep 17 00:00:00 2001 From: Malaber <32635600+Malaber@users.noreply.github.com> Date: Thu, 13 Jun 2019 00:45:17 +0200 Subject: [PATCH 09/11] Let context fail if population of match below fails --- app/interactors/advance_teams_in_intermediate_stage.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/interactors/advance_teams_in_intermediate_stage.rb b/app/interactors/advance_teams_in_intermediate_stage.rb index a85ba63..1eb1bd3 100644 --- a/app/interactors/advance_teams_in_intermediate_stage.rb +++ b/app/interactors/advance_teams_in_intermediate_stage.rb @@ -8,7 +8,9 @@ class AdvanceTeamsInIntermediateStage return if intermediate_stage.nil? intermediate_stage.matches.select { |m| m.state == 'single_team' } - .each { |match| PopulateMatchBelowAndSave.call(match: match) } + .each do |match| + context.fail! unless PopulateMatchBelowAndSave.call(match: match).success? + end (context.object_to_save ||= []) << intermediate_stage end end From 6dff05b5e60c4180fe950fa89e04963acf922bac Mon Sep 17 00:00:00 2001 From: Malaber <32635600+Malaber@users.noreply.github.com> Date: Thu, 13 Jun 2019 00:45:44 +0200 Subject: [PATCH 10/11] Test AdvanceTeamsInIntermediateStage Interactor --- ...s_in_intermediate_stage_interactor_spec.rb | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb b/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb index cdb13ce..d6c2165 100644 --- a/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb +++ b/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb @@ -1,18 +1,52 @@ # frozen_string_literal: true RSpec.describe AdvanceTeamsInIntermediateStage do + shared_examples_for 'succeeding context' do + it 'succeeds' do + expect(context).to be_a_success + end + end + + shared_examples_for 'failing context' do + it 'fails' do + expect(context).to be_a_failure + end + end + context 'intermediate_stage is nil' do let(:context) do AdvanceTeamsInIntermediateStage.call(intermediate_stage: nil) end - it 'succeeds' do - expect(context).to be_a_success - end + it_behaves_like 'succeeding context' it 'doesn\'t call PopulateMatchBelow' do expect(PopulateMatchBelowAndSave).not_to receive(:call) context end end + + context 'intermediate_stage is an realistic stage' do + let(:context) do + AdvanceTeamsInIntermediateStage.call(intermediate_stage: create(:playoff_stage, match_type: :single_team_match)) + end + + context 'PopulateMatchBelow succeeds' do + before do + expect(class_double('PopulateMatchBelowAndSave').as_stubbed_const(transfer_nested_constants: true)) + .to receive(:call).exactly(4).times.and_return(double(:context, success?: true)) + end + + it_behaves_like 'succeeding context' + end + + context 'PopulateMatchBelow fails' do + before do + expect(class_double('PopulateMatchBelowAndSave').as_stubbed_const(transfer_nested_constants: true)) + .to receive(:call).and_return(double(:context, success?: false)) + end + + it_behaves_like 'failing context' + end + end end From 7630f6b432dbd7c4c870e84ec241a974f75f478b Mon Sep 17 00:00:00 2001 From: Malaber <32635600+Malaber@users.noreply.github.com> Date: Mon, 17 Jun 2019 15:02:03 +0200 Subject: [PATCH 11/11] Fix Typo --- .../advance_teams_in_intermediate_stage_interactor_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb b/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb index d6c2165..976863b 100644 --- a/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb +++ b/spec/interactors/advance_teams_in_intermediate_stage_interactor_spec.rb @@ -26,7 +26,7 @@ RSpec.describe AdvanceTeamsInIntermediateStage do end end - context 'intermediate_stage is an realistic stage' do + context 'intermediate_stage is a realistic stage' do let(:context) do AdvanceTeamsInIntermediateStage.call(intermediate_stage: create(:playoff_stage, match_type: :single_team_match)) end