From cd3e27dadd10e47b4c7da6fd00fedce27b24e3e4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20Schr=C3=B6der?=
 <fschroeder@techfak.uni-bielefeld.de>
Date: Fri, 8 Mar 2024 16:45:07 +0100
Subject: [PATCH] Refactor game and study server scripts, update tests, improve
 error handling

The refactoring involved creating a new function to handle game server requests for cleaner and more concise code. Changes were also made to the formatting of commit messages for better readability. Invalid game environment configurations are now dealt with accordingly to prevent game errors. Several updates were made to the tests to ensure they accurately measure the performance and functionality of the game.
---
 README.md                                     |   4 +-
 cooperative_cuisine/__init__.py               |   4 +-
 .../configs/environment_config.yaml           |   8 +-
 cooperative_cuisine/game_server.py            |  16 +-
 cooperative_cuisine/study_server.py           |  39 ++-
 cooperative_cuisine/utils.py                  |  16 +-
 setup.py                                      |   2 +-
 tests/test_game_server.py                     | 284 ++++++++++++++++++
 tests/test_study_server.py                    |  95 ++++++
 tests/test_utils.py                           |  46 +++
 10 files changed, 484 insertions(+), 30 deletions(-)
 create mode 100644 tests/test_game_server.py
 create mode 100644 tests/test_study_server.py
 create mode 100644 tests/test_utils.py

diff --git a/README.md b/README.md
index ce6b0966..41178079 100644
--- a/README.md
+++ b/README.md
@@ -46,9 +46,9 @@ You can also start the **Game Server**m **Study Server** (Matchmaking),and the *
 terminals.
 
 ```bash
-python3 cooperative_cuisine/game_server.py -g localhost -gp 8000 --manager_ids SECRETKEY1 SECRETKEY2
+python3 cooperative_cuisine/game_server.py -g localhost -gp 8000 --manager-ids SECRETKEY1 SECRETKEY2
 
-python3 cooperative_cuisine/study_server.py -s localhost -sp 8080 --manager_ids SECRETKEY1
+python3 cooperative_cuisine/study_server.py -s localhost -sp 8080 --manager-ids SECRETKEY1
 
 python3 cooperative_cuisine/pygame_2d_vis/gui.py -s localhost -sp 8080 -g localhost -gp 8000
 ```
diff --git a/cooperative_cuisine/__init__.py b/cooperative_cuisine/__init__.py
index a8f49b41..a0577c01 100644
--- a/cooperative_cuisine/__init__.py
+++ b/cooperative_cuisine/__init__.py
@@ -49,9 +49,9 @@ cooperative_cuisine  -s localhost -sp 8080 -g localhost -gp 8000
 You can also start the **Game Server**, **Study Server** (Matchmaking),and the **PyGame GUI** individually in different terminals.
 
 ```bash
-python3 cooperative_cuisine/game_server.py -g localhost -gp 8000 --manager_ids SECRETKEY1 SECRETKEY2
+python3 cooperative_cuisine/game_server.py -g localhost -gp 8000 --manager-ids SECRETKEY1 SECRETKEY2
 
-python3 cooperative_cuisine/study_server.py -s localhost -sp 8080 --manager_ids SECRETKEY1
+python3 cooperative_cuisine/study_server.py -s localhost -sp 8080 --manager-ids SECRETKEY1
 
 python3 cooperative_cuisine/pygame_2d_vis/gui.py -s localhost -sp 8080 -g localhost -gp 8000
 ```
diff --git a/cooperative_cuisine/configs/environment_config.yaml b/cooperative_cuisine/configs/environment_config.yaml
index 608eb972..a6c7c752 100644
--- a/cooperative_cuisine/configs/environment_config.yaml
+++ b/cooperative_cuisine/configs/environment_config.yaml
@@ -202,7 +202,13 @@ extra_setup_functions:
         log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
         add_hook_ref: true
 
-
+  empty_info_msg:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ action_put ]
+      callback_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager ''
+      callback_class_kwargs:
+        msg: ""
 #  info_msg:
 #    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
 #    kwargs:
