diff --git a/services/app/apps/codebattle/lib/codebattle/application.ex b/services/app/apps/codebattle/lib/codebattle/application.ex index fbca272b5..0a76f96af 100644 --- a/services/app/apps/codebattle/lib/codebattle/application.ex +++ b/services/app/apps/codebattle/lib/codebattle/application.ex @@ -39,6 +39,7 @@ defmodule Codebattle.Application do children = [ {ChromicPDF, chromic_pdf_opts()}, + {Codebattle.ImageCache, []}, {Codebattle.Repo, []}, {Registry, keys: :unique, name: Codebattle.Registry}, CodebattleWeb.Telemetry, diff --git a/services/app/apps/codebattle/lib/codebattle/image_cache.ex b/services/app/apps/codebattle/lib/codebattle/image_cache.ex new file mode 100644 index 000000000..a4a26ba92 --- /dev/null +++ b/services/app/apps/codebattle/lib/codebattle/image_cache.ex @@ -0,0 +1,59 @@ +defmodule Codebattle.ImageCache do + @moduledoc false + use GenServer + + # 2 hours in milliseconds + @cleanup_interval 2 * 60 * 60 * 1000 + + def start_link(_opts) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + def init(:ok) do + create_table() + schedule_cleanup() + {:ok, %{}} + end + + def create_table do + :ets.new( + :html_images, + [ + :set, + :public, + :named_table, + {:write_concurrency, true}, + {:read_concurrency, true} + ] + ) + end + + def put_image(cache_key, image) do + :ets.insert(:html_images, {cache_key, image}) + :ok + end + + def get_image(cache_key) do + case :ets.lookup(:html_images, cache_key) do + [{^cache_key, cached_image}] -> + cached_image + + [] -> + nil + end + end + + def clean_table do + :ets.delete_all_objects(:html_images) + end + + def handle_info(:cleanup, state) do + clean_table() + schedule_cleanup() + {:noreply, state} + end + + defp schedule_cleanup do + Process.send_after(self(), :cleanup, @cleanup_interval) + end +end diff --git a/services/app/apps/codebattle/lib/codebattle_web/controllers/game/image_controller.ex b/services/app/apps/codebattle/lib/codebattle_web/controllers/game/image_controller.ex index 78ef54b08..8cc39278d 100644 --- a/services/app/apps/codebattle/lib/codebattle_web/controllers/game/image_controller.ex +++ b/services/app/apps/codebattle/lib/codebattle_web/controllers/game/image_controller.ex @@ -1,83 +1,159 @@ defmodule CodebattleWeb.Game.ImageController do use CodebattleWeb, :controller + use Gettext, backend: CodebattleWeb.Gettext alias Codebattle.Game.Context - - @fake_html_to_image Application.compile_env(:codebattle, :fake_html_to_image, false) + alias Codebattle.Game.Player + alias CodebattleWeb.HtmlImage def show(conn, %{"game_id" => id}) do case Context.fetch_game(id) do {:ok, game} -> - {:ok, image} = - game - |> prepare_image_html() - |> generate_png() - - conn - |> put_resp_content_type("image/png") - |> send_resp(200, image) + cache_key = "g_#{id}_#{game.state}" + html = prepare_image_html(game) + HtmlImage.render_image(conn, cache_key, html) - {:error, reason} -> - conn - |> put_status(:not_found) - |> json(%{error: inspect(reason)}) + {:error, _reason} -> + send_resp(conn, :ok, "") end end defp prepare_image_html(game) do """ - -
- Logo - The Codebattle - #{render_content(game)} -

Made with by CodebattleCoreTeam

-

Dear frontenders, pls, make it prettier, thx

-
+ + + + + + +
+
+ Logo +
+
+ #{render_content(game)} +
+ +
+ """ end - defp render_content(%{players: []}) do - """ -

Codebattle game

- """ - end + defp render_content(game) do + level = Gettext.gettext(CodebattleWeb.Gettext, "Level: #{game.level}") + state = Gettext.gettext(CodebattleWeb.Gettext, "Game state: #{game.state}") - defp render_content(%{players: [player1]} = game) do - """ -

Game state: #{game.state}

-

Level: #{game.level}

- #{render_player(player1)} - """ - end + # If you always have exactly two players: + [player1, player2] = + case game.players do + [p1, p2] -> [p1, p2] + [p1] -> [p1, %Player{}] + [] -> [%Player{}, %Player{}] + end - defp render_content(%{players: [player1, player2]} = game) do """ -

Game state: #{game.state}

-

Level: #{game.level}

- #{render_player(player1)} - VS - #{render_player(player2)} +
+
+ +
+ #{render_player(player1)} +
+ + +
+ VS +
+ + +
+ #{render_player(player2)} +
+
+

#{state}

+

#{level}

+
""" end defp render_player(player) do + result = Gettext.gettext(CodebattleWeb.Gettext, "#{player.result}") + """ -
-
- -

@#{player.name} (#{player.lang}) - #{player.rating}

-
+
+ +

@#{player.name}(#{player.rating}) - #{player.lang}

+

#{result}

""" end - - defp generate_png(html_content) do - if @fake_html_to_image do - {:ok, html_content} - else - ChromicPDF.capture_screenshot({:html, html_content}, capture_screenshot: %{format: "png"}) - end - end end diff --git a/services/app/apps/codebattle/lib/codebattle_web/controllers/game_controller.ex b/services/app/apps/codebattle/lib/codebattle_web/controllers/game_controller.ex index 926234110..d27ab7c66 100644 --- a/services/app/apps/codebattle/lib/codebattle_web/controllers/game_controller.ex +++ b/services/app/apps/codebattle/lib/codebattle_web/controllers/game_controller.ex @@ -39,37 +39,18 @@ defmodule CodebattleWeb.GameController do case {game.state, is_player} do {"waiting_opponent", false} -> - player = Helpers.get_first_player(game) - conn - |> put_meta_tags(%{ - title: "Hexlet Codebattle • Join game", - description: "Game against #{player_info(player, game)}", - url: Routes.game_url(conn, :show, id, level: Helpers.get_level(game)), - image: Routes.game_image_url(conn, :show, id), - twitter: get_twitter_labels_meta([player]) - }) + |> put_game_meta_tags(game) |> render("join.html", %{game: game, user: user}) _ -> - first = Helpers.get_first_player(game) - second = Helpers.get_second_player(game) - conn - |> put_meta_tags(%{ - title: "Hexlet Codebattle • Cool game", - description: "#{player_info(first, game)} vs #{player_info(second, game)}", - url: Routes.game_url(conn, :show, id), - image: Routes.game_image_url(conn, :show, id), - twitter: get_twitter_labels_meta([first, second]) - }) + |> put_game_meta_tags(game) |> render("show.html", %{game: game, user: user}) end game -> - if Playbook.Context.exists?(game.id) && can_see_game(user, game) do - [first, second] = get_users(game) - + if Playbook.Context.exists?(game.id) && can_see_history_game?(user, game) do score = Context.fetch_score_by_game_id(game.id) game_params = @@ -87,22 +68,11 @@ defmodule CodebattleWeb.GameController do langs: Languages.get_langs(), players: present_users_for_gon(game.users) ) - |> put_meta_tags(%{ - title: "Hexlet Codebattle • Cool archived game", - description: "#{user_info(first)} vs #{user_info(second)}", - url: Routes.game_url(conn, :show, id), - image: Routes.game_image_url(conn, :show, id), - twitter: get_twitter_labels_meta(game.users) - }) + |> put_game_meta_tags(game) |> render("show.html", %{game: game, user: user}) else conn - |> put_meta_tags(%{ - title: "Hexlet Codebattle • Game Result", - description: "Game is over", - image: Routes.game_image_url(conn, :show, id), - url: Routes.game_url(conn, :show, id) - }) + |> put_game_meta_tags(game) |> render("game_result.html", %{game: game, user: user}) end end @@ -138,37 +108,6 @@ defmodule CodebattleWeb.GameController do end end - defp user_info(user), do: "@#{user.name}(#{user.lang})-#{user.rating}" - - defp player_info(nil, _game), do: "" - - defp player_info(player, game) do - "@#{player.name}(#{player.lang})-#{player.rating} level:#{Helpers.get_level(game)}" - end - - defp get_twitter_labels_meta(players) do - players - |> Enum.with_index(1) - |> Enum.reduce(%{}, fn - {nil, _i}, acc -> - acc - - {player, i}, acc -> - label = player.name - data = "#{player.rating} - #{player.lang}" - - acc |> Map.put("label#{i}", label) |> Map.put("data#{i}", data) - end) - end - - defp get_users(game) do - case Enum.count(game.users) do - 0 -> [User.build_guest(), User.build_guest()] - 1 -> game.users ++ [User.build_guest()] - _ -> game.users - end - end - defp present_users_for_gon(users) do Enum.map( users, @@ -186,14 +125,14 @@ defmodule CodebattleWeb.GameController do ) end - defp can_see_game(%{subscription_type: :admin}, _game), do: true + defp can_see_history_game?(%{subscription_type: :admin}, _game), do: true - defp can_see_game(%{subscription_type: :premium} = user, game) do - [first, second] = get_users(game) - user.id == first.id || user.id == second.id + # defp can_see_game?(%{subscription_type: :premium} = user, game) do + defp can_see_history_game?(user, game) do + Enum.any?(game.players, &(&1.id == user.id)) end - defp can_see_game(_user, _game), do: false + # defp can_see_history_game?(_user, _game), do: false defp maybe_get_reports(user, game_id) do if User.admin?(user) do @@ -206,4 +145,53 @@ defmodule CodebattleWeb.GameController do defp jitsi_api_key do Application.get_env(:codebattle, :jitsi_api_key) end + + defp put_game_meta_tags(conn, game) do + put_meta_tags(conn, %{ + title: Application.get_env(:codebattle, :app_title), + description: game_meta_description(game), + url: Routes.game_url(conn, :show, game.id, level: Helpers.get_level(game)), + image: Routes.game_image_url(conn, :show, game.id), + twitter: get_twitter_labels_meta(game.players) + }) + end + + defp get_twitter_labels_meta(players) do + players + |> Enum.with_index(1) + |> Enum.reduce(%{}, fn + {nil, _i}, acc -> + acc + + {player, i}, acc -> + label = player.name + data = "#{player.rating} - #{player.lang}" + + acc |> Map.put("label#{i}", label) |> Map.put("data#{i}", data) + end) + end + + defp game_meta_description(%{state: "waiting_opponent", players: [player]} = game) do + level = Gettext.gettext(CodebattleWeb.Gettext, "Level: #{game.level}") + + gettext("Play with") <> + ": " <> + "@#{player.name}(#{player.rating})-#{player.lang}" <> + ". " <> + gettext("Waiting for an opponent") <> ". " <> level + end + + defp game_meta_description(%{players: [player1, player2]} = game) do + level = Gettext.gettext(CodebattleWeb.Gettext, "Level: #{game.level}") + state = Gettext.gettext(CodebattleWeb.Gettext, "Game state: #{game.state}") + + gettext("Game between") <> + ": " <> + "@#{player1.name}(#{player1.rating})-#{player1.lang}" <> + " VS " <> + "@#{player2.name}(#{player2.rating})-#{player2.lang}" <> + ". " <> level <> ". " <> state + end + + defp game_meta_description(_game), do: "Unknown game" end diff --git a/services/app/apps/codebattle/lib/codebattle_web/controllers/tournament/image_controller.ex b/services/app/apps/codebattle/lib/codebattle_web/controllers/tournament/image_controller.ex index a23e02ac9..0b93bb3ff 100644 --- a/services/app/apps/codebattle/lib/codebattle_web/controllers/tournament/image_controller.ex +++ b/services/app/apps/codebattle/lib/codebattle_web/controllers/tournament/image_controller.ex @@ -3,24 +3,21 @@ defmodule CodebattleWeb.Tournament.ImageController do use Gettext, backend: CodebattleWeb.Gettext alias Codebattle.Tournament + alias CodebattleWeb.HtmlImage def show(conn, %{"id" => id}) do - # TODO: add ETS cache for image case Tournament.Context.get(id) do nil -> send_resp(conn, :ok, "") tournament -> - html_content = render_image(tournament) - {:ok, image} = generate_png(html_content) - - conn - |> put_resp_content_type("image/png") - |> send_resp(200, Base.decode64!(image)) + cache_key = "t_#{id}_#{tournament.state}" + html = prepare_image_html(tournament) + HtmlImage.render_image(conn, cache_key, html) end end - defp render_image(tournament) do + defp prepare_image_html(tournament) do """ @@ -29,14 +26,16 @@ defmodule CodebattleWeb.Tournament.ImageController do html, body { margin: 0; padding: 0; - /* Let them expand to the “browser” size (which we'll define via ChromicPDF) */ width: 100%; height: 100%; background: #f5f7fa; - font-family: 'Helvetica Neue', Arial, sans-serif; /* Change to desired font */ + font-family: 'Helvetica Neue', Arial, sans-serif; + } + p { + margin: 5px; + padding: 5px; } .card { - /* Fill the entire viewport */ width: 100%; height: 100%; background: #ffffff; @@ -47,14 +46,14 @@ defmodule CodebattleWeb.Tournament.ImageController do flex-direction: column; } .header { - background: linear-gradient(135deg, #667eea, #764ba2); - background: #000; /* Dark background */ - font-family: 'Helvetica Neue', Arial, sans-serif; /* Change to desired font */ - color: #fff; /* White text */ + background: #000; + font-family: 'Helvetica Neue', Arial, sans-serif; + color: #fff; text-align: center; } .header img { - width: 60px; + width: 100px; + margin-top: 20px; height: auto; } .content { @@ -68,24 +67,33 @@ defmodule CodebattleWeb.Tournament.ImageController do } .footer { text-align: center; - font-size: 10px; - color: #aaa; - padding: 5px; - background: #f0f0f0; + font-size: 12px; + color: #fff; + padding: 8px; + background: #000; }
- Logo + Logo

#{tournament.name}

#{render_content(tournament)}
@@ -93,26 +101,14 @@ defmodule CodebattleWeb.Tournament.ImageController do """ end - defp logo_url do - if logo = Application.get_env(:codebattle, :collab_logo) do - logo - else - "https://codebattle.hexlet.io/assets/images/logo.svg" - end - end - defp render_content(tournament) do - type = to_string(tournament.type) - state = to_string(tournament.state) + type = Gettext.gettext(CodebattleWeb.Gettext, to_string(tournament.type)) + state = Gettext.gettext(CodebattleWeb.Gettext, "Tournament #{tournament.state}") """ -

#{gettext("Type: %{type}", type: type)}

-

#{gettext("State: %{state}", state: state)}

-

#{gettext("Starts At")}: #{tournament.starts_at} UTC

+

#{gettext("Type: %{type}", type: type)}

+

#{state}

+

#{gettext("Starts At")}: #{tournament.starts_at} UTC

""" end - - defp generate_png(html_content) do - ChromicPDF.capture_screenshot({:html, html_content}, capture_screenshot: %{format: "png"}) - end end diff --git a/services/app/apps/codebattle/lib/codebattle_web/controllers/tournament_controller.ex b/services/app/apps/codebattle/lib/codebattle_web/controllers/tournament_controller.ex index b3e1554cc..eb04a7c23 100644 --- a/services/app/apps/codebattle/lib/codebattle_web/controllers/tournament_controller.ex +++ b/services/app/apps/codebattle/lib/codebattle_web/controllers/tournament_controller.ex @@ -8,45 +8,11 @@ defmodule CodebattleWeb.TournamentController do def index(conn, _params) do current_user = conn.assigns[:current_user] - conn - |> put_meta_tags(%{ - title: "Hexlet Codebattle • Tournaments", - description: - "Create or join nice tournaments, have fun with your teammates! You can play `Frontend vs Backend` or `Ruby vs Js`", - url: Routes.tournament_url(conn, :index) - }) - |> live_render(CodebattleWeb.Live.Tournament.IndexView, - session: %{ - "current_user" => current_user, - "tournaments" => Tournament.Context.list_live_and_finished(current_user) - } + live_render(conn, CodebattleWeb.Live.Tournament.IndexView, + session: %{"current_user" => current_user, "tournaments" => Tournament.Context.list_live_and_finished(current_user)} ) end - def admin(conn, params) do - current_user = conn.assigns[:current_user] - tournament = Tournament.Context.get!(params["id"]) - - if Tournament.Helpers.can_access?(tournament, current_user, params) do - conn - |> put_view(CodebattleWeb.TournamentView) - |> put_meta_tags(%{ - title: "Hexlet Codebattle • Join tournament", - description: "Join tournament: #{String.slice(tournament.name, 0, 100)}", - image: Routes.tournament_image_url(conn, :show, tournament.id), - url: Routes.tournament_url(conn, :show, tournament.id) - }) - |> put_gon(tournament_id: params["id"]) - |> put_gon(event_id: tournament.event_id) - |> render("show.html") - else - conn - |> put_status(:not_found) - |> put_view(CodebattleWeb.ErrorView) - |> render("404.html", %{msg: gettext("Tournament not found")}) - end - end - def show(conn, params) do current_user = conn.assigns[:current_user] tournament = Tournament.Context.get!(params["id"]) @@ -55,8 +21,8 @@ defmodule CodebattleWeb.TournamentController do conn |> put_view(CodebattleWeb.TournamentView) |> put_meta_tags(%{ - title: "Hexlet Codebattle • Join tournament", - description: "Join tournament: #{String.slice(tournament.name, 0, 100)}", + title: tournament.name, + description: tournament.description, image: Routes.tournament_image_url(conn, :show, tournament.id), url: Routes.tournament_url(conn, :show, tournament.id) }) diff --git a/services/app/apps/codebattle/lib/codebattle_web/html_image.ex b/services/app/apps/codebattle/lib/codebattle_web/html_image.ex new file mode 100644 index 000000000..f525b7363 --- /dev/null +++ b/services/app/apps/codebattle/lib/codebattle_web/html_image.ex @@ -0,0 +1,52 @@ +defmodule CodebattleWeb.HtmlImage do + @moduledoc false + import Plug.Conn + + @fake_html_to_image Application.compile_env(:codebattle, :fake_html_to_image, false) + + @doc """ + Renders an image from the given HTML content. It first checks the cache (using the provided cache_key); + if not found, it generates a PNG from the HTML, caches it, and sends it in the response. + """ + def render_image(conn, cache_key, html_content) do + image = + case Codebattle.ImageCache.get_image(cache_key) do + nil -> + new_image = generate_png(html_content) + Codebattle.ImageCache.put_image(cache_key, new_image) + new_image + + image -> + image + end + + conn + |> put_resp_content_type("image/png") + |> send_resp(200, image) + end + + @doc """ + Generates a PNG image from the given HTML content. + If the fake HTML-to-image mode is enabled, it returns the HTML content instead. + """ + def generate_png(html_content) do + if @fake_html_to_image do + html_content + else + {:html, html_content} + |> ChromicPDF.capture_screenshot(capture_screenshot: %{format: "png"}) + |> then(fn {:ok, image} -> Base.decode64!(image) end) + end + end + + @doc """ + Returns the logo URL based on configuration. + """ + def logo_url do + if logo = Application.get_env(:codebattle, :collab_logo) do + logo + else + "https://codebattle.hexlet.io/assets/images/logo.svg" + end + end +end diff --git a/services/app/apps/codebattle/lib/codebattle_web/templates/layout/app.html.heex b/services/app/apps/codebattle/lib/codebattle_web/templates/layout/app.html.heex index df24c0746..9bd7e86ba 100644 --- a/services/app/apps/codebattle/lib/codebattle_web/templates/layout/app.html.heex +++ b/services/app/apps/codebattle/lib/codebattle_web/templates/layout/app.html.heex @@ -97,8 +97,9 @@ src="/assets/images/logo.svg" />
- Codebattle - by Hexlet’s community + + <%= Application.get_env(:codebattle, :app_title) %> +
<%= unless @current_user.is_guest do %> diff --git a/services/app/apps/codebattle/priv/gettext/ru/LC_MESSAGES/default.po b/services/app/apps/codebattle/priv/gettext/ru/LC_MESSAGES/default.po index 7144f9e0b..47c8ba46c 100644 --- a/services/app/apps/codebattle/priv/gettext/ru/LC_MESSAGES/default.po +++ b/services/app/apps/codebattle/priv/gettext/ru/LC_MESSAGES/default.po @@ -245,6 +245,9 @@ msgstr "Задачи" msgid "Tournaments" msgstr "Турниры" +msgid "Tournament" +msgstr "Турнир" + #, elixir-format msgid "Join Discord" msgstr "Чат Discord" @@ -786,3 +789,99 @@ msgstr "Связь с сервером потеряна, перезагрузи msgid "Show full tournament table" msgstr "Показать полную турнирную таблицу" + +msgid "Type: %{type}" +msgstr "Тип: %{type}" + +msgid "swiss" +msgstr "Швейцарская система" + +msgid "individual" +msgstr "Олимпийская система" + +msgid "team" +msgstr "Командный турнир" + +msgid "arena" +msgstr "Арена" + +msgid "Tournament waiting_participants" +msgstr "Турнир скоро начнется" + +msgid "Tournament canceled" +msgstr "Турнир отменен" + +msgid "Tournament active" +msgstr "Турнир активен" + +msgid "Tournament finished" +msgstr "Турнир завершен" + +msgid "Starts At" +msgstr "Начало" + +msgid "Game playing" +msgstr "Игра продолжается" + +msgid "Game waiting_opponent" +msgstr "Ожидание соперника" + +msgid "Game state: timeout" +msgstr "Статус: время истекло" + +msgid "Game state: canceled" +msgstr "Статус: игра отменена" + +msgid "Game state: game_over" +msgstr "Статус: игра окончена" + +msgid "Game state: initial" +msgstr "Статус: игра создается" + +msgid "Game state: builder" +msgstr "Статус: игра готовится к запуску" + +msgid "Game state: waiting_opponent" +msgstr "Статус: ожидание соперника" + +msgid "Game state: playing" +msgstr "Статус: игра идет" + +msgid "Level: elementary" +msgstr "Уровень: элементарный" + +msgid "Level: easy" +msgstr "Уровень: легкий" + +msgid "Level: medium" +msgstr "Уровень: средний" + +msgid "Level: hard" +msgstr "Уровень: сложный" + +msgid "Timeout: %{sec} seconds" +msgstr "Время на задачу: %{sec} секунд" + +msgid "undefined" +msgstr "не определено" + +msgid "won" +msgstr "победа" + +msgid "lost" +msgstr "поражение" + +msgid "gave_up" +msgstr "сдался" + +msgid "timeout" +msgstr "время вышло" + +msgid "Game between" +msgstr "Игра между" + +msgid "Game level" +msgstr "Уровень сложности" + +msgid "Play with" +msgstr "Играть с" diff --git a/services/app/apps/codebattle/test/codebattle_web/controllers/image_controller_test.exs b/services/app/apps/codebattle/test/codebattle_web/controllers/image_controller_test.exs index 72b21ddcc..bdc5b179c 100644 --- a/services/app/apps/codebattle/test/codebattle_web/controllers/image_controller_test.exs +++ b/services/app/apps/codebattle/test/codebattle_web/controllers/image_controller_test.exs @@ -37,15 +37,14 @@ defmodule CodebattleWeb.ImageControllerTest do conn = get(conn, Routes.game_image_path(conn, :show, game.id)) assert conn.status == 200 - assert conn.resp_body =~ "game" end - test "returns 404 withot game", %{conn: conn} do + test "returns empty 200 without a game", %{conn: conn} do response = conn |> get(Routes.game_image_path(conn, :show, 1_000_001)) - |> json_response(404) + |> response(200) - assert response == %{"error" => ":not_found"} + assert response == "" end end diff --git a/services/app/config/config.exs b/services/app/config/config.exs index 488d45610..f440b1a5a 100644 --- a/services/app/config/config.exs +++ b/services/app/config/config.exs @@ -34,6 +34,7 @@ config :codebattle, CodebattleWeb.Gettext, default_locale: "en" config :codebattle, :api_key, "x-key" +config :codebattle, :app_title, "Hexlet Codebattle" config :codebattle, :fake_html_to_image, true config :codebattle, :firebase, diff --git a/services/app/config/releases.exs b/services/app/config/releases.exs index d370d9f11..5831601b5 100644 --- a/services/app/config/releases.exs +++ b/services/app/config/releases.exs @@ -64,6 +64,7 @@ config :codebattle, CodebattleWeb.Endpoint, server: true config :codebattle, :api_key, System.get_env("CODEBATTLE_API_AUTH_KEY") +config :codebattle, :app_title, System.get_env("CODEBATTLE_APP_TITLE", "Hexlet Codebattle") config :codebattle, :firebase, sender_id: System.get_env("FIREBASE_SENDER_ID"), diff --git a/services/app/config/test.exs b/services/app/config/test.exs index d9d48a16a..248c8f50a 100644 --- a/services/app/config/test.exs +++ b/services/app/config/test.exs @@ -15,6 +15,8 @@ asserts_executor = _ -> Codebattle.AssertsService.Executor.Fake end +config :codebattle, ChromicPDF, on_demand: true + config :codebattle, Codebattle.Bot, timeout: 60_000, min_bot_step_timeout: 0