Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chat example and unit tests #16

Merged
merged 20 commits into from
Oct 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[flake8]
max-line-length = 119
max-complexity = 10
ignore =

# darglint
# Allow one line docstrings without arg spec
strictness=short
docstring_style=google
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
.mypy_cache
.pytest_cache
MANIFEST

# PyInstaller
Expand Down
8 changes: 8 additions & 0 deletions .mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[mypy]

[mypy-socketio.*]
ignore_missing_imports = True

[mypy-uvicorn.*]
ignore_missing_imports = True

6 changes: 5 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ flake8 = "*"
pytest-cov = "*"
mypy = "*"
requests = "*"
uvicorn = "*"
uvicorn = {extras = ["standard"], version = "*"}
flake8-junit-report = "*"
aiofiles = "*"
ipdb = "*"
python-socketio = {extras = ["asyncio_client"], version = "*"}
pytest-asyncio = "*"

[requires]
python_version = "3.7"
319 changes: 315 additions & 4 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function msgsuccess(){

# run first syntax and code style checks
msgrun flake8
$RUNNER run flake8 --output-file $DIR/flake8.txt src
$RUNNER run flake8 --tee --output-file $DIR/flake8.txt
msgsuccess $? flake8
$RUNNER run flake8_junit $DIR/flake8.txt $DIR/flake8_junit.xml >/dev/null

Expand Down
39 changes: 39 additions & 0 deletions src/app/chat.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!doctype html>
<html>
<head>
<title>Socket.IO chat</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font: 13px Helvetica, Arial; }
form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
form input { border: 0; padding: 10px; width: 90%; margin-right: 0.5%; }
form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }
</style>
</head>
<body>
<h1>Socket.IO chat</h1>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" /><button>Send</button>
</form>
<script src="/static/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script>
$(function () {
var socket = io({ path: "/sio/socket.io" });
$('form').submit(function(e) {
e.preventDefault(); // prevents page reloading
socket.emit('chat message', $('#m').val());
$('#m').val('');
return false;
});
socket.on('chat message', function(msg){
$('#messages').append($('<li>').text(msg));
});
});
</script>
</body>
</html>
37 changes: 37 additions & 0 deletions src/app/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
"""The FastAPI main module"""
import os
import socketio

from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles

app = FastAPI()
path = os.path.dirname(__file__)
app.mount("/static", StaticFiles(directory=os.path.join(path, "static")), name="static")

sio = socketio.AsyncServer(async_mode='asgi')
# socketio adds automatically /socket.io/ to the URL.
app.mount('/sio', socketio.ASGIApp(sio))


@sio.on('connect')
def sio_connect(sid, environ): # pylint: disable=unused-argument
"""Track user connection"""
print('A user connected')


@sio.on('disconnect')
def sio_disconnect(sid): # pylint: disable=unused-argument
"""Track user disconnection"""
print('User disconnected')


@sio.on('chat message')
async def chat_message(sid, msg): # pylint: disable=unused-argument
"""Receive a chat message and send to all clients"""
print(f"Server received and sends to all clients: {msg}")
await sio.emit('chat message', msg)


@app.get("/")
Expand All @@ -11,3 +42,9 @@ async def home() -> dict:
'message': "Hello world",
'version': "0.1",
}


@app.get("/chat", response_class=FileResponse)
def chat() -> FileResponse:
"""Load the chat html page"""
return FileResponse(os.path.join(os.path.dirname(__file__), 'chat.html'))
9 changes: 9 additions & 0 deletions src/app/static/socket.io.js

Large diffs are not rendered by default.

106 changes: 106 additions & 0 deletions src/app/tests/test_chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Some python-socketio tests"""
from typing import Any, List, Optional, Awaitable
# stdlib imports
import asyncio
import os

# 3rd party imports
import pytest
import socketio
import uvicorn

# FastAPI imports
from fastapi import FastAPI
from fastapi.testclient import TestClient

# project importsn
from .. import main

PORT = 8000

# deactivate monitoring task in python-socketio to avoid errores during shutdown
main.sio.eio.start_service_task = False
client = TestClient(main.app)


class UvicornTestServer(uvicorn.Server):
"""Uvicorn test server

Usage:
@pytest.fixture
server = UvicornTestServer()
await server.up()
yield
await server.down()
"""

def __init__(self, app: FastAPI = main.app, host: str = '127.0.0.1', port: int = PORT):
"""Create a Uvicorn test server

Args:
app (FastAPI, optional): the FastAPI app. Defaults to main.app.
host (str, optional): the host ip. Defaults to '127.0.0.1'.
port (int, optional): the port. Defaults to PORT.
"""
self._startup_done = asyncio.Event()
self._serve_task: Optional[Awaitable[Any]] = None
super().__init__(config=uvicorn.Config(app, host=host, port=port))

async def startup(self, sockets: Optional[List] = None) -> None:
"""Override uvicorn startup"""
await super().startup(sockets=sockets)
self.config.setup_event_loop()
self._startup_done.set()

async def start_up(self) -> None:
"""Start up server asynchronously"""
self._serve_task = asyncio.create_task(self.serve())
await self._startup_done.wait()

async def tear_down(self) -> None:
"""Shut down server asynchronously"""
self.should_exit = True
if self._serve_task:
await self._serve_task


@pytest.fixture
async def startup_and_shutdown_server(): # pylint: disable=unused-variable
"""Start server as test fixture and tear down after test"""
server = UvicornTestServer()
await server.start_up()
yield
await server.tear_down()


@pytest.mark.asyncio
async def test_chat_simple(startup_and_shutdown_server): # pylint: disable=unused-argument,redefined-outer-name
"""A simple websocket test"""

sio = socketio.AsyncClient()
future = asyncio.get_running_loop().create_future()

@sio.on('chat message')
def on_message_received(data): # pylint: disable=unused-variable
print(f"Client received: {data}")
# set the result
future.set_result(data)

message = 'Hello!'
await sio.connect(f'http://localhost:{PORT}', socketio_path='/sio/socket.io/')
print(f"Client sends: {message}")
await sio.emit('chat message', message)
# wait for the result to be set (avoid waiting forever)
await asyncio.wait_for(future, timeout=1.0)
await sio.disconnect()
assert future.result() == message


def test_chat_page():
"""Check if chat page returns contents"""
response = client.get("/chat")
assert response.ok
filename = os.path.join(os.path.dirname(__file__), '..', 'chat.html')
print(f"Chat page: {filename}")
with open(filename, 'rb') as page:
assert response.content == page.read()
4 changes: 3 additions & 1 deletion src/app/tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Main app tests"""

from fastapi.testclient import TestClient
from ..main import app

Expand All @@ -7,7 +9,7 @@
def test_home():
"""Test home view"""
response = client.get("/")
assert response.status_code == 200
assert response.ok
result = response.json()
assert 'message' in result
assert result['message'].lower().startswith("hello world")