diff --git a/cooperative_cuisine/game_server.py b/cooperative_cuisine/game_server.py
index d27db578..9f4271d6 100644
--- a/cooperative_cuisine/game_server.py
+++ b/cooperative_cuisine/game_server.py
@@ -159,6 +159,11 @@ class EnvironmentHandler:
             return 1
         env_id = uuid.uuid4().hex
 
+        if environment_config.number_players < 1:
+            raise HTTPException(
+                status_code=409, detail="Number players need to be positive."
+            )
+
         env = Environment(
             env_config=environment_config.environment_config,
             layout_config=environment_config.layout_config,
@@ -322,10 +327,9 @@ class EnvironmentHandler:
         if (
             manager_id in self.manager_envs
             and env_id in self.manager_envs[manager_id]
-            and self.envs[env_id].status
-            not in [EnvironmentStatus.STOPPED, EnvironmentStatus.PAUSED]
+            and self.envs[env_id].status == EnvironmentStatus.PAUSED
         ):
-            self.envs[env_id].status = EnvironmentStatus.PAUSED
+            self.envs[env_id].status = EnvironmentStatus.RUNNING
             self.envs[env_id].last_step_time = time.time_ns()
 
     def stop_env(self, manager_id: str, env_id: str, reason: str) -> int:
@@ -676,7 +680,9 @@ def manage_websocket_message(message: str, client_id: str) -> PlayerRequestResul
                     "player_hash": ws_message.player_hash,
                 }
             case PlayerRequestType.ACTION:
-                assert ws_message.action is not None
+                assert (
+                    ws_message.action is not None
+                ), "websocket msg type action needs field action filled"
                 if isinstance(ws_message.action.action_data, list):
                     ws_message.action.action_data = np.array(
                         ws_message.action.action_data, dtype=float
@@ -714,7 +720,7 @@ def manage_websocket_message(message: str, client_id: str) -> PlayerRequestResul
 
 @app.get("/")
 def read_root():
-    return {"OVER": "COOKED"}
+    return {"Cooperative": "Cuisine"}
 
 
 class CreateEnvironmentConfig(BaseModel):
diff --git a/cooperative_cuisine/study_server.py b/cooperative_cuisine/study_server.py
index ba95e28e..2f794f0e 100644
--- a/cooperative_cuisine/study_server.py
+++ b/cooperative_cuisine/study_server.py
@@ -4,7 +4,7 @@
 - Run this script. Copy the manager id that is printed
 - Run the game_server.py script with the manager id copied from the terminal
 ```
-python game_server.py --manager_ids COPIED_UUID
+python game_server.py --manager-ids COPIED_UUID
 ```
 - Run 2 gui.py scripts in different terminals. For more players change `NUMBER_PLAYER_PER_ENV` and start more guis.
 
@@ -50,6 +50,10 @@ USE_AAAMBOS_AGENT = False
 """Use the aaambos random agents instead of the simpler python script agents."""
 
 
+def request_game_server(game_server: str, json_data: dict):
+    return requests.post(game_server, json=json_data)
+
+
 class LevelConfig(BaseModel):
     """Configuration of a level in the study."""
 
@@ -190,8 +194,9 @@ class Study:
             seed=seed,
         ).model_dump(mode="json")
 
