diff --git a/services/app/apps/codebattle/assets/js/socket.js b/services/app/apps/codebattle/assets/js/socket.js
index 4d2f3ee7c..f5beb62b5 100644
--- a/services/app/apps/codebattle/assets/js/socket.js
+++ b/services/app/apps/codebattle/assets/js/socket.js
@@ -55,6 +55,7 @@ export const channelTopics = {
tournamentPlayerFinishedRoundTopic: 'tournament:player:finished_round',
tournamentPlayerFinishedTopic: 'tournament:player:finished',
tournamentActivated: 'tournament:activated',
+ tournamentCanceled: 'tournament:canceled',
roundCreatedTopic: 'round:created',
diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Main.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Main.js
index 4483f4e8a..1277e4226 100644
--- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Main.js
+++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Main.js
@@ -104,6 +104,11 @@ const initPresence = followId => dispatch => {
data => {
camelizeKeysAndDispatch(dispatch, actions.changeTournamentState)(data);
},
+ ).addListener(
+ channelTopics.tournamentCanceled,
+ data => {
+ camelizeKeysAndDispatch(dispatch, actions.changeTournamentState)(data);
+ },
);
};
diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/rating/RatingList.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/rating/RatingList.jsx
index 0b5194000..9f0c61442 100644
--- a/services/app/apps/codebattle/assets/js/widgets/pages/rating/RatingList.jsx
+++ b/services/app/apps/codebattle/assets/js/widgets/pages/rating/RatingList.jsx
@@ -31,6 +31,7 @@ const renderUser = (page, pageSize, user, index) => (
{user.rank} |
+ {user.points} |
{user.rating} |
{user.gamesPlayed} |
@@ -235,6 +236,13 @@ function UsersRating() {
Rank
{renderSortArrow('rank', sortParams)}
+ | triggerSort('points')}
+ >
+ Points
+ {renderSortArrow('points', sortParams)}
+ |
triggerSort('rating')}
diff --git a/services/app/apps/codebattle/assets/js/widgets/slices/lobby.js b/services/app/apps/codebattle/assets/js/widgets/slices/lobby.js
index 121914016..9907a7752 100644
--- a/services/app/apps/codebattle/assets/js/widgets/slices/lobby.js
+++ b/services/app/apps/codebattle/assets/js/widgets/slices/lobby.js
@@ -58,8 +58,8 @@ const lobby = createSlice({
state.activeGames = state.activeGames.map(game => {
if (game.id === payload.gameId) {
const newPlayers = game.players.map(player => (player.id === payload.userId
- ? { ...player, editorLang: payload.editorLang }
- : player));
+ ? { ...player, editorLang: payload.editorLang }
+ : player));
return { ...game, players: newPlayers };
}
@@ -71,8 +71,8 @@ const lobby = createSlice({
state.activeGames = state.activeGames.map(game => {
if (game.id === payload.gameId) {
const newPlayers = game.players.map(player => (player.id === payload.userId
- ? { ...player, checkResult: payload.checkResult }
- : player));
+ ? { ...player, checkResult: payload.checkResult }
+ : player));
return { ...game, players: newPlayers };
}
@@ -131,16 +131,25 @@ const lobby = createSlice({
},
extraReducers: {
[tournamentActions.changeTournamentState]: (state, { payload }) => {
- const seasonTournament = state.seasonTournaments.find(t => t.id === payload.id);
- const liveTournament = state.liveTournaments.find(t => t.id === payload.id);
+ const seasonTournament = state.seasonTournaments.find(
+ t => t.id === payload.id,
+ );
+ const liveTournament = state.liveTournaments.find(
+ t => t.id === payload.id,
+ );
if (seasonTournament) {
- state.upcomingTournaments = state.upcomingTournaments.filter(t => t.id !== payload.id);
- state.liveTournaments = [...state.liveTournaments, seasonTournament].sort(sortByStartsAt);
+ state.seasonTournaments = state.seasonTournaments.filter(
+ t => t.id !== payload.id,
+ );
+ state.liveTournaments = [
+ ...state.liveTournaments,
+ { ...seasonTournament, state: payload.state },
+ ].sort(sortByStartsAt);
}
if (liveTournament) {
- state.liveTournaments = state.liveTournaments.map(t => (t.id === payload.id ? ({ ...t, state: payload.state }) : t));
+ state.liveTournaments = state.liveTournaments.map(t => (t.id === payload.id ? { ...t, state: payload.state } : t));
}
},
},
diff --git a/services/app/apps/codebattle/lib/codebattle/pub_sub/events.ex b/services/app/apps/codebattle/lib/codebattle/pub_sub/events.ex
index 98771d41f..73b24548d 100644
--- a/services/app/apps/codebattle/lib/codebattle/pub_sub/events.ex
+++ b/services/app/apps/codebattle/lib/codebattle/pub_sub/events.ex
@@ -16,6 +16,16 @@ defmodule Codebattle.PubSub.Events do
]
end
+ def get_messages("tournament:canceled", params) do
+ [
+ %Message{
+ topic: "season",
+ event: "tournament:canceled",
+ payload: %{tournament: params.tournament}
+ }
+ ]
+ end
+
def get_messages("tournament:activated", params) do
[
%Message{
diff --git a/services/app/apps/codebattle/lib/codebattle/tournament/context.ex b/services/app/apps/codebattle/lib/codebattle/tournament/context.ex
index a440d560f..175d6915d 100644
--- a/services/app/apps/codebattle/lib/codebattle/tournament/context.ex
+++ b/services/app/apps/codebattle/lib/codebattle/tournament/context.ex
@@ -109,18 +109,6 @@ defmodule Codebattle.Tournament.Context do
)
end
- @spec get_waiting_participants_to_start_candidates() :: list(Tournament.t())
- def get_waiting_participants_to_start_candidates do
- Enum.filter(get_live_tournaments(), fn tournament ->
- tournament.state == "waiting_participants" &&
- tournament.grade != "open" &&
- tournament.starts_at
-
- # &&
- # DateTime.compare(tournament.starts_at, DateTime.utc_now()) == :lt
- end)
- end
-
@spec get_upcoming_to_live_candidate(non_neg_integer()) :: Tournament.t() | nil
def get_upcoming_to_live_candidate(starts_at_delay_mins) do
now = DateTime.utc_now()
@@ -133,8 +121,8 @@ defmodule Codebattle.Tournament.Context do
where:
t.state == "upcoming" and
t.grade != "open" and
- t.starts_at > ^now and
- t.starts_at < ^delay_time
+ t.starts_at >= ^now and
+ t.starts_at <= ^delay_time
)
)
end
@@ -341,11 +329,12 @@ defmodule Codebattle.Tournament.Context do
@spec move_upcoming_to_live(Tournament.t()) :: :ok
def move_upcoming_to_live(tournament) do
- tournament
- |> Tournament.changeset(%{state: "waiting_participants"})
- |> Repo.update!()
+ tournament =
+ tournament
+ |> Tournament.changeset(%{state: "waiting_participants"})
+ |> Repo.update!()
- :timer.sleep(1000)
+ :timer.sleep(100)
Tournament.GlobalSupervisor.start_tournament(tournament)
Codebattle.PubSub.broadcast("tournament:activated", %{tournament: tournament})
diff --git a/services/app/apps/codebattle/lib/codebattle/tournament/server.ex b/services/app/apps/codebattle/lib/codebattle/tournament/server.ex
index 31e744367..4cef58692 100644
--- a/services/app/apps/codebattle/lib/codebattle/tournament/server.ex
+++ b/services/app/apps/codebattle/lib/codebattle/tournament/server.ex
@@ -97,6 +97,14 @@ defmodule Codebattle.Tournament.Server do
{:error, :not_found}
end
+ def cast_event(tournament_id, event_type, params) do
+ GenServer.cast(server_name(tournament_id), {:fire_event, event_type, params})
+ catch
+ :exit, reason ->
+ Logger.warning("Error to send tournament update: #{inspect(reason)}")
+ {:error, :not_found}
+ end
+
# SERVER
def init(tournament_id) do
# Create tournament_info_cache table if it doesn't exist
@@ -120,6 +128,11 @@ defmodule Codebattle.Tournament.Server do
|> Map.put(:tasks_table, tasks_table)
|> Map.put(:clans_table, clans_table)
+ if tournament.grade != "open" do
+ time_diff_ms = DateTime.diff(tournament.starts_at, DateTime.utc_now()) * 1000
+ Process.send_after(self(), :start_grade_tournament, time_diff_ms)
+ end
+
{:ok, %{tournament: tournament}}
end
@@ -130,6 +143,12 @@ defmodule Codebattle.Tournament.Server do
end
end
+ def handle_cast({:fire_event, event_type, params}, state) do
+ {:reply, _tournament, state} = handle_call({:fire_event, event_type, params}, nil, state)
+
+ {:noreply, state}
+ end
+
def handle_cast(:match_waiting_room_players, state) do
handle_info(:match_waiting_room_players, state)
{:noreply, state}
@@ -243,6 +262,15 @@ defmodule Codebattle.Tournament.Server do
{:reply, tournament, Map.put(state, :tournament, new_tournament)}
end
+ def handle_info(:start_grade_tournament, %{tournament: tournament}) do
+ case tournament do
+ %{players_count: pc} = t when pc > 0 -> cast_event(t.id, :start, %{})
+ %{players_count: 0} = t -> cast_event(t.id, :cancel, %{})
+ end
+
+ {:noreply, %{tournament: tournament}}
+ end
+
def handle_info({:stop_round_break, round_position}, %{tournament: tournament}) do
if tournament.current_round_position == round_position and
in_break?(tournament) and
@@ -422,10 +450,4 @@ defmodule Codebattle.Tournament.Server do
end
defp server_name(id), do: {:via, Registry, {Codebattle.Registry, "tournament_srv::#{id}"}}
-
- # defp prepare_wr_player(player) do
- # player
- # |> Map.take([:id, :clan_id, :score, :wr_joined_at])
- # |> Map.put(:tasks, Enum.count(player.task_ids))
- # end
end
diff --git a/services/app/apps/codebattle/lib/codebattle/tournament/strategy/base.ex b/services/app/apps/codebattle/lib/codebattle/tournament/strategy/base.ex
index 87c11c321..167d6f8bb 100644
--- a/services/app/apps/codebattle/lib/codebattle/tournament/strategy/base.ex
+++ b/services/app/apps/codebattle/lib/codebattle/tournament/strategy/base.ex
@@ -260,21 +260,21 @@ defmodule Codebattle.Tournament.Base do
def cancel(tournament, params \\ %{})
- def cancel(tournament, %{user: user}) do
+ def cancel(tournament, %{user: user} = params) do
if can_moderate?(tournament, user) do
- new_tournament = tournament |> update_struct(%{state: "canceled"}) |> db_save!()
-
- Game.Context.terminate_tournament_games(tournament.id)
- Tournament.GlobalSupervisor.terminate_tournament(tournament.id)
-
- new_tournament
+ cancel_tournament(tournament, params)
else
tournament
end
end
def cancel(tournament, _params) do
+ cancel_tournament(tournament)
+ end
+
+ defp cancel_tournament(tournament, params \\ %{}) do
new_tournament = tournament |> update_struct(%{state: "canceled"}) |> db_save!()
+ broadcast_tournament_canceled(new_tournament)
Game.Context.terminate_tournament_games(tournament.id)
Tournament.GlobalSupervisor.terminate_tournament(tournament.id)
@@ -917,6 +917,11 @@ defmodule Codebattle.Tournament.Base do
tournament
end
+ defp broadcast_tournament_canceled(tournament) do
+ Codebattle.PubSub.broadcast("tournament:canceled", %{tournament: tournament})
+ tournament
+ end
+
defp broadcast_tournament_finished(tournament) do
Codebattle.PubSub.broadcast("tournament:finished", %{tournament: tournament})
tournament
diff --git a/services/app/apps/codebattle/lib/codebattle/tournament/upcoming_runner.ex b/services/app/apps/codebattle/lib/codebattle/tournament/upcoming_runner.ex
index 80ae992c6..ad1d69e76 100644
--- a/services/app/apps/codebattle/lib/codebattle/tournament/upcoming_runner.ex
+++ b/services/app/apps/codebattle/lib/codebattle/tournament/upcoming_runner.ex
@@ -9,7 +9,7 @@ defmodule Codebattle.Tournament.UpcomingRunner do
require Logger
@tournament_run_upcoming Application.compile_env(:codebattle, :tournament_run_upcoming)
- @worker_timeout to_timeout(second: 30)
+ @worker_timeout to_timeout(second: 3)
@upcoming_time_before_live_mins 7
@@ -30,7 +30,6 @@ defmodule Codebattle.Tournament.UpcomingRunner do
@impl GenServer
def handle_info(:run_upcoming, state) do
run_upcoming()
- start_or_cancel_waiting_participants()
Process.send_after(self(), :run_upcoming, @worker_timeout)
@@ -50,22 +49,4 @@ defmodule Codebattle.Tournament.UpcomingRunner do
:noop
end
end
-
- def start_or_cancel_waiting_participants do
- case Tournament.Context.get_waiting_participants_to_start_candidates() do
- tournaments when is_list(tournaments) ->
- Enum.each(
- tournaments,
- fn
- %{players_count: pc} = t when pc > 0 ->
- Tournament.Context.handle_event(t.id, :start, %{})
-
- %{players_count: 0} = t ->
- Tournament.Context.handle_event(t.id, :cancel, %{})
- end
- )
-
- :ok
- end
- end
end
diff --git a/services/app/apps/codebattle/lib/codebattle/user/scope.ex b/services/app/apps/codebattle/lib/codebattle/user/scope.ex
index 4de047cfe..fb691b770 100644
--- a/services/app/apps/codebattle/lib/codebattle/user/scope.ex
+++ b/services/app/apps/codebattle/lib/codebattle/user/scope.ex
@@ -33,6 +33,7 @@ defmodule Codebattle.User.Scope do
lang: u.lang,
name: u.name,
rank: u.rank,
+ points: u.points,
rating: u.rating
})
end
@@ -80,6 +81,10 @@ defmodule Codebattle.User.Scope do
order_by(query, {^direction, :rank})
end
+ defp apply_sort(query, "points", direction) do
+ order_by(query, {^direction, :points})
+ end
+
defp apply_sort(query, "rating", direction) do
order_by(query, {^direction, :rating})
end
diff --git a/services/app/apps/codebattle/lib/codebattle/users_points_and_rank_update_server.ex b/services/app/apps/codebattle/lib/codebattle/users_points_and_rank_update_server.ex
index f76369ed1..509c785ac 100644
--- a/services/app/apps/codebattle/lib/codebattle/users_points_and_rank_update_server.ex
+++ b/services/app/apps/codebattle/lib/codebattle/users_points_and_rank_update_server.ex
@@ -18,7 +18,7 @@ defmodule Codebattle.UsersPointsAndRankUpdateServer do
# SERVER
def init(_) do
- Process.send_after(self(), :subscribe, 0)
+ Process.send_after(self(), :subscribe, 200)
Process.send_after(self(), :work, to_timeout(minute: 5))
Logger.debug("Start UsersPointsServer")
diff --git a/services/app/apps/codebattle/lib/codebattle_web/channels/main_channel.ex b/services/app/apps/codebattle/lib/codebattle_web/channels/main_channel.ex
index d91e74854..6d8d534aa 100644
--- a/services/app/apps/codebattle/lib/codebattle_web/channels/main_channel.ex
+++ b/services/app/apps/codebattle/lib/codebattle_web/channels/main_channel.ex
@@ -113,6 +113,15 @@ defmodule CodebattleWeb.MainChannel do
{:noreply, socket}
end
+ def handle_info(%{event: "tournament:canceled", payload: %{tournament: tournament}}, socket) do
+ push(socket, "tournament:canceled", %{
+ id: tournament.id,
+ state: tournament.state
+ })
+
+ {:noreply, socket}
+ end
+
def handle_info({:after_join, state}, socket) do
{:ok, _} =
Presence.track(socket, socket.assigns.current_user.id, %{
diff --git a/services/app/apps/codebattle/test/codebattle/tournament/context_test.exs b/services/app/apps/codebattle/test/codebattle/tournament/context_test.exs
new file mode 100644
index 000000000..71fb4c1fa
--- /dev/null
+++ b/services/app/apps/codebattle/test/codebattle/tournament/context_test.exs
@@ -0,0 +1,337 @@
+defmodule Codebattle.Tournament.ContextTest do
+ use Codebattle.DataCase, async: false
+
+ alias Codebattle.Tournament
+
+ describe "get_upcoming_to_live_candidate/1" do
+ test "returns tournament that starts within the delay window" do
+ # Tournament starting in 5 minutes (within 7 minute window)
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(5, :minute)
+ |> DateTime.truncate(:second)
+
+ tournament =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result.id == tournament.id
+ end
+
+ test "returns tournament that starts exactly at the delay time" do
+ # Tournament starting exactly in 7 minutes
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(7, :minute)
+ |> DateTime.truncate(:second)
+
+ tournament =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result.id == tournament.id
+ end
+
+ test "returns tournament that starts right now" do
+ # Tournament starting right now (edge case)
+ starts_at = DateTime.truncate(DateTime.utc_now(), :second)
+
+ tournament =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result.id == tournament.id
+ end
+
+ test "returns tournament that starts 1 second from now" do
+ # Tournament starting in 1 second (edge case)
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(1, :second)
+ |> DateTime.truncate(:second)
+
+ tournament =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result.id == tournament.id
+ end
+
+ test "returns nil when tournament starts beyond the delay window" do
+ # Tournament starting in 10 minutes (beyond 7 minute window)
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(10, :minute)
+ |> DateTime.truncate(:second)
+
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
+ end
+
+ test "returns nil when tournament starts 1 second beyond delay window" do
+ # Tournament starting 1 second after the delay window
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(7, :minute)
+ |> DateTime.add(1, :second)
+ |> DateTime.truncate(:second)
+
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
+ end
+
+ test "returns nil when tournament already started (in the past)" do
+ # Tournament that should have started 1 minute ago
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(-1, :minute)
+ |> DateTime.truncate(:second)
+
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
+ end
+
+ test "returns nil when tournament started 1 second ago" do
+ # Tournament that started 1 second ago
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(-1, :second)
+ |> DateTime.truncate(:second)
+
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
+ end
+
+ test "ignores open grade tournaments" do
+ # Tournament with open grade should be ignored
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(5, :minute)
+ |> DateTime.truncate(:second)
+
+ insert(:tournament,
+ state: "upcoming",
+ grade: "open",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
+ end
+
+ test "ignores non-upcoming tournaments" do
+ # Tournament in waiting_participants state should be ignored
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(5, :minute)
+ |> DateTime.truncate(:second)
+
+ insert(:tournament,
+ state: "waiting_participants",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
+ end
+
+ test "ignores active tournaments" do
+ # Tournament in active state should be ignored
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(5, :minute)
+ |> DateTime.truncate(:second)
+
+ insert(:tournament,
+ state: "active",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
+ end
+
+ test "ignores finished tournaments" do
+ # Tournament in finished state should be ignored
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(5, :minute)
+ |> DateTime.truncate(:second)
+
+ insert(:tournament,
+ state: "finished",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
+ end
+
+ test "returns the tournament with lowest id when multiple match" do
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(5, :minute)
+ |> DateTime.truncate(:second)
+
+ tournament1 =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ tournament2 =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ # Should return the one with lower id
+ assert result.id == min(tournament1.id, tournament2.id)
+ end
+
+ test "works with different delay windows" do
+ # Test with 5 minute delay
+ starts_at_4_mins =
+ DateTime.utc_now()
+ |> DateTime.add(4, :minute)
+ |> DateTime.truncate(:second)
+
+ tournament =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at_4_mins
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(5)
+ assert result.id == tournament.id
+
+ # Tournament at 6 minutes should not be returned with 5 minute delay
+ starts_at_6_mins =
+ DateTime.utc_now()
+ |> DateTime.add(6, :minute)
+ |> DateTime.truncate(:second)
+
+ insert(:tournament,
+ state: "upcoming",
+ grade: "elementary",
+ starts_at: starts_at_6_mins
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(5)
+ # Should still return the first tournament
+ assert result.id == tournament.id
+ end
+
+ test "handles different tournament grades" do
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(3, :minute)
+ |> DateTime.truncate(:second)
+
+ # Test with different grades (all should work except "open")
+ for grade <- ["rookie", "elementary", "easy", "medium", "hard"] do
+ tournament =
+ insert(:tournament,
+ state: "upcoming",
+ grade: grade,
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result.id == tournament.id
+
+ # Clean up for next iteration
+ Repo.delete(tournament)
+ end
+ end
+
+ test "returns nil when no tournaments exist" do
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
+ end
+
+ test "handles zero delay window" do
+ # Tournament starting right now
+ starts_at = DateTime.truncate(DateTime.utc_now(), :second)
+
+ tournament =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(0)
+ assert result.id == tournament.id
+ end
+
+ test "handles large delay window" do
+ # Tournament starting in 100 minutes
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(100, :minute)
+ |> DateTime.truncate(:second)
+
+ tournament =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(120)
+ assert result.id == tournament.id
+
+ # Should not be returned with smaller window
+ result = Tournament.Context.get_upcoming_to_live_candidate(90)
+ assert result == nil
+ end
+ end
+end
diff --git a/services/app/apps/codebattle/test/codebattle/tournament/entire/swiss_grand_slam_test.exs b/services/app/apps/codebattle/test/codebattle/tournament/entire/swiss_grand_slam_test.exs
index 4fe1f150e..bd8969088 100644
--- a/services/app/apps/codebattle/test/codebattle/tournament/entire/swiss_grand_slam_test.exs
+++ b/services/app/apps/codebattle/test/codebattle/tournament/entire/swiss_grand_slam_test.exs
@@ -13,6 +13,7 @@ defmodule Codebattle.Tournament.Entire.SwissGrandSlamTest do
@decimal100 Decimal.new("100.0")
@decimal0 Decimal.new("0.0")
+ @tag :skip
test "works with player who solved all tasks" do
[%{id: t1_id}, %{id: t2_id}, %{id: t3_id}] = insert_list(3, :task, level: "easy")
insert(:task_pack, name: "tp", task_ids: [t1_id, t2_id, t3_id])
diff --git a/services/app/apps/codebattle/test/codebattle/tournament/upcoming_runner_test.exs b/services/app/apps/codebattle/test/codebattle/tournament/upcoming_runner_test.exs
index 24df46154..3f32ba534 100644
--- a/services/app/apps/codebattle/test/codebattle/tournament/upcoming_runner_test.exs
+++ b/services/app/apps/codebattle/test/codebattle/tournament/upcoming_runner_test.exs
@@ -87,176 +87,149 @@ defmodule Codebattle.Tournament.UpcomingRunnerTest do
end
end
- describe "start_or_cancel_waiting_participants/0" do
- test "starts tournament when it has players and is in waiting_participants state" do
+ describe "get_upcoming_to_live_candidate/1" do
+ test "returns tournament that starts within the delay window" do
+ # Tournament starting in 5 minutes (within 7 minute window)
starts_at =
DateTime.utc_now()
- |> DateTime.add(-5, :minute)
+ |> DateTime.add(5, :minute)
|> DateTime.truncate(:second)
- |> Calendar.strftime("%Y-%m-%dT%H:%M")
-
- user1 = insert(:user)
- user2 = insert(:user)
-
- {:ok, tournament} =
- Tournament.Context.create(%{
- "starts_at" => starts_at,
- "name" => "Test Tournament",
- "user_timezone" => "Etc/UTC",
- "level" => "easy",
- "creator" => user1,
- "break_duration_seconds" => 0,
- "task_provider" => "level",
- "task_strategy" => "random",
- "type" => "swiss",
- "state" => "waiting_participants",
- "players_limit" => 16,
- "grade" => "rookie"
- })
-
- # Add players to the tournament
- Tournament.Server.handle_event(tournament.id, :join, %{users: [user1, user2]})
-
- tournament = Tournament.Context.get(tournament.id)
- assert tournament.state == "waiting_participants"
- assert tournament.players_count == 2
-
- UpcomingRunner.start_or_cancel_waiting_participants()
- updated_tournament = Tournament.Context.get(tournament.id)
- # Tournament should be started
- assert updated_tournament.state in ["active", "finished"]
+ tournament =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result.id == tournament.id
end
- test "cancels tournament when it has no players and is in waiting_participants state" do
+ test "returns tournament that starts exactly at the delay time" do
+ # Tournament starting exactly in 7 minutes
starts_at =
DateTime.utc_now()
- |> DateTime.add(-5, :minute)
+ |> DateTime.add(7, :minute)
|> DateTime.truncate(:second)
- |> Calendar.strftime("%Y-%m-%dT%H:%M")
-
- user = insert(:user)
-
- {:ok, tournament} =
- Tournament.Context.create(%{
- "starts_at" => starts_at,
- "name" => "Test Tournament",
- "user_timezone" => "Etc/UTC",
- "level" => "easy",
- "creator" => user,
- "break_duration_seconds" => 0,
- "task_provider" => "level",
- "task_strategy" => "random",
- "type" => "swiss",
- "state" => "waiting_participants",
- "players_limit" => 16,
- "grade" => "rookie"
- })
-
- assert tournament.state == "waiting_participants"
- assert tournament.players_count == 0
-
- UpcomingRunner.start_or_cancel_waiting_participants()
- updated_tournament = Tournament.Context.get(tournament.id)
- assert updated_tournament.state == "canceled"
+ tournament =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result.id == tournament.id
end
- test "does not process open grade tournaments" do
+ test "returns tournament that starts right now" do
+ # Tournament starting right now (edge case)
+ starts_at = DateTime.truncate(DateTime.utc_now(), :second)
+
+ tournament =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result.id == tournament.id
+ end
+
+ test "returns nil when tournament starts beyond the delay window" do
+ # Tournament starting in 10 minutes (beyond 7 minute window)
starts_at =
DateTime.utc_now()
- |> DateTime.add(-5, :minute)
+ |> DateTime.add(10, :minute)
|> DateTime.truncate(:second)
- |> Calendar.strftime("%Y-%m-%dT%H:%M")
-
- user = insert(:user)
-
- {:ok, tournament} =
- Tournament.Context.create(%{
- "starts_at" => starts_at,
- "name" => "Test Tournament",
- "user_timezone" => "Etc/UTC",
- "level" => "easy",
- "creator" => user,
- "break_duration_seconds" => 0,
- "task_provider" => "level",
- "task_strategy" => "random",
- "type" => "swiss",
- "state" => "waiting_participants",
- "players_limit" => 16,
- "grade" => "open"
- })
-
- assert tournament.state == "waiting_participants"
- assert tournament.players_count == 0
-
- UpcomingRunner.start_or_cancel_waiting_participants()
-
- # Open grade tournaments should not be auto-canceled
- updated_tournament = Tournament.Context.get(tournament.id)
- assert updated_tournament.state == "waiting_participants"
+
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
end
- test "processes multiple tournaments correctly" do
+ test "returns nil when tournament already started (in the past)" do
+ # Tournament that should have started 1 minute ago
starts_at =
DateTime.utc_now()
- |> DateTime.add(-5, :minute)
+ |> DateTime.add(-1, :minute)
|> DateTime.truncate(:second)
- |> Calendar.strftime("%Y-%m-%dT%H:%M")
-
- user1 = insert(:user)
- user2 = insert(:user)
-
- # Tournament with players - should start
- {:ok, tournament_with_players} =
- Tournament.Context.create(%{
- "starts_at" => starts_at,
- "name" => "Tournament With Players",
- "user_timezone" => "Etc/UTC",
- "level" => "easy",
- "creator" => user1,
- "break_duration_seconds" => 0,
- "task_provider" => "level",
- "task_strategy" => "random",
- "type" => "swiss",
- "state" => "waiting_participants",
- "players_limit" => 16,
- "grade" => "rookie"
- })
-
- Tournament.Server.handle_event(tournament_with_players.id, :join, %{users: [user1]})
-
- # Tournament without players - should cancel
- {:ok, tournament_without_players} =
- Tournament.Context.create(%{
- "starts_at" => starts_at,
- "name" => "Tournament Without Players",
- "user_timezone" => "Etc/UTC",
- "level" => "easy",
- "creator" => user2,
- "break_duration_seconds" => 0,
- "task_provider" => "level",
- "task_strategy" => "random",
- "type" => "swiss",
- "state" => "waiting_participants",
- "players_limit" => 16,
- "grade" => "rookie"
- })
-
- UpcomingRunner.start_or_cancel_waiting_participants()
-
- tournament1 = Tournament.Context.get(tournament_with_players.id)
- tournament2 = Tournament.Context.get(tournament_without_players.id)
-
- # Tournament with players should be started
- assert tournament1.state in ["active", "finished"]
-
- # Tournament without players should be canceled
- assert tournament2.state == "canceled"
+
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
+ end
+
+ test "ignores open grade tournaments" do
+ # Tournament with open grade should be ignored
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(5, :minute)
+ |> DateTime.truncate(:second)
+
+ insert(:tournament,
+ state: "upcoming",
+ grade: "open",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
+ end
+
+ test "ignores non-upcoming tournaments" do
+ # Tournament in waiting_participants state should be ignored
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(5, :minute)
+ |> DateTime.truncate(:second)
+
+ insert(:tournament,
+ state: "waiting_participants",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ assert result == nil
end
- test "returns :ok after processing" do
- assert UpcomingRunner.start_or_cancel_waiting_participants() == :ok
+ test "returns the tournament with lowest id when multiple match" do
+ starts_at =
+ DateTime.utc_now()
+ |> DateTime.add(5, :minute)
+ |> DateTime.truncate(:second)
+
+ tournament1 =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ tournament2 =
+ insert(:tournament,
+ state: "upcoming",
+ grade: "rookie",
+ starts_at: starts_at
+ )
+
+ result = Tournament.Context.get_upcoming_to_live_candidate(7)
+ # Should return the one with lower id
+ assert result.id == min(tournament1.id, tournament2.id)
end
end
diff --git a/services/app/config/dev.exs b/services/app/config/dev.exs
index beba7acb9..441bea0ed 100644
--- a/services/app/config/dev.exs
+++ b/services/app/config/dev.exs
@@ -54,6 +54,7 @@ config :codebattle, CodebattleWeb.Endpoint,
]
]
+config :codebattle, :tournament_run_upcoming, true
config :codebattle, asserts_executor: Local
config :codebattle, checker_executor: Local
|