Skip to content

Commit 4b3cb5b

Browse files
committed
Implemented automated tests
1 parent bb01d34 commit 4b3cb5b

4 files changed

Lines changed: 316 additions & 1 deletion

File tree

.github/test.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Test
2+
on: [push]
3+
jobs:
4+
test-api:
5+
name: API
6+
runs-on: ubuntu-latest
7+
steps:
8+
- name: Checkout repository
9+
uses: actions/checkout@v6
10+
- name: Copy files
11+
run: |
12+
mkdir tests
13+
cp client/game_server_api.py tests/
14+
cp .github/workflows/test*.py tests/
15+
- name: Start server
16+
run: server/game_server.py &
17+
- name: Test
18+
run: tests/test_api.py
19+
test-game:
20+
name: Game
21+
runs-on: ubuntu-latest
22+
steps:
23+
- name: Checkout repository
24+
uses: actions/checkout@v6
25+
- name: Copy files
26+
run: |
27+
mkdir tests
28+
cp client/game_server_api.py tests/
29+
cp .github/workflows/test*.py tests/
30+
- name: Start server
31+
run: server/game_server.py &
32+
- name: Test
33+
run: tests/test_game.py
34+
test-macos:
35+
name: macOS
36+
runs-on: macos-latest
37+
steps:
38+
- name: Checkout repository
39+
uses: actions/checkout@v6
40+
- name: Copy files
41+
run: |
42+
mkdir tests
43+
cp client/game_server_api.py tests/
44+
cp .github/workflows/test*.py tests/
45+
- name: Start server
46+
run: server/game_server.py &
47+
- name: Test game
48+
run: tests/test_game.py
49+
- name: Test API
50+
run: tests/test_api.py
51+
test-windows:
52+
name: Windows
53+
runs-on: windows-latest
54+
steps:
55+
- name: Checkout repository
56+
uses: actions/checkout@v6
57+
- name: Copy files
58+
run: |
59+
mkdir tests
60+
cp client/game_server_api.py tests/
61+
cp .github/workflows/test*.py tests/
62+
- name: Test game
63+
run: |
64+
pythonw server/game_server.py
65+
python tests/test_game.py
66+
- name: Test API
67+
run: |
68+
pythonw server/game_server.py
69+
python tests/test_api.py