-        env_info = requests.post(
-            study_manager.game_server_url + "/manage/create_env/", json=creation_json
+        env_info = request_game_server(
+            study_manager.game_server_url + "/manage/create_env/",
+            json_data=creation_json,
         )
 
         if env_info.status_code == 403:
@@ -217,9 +222,9 @@ class Study:
         """Stops the last environment, starts the next one and
         remaps the participants to the new player infos.
         """
-        requests.post(
+        request_game_server(
             f"{study_manager.game_server_url}/manage/stop_env/",
-            json={
+            json_data={
                 "manager_id": study_manager.server_manager_id,
                 "env_id": self.current_running_env["env_id"],
                 "reason": "Next level",
@@ -364,13 +369,15 @@ class StudyManager:
 
     def __init__(self):
         """Constructor of the StudyManager class."""
-        self.game_host: str
+        self.game_host: str = "localhost"
         """Host address of the game server where the studies are running their environments."""
-        self.game_port: int
+        self.game_port: int = 8000
         """Port of the game server where the studies are running their environments."""
-        self.game_server_url: str
+        self.game_server_url: str = ""
         """Combined URL of the game server where the studies are running their environments."""
-        self.server_manager_id: str
+        self.create_game_server_url()
+
+        self.server_manager_id: str = ""
         """Manager id of this manager which will be registered in the game server."""
         self.running_studies: list[Study] = []
         """List of currently running studies."""
@@ -381,7 +388,7 @@ class StudyManager:
         self.running_tutorials: dict[str, CreateEnvResult] = {}
         """Dict which saves currently running tutorial envs, as these do not need advanced player management."""
 
-        self.study_config_path = ROOT_DIR / "configs" / "study" / "study_config.yml"
+        self.study_config_path = ROOT_DIR / "configs" / "study" / "study_config.yaml"
         """Path to the configuration file for the studies."""
 
     def create_study(self):
@@ -470,6 +477,9 @@ class StudyManager:
         """
         self.game_host = game_host
         self.game_port = game_port
+        self.create_game_server_url()
+
+    def create_game_server_url(self):
         self.game_server_url = f"http://{self.game_host}:{self.game_port}"
 
     def set_manager_id(self, manager_id: str):
@@ -513,8 +523,9 @@ class StudyManager:
             seed=1234567890,
         ).model_dump(mode="json")
         # todo async
-        env_info = requests.post(
-            study_manager.game_server_url + "/manage/create_env/", json=creation_json
+        env_info = request_game_server(
+            study_manager.game_server_url + "/manage/create_env/",
+            json_data=creation_json,
         )
         match env_info.status_code:
             case 200:
@@ -534,9 +545,9 @@ class StudyManager:
 
     def end_tutorial(self, participant_id: str):
         env = study_manager.running_tutorials[participant_id]
-        answer = requests.post(
+        answer = request_game_server(
             f"{study_manager.game_server_url}/manage/stop_env/",
-            json={
+            json_data={
                 "manager_id": study_manager.server_manager_id,
                 "env_id": env["env_id"],
                 "reason": "Finished tutorial",
diff --git a/cooperative_cuisine/utils.py b/cooperative_cuisine/utils.py
index 7cb4e28e..fd1effc4 100644
--- a/cooperative_cuisine/utils.py
+++ b/cooperative_cuisine/utils.py
@@ -364,7 +364,7 @@ def add_list_of_manager_ids_arguments(parser):
     """
     parser.add_argument(
         "-m",
-        "--manager_ids",
+        "--manager-ids",
         nargs="+",
         type=str,
         default=[uuid.uuid4().hex],
@@ -439,17 +439,23 @@ class NumpyAndDataclassEncoder(json.JSONEncoder):
         return json.JSONEncoder.default(self, obj)
 
 
-def create_layout_with_counters(w, h):
+def create_layout_with_counters(w, h) -> str:
     """Print a layout string that has counters at the world borders.
 
     Args:
         w: The width of the layout.
         h: The height of the layout.
+
+    Returns:
+        str of the layout
     """
+    string = ""
     for y in range(h):
         for x in range(w):
             if x == 0 or y == 0 or x == w - 1 or y == h - 1:
-                print("#", end="")
+                string += "#"
             else:
-                print("_", end="")
-        print("")
+                string += "_"
+        string += "\n"
+    print(string)
+    return string
diff --git a/setup.py b/setup.py
index bd15680e..bd89c44b 100644
--- a/setup.py
+++ b/setup.py
@@ -31,7 +31,7 @@ requirements = [
     "networkx",
 ]
 
-test_requirements = ["pytest>=3", "pytest-cov>=4.1"]
+test_requirements = ["pytest>=3", "pytest-cov>=4.1", "httpx"]
 
 setup(
     author="Annika Österdiekhoff, Dominik Battefeld, Fabian Heinrich, Florian Schröder",
diff --git a/tests/test_game_server.py b/tests/test_game_server.py
new file mode 100644
index 00000000..f072fed2
--- /dev/null
+++ b/tests/test_game_server.py
@@ -0,0 +1,284 @@
+import asyncio
+import json
+
+import pytest
+from fastapi import status
+from fastapi.testclient import TestClient
+
+from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.action import ActionType
+from cooperative_cuisine.game_server import (
+    app,
+    environment_handler,
+    CreateEnvironmentConfig,
+    ManageEnv,
+)
+from cooperative_cuisine.server_results import CreateEnvResult
+from cooperative_cuisine.state_representation import StateRepresentation
+
+environment_handler.extend_allowed_manager(["123"])
+
+
+@pytest.fixture
+def create_env_config():
+    layout_path = ROOT_DIR / "configs" / "layouts" / "tutorial.layout"
+    environment_config_path = ROOT_DIR / "configs" / "tutorial_env_config.yaml"
+    item_info_path = ROOT_DIR / "configs" / "item_info.yaml"
+    with open(item_info_path, "r") as file:
+        item_info = file.read()
+    with open(layout_path, "r") as file:
+        layout = file.read()
+    with open(environment_config_path, "r") as file:
+        environment_config = file.read()
+
+    return CreateEnvironmentConfig(
+        manager_id="123",
+        number_players=1,
+        environment_settings={"all_player_can_pause_game": False},
+        item_info_config=item_info,
+        layout_config=layout,
+        environment_config=environment_config,
+        seed=123,
+    )
+
+
+def test_create_env(create_env_config):
+    with TestClient(app) as client:
+        res = client.post(
+            "/manage/create_env/",
+            json=create_env_config.model_dump(mode="json"),
+        )
+
+    assert res.status_code == status.HTTP_200_OK
+    create_env_result = CreateEnvResult(**res.json())
+    assert len(create_env_result["player_info"]) == 1
+    assert isinstance(create_env_result["env_id"], str)
+
+
+def test_invalid_manager_id_create_env(create_env_config):
+    create_env_config.manager_id = "!"
+    with TestClient(app) as client:
+        res = client.post(
+            "/manage/create_env/",
+            json=create_env_config.model_dump(mode="json"),
+        )
+    assert res.status_code == status.HTTP_403_FORBIDDEN
+    assert res.json() == {"detail": "Manager ID not known/registered."}
+
+
+def test_invalid_create_env_config(create_env_config):
+    create_env_config.number_players = -1
+    with TestClient(app) as client:
+        res = client.post(
+            "/manage/create_env/",
+            json=create_env_config.model_dump(mode="json"),
+        )
+    assert res.status_code == status.HTTP_409_CONFLICT
+    assert res.json() == {"detail": "Number players need to be positive."}
+
+
+def test_stop_env(create_env_config):
+    with TestClient(app) as client:
+        res = client.post(
+            "/manage/create_env/",
+            json=create_env_config.model_dump(mode="json"),
+        )
+
+    with TestClient(app) as client:
+        res = client.post(
+            "/manage/stop_env/",
+            json=ManageEnv(
+                manager_id="123", env_id=res.json()["env_id"], reason="test"
+            ).model_dump(mode="json"),
+        )
+
+    assert res.status_code == status.HTTP_200_OK
+    with TestClient(app) as client:
+        res = client.post(
+            "/manage/stop_env/",
+            json=ManageEnv(manager_id="123", env_id="123456", reason="test").model_dump(
+                mode="json"
+            ),
+        )
+
+    assert res.status_code == status.HTTP_403_FORBIDDEN
+
+
+def test_websocket(create_env_config):
+    with TestClient(app) as client:
+        environment_handler.envs = {}
+        res = client.post(
+            "/manage/create_env/",
+            json=create_env_config.model_dump(mode="json"),
+        )
+        player_hash = res.json()["player_info"]["0"]["player_hash"]
+        loop = asyncio.new_event_loop()
+        task = loop.create_task(environment_handler.environment_steps())
+        try:
+            with client.websocket_connect(
+                f"/ws/player/{res.json()['player_info']['0']['client_id']}"
+            ) as websocket:
+                assert environment_handler.check_all_players_connected(
+                    res.json()["env_id"]
+                )
+                websocket.send_json({"player_hash": player_hash, "type": "ready"})
+                assert websocket.receive_json() == {
+                    "request_type": "ready",
+                    "msg": f"ready accepted",
+                    "status": 200,
+                    "player_hash": player_hash,
+                }
+                loop.run_until_complete(asyncio.sleep(0.001))
+                websocket.send_json({"player_hash": player_hash, "type": "get_state"})
+                state = websocket.receive_json()
+                assert state["all_players_ready"]
+                del state["all_players_ready"]
+                StateRepresentation.model_validate_json(json_data=json.dumps(state))
+
+                websocket.send_json(
+                    {
+                        "player_hash": player_hash,
+                        "type": "action",
+                        "action": {
+                            "player": "0",
+                            "action_type": ActionType.PICK_UP_DROP.value,
+                            "action_data": None,
+                        },
+                    }
+                )
+                assert websocket.receive_json() == {
+                    "request_type": "action",
+                    "status": 200,
+                    "msg": f"action accepted",
+                    "player_hash": player_hash,
+                }
+
+                assert (
+                    len(
+                        environment_handler.list_not_ready_players(res.json()["env_id"])
+                    )
+                    == 0
+                )
+                assert (
+                    len(
+                        environment_handler.list_not_connected_players(
+                            res.json()["env_id"]
+                        )
+                    )
+                    == 0
+                )
+        finally:
+            task.cancel()
+            loop.close()
+
+
+def test_websocket_wrong_inputs(create_env_config):
+    with TestClient(app) as client:
+        environment_handler.envs = {}
+        res = client.post(
+            "/manage/create_env/",
+            json=create_env_config.model_dump(mode="json"),
+        )
+        player_hash = res.json()["player_info"]["0"]["player_hash"]
+        wrong_player_hash = player_hash + "-------"
+        loop = asyncio.new_event_loop()
+        task = loop.create_task(environment_handler.environment_steps())
+        assert (
+            len(environment_handler.list_not_connected_players(res.json()["env_id"]))
+            == 1
+        )
+        try:
+            with client.websocket_connect(
+                f"/ws/player/{res.json()['player_info']['0']['client_id']}"
+            ) as websocket:
+                assert (
+                    len(
+                        environment_handler.list_not_ready_players(res.json()["env_id"])
+                    )
+                    == 1
+                )
+                assert (
+                    len(
+                        environment_handler.list_not_connected_players(
+                            res.json()["env_id"]
+                        )
+                    )
+                    == 0
+                )
+
+                websocket.send_json({"player_hash": wrong_player_hash, "type": "ready"})
+                assert websocket.receive_json() == {
+                    "request_type": "ready",
+                    "msg": f"ready not accepted",
+                    "status": 400,
+                    "player_hash": wrong_player_hash,
+                }
+                loop.run_until_complete(asyncio.sleep(0.001))
+                websocket.send_json(
+                    {"player_hash": wrong_player_hash, "type": "get_state"}
+                )
+                state = websocket.receive_json()
+                assert state == {
+                    "request_type": "get_state",
+                    "status": 400,
+                    "msg": "player hash unknown",
+                    "player_hash": None,
+                }
+
+                websocket.send_json(
+                    {
+                        "player_hash": wrong_player_hash,
+                        "type": "action",
+                        "action": {
+                            "player": "0",
+                            "action_type": ActionType.PICK_UP_DROP.value,
+                            "action_data": None,
+                        },
+                    }
+                )
+                assert websocket.receive_json() == {
+                    "request_type": "action",
+                    "status": 400,
+                    "msg": f"action not accepted",
+                    "player_hash": wrong_player_hash,
+                }
+
+                websocket.send_json(
+                    {
+                        "player_hash": wrong_player_hash,
+                        "type": "delta_v",
+                        "action": {
+                            "player": "0",
+                            "action_type": ActionType.PICK_UP_DROP.value,
+                            "action_data": None,
+                        },
+                    }
+                )
+                assert websocket.receive_json()["status"] == 400
+
+                websocket.send_json(
+                    {
+                        "player_hash": wrong_player_hash,
+                        "type": "action",
+                    }
+                )
+                assert websocket.receive_json()["status"] == 400
+
+                assert (
+                    len(
+                        environment_handler.list_not_ready_players(res.json()["env_id"])
+                    )
+                    == 1
+                )
+                assert (
+                    len(
+                        environment_handler.list_not_connected_players(
+                            res.json()["env_id"]
+                        )
+                    )
+                    == 0
+                )
+
+        finally:
+            task.cancel()
+            loop.close()
diff --git a/tests/test_study_server.py b/tests/test_study_server.py
new file mode 100644
index 00000000..0d979633
--- /dev/null
+++ b/tests/test_study_server.py
@@ -0,0 +1,95 @@
+import json
+from unittest import mock
+
+from fastapi import status
+from fastapi.testclient import TestClient
+from requests import Response
+
+import cooperative_cuisine.study_server as study_server_module
+from cooperative_cuisine.study_server import app
+
+
+def test_valid_post_requests():
+    test_response = Response()
+    test_response.status_code = status.HTTP_200_OK
+    test_response.encoding = "utf8"
+    test_response._content = json.dumps(
+        {
+            "player_info": {
+                "0": {
+                    "player_id": "0",
+                    "client_id": "ksjdhfkjsdfn",
+                    "player_hash": "shdfbmsndfb",
+                }
+            },
+            "env_id": "123456789",
+            "recipe_graphs": [],
+        }
+    ).encode()
+    with mock.patch.object(
+        study_server_module, "request_game_server", return_value=test_response
+    ) as mock_call:
+        with TestClient(app) as client:
+            res = client.post("/start_study/124/1")
+
+    assert res.status_code == status.HTTP_200_OK
+
+    mock_call.assert_called_once()
+
+    with mock.patch.object(
+        study_server_module, "request_game_server", return_value=test_response
+    ) as mock_call:
+        with TestClient(app) as client:
+            res = client.post("/get_game_connection/124")
+
+    assert res.status_code == status.HTTP_200_OK
+    assert res.json()["player_info"] == {
+        "0": {
+            "player_id": "0",
+            "client_id": "ksjdhfkjsdfn",
+            "player_hash": "shdfbmsndfb",
+        }
+    }
+
+    with mock.patch.object(
+        study_server_module, "request_game_server", return_value=test_response
+    ) as mock_call:
+        with TestClient(app) as client:
+            res = client.post("/level_done/124")
+
+    assert res.status_code == status.HTTP_200_OK
+
+
+def test_invalid_post_requests():
+    test_response = ""
+    with mock.patch.object(
+        study_server_module, "request_game_server", return_value=test_response
+    ) as mock_call:
+        with TestClient(app) as client:
+            res = client.post("/level_done/125")
+
+    assert res.status_code == status.HTTP_409_CONFLICT
+
+    with mock.patch.object(
+        study_server_module, "request_game_server", return_value=test_response
+    ) as mock_call:
+        with TestClient(app) as client:
+            res = client.post("/get_game_connection/125")
+
+    assert res.status_code == status.HTTP_409_CONFLICT
+
+
+def test_game_server_crashed():
+    test_response = Response()
+    test_response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
+
+    with mock.patch.object(
+        study_server_module, "request_game_server", return_value=test_response
+    ) as mock_call:
+        with TestClient(app) as client:
+            res = client.post("/start_study/124/1")
+
+    assert res.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
+
+
+# TOOD test bots
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 00000000..ad9fb6b6
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,46 @@
+from argparse import ArgumentParser
+
+from cooperative_cuisine.utils import (
+    url_and_port_arguments,
+    add_list_of_manager_ids_arguments,
+    disable_websocket_logging_arguments,
+    add_study_arguments,
+    add_gui_arguments,
+    create_layout_with_counters,
+    setup_logging,
+)
+
+
+def test_parser_gen():
+    parser = ArgumentParser()
+    url_and_port_arguments(parser)
+    disable_websocket_logging_arguments(parser)
+    add_list_of_manager_ids_arguments(parser)
+    add_study_arguments(parser)
+    add_gui_arguments(parser)
+
+    parser.parse_args(
+        [
+            "-s",
+            "localhost",
+            "-sp",
+            "8000",
+            "-g",
+            "localhost",
+            "-gp",
+            "8080",
+            "--manager-ids",
+            "123",
+            "123123",
+            "--do-study",
+        ]
+    )
+
+
+def test_layout_creation():
+    assert """###\n#_#\n###\n""" == create_layout_with_counters(3, 3)
+    assert """###\n#_#\n#_#\n###\n""" == create_layout_with_counters(3, 4)
+
+
+def test_setup_logging():
+    setup_logging()
-- 
GitLab