Skip to content

Commit e7cc43d

Browse files
committed
Add tests
1 parent 74ab6f2 commit e7cc43d

1 file changed

Lines changed: 246 additions & 0 deletions

File tree

tests/arenas/test_chess.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
"""
2+
Unit tests for ChessArena.
3+
4+
Tests validate_code() and get_results() methods without requiring Docker.
5+
"""
6+
7+
import json
8+
import pytest
9+
10+
from codeclash.arenas.arena import RoundStats
11+
from codeclash.arenas.chess.chess import ChessArena
12+
from codeclash.constants import RESULT_TIE
13+
14+
from .conftest import MockPlayer
15+
16+
17+
class TestChessValidation:
18+
"""Tests for ChessArena.validate_code()"""
19+
20+
@pytest.fixture
21+
def arena(self, tmp_log_dir, minimal_config):
22+
"""Create ChessArena instance with mocked environment."""
23+
arena = ChessArena.__new__(ChessArena)
24+
arena.submission = "src/"
25+
arena.log_local = tmp_log_dir
26+
# Minimal attributes used in validate_code
27+
arena.logger = type("Logger", (), {"debug": lambda self, msg: None, "info": lambda self, msg: None})()
28+
return arena
29+
30+
def test_valid_submission(self, arena, mock_player_factory):
31+
"""Valid C++ engine compiles and produces `src/kojiro` executable."""
32+
player = mock_player_factory(
33+
name="test_player",
34+
files={
35+
# Not strictly used by validate_code, but helpful if commands fall back to defaults
36+
"src/kojiro": "",
37+
},
38+
command_outputs={
39+
"ls": {"output": "src\n", "returncode": 0},
40+
"cd src && make native": {"output": "Compile OK", "returncode": 0},
41+
"ls src/kojiro": {"output": "kojiro\n", "returncode": 0},
42+
},
43+
)
44+
45+
is_valid, error = arena.validate_code(player)
46+
assert is_valid is True
47+
assert error is None
48+
49+
def test_missing_src_directory(self, arena, mock_player_factory):
50+
"""Missing `src/` directory fails validation."""
51+
player = mock_player_factory(
52+
name="test_player",
53+
files={},
54+
command_outputs={
55+
"ls": {"output": "README.md\n", "returncode": 0},
56+
},
57+
)
58+
59+
is_valid, error = arena.validate_code(player)
60+
assert is_valid is False
61+
assert "src/" in error
62+
63+
def test_compilation_failure(self, arena, mock_player_factory):
64+
"""Compilation errors are surfaced and fail validation."""
65+
player = mock_player_factory(
66+
name="test_player",
67+
files={},
68+
command_outputs={
69+
"ls": {"output": "src\n", "returncode": 0},
70+
"cd src && make native": {"output": "error: failed to compile", "returncode": 1},
71+
},
72+
)
73+
74+
is_valid, error = arena.validate_code(player)
75+
assert is_valid is False
76+
assert "Compilation failed" in error
77+
78+
def test_missing_executable_after_compilation(self, arena, mock_player_factory):
79+
"""Compilation succeeds but missing `kojiro` executable fails validation."""
80+
player = mock_player_factory(
81+
name="test_player",
82+
files={},
83+
command_outputs={
84+
"ls": {"output": "src\n", "returncode": 0},
85+
"cd src && make native": {"output": "Compile OK", "returncode": 0},
86+
"ls src/kojiro": {"output": "", "returncode": 1},
87+
},
88+
)
89+
90+
is_valid, error = arena.validate_code(player)
91+
assert is_valid is False
92+
assert "executable 'kojiro' not found" in error
93+
94+
95+
class TestChessResults:
96+
"""Tests for ChessArena.get_results()"""
97+
98+
@pytest.fixture
99+
def arena(self, tmp_log_dir, minimal_config):
100+
"""Create ChessArena-like instance with local logging directory."""
101+
config = minimal_config.copy()
102+
config["game"]["name"] = "Chess"
103+
config["game"]["sims_per_round"] = 2
104+
105+
arena = ChessArena.__new__(ChessArena)
106+
arena.submission = "src/"
107+
arena.log_local = tmp_log_dir
108+
arena.config = config
109+
# Lightweight logger stub
110+
arena.logger = type(
111+
"Logger",
112+
(),
113+
{
114+
"debug": lambda self, msg: None,
115+
"info": lambda self, msg: None,
116+
"warning": lambda self, msg: None,
117+
"error": lambda self, msg, **kwargs: None,
118+
},
119+
)()
120+
return arena
121+
122+
def _write_pairings(self, round_dir, pairings):
123+
pairings_file = round_dir / "pairings.json"
124+
pairings_file.write_text(json.dumps(pairings, indent=2))
125+
126+
def _write_pgn(self, file_path, white: str, black: str, result: str):
127+
content = (
128+
"""
129+
[Event "FastChess Match"]
130+
[Site "-"]
131+
[Date "2026.01.07"]
132+
[Round "1"]
133+
""".strip()
134+
+ f"\n[White \"{white}\"]\n[Black \"{black}\"]\n[Result \"{result}\"]\n\n"
135+
)
136+
file_path.write_text(content)
137+
138+
def test_player1_wins(self, arena, tmp_log_dir):
139+
"""Alice wins one match; overall winner is Alice."""
140+
round_dir = tmp_log_dir / "rounds" / "1"
141+
round_dir.mkdir(parents=True)
142+
143+
# sims_per_round = 2 but only first match is valid; second missing -> ignored
144+
pairings = [
145+
{"match_idx": 0, "agent1": "Alice", "agent2": "Bob"},
146+
{"match_idx": 1, "agent1": "Alice", "agent2": "Bob"},
147+
]
148+
self._write_pairings(round_dir, pairings)
149+
150+
# Match 0: Alice (White) wins
151+
self._write_pgn(round_dir / "match_0.pgn", white="Alice", black="Bob", result="1-0")
152+
# Match 1: no file -> ignored
153+
154+
agents = [MockPlayer("Alice"), MockPlayer("Bob")]
155+
stats = RoundStats(round_num=1, agents=agents)
156+
157+
arena.get_results(agents, round_num=1, stats=stats)
158+
159+
assert stats.winner == "Alice"
160+
assert stats.scores["Alice"] == 1
161+
assert stats.scores["Bob"] == 0
162+
163+
def test_player2_wins(self, arena, tmp_log_dir):
164+
"""Bob wins one match; overall winner is Bob."""
165+
round_dir = tmp_log_dir / "rounds" / "1"
166+
round_dir.mkdir(parents=True)
167+
168+
pairings = [
169+
{"match_idx": 0, "agent1": "Alice", "agent2": "Bob"},
170+
{"match_idx": 1, "agent1": "Alice", "agent2": "Bob"},
171+
]
172+
self._write_pairings(round_dir, pairings)
173+
174+
# Match 0: Bob (Black) wins
175+
self._write_pgn(round_dir / "match_0.pgn", white="Alice", black="Bob", result="0-1")
176+
177+
agents = [MockPlayer("Alice"), MockPlayer("Bob")]
178+
stats = RoundStats(round_num=1, agents=agents)
179+
180+
arena.get_results(agents, round_num=1, stats=stats)
181+
182+
assert stats.winner == "Bob"
183+
assert stats.scores["Alice"] == 0
184+
assert stats.scores["Bob"] == 1
185+
186+
def test_all_draws(self, arena, tmp_log_dir):
187+
"""All matches draw -> overall tie with zero scores."""
188+
round_dir = tmp_log_dir / "rounds" / "1"
189+
round_dir.mkdir(parents=True)
190+
191+
pairings = [
192+
{"match_idx": 0, "agent1": "Alice", "agent2": "Bob"},
193+
{"match_idx": 1, "agent1": "Alice", "agent2": "Bob"},
194+
]
195+
self._write_pairings(round_dir, pairings)
196+
197+
# Two draws
198+
self._write_pgn(round_dir / "match_0.pgn", white="Alice", black="Bob", result="1/2-1/2")
199+
self._write_pgn(round_dir / "match_1.pgn", white="Bob", black="Alice", result="1/2-1/2")
200+
201+
agents = [MockPlayer("Alice"), MockPlayer("Bob")]
202+
stats = RoundStats(round_num=1, agents=agents)
203+
204+
arena.get_results(agents, round_num=1, stats=stats)
205+
206+
assert stats.winner == RESULT_TIE
207+
assert stats.scores["Alice"] == 0
208+
assert stats.scores["Bob"] == 0
209+
210+
def test_split_wins_results_in_tie(self, arena, tmp_log_dir):
211+
"""Each player wins one match -> tie overall."""
212+
round_dir = tmp_log_dir / "rounds" / "1"
213+
round_dir.mkdir(parents=True)
214+
215+
pairings = [
216+
{"match_idx": 0, "agent1": "Alice", "agent2": "Bob"},
217+
{"match_idx": 1, "agent1": "Alice", "agent2": "Bob"},
218+
]
219+
self._write_pairings(round_dir, pairings)
220+
221+
# Alice wins match 0, Bob wins match 1
222+
self._write_pgn(round_dir / "match_0.pgn", white="Alice", black="Bob", result="1-0")
223+
self._write_pgn(round_dir / "match_1.pgn", white="Alice", black="Bob", result="0-1")
224+
225+
agents = [MockPlayer("Alice"), MockPlayer("Bob")]
226+
stats = RoundStats(round_num=1, agents=agents)
227+
228+
arena.get_results(agents, round_num=1, stats=stats)
229+
230+
assert stats.winner == RESULT_TIE
231+
assert stats.scores["Alice"] == 1
232+
assert stats.scores["Bob"] == 1
233+
234+
235+
class TestChessConfig:
236+
"""Tests for ChessArena configuration and properties."""
237+
238+
def test_arena_name(self):
239+
assert ChessArena.name == "Chess"
240+
241+
def test_submission_folder(self):
242+
assert ChessArena.submission == "src/"
243+
244+
def test_default_args_contains_time_control(self):
245+
assert "time_control" in ChessArena.default_args
246+

0 commit comments

Comments
 (0)