.github/test_api.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Testing all API functions.
4+
"""
5+
6+
from game_server_api import GameServerAPI, GameServerError, IllegalMove
7+
8+
SERVER = '127.0.0.1'
9+
PORT = 4711
10+
11+
def fail(msg):
12+
exit(f'ERROR: {msg}')
13+
14+
# ========== join ==========
15+
16+
GAME = 'Echo'
17+
TOKEN = 'test'
18+
NAME = 'bob'
19+
20+
try:
21+
game_err = GameServerAPI(SERVER, PORT, 'InvalidGame', TOKEN, 1)
22+
my_id = game_err.join()
23+
fail('no exception when trying to start non-existent game')
24+
except GameServerError:
25+
pass
26+
27+
try:
28+
game_err = GameServerAPI(SERVER, 9999, GAME, TOKEN, 1)
29+
my_id = game_err.join()
30+
fail('no exception despite invalid port')
31+
except GameServerError:
32+
pass
33+
34+
try:
35+
game_err = GameServerAPI('127.0.0.13', PORT, GAME, TOKEN, 1)
36+
my_id = game_err.join()
37+
fail('no exception despite invalid ip')
38+
except GameServerError:
39+
pass
40+
41+
try:
42+
game_err = GameServerAPI(SERVER, PORT, GAME, TOKEN)
43+
my_id = game_err.join()
44+
fail('no exception when trying to join non-existent session')
45+
except GameServerError:
46+
pass
47+
48+
game = GameServerAPI(SERVER, PORT, GAME, TOKEN, 1, NAME)
49+
50+
try:
51+
game.move(msg='invalid')
52+
fail('no exception when performing move before game has started')
53+
except GameServerError:
54+
pass
55+
56+
my_id = game.join()
57+
58+
try:
59+
game_err = GameServerAPI(SERVER, PORT, GAME, TOKEN)
60+
my_id = game_err.join()
61+
fail('no exception when trying to join full session')
62+
except GameServerError:
63+
pass
64+
65+
# ========== move ==========
66+
67+
game.move(msg='hello')
68+
state = game.state()
69+
70+
if state['echo'] != 'hello':
71+
fail('wrong echo after valid move')
72+
73+
try:
74+
game.move(invalid_key='hello')
75+
fail('no exception despite invalid key')
76+
except IllegalMove:
77+
pass
78+
79+
try:
80+
game.move(msg='error')
81+
fail('no exception despite illegal move')
82+
except IllegalMove:
83+
pass
84+
85+
state = game.state()
86+
if state['echo'] != 'error':
87+
fail('wrong echo after sending error')
88+
89+
# ========== gameover ==========
90+
91+
if state['gameover']:
92+
fail('gameover although game has not ended')
93+
94+
game.move(msg='quit')
95+
state = game.state()
96+
97+
if not state['gameover']:
98+
fail('no gameover despite quit')
99+
100+
try:
101+
game.move(msg='invalid')
102+
fail('no exception when performing move after game has ended')
103+
except GameServerError:
104+
pass
105+
106+
# ========== restart ==========
107+
108+
game.restart()
109+
state = game.state()
110+
111+
if state['echo'] != '':
112+
fail('wrong echo after restart')
113+
114+
if state['gameover']:
115+
fail('gameover after restart')
116+
117+
# ========== observe ==========
118+
119+
try:
120+
observer = GameServerAPI(SERVER, PORT, GAME, TOKEN, name='invalid')
121+
observer.observe()
122+
fail('observer: no exception despite invalid name')
123+
except GameServerError:
124+
pass
125+
126+
observer = GameServerAPI(SERVER, PORT, GAME, TOKEN, name=NAME)
127+
observer.observe()
128+
129+
game.move(msg='hello observer')
130+
state = observer.state() # expecting state of old game before restart
131+
132+
if state['echo'] != 'quit':
133+
fail('observer: wrong echo in old game')
134+
135+
if not state['gameover']:
136+
fail('observer missed gameover')
137+
138+
state = observer.state() # expecting state of new game after restart
139+
140+
if state['echo'] != 'hello observer':
141+
fail('observer: wrong echo in new game')
142+
143+
if state['gameover']:
144+
fail('observer: wrong game status in new game')
145+
146+
try:
147+
observer.move(msg='invalid')
148+
fail('observer: no exception despite move')
149+
except GameServerError:
150+
pass
151+
152+
try:
153+
observer.restart()
154+
fail('observer: no exception despite restart')
155+
except GameServerError:
156+
pass
157+
158+
print('OK')

.github/test_game.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Playing through a game, provoking all error cases.
4+
"""
5+
6+
from game_server_api import GameServerAPI, GameServerError, IllegalMove
7+
8+
import threading
9+
import traceback
10+
11+
SERVER = '127.0.0.1'
12+
PORT = 4711
13+
14+
end = threading.Event()
15+
sync = threading.Event()
16+
17+
error = False
18+
19+
def fail(msg):
20+
global error
21+
error = True
22+
print(f'ERROR: {msg}')
23+
end.set()
24+
25+
def synchronize_threads():
26+
sync.set()
27+
sync.clear()
28+
sync.wait()
29+
30+
moves = [(1, True), (2, True), (4, True), (4, False), (42, False), (5, True), (7, True)]
31+
32+
def play():
33+
try:
34+
game = GameServerAPI(SERVER, PORT, 'TicTacToe', 'test', 2)
35+
my_id = game.join()
36+
state = game.state()
37+
38+
if my_id not in state['current']:
39+
try:
40+
game.move(position=1)
41+
fail('no exception although not players turn')
42+
except GameServerError:
43+
pass
44+
45+
synchronize_threads()
46+
47+
while len(moves):
48+
if state['gameover']: fail('gameover although game has not ended')
49+
if state['winner'] is not None: fail('winner although game still active')
50+
51+
if my_id in state['current']:
52+
try:
53+
move, legal = moves.pop(0)
54+
game.move(position=move)
55+
if not legal: fail('no exception despite illegal move')
56+
except IllegalMove:
57+
if legal: fail('exception despite legal move')
58+
59+
synchronize_threads()
60+
state = game.state()
61+
62+
if not state['gameover']: fail('no gameover after final move')
63+
64+
try:
65+
game.move(position=1)
66+
fail('no exception when performing move after game has ended')
67+
except GameServerError:
68+
pass
69+
70+
if state['winner'] is None: fail('no winner despite win')
71+
if len(state['current']): fail('current player list not empty although game has ended')
72+
except:
73+
fail('unexpected exception:\n' + traceback.format_exc())
74+
75+
end.set()
76+
77+
for _ in range(2):
78+
threading.Thread(target=play, daemon=True).start()
79+
80+
end.wait()
81+
82+
if error:
83+
exit(1)
84+
else:
85+
print('OK')
86+
exit(0)

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
[![](https://github.com/feberts/python-game-server/actions/workflows/test.yml/badge.svg)](https://github.com/feberts/python-game-server/actions/workflows/test.yml)
12
[![](https://github.com/feberts/python-game-server/actions/workflows/lint.yml/badge.svg)](https://github.com/feberts/python-game-server/actions/workflows/lint.yml)
23
![](https://img.shields.io/badge/OS-Linux_%7C_Win_%7C_Mac-30C452?labelColor=373F46)
34
![](https://img.shields.io/badge/Python-blue?logo=python&logoColor=FFCF3E)
@@ -50,7 +51,8 @@ Here is a simplified example of the API usage:
5051
```py
5152
from game_server_api import GameServerAPI
5253

53-
game = GameServerAPI(server='127.0.0.1', port=4711, game='TicTacToe', token='mygame', players=2)
54+
game = GameServerAPI(server='127.0.0.1', port=4711, game='TicTacToe',
55+
token='mygame', players=2)
5456

5557
my_id = game.join() # starting/joining a game - each client is assigned an ID
5658
game.move(position=5) # performing a move - the function accepts keyword arguments (**kwargs)

0 commit comments

Comments
 (0)