Included gomill framework for SGF and GTP support, and sketched out SGF game-loading code.

This commit is contained in:
2012-04-21 04:27:05 -04:00
parent 700a6a2f32
commit 692dc294d6
119 changed files with 27458 additions and 3 deletions

View File

@ -0,0 +1 @@
# gomill_tests package

View File

@ -0,0 +1,289 @@
"""Tests for allplayalls.py"""
from __future__ import with_statement
from textwrap import dedent
import cPickle as pickle
from gomill import competitions
from gomill import allplayalls
from gomill.gtp_games import Game_result
from gomill.game_jobs import Game_job, Game_job_result
from gomill.competitions import (
Player_config, NoGameAvailable, CompetitionError, ControlFileError)
from gomill.allplayalls import Competitor_config
from gomill_tests import competition_test_support
from gomill_tests import gomill_test_support
from gomill_tests import test_framework
from gomill_tests.competition_test_support import (
fake_response, check_screen_report)
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def check_short_report(tc, comp,
expected_grid, expected_matchups, expected_players,
competition_name="testcomp"):
"""Check that an allplayall's short report is as expected."""
expected = ("allplayall: %s\n\n%s\n%s\n%s\n" %
(competition_name, expected_grid,
expected_matchups, expected_players))
tc.assertMultiLineEqual(competition_test_support.get_short_report(comp),
expected)
class Allplayall_fixture(test_framework.Fixture):
"""Fixture setting up a Allplayall.
attributes:
comp -- Allplayall
"""
def __init__(self, tc, config=None):
if config is None:
config = default_config()
self.tc = tc
self.comp = allplayalls.Allplayall('testcomp')
self.comp.initialise_from_control_file(config)
self.comp.set_clean_status()
def check_screen_report(self, expected):
"""Check that the screen report is as expected."""
check_screen_report(self.tc, self.comp, expected)
def check_short_report(self, *args, **kwargs):
"""Check that the short report is as expected."""
check_short_report(self.tc, self.comp, *args, **kwargs)
def default_config():
return {
'players' : {
't1' : Player_config("test1"),
't2' : Player_config("test2"),
't3' : Player_config("test3"),
},
'board_size' : 13,
'komi' : 7.5,
'competitors' : [
Competitor_config('t1'),
Competitor_config('t2'),
't3',
],
}
def test_default_config(tc):
comp = allplayalls.Allplayall('test')
config = default_config()
comp.initialise_from_control_file(config)
comp.set_clean_status()
tr = comp.get_tournament_results()
tc.assertListEqual(tr.get_matchup_ids(), ['AvB', 'AvC', 'BvC'])
mBvC = tr.get_matchup('BvC')
tc.assertEqual(mBvC.player_1, 't2')
tc.assertEqual(mBvC.player_2, 't3')
tc.assertEqual(mBvC.board_size, 13)
tc.assertEqual(mBvC.komi, 7.5)
tc.assertEqual(mBvC.move_limit, 1000)
tc.assertEqual(mBvC.scorer, 'players')
tc.assertEqual(mBvC.internal_scorer_handicap_compensation, 'full')
tc.assertEqual(mBvC.number_of_games, None)
tc.assertIs(mBvC.alternating, True)
tc.assertIs(mBvC.handicap, None)
tc.assertEqual(mBvC.handicap_style, 'fixed')
def test_basic_config(tc):
comp = allplayalls.Allplayall('test')
config = default_config()
config['description'] = "default\nconfig"
config['board_size'] = 9
config['komi'] = 0.5
config['move_limit'] = 200
config['scorer'] = 'internal'
config['internal_scorer_handicap_compensation'] = 'short'
config['rounds'] = 20
comp.initialise_from_control_file(config)
tc.assertEqual(comp.description, "default\nconfig")
comp.set_clean_status()
mBvC = comp.get_tournament_results().get_matchup('BvC')
tc.assertEqual(mBvC.player_1, 't2')
tc.assertEqual(mBvC.player_2, 't3')
tc.assertEqual(mBvC.board_size, 9)
tc.assertEqual(mBvC.komi, 0.5)
tc.assertEqual(mBvC.move_limit, 200)
tc.assertEqual(mBvC.scorer, 'internal')
tc.assertEqual(mBvC.internal_scorer_handicap_compensation, 'short')
tc.assertEqual(mBvC.number_of_games, 20)
tc.assertIs(mBvC.alternating, True)
tc.assertIs(mBvC.handicap, None)
tc.assertEqual(mBvC.handicap_style, 'fixed')
def test_unknown_player(tc):
comp = allplayalls.Allplayall('test')
config = default_config()
config['competitors'].append('nonex')
tc.assertRaisesRegexp(
ControlFileError, "competitor nonex: unknown player",
comp.initialise_from_control_file, config)
def test_duplicate_player(tc):
comp = allplayalls.Allplayall('test')
config = default_config()
config['competitors'].append('t2')
tc.assertRaisesRegexp(
ControlFileError, "duplicate competitor: t2",
comp.initialise_from_control_file, config)
def test_game_id_format(tc):
config = default_config()
config['rounds'] = 1000
fx = Allplayall_fixture(tc, config)
tc.assertEqual(fx.comp.get_game().game_id, 'AvB_000')
def test_get_player_checks(tc):
fx = Allplayall_fixture(tc)
checks = fx.comp.get_player_checks()
tc.assertEqual(len(checks), 3)
tc.assertEqual(checks[0].board_size, 13)
tc.assertEqual(checks[0].komi, 7.5)
tc.assertEqual(checks[0].player.code, "t1")
tc.assertEqual(checks[0].player.cmd_args, ['test1'])
tc.assertEqual(checks[1].player.code, "t2")
tc.assertEqual(checks[1].player.cmd_args, ['test2'])
tc.assertEqual(checks[2].player.code, "t3")
tc.assertEqual(checks[2].player.cmd_args, ['test3'])
def test_play(tc):
fx = Allplayall_fixture(tc)
tc.assertIsNone(fx.comp.description)
job1 = fx.comp.get_game()
tc.assertIsInstance(job1, Game_job)
tc.assertEqual(job1.game_id, 'AvB_0')
tc.assertEqual(job1.player_b.code, 't1')
tc.assertEqual(job1.player_w.code, 't2')
tc.assertEqual(job1.board_size, 13)
tc.assertEqual(job1.komi, 7.5)
tc.assertEqual(job1.move_limit, 1000)
tc.assertIs(job1.use_internal_scorer, False)
tc.assertEqual(job1.internal_scorer_handicap_compensation, 'full')
tc.assertEqual(job1.game_data, ('AvB', 0))
tc.assertIsNone(job1.sgf_filename)
tc.assertIsNone(job1.sgf_dirname)
tc.assertIsNone(job1.void_sgf_dirname)
tc.assertEqual(job1.sgf_event, 'testcomp')
tc.assertIsNone(job1.gtp_log_pathname)
job2 = fx.comp.get_game()
tc.assertIsInstance(job2, Game_job)
tc.assertEqual(job2.game_id, 'AvC_0')
tc.assertEqual(job2.player_b.code, 't1')
tc.assertEqual(job2.player_w.code, 't3')
response1 = fake_response(job1, 'b')
fx.comp.process_game_result(response1)
response2 = fake_response(job2, None)
fx.comp.process_game_result(response2)
expected_grid = dedent("""\
2 games played
A B C
A t1 1-0 0.5-0.5
B t2 0-1 0-0
C t3 0.5-0.5 0-0
""")
expected_matchups = dedent("""\
t1 v t2 (1 games)
board size: 13 komi: 7.5
wins
t1 1 100.00% (black)
t2 0 0.00% (white)
t1 v t3 (1 games)
board size: 13 komi: 7.5
wins
t1 0.5 50.00% (black)
t3 0.5 50.00% (white)
""")
expected_players = dedent("""\
player t1: t1 engine:v1.2.3
player t2: t2 engine
testdescription
player t3: t3 engine
testdescription
""")
fx.check_screen_report(expected_grid)
fx.check_short_report(expected_grid, expected_matchups, expected_players)
avb_results = fx.comp.get_tournament_results().get_matchup_results('AvB')
tc.assertEqual(avb_results, [response1.game_result])
def test_play_many(tc):
config = default_config()
config['rounds'] = 30
fx = Allplayall_fixture(tc, config)
jobs = [fx.comp.get_game() for _ in xrange(57)]
for i in xrange(57):
response = fake_response(jobs[i], 'b')
fx.comp.process_game_result(response)
fx.check_screen_report(dedent("""\
57/90 games played
A B C
A t1 10-9 10-9
B t2 9-10 10-9
C t3 9-10 9-10
"""))
tc.assertEqual(
len(fx.comp.get_tournament_results().get_matchup_results('AvB')), 19)
comp2 = competition_test_support.check_round_trip(tc, fx.comp, config)
jobs2 = [comp2.get_game() for _ in range(4)]
tc.assertListEqual([job.game_id for job in jobs2],
['AvB_19', 'AvC_19', 'BvC_19', 'AvB_20'])
tr = comp2.get_tournament_results()
tc.assertEqual(len(tr.get_matchup_results('AvB')), 19)
ms = tr.get_matchup_stats('AvB')
tc.assertEqual(ms.total, 19)
tc.assertEqual(ms.wins_1, 10)
tc.assertIs(ms.alternating, True)
def test_competitor_change(tc):
fx = Allplayall_fixture(tc)
status = pickle.loads(pickle.dumps(fx.comp.get_status()))
config2 = default_config()
del config2['competitors'][2]
comp2 = allplayalls.Allplayall('testcomp')
comp2.initialise_from_control_file(config2)
with tc.assertRaises(CompetitionError) as ar:
comp2.set_status(status)
tc.assertEqual(
str(ar.exception),
"competitor has been removed from control file")
config3 = default_config()
config3['players']['t4'] = Player_config("test4")
config3['competitors'][2] = 't4'
comp3 = allplayalls.Allplayall('testcomp')
comp3.initialise_from_control_file(config3)
with tc.assertRaises(CompetitionError) as ar:
comp3.set_status(status)
tc.assertEqual(
str(ar.exception),
"competitors have changed in the control file")
config4 = default_config()
config4['players']['t4'] = Player_config("test4")
config4['competitors'].append('t4')
comp4 = allplayalls.Allplayall('testcomp')
comp4.initialise_from_control_file(config4)
comp4.set_status(status)

View File

@ -0,0 +1,395 @@
play_tests = [
# code, list of moves to play, board representation, simple ko point, score
('blank', [
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 . . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J
""", None, 0),
('twostone', [
"B B2", "W C2",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 . . . . . . . . .
2 . # o . . . . . .
1 . . . . . . . . .
A B C D E F G H J
""", None, 0),
('many-groups-1-capture', [
"B C3", "W D3", "B C5", "B C4", "W D4", "B H1", "B B9", "B J6",
"B A7", "B B7", "W A3", "W J2", "W H2", "W G2", "W J3",
"B F7",
"W E6", "W G8", "W G6", "W F8", "W E7", "W F6", "W G7", "W E8",
], """\
9 . # . . . . . . .
8 . . . . o o o . .
7 # # . . o . o . .
6 . . . . o o o . #
5 . . # . . . . . .
4 . . # o . . . . .
3 o . # o . . . . o
2 . . . . . . o o o
1 . . . . . . . # .
A B C D E F G H J
""", None, -8),
('corner-bl', [
"B A1", "W B1", "W A2",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 . . . . . . . . .
2 o . . . . . . . .
1 . o . . . . . . .
A B C D E F G H J
""", None, -81),
('corner-all', [
"B A1", "W B1", "W A2",
"B J1", "W H1", "W J2",
"B A9", "W B9", "W A8",
"B J9", "W H9", "W J8",
], """\
9 . o . . . . . o .
8 o . . . . . . . o
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 . . . . . . . . .
2 o . . . . . . . o
1 . o . . . . . o .
A B C D E F G H J
""", None, -81),
('multiple', [
"W D4", "B D3", "W C5", "B C4", "W E5", "B E4",
"B B5", "B F5", "B C6", "B E6", "B D7", "W D6",
"B D5",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . # . . . . .
6 . . # . # . . . .
5 . # . # . # . . .
4 . . # . # . . . .
3 . . . # . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J
""", None, 81),
('large', [
"W D2", "W G2", "W E3", "W F3", "W F4", "W D5", "W E5", "W F5", "B E2", "B F2",
"B D3", "B G3", "B D4", "B G4", "B C5", "B G5", "B D6", "B E6", "B F6", "B E4",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . # # # . . .
5 . . # . . . # . .
4 . . . # # . # . .
3 . . . # . . # . .
2 . . . o # # o . .
1 . . . . . . . . .
A B C D E F G H J
""", None, 16),
('pre-recapture', [
"W A1", "W B1", "W B2", "W C2", "W D3", "W E3", "W A4", "W B4", "W C4", "W E4",
"B A2", "B D2", "B E2", "B A3", "B B3", "B F3", "B D4", "B F4", "B E5",
"B C3",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . # . . . .
4 o o o # . # . . .
3 # # # . . # . . .
2 # o o # # . . . .
1 o o . . . . . . .
A B C D E F G H J
""", None, 6),
('recapture', [
"W A1", "W B1", "W B2", "W C2", "W D3", "W E3", "W A4", "W B4", "W C4", "W E4",
"B A2", "B D2", "B E2", "B A3", "B B3", "B F3", "B D4", "B F4", "B E5",
"B C3", "W D3",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . # . . . .
4 o o o # . # . . .
3 . . . o . # . . .
2 . o o # # . . . .
1 o o . . . . . . .
A B C D E F G H J
""", None, -6),
('self-capture-1', [
"B D4", "B C5", "B E5", "B D6", "W D5",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . # . . . . .
5 . . # . # . . . .
4 . . . # . . . . .
3 . . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J
""", None, 81),
('self-capture-2', [
"B D4", "B E4", "B C5", "B F5", "B D6", "B E6", "W D5", "W E5",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . # # . . . .
5 . . # . . # . . .
4 . . . # # . . . .
3 . . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J
""", None, 81),
('self-capture-3', [
"B D4", "B E4", "B F4", "B C5", "B G5", "B D6", "B E6", "B F6",
"W D5", "W F5", "W E5",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . # # # . . .
5 . . # . . . # . .
4 . . . # # # . . .
3 . . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J
""", None, 81),
('self-capture-many', [
"B E2", "B D3", "B F3", "B D4", "B F4", "B G4", "B C5", "B H5", "B C6",
"B F6", "B G6", "B D7", "B E7",
"W E3", "W E4", "W D5", "W F5", "W G5", "W D6", "W E6", "W E5",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . # # . . . .
6 . . # . . # # . .
5 . . # . . . . # .
4 . . . # . # # . .
3 . . . # . # . . .
2 . . . . # . . . .
1 . . . . . . . . .
A B C D E F G H J
""", None, 81),
('ko-corner', [
"B A1", "B B2", "B A3",
"W B1", "W A2",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 # . . . . . . . .
2 o # . . . . . . .
1 . o . . . . . . .
A B C D E F G H J
""", 'A1', -1),
('notko-twocaptured', [
"B B2", "B B3", "B A4",
"W B1", "W A2", "W A3",
"B A1",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 # . . . . . . . .
3 . # . . . . . . .
2 . # . . . . . . .
1 # o . . . . . . .
A B C D E F G H J
""", None, 5),
('notko-tworecaptured', [
"B A1", "B B3", "B A4",
"W B1", "W B2", "W A3",
"B A2",
], """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 # . . . . . . . .
3 . # . . . . . . .
2 # o . . . . . . .
1 # o . . . . . . .
A B C D E F G H J
""", None, 3),
]
score_tests = [
# code, board representation, score
('empty', """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 . . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J
""", 0),
('onestone', """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 . . . . . . . . .
2 . # . . . . . . .
1 . . . . . . . . .
A B C D E F G H J
""", 81),
('easy', """\
9 . . . # o . . . .
8 . . . # o . . . .
7 . . . # o . . . .
6 . . . # o . . . .
5 . . . # o . . . .
4 . . . # o . . . .
3 . . . # o . . . .
2 . . . # o . . . .
1 . . . # o . . . .
A B C D E F G H J
""", -9),
('spoilt', """\
9 . . . # o . . . .
8 . . . # o . . . .
7 . . . # o . . . .
6 . . . # o . . . .
5 . . . # o . . # .
4 . . . # o . . . .
3 . . . # o . . . .
2 . . . # o . . . .
1 . . . # o . . . .
A B C D E F G H J
""", 28),
('busy', """\
9 . . o . . o # . #
8 . o o o o o # . #
7 . o . o o # # # o
6 o . o # o # o o o
5 o o # # # # # o o
4 . o o # . o # o .
3 o # # # o o o o o
2 . o o # # # o o .
1 o . o # . # o o o
A B C D E F G H J
""", -26),
]
setup_tests = [
# code, black points, white points, empty points, diagram, is_legal
('blank',
[],
[],
[],
"""\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 . . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J
""", True),
('simple',
['D4', 'D5'],
['B1', 'J9'],
[],
"""\
9 . . . . . . . . o
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . # . . . . .
4 . . . # . . . . .
3 . . . . . . . . .
2 . . . . . . . . .
1 . o . . . . . . .
A B C D E F G H J
""", True),
('illegal',
['A8', 'B9'],
['A9'],
[],
"""\
9 . # . . . . . . .
8 # . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 . . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J
""", False),
]

View File

@ -0,0 +1,184 @@
"""Tests for boards.py and ascii_boards.py
We test these together because it's convenient for later boards tests to use
ascii_boards facilities.
"""
from __future__ import with_statement
from gomill.common import format_vertex, move_from_vertex
from gomill import ascii_boards
from gomill import boards
from gomill_tests import gomill_test_support
from gomill_tests import board_test_data
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
for t in board_test_data.play_tests:
suite.addTest(Play_test_TestCase(*t))
for t in board_test_data.score_tests:
suite.addTest(Score_test_TestCase(*t))
for t in board_test_data.setup_tests:
suite.addTest(Setup_test_TestCase(*t))
def test_attributes(tc):
b = boards.Board(5)
tc.assertEqual(b.side, 5)
tc.assertEqual(
b.board_points,
[(0, 0), (0, 1), (0, 2), (0, 3), (0, 4),
(1, 0), (1, 1), (1, 2), (1, 3), (1, 4),
(2, 0), (2, 1), (2, 2), (2, 3), (2, 4),
(3, 0), (3, 1), (3, 2), (3, 3), (3, 4),
(4, 0), (4, 1), (4, 2), (4, 3), (4, 4)])
def test_basics(tc):
b = boards.Board(9)
tc.assertTrue(b.is_empty())
tc.assertItemsEqual(b.list_occupied_points(), [])
tc.assertEqual(b.get(2, 3), None)
b.play(2, 3, 'b')
tc.assertEqual(b.get(2, 3), 'b')
tc.assertFalse(b.is_empty())
b.play(3, 4, 'w')
with tc.assertRaises(ValueError):
b.play(3, 4, 'w')
tc.assertItemsEqual(b.list_occupied_points(),
[('b', (2, 3)), ('w', (3, 4))])
_9x9_expected = """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . o . . . .
3 . . . # . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J\
"""
_13x13_expected = """\
13 . . . . . . . . . . . . .
12 . . . . . . . . . . . . .
11 . . . . . . . . . . . . .
10 . . . . . . . . . . . . .
9 . . . . . . . . . . . . .
8 . . . . . . . . . . . . .
7 . . . . . . . . . . . . .
6 . . . . . . . . . . . . .
5 . . . . . . . . . . . . .
4 . . . . o . . . . . . . .
3 . . . # . . . . . . . . .
2 . . . . . . . . . . . . .
1 . . . . . . . . . . . . .
A B C D E F G H J K L M N\
"""
def test_render_board_9x9(tc):
b = boards.Board(9)
b.play(2, 3, 'b')
b.play(3, 4, 'w')
tc.assertDiagramEqual(ascii_boards.render_board(b), _9x9_expected)
def test_render_board_13x13(tc):
b = boards.Board(13)
b.play(2, 3, 'b')
b.play(3, 4, 'w')
tc.assertDiagramEqual(ascii_boards.render_board(b), _13x13_expected)
def test_interpret_diagram(tc):
b1 = boards.Board(9)
b1.play(2, 3, 'b')
b1.play(3, 4, 'w')
b2 = ascii_boards.interpret_diagram(_9x9_expected, 9)
tc.assertEqual(b1, b2)
b3 = boards.Board(9)
b4 = ascii_boards.interpret_diagram(_9x9_expected, 9, b3)
tc.assertIs(b3, b4)
tc.assertEqual(b1, b3)
tc.assertRaisesRegexp(ValueError, "board not empty",
ascii_boards.interpret_diagram, _9x9_expected, 9, b3)
b5 = boards.Board(19)
tc.assertRaisesRegexp(ValueError, "wrong board size, must be 9$",
ascii_boards.interpret_diagram, _9x9_expected, 9, b5)
tc.assertRaises(ValueError, ascii_boards.interpret_diagram, "nonsense", 9)
b6 = ascii_boards.interpret_diagram(_13x13_expected, 13)
tc.assertDiagramEqual(ascii_boards.render_board(b6), _13x13_expected)
def test_copy(tc):
b1 = boards.Board(9)
b1.play(2, 3, 'b')
b1.play(3, 4, 'w')
b2 = b1.copy()
tc.assertEqual(b1, b2)
b2.play(5, 5, 'b')
b2.play(2, 1, 'b')
tc.assertNotEqual(b1, b2)
b1.play(5, 5, 'b')
b1.play(2, 1, 'b')
tc.assertEqual(b1, b2)
class Play_test_TestCase(gomill_test_support.Gomill_ParameterisedTestCase):
"""Check final position reached by playing a sequence of moves."""
test_name = "play_test"
parameter_names = ('moves', 'diagram', 'ko_vertex', 'score')
def runTest(self):
b = boards.Board(9)
ko_point = None
for move in self.moves:
colour, vertex = move.split()
colour = colour.lower()
row, col = move_from_vertex(vertex, b.side)
ko_point = b.play(row, col, colour)
self.assertDiagramEqual(ascii_boards.render_board(b),
self.diagram.rstrip())
if ko_point is None:
ko_vertex = None
else:
ko_vertex = format_vertex(ko_point)
self.assertEqual(ko_vertex, self.ko_vertex, "wrong ko point")
self.assertEqual(b.area_score(), self.score, "wrong score")
class Score_test_TestCase(gomill_test_support.Gomill_ParameterisedTestCase):
"""Check score of a diagram."""
test_name = "score_test"
parameter_names = ('diagram', 'score')
def runTest(self):
b = ascii_boards.interpret_diagram(self.diagram, 9)
self.assertEqual(b.area_score(), self.score, "wrong score")
class Setup_test_TestCase(gomill_test_support.Gomill_ParameterisedTestCase):
"""Check apply_setup()."""
test_name = "setup_test"
parameter_names = ('black_points', 'white_points', 'empty_points',
'diagram', 'is_legal')
def runTest(self):
def _interpret(moves):
return [move_from_vertex(v, b.side) for v in moves]
b = boards.Board(9)
is_legal = b.apply_setup(_interpret(self.black_points),
_interpret(self.white_points),
_interpret(self.empty_points))
self.assertDiagramEqual(ascii_boards.render_board(b),
self.diagram.rstrip())
if self.is_legal:
self.assertTrue(is_legal, "setup should be considered legal")
else:
self.assertFalse(is_legal, "setup should be considered illegal")

View File

@ -0,0 +1,227 @@
"""Tests for cem_tuners.py"""
from __future__ import with_statement, division
import cPickle as pickle
from textwrap import dedent
from gomill import cem_tuners
from gomill.game_jobs import Game_job, Game_job_result
from gomill.gtp_games import Game_result
from gomill.cem_tuners import Parameter_config
from gomill.competitions import (
Player_config, CompetitionError, ControlFileError)
from gomill_tests import gomill_test_support
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def simple_make_candidate(*args):
if -1 in args:
raise ValueError("oops")
return Player_config("cand " + " ".join(map(str, args)))
def clip_axisb(f):
f = float(f)
return max(0.0, min(100.0, f))
def default_config():
return {
'board_size' : 13,
'komi' : 7.5,
'players' : {
'opp' : Player_config("test"),
},
'candidate_colour' : 'w',
'opponent' : 'opp',
'parameters' : [
Parameter_config(
'axisa',
initial_mean = 0.5,
initial_variance = 1.0,
format = "axa %.3f"),
Parameter_config(
'axisb',
initial_mean = 50.0,
initial_variance = 1000.0,
transform = clip_axisb,
format = "axb %.1f"),
],
'batch_size' : 3,
'samples_per_generation' : 4,
'number_of_generations' : 3,
'elite_proportion' : 0.1,
'step_size' : 0.8,
'make_candidate' : simple_make_candidate,
}
def test_parameter_config(tc):
comp = cem_tuners.Cem_tuner('cemtest')
config = default_config()
comp.initialise_from_control_file(config)
tc.assertEqual(comp.initial_distribution.format(),
" 0.50~1.00 50.00~1000.00")
tc.assertEqual(comp.format_engine_parameters((0.5, 23)),
"axa 0.500; axb 23.0")
tc.assertEqual(comp.format_engine_parameters(('x', 23)),
"[axisa?x]; axb 23.0")
tc.assertEqual(comp.format_optimiser_parameters((0.5, 500)),
"axa 0.500; axb 100.0")
tc.assertEqual(comp.transform_parameters((0.5, 1e6)), (0.5, 100.0))
with tc.assertRaises(CompetitionError) as ar:
comp.transform_parameters((0.5, None))
tc.assertTracebackStringEqual(str(ar.exception), dedent("""\
error from transform for axisb
TypeError: expected-float
traceback (most recent call last):
cem_tuner_tests|clip_axisb
failing line:
f = float(f)
"""), fixups=[
("float() argument must be a string or a number", "expected-float"),
("expected float, got NoneType object", "expected-float"),
])
tc.assertRaisesRegexp(
ValueError, "'initial_variance': must be nonnegative",
comp.parameter_spec_from_config,
Parameter_config('pa1', initial_mean=0,
initial_variance=-1, format="%s"))
tc.assertRaisesRegexp(
ControlFileError, "'format': invalid format string",
comp.parameter_spec_from_config,
Parameter_config('pa1', initial_mean=0, initial_variance=1,
format="nopct"))
def test_nonsense_parameter_config(tc):
comp = cem_tuners.Cem_tuner('cemtest')
config = default_config()
config['parameters'].append(99)
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertMultiLineEqual(str(ar.exception), dedent("""\
'parameters': item 2: not a Parameter"""))
def test_transform_check(tc):
comp = cem_tuners.Cem_tuner('cemtest')
config = default_config()
config['parameters'][0] = Parameter_config(
'axisa',
initial_mean = 0.5,
initial_variance = 1.0,
transform = str.split)
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertTracebackStringEqual(str(ar.exception), dedent("""\
parameter axisa: error from transform (applied to initial_mean)
TypeError: split-wants-float-not-str
traceback (most recent call last):
"""), fixups=[
("descriptor 'split' requires a 'str' object but received a 'float'",
"split-wants-float-not-str"),
("unbound method split() must be called with str instance as "
"first argument (got float instance instead)",
"split-wants-float-not-str"),
])
def test_format_validation(tc):
comp = cem_tuners.Cem_tuner('cemtest')
config = default_config()
config['parameters'][0] = Parameter_config(
'axisa',
initial_mean = 0.5,
initial_variance = 1.0,
transform = str,
format = "axa %f")
tc.assertRaisesRegexp(
ControlFileError, "'format': invalid format string",
comp.initialise_from_control_file, config)
def test_make_candidate(tc):
comp = cem_tuners.Cem_tuner('cemtest')
config = default_config()
comp.initialise_from_control_file(config)
cand = comp.make_candidate('g0#1', (0.5, 23.0))
tc.assertEqual(cand.code, 'g0#1')
tc.assertListEqual(cand.cmd_args, ['cand', '0.5', '23.0'])
with tc.assertRaises(CompetitionError) as ar:
comp.make_candidate('g0#1', (-1, 23))
tc.assertTracebackStringEqual(str(ar.exception), dedent("""\
error from make_candidate()
ValueError: oops
traceback (most recent call last):
cem_tuner_tests|simple_make_candidate
failing line:
raise ValueError("oops")
"""))
def test_play(tc):
comp = cem_tuners.Cem_tuner('cemtest')
comp.initialise_from_control_file(default_config())
comp.set_clean_status()
tc.assertEqual(comp.generation, 0)
tc.assertEqual(comp.distribution.format(),
" 0.50~1.00 50.00~1000.00")
job1 = comp.get_game()
tc.assertIsInstance(job1, Game_job)
tc.assertEqual(job1.game_id, 'g0#0r0')
tc.assertEqual(job1.player_b.code, 'g0#0')
tc.assertEqual(job1.player_w.code, 'opp')
tc.assertEqual(job1.board_size, 13)
tc.assertEqual(job1.komi, 7.5)
tc.assertEqual(job1.move_limit, 1000)
tc.assertIs(job1.use_internal_scorer, False)
tc.assertEqual(job1.internal_scorer_handicap_compensation, 'full')
tc.assertEqual(job1.game_data, (0, 'g0#0', 0))
tc.assertEqual(job1.sgf_event, 'cemtest')
tc.assertRegexpMatches(job1.sgf_note, '^Candidate parameters: axa ')
job2 = comp.get_game()
tc.assertIsInstance(job2, Game_job)
tc.assertEqual(job2.game_id, 'g0#1r0')
tc.assertEqual(job2.player_b.code, 'g0#1')
tc.assertEqual(job2.player_w.code, 'opp')
tc.assertEqual(comp.wins, [0, 0, 0, 0])
result1 = Game_result({'b' : 'g0#0', 'w' : 'opp'}, 'b')
result1.sgf_result = "B+8.5"
response1 = Game_job_result()
response1.game_id = job1.game_id
response1.game_result = result1
response1.engine_names = {
'opp' : 'opp engine:v1.2.3',
'g0#0' : 'candidate engine',
}
response1.engine_descriptions = {
'opp' : 'opp engine:v1.2.3',
'g0#0' : 'candidate engine description',
}
response1.game_data = job1.game_data
comp.process_game_result(response1)
tc.assertEqual(comp.wins, [1, 0, 0, 0])
comp2 = cem_tuners.Cem_tuner('cemtest')
comp2.initialise_from_control_file(default_config())
status = pickle.loads(pickle.dumps(comp.get_status()))
comp2.set_status(status)
tc.assertEqual(comp2.wins, [1, 0, 0, 0])
result2 = Game_result({'b' : 'g0#1', 'w' : 'opp'}, None)
result2.set_jigo()
response2 = Game_job_result()
response2.game_id = job2.game_id
response2.game_result = result2
response2.engine_names = response1.engine_names
response2.engine_descriptions = response1.engine_descriptions
response2.game_data = job2.game_data
comp.process_game_result(response2)
tc.assertEqual(comp.wins, [1, 0.5, 0, 0])

View File

@ -0,0 +1,72 @@
"""Tests for common.py."""
import string
from gomill_tests import gomill_test_support
from gomill import common
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def test_opponent_of(tc):
oo = common.opponent_of
tc.assertEqual(oo('b'), 'w')
tc.assertEqual(oo('w'), 'b')
tc.assertRaises(ValueError, oo, 'x')
tc.assertRaises(ValueError, oo, None)
tc.assertRaises(ValueError, oo, 'B')
def test_colour_name(tc):
cn = common.colour_name
tc.assertEqual(cn('b'), 'black')
tc.assertEqual(cn('w'), 'white')
tc.assertRaises(ValueError, cn, 'x')
tc.assertRaises(ValueError, cn, None)
tc.assertRaises(ValueError, cn, 'B')
def test_column_letters(tc):
tc.assertEqual(common.column_letters,
"".join(c for c in string.uppercase if c != 'I'))
def test_format_vertex(tc):
fv = common.format_vertex
tc.assertEqual(fv(None), "pass")
tc.assertEqual(fv((0, 0)), "A1")
tc.assertEqual(fv((8, 8)), "J9")
tc.assertEqual(fv((1, 5)), "F2")
tc.assertEqual(fv((24, 24)), "Z25")
tc.assertRaises(ValueError, fv, (-1, 2))
tc.assertRaises(ValueError, fv, (2, -1))
tc.assertRaises(ValueError, fv, (25, 1))
tc.assertRaises(ValueError, fv, (1, 25))
def test_format_vertex_list(tc):
fvl = common.format_vertex_list
tc.assertEqual(fvl([]), "")
tc.assertEqual(fvl([(0, 0)]), "A1")
tc.assertEqual(fvl([(0, 0), (1, 5)]), "A1,F2")
tc.assertEqual(fvl([(0, 0), None, (1, 5)]), "A1,pass,F2")
def test_move_from_vertex(tc):
cv = common.move_from_vertex
tc.assertEqual(cv("pass", 9), None)
tc.assertEqual(cv("pAss", 9), None)
tc.assertEqual(cv("A1", 9), (0, 0))
tc.assertEqual(cv("a1", 9), (0, 0))
tc.assertEqual(cv("A01", 9), (0, 0))
tc.assertEqual(cv("J9", 9), (8, 8))
tc.assertEqual(cv("M11", 19), (10, 11))
tc.assertRaises(ValueError, cv, "M11", 9)
tc.assertRaises(ValueError, cv, "K9", 9)
tc.assertRaises(ValueError, cv, "J10", 9)
tc.assertRaises(ValueError, cv, "I5", 9)
tc.assertRaises(ValueError, cv, "", 9)
tc.assertRaises(ValueError, cv, "29", 9)
tc.assertRaises(ValueError, cv, "@9", 9)
tc.assertRaises(ValueError, cv, "A-3", 9)
tc.assertRaises(ValueError, cv, None, 9)
tc.assertRaises(ValueError, cv, "A1", 0)
tc.assertRaises(ValueError, cv, "A1", 30)

View File

@ -0,0 +1,98 @@
"""Tests for competition_schedulers.py"""
import cPickle as pickle
from gomill import competition_schedulers
from gomill_tests import gomill_test_support
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def test_simple(tc):
sc = competition_schedulers.Simple_scheduler()
def issue(n):
result = [sc.issue() for _ in xrange(n)]
sc._check_consistent()
return result
sc._check_consistent()
tc.assertEqual(issue(4), [0, 1, 2, 3])
sc.fix(2)
sc._check_consistent()
sc.fix(1)
sc._check_consistent()
tc.assertEqual(sc.issue(), 4)
tc.assertEqual(sc.fixed, 2)
tc.assertEqual(sc.issued, 5)
sc.rollback()
sc._check_consistent()
tc.assertEqual(sc.issued, 2)
tc.assertEqual(sc.fixed, 2)
tc.assertListEqual(issue(2), [0, 3])
sc.rollback()
sc._check_consistent()
tc.assertListEqual(issue(4), [0, 3, 4, 5])
sc.fix(3)
sc._check_consistent()
sc.fix(5)
sc._check_consistent()
tc.assertEqual(sc.issue(), 6)
sc._check_consistent()
sc = pickle.loads(pickle.dumps(sc))
sc._check_consistent()
sc.rollback()
sc._check_consistent()
tc.assertListEqual(issue(6), [0, 4, 6, 7, 8, 9])
tc.assertEqual(sc.issued, 10)
tc.assertEqual(sc.fixed, 4)
def test_grouped(tc):
sc = competition_schedulers.Group_scheduler()
def issue(n):
return [sc.issue() for _ in xrange(n)]
sc.set_groups([('m1', 4), ('m2', None)])
tc.assertTrue(sc.nothing_issued_yet())
tc.assertFalse(sc.all_fixed())
tc.assertListEqual(issue(3), [
('m1', 0),
('m2', 0),
('m1', 1),
])
tc.assertFalse(sc.nothing_issued_yet())
sc.fix('m1', 1)
sc.rollback()
issued = issue(14)
tc.assertListEqual(issued, [
('m2', 0),
('m1', 0),
('m2', 1),
('m1', 2),
('m2', 2),
('m1', 3),
('m2', 3),
('m2', 4),
('m2', 5),
('m2', 6),
('m2', 7),
('m2', 8),
('m2', 9),
('m2', 10),
])
tc.assertFalse(sc.all_fixed())
for token in issued:
sc.fix(*token)
tc.assertTrue(sc.all_fixed())

View File

@ -0,0 +1,80 @@
"""Test support code for testing Competitions and Ringmasters."""
import cPickle as pickle
from cStringIO import StringIO
from gomill import game_jobs
from gomill import gtp_games
def fake_response(job, winner):
"""Produce a response for the specified job.
job -- Game_job
winner -- winning colour (None for a jigo, 'unknown' for unknown result)
The winning margin (if not a jigo) is 1.5.
"""
players = {'b' : job.player_b.code, 'w' : job.player_w.code}
if winner == 'unknown':
winner = None
is_unknown = True
else:
is_unknown = False
result = gtp_games.Game_result(players, winner)
result.game_id = job.game_id
if winner is None:
if is_unknown:
result.sgf_result = "Void"
result.detail = "fake unknown result"
else:
result.set_jigo()
else:
result.sgf_result += "1.5"
response = game_jobs.Game_job_result()
response.game_id = job.game_id
response.game_result = result
response.engine_names = {
job.player_b.code : '%s engine:v1.2.3' % job.player_b.code,
job.player_w.code : '%s engine' % job.player_w.code,
}
response.engine_descriptions = {
job.player_b.code : '%s engine:v1.2.3' % job.player_b.code,
job.player_w.code : '%s engine\ntestdescription' % job.player_w.code,
}
response.game_data = job.game_data
response.warnings = []
response.log_entries = []
return response
def get_screen_report(comp):
"""Retrieve a competition's screen report."""
out = StringIO()
comp.write_screen_report(out)
return out.getvalue()
def get_short_report(comp):
"""Retrieve a competition's short report."""
out = StringIO()
comp.write_short_report(out)
return out.getvalue()
def check_screen_report(tc, comp, expected):
"""Check that a competition's screen report is as expected."""
tc.assertMultiLineEqual(get_screen_report(comp), expected)
def check_round_trip(tc, comp, config):
"""Check that a competition round-trips through saved state.
Makes a new Competition, loads it from comp's saved state, and checks that
the resulting screen report is identical.
Returns the new Competition.
"""
comp2 = comp.__class__(comp.competition_code)
comp2.initialise_from_control_file(config)
status = pickle.loads(pickle.dumps(comp.get_status()))
comp2.set_status(status)
check_screen_report(tc, comp2, get_screen_report(comp))
return comp2

View File

@ -0,0 +1,165 @@
"""Tests for competitions.py"""
import os
from gomill import competitions
from gomill.competitions import ControlFileError, Player_config
from gomill_tests import gomill_test_support
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def test_global_config(tc):
comp = competitions.Competition('test')
config = {
'description' : "\nsome\ndescription ",
'players' : {},
}
comp.initialise_from_control_file(config)
tc.assertEqual(comp.description, "some\ndescription")
def test_player_config(tc):
comp = competitions.Competition('test')
p1 = comp.game_jobs_player_from_config('pp', Player_config("cmd"))
tc.assertEqual(p1.code, 'pp')
tc.assertEqual(p1.cmd_args, ["cmd"])
p2 = comp.game_jobs_player_from_config('pp', Player_config(command="cmd"))
tc.assertEqual(p2.code, 'pp')
tc.assertEqual(p2.cmd_args, ["cmd"])
tc.assertRaisesRegexp(
Exception, "'command' not specified",
comp.game_jobs_player_from_config, 'pp',
Player_config())
tc.assertRaisesRegexp(
Exception, "too many positional arguments",
comp.game_jobs_player_from_config, 'pp',
Player_config("cmd", "xxx"))
tc.assertRaisesRegexp(
Exception, "command specified as both positional and keyword argument",
comp.game_jobs_player_from_config, 'pp',
Player_config("cmd", command="cmd2"))
tc.assertRaisesRegexp(
Exception, "unknown argument 'unexpected'",
comp.game_jobs_player_from_config, 'pp',
Player_config("cmd", unexpected=3))
def test_bad_player(tc):
comp = competitions.Competition('test')
config = {
'players' : {
't1' : Player_config("test"),
't2' : None,
}
}
tc.assertRaisesRegexp(
ControlFileError, "'players': bad value for 't2': not a Player",
comp.initialise_from_control_file, config)
def test_player_command(tc):
comp = competitions.Competition('test')
comp.set_base_directory("/base")
config = {
'players' : {
't1' : Player_config("test"),
't2' : Player_config("/bin/test foo"),
't3' : Player_config(["bin/test", "foo"]),
't4' : Player_config("~/test foo"),
't5' : Player_config("~root"),
}
}
comp.initialise_from_control_file(config)
tc.assertEqual(comp.players['t1'].cmd_args, ["test"])
tc.assertEqual(comp.players['t2'].cmd_args, ["/bin/test", "foo"])
tc.assertEqual(comp.players['t3'].cmd_args, ["/base/bin/test", "foo"])
tc.assertEqual(comp.players['t4'].cmd_args,
[os.path.expanduser("~") + "/test", "foo"])
tc.assertEqual(comp.players['t5'].cmd_args, ["~root"])
def test_player_is_reliable_scorer(tc):
comp = competitions.Competition('test')
config = {
'players' : {
't1' : Player_config("test"),
't2' : Player_config("test", is_reliable_scorer=False),
't3' : Player_config("test", is_reliable_scorer=True),
}
}
comp.initialise_from_control_file(config)
tc.assertTrue(comp.players['t1'].is_reliable_scorer)
tc.assertFalse(comp.players['t2'].is_reliable_scorer)
tc.assertTrue(comp.players['t3'].is_reliable_scorer)
def test_player_cwd(tc):
comp = competitions.Competition('test')
comp.set_base_directory("/base")
config = {
'players' : {
't1' : Player_config("test"),
't2' : Player_config("test", cwd="/abs"),
't3' : Player_config("test", cwd="rel/sub"),
't4' : Player_config("test", cwd="."),
't5' : Player_config("test", cwd="~/tmp/sub"),
}
}
comp.initialise_from_control_file(config)
tc.assertIsNone(comp.players['t1'].cwd)
tc.assertEqual(comp.players['t2'].cwd, "/abs")
tc.assertEqual(comp.players['t3'].cwd, "/base/rel/sub")
tc.assertEqual(comp.players['t4'].cwd, "/base/.")
tc.assertEqual(comp.players['t5'].cwd, os.path.expanduser("~") + "/tmp/sub")
def test_player_stderr(tc):
comp = competitions.Competition('test')
config = {
'players' : {
't1' : Player_config("test"),
't2' : Player_config("test", discard_stderr=True),
't3' : Player_config("test", discard_stderr=False),
}
}
comp.initialise_from_control_file(config)
tc.assertIs(comp.players['t1'].discard_stderr, False)
tc.assertEqual(comp.players['t2'].discard_stderr, True)
tc.assertIs(comp.players['t3'].discard_stderr, False)
def test_player_startup_gtp_commands(tc):
comp = competitions.Competition('test')
config = {
'players' : {
't1' : Player_config(
"test", startup_gtp_commands=["foo"]),
't2' : Player_config(
"test", startup_gtp_commands=["foo bar baz"]),
't3' : Player_config(
"test", startup_gtp_commands=[["foo", "bar", "baz"]]),
't4' : Player_config(
"test", startup_gtp_commands=[
"xyzzy test",
["foo", "bar", "baz"]]),
}
}
comp.initialise_from_control_file(config)
tc.assertListEqual(comp.players['t1'].startup_gtp_commands,
[("foo", [])])
tc.assertListEqual(comp.players['t2'].startup_gtp_commands,
[("foo", ["bar", "baz"])])
tc.assertListEqual(comp.players['t3'].startup_gtp_commands,
[("foo", ["bar", "baz"])])
tc.assertListEqual(comp.players['t4'].startup_gtp_commands,
[("xyzzy", ["test"]),
("foo", ["bar", "baz"])])
def test_player_gtp_aliases(tc):
comp = competitions.Competition('test')
config = {
'players' : {
't1' : Player_config(
"test", gtp_aliases={'foo' : 'bar', 'baz' : 'quux'}),
}
}
comp.initialise_from_control_file(config)
tc.assertDictEqual(comp.players['t1'].gtp_aliases,
{'foo' : 'bar', 'baz' : 'quux'})

View File

@ -0,0 +1,458 @@
"""Tests for game_jobs.py"""
from __future__ import with_statement
import os
from textwrap import dedent
from gomill import gtp_controller
from gomill import game_jobs
from gomill.gtp_engine import GtpError, GtpFatalError
from gomill.job_manager import JobFailed
from gomill_tests import test_framework
from gomill_tests import gomill_test_support
from gomill_tests import gtp_engine_fixtures
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
### Game_job proper
class Test_game_job(game_jobs.Game_job):
"""Variant of Game_job that doesn't write sgf files.
Additional attributes:
_sgf_pathname_written -- pathname sgf file would have been written to
_sgf_written -- contents that would have been written
_mkdir_pathname -- directory pathname that would have been created
"""
def __init__(self, *args, **kwargs):
game_jobs.Game_job.__init__(self, *args, **kwargs)
self._sgf_pathname_written = None
self._sgf_written = None
self._mkdir_pathname = None
def _write_sgf(self, pathname, sgf_string):
self._sgf_pathname_written = pathname
self._sgf_written = sgf_string
def _mkdir(self, pathname):
self._mkdir_pathname = pathname
def _get_sgf_written(self):
"""Return the 'scrubbed' sgf contents."""
return gomill_test_support.scrub_sgf(self._sgf_written)
class Game_job_fixture(test_framework.Fixture):
"""Fixture setting up a Game_job.
attributes:
job -- game_jobs.Game_job (in fact, a Test_game_job)
"""
def __init__(self, tc):
player_b = game_jobs.Player()
player_b.code = 'one'
player_b.cmd_args = ['test', 'id=one']
player_w = game_jobs.Player()
player_w.code = 'two'
player_w.cmd_args = ['test', 'id=two']
self.job = Test_game_job()
self.job.game_id = 'gameid'
self.job.player_b = player_b
self.job.player_w = player_w
self.job.board_size = 9
self.job.komi = 7.5
self.job.move_limit = 1000
self.job.sgf_dirname = "/sgf/test.games"
self.job.void_sgf_dirname = "/sgf/test.void"
self.job.sgf_filename = "gjtest.sgf"
def test_player_copy(tc):
gj = Game_job_fixture(tc)
p1 = gj.job.player_b
p2 = p1.copy("clone")
tc.assertEqual(p2.code, "clone")
tc.assertEqual(p2.cmd_args, ['test', 'id=one'])
tc.assertIsNot(p1.cmd_args, p2.cmd_args)
def test_game_job(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
gj = Game_job_fixture(tc)
gj.job.game_data = 'gamedata'
gj.job.sgf_game_name = "gjt 0_000"
gj.job.sgf_event = "game_job_tests"
gj.job.sgf_note = "test sgf_note\non two lines"
result = gj.job.run()
# Win by 18 on the board minus 7.5 komi
tc.assertEqual(result.game_result.sgf_result, "B+10.5")
tc.assertEqual(result.game_id, 'gameid')
tc.assertEqual(result.game_result.game_id, 'gameid')
tc.assertEqual(result.game_data, 'gamedata')
tc.assertEqual(result.warnings, [])
tc.assertEqual(result.log_entries, [])
channel = fx.get_channel('one')
tc.assertIsNone(channel.requested_stderr)
tc.assertIsNone(channel.requested_cwd)
tc.assertIsNone(channel.requested_env)
tc.assertEqual(gj.job._sgf_pathname_written, '/sgf/test.games/gjtest.sgf')
tc.assertIsNone(gj.job._mkdir_pathname)
tc.assertMultiLineEqual(gj.job._get_sgf_written(), dedent("""\
(;FF[4]AP[gomill:VER]
C[Event: game_job_tests
Game id gameid
Date ***
Result one beat two B+10.5
test sgf_note
on two lines
Black one one
White two two]
CA[UTF-8]DT[***]EV[game_job_tests]GM[1]GN[gjt 0_000]KM[7.5]PB[one]
PW[two]RE[B+10.5]SZ[9];B[ei];W[gi];B[eh];W[gh];B[eg];W[gg];B[ef];W[gf];B[ee];
W[ge];B[ed];W[gd];B[ec];W[gc];B[eb];W[gb];B[ea];W[ga];B[tt];
C[one beat two B+10.5]W[tt])
"""))
def test_duplicate_player_codes(tc):
gj = Game_job_fixture(tc)
gj.job.player_w.code = "one"
tc.assertRaisesRegexp(
JobFailed, "error creating game: player codes must be distinct",
gj.job.run)
def test_game_job_no_sgf(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
gj = Game_job_fixture(tc)
gj.job.sgf_dirname = None
result = gj.job.run()
tc.assertEqual(result.game_result.sgf_result, "B+10.5")
tc.assertIsNone(gj.job._sgf_pathname_written)
def test_game_job_forfeit(tc):
def handle_genmove(args):
raise GtpError("error")
def reject_genmove(channel):
channel.engine.add_command('genmove', handle_genmove)
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
fx.register_init_callback('reject_genmove', reject_genmove)
gj = Game_job_fixture(tc)
gj.job.player_w.cmd_args.append('init=reject_genmove')
result = gj.job.run()
tc.assertEqual(result.game_result.sgf_result, "B+F")
tc.assertEqual(
result.game_result.detail,
"forfeit: failure response from 'genmove w' to player two:\n"
"error")
tc.assertEqual(
result.warnings,
["forfeit: failure response from 'genmove w' to player two:\n"
"error"])
tc.assertEqual(result.log_entries, [])
tc.assertEqual(gj.job._sgf_pathname_written, '/sgf/test.games/gjtest.sgf')
def test_game_job_forfeit_and_quit(tc):
def handle_genmove(args):
raise GtpFatalError("I'm out of here")
def reject_genmove(channel):
channel.engine.add_command('genmove', handle_genmove)
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
fx.register_init_callback('reject_genmove', reject_genmove)
gj = Game_job_fixture(tc)
gj.job.player_w.cmd_args.append('init=reject_genmove')
result = gj.job.run()
tc.assertEqual(result.game_result.sgf_result, "B+F")
tc.assertEqual(
result.game_result.detail,
"forfeit: failure response from 'genmove w' to player two:\n"
"I'm out of here")
tc.assertEqual(
result.warnings,
["forfeit: failure response from 'genmove w' to player two:\n"
"I'm out of here"])
tc.assertEqual(
result.log_entries,
["error sending 'known_command gomill-cpu_time' to player two:\n"
"engine has closed the command channel"])
tc.assertEqual(gj.job._sgf_pathname_written, '/sgf/test.games/gjtest.sgf')
def test_game_job_exec_failure(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
gj = Game_job_fixture(tc)
gj.job.player_w.cmd_args.append('fail=startup')
with tc.assertRaises(JobFailed) as ar:
gj.job.run()
tc.assertEqual(str(ar.exception),
"aborting game due to error:\n"
"error starting subprocess for player two:\n"
"exec forced to fail")
# No void sgf file unless at least one move was played
tc.assertIsNone(gj.job._sgf_pathname_written)
def test_game_job_channel_error(tc):
def fail_first_genmove(channel):
channel.fail_command = 'genmove'
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
fx.register_init_callback('fail_first_genmove', fail_first_genmove)
gj = Game_job_fixture(tc)
gj.job.player_w.cmd_args.append('init=fail_first_genmove')
with tc.assertRaises(JobFailed) as ar:
gj.job.run()
tc.assertEqual(str(ar.exception),
"aborting game due to error:\n"
"transport error sending 'genmove w' to player two:\n"
"forced failure for send_command_line")
tc.assertEqual(gj.job._sgf_pathname_written, '/sgf/test.void/gjtest.sgf')
tc.assertEqual(gj.job._mkdir_pathname, '/sgf/test.void')
tc.assertMultiLineEqual(gj.job._get_sgf_written(), dedent("""\
(;FF[4]AP[gomill:VER]
C[Game id gameid
Date ***
Black one one
White two two]CA[UTF-8]
DT[***]GM[1]GN[gameid]KM[7.5]PB[one]PW[two]RE[Void]SZ[9];B[ei]
C[aborting game due to error:
transport error sending 'genmove w' to player two:
forced failure for send_command_line]
)
"""))
def test_game_job_late_errors(tc):
def fail_close(channel):
channel.fail_close = True
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
fx.register_init_callback('fail_close', fail_close)
gj = Game_job_fixture(tc)
gj.job.player_w.cmd_args.append('init=fail_close')
result = gj.job.run()
tc.assertEqual(result.game_result.sgf_result, "B+10.5")
tc.assertEqual(result.warnings, [])
tc.assertEqual(result.log_entries,
["error closing player two:\nforced failure for close"])
def test_game_job_late_error_from_void_game(tc):
def fail_genmove_and_close(channel):
channel.fail_command = 'genmove'
channel.fail_close = True
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
fx.register_init_callback('fail_genmove_and_close', fail_genmove_and_close)
gj = Game_job_fixture(tc)
gj.job.player_w.cmd_args.append('init=fail_genmove_and_close')
with tc.assertRaises(JobFailed) as ar:
gj.job.run()
tc.assertMultiLineEqual(
str(ar.exception),
"aborting game due to error:\n"
"transport error sending 'genmove w' to player two:\n"
"forced failure for send_command_line\n"
"also:\n"
"error closing player two:\n"
"forced failure for close")
tc.assertEqual(gj.job._sgf_pathname_written, '/sgf/test.void/gjtest.sgf')
def test_game_job_cwd_env(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
gj = Game_job_fixture(tc)
gj.job.player_b.cwd = "/nonexistent_directory"
gj.job.player_b.environ = {'GOMILL_TEST' : 'gomill'}
result = gj.job.run()
channel = fx.get_channel('one')
tc.assertIsNone(channel.requested_stderr)
tc.assertEqual(channel.requested_cwd, "/nonexistent_directory")
tc.assertEqual(channel.requested_env['GOMILL_TEST'], 'gomill')
# Check environment was merged, not replaced
tc.assertIn('PATH', channel.requested_env)
tc.assertEqual(gj.job._sgf_pathname_written, '/sgf/test.games/gjtest.sgf')
def test_game_job_stderr_discarded(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
gj = Game_job_fixture(tc)
gj.job.player_b.discard_stderr = True
result = gj.job.run()
channel = fx.get_channel('one')
tc.assertIsInstance(channel.requested_stderr, file)
tc.assertEqual(channel.requested_stderr.name, os.devnull)
def test_game_job_stderr_set(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
gj = Game_job_fixture(tc)
gj.job.stderr_pathname = "/dev/full"
result = gj.job.run()
channel = fx.get_channel('one')
tc.assertIsInstance(channel.requested_stderr, file)
tc.assertEqual(channel.requested_stderr.name, "/dev/full")
def test_game_job_stderr_set_and_discarded(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
gj = Game_job_fixture(tc)
gj.job.player_b.discard_stderr = True
result = gj.job.run()
channel = fx.get_channel('one')
tc.assertIsInstance(channel.requested_stderr, file)
tc.assertEqual(channel.requested_stderr.name, os.devnull)
def test_game_job_gtp_aliases(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
gj = Game_job_fixture(tc)
gj.job.player_w.gtp_aliases = {'genmove': 'fail'}
result = gj.job.run()
tc.assertEqual(result.game_result.sgf_result, "B+F")
def test_game_job_claim(tc):
def handle_genmove_ex(args):
tc.assertIn('claim', args)
return "claim"
def register_genmove_ex(channel):
channel.engine.add_command('gomill-genmove_ex', handle_genmove_ex)
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
fx.register_init_callback('genmove_ex', register_genmove_ex)
gj = Game_job_fixture(tc)
gj.job.player_b.cmd_args.append('init=genmove_ex')
gj.job.player_w.cmd_args.append('init=genmove_ex')
gj.job.player_w.allow_claim = True
result = gj.job.run()
tc.assertEqual(result.game_result.sgf_result, "W+")
tc.assertEqual(gj.job._sgf_pathname_written, '/sgf/test.games/gjtest.sgf')
def test_game_job_handicap(tc):
def handle_fixed_handicap(args):
return "D4 K10 D10"
def register_fixed_handicap(channel):
channel.engine.add_command('fixed_handicap', handle_fixed_handicap)
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
fx.register_init_callback('handicap', register_fixed_handicap)
gj = Game_job_fixture(tc)
gj.job.player_b.cmd_args.append('init=handicap')
gj.job.player_w.cmd_args.append('init=handicap')
gj.job.board_size = 13
gj.job.handicap = 3
gj.job.handicap_is_free = False
gj.job.internal_scorer_handicap_compensation = 'full'
result = gj.job.run()
# area score 53, less 7.5 komi, less 3 handicap compensation
tc.assertEqual(result.game_result.sgf_result, "B+42.5")
### check_player
class Player_check_fixture(test_framework.Fixture):
"""Fixture setting up a Player_check.
attributes:
player -- game_jobs.Player
check -- game_jobs.Player_check
"""
def __init__(self, tc):
self.player = game_jobs.Player()
self.player.code = 'test'
self.player.cmd_args = ['test', 'id=test']
self.check = game_jobs.Player_check()
self.check.player = self.player
self.check.board_size = 9
self.check.komi = 7.0
def test_check_player(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
ck = Player_check_fixture(tc)
tc.assertEqual(game_jobs.check_player(ck.check), [])
channel = fx.get_channel('test')
tc.assertIsNone(channel.requested_stderr)
tc.assertIsNone(channel.requested_cwd)
tc.assertIsNone(channel.requested_env)
def test_check_player_discard_stderr(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
ck = Player_check_fixture(tc)
tc.assertEqual(game_jobs.check_player(ck.check, discard_stderr=True), [])
channel = fx.get_channel('test')
tc.assertIsInstance(channel.requested_stderr, file)
tc.assertEqual(channel.requested_stderr.name, os.devnull)
def test_check_player_boardsize_fails(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
engine = gtp_engine_fixtures.get_test_engine()
fx.register_engine('no_boardsize', engine)
ck = Player_check_fixture(tc)
ck.player.cmd_args.append('engine=no_boardsize')
with tc.assertRaises(game_jobs.CheckFailed) as ar:
game_jobs.check_player(ck.check)
tc.assertEqual(str(ar.exception),
"failure response from 'boardsize 9' to test:\n"
"unknown command")
def test_check_player_startup_gtp_commands(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
ck = Player_check_fixture(tc)
ck.player.startup_gtp_commands = [('list_commands', []),
('nonexistent', ['command'])]
with tc.assertRaises(game_jobs.CheckFailed) as ar:
game_jobs.check_player(ck.check)
tc.assertEqual(str(ar.exception),
"failure response from 'nonexistent command' to test:\n"
"unknown command")
def test_check_player_nonexistent_cwd(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
ck = Player_check_fixture(tc)
ck.player.cwd = "/nonexistent/directory"
with tc.assertRaises(game_jobs.CheckFailed) as ar:
game_jobs.check_player(ck.check)
tc.assertEqual(str(ar.exception),
"bad working directory: /nonexistent/directory")
def test_check_player_cwd(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
ck = Player_check_fixture(tc)
ck.player.cwd = "/"
tc.assertEqual(game_jobs.check_player(ck.check), [])
channel = fx.get_channel('test')
tc.assertEqual(channel.requested_cwd, "/")
def test_check_player_env(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
ck = Player_check_fixture(tc)
ck.player.environ = {'GOMILL_TEST' : 'gomill'}
tc.assertEqual(game_jobs.check_player(ck.check), [])
channel = fx.get_channel('test')
tc.assertEqual(channel.requested_env['GOMILL_TEST'], 'gomill')
# Check environment was merged, not replaced
tc.assertIn('PATH', channel.requested_env)
def test_check_player_exec_failure(tc):
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
ck = Player_check_fixture(tc)
ck.player.cmd_args.append('fail=startup')
with tc.assertRaises(game_jobs.CheckFailed) as ar:
game_jobs.check_player(ck.check)
tc.assertEqual(str(ar.exception),
"error starting subprocess for test:\n"
"exec forced to fail")
def test_check_player_channel_error(tc):
def fail_first_command(channel):
channel.fail_next_command = True
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
fx.register_init_callback('fail_first_command', fail_first_command)
ck = Player_check_fixture(tc)
ck.player.cmd_args.append('init=fail_first_command')
with tc.assertRaises(game_jobs.CheckFailed) as ar:
game_jobs.check_player(ck.check)
tc.assertEqual(str(ar.exception),
"transport error sending first command (protocol_version) "
"to test:\n"
"forced failure for send_command_line")
def test_check_player_channel_error_on_close(tc):
def fail_close(channel):
channel.fail_close = True
fx = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
fx.register_init_callback('fail_close', fail_close)
ck = Player_check_fixture(tc)
ck.player.cmd_args.append('init=fail_close')
tc.assertEqual(game_jobs.check_player(ck.check),
["error closing test:\nforced failure for close"])

View File

@ -0,0 +1,161 @@
"""Gomill-specific test support code."""
import re
from gomill import __version__
from gomill_tests.test_framework import unittest2
from gomill_tests import test_framework
from gomill.common import *
from gomill import ascii_boards
from gomill import boards
# This makes TestResult ignore lines from this module in tracebacks
__unittest = True
def compare_boards(b1, b2):
"""Check whether two boards have the same position.
returns a pair (position_is_the_same, message)
"""
if b1.side != b2.side:
raise ValueError("size is different: %s, %s" % (b1.side, b2.side))
differences = []
for row, col in b1.board_points:
if b1.get(row, col) != b2.get(row, col):
differences.append((row, col))
if not differences:
return True, None
msg = "boards differ at %s" % " ".join(map(format_vertex, differences))
try:
msg += "\n%s\n%s" % (
ascii_boards.render_board(b1), ascii_boards.render_board(b2))
except Exception:
pass
return False, msg
def compare_diagrams(d1, d2):
"""Compare two ascii board diagrams.
returns a pair (strings_are_equal, message)
(assertMultiLineEqual tends to look nasty for these, so we just show them
both in full)
"""
if d1 == d2:
return True, None
return False, "diagrams differ:\n%s\n\n%s" % (d1, d2)
def scrub_sgf(s):
"""Normalise sgf string for convenience of testing.
Replaces dates with '***', and 'gomill:<__version__>' with 'gomill:VER'.
Be careful: gomill version length can affect line wrapping. Either
serialise with wrap=None or remove newlines before comparing.
"""
s = re.sub(r"(?m)(?<=^Date ).*$", "***", s)
s = re.sub(r"(?<=DT\[)[-0-9]+(?=\])", "***", s)
s = re.sub(r"gomill:" + re.escape(__version__), "gomill:VER", s)
return s
traceback_line_re = re.compile(
r" .*/([a-z0-9_]+)\.pyc?:[0-9]+ \(([a-z0-9_]+)\)")
class Gomill_testcase_mixin(object):
"""TestCase mixin adding support for gomill-specific types.
This adds:
assertBoardEqual
assertEqual and assertNotEqual for Boards
"""
def init_gomill_testcase_mixin(self):
self.addTypeEqualityFunc(boards.Board, self.assertBoardEqual)
def _format_message(self, msg, standardMsg):
# This is the same as _formatMessage from python 2.7 unittest; copying
# it because it's not part of the public API.
if not self.longMessage:
return msg or standardMsg
if msg is None:
return standardMsg
try:
return '%s : %s' % (standardMsg, msg)
except UnicodeDecodeError:
return '%s : %s' % (unittest2.util.safe_repr(standardMsg),
unittest2.util.safe_repr(msg))
def assertBoardEqual(self, b1, b2, msg=None):
are_equal, desc = compare_boards(b1, b2)
if not are_equal:
self.fail(self._format_message(msg, desc+"\n"))
def assertDiagramEqual(self, d1, d2, msg=None):
are_equal, desc = compare_diagrams(d1, d2)
if not are_equal:
self.fail(self._format_message(msg, desc+"\n"))
def assertNotEqual(self, first, second, msg=None):
if isinstance(first, boards.Board) and isinstance(second, boards.Board):
are_equal, _ = compare_boards(first, second)
if not are_equal:
return
msg = self._format_message(msg, 'boards have the same position')
raise self.failureException(msg)
super(Gomill_testcase_mixin, self).assertNotEqual(first, second, msg)
def assertTracebackStringEqual(self, seen, expected, fixups=()):
"""Compare two strings which include tracebacks.
This is for comparing strings containing tracebacks from
the compact_tracebacks module.
Replaces the traceback lines describing source locations with
'<filename>|<functionname>', for robustness.
fixups -- list of pairs of strings
(additional substitutions to make in the 'seen' string)
"""
lines = seen.split("\n")
new_lines = []
for l in lines:
match = traceback_line_re.match(l)
if match:
l = "|".join(match.groups())
for a, b in fixups:
l = l.replace(a, b)
new_lines.append(l)
self.assertMultiLineEqual("\n".join(new_lines), expected)
class Gomill_SimpleTestCase(
Gomill_testcase_mixin, test_framework.SimpleTestCase):
"""SimpleTestCase with the Gomill mixin."""
def __init__(self, *args, **kwargs):
test_framework.SimpleTestCase.__init__(self, *args, **kwargs)
self.init_gomill_testcase_mixin()
class Gomill_ParameterisedTestCase(
Gomill_testcase_mixin, test_framework.ParameterisedTestCase):
"""ParameterisedTestCase with the Gomill mixin."""
def __init__(self, *args, **kwargs):
test_framework.ParameterisedTestCase.__init__(self, *args, **kwargs)
self.init_gomill_testcase_mixin()
def make_simple_tests(source, prefix="test_"):
"""Make test cases from a module's test_xxx functions.
See test_framework for details.
The test functions can use the Gomill_testcase_mixin enhancements.
"""
return test_framework.make_simple_tests(
source, prefix, testcase_class=Gomill_SimpleTestCase)

View File

@ -0,0 +1,141 @@
"""Support code for gtp_controller_tests.
This is also used by gtp_engine_fixtures (and so by gtp proxy tests).
"""
from gomill import gtp_controller
from gomill.gtp_controller import (
GtpChannelError, GtpProtocolError, GtpTransportError, GtpChannelClosed,
BadGtpResponse)
from gomill_tests import test_support
from gomill_tests.test_framework import SupporterError
class Preprogrammed_gtp_channel(gtp_controller.Subprocess_gtp_channel):
"""A Linebased_gtp_channel with hardwired response stream.
Instantiate with a string containing the complete response stream.
This sends the contents of the response stream, irrespective of what
commands are received.
Pass hangs_before_eof True to simulate an engine that doesn't close its
response pipe when the preprogrammed response data runs out.
The command stream is available from get_command_stream().
"""
def __init__(self, response, hangs_before_eof=False):
gtp_controller.Linebased_gtp_channel.__init__(self)
self.command_pipe = test_support.Mock_writing_pipe()
self.response_pipe = test_support.Mock_reading_pipe(response)
self.response_pipe.hangs_before_eof = hangs_before_eof
def close(self):
self.command_pipe.close()
self.response_pipe.close()
def get_command_stream(self):
"""Return the complete contents of the command stream sent so far."""
return self.command_pipe.getvalue()
def break_command_stream(self):
"""Break the simulated pipe for the command stream."""
self.command_pipe.simulate_broken_pipe()
def break_response_stream(self):
"""Break the simulated pipe for the response stream."""
self.response_pipe.simulate_broken_pipe()
class Testing_gtp_channel(gtp_controller.Linebased_gtp_channel):
"""Linebased GTP channel that runs an internal Gtp_engine.
Instantiate with a Gtp_engine_protocol object.
This is used for testing how controllers handle GtpChannelError.
Public attributes:
engine -- the engine it was instantiated with
is_closed -- bool (closed() has been called without a forced error)
This raises an error if sent two commands without requesting a response in
between, or if asked for a response when no command was sent since the last
response. (GTP permits stacking up commands, but Gtp_controller should never
do it, so we want to report it).
Unlike Internal_gtp_channel, this runs the command as the point when it is
sent.
If you send a command after the engine has exited, this raises
GtpChannelClosed. Set the attribute engine_exit_breaks_commands to False to
disable this behaviour (it will ignore the command and respond with EOF
instead).
You can force errors by setting the following attributes:
fail_next_command -- bool (send_command_line raises GtpTransportError)
fail_command -- string (like fail_next_command, if command line
starts with this string)
fail_next_response -- bool (get_response_line raises GtpTransportError)
force_next_response -- string (get_response_line uses this string)
fail_close -- bool (close raises GtpTransportError)
"""
def __init__(self, engine):
gtp_controller.Linebased_gtp_channel.__init__(self)
self.engine = engine
self.stored_response = ""
self.session_is_ended = False
self.is_closed = False
self.engine_exit_breaks_commands = True
self.fail_next_command = False
self.fail_next_response = False
self.force_next_response = None
self.fail_close = False
self.fail_command = None
def send_command_line(self, command):
if self.is_closed:
raise SupporterError("channel is closed")
if self.stored_response != "":
raise SupporterError("two commands in a row")
if self.session_is_ended:
if self.engine_exit_breaks_commands:
raise GtpChannelClosed("engine has closed the command channel")
return
if self.fail_next_command:
self.fail_next_command = False
raise GtpTransportError("forced failure for send_command_line")
if self.fail_command and command.startswith(self.fail_command):
self.fail_command = None
raise GtpTransportError("forced failure for send_command_line")
cmd_list = command.strip().split(" ")
is_error, response, end_session = \
self.engine.run_command(cmd_list[0], cmd_list[1:])
if end_session:
self.session_is_ended = True
self.stored_response = ("? " if is_error else "= ") + response + "\n\n"
def get_response_line(self):
if self.is_closed:
raise SupporterError("channel is closed")
if self.stored_response == "":
if self.session_is_ended:
return ""
raise SupporterError("response request without command")
if self.fail_next_response:
self.fail_next_response = False
raise GtpTransportError("forced failure for get_response_line")
if self.force_next_response is not None:
self.stored_response = self.force_next_response
self.force_next_response = None
line, self.stored_response = self.stored_response.split("\n", 1)
return line + "\n"
def close(self):
if self.fail_close:
raise GtpTransportError("forced failure for close")
self.is_closed = True

View File

@ -0,0 +1,698 @@
"""Tests for gtp_controller.py"""
from __future__ import with_statement
import os
from gomill import gtp_controller
from gomill.gtp_controller import (
GtpChannelError, GtpProtocolError, GtpTransportError, GtpChannelClosed,
BadGtpResponse, Gtp_controller)
from gomill_tests import gomill_test_support
from gomill_tests import gtp_controller_test_support
from gomill_tests import gtp_engine_fixtures
from gomill_tests.test_framework import SupporterError
from gomill_tests.gtp_controller_test_support import Preprogrammed_gtp_channel
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
### Channel-level
def test_linebased_channel(tc):
channel = Preprogrammed_gtp_channel("=\n\n=\n\n")
tc.assertEqual(channel.get_command_stream(), "")
channel.send_command("play", ["b", "a3"])
tc.assertEqual(channel.get_command_stream(), "play b a3\n")
tc.assertEqual(channel.get_response(), (False, ""))
channel.send_command("quit", [])
tc.assertEqual(channel.get_command_stream(), "play b a3\nquit\n")
tc.assertEqual(channel.get_response(), (False, ""))
tc.assertRaisesRegexp(
GtpChannelClosed, "engine has closed the response channel",
channel.get_response)
channel.close()
def test_linebased_channel_responses(tc):
channel = Preprogrammed_gtp_channel(
"= 2\n\n"
# failure response
"? unknown command\n\n"
# final response with no newlines
"= ok")
channel.send_command("protocol_version", [])
tc.assertEqual(channel.get_response(), (False, "2"))
channel.send_command("xyzzy", ["1", "2"])
tc.assertEqual(channel.get_response(), (True, "unknown command"))
channel.send_command("quit", ["1", "2"])
tc.assertEqual(channel.get_response(), (False, "ok"))
def test_linebased_channel_response_cleaning(tc):
channel = Preprogrammed_gtp_channel(
# empty response
"=\n\n"
# whitespace-only response
"= \n\n"
# ignores CRs (GTP spec)
"= 1abc\rde\r\n\r\n"
# ignores extra blank lines (GTP spec)
"= 2abcde\n\n\n\n"
# strips control characters (GTP spec)
"= 3a\x7fbc\x00d\x07e\n\x01\n"
# converts tabs to spaces (GTP spec)
"= 4abc\tde\n\n"
# strips leading whitespace (channel docs)
"= \t 5abcde\n\n"
# strips trailing whitepace (channel docs)
"= 6abcde \t \n\n"
# doesn't strip whitespace in the middle of a multiline response
"= 7aaa \n bbb\tccc\nddd \t \n\n"
# passes high characters through
"= 8ab\xc3\xa7de\n\n"
# all this at once, in a failure response
"? a\raa \r\n b\rbb\tcc\x01c\nddd \t \n\n"
)
tc.assertEqual(channel.get_response(), (False, ""))
tc.assertEqual(channel.get_response(), (False, ""))
tc.assertEqual(channel.get_response(), (False, "1abcde"))
tc.assertEqual(channel.get_response(), (False, "2abcde"))
tc.assertEqual(channel.get_response(), (False, "3abcde"))
tc.assertEqual(channel.get_response(), (False, "4abc de"))
tc.assertEqual(channel.get_response(), (False, "5abcde"))
tc.assertEqual(channel.get_response(), (False, "6abcde"))
tc.assertEqual(channel.get_response(), (False, "7aaa \n bbb ccc\nddd"))
tc.assertEqual(channel.get_response(), (False, "8ab\xc3\xa7de"))
tc.assertEqual(channel.get_response(), (True, "aaa \n bbb ccc\nddd"))
def test_linebased_channel_invalid_responses(tc):
channel = Preprogrammed_gtp_channel(
# good response first, to get past the "isn't speaking GTP" checking
"=\n\n"
"ERROR\n\n"
"# comments not allowed in responses\n\n"
)
tc.assertEqual(channel.get_response(), (False, ""))
tc.assertRaisesRegexp(
GtpProtocolError, "^no success/failure indication from engine: "
"first line is `ERROR`$",
channel.get_response)
tc.assertRaisesRegexp(
GtpProtocolError, "^no success/failure indication from engine: "
"first line is `#",
channel.get_response)
def test_linebased_channel_without_response(tc):
channel = Preprogrammed_gtp_channel("")
channel.send_command("protocol_version", [])
tc.assertRaisesRegexp(
GtpChannelClosed, "^engine has closed the response channel$",
channel.get_response)
channel.close()
def test_linebased_channel_with_usage_message_response(tc):
channel = Preprogrammed_gtp_channel(
"Usage: randomprogram [options]\n\nOptions:\n"
"--help show this help message and exit\n")
channel.send_command("protocol_version", [])
tc.assertRaisesRegexp(
GtpProtocolError, "^engine isn't speaking GTP: first byte is 'U'$",
channel.get_response)
channel.close()
def test_linebased_channel_with_interactive_response(tc):
channel = Preprogrammed_gtp_channel("prompt> \n", hangs_before_eof=True)
channel.send_command("protocol_version", [])
tc.assertRaisesRegexp(
GtpProtocolError, "^engine isn't speaking GTP", channel.get_response)
channel.close()
def test_linebased_channel_hang(tc):
# Correct behaviour for a GTP controller here is to wait for a newline.
# (Would be nice to have a timeout.)
# This serves as a check that the hangs_before_eof modelling is working.
channel = Preprogrammed_gtp_channel("=prompt> ", hangs_before_eof=True)
channel.send_command("protocol_version", [])
tc.assertRaisesRegexp(
SupporterError, "this would hang", channel.get_response)
channel.close()
def test_linebased_channel_with_gmp_response(tc):
channel = Preprogrammed_gtp_channel("\x01\xa1\xa0\x80",
hangs_before_eof=True)
channel.send_command("protocol_version", [])
tc.assertRaisesRegexp(
GtpProtocolError, "appears to be speaking GMP", channel.get_response)
channel.close()
def test_linebased_channel_with_broken_command_pipe(tc):
channel = Preprogrammed_gtp_channel(
"Usage: randomprogram [options]\n\nOptions:\n"
"--help show this help message and exit\n")
channel.break_command_stream()
tc.assertRaisesRegexp(
GtpChannelClosed, "^engine has closed the command channel$",
channel.send_command, "protocol_version", [])
channel.close()
def test_linebased_channel_with_broken_response_pipe(tc):
channel = Preprogrammed_gtp_channel("= 2\n\n? unreached\n\n")
channel.send_command("protocol_version", [])
tc.assertEqual(channel.get_response(), (False, "2"))
channel.break_response_stream()
channel.send_command("list_commands", [])
tc.assertRaisesRegexp(
GtpChannelClosed, "^engine has closed the response channel$",
channel.get_response)
channel.close()
def test_channel_command_validation(tc):
channel = Preprogrammed_gtp_channel("\n\n")
# empty command
tc.assertRaises(ValueError, channel.send_command, "", [])
# space in command
tc.assertRaises(ValueError, channel.send_command, "play b a3", [])
# space after command
tc.assertRaises(ValueError, channel.send_command, "play ", ["b", "a3"])
# control character in command
tc.assertRaises(ValueError, channel.send_command, "pla\x01y", ["b", "a3"])
# unicode command
tc.assertRaises(ValueError, channel.send_command, u"protocol_version", [])
# space in argument
tc.assertRaises(ValueError, channel.send_command, "play", ["b a3"])
# unicode argument
tc.assertRaises(ValueError, channel.send_command, "play ", [u"b", "a3"])
# high characters
channel.send_command("pl\xc3\xa1y", ["b", "\xc3\xa13"])
tc.assertEqual(channel.get_command_stream(), "pl\xc3\xa1y b \xc3\xa13\n")
### Validating Testing_gtp_channel
def test_testing_gtp_channel(tc):
engine = gtp_engine_fixtures.get_test_engine()
channel = gtp_controller_test_support.Testing_gtp_channel(engine)
channel.send_command("play", ["b", "a3"])
tc.assertEqual(channel.get_response(), (True, "unknown command"))
channel.send_command("test", [])
tc.assertEqual(channel.get_response(), (False, "test response"))
channel.send_command("multiline", [])
tc.assertEqual(channel.get_response(),
(False, "first line \n second line\nthird line"))
channel.send_command("quit", [])
tc.assertEqual(channel.get_response(), (False, ""))
tc.assertRaisesRegexp(
GtpChannelClosed, "engine has closed the command channel",
channel.send_command, "quit", [])
channel.close()
def test_testing_gtp_channel_alt(tc):
engine = gtp_engine_fixtures.get_test_engine()
channel = gtp_controller_test_support.Testing_gtp_channel(engine)
channel.engine_exit_breaks_commands = False
channel.send_command("test", [])
tc.assertEqual(channel.get_response(), (False, "test response"))
channel.send_command("quit", [])
tc.assertEqual(channel.get_response(), (False, ""))
channel.send_command("test", [])
tc.assertRaisesRegexp(
GtpChannelClosed, "engine has closed the response channel",
channel.get_response)
channel.close()
def test_testing_gtp_channel_fatal_errors(tc):
engine = gtp_engine_fixtures.get_test_engine()
channel = gtp_controller_test_support.Testing_gtp_channel(engine)
channel.send_command("fatal", [])
tc.assertEqual(channel.get_response(), (True, "fatal error"))
tc.assertRaisesRegexp(
GtpChannelClosed, "engine has closed the response channel",
channel.get_response)
channel.close()
def test_testing_gtp_channel_sequencing(tc):
engine = gtp_engine_fixtures.get_test_engine()
channel = gtp_controller_test_support.Testing_gtp_channel(engine)
tc.assertRaisesRegexp(
SupporterError, "response request without command",
channel.get_response)
channel.send_command("test", [])
tc.assertRaisesRegexp(
SupporterError, "two commands in a row",
channel.send_command, "test", [])
def test_testing_gtp_force_error(tc):
engine = gtp_engine_fixtures.get_test_engine()
channel = gtp_controller_test_support.Testing_gtp_channel(engine)
channel.fail_next_command = True
tc.assertRaisesRegexp(
GtpTransportError, "forced failure for send_command_line",
channel.send_command, "test", [])
channel.send_command("test", [])
channel.fail_next_response = True
tc.assertRaisesRegexp(
GtpTransportError, "forced failure for get_response_line",
channel.get_response)
channel.force_next_response = "# error\n\n"
tc.assertRaisesRegexp(
GtpProtocolError,
"no success/failure indication from engine: first line is `# error`",
channel.get_response)
channel.fail_close = True
tc.assertRaisesRegexp(
GtpTransportError, "forced failure for close",
channel.close)
### Controller-level
def test_controller(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
tc.assertEqual(controller.name, 'player test')
tc.assertIs(controller.channel, channel)
tc.assertFalse(controller.channel_is_bad)
tc.assertEqual(controller.do_command("test", "ab", "cd"), "args: ab cd")
with tc.assertRaises(BadGtpResponse) as ar:
controller.do_command("error")
tc.assertEqual(ar.exception.gtp_error_message, "normal error")
tc.assertEqual(ar.exception.gtp_command, "error")
tc.assertSequenceEqual(ar.exception.gtp_arguments, [])
tc.assertEqual(str(ar.exception),
"failure response from 'error' to player test:\n"
"normal error")
with tc.assertRaises(BadGtpResponse) as ar:
controller.do_command("fatal")
tc.assertFalse(controller.channel_is_bad)
with tc.assertRaises(GtpChannelClosed) as ar:
controller.do_command("test")
tc.assertEqual(str(ar.exception),
"error sending 'test' to player test:\n"
"engine has closed the command channel")
tc.assertTrue(controller.channel_is_bad)
controller.close()
tc.assertListEqual(controller.retrieve_error_messages(), [])
def test_controller_alt_exit(tc):
channel = gtp_engine_fixtures.get_test_channel()
channel.engine_exit_breaks_commands = False
controller = Gtp_controller(channel, 'player test')
controller.do_command("quit")
tc.assertFalse(controller.channel_is_bad)
with tc.assertRaises(GtpChannelClosed) as ar:
controller.do_command("test")
tc.assertEqual(str(ar.exception),
"error reading response to 'test' from player test:\n"
"engine has closed the response channel")
tc.assertTrue(controller.channel_is_bad)
controller.close()
tc.assertListEqual(controller.retrieve_error_messages(), [])
def test_controller_first_command_error(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
with tc.assertRaises(BadGtpResponse) as ar:
controller.do_command("error")
tc.assertEqual(
str(ar.exception),
"failure response from first command (error) to player test:\n"
"normal error")
tc.assertListEqual(controller.retrieve_error_messages(), [])
def test_controller_command_transport_error(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
tc.assertEqual(controller.do_command("test"), "test response")
tc.assertFalse(controller.channel_is_bad)
channel.fail_next_command = True
with tc.assertRaises(GtpTransportError) as ar:
controller.do_command("test")
tc.assertEqual(
str(ar.exception),
"transport error sending 'test' to player test:\n"
"forced failure for send_command_line")
tc.assertTrue(controller.channel_is_bad)
tc.assertListEqual(controller.retrieve_error_messages(), [])
def test_controller_response_transport_error(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
tc.assertFalse(controller.channel_is_bad)
channel.fail_next_response = True
with tc.assertRaises(GtpTransportError) as ar:
controller.do_command("test")
tc.assertEqual(
str(ar.exception),
"transport error reading response to first command (test) "
"from player test:\n"
"forced failure for get_response_line")
tc.assertTrue(controller.channel_is_bad)
tc.assertListEqual(controller.retrieve_error_messages(), [])
def test_controller_response_protocol_error(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
tc.assertEqual(controller.do_command("test"), "test response")
tc.assertFalse(controller.channel_is_bad)
channel.force_next_response = "# error\n\n"
with tc.assertRaises(GtpProtocolError) as ar:
controller.do_command("test")
tc.assertEqual(
str(ar.exception),
"GTP protocol error reading response to 'test' from player test:\n"
"no success/failure indication from engine: first line is `# error`")
tc.assertTrue(controller.channel_is_bad)
tc.assertListEqual(controller.retrieve_error_messages(), [])
def test_controller_close(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
tc.assertFalse(controller.channel_is_closed)
tc.assertEqual(controller.do_command("test"), "test response")
tc.assertFalse(controller.channel_is_closed)
tc.assertFalse(controller.channel.is_closed)
controller.close()
tc.assertTrue(controller.channel_is_closed)
tc.assertTrue(controller.channel.is_closed)
tc.assertRaisesRegexp(StandardError, "^channel is closed$",
controller.do_command, "test")
tc.assertRaisesRegexp(StandardError, "^channel is closed$",
controller.close)
tc.assertListEqual(controller.retrieve_error_messages(), [])
def test_controller_close_error(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
channel.fail_close = True
with tc.assertRaises(GtpTransportError) as ar:
controller.close()
tc.assertEqual(
str(ar.exception),
"error closing player test:\n"
"forced failure for close")
tc.assertListEqual(controller.retrieve_error_messages(), [])
def test_controller_safe_close(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
tc.assertFalse(controller.channel_is_closed)
tc.assertEqual(controller.do_command("test"), "test response")
tc.assertFalse(controller.channel_is_closed)
tc.assertFalse(controller.channel.is_closed)
controller.safe_close()
tc.assertTrue(controller.channel_is_closed)
tc.assertTrue(controller.channel.is_closed)
tc.assertListEqual(channel.engine.commands_handled,
[('test', []), ('quit', [])])
# safe to call twice
controller.safe_close()
tc.assertListEqual(controller.retrieve_error_messages(), [])
def test_controller_safe_close_after_error(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
tc.assertEqual(controller.do_command("test"), "test response")
tc.assertFalse(controller.channel_is_bad)
channel.force_next_response = "# error\n\n"
with tc.assertRaises(GtpProtocolError) as ar:
controller.do_command("test")
tc.assertTrue(controller.channel_is_bad)
# doesn't send quit when channel_is_bad
controller.safe_close()
tc.assertTrue(controller.channel_is_closed)
tc.assertTrue(controller.channel.is_closed)
tc.assertListEqual(channel.engine.commands_handled,
[('test', []), ('test', [])])
tc.assertListEqual(controller.retrieve_error_messages(), [])
def test_controller_safe_close_with_error_from_quit(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
channel.force_next_response = "# error\n\n"
controller.safe_close()
tc.assertTrue(controller.channel_is_closed)
tc.assertTrue(controller.channel.is_closed)
tc.assertListEqual(channel.engine.commands_handled,
[('quit', [])])
tc.assertListEqual(
controller.retrieve_error_messages(),
["GTP protocol error reading response to first command (quit) "
"from player test:\n"
"no success/failure indication from engine: first line is `# error`"])
def test_controller_safe_close_with_failure_response_from_quit(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
channel.engine.force_error("quit")
controller.safe_close()
tc.assertTrue(controller.channel_is_closed)
tc.assertTrue(controller.channel.is_closed)
tc.assertListEqual(channel.engine.commands_handled,
[('quit', [])])
error_messages = controller.retrieve_error_messages()
tc.assertEqual(len(error_messages), 1)
tc.assertEqual(
error_messages[0],
"failure response from first command (quit) to player test:\n"
"handler forced to fail")
def test_controller_safe_close_with_error_from_close(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
channel.fail_close = True
controller.safe_close()
tc.assertTrue(controller.channel_is_closed)
tc.assertListEqual(channel.engine.commands_handled,
[('quit', [])])
tc.assertListEqual(
controller.retrieve_error_messages(),
["error closing player test:\n"
"forced failure for close"])
def test_safe_do_command(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
tc.assertEqual(controller.safe_do_command("test", "ab"), "args: ab")
with tc.assertRaises(BadGtpResponse) as ar:
controller.safe_do_command("error")
tc.assertFalse(controller.channel_is_bad)
channel.fail_next_response = True
tc.assertIsNone(controller.safe_do_command("test"))
tc.assertTrue(controller.channel_is_bad)
tc.assertIsNone(controller.safe_do_command("test"))
tc.assertListEqual(
controller.retrieve_error_messages(),
["transport error reading response to 'test' from player test:\n"
"forced failure for get_response_line"])
controller.safe_close()
# check that third 'test' wasn't sent, and nor was 'quit'
tc.assertListEqual(channel.engine.commands_handled,
[('test', ['ab']), ('error', []), ('test', [])])
def test_safe_do_command_closed_channel(tc):
# check it's ok to call safe_do_command() on a closed channel
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
controller.safe_close()
tc.assertIsNone(controller.safe_do_command("test"))
tc.assertListEqual(channel.engine.commands_handled,
[('quit', [])])
tc.assertListEqual(controller.retrieve_error_messages(), [])
def test_known_command(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'kc test')
tc.assertTrue(controller.known_command("test"))
tc.assertFalse(controller.known_command("nonesuch"))
tc.assertTrue(controller.known_command("test"))
tc.assertFalse(controller.known_command("nonesuch"))
def test_known_command_2(tc):
# Checking that known_command caches its responses
# and that it treats an error or unknown value the same as 'false'.
channel = Preprogrammed_gtp_channel(
"= true\n\n= absolutely not\n\n? error\n\n# unreached\n\n")
controller = Gtp_controller(channel, 'kc2 test')
tc.assertTrue(controller.known_command("one"))
tc.assertFalse(controller.known_command("two"))
tc.assertFalse(controller.known_command("three"))
tc.assertTrue(controller.known_command("one"))
tc.assertFalse(controller.known_command("two"))
tc.assertEqual(
channel.get_command_stream(),
"known_command one\nknown_command two\nknown_command three\n")
def test_check_protocol_version(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'pv test')
controller.check_protocol_version()
def test_check_protocol_version_2(tc):
channel = Preprogrammed_gtp_channel("= 1\n\n? error\n\n# unreached\n\n")
controller = Gtp_controller(channel, 'pv2 test')
with tc.assertRaises(BadGtpResponse) as ar:
controller.check_protocol_version()
tc.assertEqual(str(ar.exception), "pv2 test reports GTP protocol version 1")
tc.assertEqual(ar.exception.gtp_error_message, None)
# check error is not treated as a check failure
controller.check_protocol_version()
def test_list_commands(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'lc test')
channel.engine.add_command("xyzzy", None)
channel.engine.add_command("pl ugh", None)
tc.assertListEqual(
controller.list_commands(),
['error', 'fatal', 'known_command', 'list_commands',
'multiline', 'protocol_version', 'quit', 'test', 'xyzzy'])
def test_gtp_aliases(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'alias test')
controller.set_gtp_aliases({
'aliased' : 'test',
'aliased2' : 'nonesuch',
})
tc.assertIs(controller.known_command("test"), True)
tc.assertIs(controller.known_command("aliased"), True)
tc.assertIs(controller.known_command("nonesuch"), False)
tc.assertIs(controller.known_command("test"), True)
tc.assertIs(controller.known_command("aliased"), True)
tc.assertIs(controller.known_command("nonesuch"), False)
tc.assertEqual(controller.do_command("test"), "test response")
tc.assertEqual(controller.do_command("aliased"), "test response")
with tc.assertRaises(BadGtpResponse) as ar:
controller.do_command("aliased2")
tc.assertEqual(ar.exception.gtp_error_message, "unknown command")
tc.assertEqual(ar.exception.gtp_command, "nonesuch")
def test_gtp_aliases_safe(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'alias test')
controller.set_gtp_aliases({
'aliased' : 'test',
'aliased2' : 'nonesuch',
})
tc.assertIs(controller.safe_known_command("test"), True)
tc.assertIs(controller.safe_known_command("aliased"), True)
tc.assertIs(controller.safe_known_command("nonesuch"), False)
tc.assertIs(controller.safe_known_command("test"), True)
tc.assertIs(controller.safe_known_command("aliased"), True)
tc.assertIs(controller.safe_known_command("nonesuch"), False)
tc.assertEqual(controller.safe_do_command("test"), "test response")
tc.assertEqual(controller.safe_do_command("aliased"), "test response")
with tc.assertRaises(BadGtpResponse) as ar:
controller.safe_do_command("aliased2")
tc.assertEqual(ar.exception.gtp_error_message, "unknown command")
tc.assertEqual(ar.exception.gtp_command, "nonesuch")
def test_fix_version(tc):
fv = gtp_controller._fix_version
tc.assertEqual(fv("foo", "bar"), "bar")
tc.assertEqual(fv("foo", "FOO bar"), "bar")
tc.assertEqual(fv("foo", "asd " * 16), "asd " * 16)
tc.assertEqual(fv("foo", "asd " * 17), "asd")
tc.assertEqual(
fv("MoGo", "MoGo release 1. Please read http://www.lri.fr/~gelly/MoGo.htm for more information. That is NOT an official developpement MoGo version, but it is a public release. Its strength highly depends on your hardware and the time settings."),
"release 1")
tc.assertEqual(
fv("Pachi UCT Engine", "8.99 (Hakugen-devel): I'm playing UCT. When I'm losing, I will resign, if I think I win, I play until you pass. Anyone can send me 'winrate' in private chat to get my assessment of the position."),
"8.99 (Hakugen-devel)")
def test_describe_engine(tc):
channel = gtp_engine_fixtures.get_test_channel()
controller = Gtp_controller(channel, 'player test')
short_s, long_s = gtp_controller.describe_engine(controller)
tc.assertEqual(short_s, "unknown")
tc.assertEqual(long_s, "unknown")
channel = gtp_engine_fixtures.get_test_channel()
channel.engine.add_command('name', lambda args:"test engine")
controller = Gtp_controller(channel, 'player test')
short_s, long_s = gtp_controller.describe_engine(controller)
tc.assertEqual(short_s, "test engine")
tc.assertEqual(long_s, "test engine")
channel = gtp_engine_fixtures.get_test_channel()
channel.engine.add_command('name', lambda args:"test engine")
channel.engine.add_command('version', lambda args:"1.2.3")
controller = Gtp_controller(channel, 'player test')
short_s, long_s = gtp_controller.describe_engine(controller)
tc.assertEqual(short_s, "test engine:1.2.3")
tc.assertEqual(long_s, "test engine:1.2.3")
channel = gtp_engine_fixtures.get_test_channel()
channel.engine.add_command('name', lambda args:"test engine")
channel.engine.add_command('version', lambda args:"1.2.3")
channel.engine.add_command(
'gomill-describe_engine',
lambda args:"test engine (v1.2.3):\n pl\xc3\xa1yer \xa3")
controller = Gtp_controller(channel, 'player test')
short_s, long_s = gtp_controller.describe_engine(controller)
tc.assertEqual(short_s, "test engine:1.2.3")
tc.assertEqual(long_s, "test engine (v1.2.3):\n pl\xc3\xa1yer ?")
channel = gtp_engine_fixtures.get_test_channel()
channel.engine.add_command('name', lambda args:"test engine")
channel.engine.add_command('version', lambda args:"test engine v1.2.3")
controller = Gtp_controller(channel, 'player test')
short_s, long_s = gtp_controller.describe_engine(controller)
tc.assertEqual(short_s, "test engine:v1.2.3")
tc.assertEqual(long_s, "test engine:v1.2.3")
### Subprocess-specific
def test_subprocess_channel(tc):
# This tests that Subprocess_gtp_channel really launches a subprocess.
# It also checks that the 'stderr', 'env' and 'cwd' parameters work.
# This test relies on there being a 'python' executable on the PATH
# (doesn't have to be the same version as is running the testsuite).
fx = gtp_engine_fixtures.State_reporter_fixture(tc)
rd, wr = os.pipe()
try:
channel = gtp_controller.Subprocess_gtp_channel(
fx.cmd,
stderr=wr,
env={'GOMILL_TEST' : "from_gtp_controller_tests"},
cwd="/")
tc.assertEqual(os.read(rd, 256), "subprocess_state_reporter: testing\n")
finally:
os.close(wr)
os.close(rd)
tc.assertIsNone(channel.exit_status)
tc.assertIsNone(channel.resource_usage)
channel.send_command("tell", [])
tc.assertEqual(channel.get_response(),
(False, "cwd: /\nGOMILL_TEST:from_gtp_controller_tests"))
channel.close()
tc.assertEqual(channel.exit_status, 0)
rusage = channel.resource_usage
tc.assertTrue(hasattr(rusage, 'ru_utime'))
tc.assertTrue(hasattr(rusage, 'ru_stime'))
def test_subprocess_channel_nonexistent_program(tc):
with tc.assertRaises(GtpChannelError) as ar:
gtp_controller.Subprocess_gtp_channel(["/nonexistent/program"])
tc.assertIn("[Errno 2] No such file or directory", str(ar.exception))
def test_subprocess_channel_with_controller(tc):
# Also tests that leaving 'env' and 'cwd' unset works
fx = gtp_engine_fixtures.State_reporter_fixture(tc)
channel = gtp_controller.Subprocess_gtp_channel(fx.cmd, stderr=fx.devnull)
controller = Gtp_controller(channel, 'subprocess test')
tc.assertEqual(controller.do_command("tell"),
"cwd: %s\nGOMILL_TEST:None" % os.getcwd())
controller.close()
tc.assertEqual(channel.exit_status, 0)
rusage = channel.resource_usage
tc.assertTrue(hasattr(rusage, 'ru_utime'))

View File

@ -0,0 +1,390 @@
"""Engines (and channels) provided for the use of controller-side testing."""
import os
from gomill import gtp_controller
from gomill import gtp_engine
from gomill.gtp_engine import GtpError, GtpFatalError
from gomill.gtp_controller import GtpChannelError
from gomill.common import *
from gomill_tests import test_framework
from gomill_tests import gtp_controller_test_support
from gomill_tests.test_framework import SupporterError
## Test engine
class Test_gtp_engine_protocol(gtp_engine.Gtp_engine_protocol):
"""Variant of Gtp_engine_protocol with additional facilities for testing.
Public attributes:
commands_handled -- list of pairs (command, args)
This records all commands sent to the engine and makes them available in the
commands_handled attribute. It also provides a mechanism to force commands
to fail.
"""
def __init__(self):
gtp_engine.Gtp_engine_protocol.__init__(self)
self.commands_handled = []
def run_command(self, command, args):
self.commands_handled.append((command, args))
return gtp_engine.Gtp_engine_protocol.run_command(self, command, args)
def _forced_error(self, args):
raise GtpError("handler forced to fail")
def _forced_fatal_error(self, args):
raise GtpFatalError("handler forced to fail and exit")
def force_error(self, command):
"""Set the handler for 'command' to report failure."""
self.add_command(command, self._forced_error)
def force_fatal_error(self, command):
"""Set the handler for 'command' to report failure and exit."""
self.add_command(command, self._forced_fatal_error)
def get_test_engine():
"""Return a Gtp_engine_protocol useful for testing controllers.
Actually returns a Test_gtp_engine_protocol.
"""
def handle_test(args):
if args:
return "args: " + " ".join(args)
else:
return "test response"
def handle_multiline(args):
return "first line \n second line\nthird line"
def handle_error(args):
raise GtpError("normal error")
def handle_fatal_error(args):
raise GtpFatalError("fatal error")
engine = Test_gtp_engine_protocol()
engine.add_protocol_commands()
engine.add_command('test', handle_test)
engine.add_command('multiline', handle_multiline)
engine.add_command('error', handle_error)
engine.add_command('fatal', handle_fatal_error)
return engine
def get_test_channel():
"""Return a Testing_gtp_channel connected to the test engine."""
engine = get_test_engine()
return gtp_controller_test_support.Testing_gtp_channel(engine)
## Test player engine
class Test_player(object):
"""Trivial player.
This supports at least the minimal commands required to play a game.
At present, this plays up column 4 (for black) or 6 (for white), then
passes. It pays no attention to its opponent's moves, and doesn't maintain a
board position.
(This means that if two Test_players play each other, black will win by
boardsize*2 on the board).
"""
def __init__(self):
self.boardsize = None
self.row_to_play = 0
def handle_boardsize(self, args):
self.boardsize = gtp_engine.interpret_int(args[0])
def handle_clear_board(self, args):
pass
def handle_komi(self, args):
pass
def handle_play(self, args):
pass
def handle_genmove(self, args):
colour = gtp_engine.interpret_colour(args[0])
if self.row_to_play < self.boardsize:
col = 4 if colour == 'b' else 6
result = format_vertex((self.row_to_play, col))
self.row_to_play += 1
return result
else:
return "pass"
def handle_fail(self, args):
raise GtpError("test player forced to fail")
def get_handlers(self):
return {
'boardsize' : self.handle_boardsize,
'clear_board' : self.handle_clear_board,
'komi' : self.handle_komi,
'play' : self.handle_play,
'genmove' : self.handle_genmove,
'fail' : self.handle_fail,
}
class Programmed_player(object):
"""Player that follows a preset sequence of moves.
Instantiate with
moves -- a sequence of pairs (colour, vertex)
The sequence can have moves for both colours; genmove goes through the
moves in order and ignores ones for the colour that wasn't requested (the
idea is that you can create two players with the same move list).
Passes when it runs out of moves.
if 'vertex' is a tuple, it's interpreted as (row, col) and converted to a
gtp vertex. The special value 'fail' causes a GtpError. Otherwise it's
returned literally.
"""
def __init__(self, moves, reject=None):
self.moves = []
for colour, vertex in moves:
if isinstance(vertex, tuple):
vertex = format_vertex(vertex)
self.moves.append((colour, vertex))
self.reject = reject
self._reset()
def _reset(self):
self.iter = iter(self.moves)
def handle_boardsize(self, args):
pass
def handle_clear_board(self, args):
self._reset()
def handle_komi(self, args):
pass
def handle_play(self, args):
if self.reject is None:
return
vertex, msg = self.reject
if args[1].lower() == vertex.lower():
raise GtpError(msg)
def handle_genmove(self, args):
colour = gtp_engine.interpret_colour(args[0])
for move_colour, vertex in self.iter:
if move_colour == colour:
if vertex == 'fail':
raise GtpError("forced to fail")
return vertex
return "pass"
def get_handlers(self):
return {
'boardsize' : self.handle_boardsize,
'clear_board' : self.handle_clear_board,
'komi' : self.handle_komi,
'play' : self.handle_play,
'genmove' : self.handle_genmove,
}
def make_player_engine(player):
"""Return a Gtp_engine_protocol based on a specified player object.
Actually returns a Test_gtp_engine_protocol.
It has an additional 'player' attribute, which gives access to the
player object.
"""
engine = Test_gtp_engine_protocol()
engine.add_protocol_commands()
engine.add_commands(player.get_handlers())
engine.player = player
return engine
def get_test_player_engine():
"""Return a Gtp_engine_protocol based on a Test_player.
Actually returns a Test_gtp_engine_protocol.
It has an additional 'player' attribute, which gives access to the
Test_player.
"""
return make_player_engine(Test_player())
def get_test_player_channel():
"""Return a Testing_gtp_channel connected to the test player engine."""
engine = get_test_player_engine()
return gtp_controller_test_support.Testing_gtp_channel(engine)
## State reporter subprocess
class State_reporter_fixture(test_framework.Fixture):
"""Fixture for use with suprocess_state_reporter.py
Attributes:
pathname -- pathname of the state reporter python script
cmd -- command list suitable for use with suprocess.Popen
devnull -- file open for writing to /dev/null
"""
def __init__(self, tc):
self._pathname = os.path.abspath(
os.path.join(os.path.dirname(__file__),
"subprocess_state_reporter.py"))
self.cmd = ["python", self._pathname]
self.devnull = open(os.devnull, "w")
tc.addCleanup(self.devnull.close)
## Mock subprocess gtp channel
class Mock_subprocess_gtp_channel(
gtp_controller_test_support.Testing_gtp_channel):
"""Mock substitute for Subprocess_gtp_channel.
This has the same construction interface as Subprocess_gtp_channel, but is
in fact a Testing_gtp_channel.
This accepts the following 'command line arguments' in the 'command' list:
id=<string> -- id to use in the channels registry
engine=<string> -- look up engine in the engine registry
init=<string> -- look up initialisation fn in the callback registry
fail=startup -- simulate exec failure
By default, the underlying engine is a newly-created test player engine.
You can override this using 'engine=xxx'.
If you want to get at the channel object after creating it, pass 'id=xxx'
and find it using the 'channels' class attribute.
If you want to modify the returned channel object, pass 'init=xxx' and
register a callback function taking a channel parameter.
Class attributes:
engine_registry -- map engine code -> Gtp_engine_protocol
callback_registry -- map callback code -> function
channels -- map id string -> Mock_subprocess_gtp_channel
Instance attributes:
id -- string or None
requested_command -- list of strings
requested_stderr
requested_cwd
requested_env
"""
engine_registry = {}
callback_registry = {}
channels = {}
def __init__(self, command, stderr=None, cwd=None, env=None):
self.requested_command = command
self.requested_stderr = stderr
self.requested_cwd = cwd
self.requested_env = env
self.id = None
engine = None
callback = None
for arg in command[1:]:
key, eq, value = arg.partition("=")
if not eq:
raise SupporterError("Mock_subprocess_gtp_channel: "
"bad command-line argument: %s" % arg)
if key == 'id':
self.id = value
self.channels[value] = self
elif key == 'engine':
try:
engine = self.engine_registry[value]
except KeyError:
raise SupporterError(
"Mock_subprocess_gtp_channel: unregistered engine '%s'"
% value)
elif key == 'init':
try:
callback = self.callback_registry[value]
except KeyError:
raise SupporterError(
"Mock_subprocess_gtp_channel: unregistered init '%s'"
% value)
elif key == 'fail' and value == 'startup':
raise GtpChannelError("exec forced to fail")
else:
raise SupporterError("Mock_subprocess_gtp_channel: "
"bad command-line argument: %s" % arg)
if engine is None:
engine = get_test_player_engine()
gtp_controller_test_support.Testing_gtp_channel.__init__(self, engine)
if callback is not None:
callback(self)
class Mock_subprocess_fixture(test_framework.Fixture):
"""Fixture for using Mock_subprocess_gtp_channel.
While this fixture is active, attempts to instantiate a
Subprocess_gtp_channel will produce a Testing_gtp_channel.
"""
def __init__(self, tc):
self._patch()
tc.addCleanup(self._unpatch)
def _patch(self):
self._sgc = gtp_controller.Subprocess_gtp_channel
gtp_controller.Subprocess_gtp_channel = Mock_subprocess_gtp_channel
def _unpatch(self):
Mock_subprocess_gtp_channel.engine_registry.clear()
Mock_subprocess_gtp_channel.channels.clear()
gtp_controller.Subprocess_gtp_channel = self._sgc
def register_engine(self, code, engine):
"""Specify an engine for a mock subprocess channel to run.
code -- string
engine -- Gtp_engine_protocol
After this is called, attempts to instantiate a Subprocess_gtp_channel
with an 'engine=code' argument will return a Testing_gtp_channel based
on the specified engine.
"""
Mock_subprocess_gtp_channel.engine_registry[code] = engine
def register_init_callback(self, code, fn):
"""Specify an initialisation callback for the mock subprocess channel.
code -- string
fn -- function
After this is called, attempts to instantiate a Subprocess_gtp_channel
with an 'init=code' argument will call the specified function, passing
the Testing_gtp_channel as its parameter.
"""
Mock_subprocess_gtp_channel.callback_registry[code] = fn
def get_channel(self, id):
"""Retrieve a channel via its 'id' command-line argument."""
return Mock_subprocess_gtp_channel.channels[id]

View File

@ -0,0 +1,51 @@
"""Test support code for testing gomill GTP engines.
(Which includes proxy engines.)
"""
def check_engine(tc, engine, command, args, expected,
expect_failure=False, expect_end=False,
expect_internal_error=False):
"""Send a command to an engine and check its response.
tc -- TestCase
engine -- Gtp_engine_protocol
command -- GTP command to send
args -- list of GTP arguments to send
expected -- expected response string (None to skip check)
expect_failure -- expect a GTP failure response
expect_end -- expect the engine to report 'end session'
expect_internal_error -- see below
If the response isn't as expected, uses 'tc' to report this.
If expect_internal_error is true, expect_failure is forced true, and the
check for expected (if specified) is that it's included in the response,
rather than equal to the response.
"""
failure, response, end = engine.run_command(command, args)
if expect_internal_error:
expect_failure = True
if expect_failure:
tc.assertTrue(failure,
"unexpected GTP success response: %s" % response)
else:
tc.assertFalse(failure,
"unexpected GTP failure response: %s" % response)
if expect_internal_error:
tc.assertTrue(response.startswith("internal error\n"), response)
if expected is not None:
tc.assertTrue(expected in response, response)
elif expected is not None:
if command == "showboard":
tc.assertDiagramEqual(response, expected,
"showboard response not as expected")
else:
tc.assertEqual(response, expected, "GTP response not as expected")
if expect_end:
tc.assertTrue(end, "expected end-session not seen")
else:
tc.assertFalse(end, "unexpected end-session")

View File

@ -0,0 +1,56 @@
"""Tests for gtp_engine.py"""
from __future__ import with_statement
from gomill import gtp_engine
from gomill_tests import gomill_test_support
from gomill_tests import gtp_engine_test_support
from gomill_tests import test_support
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def test_engine(tc):
def handle_test(args):
if args:
return "args: " + " ".join(args)
else:
return "test response"
engine = gtp_engine.Gtp_engine_protocol()
engine.add_protocol_commands()
engine.add_command('test', handle_test)
check_engine = gtp_engine_test_support.check_engine
check_engine(tc, engine, 'test', ['ab', 'cd'], "args: ab cd")
def test_run_gtp_session(tc):
engine = gtp_engine.Gtp_engine_protocol()
engine.add_protocol_commands()
stream = "known_command list_commands\nxyzzy\nquit\n"
command_pipe = test_support.Mock_reading_pipe(stream)
response_pipe = test_support.Mock_writing_pipe()
gtp_engine.run_gtp_session(engine, command_pipe, response_pipe)
tc.assertMultiLineEqual(response_pipe.getvalue(),
"= true\n\n? unknown command\n\n=\n\n")
command_pipe.close()
response_pipe.close()
def test_run_gtp_session_broken_pipe(tc):
def break_pipe(args):
response_pipe.simulate_broken_pipe()
engine = gtp_engine.Gtp_engine_protocol()
engine.add_protocol_commands()
engine.add_command("break", break_pipe)
stream = "known_command list_commands\nbreak\nquit\n"
command_pipe = test_support.Mock_reading_pipe(stream)
response_pipe = test_support.Mock_writing_pipe()
with tc.assertRaises(gtp_engine.ControllerDisconnected) as ar:
gtp_engine.run_gtp_session(engine, command_pipe, response_pipe)
command_pipe.close()
response_pipe.close()

View File

@ -0,0 +1,630 @@
"""Tests for gtp_games.py"""
import cPickle as pickle
from gomill import gtp_controller
from gomill import gtp_games
from gomill import sgf
from gomill.common import format_vertex
from gomill_tests import test_framework
from gomill_tests import gomill_test_support
from gomill_tests import gtp_controller_test_support
from gomill_tests import gtp_engine_fixtures
from gomill_tests.gtp_engine_fixtures import Programmed_player
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
for t in handicap_compensation_tests:
suite.addTest(Handicap_compensation_TestCase(*t))
class Game_fixture(test_framework.Fixture):
"""Fixture managing a Gtp_game.
Instantiate with the player objects (defaults to a Test_player) and
optionally komi.
attributes:
game -- Gtp_game
controller_b -- Gtp_controller
controller_w -- Gtp_controller
channel_b -- Testing_gtp_channel
channel_w -- Testing_gtp_channel
engine_b -- Test_gtp_engine_protocol
engine_w -- Test_gtp_engine_protocol
player_b -- player object
player_w -- player object
"""
def __init__(self, tc, player_b=None, player_w=None,
komi=0.0, board_size=9):
self.tc = tc
game = gtp_games.Game(board_size, komi=komi)
game.set_player_code('b', 'one')
game.set_player_code('w', 'two')
if player_b is None:
player_b = gtp_engine_fixtures.Test_player()
if player_w is None:
player_w = gtp_engine_fixtures.Test_player()
engine_b = gtp_engine_fixtures.make_player_engine(player_b)
engine_w = gtp_engine_fixtures.make_player_engine(player_w)
channel_b = gtp_controller_test_support.Testing_gtp_channel(engine_b)
channel_w = gtp_controller_test_support.Testing_gtp_channel(engine_w)
controller_b = gtp_controller.Gtp_controller(channel_b, 'player one')
controller_w = gtp_controller.Gtp_controller(channel_w, 'player two')
game.set_player_controller('b', controller_b)
game.set_player_controller('w', controller_w)
self.game = game
self.controller_b = controller_b
self.controller_w = controller_w
self.channel_b = channel_b
self.channel_w = channel_w
self.engine_b = channel_b.engine
self.engine_w = channel_w.engine
self.player_b = channel_b.engine.player
self.player_w = channel_w.engine.player
def check_moves(self, expected_moves):
"""Check that the game's moves are as expected.
expected_moves -- list of pairs (colour, vertex)
"""
game_moves = [(colour, format_vertex(move))
for (colour, move, comment) in self.game.moves]
self.tc.assertListEqual(game_moves, expected_moves)
def run_score_test(self, b_score, w_score, allowed_scorers="bw"):
"""Run a game and let the players score it.
b_score, w_score -- string for final_score to return
If b_score or w_score is None, the player won't implement final_score.
If b_score or w_score is an exception, the final_score will fail
"""
def handle_final_score_b(args):
if b_score is Exception:
raise b_score
return b_score
def handle_final_score_w(args):
if w_score is Exception:
raise w_score
return w_score
if b_score is not None:
self.engine_b.add_command('final_score', handle_final_score_b)
if w_score is not None:
self.engine_w.add_command('final_score', handle_final_score_w)
for colour in allowed_scorers:
self.game.allow_scorer(colour)
self.game.ready()
self.game.run()
def sgf_string(self):
return gomill_test_support.scrub_sgf(
self.game.make_sgf().serialise(wrap=None))
def test_game(tc):
fx = Game_fixture(tc)
tc.assertDictEqual(fx.game.players, {'b' : 'one', 'w' : 'two'})
tc.assertIs(fx.game.get_controller('b'), fx.controller_b)
tc.assertIs(fx.game.get_controller('w'), fx.controller_w)
fx.game.use_internal_scorer()
fx.game.ready()
tc.assertIsNone(fx.game.game_id)
tc.assertIsNone(fx.game.result)
fx.game.run()
fx.game.close_players()
tc.assertIsNone(fx.game.describe_late_errors())
tc.assertDictEqual(fx.game.result.players, {'b' : 'one', 'w' : 'two'})
tc.assertEqual(fx.game.result.player_b, 'one')
tc.assertEqual(fx.game.result.player_w, 'two')
tc.assertEqual(fx.game.result.winning_colour, 'b')
tc.assertEqual(fx.game.result.losing_colour, 'w')
tc.assertEqual(fx.game.result.winning_player, 'one')
tc.assertEqual(fx.game.result.losing_player, 'two')
tc.assertEqual(fx.game.result.sgf_result, "B+18")
tc.assertFalse(fx.game.result.is_forfeit)
tc.assertIs(fx.game.result.is_jigo, False)
tc.assertIsNone(fx.game.result.detail)
tc.assertIsNone(fx.game.result.game_id)
tc.assertEqual(fx.game.result.describe(), "one beat two B+18")
result2 = pickle.loads(pickle.dumps(fx.game.result))
tc.assertEqual(result2.describe(), "one beat two B+18")
tc.assertEqual(fx.game.describe_scoring(), "one beat two B+18")
tc.assertEqual(result2.player_b, 'one')
tc.assertEqual(result2.player_w, 'two')
tc.assertIs(result2.is_jigo, False)
tc.assertDictEqual(fx.game.result.cpu_times, {'one' : None, 'two' : None})
tc.assertListEqual(fx.game.moves, [
('b', (0, 4), None), ('w', (0, 6), None),
('b', (1, 4), None), ('w', (1, 6), None),
('b', (2, 4), None), ('w', (2, 6), None),
('b', (3, 4), None), ('w', (3, 6), None),
('b', (4, 4), None), ('w', (4, 6), None),
('b', (5, 4), None), ('w', (5, 6), None),
('b', (6, 4), None), ('w', (6, 6), None),
('b', (7, 4), None), ('w', (7, 6), None),
('b', (8, 4), None), ('w', (8, 6), None),
('b', None, None), ('w', None, None)])
fx.check_moves([
('b', 'E1'), ('w', 'G1'),
('b', 'E2'), ('w', 'G2'),
('b', 'E3'), ('w', 'G3'),
('b', 'E4'), ('w', 'G4'),
('b', 'E5'), ('w', 'G5'),
('b', 'E6'), ('w', 'G6'),
('b', 'E7'), ('w', 'G7'),
('b', 'E8'), ('w', 'G8'),
('b', 'E9'), ('w', 'G9'),
('b', 'pass'), ('w', 'pass'),
])
def test_unscored_game(tc):
fx = Game_fixture(tc)
tc.assertDictEqual(fx.game.players, {'b' : 'one', 'w' : 'two'})
tc.assertIs(fx.game.get_controller('b'), fx.controller_b)
tc.assertIs(fx.game.get_controller('w'), fx.controller_w)
fx.game.allow_scorer('b') # it can't score
fx.game.ready()
fx.game.run()
fx.game.close_players()
tc.assertIsNone(fx.game.describe_late_errors())
tc.assertDictEqual(fx.game.result.players, {'b' : 'one', 'w' : 'two'})
tc.assertIsNone(fx.game.result.winning_colour)
tc.assertIsNone(fx.game.result.losing_colour)
tc.assertIsNone(fx.game.result.winning_player)
tc.assertIsNone(fx.game.result.losing_player)
tc.assertEqual(fx.game.result.sgf_result, "?")
tc.assertFalse(fx.game.result.is_forfeit)
tc.assertIs(fx.game.result.is_jigo, False)
tc.assertEqual(fx.game.result.detail, "no score reported")
tc.assertEqual(fx.game.result.describe(),
"one vs two ? (no score reported)")
tc.assertEqual(fx.game.describe_scoring(),
"one vs two ? (no score reported)")
result2 = pickle.loads(pickle.dumps(fx.game.result))
tc.assertEqual(result2.describe(), "one vs two ? (no score reported)")
tc.assertIs(result2.is_jigo, False)
def test_jigo(tc):
fx = Game_fixture(tc, komi=18.0)
fx.game.use_internal_scorer()
fx.game.ready()
tc.assertIsNone(fx.game.result)
fx.game.run()
fx.game.close_players()
tc.assertIsNone(fx.game.describe_late_errors())
tc.assertDictEqual(fx.game.result.players, {'b' : 'one', 'w' : 'two'})
tc.assertEqual(fx.game.result.player_b, 'one')
tc.assertEqual(fx.game.result.player_w, 'two')
tc.assertEqual(fx.game.result.winning_colour, None)
tc.assertEqual(fx.game.result.losing_colour, None)
tc.assertEqual(fx.game.result.winning_player, None)
tc.assertEqual(fx.game.result.losing_player, None)
tc.assertEqual(fx.game.result.sgf_result, "0")
tc.assertIs(fx.game.result.is_forfeit, False)
tc.assertIs(fx.game.result.is_jigo, True)
tc.assertIsNone(fx.game.result.detail)
tc.assertEqual(fx.game.result.describe(), "one vs two jigo")
tc.assertEqual(fx.game.describe_scoring(), "one vs two jigo")
result2 = pickle.loads(pickle.dumps(fx.game.result))
tc.assertEqual(result2.describe(), "one vs two jigo")
tc.assertEqual(result2.player_b, 'one')
tc.assertEqual(result2.player_w, 'two')
tc.assertIs(result2.is_jigo, True)
def test_players_score_agree(tc):
fx = Game_fixture(tc)
fx.run_score_test("b+3", "B+3.0")
tc.assertEqual(fx.game.result.sgf_result, "B+3")
tc.assertIsNone(fx.game.result.detail)
tc.assertEqual(fx.game.result.winning_colour, 'b')
tc.assertEqual(fx.game.describe_scoring(), "one beat two B+3")
def test_players_score_agree_draw(tc):
fx = Game_fixture(tc)
fx.run_score_test("0", "0")
tc.assertEqual(fx.game.result.sgf_result, "0")
tc.assertIsNone(fx.game.result.detail)
tc.assertIsNone(fx.game.result.winning_colour)
tc.assertEqual(fx.game.describe_scoring(), "one vs two jigo")
def test_players_score_disagree(tc):
fx = Game_fixture(tc)
fx.run_score_test("b+3.0", "W+4")
tc.assertEqual(fx.game.result.sgf_result, "?")
tc.assertEqual(fx.game.result.detail, "players disagreed")
tc.assertIsNone(fx.game.result.winning_colour)
tc.assertEqual(fx.game.describe_scoring(),
"one vs two ? (players disagreed)\n"
"one final_score: b+3.0\n"
"two final_score: W+4")
def test_players_score_disagree_one_no_margin(tc):
fx = Game_fixture(tc)
fx.run_score_test("b+", "W+4")
tc.assertEqual(fx.game.result.sgf_result, "?")
tc.assertEqual(fx.game.result.detail, "players disagreed")
tc.assertEqual(fx.game.describe_scoring(),
"one vs two ? (players disagreed)\n"
"one final_score: b+\n"
"two final_score: W+4")
def test_players_score_disagree_one_jigo(tc):
fx = Game_fixture(tc)
fx.run_score_test("0", "W+4")
tc.assertEqual(fx.game.result.sgf_result, "?")
tc.assertEqual(fx.game.result.detail, "players disagreed")
tc.assertIsNone(fx.game.result.winning_colour)
tc.assertEqual(fx.game.describe_scoring(),
"one vs two ? (players disagreed)\n"
"one final_score: 0\n"
"two final_score: W+4")
def test_players_score_disagree_equal_margin(tc):
# check equal margin in both directions doesn't confuse it
fx = Game_fixture(tc)
fx.run_score_test("b+4", "W+4")
tc.assertEqual(fx.game.result.sgf_result, "?")
tc.assertEqual(fx.game.result.detail, "players disagreed")
tc.assertIsNone(fx.game.result.winning_colour)
tc.assertEqual(fx.game.describe_scoring(),
"one vs two ? (players disagreed)\n"
"one final_score: b+4\n"
"two final_score: W+4")
def test_players_score_one_unreliable(tc):
fx = Game_fixture(tc)
fx.run_score_test("b+3", "W+4", allowed_scorers="w")
tc.assertEqual(fx.game.result.sgf_result, "W+4")
tc.assertIsNone(fx.game.result.detail)
tc.assertEqual(fx.game.result.winning_colour, 'w')
tc.assertEqual(fx.game.describe_scoring(), "two beat one W+4")
def test_players_score_one_cannot_score(tc):
fx = Game_fixture(tc)
fx.run_score_test(None, "W+4")
tc.assertEqual(fx.game.result.sgf_result, "W+4")
tc.assertIsNone(fx.game.result.detail)
tc.assertEqual(fx.game.result.winning_colour, 'w')
tc.assertEqual(fx.game.describe_scoring(), "two beat one W+4")
def test_players_score_one_fails(tc):
fx = Game_fixture(tc)
fx.run_score_test(Exception, "W+4")
tc.assertEqual(fx.game.result.sgf_result, "W+4")
tc.assertIsNone(fx.game.result.detail)
tc.assertEqual(fx.game.result.winning_colour, 'w')
tc.assertEqual(fx.game.describe_scoring(), "two beat one W+4")
def test_players_score_one_illformed(tc):
fx = Game_fixture(tc)
fx.run_score_test("black wins", "W+4.5")
tc.assertEqual(fx.game.result.sgf_result, "W+4.5")
tc.assertIsNone(fx.game.result.detail)
tc.assertEqual(fx.game.result.winning_colour, 'w')
tc.assertEqual(fx.game.describe_scoring(),
"two beat one W+4.5\n"
"one final_score: black wins\n"
"two final_score: W+4.5")
def test_players_score_agree_except_margin(tc):
fx = Game_fixture(tc)
fx.run_score_test("b+3", "B+4.0")
tc.assertEqual(fx.game.result.sgf_result, "B+")
tc.assertEqual(fx.game.result.detail, "unknown margin")
tc.assertEqual(fx.game.result.winning_colour, 'b')
tc.assertEqual(fx.game.describe_scoring(),
"one beat two B+ (unknown margin)\n"
"one final_score: b+3\n"
"two final_score: B+4.0")
def test_players_score_agree_one_no_margin(tc):
fx = Game_fixture(tc)
fx.run_score_test("b+3", "B+")
tc.assertEqual(fx.game.result.sgf_result, "B+")
tc.assertEqual(fx.game.result.detail, "unknown margin")
tc.assertEqual(fx.game.result.winning_colour, 'b')
tc.assertEqual(fx.game.describe_scoring(),
"one beat two B+ (unknown margin)\n"
"one final_score: b+3\n"
"two final_score: B+")
def test_players_score_agree_one_illformed_margin(tc):
fx = Game_fixture(tc)
fx.run_score_test("b+3", "B+a")
tc.assertEqual(fx.game.result.sgf_result, "B+")
tc.assertEqual(fx.game.result.detail, "unknown margin")
tc.assertEqual(fx.game.result.winning_colour, 'b')
tc.assertEqual(fx.game.describe_scoring(),
"one beat two B+ (unknown margin)\n"
"one final_score: b+3\n"
"two final_score: B+a")
def test_players_score_agree_margin_zero(tc):
fx = Game_fixture(tc)
fx.run_score_test("b+0", "B+0")
tc.assertEqual(fx.game.result.sgf_result, "B+")
tc.assertEqual(fx.game.result.detail, "unknown margin")
tc.assertEqual(fx.game.result.winning_colour, 'b')
tc.assertEqual(fx.game.describe_scoring(),
"one beat two B+ (unknown margin)\n"
"one final_score: b+0\n"
"two final_score: B+0")
def test_claim(tc):
def handle_genmove_ex_b(args):
tc.assertIn('claim', args)
if fx.player_b.row_to_play < 3:
return fx.player_b.handle_genmove(args)
return "claim"
def handle_genmove_ex_w(args):
return "claim"
fx = Game_fixture(tc)
fx.engine_b.add_command('gomill-genmove_ex', handle_genmove_ex_b)
fx.engine_w.add_command('gomill-genmove_ex', handle_genmove_ex_w)
fx.game.use_internal_scorer()
fx.game.set_claim_allowed('b')
fx.game.ready()
fx.game.run()
fx.game.close_players()
tc.assertEqual(fx.game.result.sgf_result, "B+")
tc.assertEqual(fx.game.result.detail, "claim")
tc.assertEqual(fx.game.result.winning_colour, 'b')
tc.assertEqual(fx.game.result.winning_player, 'one')
tc.assertFalse(fx.game.result.is_forfeit)
tc.assertEqual(fx.game.result.describe(), "one beat two B+ (claim)")
tc.assertEqual(fx.game.describe_scoring(), "one beat two B+ (claim)")
fx.check_moves([
('b', 'E1'), ('w', 'G1'),
('b', 'E2'), ('w', 'G2'),
('b', 'E3'), ('w', 'G3'),
])
def test_forfeit_occupied_point(tc):
moves = [
('b', 'C3'), ('w', 'D3'),
('b', 'D4'), ('w', 'D4'), # occupied point
]
fx = Game_fixture(tc, Programmed_player(moves), Programmed_player(moves))
fx.game.use_internal_scorer()
fx.game.ready()
fx.game.run()
fx.game.close_players()
tc.assertEqual(fx.game.result.sgf_result, "B+F")
tc.assertEqual(fx.game.result.winning_colour, 'b')
tc.assertEqual(fx.game.result.winning_player, 'one')
tc.assertTrue(fx.game.result.is_forfeit)
tc.assertEqual(fx.game.result.detail,
"forfeit: two attempted move to occupied point d4")
tc.assertEqual(fx.game.result.describe(),
"one beat two B+F "
"(forfeit: two attempted move to occupied point d4)")
fx.check_moves(moves[:-1])
def test_forfeit_simple_ko(tc):
moves = [
('b', 'C5'), ('w', 'F5'),
('b', 'D6'), ('w', 'E4'),
('b', 'D4'), ('w', 'E6'),
('b', 'E5'), ('w', 'D5'),
('b', 'E5'), # ko violation
]
fx = Game_fixture(tc, Programmed_player(moves), Programmed_player(moves))
fx.game.use_internal_scorer()
fx.game.ready()
fx.game.run()
fx.game.close_players()
tc.assertEqual(fx.game.result.sgf_result, "W+F")
tc.assertEqual(fx.game.result.winning_colour, 'w')
tc.assertEqual(fx.game.result.winning_player, 'two')
tc.assertTrue(fx.game.result.is_forfeit)
tc.assertEqual(fx.game.result.detail,
"forfeit: one attempted move to ko-forbidden point e5")
fx.check_moves(moves[:-1])
def test_forfeit_illformed_move(tc):
moves = [
('b', 'C5'), ('w', 'F5'),
('b', 'D6'), ('w', 'Z99'), # ill-formed move
]
fx = Game_fixture(tc, Programmed_player(moves), Programmed_player(moves))
fx.game.use_internal_scorer()
fx.game.ready()
fx.game.run()
fx.game.close_players()
tc.assertEqual(fx.game.result.sgf_result, "B+F")
tc.assertEqual(fx.game.result.winning_colour, 'b')
tc.assertEqual(fx.game.result.winning_player, 'one')
tc.assertTrue(fx.game.result.is_forfeit)
tc.assertEqual(fx.game.result.detail,
"forfeit: two attempted ill-formed move z99")
fx.check_moves(moves[:-1])
def test_forfeit_genmove_fails(tc):
moves = [
('b', 'C5'), ('w', 'F5'),
('b', 'fail'), # GTP failure response
]
fx = Game_fixture(tc, Programmed_player(moves), Programmed_player(moves))
fx.game.use_internal_scorer()
fx.game.ready()
fx.game.run()
fx.game.close_players()
tc.assertEqual(fx.game.result.sgf_result, "W+F")
tc.assertEqual(fx.game.result.winning_colour, 'w')
tc.assertEqual(fx.game.result.winning_player, 'two')
tc.assertTrue(fx.game.result.is_forfeit)
tc.assertEqual(fx.game.result.detail,
"forfeit: failure response from 'genmove b' to player one:\n"
"forced to fail")
fx.check_moves(moves[:-1])
def test_forfeit_rejected_as_illegal(tc):
moves = [
('b', 'C5'), ('w', 'F5'),
('b', 'D6'), ('w', 'E4'), # will be rejected
]
fx = Game_fixture(tc,
Programmed_player(moves, reject=('E4', 'illegal move')),
Programmed_player(moves))
fx.game.use_internal_scorer()
fx.game.ready()
fx.game.run()
fx.game.close_players()
tc.assertEqual(fx.game.result.sgf_result, "B+F")
tc.assertEqual(fx.game.result.winning_colour, 'b')
tc.assertEqual(fx.game.result.winning_player, 'one')
tc.assertTrue(fx.game.result.is_forfeit)
tc.assertEqual(fx.game.result.detail,
"forfeit: one claims move e4 is illegal")
fx.check_moves(moves[:-1])
def test_forfeit_play_failed(tc):
moves = [
('b', 'C5'), ('w', 'F5'),
('b', 'D6'), ('w', 'E4'), # will be rejected
]
fx = Game_fixture(tc,
Programmed_player(moves, reject=('E4', 'crash')),
Programmed_player(moves))
fx.game.use_internal_scorer()
fx.game.ready()
fx.game.run()
fx.game.close_players()
tc.assertEqual(fx.game.result.sgf_result, "W+F")
tc.assertEqual(fx.game.result.winning_colour, 'w')
tc.assertEqual(fx.game.result.winning_player, 'two')
tc.assertTrue(fx.game.result.is_forfeit)
tc.assertEqual(fx.game.result.detail,
"forfeit: failure response from 'play w e4' to player one:\n"
"crash")
fx.check_moves(moves[:-1])
def test_same_player_code(tc):
game = gtp_games.Game(board_size=9, komi=0)
game.set_player_code('b', 'one')
tc.assertRaisesRegexp(ValueError, "player codes must be distinct",
game.set_player_code, 'w', 'one')
def test_make_sgf(tc):
fx = Game_fixture(tc)
fx.game.use_internal_scorer()
fx.game.ready()
fx.game.run()
fx.game.close_players()
tc.assertMultiLineEqual(fx.sgf_string(), """\
(;FF[4]AP[gomill:VER]CA[UTF-8]DT[***]GM[1]KM[0]RE[B+18]SZ[9];B[ei];W[gi];B[eh];W[gh];B[eg];W[gg];B[ef];W[gf];B[ee];W[ge];B[ed];W[gd];B[ec];W[gc];B[eb];W[gb];B[ea];W[ga];B[tt];C[one beat two B+18]W[tt])
""")
tc.assertMultiLineEqual(gomill_test_support.scrub_sgf(
fx.game.make_sgf(game_end_message="zzzz").serialise(wrap=None)), """\
(;FF[4]AP[gomill:VER]CA[UTF-8]DT[***]GM[1]KM[0]RE[B+18]SZ[9];B[ei];W[gi];B[eh];W[gh];B[eg];W[gg];B[ef];W[gf];B[ee];W[ge];B[ed];W[gd];B[ec];W[gc];B[eb];W[gb];B[ea];W[ga];B[tt];C[one beat two B+18
zzzz]W[tt])
""")
def test_game_id(tc):
fx = Game_fixture(tc)
fx.game.use_internal_scorer()
fx.game.set_game_id("gitest")
fx.game.ready()
fx.game.run()
fx.game.close_players()
tc.assertEqual(fx.game.result.game_id, "gitest")
tc.assertMultiLineEqual(fx.sgf_string(), """\
(;FF[4]AP[gomill:VER]CA[UTF-8]DT[***]GM[1]GN[gitest]KM[0]RE[B+18]SZ[9];B[ei];W[gi];B[eh];W[gh];B[eg];W[gg];B[ef];W[gf];B[ee];W[ge];B[ed];W[gd];B[ec];W[gc];B[eb];W[gb];B[ea];W[ga];B[tt];C[one beat two B+18]W[tt])
""")
def test_explain_last_move(tc):
counter = [0]
def handle_explain_last_move(args):
counter[0] += 1
return "EX%d" % counter[0]
fx = Game_fixture(tc)
fx.engine_b.add_command('gomill-explain_last_move',
handle_explain_last_move)
fx.game.ready()
fx.game.run()
fx.game.close_players()
tc.assertMultiLineEqual(fx.sgf_string(), """\
(;FF[4]AP[gomill:VER]CA[UTF-8]DT[***]GM[1]KM[0]RE[?]SZ[9];B[ei]C[EX1];W[gi];B[eh]C[EX2];W[gh];B[eg]C[EX3];W[gg];B[ef]C[EX4];W[gf];B[ee]C[EX5];W[ge];B[ed]C[EX6];W[gd];B[ec]C[EX7];W[gc];B[eb]C[EX8];W[gb];B[ea]C[EX9];W[ga];B[tt]C[EX10];C[one vs two ? (no score reported)]W[tt])
""")
def test_fixed_handicap(tc):
fh_calls = []
def handle_fixed_handicap(args):
fh_calls.append(args[0])
return "C3 G7 C7"
fx = Game_fixture(tc)
fx.engine_b.add_command('fixed_handicap', handle_fixed_handicap)
fx.engine_w.add_command('fixed_handicap', handle_fixed_handicap)
fx.game.ready()
fx.game.set_handicap(3, is_free=False)
tc.assertEqual(fh_calls, ["3", "3"])
fx.game.run()
fx.game.close_players()
tc.assertEqual(fx.game.result.sgf_result, "B+F")
tc.assertEqual(fx.game.result.detail,
"forfeit: two attempted move to occupied point g7")
fx.check_moves([
('w', 'G1'), ('b', 'E1'),
('w', 'G2'), ('b', 'E2'),
('w', 'G3'), ('b', 'E3'),
('w', 'G4'), ('b', 'E4'),
('w', 'G5'), ('b', 'E5'),
('w', 'G6'), ('b', 'E6'),
])
tc.assertMultiLineEqual(fx.sgf_string(), """\
(;FF[4]AB[cc][cg][gc]AP[gomill:VER]CA[UTF-8]DT[***]GM[1]HA[3]KM[0]RE[B+F]SZ[9];W[gi];B[ei];W[gh];B[eh];W[gg];B[eg];W[gf];B[ef];W[ge];B[ee];W[gd];B[ed]C[one beat two B+F (forfeit: two attempted move to occupied point g7)])
""")
def test_fixed_handicap_bad_engine(tc):
fh_calls = []
def handle_fixed_handicap_good(args):
fh_calls.append(args[0])
return "g7 c7 c3"
def handle_fixed_handicap_bad(args):
fh_calls.append(args[0])
return "C3 G3 C7" # Should be G7, not G3
fx = Game_fixture(tc)
fx.engine_b.add_command('fixed_handicap', handle_fixed_handicap_good)
fx.engine_w.add_command('fixed_handicap', handle_fixed_handicap_bad)
fx.game.ready()
tc.assertRaisesRegexp(
gtp_controller.BadGtpResponse,
"^bad response from fixed_handicap command to two: C3 G3 C7$",
fx.game.set_handicap, 3, is_free=False)
handicap_compensation_tests = [
# test code, handicap_compensation, result
('no', 'no', "B+53"),
('full', 'full', "B+50"),
('short', 'short', "B+51"),
]
class Handicap_compensation_TestCase(
gomill_test_support.Gomill_ParameterisedTestCase):
test_name = "test_handicap_compensation"
parameter_names = ('hc', 'result')
def runTest(self):
def handle_fixed_handicap(args):
return "D4 K10 D10"
fx = Game_fixture(self, board_size=13)
fx.engine_b.add_command('fixed_handicap', handle_fixed_handicap)
fx.engine_w.add_command('fixed_handicap', handle_fixed_handicap)
fx.game.use_internal_scorer(handicap_compensation=self.hc)
fx.game.set_handicap(3, is_free=False)
fx.game.ready()
fx.game.run()
fx.game.close_players()
self.assertEqual(fx.game.result.sgf_result, self.result)

View File

@ -0,0 +1,242 @@
"""Tests for gtp_proxy.py"""
from __future__ import with_statement
from gomill import gtp_controller
from gomill import gtp_proxy
from gomill.gtp_engine import GtpError, GtpFatalError
from gomill.gtp_controller import (
GtpChannelError, GtpProtocolError, GtpTransportError, GtpChannelClosed,
BadGtpResponse, Gtp_controller)
from gomill.gtp_proxy import BackEndError
from gomill_tests import test_framework
from gomill_tests import gomill_test_support
from gomill_tests import gtp_controller_test_support
from gomill_tests import gtp_engine_fixtures
from gomill_tests import gtp_engine_test_support
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
class Proxy_fixture(test_framework.Fixture):
"""Fixture managing a Gtp_proxy with the test engine as its back-end.
attributes:
proxy -- Gtp_proxy
controller -- Gtp_controller
channel -- Testing_gtp_channel (like get_test_channel())
engine -- the proxy engine
underlying_engine -- the underlying test engine (like get_test_engine())
commands_handled -- from the underlying Test_gtp_engine_protocol
"""
def __init__(self, tc):
self.tc = tc
self.channel = gtp_engine_fixtures.get_test_channel()
self.underlying_engine = self.channel.engine
self.controller = gtp_controller.Gtp_controller(
self.channel, 'testbackend')
self.proxy = gtp_proxy.Gtp_proxy()
self.proxy.set_back_end_controller(self.controller)
self.engine = self.proxy.engine
self.commands_handled = self.underlying_engine.commands_handled
def check_command(self, *args, **kwargs):
"""Send a command to the proxy engine and check its response.
(This is testing the proxy engine, not the underlying engine.)
parameters as for gtp_engine_test_support.check_engine()
"""
gtp_engine_test_support.check_engine(
self.tc, self.engine, *args, **kwargs)
def test_proxy(tc):
fx = Proxy_fixture(tc)
fx.check_command('test', ['ab', 'cd'], "args: ab cd")
fx.proxy.close()
tc.assertEqual(
fx.commands_handled,
[('list_commands', []), ('test', ['ab', 'cd']), ('quit', [])])
tc.assertTrue(fx.controller.channel.is_closed)
def test_close_after_quit(tc):
fx = Proxy_fixture(tc)
fx.check_command('quit', [], "", expect_end=True)
fx.proxy.close()
tc.assertEqual(
fx.commands_handled,
[('list_commands', []), ('quit', [])])
tc.assertTrue(fx.channel.is_closed)
def test_list_commands(tc):
fx = Proxy_fixture(tc)
tc.assertListEqual(
fx.engine.list_commands(),
['error', 'fatal', 'gomill-passthrough', 'known_command',
'list_commands', 'multiline', 'protocol_version', 'quit', 'test'])
fx.proxy.close()
def test_back_end_has_command(tc):
fx = Proxy_fixture(tc)
tc.assertTrue(fx.proxy.back_end_has_command('test'))
tc.assertFalse(fx.proxy.back_end_has_command('xyzzy'))
tc.assertFalse(fx.proxy.back_end_has_command('gomill-passthrough'))
def test_passthrough(tc):
fx = Proxy_fixture(tc)
fx.check_command('known_command', ['gomill-passthrough'], "true")
fx.check_command('gomill-passthrough', ['test', 'ab', 'cd'], "args: ab cd")
fx.check_command(
'gomill-passthrough', ['known_command', 'gomill-passthrough'], "false")
fx.check_command('gomill-passthrough', [],
"invalid arguments", expect_failure=True)
tc.assertEqual(
fx.commands_handled,
[('list_commands', []), ('test', ['ab', 'cd']),
('known_command', ['gomill-passthrough'])])
def test_pass_command(tc):
fx = Proxy_fixture(tc)
tc.assertEqual(fx.proxy.pass_command("test", ["ab", "cd"]), "args: ab cd")
with tc.assertRaises(BadGtpResponse) as ar:
fx.proxy.pass_command("error", [])
tc.assertEqual(ar.exception.gtp_error_message, "normal error")
tc.assertEqual(str(ar.exception),
"failure response from 'error' to testbackend:\n"
"normal error")
def test_pass_command_with_channel_error(tc):
fx = Proxy_fixture(tc)
fx.channel.fail_next_command = True
with tc.assertRaises(BackEndError) as ar:
fx.proxy.pass_command("test", [])
tc.assertEqual(str(ar.exception),
"transport error sending 'test' to testbackend:\n"
"forced failure for send_command_line")
tc.assertIsInstance(ar.exception.cause, GtpTransportError)
fx.proxy.close()
tc.assertEqual(fx.commands_handled, [('list_commands', [])])
def test_handle_command(tc):
def handle_xyzzy(args):
if args and args[0] == "error":
return fx.proxy.handle_command("error", [])
else:
return fx.proxy.handle_command("test", ["nothing", "happens"])
fx = Proxy_fixture(tc)
fx.engine.add_command("xyzzy", handle_xyzzy)
fx.check_command('xyzzy', [], "args: nothing happens")
fx.check_command('xyzzy', ['error'],
"normal error", expect_failure=True)
def test_handle_command_with_channel_error(tc):
def handle_xyzzy(args):
return fx.proxy.handle_command("test", [])
fx = Proxy_fixture(tc)
fx.engine.add_command("xyzzy", handle_xyzzy)
fx.channel.fail_next_command = True
fx.check_command('xyzzy', [],
"transport error sending 'test' to testbackend:\n"
"forced failure for send_command_line",
expect_failure=True, expect_end=True)
fx.proxy.close()
tc.assertEqual(fx.commands_handled, [('list_commands', [])])
def test_back_end_goes_away(tc):
fx = Proxy_fixture(tc)
tc.assertEqual(fx.proxy.pass_command("quit", []), "")
fx.check_command('test', ['ab', 'cd'],
"error sending 'test ab cd' to testbackend:\n"
"engine has closed the command channel",
expect_failure=True, expect_end=True)
def test_close_with_errors(tc):
fx = Proxy_fixture(tc)
fx.channel.fail_next_command = True
with tc.assertRaises(BackEndError) as ar:
fx.proxy.close()
tc.assertEqual(str(ar.exception),
"transport error sending 'quit' to testbackend:\n"
"forced failure for send_command_line")
tc.assertTrue(fx.channel.is_closed)
def test_quit_ignores_already_closed(tc):
fx = Proxy_fixture(tc)
tc.assertEqual(fx.proxy.pass_command("quit", []), "")
fx.check_command('quit', [], "", expect_end=True)
fx.proxy.close()
tc.assertEqual(fx.commands_handled,
[('list_commands', []), ('quit', [])])
def test_quit_with_failure_response(tc):
fx = Proxy_fixture(tc)
fx.underlying_engine.force_error("quit")
fx.check_command('quit', [], None,
expect_failure=True, expect_end=True)
fx.proxy.close()
tc.assertEqual(fx.commands_handled,
[('list_commands', []), ('quit', [])])
def test_quit_with_channel_error(tc):
fx = Proxy_fixture(tc)
fx.channel.fail_next_command = True
fx.check_command('quit', [],
"transport error sending 'quit' to testbackend:\n"
"forced failure for send_command_line",
expect_failure=True, expect_end=True)
fx.proxy.close()
tc.assertEqual(fx.commands_handled, [('list_commands', [])])
def test_nontgtp_backend(tc):
channel = gtp_controller_test_support.Preprogrammed_gtp_channel(
"Usage: randomprogram [options]\n\nOptions:\n"
"--help show this help message and exit\n")
controller = gtp_controller.Gtp_controller(channel, 'testbackend')
proxy = gtp_proxy.Gtp_proxy()
with tc.assertRaises(BackEndError) as ar:
proxy.set_back_end_controller(controller)
tc.assertEqual(str(ar.exception),
"GTP protocol error reading response to first command "
"(list_commands) from testbackend:\n"
"engine isn't speaking GTP: first byte is 'U'")
tc.assertIsInstance(ar.exception.cause, GtpProtocolError)
proxy.close()
def test_error_from_list_commands(tc):
channel = gtp_engine_fixtures.get_test_channel()
channel.engine.force_error("list_commands")
controller = gtp_controller.Gtp_controller(channel, 'testbackend')
proxy = gtp_proxy.Gtp_proxy()
with tc.assertRaises(BackEndError) as ar:
proxy.set_back_end_controller(controller)
tc.assertEqual(str(ar.exception),
"failure response from first command "
"(list_commands) to testbackend:\n"
"handler forced to fail")
tc.assertIsInstance(ar.exception.cause, BadGtpResponse)
proxy.close()
def test_set_back_end_subprocess(tc):
fx = gtp_engine_fixtures.State_reporter_fixture(tc)
proxy = gtp_proxy.Gtp_proxy()
# the state-report will be taken as the response to list_commands
proxy.set_back_end_subprocess(fx.cmd, stderr=fx.devnull)
proxy.expect_back_end_exit()
proxy.close()
def test_set_back_end_subprocess_nonexistent_program(tc):
proxy = gtp_proxy.Gtp_proxy()
with tc.assertRaises(BackEndError) as ar:
proxy.set_back_end_subprocess("/nonexistent/program")
tc.assertEqual(str(ar.exception),
"can't launch back end command\n"
"[Errno 2] No such file or directory")
tc.assertIsInstance(ar.exception.cause, GtpChannelError)
# check it's safe to close when the controller was never set
proxy.close()

View File

@ -0,0 +1,106 @@
"""Support code for testing gtp_states."""
from gomill import gtp_states
from gomill.common import *
class Player(object):
"""Player (stateful move generator) for testing gtp_states.
Public attributes:
last_game_state -- the Game_state from the last genmove-like command
"""
def __init__(self):
self.next_move = None
self.next_comment = None
self.next_cookie = None
self.last_game_state = None
self.resign_next_move = False
def set_next_move(self, vertex, comment=None, cookie=None):
"""Specify what to return from the next genmove-like command."""
self.next_move = move_from_vertex(vertex, 19)
self.next_comment = comment
self.next_cookie = cookie
def set_next_move_resign(self):
self.resign_next_move = True
def genmove(self, game_state, player):
"""Move generator returns points from the move list.
game_state -- gtp_states.Game_state
player -- 'b' or 'w'
"""
self.last_game_state = game_state
# Freeze the move_history as we saw it
self.last_game_state.move_history = self.last_game_state.move_history[:]
result = gtp_states.Move_generator_result()
if self.resign_next_move:
result.resign = True
elif self.next_move is not None:
result.move = self.next_move
else:
result.pass_move = True
if self.next_comment is not None:
result.comments = self.next_comment
if self.next_cookie is not None:
result.cookie = self.next_cookie
self.next_move = None
self.next_comment = None
self.next_cookie = None
self.resign_next_move = False
return result
class Testing_gtp_state(gtp_states.Gtp_state):
"""Variant of Gtp_state suitable for use in tests.
This doesn't read from or write to the filesystem.
"""
def __init__(self, *args, **kwargs):
super(Testing_gtp_state, self).__init__(*args, **kwargs)
self._file_contents = {}
def _register_file(self, pathname, contents):
self._file_contents[pathname] = contents
def _load_file(self, pathname):
try:
return self._file_contents[pathname]
except KeyError:
raise EnvironmentError("unknown file: %s" % pathname)
def _save_file(self, pathname, contents):
if pathname == "force_fail":
open("/nonexistent_directory/foo.sgf", "w")
self._file_contents[pathname] = contents
def _choose_free_handicap_moves(self, number_of_stones):
"""Implementation of place_free_handicap.
Returns for a given number of stones:
2 -- A1 A2 A3 (too many stones)
4 -- A1 A2 A3 pass (pass isn't permitted)
5 -- A1 A2 A3 A4 A5 (which is ok)
6 -- A1 A2 A3 A4 A5 A1 (repeated point)
8 -- not even the right result type
otherwise Gtp_state default (which is to use the fixed handicap points)
"""
if number_of_stones == 2:
return ((0, 0), (1, 0), (2, 0))
elif number_of_stones == 4:
return ((0, 0), (1, 0), (2, 0), None)
elif number_of_stones == 5:
return ((0, 0), (1, 0), (2, 0), (3, 0), (4, 0))
elif number_of_stones == 6:
return ((0, 0), (1, 0), (2, 0), (3, 0), (4, 0), (0, 0))
elif number_of_stones == 8:
return "nonsense"
else:
return super(Testing_gtp_state, self).\
_choose_free_handicap_moves(number_of_stones)

View File

@ -0,0 +1,581 @@
"""Tests for gtp_state.py."""
from textwrap import dedent
from gomill import boards
from gomill import gtp_engine
from gomill import gtp_states
from gomill.common import format_vertex
from gomill_tests import test_framework
from gomill_tests import gomill_test_support
from gomill_tests import gtp_engine_test_support
from gomill_tests import gtp_state_test_support
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
class Gtp_state_fixture(test_framework.Fixture):
"""Fixture for managing a Gtp_state.
The move generator comes from gtp_state_test_support.Player
Adds a type equality function for History_move.
"""
def __init__(self, tc):
self.tc = tc
self.player = gtp_state_test_support.Player()
self.gtp_state = gtp_state_test_support.Testing_gtp_state(
move_generator=self.player.genmove,
acceptable_sizes=(9, 11, 13, 19))
self.engine = gtp_engine.Gtp_engine_protocol()
self.engine.add_protocol_commands()
self.engine.add_commands(self.gtp_state.get_handlers())
self.tc.addTypeEqualityFunc(
gtp_states.History_move, self.assertHistoryMoveEqual)
def assertHistoryMoveEqual(self, hm1, hm2, msg=None):
t1 = (hm1.colour, hm1.move, hm1.comments, hm1.cookie)
t2 = (hm2.colour, hm2.move, hm2.comments, hm2.cookie)
self.tc.assertEqual(t1, t2, "History_moves differ")
def check_command(self, *args, **kwargs):
"""Check a single GTP command.
parameters as for gtp_engine_test_support.check_engine()
"""
gtp_engine_test_support.check_engine(
self.tc, self.engine, *args, **kwargs)
def check_board_empty_9(self):
self.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 . . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
def test_gtp_state(tc):
fx = Gtp_state_fixture(tc)
fx.check_command('nonsense', [''], "unknown command",
expect_failure=True)
fx.check_command('protocol_version', [''], "2")
fx.player.set_next_move("A3", "preprogrammed move 0")
fx.check_command('genmove', ['B'], "A3")
game_state = fx.player.last_game_state
# default board size is min(acceptable_sizes)
tc.assertEqual(game_state.size, 9)
b = boards.Board(9)
b.play(2, 0, 'b')
tc.assertEqual(game_state.board, b)
tc.assertEqual(game_state.komi, 0.0)
tc.assertEqual(game_state.history_base, boards.Board(9))
tc.assertEqual(game_state.move_history, [])
tc.assertIsNone(game_state.ko_point)
tc.assertIsNone(game_state.handicap)
tc.assertIs(game_state.for_regression, False)
tc.assertIsNone(game_state.time_settings)
tc.assertIsNone(game_state.time_remaining)
tc.assertIsNone(game_state.canadian_stones_remaining)
fx.check_command('gomill-explain_last_move', [], "preprogrammed move 0")
fx.check_command('play', ['W', 'A4'], "")
fx.check_command('komi', ['5.5'], "")
fx.player.set_next_move("C9")
fx.check_command('genmove', ['B'], "C9")
game_state = fx.player.last_game_state
tc.assertEqual(game_state.komi, 5.5)
tc.assertEqual(game_state.history_base, boards.Board(9))
tc.assertEqual(len(game_state.move_history), 2)
tc.assertEqual(game_state.move_history[0],
gtp_states.History_move('b', (2, 0), "preprogrammed move 0"))
tc.assertEqual(game_state.move_history[1],
gtp_states.History_move('w', (3, 0)))
fx.check_command('genmove', ['B'], "pass")
fx.check_command('gomill-explain_last_move', [], "")
fx.check_command('genmove', ['W'], "pass")
fx.check_command('showboard', [], dedent("""
9 . . # . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 o . . . . . . . .
3 # . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.player.set_next_move_resign()
fx.check_command('genmove', ['B'], "resign")
fx.check_command('quit', [''], "", expect_end=True)
def test_clear_board_and_boardsize(tc):
fx = Gtp_state_fixture(tc)
fx.check_command('play', ['W', 'A4'], "")
fx.check_command('boardsize', ['7'], "unacceptable size",
expect_failure=True)
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 o . . . . . . . .
3 . . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.check_command('clear_board', [], "")
fx.check_board_empty_9()
fx.check_command('play', ['W', 'A4'], "")
fx.check_command('boardsize', ['11'], "")
fx.check_command('showboard', [], dedent("""
11 . . . . . . . . . . .
10 . . . . . . . . . . .
9 . . . . . . . . . . .
8 . . . . . . . . . . .
7 . . . . . . . . . . .
6 . . . . . . . . . . .
5 . . . . . . . . . . .
4 . . . . . . . . . . .
3 . . . . . . . . . . .
2 . . . . . . . . . . .
1 . . . . . . . . . . .
A B C D E F G H J K L"""))
def test_play(tc):
fx = Gtp_state_fixture(tc)
fx.check_command('play', ['B', "E5"], "")
fx.check_command('play', ['w', "e4"], "")
fx.check_command('play', [], "invalid arguments", expect_failure=True)
fx.check_command('play', ['B'], "invalid arguments", expect_failure=True)
# additional arguments are ignored (following gnugo)
fx.check_command('play', ['B', "F4", "E5"], "")
fx.check_command('play', ['white', "f5"], "")
fx.check_command('play', ['W', "K3"], "vertex is off board: 'k3'",
expect_failure=True)
fx.check_command('play', ['X', "A4"], "invalid colour: 'X'",
expect_failure=True)
fx.check_command('play', ['BLACK', "e4"], "illegal move",
expect_failure=True)
fx.check_command('play', ['B', "pass"], "")
fx.check_command('play', ['W', "PASS"], "")
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . # o . . .
4 . . . . o # . . .
3 . . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
def test_komi(tc):
fx = Gtp_state_fixture(tc)
fx.check_command('genmove', ['B'], "pass")
tc.assertEqual(fx.player.last_game_state.komi, 0.0)
fx.check_command('komi', ['1'], "")
fx.check_command('genmove', ['B'], "pass")
tc.assertEqual(fx.player.last_game_state.komi, 1.0)
fx.check_command('komi', ['1.0'], "")
fx.check_command('genmove', ['B'], "pass")
tc.assertEqual(fx.player.last_game_state.komi, 1.0)
fx.check_command('komi', ['7.5'], "")
fx.check_command('genmove', ['B'], "pass")
tc.assertEqual(fx.player.last_game_state.komi, 7.5)
fx.check_command('komi', ['-3.5'], "")
fx.check_command('genmove', ['B'], "pass")
tc.assertEqual(fx.player.last_game_state.komi, -3.5)
fx.check_command('komi', ['20000'], "")
fx.check_command('genmove', ['B'], "pass")
tc.assertEqual(fx.player.last_game_state.komi, 625.0)
fx.check_command('komi', ['-20000'], "")
fx.check_command('genmove', ['B'], "pass")
tc.assertEqual(fx.player.last_game_state.komi, -625.0)
fx.check_command('komi', ['nonsense'], "invalid float: 'nonsense'",
expect_failure=True)
fx.check_command('komi', ['NaN'], "invalid float: 'NaN'",
expect_failure=True)
fx.check_command('komi', ['inf'], "invalid float: 'inf'",
expect_failure=True)
fx.check_command('komi', ['-1e400'], "invalid float: '-1e400'",
expect_failure=True)
def test_undo(tc):
fx = Gtp_state_fixture(tc)
fx.player.set_next_move("A3", "preprogrammed move A3")
fx.check_command('genmove', ['B'], "A3")
fx.check_command('gomill-explain_last_move', [], "preprogrammed move A3")
fx.check_command('play', ['W', 'A4'], "")
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 o . . . . . . . .
3 # . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.check_command('undo', [], "")
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 # . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.player.set_next_move("D4", "preprogrammed move D4")
fx.check_command('genmove', ['W'], "D4")
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . o . . . . .
3 # . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.check_command('gomill-explain_last_move', [], "preprogrammed move D4")
fx.check_command('undo', [], "")
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 # . . . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.check_command('gomill-explain_last_move', [], "preprogrammed move A3")
fx.check_command('undo', [], "")
fx.check_board_empty_9()
fx.check_command('gomill-explain_last_move', [], "")
fx.check_command('undo', [], "cannot undo", expect_failure=True)
def test_fixed_handicap(tc):
fx = Gtp_state_fixture(tc)
fx.check_command('fixed_handicap', ['3'], "C3 G7 C7")
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . # . . . # . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 . . # . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.check_command('genmove', ['B'], "pass")
tc.assertEqual(fx.player.last_game_state.handicap, 3)
fx.check_command('boardsize', ['19'], "")
fx.check_command('fixed_handicap', ['7'], "D4 Q16 D16 Q4 D10 Q10 K10")
fx.check_command('fixed_handicap', ['7'], "board not empty",
expect_failure=True)
fx.check_command('boardsize', ['9'], "")
fx.check_command('play', ['B', 'B2'], "")
fx.check_command('fixed_handicap', ['2'], "board not empty",
expect_failure=True)
fx.check_command('clear_board', [], "")
fx.check_command('fixed_handicap', ['0'], "invalid number of stones",
expect_failure=True)
fx.check_command('fixed_handicap', ['1'], "invalid number of stones",
expect_failure=True)
fx.check_command('fixed_handicap', ['10'], "invalid number of stones",
expect_failure=True)
fx.check_command('fixed_handicap', ['2.5'], "invalid int: '2.5'",
expect_failure=True)
fx.check_command('fixed_handicap', [], "invalid arguments",
expect_failure=True)
def test_place_free_handicap(tc):
# See gtp_state_test_support.Testing_gtp_state for description of the choice
# of points.
fx = Gtp_state_fixture(tc)
fx.check_command('place_free_handicap', ['3'], "C3 G7 C7")
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . # . . . # . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . . . . . .
3 . . # . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.check_command('genmove', ['B'], "pass")
tc.assertEqual(fx.player.last_game_state.handicap, 3)
fx.check_command('boardsize', ['19'], "")
fx.check_command('place_free_handicap', ['7'], "D4 Q16 D16 Q4 D10 Q10 K10")
fx.check_command('place_free_handicap', ['7'], "board not empty",
expect_failure=True)
fx.check_command('boardsize', ['9'], "")
fx.check_command('play', ['B', 'B2'], "")
fx.check_command('place_free_handicap', ['2'], "board not empty",
expect_failure=True)
fx.check_command('clear_board', [], "")
fx.check_command('place_free_handicap', ['0'], "invalid number of stones",
expect_failure=True)
fx.check_command('place_free_handicap', ['1'], "invalid number of stones",
expect_failure=True)
fx.check_command('place_free_handicap', ['2.5'], "invalid int: '2.5'",
expect_failure=True)
fx.check_command('place_free_handicap', [], "invalid arguments",
expect_failure=True)
fx.check_command('place_free_handicap', ['10'],
"C3 G7 C7 G3 C5 G5 E3 E7 E5")
fx.check_command('clear_board', [''], "")
fx.check_command('place_free_handicap', ['5'],
"A1 A2 A3 A4 A5")
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 # . . . . . . . .
4 # . . . . . . . .
3 # . . . . . . . .
2 # . . . . . . . .
1 # . . . . . . . .
A B C D E F G H J"""))
fx.check_command('clear_board', [''], "")
fx.check_command('place_free_handicap', ['6'],
"invalid result from move generator: A1,A2,A3,A4,A5,A1",
expect_failure=True)
fx.check_board_empty_9()
fx.check_command('place_free_handicap', ['2'],
"invalid result from move generator: A1,A2,A3",
expect_failure=True)
fx.check_board_empty_9()
fx.check_command('place_free_handicap', ['4'],
"invalid result from move generator: A1,A2,A3,pass",
expect_failure=True)
fx.check_board_empty_9()
fx.check_command('place_free_handicap', ['8'],
"ValueError: need more than 1 value to unpack",
expect_internal_error=True)
fx.check_board_empty_9()
def test_set_free_handicap(tc):
fx = Gtp_state_fixture(tc)
fx.check_command('set_free_handicap', ["C3", "E5", "C7"], "")
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . # . . . . . .
6 . . . . . . . . .
5 . . . . # . . . .
4 . . . . . . . . .
3 . . # . . . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.check_command('genmove', ['B'], "pass")
tc.assertEqual(fx.player.last_game_state.handicap, 3)
fx.check_command('boardsize', ['9'], "")
fx.check_command('play', ['B', 'B2'], "")
fx.check_command('set_free_handicap', ["C3", "E5"], "board not empty",
expect_failure=True)
fx.check_command('clear_board', [], "")
fx.check_command('set_free_handicap', ["C3"], "invalid number of stones",
expect_failure=True)
fx.check_command('set_free_handicap', [], "invalid number of stones",
expect_failure=True)
all_points = [format_vertex((i, j)) for i in range(9) for j in range(9)]
fx.check_command('set_free_handicap', all_points,
"invalid number of stones", expect_failure=True)
fx.check_command('set_free_handicap', ["C3", "asdasd"],
"invalid vertex: 'asdasd'", expect_failure=True)
fx.check_board_empty_9()
fx.check_command('set_free_handicap', ["C3", "pass"],
"'pass' not permitted", expect_failure=True)
fx.check_board_empty_9()
fx.check_command('set_free_handicap', ["C3", "E5", "C3"],
"engine error: C3 is occupied", expect_failure=True)
fx.check_board_empty_9()
def test_loadsgf(tc):
fx = Gtp_state_fixture(tc)
fx.gtp_state._register_file("invalid.sgf", "non-SGF data")
fx.gtp_state._register_file(
"test1.sgf",
"(;SZ[9];B[ee];W[eg];B[dg];W[dh];B[df];W[fh];B[];W[])")
fx.gtp_state._register_file(
"test2.sgf",
"(;SZ[9]AB[fe:ff]AW[gf:gg]PL[W];W[eh];B[ge])")
fx.check_command('loadsgf', ["unknown.sgf"],
"cannot load file", expect_failure=True)
fx.check_command('loadsgf', ["invalid.sgf"],
"cannot load file", expect_failure=True)
fx.check_command('loadsgf', ["test1.sgf"], "")
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . # . . . .
4 . . . # . . . . .
3 . . . # o . . . .
2 . . . o . o . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.check_command('loadsgf', ["test1.sgf", "4"], "")
# position _before_ move 4
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . # . . . .
4 . . . . . . . . .
3 . . . # o . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.check_command('undo', [], "")
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . # . . . .
4 . . . . . . . . .
3 . . . . o . . . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.check_command('loadsgf', ["test2.sgf"], "")
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . # # . .
4 . . . . . # o . .
3 . . . . . . o . .
2 . . . . o . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
fx.check_command('undo', [], "")
fx.check_command('undo', [], "")
fx.check_command('undo', [], "cannot undo", expect_failure=True)
fx.check_command('showboard', [], dedent("""
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . # . . .
4 . . . . . # o . .
3 . . . . . . o . .
2 . . . . . . . . .
1 . . . . . . . . .
A B C D E F G H J"""))
def test_savesgf(tc):
scrub_sgf = gomill_test_support.scrub_sgf
fx = Gtp_state_fixture(tc)
fx.check_command("play", ['B', 'D4'], "")
fx.player.set_next_move("C3", "preprogrammed move C3")
fx.check_command('genmove', ['W'], "C3")
fx.check_command('gomill-savesgf', ['out1.sgf'], "")
tc.assertEqual(
scrub_sgf(fx.gtp_state._load_file('out1.sgf')).replace("\n", ""),
"(;FF[4]AP[gomill:VER]CA[UTF-8]DT[***]GM[1]KM[0]"
"SZ[9];B[df];C[preprogrammed move C3]W[cg])")
fx.check_command(
'gomill-savesgf',
['out2.sgf', "PB=testplayer", "PW=GNU\_Go:3.8", "RE=W+3.5"],
"")
tc.assertEqual(
scrub_sgf(fx.gtp_state._load_file('out2.sgf')).replace("\n", ""),
"(;FF[4]AP[gomill:VER]CA[UTF-8]DT[***]GM[1]KM[0]"
"PB[testplayer]PW[GNU Go:3.8]RE[W+3.5]SZ[9];B[df]"
";C[preprogrammed move C3]W[cg])")
fx.check_command("boardsize", ['19'], "")
fx.check_command("fixed_handicap", ['3'], "D4 Q16 D16")
fx.check_command("komi", ['5.5'], "")
fx.check_command("play", ['W', 'A2'], "")
fx.check_command('gomill-savesgf', ['out3.sgf'], "")
tc.assertEqual(
scrub_sgf(fx.gtp_state._load_file('out3.sgf')).replace("\n", ""),
"(;FF[4]AB[dd][dp][pd]AP[gomill:VER]CA[UTF-8]DT[***]"
"GM[1]HA[3]KM[5.5]SZ[19];W[ar])")
fx.check_command(
'gomill-savesgf', ['force_fail'],
"error writing file: [Errno 2] "
"No such file or directory: '/nonexistent_directory/foo.sgf'",
expect_failure=True)
def test_get_last_move(tc):
fx = Gtp_state_fixture(tc)
fx.player.set_next_move("A3", "preprogrammed move A3")
fx.check_command('genmove', ['B'], "A3")
history_moves = fx.player.last_game_state.move_history
tc.assertEqual(gtp_states.get_last_move(history_moves, 'b'), (False, None))
tc.assertEqual(gtp_states.get_last_move(history_moves, 'w'), (False, None))
fx.player.set_next_move("B3", "preprogrammed move B3")
fx.check_command('genmove', ['W'], "B3")
history_moves = fx.player.last_game_state.move_history
tc.assertEqual(gtp_states.get_last_move(history_moves, 'b'), (False, None))
move_is_available, move = gtp_states.get_last_move(history_moves, 'w')
tc.assertIs(move_is_available, True)
tc.assertEqual(format_vertex(move), "A3")
fx.check_command('genmove', ['B'], "pass")
history_moves = fx.player.last_game_state.move_history
move_is_available, move = gtp_states.get_last_move(history_moves, 'b')
tc.assertIs(move_is_available, True)
tc.assertEqual(format_vertex(move), "B3")
tc.assertEqual(gtp_states.get_last_move(history_moves, 'w'), (False, None))
def test_get_last_move_and_cookie(tc):
fx = Gtp_state_fixture(tc)
fx.player.set_next_move("A3", "preprogrammed move A3", "COOKIE 1")
fx.check_command('genmove', ['B'], "A3")
history_moves = fx.player.last_game_state.move_history
tc.assertEqual(gtp_states.get_last_move_and_cookie(history_moves, 'b'),
(False, None, None))
tc.assertEqual(gtp_states.get_last_move_and_cookie(history_moves, 'w'),
(False, None, None))
fx.check_command('play', ['W', 'B3'], "")
fx.check_command('genmove', ['B'], "pass")
history_moves = fx.player.last_game_state.move_history
move_is_available, move, cookie = gtp_states.get_last_move_and_cookie(
history_moves, 'b')
tc.assertIs(move_is_available, True)
tc.assertEqual(format_vertex(move), "B3")
tc.assertIs(cookie, "COOKIE 1")
tc.assertEqual(gtp_states.get_last_move_and_cookie(history_moves, 'w'),
(False, None, None))

View File

@ -0,0 +1,481 @@
"""Tests for mcts_tuners.py"""
from __future__ import with_statement, division
from math import sqrt
import random
from textwrap import dedent
import cPickle as pickle
from gomill import mcts_tuners
from gomill.game_jobs import Game_job, Game_job_result
from gomill.gtp_games import Game_result
from gomill.mcts_tuners import Parameter_config
from gomill.competitions import (
Player_config, CompetitionError, ControlFileError)
from gomill_tests import gomill_test_support
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def simple_make_candidate(*args):
if -1 in args:
raise ValueError("oops")
return Player_config("cand " + " ".join(map(str, args)))
def default_config():
return {
'board_size' : 13,
'komi' : 7.5,
'players' : {
'opp' : Player_config("test"),
},
'candidate_colour' : 'w',
'opponent' : 'opp',
'exploration_coefficient' : 0.2,
'initial_visits' : 10,
'initial_wins' : 5,
'parameters' : [
Parameter_config(
'resign_at',
scale = float,
split = 12,
format = "rsn@ %.2f"),
Parameter_config(
'initial_wins',
scale = mcts_tuners.LINEAR(0, 100),
split = 10,
format = "iwins %d"),
],
'make_candidate' : simple_make_candidate,
}
def test_bad_komi(tc):
comp = mcts_tuners.Mcts_tuner('mctstest')
config = default_config()
config['komi'] = 6
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertEqual(str(ar.exception),
"komi: must be fractional to prevent jigos")
def test_parameter_config(tc):
comp = mcts_tuners.Mcts_tuner('mctstest')
config = default_config()
comp.initialise_from_control_file(config)
tc.assertEqual(comp.tree.max_depth, 1)
tc.assertEqual(comp.format_engine_parameters((0.5, 23)),
"rsn@ 0.50; iwins 23")
tc.assertEqual(comp.format_engine_parameters(('x', 23)),
"[resign_at?x]; iwins 23")
tc.assertEqual(comp.format_optimiser_parameters((0.5, 0.23)),
"rsn@ 0.50; iwins 23")
tc.assertEqual(comp.scale_parameters((0.5, 0.23)), (0.5, 23))
with tc.assertRaises(CompetitionError) as ar:
comp.scale_parameters((0.5, None))
tc.assertTracebackStringEqual(str(ar.exception), dedent("""\
error from scale for initial_wins
TypeError: unsupported operand type(s) for *: 'NoneType' and 'float'
traceback (most recent call last):
mcts_tuners|__call__
failing line:
result = (f * self.range) + self.lower_bound
"""))
tc.assertRaisesRegexp(
ValueError, "'code' not specified",
comp.parameter_spec_from_config, Parameter_config())
tc.assertRaisesRegexp(
ValueError, "code specified as both positional and keyword argument",
comp.parameter_spec_from_config,
Parameter_config('pa1', code='pa2', scale=float, split=2, format="%s"))
tc.assertRaisesRegexp(
ValueError, "too many positional arguments",
comp.parameter_spec_from_config,
Parameter_config('pa1', float, scale=float, split=2, format="%s"))
tc.assertRaisesRegexp(
ValueError, "'scale': invalid callable",
comp.parameter_spec_from_config,
Parameter_config('pa1', scale=None, split=2, format="%s"))
pspec = comp.parameter_spec_from_config(
Parameter_config('pa1', scale=float, split=2))
tc.assertRaisesRegexp(
ControlFileError, "'format': invalid format string",
comp.parameter_spec_from_config,
Parameter_config('pa1', scale=float, split=2, format="nopct"))
def test_bad_parameter_config(tc):
comp = mcts_tuners.Mcts_tuner('mctstest')
config = default_config()
config['parameters'].append(
Parameter_config(
'bad',
scale = mcts_tuners.LOG(0.0, 1.0),
split = 10))
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertMultiLineEqual(str(ar.exception), dedent("""\
parameter bad: 'scale': invalid parameters for LOG:
lower bound is zero"""))
def test_nonsense_parameter_config(tc):
comp = mcts_tuners.Mcts_tuner('mctstest')
config = default_config()
config['parameters'].append(99)
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertMultiLineEqual(str(ar.exception), dedent("""\
'parameters': item 2: not a Parameter"""))
def test_nocode_parameter_config(tc):
comp = mcts_tuners.Mcts_tuner('mctstest')
config = default_config()
config['parameters'].append(Parameter_config())
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertMultiLineEqual(str(ar.exception), dedent("""\
parameter 2: 'code' not specified"""))
def test_scale_check(tc):
comp = mcts_tuners.Mcts_tuner('mctstest')
config = default_config()
config['parameters'].append(
Parameter_config(
'bad',
scale = str.split,
split = 10))
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertTracebackStringEqual(str(ar.exception), dedent("""\
parameter bad: error from scale (applied to 0.05)
TypeError: split-wants-float-not-str
traceback (most recent call last):
"""), fixups=[
("descriptor 'split' requires a 'str' object but received a 'float'",
"split-wants-float-not-str"),
("unbound method split() must be called with str instance as "
"first argument (got float instance instead)",
"split-wants-float-not-str"),
])
def test_format_validation(tc):
comp = mcts_tuners.Mcts_tuner('mctstest')
config = default_config()
config['parameters'].append(
Parameter_config(
'bad',
scale = str,
split = 10,
format = "bad: %.2f"))
tc.assertRaisesRegexp(
ControlFileError, "'format': invalid format string",
comp.initialise_from_control_file, config)
def test_make_candidate(tc):
comp = mcts_tuners.Mcts_tuner('mctstest')
config = default_config()
comp.initialise_from_control_file(config)
cand = comp.make_candidate('c#1', (0.5, 23))
tc.assertEqual(cand.code, 'c#1')
tc.assertListEqual(cand.cmd_args, ['cand', '0.5', '23'])
with tc.assertRaises(CompetitionError) as ar:
comp.make_candidate('c#1', (-1, 23))
tc.assertTracebackStringEqual(str(ar.exception), dedent("""\
error from make_candidate()
ValueError: oops
traceback (most recent call last):
mcts_tuner_tests|simple_make_candidate
failing line:
raise ValueError("oops")
"""))
def test_get_player_checks(tc):
comp = mcts_tuners.Mcts_tuner('mctstest')
config = default_config()
comp.initialise_from_control_file(config)
checks = comp.get_player_checks()
tc.assertEqual(len(checks), 2)
tc.assertEqual(checks[0].player.code, "candidate")
tc.assertEqual(checks[0].player.cmd_args, ['cand', str(1/24), '5.0'])
tc.assertEqual(checks[1].player.code, "opp")
tc.assertEqual(checks[1].player.cmd_args, ['test'])
def test_linear_scale(tc):
lsf = mcts_tuners.Linear_scale_fn(20.0, 30.0)
tc.assertEqual(lsf(0.0), 20.0)
tc.assertEqual(lsf(1.0), 30.0)
tc.assertEqual(lsf(0.5), 25.0)
tc.assertEqual(lsf(0.49), 24.9)
lsi = mcts_tuners.Linear_scale_fn(20.0, 30.0, integer=True)
tc.assertEqual(lsi(0.0), 20)
tc.assertEqual(lsi(1.0), 30)
tc.assertEqual(lsi(0.49), 25)
tc.assertEqual(lsi(0.51), 25)
def test_log_scale(tc):
lsf = mcts_tuners.Log_scale_fn(2, 200000)
tc.assertAlmostEqual(lsf(0.0), 2.0)
tc.assertAlmostEqual(lsf(0.2), 20.0)
tc.assertAlmostEqual(lsf(0.4), 200.0)
tc.assertAlmostEqual(lsf(0.5), 2*sqrt(100000.00))
tc.assertAlmostEqual(lsf(0.6), 2000.0)
tc.assertAlmostEqual(lsf(0.8), 20000.0)
tc.assertAlmostEqual(lsf(1.0), 200000.0)
lsi = mcts_tuners.Log_scale_fn(1, 100, integer=True)
tc.assertAlmostEqual(lsi(0.1), 2)
lsn = mcts_tuners.Log_scale_fn(-2, -200)
tc.assertAlmostEqual(lsn(0.5), -20)
tc.assertRaises(ValueError, mcts_tuners.Log_scale_fn, 1, -2)
def test_explicit_scale(tc):
tc.assertRaises(ValueError, mcts_tuners.Explicit_scale_fn, [])
pvalues = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']
comp = mcts_tuners.Mcts_tuner('mctstest')
config = default_config()
config['parameters'] = [
Parameter_config(
'range',
scale = mcts_tuners.EXPLICIT(pvalues),
split = len(pvalues))]
comp.initialise_from_control_file(config)
candidate_sees = [
comp.scale_parameters(comp.tree.parameters_for_path([i]))[0]
for i, _ in enumerate(pvalues)
]
tc.assertEqual(candidate_sees, pvalues)
def test_integer_scale_example(tc):
comp = mcts_tuners.Mcts_tuner('mctstest')
config = default_config()
config['parameters'] = [
Parameter_config(
'range',
scale = mcts_tuners.LINEAR(-.5, 10.5, integer=True),
split = 11)]
comp.initialise_from_control_file(config)
candidate_sees = [
comp.scale_parameters(comp.tree.parameters_for_path([i]))[0]
for i in xrange(11)
]
tc.assertEqual(candidate_sees, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
def test_tree(tc):
tree1 = mcts_tuners.Tree(
splits=[3, 3],
max_depth=5,
exploration_coefficient=0.5,
initial_visits=10,
initial_wins=5,
parameter_formatter=str,
)
tree2 = mcts_tuners.Tree(
splits=[2, 4],
max_depth=5,
exploration_coefficient=0.5,
initial_visits=10,
initial_wins=5,
parameter_formatter=str,
)
tc.assertEqual(tree1.max_depth, 5)
tc.assertEqual(tree1._cube_coordinates, [
(0, 0), (0, 1), (0, 2),
(1, 0), (1, 1), (1, 2),
(2, 0), (2, 1), (2, 2),
])
tc.assertEqual(tree2._cube_coordinates, [
(0, 0), (0, 1), (0, 2), (0, 3),
(1, 0), (1, 1), (1, 2), (1, 3),
])
def scaleup(vector, scales):
"""Multiply each member of 'vector' by the corresponding scale.
Rounds to nearest integer if the difference is very small.
"""
result = []
for v, scale in zip(vector, scales):
f = v*scale
i = int(f+.5)
if abs(f - i) > 0.0000000001:
result.append(f)
else:
result.append(i)
return result
def pfp1(choice_path):
optimiser_params = tree1.parameters_for_path(choice_path)
scale = 3**len(choice_path) * 2
return scaleup(optimiser_params, [scale, scale])
# scale is 1/6
tc.assertEqual(pfp1([0]), [1, 1])
tc.assertEqual(pfp1([1]), [1, 3])
tc.assertEqual(pfp1([3]), [3, 1])
tc.assertEqual(pfp1([8]), [5, 5])
# scale is 1/18
tc.assertEqual(pfp1([0, 0]), [1, 1])
tc.assertEqual(pfp1([0, 1]), [1, 3])
tc.assertEqual(pfp1([3, 1]), [7, 3])
tc.assertEqual(pfp1([3, 4]), [9, 3])
def pfp2(choice_path):
optimiser_params = tree2.parameters_for_path(choice_path)
scale1 = 2**len(choice_path) * 2
scale2 = 4**len(choice_path) * 2
return scaleup(optimiser_params, [scale1, scale2])
# scale is 1/4, 1/8
tc.assertEqual(pfp2([0]), [1, 1])
tc.assertEqual(pfp2([1]), [1, 3])
tc.assertEqual(pfp2([2]), [1, 5])
tc.assertEqual(pfp2([3]), [1, 7])
tc.assertEqual(pfp2([4]), [3, 1])
tc.assertEqual(pfp2([5]), [3, 3])
# scale is 1/8, 1/32
tc.assertEqual(pfp2([0, 0]), [1, 1])
tc.assertEqual(pfp2([0, 1]), [1, 3])
tc.assertEqual(pfp2([0, 2]), [1, 5])
tc.assertEqual(pfp2([0, 3]), [1, 7])
tc.assertEqual(pfp2([0, 4]), [3, 1])
tc.assertEqual(pfp2([1, 0]), [1, 9])
tc.assertEqual(pfp2([7, 7]), [7, 31])
def test_play(tc):
comp = mcts_tuners.Mcts_tuner('mctstest')
comp.initialise_from_control_file(default_config())
comp.set_clean_status()
tree = comp.tree
tc.assertEqual(comp.outstanding_simulations, {})
tc.assertEqual(tree.root.visits, 10)
tc.assertEqual(tree.root.wins, 5)
tc.assertEqual(sum(node.visits-10 for node in tree.root.children), 0)
tc.assertEqual(sum(node.wins-5 for node in tree.root.children), 0)
job1 = comp.get_game()
tc.assertIsInstance(job1, Game_job)
tc.assertEqual(job1.game_id, '0')
tc.assertEqual(job1.player_b.code, 'opp')
tc.assertEqual(job1.player_w.code, '#0')
tc.assertEqual(job1.board_size, 13)
tc.assertEqual(job1.komi, 7.5)
tc.assertEqual(job1.move_limit, 1000)
tc.assertIs(job1.use_internal_scorer, False)
tc.assertEqual(job1.internal_scorer_handicap_compensation, 'full')
tc.assertEqual(job1.game_data, 0)
tc.assertEqual(job1.sgf_event, 'mctstest')
tc.assertRegexpMatches(job1.sgf_note, '^Candidate parameters: rsn@ ')
tc.assertItemsEqual(comp.outstanding_simulations.keys(), [0])
job2 = comp.get_game()
tc.assertIsInstance(job2, Game_job)
tc.assertEqual(job2.game_id, '1')
tc.assertEqual(job2.player_b.code, 'opp')
tc.assertEqual(job2.player_w.code, '#1')
tc.assertItemsEqual(comp.outstanding_simulations.keys(), [0, 1])
result1 = Game_result({'b' : 'opp', 'w' : '#1'}, 'w')
result1.sgf_result = "W+8.5"
response1 = Game_job_result()
response1.game_id = job1.game_id
response1.game_result = result1
response1.engine_names = {
'opp' : 'opp engine:v1.2.3',
'#0' : 'candidate engine',
}
response1.engine_descriptions = {
'opp' : 'opp engine:v1.2.3',
'#0' : 'candidate engine description',
}
response1.game_data = job1.game_data
comp.process_game_result(response1)
tc.assertItemsEqual(comp.outstanding_simulations.keys(), [1])
tc.assertEqual(tree.root.visits, 11)
tc.assertEqual(tree.root.wins, 6)
tc.assertEqual(sum(node.visits-10 for node in tree.root.children), 1)
tc.assertEqual(sum(node.wins-5 for node in tree.root.children), 1)
comp2 = mcts_tuners.Mcts_tuner('mctstest')
comp2.initialise_from_control_file(default_config())
status = pickle.loads(pickle.dumps(comp.get_status()))
comp2.set_status(status)
tc.assertEqual(comp2.tree.root.visits, 11)
tc.assertEqual(comp2.tree.root.wins, 6)
tc.assertEqual(sum(node.visits-10 for node in comp2.tree.root.children), 1)
tc.assertEqual(sum(node.wins-5 for node in comp2.tree.root.children), 1)
config3 = default_config()
# changed split
config3['parameters'][0] = Parameter_config(
'resign_at',
scale = float,
split = 11,
format = "rsn@ %.2f")
comp3 = mcts_tuners.Mcts_tuner('mctstest')
comp3.initialise_from_control_file(config3)
status = pickle.loads(pickle.dumps(comp.get_status()))
with tc.assertRaises(CompetitionError) as ar:
comp3.set_status(status)
tc.assertEqual(str(ar.exception),
"status file is inconsistent with control file")
config4 = default_config()
# changed upper bound
config4['parameters'][1] = Parameter_config(
'initial_wins',
scale = mcts_tuners.LINEAR(0, 200),
split = 10,
format = "iwins %d")
comp4 = mcts_tuners.Mcts_tuner('mctstest')
comp4.initialise_from_control_file(config4)
status = pickle.loads(pickle.dumps(comp.get_status()))
with tc.assertRaises(CompetitionError) as ar:
comp4.set_status(status)
tc.assertEqual(str(ar.exception),
"status file is inconsistent with control file")
def _disabled_test_tree_run(tc):
# Something like this test can be useful when changing the tree code,
# if you want to verify that you're not changing behaviour.
tree = mcts_tuners.Tree(
splits=[2, 3],
max_depth=5,
exploration_coefficient=0.5,
initial_visits=10,
initial_wins=5,
parameter_formatter=str,
)
tree.new_root()
random.seed(12345)
for i in range(1100):
simulation = mcts_tuners.Simulation(tree)
simulation.run()
simulation.update_stats(candidate_won=random.randrange(2))
tc.assertEqual(simulation.get_parameters(),
[0.0625, 0.42592592592592593])
tc.assertEqual(tree.node_count, 1597)
tc.assertEqual(simulation.choice_path, [1, 0, 2])
tc.assertEqual(tree.retrieve_best_parameters(),
[0.609375, 0.68930041152263366])

View File

@ -0,0 +1,570 @@
"""Tests for playoffs.py"""
from __future__ import with_statement
from textwrap import dedent
import cPickle as pickle
from gomill import competitions
from gomill import playoffs
from gomill.gtp_games import Game_result
from gomill.game_jobs import Game_job, Game_job_result
from gomill.competitions import (
Player_config, NoGameAvailable, CompetitionError, ControlFileError)
from gomill.playoffs import Matchup_config
from gomill_tests import competition_test_support
from gomill_tests import gomill_test_support
from gomill_tests import test_framework
from gomill_tests.competition_test_support import (
fake_response, check_screen_report)
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def check_short_report(tc, comp, expected_matchups, expected_players,
competition_name="testcomp"):
"""Check that a playoff's short report is as expected."""
expected = ("playoff: %s\n\n%s\n%s\n" %
(competition_name, expected_matchups, expected_players))
tc.assertMultiLineEqual(competition_test_support.get_short_report(comp),
expected)
expected_fake_players = dedent("""\
player t1: t1 engine
testdescription
player t2: t2 engine:v1.2.3
""")
class Playoff_fixture(test_framework.Fixture):
"""Fixture setting up a Playoff.
attributes:
comp -- Playoff
"""
def __init__(self, tc, config=None):
if config is None:
config = default_config()
self.tc = tc
self.comp = playoffs.Playoff('testcomp')
self.comp.initialise_from_control_file(config)
self.comp.set_clean_status()
def check_screen_report(self, expected):
"""Check that the screen report is as expected."""
check_screen_report(self.tc, self.comp, expected)
def check_short_report(self, *args, **kwargs):
"""Check that the short report is as expected."""
check_short_report(self.tc, self.comp, *args, **kwargs)
def default_config():
return {
'players' : {
't1' : Player_config("test1"),
't2' : Player_config("test2"),
},
'board_size' : 13,
'komi' : 7.5,
'matchups' : [
Matchup_config('t1', 't2', alternating=True),
],
}
def test_basic_config(tc):
comp = playoffs.Playoff('test')
config = default_config()
config['description'] = "default\nconfig"
config['matchups'] = [
Matchup_config(
't1', 't2', board_size=9, komi=0.5, alternating=True,
handicap=6, handicap_style='free',
move_limit=50,
scorer="internal", internal_scorer_handicap_compensation='no',
number_of_games=20),
Matchup_config('t2', 't1', id='m1'),
Matchup_config('t1', 't2'),
]
comp.initialise_from_control_file(config)
tc.assertEqual(comp.description, "default\nconfig")
comp.set_clean_status()
tr = comp.get_tournament_results()
m0 = tr.get_matchup('0')
m1 = tr.get_matchup('m1')
m2 = tr.get_matchup('2')
tc.assertListEqual(tr.get_matchup_ids(), ['0', 'm1', '2'])
tc.assertDictEqual(tr.get_matchups(), {'0' : m0, 'm1' : m1, '2' : m2})
tc.assertEqual(m0.player_1, 't1')
tc.assertEqual(m0.player_2, 't2')
tc.assertEqual(m0.board_size, 9)
tc.assertEqual(m0.komi, 0.5)
tc.assertIs(m0.alternating, True)
tc.assertEqual(m0.handicap, 6)
tc.assertEqual(m0.handicap_style, 'free')
tc.assertEqual(m0.move_limit, 50)
tc.assertEqual(m0.scorer, 'internal')
tc.assertEqual(m0.internal_scorer_handicap_compensation, 'no')
tc.assertEqual(m0.number_of_games, 20)
tc.assertEqual(m1.player_1, 't2')
tc.assertEqual(m1.player_2, 't1')
tc.assertEqual(m1.board_size, 13)
tc.assertEqual(m1.komi, 7.5)
tc.assertIs(m1.alternating, False)
tc.assertEqual(m1.handicap, None)
tc.assertEqual(m1.handicap_style, 'fixed')
tc.assertEqual(m1.move_limit, 1000)
tc.assertEqual(m1.scorer, 'players')
tc.assertEqual(m1.internal_scorer_handicap_compensation, 'full')
tc.assertEqual(m1.number_of_games, None)
def test_nonsense_matchup_config(tc):
comp = playoffs.Playoff('test')
config = default_config()
config['matchups'].append(99)
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertMultiLineEqual(str(ar.exception), dedent("""\
'matchups': item 1: not a Matchup"""))
def test_bad_matchup_config(tc):
comp = playoffs.Playoff('test')
config = default_config()
config['matchups'].append(Matchup_config())
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertMultiLineEqual(str(ar.exception), dedent("""\
matchup 1: not enough arguments"""))
def test_bad_matchup_config_bad_id(tc):
comp = playoffs.Playoff('test')
config = default_config()
config['matchups'].append(Matchup_config('t1', 't2', id=99))
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertMultiLineEqual(str(ar.exception), dedent("""\
matchup 1: 'id': not a string"""))
def test_bad_matchup_config_bad_setting(tc):
comp = playoffs.Playoff('test')
config = default_config()
config['matchups'].append(Matchup_config('t1', 't2', board_size="X"))
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertMultiLineEqual(str(ar.exception), dedent("""\
matchup 1: 'board_size': invalid integer"""))
def test_bad_matchup_config_unknown_player(tc):
comp = playoffs.Playoff('test')
config = default_config()
config['matchups'].append(Matchup_config('t1', 'nonex'))
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertMultiLineEqual(str(ar.exception), dedent("""\
matchup 1: unknown player nonex"""))
def test_bad_matchup_config_no_board_size(tc):
comp = playoffs.Playoff('test')
config = default_config()
del config['board_size']
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertMultiLineEqual(str(ar.exception), dedent("""\
matchup 0: 'board_size' not specified"""))
def test_bad_matchup_config_bad_handicap(tc):
comp = playoffs.Playoff('test')
config = default_config()
config['matchups'].append(Matchup_config('t1', 't2', handicap=10))
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertMultiLineEqual(str(ar.exception), dedent("""\
matchup 1: fixed handicap out of range for board size 13"""))
def test_matchup_config_board_size_in_matchup_only(tc):
comp = playoffs.Playoff('test')
config = default_config()
del config['board_size']
config['matchups'] = [Matchup_config('t1', 't2', alternating=True,
board_size=9)]
comp.initialise_from_control_file(config)
comp.set_clean_status()
tr = comp.get_tournament_results()
m0 = tr.get_matchup('0')
tc.assertEqual(m0.board_size, 9)
def test_matchup_name(tc):
comp = playoffs.Playoff('test')
config = default_config()
config['matchups'] = [Matchup_config('t1', 't2', name="asd")]
comp.initialise_from_control_file(config)
comp.set_clean_status()
tr = comp.get_tournament_results()
m0 = tr.get_matchup('0')
tc.assertEqual(m0.name, "asd")
def test_global_handicap_validation(tc):
comp = playoffs.Playoff('testcomp')
config = default_config()
config['board_size'] = 12
config['handicap'] = 6
with tc.assertRaises(ControlFileError) as ar:
comp.initialise_from_control_file(config)
tc.assertEqual(str(ar.exception),
"default fixed handicap out of range for board size 12")
def test_game_id_format(tc):
config = default_config()
config['matchups'][0] = Matchup_config('t1', 't2', number_of_games=1000)
fx = Playoff_fixture(tc, config)
tc.assertEqual(fx.comp.get_game().game_id, '0_000')
def test_get_player_checks(tc):
comp = playoffs.Playoff('testcomp')
config = default_config()
config['players']['t3'] = Player_config("test3")
config['matchups'].append(
Matchup_config('t1', 't3', number_of_games=0),
),
comp.initialise_from_control_file(config)
checks = comp.get_player_checks()
tc.assertEqual(len(checks), 2)
tc.assertEqual(checks[0].board_size, 13)
tc.assertEqual(checks[0].komi, 7.5)
tc.assertEqual(checks[0].player.code, "t1")
tc.assertEqual(checks[0].player.cmd_args, ['test1'])
tc.assertEqual(checks[1].player.code, "t2")
tc.assertEqual(checks[1].player.cmd_args, ['test2'])
def test_play(tc):
fx = Playoff_fixture(tc)
tc.assertIsNone(fx.comp.description)
job1 = fx.comp.get_game()
tc.assertIsInstance(job1, Game_job)
tc.assertEqual(job1.game_id, '0_0')
tc.assertEqual(job1.player_b.code, 't1')
tc.assertEqual(job1.player_w.code, 't2')
tc.assertEqual(job1.board_size, 13)
tc.assertEqual(job1.komi, 7.5)
tc.assertEqual(job1.move_limit, 1000)
tc.assertIs(job1.use_internal_scorer, False)
tc.assertEqual(job1.internal_scorer_handicap_compensation, 'full')
tc.assertEqual(job1.game_data, ('0', 0))
tc.assertIsNone(job1.sgf_filename)
tc.assertIsNone(job1.sgf_dirname)
tc.assertIsNone(job1.void_sgf_dirname)
tc.assertEqual(job1.sgf_event, 'testcomp')
tc.assertIsNone(job1.gtp_log_pathname)
job2 = fx.comp.get_game()
tc.assertIsInstance(job2, Game_job)
tc.assertEqual(job2.game_id, '0_1')
tc.assertEqual(job2.player_b.code, 't2')
tc.assertEqual(job2.player_w.code, 't1')
result1 = Game_result({'b' : 't1', 'w' : 't2'}, 'b')
result1.sgf_result = "B+8.5"
response1 = Game_job_result()
response1.game_id = job1.game_id
response1.game_result = result1
response1.engine_names = {
't1' : 't1 engine:v1.2.3',
't2' : 't2 engine',
}
response1.engine_descriptions = {
't1' : 't1 engine:v1.2.3',
't2' : 't2 engine\ntest \xc2\xa3description',
}
response1.game_data = job1.game_data
fx.comp.process_game_result(response1)
expected_report = dedent("""\
t1 v t2 (1 games)
board size: 13 komi: 7.5
wins
t1 1 100.00% (black)
t2 0 0.00% (white)
""")
expected_players = dedent("""\
player t1: t1 engine:v1.2.3
player t2: t2 engine
test \xc2\xa3description
""")
fx.check_screen_report(expected_report)
fx.check_short_report(expected_report, expected_players)
tc.assertListEqual(
fx.comp.get_tournament_results().get_matchup_results('0'), [result1])
def test_play_many(tc):
fx = Playoff_fixture(tc)
jobs = [fx.comp.get_game() for _ in range(8)]
for i in [0, 3]:
response = fake_response(jobs[i], 'b')
fx.comp.process_game_result(response)
jobs += [fx.comp.get_game() for _ in range(3)]
for i in [4, 2, 6, 7]:
response = fake_response(jobs[i], 'w')
fx.comp.process_game_result(response)
fx.check_screen_report(dedent("""\
t1 v t2 (6 games)
board size: 13 komi: 7.5
wins black white
t1 2 33.33% 1 25.00% 1 50.00%
t2 4 66.67% 1 50.00% 3 75.00%
2 33.33% 4 66.67%
"""))
tc.assertEqual(
len(fx.comp.get_tournament_results().get_matchup_results('0')), 6)
#tc.assertEqual(fx.comp.scheduler.allocators['0'].issued, 11)
#tc.assertEqual(fx.comp.scheduler.allocators['0'].fixed, 6)
comp2 = competition_test_support.check_round_trip(
tc, fx.comp, default_config())
#tc.assertEqual(comp2.scheduler.allocators['0'].issued, 6)
#tc.assertEqual(comp2.scheduler.allocators['0'].fixed, 6)
jobs2 = [comp2.get_game() for _ in range(4)]
tc.assertListEqual([job.game_id for job in jobs2],
['0_1', '0_5', '0_8', '0_9'])
tr = comp2.get_tournament_results()
tc.assertEqual(len(tr.get_matchup_results('0')), 6)
ms = tr.get_matchup_stats('0')
tc.assertEqual(ms.total, 6)
tc.assertEqual(ms.wins_1, 2)
tc.assertEqual(ms.wins_b, 2)
def test_jigo_reporting(tc):
fx = Playoff_fixture(tc)
def winner(i):
if i in (0, 3):
return 'b'
elif i in (2, 4):
return 'w'
else:
return None
jobs = [fx.comp.get_game() for _ in range(8)]
for i in range(6):
response = fake_response(jobs[i], winner(i))
fx.comp.process_game_result(response)
fx.check_screen_report(dedent("""\
t1 v t2 (6 games)
board size: 13 komi: 7.5
wins black white
t1 2 33.33% 2 66.67% 1 33.33%
t2 4 66.67% 2 66.67% 3 100.00%
3 50.00% 3 50.00%
"""))
response = fake_response(jobs[6], None)
fx.comp.process_game_result(response)
fx.check_screen_report(dedent("""\
t1 v t2 (7 games)
board size: 13 komi: 7.5
wins black white
t1 2.5 35.71% 2.5 62.50% 1.5 50.00%
t2 4.5 64.29% 2.5 83.33% 3.5 87.50%
3.5 50.00% 3.5 50.00%
"""))
def test_unknown_result_reporting(tc):
fx = Playoff_fixture(tc)
def winner(i):
if i in (0, 3):
return 'b'
elif i in (2, 4):
return 'w'
else:
return 'unknown'
jobs = [fx.comp.get_game() for _ in range(8)]
for i in range(6):
response = fake_response(jobs[i], winner(i))
fx.comp.process_game_result(response)
fx.check_screen_report(dedent("""\
t1 v t2 (6 games)
unknown results: 2 33.33%
board size: 13 komi: 7.5
wins black white
t1 1 16.67% 1 33.33% 0 0.00%
t2 3 50.00% 1 33.33% 2 66.67%
2 33.33% 2 33.33%
"""))
def test_self_play(tc):
config = default_config()
config['matchups'] = [
Matchup_config('t1', 't1', alternating=True),
Matchup_config('t1', 't1', alternating=False),
]
fx = Playoff_fixture(tc, config)
jobs = [fx.comp.get_game() for _ in range(20)]
for i, job in enumerate(jobs):
response = fake_response(job, 'b' if i < 9 else 'w')
fx.comp.process_game_result(response)
fx.check_screen_report(dedent("""\
t1 v t1#2 (10 games)
board size: 13 komi: 7.5
wins black white
t1 6 60.00% 3 60.00% 3 60.00%
t1#2 4 40.00% 2 40.00% 2 40.00%
5 50.00% 5 50.00%
t1 v t1#2 (10 games)
board size: 13 komi: 7.5
wins
t1 4 40.00% (black)
t1#2 6 60.00% (white)
"""))
competition_test_support.check_round_trip(tc, fx.comp, config)
def test_bad_state(tc):
fx = Playoff_fixture(tc)
bad_status = fx.comp.get_status()
del bad_status['scheduler']
comp2 = playoffs.Playoff('testcomp')
comp2.initialise_from_control_file(default_config())
tc.assertRaises(KeyError, comp2.set_status, bad_status)
def test_matchup_change(tc):
fx = Playoff_fixture(tc)
jobs = [fx.comp.get_game() for _ in range(8)]
for i in [0, 2, 3, 4, 6, 7]:
response = fake_response(jobs[i], ('b' if i in (0, 3) else 'w'))
fx.comp.process_game_result(response)
fx.check_screen_report(dedent("""\
t1 v t2 (6 games)
board size: 13 komi: 7.5
wins black white
t1 2 33.33% 1 25.00% 1 50.00%
t2 4 66.67% 1 50.00% 3 75.00%
2 33.33% 4 66.67%
"""))
config2 = default_config()
config2['players']['t3'] = Player_config("test3")
config2['matchups'][0] = Matchup_config('t1', 't3', alternating=True)
comp2 = playoffs.Playoff('testcomp')
comp2.initialise_from_control_file(config2)
status = pickle.loads(pickle.dumps(fx.comp.get_status()))
with tc.assertRaises(CompetitionError) as ar:
comp2.set_status(status)
tc.assertEqual(
str(ar.exception),
"existing results for matchup 0 are inconsistent with control file:\n"
"result players are t1,t2;\n"
"control file players are t1,t3")
def test_matchup_reappearance(tc):
# Test that if a matchup is removed and added again, we remember the game
# number. Test that we report the 'ghost' matchup in the short report (but
# not the screen report).
config1 = default_config()
config1['matchups'].append(Matchup_config('t2', 't1'))
config2 = default_config()
config3 = default_config()
config3['matchups'].append(Matchup_config('t2', 't1'))
comp1 = playoffs.Playoff('testcomp')
comp1.initialise_from_control_file(config1)
comp1.set_clean_status()
jobs1 = [comp1.get_game() for _ in range(8)]
for job in jobs1:
comp1.process_game_result(fake_response(job, 'b'))
tc.assertListEqual(
[job.game_id for job in jobs1],
['0_0', '1_0', '0_1', '1_1', '0_2', '1_2', '0_3', '1_3'])
expected_matchups_1 = dedent("""\
t1 v t2 (4 games)
board size: 13 komi: 7.5
wins black white
t1 2 50.00% 2 100.00% 0 0.00%
t2 2 50.00% 2 100.00% 0 0.00%
4 100.00% 0 0.00%
t2 v t1 (4 games)
board size: 13 komi: 7.5
wins
t2 4 100.00% (black)
t1 0 0.00% (white)
""")
check_screen_report(tc, comp1, expected_matchups_1)
check_short_report(tc, comp1, expected_matchups_1, expected_fake_players)
comp2 = playoffs.Playoff('testcomp')
comp2.initialise_from_control_file(config2)
comp2.set_status(pickle.loads(pickle.dumps(comp1.get_status())))
jobs2 = [comp2.get_game() for _ in range(4)]
tc.assertListEqual(
[job.game_id for job in jobs2],
['0_4', '0_5', '0_6', '0_7'])
for job in jobs2:
comp2.process_game_result(fake_response(job, 'b'))
expected_matchups_2 = dedent("""\
t1 v t2 (8 games)
board size: 13 komi: 7.5
wins black white
t1 4 50.00% 4 100.00% 0 0.00%
t2 4 50.00% 4 100.00% 0 0.00%
8 100.00% 0 0.00%
""")
check_screen_report(tc, comp2, expected_matchups_2)
expected_matchups_2b = dedent("""\
t2 v t1 (4 games)
?? (missing from control file)
wins
t2 4 100.00% (black)
t1 0 0.00% (white)
""")
check_short_report(
tc, comp2,
expected_matchups_2 + "\n" + expected_matchups_2b,
expected_fake_players)
comp3 = playoffs.Playoff('testcomp')
comp3.initialise_from_control_file(config3)
comp3.set_status(pickle.loads(pickle.dumps(comp2.get_status())))
jobs3 = [comp3.get_game() for _ in range(8)]
tc.assertListEqual(
[job.game_id for job in jobs3],
['1_4', '1_5', '1_6', '1_7', '0_8', '1_8', '0_9', '1_9'])
expected_matchups_3 = dedent("""\
t1 v t2 (8 games)
board size: 13 komi: 7.5
wins black white
t1 4 50.00% 4 100.00% 0 0.00%
t2 4 50.00% 4 100.00% 0 0.00%
8 100.00% 0 0.00%
t2 v t1 (4 games)
board size: 13 komi: 7.5
wins
t2 4 100.00% (black)
t1 0 0.00% (white)
""")
check_screen_report(tc, comp3, expected_matchups_3)
check_short_report(tc, comp3, expected_matchups_3, expected_fake_players)

View File

@ -0,0 +1,93 @@
"""Test support code for testing Ringmasters."""
from collections import defaultdict
from cStringIO import StringIO
from gomill import ringmasters
from gomill import ringmaster_presenters
class Test_presenter(ringmaster_presenters.Presenter):
"""Presenter which stores the messages."""
def __init__(self):
ringmaster_presenters.Presenter.__init__(self)
self.channels = defaultdict(list)
shows_warnings_only = False
def clear(self, channel):
self.channels[channel] = []
def say(self, channel, s):
self.channels[channel].append(s)
def refresh(self):
pass
def recent_messages(self, channel):
"""Retrieve messages sent since the channel was last cleared.
Returns a list of strings.
"""
return self.channels[channel][:]
class Testing_ringmaster(ringmasters.Ringmaster):
"""Variant of ringmaster suitable for use in tests.
This doesn't read from or write to the filesystem.
(If you're testing run(), make sure record_games is False, and
discard_stderr is True for each player.)
(Currently, write_status is made to do nothing, so it's not usefully
testable.)
Instantiate with the control file contents as an 8-bit string.
It will act as if the control file had been loaded from
/nonexistent/ctl/test.ctl.
You'll want to run set_display_mode('test') with this.
"""
def __init__(self, control_file_contents):
self._control_file_contents = control_file_contents
self._test_status = None
self._written_status = None
ringmasters.Ringmaster.__init__(self, '/nonexistent/ctl/test.ctl')
_presenter_classes = {
'test' : Test_presenter,
}
def _open_files(self):
self.logfile = StringIO()
self.historyfile = StringIO()
def _close_files(self):
# Don't want to close the StringIOs
pass
def _read_control_file(self):
return self._control_file_contents
def set_test_status(self, test_status):
"""Specify the value that will be loaded from the state file.
test_status -- fake state file contents
test_status should be a pair (status_format_version, status dict)
"""
self._test_status = test_status
def _load_status(self):
return self._test_status
def status_file_exists(self):
return (self._test_status is not None)
def _write_status(self, value):
self._written_status = value

View File

@ -0,0 +1,520 @@
"""Tests for ringmaster.py."""
import os
import re
from textwrap import dedent
from gomill_tests import test_framework
from gomill_tests import gomill_test_support
from gomill_tests import ringmaster_test_support
from gomill_tests import gtp_engine_fixtures
from gomill_tests.playoff_tests import fake_response
from gomill.ringmasters import RingmasterError
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
class Ringmaster_fixture(test_framework.Fixture):
"""Fixture setting up a Ringmaster with mock suprocesses.
Instantiate with testcase, the text to be used as the contents of the
control file, and a list of strings to be added (as a line each) to the end
of the control file.
attributes:
ringmaster -- Testing_ringmaster
msf -- Mock_subprocess_fixture
See Mock_subprocess_gtp_channel for an explanation of how player command
lines are interpreted.
"""
def __init__(self, tc, control_file_contents, extra_lines=[]):
self.ringmaster = ringmaster_test_support.Testing_ringmaster(
control_file_contents + "\n".join(extra_lines))
self.ringmaster.set_display_mode('test')
self.msf = gtp_engine_fixtures.Mock_subprocess_fixture(tc)
def messages(self, channel):
"""Return messages sent to the specified channel."""
return self.ringmaster.presenter.recent_messages(channel)
def initialise_clean(self):
"""Initialise the ringmaster (with clean status)."""
self.ringmaster.set_clean_status()
self.ringmaster._open_files()
self.ringmaster._initialise_presenter()
self.ringmaster._initialise_terminal_reader()
def initialise_with_state(self, ringmaster_status):
"""Initialise the ringmaster with specified status."""
self.ringmaster.set_test_status(ringmaster_status)
self.ringmaster.load_status()
self.ringmaster._open_files()
self.ringmaster._initialise_presenter()
self.ringmaster._initialise_terminal_reader()
def get_job(self):
"""Initialise the ringmaster, and call get_job() once."""
self.initialise_clean()
return self.ringmaster.get_job()
def get_log(self):
"""Retrieve the log file contents with timestamps scrubbed out."""
s = self.ringmaster.logfile.getvalue()
s = re.sub(r"(?<= at )([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2})",
"***", s)
return s
def get_history(self):
"""Retrieve the history file contents."""
return self.ringmaster.historyfile.getvalue()
def get_written_state(self):
"""Return the unpickled value written to the state file."""
return self.ringmaster._written_status
playoff_ctl = """
competition_type = 'playoff'
description = 'gomill_tests playoff.'
players = {
'p1' : Player('test'),
'p2' : Player('test'),
}
move_limit = 400
record_games = False
board_size = 9
komi = 7.5
scorer = 'internal'
number_of_games = 400
matchups = [
Matchup('p1', 'p2'),
]
"""
allplayall_ctl = """
competition_type = 'allplayall'
description = 'gomill_tests allplayall_ctl.'
players = {
'p1' : Player('test'),
'p2' : Player('test'),
}
move_limit = 400
record_games = False
board_size = 9
komi = 7.5
scorer = 'internal'
rounds = 8
competitors = ['p1', 'p2']
"""
mcts_ctl = """
competition_type = 'mc_tuner'
description = 'gomill_tests mc_tuner.'
players = {
'p1' : Player('test'),
}
record_games = False
board_size = 9
komi = 7.5
candidate_colour = 'w'
opponent = 'p1'
exploration_coefficient = 0.45
initial_visits = 10
initial_wins = 5
parameters = [
Parameter('foo',
scale = LOG(0.01, 5.0),
split = 8,
format = 'I: %4.2f'),
]
def make_candidate(foo):
return Player('candidate')
"""
def test_get_job(tc):
fx = Ringmaster_fixture(tc, playoff_ctl, [
"players['p2'] = Player('test sing song')",
])
job = fx.get_job()
tc.assertEqual(job.game_id, "0_000")
tc.assertEqual(job.game_data, ("0", 0))
tc.assertEqual(job.board_size, 9)
tc.assertEqual(job.komi, 7.5)
tc.assertEqual(job.move_limit, 400)
tc.assertEqual(job.handicap, None)
tc.assertIs(job.handicap_is_free, False)
tc.assertIs(job.use_internal_scorer, True)
tc.assertEqual(job.sgf_game_name, 'test 0_000')
tc.assertEqual(job.sgf_event, 'test')
tc.assertIsNone(job.gtp_log_pathname)
tc.assertIsNone(job.sgf_filename)
tc.assertIsNone(job.sgf_dirname)
tc.assertIsNone(job.void_sgf_dirname)
tc.assertEqual(job.player_b.code, 'p1')
tc.assertEqual(job.player_w.code, 'p2')
tc.assertEqual(job.player_b.cmd_args, ['test'])
tc.assertEqual(job.player_w.cmd_args, ['test', 'sing', 'song'])
tc.assertDictEqual(job.player_b.gtp_aliases, {})
tc.assertListEqual(job.player_b.startup_gtp_commands, [])
tc.assertEqual(job.stderr_pathname, "/nonexistent/ctl/test.log")
tc.assertIsNone(job.player_b.cwd)
tc.assertIsNone(job.player_b.environ)
tc.assertEqual(fx.ringmaster.games_in_progress, {'0_000': job})
tc.assertEqual(fx.get_log(),
"starting game 0_000: p1 (b) vs p2 (w)\n")
tc.assertEqual(fx.get_history(), "")
def test_settings(tc):
fx = Ringmaster_fixture(tc, playoff_ctl, [
"handicap = 9",
"handicap_style = 'free'",
"record_games = True",
"scorer = 'players'"
])
fx.ringmaster.enable_gtp_logging()
job = fx.get_job()
tc.assertEqual(job.game_id, "0_000")
tc.assertEqual(job.handicap, 9)
tc.assertIs(job.handicap_is_free, True)
tc.assertIs(job.use_internal_scorer, False)
tc.assertEqual(job.stderr_pathname, "/nonexistent/ctl/test.log")
tc.assertEqual(job.gtp_log_pathname,
'/nonexistent/ctl/test.gtplogs/0_000.log')
tc.assertEqual(job.sgf_filename, '0_000.sgf')
tc.assertEqual(job.sgf_dirname, '/nonexistent/ctl/test.games')
tc.assertEqual(job.void_sgf_dirname, '/nonexistent/ctl/test.void')
tc.assertEqual(fx.ringmaster.get_sgf_filename("0_000"), "0_000.sgf")
tc.assertEqual(fx.ringmaster.get_sgf_pathname("0_000"),
"/nonexistent/ctl/test.games/0_000.sgf")
def test_stderr_settings(tc):
fx = Ringmaster_fixture(tc, playoff_ctl, [
"players['p2'] = Player('test', discard_stderr=True)",
])
job = fx.get_job()
tc.assertEqual(job.stderr_pathname, "/nonexistent/ctl/test.log")
tc.assertIs(job.player_b.discard_stderr, False)
tc.assertIs(job.player_w.discard_stderr, True)
def test_stderr_settings_nolog(tc):
fx = Ringmaster_fixture(tc, playoff_ctl, [
"players['p2'] = Player('test', discard_stderr=True)",
"stderr_to_log = False",
])
job = fx.get_job()
tc.assertIs(job.stderr_pathname, None)
tc.assertIs(job.player_b.discard_stderr, False)
tc.assertIs(job.player_w.discard_stderr, True)
def test_get_tournament_results(tc):
fx = Ringmaster_fixture(tc, playoff_ctl)
tc.assertRaisesRegexp(RingmasterError, "^status is not loaded$",
fx.ringmaster.get_tournament_results)
fx.initialise_clean()
tr = fx.ringmaster.get_tournament_results()
tc.assertEqual(tr.get_matchup_ids(), ['0'])
fx2 = Ringmaster_fixture(tc, mcts_ctl)
fx2.initialise_clean()
tc.assertRaisesRegexp(RingmasterError, "^competition is not a tournament$",
fx2.ringmaster.get_tournament_results)
def test_process_response(tc):
fx = Ringmaster_fixture(tc, playoff_ctl)
job = fx.get_job()
tc.assertEqual(fx.ringmaster.games_in_progress, {'0_000': job})
tc.assertEqual(
fx.ringmaster.get_tournament_results().get_matchup_results('0'), [])
response = fake_response(job, 'w')
response.warnings = ['warningtest']
response.log_entries = ['logtest']
fx.ringmaster.process_response(response)
tc.assertEqual(fx.ringmaster.games_in_progress, {})
tc.assertListEqual(
fx.messages('warnings'),
["warningtest"])
tc.assertListEqual(
fx.messages('results'),
["game 0_000: p2 beat p1 W+1.5"])
tc.assertEqual(
fx.ringmaster.get_tournament_results().get_matchup_results('0'),
[response.game_result])
tc.assertEqual(fx.get_log(),
"starting game 0_000: p1 (b) vs p2 (w)\n"
"response from game 0_000\n"
"warningtest\n"
"logtest\n")
tc.assertEqual(fx.get_history(), "")
def test_check_players(tc):
fx = Ringmaster_fixture(tc, playoff_ctl)
tc.assertTrue(fx.ringmaster.check_players(discard_stderr=True))
def test_run(tc):
fx = Ringmaster_fixture(tc, playoff_ctl, [
"players['p1'] = Player('test', discard_stderr=True)",
"players['p2'] = Player('test', discard_stderr=True)",
])
fx.initialise_clean()
fx.ringmaster.run(max_games=3)
tc.assertListEqual(
fx.messages('warnings'),
[])
tc.assertListEqual(
fx.messages('screen_report'),
["p1 v p2 (3/400 games)\n"
"board size: 9 komi: 7.5\n"
" wins\n"
"p1 3 100.00% (black)\n"
"p2 0 0.00% (white)"])
tc.assertMultiLineEqual(
fx.get_log(),
"run started at *** with max_games 3\n"
"starting game 0_000: p1 (b) vs p2 (w)\n"
"response from game 0_000\n"
"starting game 0_001: p1 (b) vs p2 (w)\n"
"response from game 0_001\n"
"starting game 0_002: p1 (b) vs p2 (w)\n"
"response from game 0_002\n"
"halting competition: max-games reached for this run\n"
"run finished at ***\n"
)
tc.assertMultiLineEqual(
fx.get_history(),
" 0_000 p1 beat p2 B+10.5\n"
" 0_001 p1 beat p2 B+10.5\n"
" 0_002 p1 beat p2 B+10.5\n")
def test_run_allplayall(tc):
fx = Ringmaster_fixture(tc, allplayall_ctl, [
"players['p1'] = Player('test', discard_stderr=True)",
"players['p2'] = Player('test', discard_stderr=True)",
])
fx.initialise_clean()
fx.ringmaster.run(max_games=3)
tc.assertListEqual(
fx.messages('warnings'),
[])
tc.assertListEqual(
fx.messages('screen_report'),
[dedent("""\
3/8 games played
A B
A p1 2-1
B p2 1-2""")])
tc.assertMultiLineEqual(
fx.get_log(),
"run started at *** with max_games 3\n"
"starting game AvB_0: p1 (b) vs p2 (w)\n"
"response from game AvB_0\n"
"starting game AvB_1: p2 (b) vs p1 (w)\n"
"response from game AvB_1\n"
"starting game AvB_2: p1 (b) vs p2 (w)\n"
"response from game AvB_2\n"
"halting competition: max-games reached for this run\n"
"run finished at ***\n"
)
tc.assertMultiLineEqual(
fx.get_history(),
" AvB_0 p1 beat p2 B+10.5\n"
" AvB_1 p2 beat p1 B+10.5\n"
" AvB_2 p1 beat p2 B+10.5\n")
def test_check_players_fail(tc):
fx = Ringmaster_fixture(tc, playoff_ctl, [
"players['p2'] = Player('test fail=startup')"
])
tc.assertFalse(fx.ringmaster.check_players(discard_stderr=True))
def test_run_fail(tc):
fx = Ringmaster_fixture(tc, playoff_ctl, [
"players['p1'] = Player('test', discard_stderr=True)",
"players['p2'] = Player('test fail=startup', discard_stderr=True)",
])
fx.initialise_clean()
fx.ringmaster.run()
tc.assertListEqual(
fx.messages('warnings'),
["game 0_000 -- aborting game due to error:\n"
"error starting subprocess for player p2:\n"
"exec forced to fail",
"halting run due to void games"])
tc.assertListEqual(
fx.messages('screen_report'),
["1 void games; see log file."])
tc.assertMultiLineEqual(
fx.get_log(),
"run started at *** with max_games None\n"
"starting game 0_000: p1 (b) vs p2 (w)\n"
"game 0_000 -- aborting game due to error:\n"
"error starting subprocess for player p2:\n"
"exec forced to fail\n"
"halting competition: too many void games\n"
"run finished at ***\n")
tc.assertMultiLineEqual(fx.get_history(), "")
def test_run_with_late_errors(tc):
fx = Ringmaster_fixture(tc, playoff_ctl, [
"players['p1'] = Player('test', discard_stderr=True)",
"players['p2'] = Player('test init=fail_close', discard_stderr=True)",
])
def fail_close(channel):
channel.fail_close = True
fx.msf.register_init_callback('fail_close', fail_close)
fx.initialise_clean()
fx.ringmaster.run(max_games=2)
tc.assertListEqual(fx.messages('warnings'), [])
tc.assertMultiLineEqual(
fx.get_log(),
"run started at *** with max_games 2\n"
"starting game 0_000: p1 (b) vs p2 (w)\n"
"response from game 0_000\n"
"error closing player p2:\n"
"forced failure for close\n"
"starting game 0_001: p1 (b) vs p2 (w)\n"
"response from game 0_001\n"
"error closing player p2:\n"
"forced failure for close\n"
"halting competition: max-games reached for this run\n"
"run finished at ***\n")
tc.assertMultiLineEqual(
fx.get_history(),
" 0_000 p1 beat p2 B+10.5\n"
" 0_001 p1 beat p2 B+10.5\n")
def test_status_roundtrip(tc):
fx1 = Ringmaster_fixture(tc, playoff_ctl, [
"players['p1'] = Player('test', discard_stderr=True)",
"players['p2'] = Player('test', discard_stderr=True)",
])
fx1.initialise_clean()
fx1.ringmaster.run(max_games=2)
tc.assertListEqual(
fx1.messages('warnings'),
[])
state = fx1.get_written_state()
fx2 = Ringmaster_fixture(tc, playoff_ctl, [
"players['p1'] = Player('test', discard_stderr=True)",
"players['p2'] = Player('test', discard_stderr=True)",
])
fx2.initialise_with_state(state)
fx2.ringmaster.run(max_games=1)
tc.assertListEqual(
fx2.messages('warnings'),
[])
tc.assertListEqual(
fx2.messages('screen_report'),
["p1 v p2 (3/400 games)\n"
"board size: 9 komi: 7.5\n"
" wins\n"
"p1 3 100.00% (black)\n"
"p2 0 0.00% (white)"])
def test_status(tc):
# Construct suitable competition status
fx1 = Ringmaster_fixture(tc, playoff_ctl, [
"players['p1'] = Player('test', discard_stderr=True)",
"players['p2'] = Player('test', discard_stderr=True)",
])
sfv = fx1.ringmaster.status_format_version
fx1.initialise_clean()
fx1.ringmaster.run(max_games=2)
competition_status = fx1.ringmaster.competition.get_status()
tc.assertListEqual(
fx1.messages('warnings'),
[])
status = {
'void_game_count' : 0,
'comp_vn' : fx1.ringmaster.competition.status_format_version,
'comp' : competition_status,
}
fx = Ringmaster_fixture(tc, playoff_ctl, [
"players['p1'] = Player('test', discard_stderr=True)",
"players['p2'] = Player('test', discard_stderr=True)",
])
fx.initialise_with_state((sfv, status.copy()))
fx.ringmaster.run(max_games=1)
tc.assertListEqual(
fx.messages('warnings'),
[])
tc.assertListEqual(
fx.messages('screen_report'),
["p1 v p2 (3/400 games)\n"
"board size: 9 komi: 7.5\n"
" wins\n"
"p1 3 100.00% (black)\n"
"p2 0 0.00% (white)"])
fx.ringmaster.set_test_status((-1, status.copy()))
tc.assertRaisesRegexp(
RingmasterError,
"incompatible status file",
fx.ringmaster.load_status)
bad_status = status.copy()
del bad_status['void_game_count']
fx.ringmaster.set_test_status((sfv, bad_status))
tc.assertRaisesRegexp(
RingmasterError,
"incompatible status file: missing 'void_game_count'",
fx.ringmaster.load_status)
bad_competition_status = competition_status.copy()
del bad_competition_status['results']
bad_status_2 = status.copy()
bad_status_2['comp'] = bad_competition_status
fx.ringmaster.set_test_status((sfv, bad_status_2))
tc.assertRaisesRegexp(
RingmasterError,
"error loading competition state: missing 'results'",
fx.ringmaster.load_status)
bad_competition_status_2 = competition_status.copy()
bad_competition_status_2['scheduler'] = None
bad_status_3 = status.copy()
bad_status_3['comp'] = bad_competition_status_2
fx.ringmaster.set_test_status((sfv, bad_status_3))
tc.assertRaisesRegexp(
RingmasterError,
"error loading competition state:\n"
"AttributeError: 'NoneType' object has no attribute 'set_groups'",
fx.ringmaster.load_status)
bad_status_4 = status.copy()
bad_status_4['comp_vn'] = -1
fx.ringmaster.set_test_status((sfv, bad_status_4))
tc.assertRaisesRegexp(
RingmasterError,
"incompatible status file",
fx.ringmaster.load_status)

View File

@ -0,0 +1,111 @@
"""Construct and run the gomill testsuite."""
import sys
from optparse import OptionParser
test_modules = [
'utils_tests',
'common_tests',
'board_tests',
'sgf_grammar_tests',
'sgf_properties_tests',
'sgf_tests',
'sgf_moves_tests',
'gtp_engine_tests',
'gtp_state_tests',
'gtp_controller_tests',
'gtp_proxy_tests',
'gtp_game_tests',
'game_job_tests',
'setting_tests',
'competition_scheduler_tests',
'competition_tests',
'playoff_tests',
'allplayall_tests',
'mcts_tuner_tests',
'cem_tuner_tests',
'ringmaster_tests',
]
def get_test_module(name):
"""Import the specified gomill_tests module and return it."""
dotted_name = "gomill_tests." + name
__import__(dotted_name)
return sys.modules[dotted_name]
def get_test_modules():
"""Import all _tests modules in the specified order.
Returns a list of module objects.
"""
return [get_test_module(name) for name in test_modules]
def run_testsuite(module_names, failfast, buffer):
"""Run the gomill testsuite.
module_names -- names of modules from gomill_tests, or None for all
failfast -- bool (stop at first failing test)
buffer -- bool (show stderr/stdout only for failing tests)
Output is to stderr
"""
try:
# This gives 'catchbreak' behaviour
unittest2.signals.installHandler()
except Exception:
pass
if module_names is None:
modules = get_test_modules()
else:
modules = [get_test_module(name) for name in module_names]
suite = unittest2.TestSuite()
for mdl in modules:
mdl.make_tests(suite)
runner = unittest2.TextTestRunner(failfast=failfast, buffer=buffer)
runner.run(suite)
def run(argv):
parser = OptionParser(usage="%prog [options] [module] ...")
parser.add_option("-f", "--failfast", action="store_true",
help="stop after first test")
parser.add_option("-p", "--nobuffer", action="store_true",
help="show stderr/stdout for successful tests")
(options, args) = parser.parse_args(argv)
if args:
module_names = args
for module_name in module_names:
if module_name not in test_modules:
parser.error("unknown module: %s" % module_name)
else:
module_names = None
run_testsuite(module_names, options.failfast, not options.nobuffer)
def import_unittest():
"""Import unittest2 into global scope.
Raises NameError if it isn't available.
Call this before using the functions in this module other than main().
"""
global unittest2
try:
from gomill_tests.test_framework import unittest2
except ImportError, e:
if hasattr(e, 'unittest2_missing'):
raise NameError("unittest2")
raise
def main():
try:
import_unittest()
except NameError:
sys.exit("gomill_tests: requires either Python 2.7 or "
"the 'unittest2' package")
run(sys.argv[1:])
if __name__ == "__main__":
main()

View File

@ -0,0 +1,29 @@
"""Tests for settings.py"""
from gomill.settings import *
from gomill_tests import gomill_test_support
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def test_interpret_shlex_sequence(tc):
iss = interpret_shlex_sequence
tc.assertEqual(iss("test"), ["test"])
tc.assertEqual(iss("test "), ["test"])
tc.assertEqual(iss("~test"), ["~test"])
tc.assertEqual(iss("test foo bar"), ["test", "foo", "bar"])
tc.assertEqual(iss("test 'foo bar'"), ["test", "foo bar"])
tc.assertEqual(iss(u"test foo bar"), ["test", "foo", "bar"])
tc.assertEqual(iss(["test"]), ["test"])
tc.assertEqual(iss(["test", "foo", "bar"]), ["test", "foo", "bar"])
tc.assertEqual(iss(["test", "foo bar"]), ["test", "foo bar"])
tc.assertEqual(iss(("test", "foo", "bar")), ["test", "foo", "bar"])
tc.assertRaisesRegexp(ValueError, "^empty$", iss, "")
tc.assertRaisesRegexp(ValueError, "^not a string or a sequence$", iss, None)
tc.assertRaisesRegexp(ValueError, "^element not a string$",
iss, ["test", None])
tc.assertRaisesRegexp(ValueError, "^element contains NUL$",
iss, ["test", "fo\x00"])

View File

@ -0,0 +1,362 @@
"""Tests for sgf_grammar.py."""
from __future__ import with_statement
from gomill_tests import gomill_test_support
from gomill import sgf_grammar
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def test_is_valid_property_identifier(tc):
ivpi = sgf_grammar.is_valid_property_identifier
tc.assertIs(ivpi("B"), True)
tc.assertIs(ivpi("PB"), True)
tc.assertIs(ivpi("ABCDEFGH"), True)
tc.assertIs(ivpi("ABCDEFGHI"), False)
tc.assertIs(ivpi(""), False)
tc.assertIs(ivpi("b"), False)
tc.assertIs(ivpi("Player"), False)
tc.assertIs(ivpi("P2"), False)
tc.assertIs(ivpi(" PB"), False)
tc.assertIs(ivpi("PB "), False)
tc.assertIs(ivpi("P B"), False)
tc.assertIs(ivpi("PB\x00"), False)
def test_is_valid_property_value(tc):
ivpv = sgf_grammar.is_valid_property_value
tc.assertIs(ivpv(""), True)
tc.assertIs(ivpv("hello world"), True)
tc.assertIs(ivpv("hello\nworld"), True)
tc.assertIs(ivpv("hello \x00 world"), True)
tc.assertIs(ivpv("hello \xa3 world"), True)
tc.assertIs(ivpv("hello \xc2\xa3 world"), True)
tc.assertIs(ivpv("hello \\-) world"), True)
tc.assertIs(ivpv("hello (;[) world"), True)
tc.assertIs(ivpv("[hello world]"), False)
tc.assertIs(ivpv("hello ] world"), False)
tc.assertIs(ivpv("hello \\] world"), True)
tc.assertIs(ivpv("hello world \\"), False)
tc.assertIs(ivpv("hello world \\\\"), True)
tc.assertIs(ivpv("x" * 70000), True)
def test_tokeniser(tc):
tokenise = sgf_grammar.tokenise
tc.assertEqual(tokenise("(;B[ah][]C[a\xa3b])")[0],
[('D', '('),
('D', ';'),
('I', 'B'),
('V', 'ah'),
('V', ''),
('I', 'C'),
('V', 'a\xa3b'),
('D', ')')])
def check_complete(s, *args):
tokens, tail_index = tokenise(s, *args)
tc.assertEqual(tail_index, len(s))
return len(tokens)
def check_incomplete(s, *args):
tokens, tail_index = tokenise(s, *args)
return len(tokens), tail_index
# check surrounding junk
tc.assertEqual(check_complete(""), 0)
tc.assertEqual(check_complete("junk (;B[ah])"), 5)
tc.assertEqual(check_incomplete("junk"), (0, 0))
tc.assertEqual(check_incomplete("junk (B[ah])"), (0, 0))
tc.assertEqual(check_incomplete("(;B[ah]) junk"), (5, 8))
# check paren-balance count
tc.assertEqual(check_incomplete("(; ))(([ag]B C[ah])"), (3, 4))
tc.assertEqual(check_incomplete("(;( )) (;)"), (5, 6))
tc.assertEqual(check_incomplete("(;(()())) (;)"), (9, 9))
# check start_position
tc.assertEqual(check_complete("(; ))(;B[ah])", 4), 5)
tc.assertEqual(check_complete("(; ))junk (;B[ah])", 4), 5)
tc.assertEqual(check_complete("(;XX[abc][def]KO[];B[bc])"), 11)
tc.assertEqual(check_complete("( ;XX[abc][def]KO[];B[bc])"), 11)
tc.assertEqual(check_complete("(; XX[abc][def]KO[];B[bc])"), 11)
tc.assertEqual(check_complete("(;XX [abc][def]KO[];B[bc])"), 11)
tc.assertEqual(check_complete("(;XX[abc] [def]KO[];B[bc])"), 11)
tc.assertEqual(check_complete("(;XX[abc][def] KO[];B[bc])"), 11)
tc.assertEqual(check_complete("(;XX[abc][def]KO [];B[bc])"), 11)
tc.assertEqual(check_complete("(;XX[abc][def]KO[] ;B[bc])"), 11)
tc.assertEqual(check_complete("(;XX[abc][def]KO[]; B[bc])"), 11)
tc.assertEqual(check_complete("(;XX[abc][def]KO[];B [bc])"), 11)
tc.assertEqual(check_complete("(;XX[abc][def]KO[];B[bc] )"), 11)
tc.assertEqual(check_complete("( ;\nB\t[ah]\f[ef]\v)"), 6)
tc.assertEqual(check_complete("(;[Ran\xc2\xa3dom :\nstu@ff][ef]"), 4)
tc.assertEqual(check_complete("(;[ah)])"), 4)
tc.assertEqual(check_incomplete("(;B[ag"), (3, 3))
tc.assertEqual(check_incomplete("(;B[ag)"), (3, 3))
tc.assertEqual(check_incomplete("(;AddBlack[ag])"), (3, 3))
tc.assertEqual(check_incomplete("(;+B[ag])"), (2, 2))
tc.assertEqual(check_incomplete("(;B+[ag])"), (3, 3))
tc.assertEqual(check_incomplete("(;B[ag]+)"), (4, 7))
tc.assertEqual(check_complete(r"(;[ab \] cd][ef]"), 4)
tc.assertEqual(check_complete(r"(;[ab \] cd\\][ef]"), 4)
tc.assertEqual(check_complete(r"(;[ab \] cd\\\\][ef]"), 4)
tc.assertEqual(check_complete(r"(;[ab \] \\\] cd][ef]"), 4)
tc.assertEqual(check_incomplete(r"(;B[ag\])"), (3, 3))
tc.assertEqual(check_incomplete(r"(;B[ag\\\])"), (3, 3))
def test_parser_structure(tc):
parse_sgf_game = sgf_grammar.parse_sgf_game
def shape(s):
coarse_game = parse_sgf_game(s)
return len(coarse_game.sequence), len(coarse_game.children)
tc.assertEqual(shape("(;C[abc]KO[];B[bc])"), (2, 0))
tc.assertEqual(shape("initial junk (;C[abc]KO[];B[bc])"), (2, 0))
tc.assertEqual(shape("(;C[abc]KO[];B[bc]) final junk"), (2, 0))
tc.assertEqual(shape("(;C[abc]KO[];B[bc]) (;B[ag])"), (2, 0))
tc.assertRaisesRegexp(ValueError, "no SGF data found",
parse_sgf_game, r"")
tc.assertRaisesRegexp(ValueError, "no SGF data found",
parse_sgf_game, r"junk")
tc.assertRaisesRegexp(ValueError, "no SGF data found",
parse_sgf_game, r"()")
tc.assertRaisesRegexp(ValueError, "no SGF data found",
parse_sgf_game, r"(B[ag])")
tc.assertRaisesRegexp(ValueError, "no SGF data found",
parse_sgf_game, r"B[ag]")
tc.assertRaisesRegexp(ValueError, "no SGF data found",
parse_sgf_game, r"[ag]")
tc.assertEqual(shape("(;C[abc]AB[ab][bc];B[bc])"), (2, 0))
tc.assertEqual(shape("(;C[abc] AB[ab]\n[bc]\t;B[bc])"), (2, 0))
tc.assertEqual(shape("(;C[abc]KO[];;B[bc])"), (3, 0))
tc.assertEqual(shape("(;)"), (1, 0))
tc.assertRaisesRegexp(ValueError, "property with no values",
parse_sgf_game, r"(;B)")
tc.assertRaisesRegexp(ValueError, "unexpected value",
parse_sgf_game, r"(;[ag])")
tc.assertRaisesRegexp(ValueError, "unexpected value",
parse_sgf_game, r"(;[ag][ah])")
tc.assertRaisesRegexp(ValueError, "unexpected value",
parse_sgf_game, r"(;[B][ag])")
tc.assertRaisesRegexp(ValueError, "unexpected end of SGF data",
parse_sgf_game, r"(;B[ag]")
tc.assertRaisesRegexp(ValueError, "unexpected end of SGF data",
parse_sgf_game, r"(;B[ag][)]")
tc.assertRaisesRegexp(ValueError, "property with no values",
parse_sgf_game, r"(;B;W[ah])")
tc.assertRaisesRegexp(ValueError, "unexpected value",
parse_sgf_game, r"(;B[ag](;[ah]))")
tc.assertRaisesRegexp(ValueError, "property with no values",
parse_sgf_game, r"(;B W[ag])")
def test_parser_tree_structure(tc):
parse_sgf_game = sgf_grammar.parse_sgf_game
def shape(s):
coarse_game = parse_sgf_game(s)
return len(coarse_game.sequence), len(coarse_game.children)
tc.assertEqual(shape("(;C[abc]AB[ab](;B[bc]))"), (1, 1))
tc.assertEqual(shape("(;C[abc]AB[ab](;B[bc])))"), (1, 1))
tc.assertEqual(shape("(;C[abc]AB[ab](;B[bc])(;B[bd]))"), (1, 2))
def shapetree(s):
def _shapetree(coarse_game):
return (
len(coarse_game.sequence),
[_shapetree(pg) for pg in coarse_game.children])
return _shapetree(parse_sgf_game(s))
tc.assertEqual(shapetree("(;C[abc]AB[ab](;B[bc])))"),
(1, [(1, [])])
)
tc.assertEqual(shapetree("(;C[abc]AB[ab](;B[bc]))))"),
(1, [(1, [])])
)
tc.assertEqual(shapetree("(;C[abc]AB[ab](;B[bc])(;B[bd])))"),
(1, [(1, []), (1, [])])
)
tc.assertEqual(shapetree("""
(;C[abc]AB[ab];C[];C[]
(;B[bc])
(;B[bd];W[ca] (;B[da])(;B[db];W[ea]) )
)"""),
(3, [
(1, []),
(2, [(1, []), (2, [])])
])
)
tc.assertRaisesRegexp(ValueError, "unexpected end of SGF data",
parse_sgf_game, "(;B[ag];W[ah](;B[ai])")
tc.assertRaisesRegexp(ValueError, "empty sequence",
parse_sgf_game, "(;B[ag];())")
tc.assertRaisesRegexp(ValueError, "empty sequence",
parse_sgf_game, "(;B[ag]())")
tc.assertRaisesRegexp(ValueError, "empty sequence",
parse_sgf_game, "(;B[ag]((;W[ah])(;W[ai]))")
tc.assertRaisesRegexp(ValueError, "unexpected node",
parse_sgf_game, "(;B[ag];W[ah](;B[ai]);W[bd])")
tc.assertRaisesRegexp(ValueError, "property value outside a node",
parse_sgf_game, "(;B[ag];(W[ah];B[ai]))")
tc.assertRaisesRegexp(ValueError, "property value outside a node",
parse_sgf_game, "(;B[ag](;W[ah];)B[ai])")
tc.assertRaisesRegexp(ValueError, "property value outside a node",
parse_sgf_game, "(;B[ag](;W[ah])(B[ai]))")
def test_parser_properties(tc):
parse_sgf_game = sgf_grammar.parse_sgf_game
def props(s):
coarse_game = parse_sgf_game(s)
return coarse_game.sequence
tc.assertEqual(props("(;C[abc]KO[]AB[ai][bh][ee];B[ bc])"),
[{'C': ['abc'], 'KO': [''], 'AB': ['ai', 'bh', 'ee']},
{'B': [' bc']}])
tc.assertEqual(props(r"(;C[ab \] \) cd\\])"),
[{'C': [r"ab \] \) cd\\"]}])
tc.assertEqual(props("(;XX[1]YY[2]XX[3]YY[4])"),
[{'XX': ['1', '3'], 'YY' : ['2', '4']}])
def test_parse_sgf_collection(tc):
parse_sgf_collection = sgf_grammar.parse_sgf_collection
tc.assertRaisesRegexp(ValueError, "no SGF data found",
parse_sgf_collection, r"")
tc.assertRaisesRegexp(ValueError, "no SGF data found",
parse_sgf_collection, r"()")
games = parse_sgf_collection("(;C[abc]AB[ab];X[];X[](;B[bc]))")
tc.assertEqual(len(games), 1)
tc.assertEqual(len(games[0].sequence), 3)
games = parse_sgf_collection("(;X[1];X[2];X[3](;B[bc])) (;Y[1];Y[2])")
tc.assertEqual(len(games), 2)
tc.assertEqual(len(games[0].sequence), 3)
tc.assertEqual(len(games[1].sequence), 2)
games = parse_sgf_collection(
"dummy (;X[1];X[2];X[3](;B[bc])) junk (;Y[1];Y[2]) Nonsense")
tc.assertEqual(len(games), 2)
tc.assertEqual(len(games[0].sequence), 3)
tc.assertEqual(len(games[1].sequence), 2)
games = parse_sgf_collection(
"(( (;X[1];X[2];X[3](;B[bc])) ();) (;Y[1];Y[2]) )(Nonsense")
tc.assertEqual(len(games), 2)
tc.assertEqual(len(games[0].sequence), 3)
tc.assertEqual(len(games[1].sequence), 2)
with tc.assertRaises(ValueError) as ar:
parse_sgf_collection(
"(( (;X[1];X[2];X[3](;B[bc])) ();) (;Y[1];Y[2]")
tc.assertEqual(str(ar.exception),
"error parsing game 1: unexpected end of SGF data")
def test_parse_compose(tc):
pc = sgf_grammar.parse_compose
tc.assertEqual(pc("word"), ("word", None))
tc.assertEqual(pc("word:"), ("word", ""))
tc.assertEqual(pc("word:?"), ("word", "?"))
tc.assertEqual(pc("word:123"), ("word", "123"))
tc.assertEqual(pc("word:123:456"), ("word", "123:456"))
tc.assertEqual(pc(":123"), ("", "123"))
tc.assertEqual(pc(r"word\:more"), (r"word\:more", None))
tc.assertEqual(pc(r"word\:more:?"), (r"word\:more", "?"))
tc.assertEqual(pc(r"word\\:more:?"), ("word\\\\", "more:?"))
tc.assertEqual(pc(r"word\\\:more:?"), (r"word\\\:more", "?"))
tc.assertEqual(pc("word\\\nmore:123"), ("word\\\nmore", "123"))
def test_text_value(tc):
text_value = sgf_grammar.text_value
tc.assertEqual(text_value("abc "), "abc ")
tc.assertEqual(text_value("ab c"), "ab c")
tc.assertEqual(text_value("ab\tc"), "ab c")
tc.assertEqual(text_value("ab \tc"), "ab c")
tc.assertEqual(text_value("ab\nc"), "ab\nc")
tc.assertEqual(text_value("ab\\\nc"), "abc")
tc.assertEqual(text_value("ab\\\\\nc"), "ab\\\nc")
tc.assertEqual(text_value("ab\xa0c"), "ab\xa0c")
tc.assertEqual(text_value("ab\rc"), "ab\nc")
tc.assertEqual(text_value("ab\r\nc"), "ab\nc")
tc.assertEqual(text_value("ab\n\rc"), "ab\nc")
tc.assertEqual(text_value("ab\r\n\r\nc"), "ab\n\nc")
tc.assertEqual(text_value("ab\r\n\r\n\rc"), "ab\n\n\nc")
tc.assertEqual(text_value("ab\\\r\nc"), "abc")
tc.assertEqual(text_value("ab\\\n\nc"), "ab\nc")
tc.assertEqual(text_value("ab\\\tc"), "ab c")
# These can't actually appear as SGF PropValues; anything sane will do
tc.assertEqual(text_value("abc\\"), "abc")
tc.assertEqual(text_value("abc]"), "abc]")
def test_simpletext_value(tc):
simpletext_value = sgf_grammar.simpletext_value
tc.assertEqual(simpletext_value("abc "), "abc ")
tc.assertEqual(simpletext_value("ab c"), "ab c")
tc.assertEqual(simpletext_value("ab\tc"), "ab c")
tc.assertEqual(simpletext_value("ab \tc"), "ab c")
tc.assertEqual(simpletext_value("ab\nc"), "ab c")
tc.assertEqual(simpletext_value("ab\\\nc"), "abc")
tc.assertEqual(simpletext_value("ab\\\\\nc"), "ab\\ c")
tc.assertEqual(simpletext_value("ab\xa0c"), "ab\xa0c")
tc.assertEqual(simpletext_value("ab\rc"), "ab c")
tc.assertEqual(simpletext_value("ab\r\nc"), "ab c")
tc.assertEqual(simpletext_value("ab\n\rc"), "ab c")
tc.assertEqual(simpletext_value("ab\r\n\r\nc"), "ab c")
tc.assertEqual(simpletext_value("ab\r\n\r\n\rc"), "ab c")
tc.assertEqual(simpletext_value("ab\\\r\nc"), "abc")
tc.assertEqual(simpletext_value("ab\\\n\nc"), "ab c")
tc.assertEqual(simpletext_value("ab\\\tc"), "ab c")
# These can't actually appear as SGF PropValues; anything sane will do
tc.assertEqual(simpletext_value("abc\\"), "abc")
tc.assertEqual(simpletext_value("abc]"), "abc]")
def test_escape_text(tc):
tc.assertEqual(sgf_grammar.escape_text("abc"), "abc")
tc.assertEqual(sgf_grammar.escape_text(r"a\bc"), r"a\\bc")
tc.assertEqual(sgf_grammar.escape_text(r"ab[c]"), r"ab[c\]")
tc.assertEqual(sgf_grammar.escape_text(r"a\]bc"), r"a\\\]bc")
def test_text_roundtrip(tc):
def roundtrip(s):
return sgf_grammar.text_value(sgf_grammar.escape_text(s))
tc.assertEqual(roundtrip(r"abc"), r"abc")
tc.assertEqual(roundtrip(r"a\bc"), r"a\bc")
tc.assertEqual(roundtrip("abc\\"), "abc\\")
tc.assertEqual(roundtrip("ab]c"), "ab]c")
tc.assertEqual(roundtrip("abc]"), "abc]")
tc.assertEqual(roundtrip(r"abc\]"), r"abc\]")
tc.assertEqual(roundtrip("ab\nc"), "ab\nc")
tc.assertEqual(roundtrip("ab\n c"), "ab\n c")
tc.assertEqual(roundtrip("ab\tc"), "ab c")
tc.assertEqual(roundtrip("ab\r\nc\n"), "ab\nc\n")
def test_serialise_game_tree(tc):
serialised = ("(;AB[aa][ab][ac]C[comment \xa3];W[ab];C[];C[]"
"(;B[bc])(;B[bd];W[ca](;B[da])(;B[db];\n"
"W[ea])))\n")
coarse_game = sgf_grammar.parse_sgf_game(serialised)
tc.assertEqual(sgf_grammar.serialise_game_tree(coarse_game), serialised)
tc.assertEqual(sgf_grammar.serialise_game_tree(coarse_game, wrap=None),
serialised.replace("\n", "")+"\n")

View File

@ -0,0 +1,141 @@
from gomill_tests import gomill_test_support
from gomill import ascii_boards
from gomill import boards
from gomill import sgf
from gomill import sgf_moves
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
SAMPLE_SGF = """\
(;AP[testsuite:0]CA[utf-8]DT[2009-06-06]FF[4]GM[1]KM[7.5]PB[Black engine]
PL[B]PW[White engine]RE[W+R]SZ[9]AB[ai][bh][ee]AW[fc][gc];B[dg];W[ef]C[comment
on two lines];B[];W[tt]C[Final comment])
"""
DIAGRAM1 = """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . o o . .
6 . . . . . . . . .
5 . . . . # . . . .
4 . . . . . . . . .
3 . . . . . . . . .
2 . # . . . . . . .
1 # . . . . . . . .
A B C D E F G H J\
"""
DIAGRAM2 = """\
9 . . . . . . . . .
8 . . . . . . . . .
7 . . . . . . . . .
6 . . . . . . . . .
5 . . . . . . . . .
4 . . . . # . . . .
3 . . . . . . . . .
2 . . # . . . . . .
1 . . . . . . . . .
A B C D E F G H J\
"""
def test_get_setup_and_moves(tc):
g1 = sgf.Sgf_game.from_string(SAMPLE_SGF)
board1, plays1 = sgf_moves.get_setup_and_moves(g1)
tc.assertDiagramEqual(ascii_boards.render_board(board1), DIAGRAM1)
tc.assertEqual(plays1,
[('b', (2, 3)), ('w', (3, 4)), ('b', None), ('w', None)])
g2 = sgf.Sgf_game(size=9)
root = g2.get_root()
root.set("AB", [(1, 2), (3, 4)]);
node = g2.extend_main_sequence()
node.set("B", (5, 6))
node = g2.extend_main_sequence()
node.set("W", (5, 7))
board2, plays2 = sgf_moves.get_setup_and_moves(g2)
tc.assertDiagramEqual(ascii_boards.render_board(board2), DIAGRAM2)
tc.assertEqual(plays2,
[('b', (5, 6)), ('w', (5, 7))])
g3 = sgf.Sgf_game.from_string("(;AB[ab][ba]AW[aa])")
tc.assertRaisesRegexp(ValueError, "setup position not legal",
sgf_moves.get_setup_and_moves, g3)
g4 = sgf.Sgf_game.from_string("(;SZ[9];B[ab];AW[bc])")
tc.assertRaisesRegexp(ValueError, "setup properties after the root node",
sgf_moves.get_setup_and_moves, g4)
g5 = sgf.Sgf_game.from_string("(;SZ[26];B[ab];W[bc])")
board5, plays5 = sgf_moves.get_setup_and_moves(g5)
tc.assertEqual(plays5,
[('b', (24, 0)), ('w', (23, 1))])
def test_get_setup_and_moves_move_in_root(tc):
# A move in the root node is allowed (though deprecated) if there are no
# setup stones.
g1 = sgf.Sgf_game(size=9)
root = g1.get_root()
root.set("B", (1, 2));
node = g1.extend_main_sequence()
node.set("W", (3, 4))
board1, plays1 = sgf_moves.get_setup_and_moves(g1)
tc.assertTrue(board1.is_empty())
tc.assertEqual(plays1,
[('b', (1, 2)), ('w', (3, 4))])
g2 = sgf.Sgf_game(size=9)
root = g2.get_root()
root.set("B", (1, 2));
root.set("AW", [(3, 3)]);
node = g2.extend_main_sequence()
node.set("W", (3, 4))
tc.assertRaisesRegexp(ValueError, "mixed setup and moves in root node",
sgf_moves.get_setup_and_moves, g2)
def test_get_setup_and_moves_board_provided(tc):
b = boards.Board(9)
g1 = sgf.Sgf_game.from_string(SAMPLE_SGF)
board1, plays1 = sgf_moves.get_setup_and_moves(g1, b)
tc.assertIs(board1, b)
tc.assertDiagramEqual(ascii_boards.render_board(board1), DIAGRAM1)
tc.assertEqual(plays1,
[('b', (2, 3)), ('w', (3, 4)), ('b', None), ('w', None)])
tc.assertRaisesRegexp(ValueError, "board not empty",
sgf_moves.get_setup_and_moves, g1, b)
b2 = boards.Board(19)
tc.assertRaisesRegexp(ValueError, "wrong board size, must be 9$",
sgf_moves.get_setup_and_moves, g1, b2)
def test_set_initial_position(tc):
board = ascii_boards.interpret_diagram(DIAGRAM1, 9)
sgf_game = sgf.Sgf_game(9)
sgf_moves.set_initial_position(sgf_game, board)
root = sgf_game.get_root()
tc.assertEqual(root.get("AB"), set([(0, 0), (1, 1), (4, 4)]))
tc.assertEqual(root.get("AW"), set([(6, 5), (6, 6)]))
tc.assertRaises(KeyError, root.get, 'AE')
def test_indicate_first_player(tc):
g1 = sgf.Sgf_game.from_string("(;FF[4]GM[1]SZ[9];B[aa];W[ab])")
sgf_moves.indicate_first_player(g1)
tc.assertEqual(g1.serialise(),
"(;FF[4]GM[1]SZ[9];B[aa];W[ab])\n")
g2 = sgf.Sgf_game.from_string("(;FF[4]GM[1]SZ[9];W[aa];B[ab])")
sgf_moves.indicate_first_player(g2)
tc.assertEqual(g2.serialise(),
"(;FF[4]GM[1]PL[W]SZ[9];W[aa];B[ab])\n")
g3 = sgf.Sgf_game.from_string("(;AW[bc]FF[4]GM[1]SZ[9];B[aa];W[ab])")
sgf_moves.indicate_first_player(g3)
tc.assertEqual(g3.serialise(),
"(;FF[4]AW[bc]GM[1]PL[B]SZ[9];B[aa];W[ab])\n")
g4 = sgf.Sgf_game.from_string("(;FF[4]GM[1]SZ[9];C[no game])")
sgf_moves.indicate_first_player(g4)
tc.assertEqual(g4.serialise(),
"(;FF[4]GM[1]SZ[9];C[no game])\n")

View File

@ -0,0 +1,386 @@
"""Tests for sgf_properties.py."""
from textwrap import dedent
from gomill_tests import gomill_test_support
from gomill import sgf_properties
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def test_interpret_simpletext(tc):
def interpret(s, encoding):
context = sgf_properties._Context(19, encoding)
return sgf_properties.interpret_simpletext(s, context)
tc.assertEqual(interpret("a\nb\\\\c", "utf-8"), "a b\\c")
u = u"test \N{POUND SIGN}"
tc.assertEqual(interpret(u.encode("utf-8"), "UTF-8"),
u.encode("utf-8"))
tc.assertEqual(interpret(u.encode("iso-8859-1"), "ISO-8859-1"),
u.encode("utf-8"))
tc.assertRaises(UnicodeDecodeError, interpret,
u.encode("iso-8859-1"), "UTF-8")
tc.assertRaises(UnicodeDecodeError, interpret, u.encode("utf-8"), "ASCII")
def test_serialise_simpletext(tc):
def serialise(s, encoding):
context = sgf_properties._Context(19, encoding)
return sgf_properties.serialise_simpletext(s, context)
tc.assertEqual(serialise("ab\\c", "utf-8"), "ab\\\\c")
u = u"test \N{POUND SIGN}"
tc.assertEqual(serialise(u.encode("utf-8"), "UTF-8"),
u.encode("utf-8"))
tc.assertEqual(serialise(u.encode("utf-8"), "ISO-8859-1"),
u.encode("iso-8859-1"))
tc.assertRaises(UnicodeEncodeError, serialise,
u"\N{EN DASH}".encode("utf-8"), "ISO-8859-1")
def test_interpret_text(tc):
def interpret(s, encoding):
context = sgf_properties._Context(19, encoding)
return sgf_properties.interpret_text(s, context)
tc.assertEqual(interpret("a\nb\\\\c", "utf-8"), "a\nb\\c")
u = u"test \N{POUND SIGN}"
tc.assertEqual(interpret(u.encode("utf-8"), "UTF-8"),
u.encode("utf-8"))
tc.assertEqual(interpret(u.encode("iso-8859-1"), "ISO-8859-1"),
u.encode("utf-8"))
tc.assertRaises(UnicodeDecodeError, interpret,
u.encode("iso-8859-1"), "UTF-8")
tc.assertRaises(UnicodeDecodeError, interpret, u.encode("utf-8"), "ASCII")
def test_serialise_text(tc):
def serialise(s, encoding):
context = sgf_properties._Context(19, encoding)
return sgf_properties.serialise_text(s, context)
tc.assertEqual(serialise("ab\\c", "utf-8"), "ab\\\\c")
u = u"test \N{POUND SIGN}"
tc.assertEqual(serialise(u.encode("utf-8"), "UTF-8"),
u.encode("utf-8"))
tc.assertEqual(serialise(u.encode("utf-8"), "ISO-8859-1"),
u.encode("iso-8859-1"))
tc.assertRaises(UnicodeEncodeError, serialise,
u"\N{EN DASH}".encode("utf-8"), "ISO-8859-1")
def test_interpret_number(tc):
interpret_number = sgf_properties.interpret_number
tc.assertEqual(interpret_number("1"), 1)
tc.assertIs(type(interpret_number("1")), int)
tc.assertEqual(interpret_number("0"), 0)
tc.assertEqual(interpret_number("-1"), -1)
tc.assertEqual(interpret_number("+1"), 1)
tc.assertRaises(ValueError, interpret_number, "1.5")
tc.assertRaises(ValueError, interpret_number, "0xaf")
tc.assertRaises(TypeError, interpret_number, 1)
def test_interpret_real(tc):
interpret_real = sgf_properties.interpret_real
tc.assertEqual(interpret_real("1"), 1.0)
tc.assertIs(type(interpret_real("1")), float)
tc.assertEqual(interpret_real("0"), 0.0)
tc.assertEqual(interpret_real("1.0"), 1.0)
tc.assertEqual(interpret_real("1.5"), 1.5)
tc.assertEqual(interpret_real("-1.5"), -1.5)
tc.assertEqual(interpret_real("+0.5"), 0.5)
tc.assertRaises(ValueError, interpret_real, "+")
tc.assertRaises(ValueError, interpret_real, "0xaf")
tc.assertRaises(ValueError, interpret_real, "inf")
tc.assertRaises(ValueError, interpret_real, "-inf")
tc.assertRaises(ValueError, interpret_real, "NaN")
tc.assertRaises(ValueError, interpret_real, "1e400")
#tc.assertRaises(TypeError, interpret_real, 1.0)
def test_serialise_real(tc):
serialise_real = sgf_properties.serialise_real
tc.assertEqual(serialise_real(1), "1")
tc.assertEqual(serialise_real(-1), "-1")
tc.assertEqual(serialise_real(1.0), "1")
tc.assertEqual(serialise_real(-1.0), "-1")
tc.assertEqual(serialise_real(1.5), "1.5")
tc.assertEqual(serialise_real(-1.5), "-1.5")
tc.assertEqual(serialise_real(0.001), "0.001")
tc.assertEqual(serialise_real(0.0001), "0.0001")
tc.assertEqual(serialise_real(0.00001), "0")
tc.assertEqual(serialise_real(1e15), "1000000000000000")
tc.assertEqual(serialise_real(1e16), "10000000000000000")
tc.assertEqual(serialise_real(1e17), "100000000000000000")
tc.assertEqual(serialise_real(1e18), "1000000000000000000")
tc.assertEqual(serialise_real(-1e18), "-1000000000000000000")
# 1e400 is inf
tc.assertRaises(ValueError, serialise_real, 1e400)
# Python 2.5 returns 0
#tc.assertRaises(ValueError, serialise_real, float("NaN"))
def test_interpret_move(tc):
def interpret_move(s, size):
context = sgf_properties._Context(size, "UTF-8")
return sgf_properties.interpret_move(s, context)
tc.assertEqual(interpret_move("aa", 19), (18, 0))
tc.assertEqual(interpret_move("ai", 19), (10, 0))
tc.assertEqual(interpret_move("ba", 9), (8, 1))
tc.assertEqual(interpret_move("tt", 21), (1, 19))
tc.assertIs(interpret_move("tt", 19), None)
tc.assertIs(interpret_move("", 19), None)
tc.assertIs(interpret_move("", 21), None)
tc.assertRaises(ValueError, interpret_move, "Aa", 19)
tc.assertRaises(ValueError, interpret_move, "aA", 19)
tc.assertRaises(ValueError, interpret_move, "aaa", 19)
tc.assertRaises(ValueError, interpret_move, "a", 19)
tc.assertRaises(ValueError, interpret_move, "au", 19)
tc.assertRaises(ValueError, interpret_move, "ua", 19)
tc.assertRaises(ValueError, interpret_move, "a`", 19)
tc.assertRaises(ValueError, interpret_move, "`a", 19)
tc.assertRaises(ValueError, interpret_move, "11", 19)
tc.assertRaises(ValueError, interpret_move, " aa", 19)
tc.assertRaises(ValueError, interpret_move, "aa\x00", 19)
tc.assertRaises(TypeError, interpret_move, None, 19)
#tc.assertRaises(TypeError, interpret_move, ('a', 'a'), 19)
def test_serialise_move(tc):
def serialise_move(s, size):
context = sgf_properties._Context(size, "UTF-8")
return sgf_properties.serialise_move(s, context)
tc.assertEqual(serialise_move((18, 0), 19), "aa")
tc.assertEqual(serialise_move((10, 0), 19), "ai")
tc.assertEqual(serialise_move((8, 1), 19), "bk")
tc.assertEqual(serialise_move((8, 1), 9), "ba")
tc.assertEqual(serialise_move((1, 19), 21), "tt")
tc.assertEqual(serialise_move(None, 19), "tt")
tc.assertEqual(serialise_move(None, 20), "")
tc.assertRaises(ValueError, serialise_move, (3, 3), 0)
tc.assertRaises(ValueError, serialise_move, (3, 3), 27)
tc.assertRaises(ValueError, serialise_move, (9, 0), 9)
tc.assertRaises(ValueError, serialise_move, (-1, 0), 9)
tc.assertRaises(ValueError, serialise_move, (0, 9), 9)
tc.assertRaises(ValueError, serialise_move, (0, -1), 9)
tc.assertRaises(TypeError, serialise_move, (1, 1.5), 9)
def test_interpret_point(tc):
def interpret_point(s, size):
context = sgf_properties._Context(size, "UTF-8")
return sgf_properties.interpret_point(s, context)
tc.assertEqual(interpret_point("aa", 19), (18, 0))
tc.assertEqual(interpret_point("ai", 19), (10, 0))
tc.assertEqual(interpret_point("ba", 9), (8, 1))
tc.assertEqual(interpret_point("tt", 21), (1, 19))
tc.assertRaises(ValueError, interpret_point, "tt", 19)
tc.assertRaises(ValueError, interpret_point, "", 19)
tc.assertRaises(ValueError, interpret_point, "", 21)
tc.assertRaises(ValueError, interpret_point, "Aa", 19)
tc.assertRaises(ValueError, interpret_point, "aA", 19)
tc.assertRaises(ValueError, interpret_point, "aaa", 19)
tc.assertRaises(ValueError, interpret_point, "a", 19)
tc.assertRaises(ValueError, interpret_point, "au", 19)
tc.assertRaises(ValueError, interpret_point, "ua", 19)
tc.assertRaises(ValueError, interpret_point, "a`", 19)
tc.assertRaises(ValueError, interpret_point, "`a", 19)
tc.assertRaises(ValueError, interpret_point, "11", 19)
tc.assertRaises(ValueError, interpret_point, " aa", 19)
tc.assertRaises(ValueError, interpret_point, "aa\x00", 19)
tc.assertRaises(TypeError, interpret_point, None, 19)
#tc.assertRaises(TypeError, interpret_point, ('a', 'a'), 19)
def test_serialise_point(tc):
def serialise_point(s, size):
context = sgf_properties._Context(size, "UTF-8")
return sgf_properties.serialise_point(s, context)
tc.assertEqual(serialise_point((18, 0), 19), "aa")
tc.assertEqual(serialise_point((10, 0), 19), "ai")
tc.assertEqual(serialise_point((8, 1), 19), "bk")
tc.assertEqual(serialise_point((8, 1), 9), "ba")
tc.assertEqual(serialise_point((1, 19), 21), "tt")
tc.assertRaises(ValueError, serialise_point, None, 19)
tc.assertRaises(ValueError, serialise_point, None, 20)
tc.assertRaises(ValueError, serialise_point, (3, 3), 0)
tc.assertRaises(ValueError, serialise_point, (3, 3), 27)
tc.assertRaises(ValueError, serialise_point, (9, 0), 9)
tc.assertRaises(ValueError, serialise_point, (-1, 0), 9)
tc.assertRaises(ValueError, serialise_point, (0, 9), 9)
tc.assertRaises(ValueError, serialise_point, (0, -1), 9)
tc.assertRaises(TypeError, serialise_point, (1, 1.5), 9)
def test_interpret_point_list(tc):
def ipl(l, size):
context = sgf_properties._Context(size, "UTF-8")
return sgf_properties.interpret_point_list(l, context)
tc.assertEqual(ipl([], 19),
set())
tc.assertEqual(ipl(["aa"], 19),
set([(18, 0)]))
tc.assertEqual(ipl(["aa", "ai"], 19),
set([(18, 0), (10, 0)]))
tc.assertEqual(ipl(["ab:bc"], 19),
set([(16, 0), (16, 1), (17, 0), (17, 1)]))
tc.assertEqual(ipl(["ab:bc", "aa"], 19),
set([(18, 0), (16, 0), (16, 1), (17, 0), (17, 1)]))
# overlap is forbidden by the spec, but we accept it
tc.assertEqual(ipl(["aa", "aa"], 19),
set([(18, 0)]))
tc.assertEqual(ipl(["ab:bc", "bb:bc"], 19),
set([(16, 0), (16, 1), (17, 0), (17, 1)]))
# 1x1 rectangles are forbidden by the spec, but we accept them
tc.assertEqual(ipl(["aa", "bb:bb"], 19),
set([(18, 0), (17, 1)]))
# 'backwards' rectangles are forbidden by the spec, and we reject them
tc.assertRaises(ValueError, ipl, ["ab:aa"], 19)
tc.assertRaises(ValueError, ipl, ["ba:aa"], 19)
tc.assertRaises(ValueError, ipl, ["bb:aa"], 19)
tc.assertRaises(ValueError, ipl, ["aa", "tt"], 19)
tc.assertRaises(ValueError, ipl, ["aa", ""], 19)
tc.assertRaises(ValueError, ipl, ["aa:", "aa"], 19)
tc.assertRaises(ValueError, ipl, ["aa:tt", "aa"], 19)
tc.assertRaises(ValueError, ipl, ["tt:aa", "aa"], 19)
def test_compressed_point_list_spec_example(tc):
# Checks the examples at http://www.red-bean.com/sgf/DD_VW.html
def sgf_point(move, size):
row, col = move
row = size - row - 1
col_s = "abcdefghijklmnopqrstuvwxy"[col]
row_s = "abcdefghijklmnopqrstuvwxy"[row]
return col_s + row_s
def ipl(l, size):
context = sgf_properties._Context(size, "UTF-8")
return sgf_properties.interpret_point_list(l, context)
tc.assertEqual(
set(sgf_point(move, 9) for move in ipl(["ac:ic"], 9)),
set(["ac", "bc", "cc", "dc", "ec", "fc", "gc", "hc", "ic"]))
tc.assertEqual(
set(sgf_point(move, 9) for move in ipl(["ae:ie"], 9)),
set(["ae", "be", "ce", "de", "ee", "fe", "ge", "he", "ie"]))
tc.assertEqual(
set(sgf_point(move, 9) for move in ipl(["aa:bi", "ca:ce"], 9)),
set(["aa", "ab", "ac", "ad", "ae", "af", "ag", "ah", "ai",
"bi", "bh", "bg", "bf", "be", "bd", "bc", "bb", "ba",
"ca", "cb", "cc", "cd", "ce"]))
def test_serialise_point_list(tc):
def ipl(l, size):
context = sgf_properties._Context(size, "UTF-8")
return sgf_properties.interpret_point_list(l, context)
def spl(l, size):
context = sgf_properties._Context(size, "UTF-8")
return sgf_properties.serialise_point_list(l, context)
tc.assertEqual(spl([(18, 0), (17, 1)], 19), ['aa', 'bb'])
tc.assertEqual(spl([(17, 1), (18, 0)], 19), ['aa', 'bb'])
tc.assertEqual(spl([], 9), [])
tc.assertEqual(ipl(spl([(1,2), (3,4), (4,5)], 19), 19),
set([(1,2), (3,4), (4,5)]))
tc.assertRaises(ValueError, spl, [(18, 0), None], 19)
def test_AP(tc):
def serialise(arg):
context = sgf_properties._Context(19, "UTF-8")
return sgf_properties.serialise_AP(arg, context)
def interpret(arg):
context = sgf_properties._Context(19, "UTF-8")
return sgf_properties.interpret_AP(arg, context)
tc.assertEqual(serialise(("foo:bar", "2\n3")), "foo\\:bar:2\n3")
tc.assertEqual(interpret("foo\\:bar:2 3"), ("foo:bar", "2 3"))
tc.assertEqual(interpret("foo bar"), ("foo bar", ""))
def test_ARLN(tc):
def serialise(arg, size):
context = sgf_properties._Context(size, "UTF-8")
return sgf_properties.serialise_ARLN_list(arg, context)
def interpret(arg, size):
context = sgf_properties._Context(size, "UTF-8")
return sgf_properties.interpret_ARLN_list(arg, context)
tc.assertEqual(serialise([], 19), [])
tc.assertEqual(interpret([], 19), [])
tc.assertEqual(serialise([((7, 0), (5, 2)), ((4, 3), (2, 5))], 9),
['ab:cd', 'de:fg'])
tc.assertEqual(interpret(['ab:cd', 'de:fg'], 9),
[((7, 0), (5, 2)), ((4, 3), (2, 5))])
tc.assertRaises(ValueError, serialise, [((7, 0), None)], 9)
tc.assertRaises(ValueError, interpret, ['ab:tt', 'de:fg'], 9)
def test_FG(tc):
def serialise(arg):
context = sgf_properties._Context(19, "UTF-8")
return sgf_properties.serialise_FG(arg, context)
def interpret(arg):
context = sgf_properties._Context(19, "UTF-8")
return sgf_properties.interpret_FG(arg, context)
tc.assertEqual(serialise(None), "")
tc.assertEqual(interpret(""), None)
tc.assertEqual(serialise((515, "th]is")), "515:th\\]is")
tc.assertEqual(interpret("515:th\\]is"), (515, "th]is"))
def test_LB(tc):
def serialise(arg, size):
context = sgf_properties._Context(size, "UTF-8")
return sgf_properties.serialise_LB_list(arg, context)
def interpret(arg, size):
context = sgf_properties._Context(size, "UTF-8")
return sgf_properties.interpret_LB_list(arg, context)
tc.assertEqual(serialise([], 19), [])
tc.assertEqual(interpret([], 19), [])
tc.assertEqual(
serialise([((6, 0), "lbl"), ((6, 1), "lb]l2")], 9),
["ac:lbl", "bc:lb\\]l2"])
tc.assertEqual(
interpret(["ac:lbl", "bc:lb\\]l2"], 9),
[((6, 0), "lbl"), ((6, 1), "lb]l2")])
tc.assertRaises(ValueError, serialise, [(None, "lbl")], 9)
tc.assertRaises(ValueError, interpret, [':lbl', 'de:lbl2'], 9)
def test_presenter_interpret(tc):
p9 = sgf_properties.Presenter(9, "UTF-8")
p19 = sgf_properties.Presenter(19, "UTF-8")
tc.assertEqual(p9.interpret('KO', [""]), True)
tc.assertEqual(p9.interpret('SZ', ["9"]), 9)
tc.assertRaisesRegexp(ValueError, "multiple values",
p9.interpret, 'SZ', ["9", "blah"])
tc.assertEqual(p9.interpret('CR', ["ab", "cd"]), set([(5, 2), (7, 0)]))
tc.assertRaises(ValueError, p9.interpret, 'SZ', [])
tc.assertRaises(ValueError, p9.interpret, 'CR', [])
tc.assertEqual(p9.interpret('DD', [""]), set())
# all lists are treated like elists
tc.assertEqual(p9.interpret('CR', [""]), set())
def test_presenter_serialise(tc):
p9 = sgf_properties.Presenter(9, "UTF-8")
p19 = sgf_properties.Presenter(19, "UTF-8")
tc.assertEqual(p9.serialise('KO', True), [""])
tc.assertEqual(p9.serialise('SZ', 9), ["9"])
tc.assertEqual(p9.serialise('KM', 3.5), ["3.5"])
tc.assertEqual(p9.serialise('C', "foo\\:b]ar\n"), ["foo\\\\:b\\]ar\n"])
tc.assertEqual(p19.serialise('B', (1, 2)), ["cr"])
tc.assertEqual(p9.serialise('B', None), ["tt"])
tc.assertEqual(p19.serialise('AW', set([(17, 1), (18, 0)])),["aa", "bb"])
tc.assertEqual(p9.serialise('DD', [(1, 2), (3, 4)]), ["ch", "ef"])
tc.assertEqual(p9.serialise('DD', []), [""])
tc.assertRaisesRegexp(ValueError, "empty list", p9.serialise, 'CR', [])
tc.assertEqual(p9.serialise('AP', ("na:me", "2.3")), ["na\\:me:2.3"])
tc.assertEqual(p9.serialise('FG', (515, "th]is")), ["515:th\\]is"])
tc.assertEqual(p9.serialise('XX', "foo\\bar"), ["foo\\\\bar"])
tc.assertRaises(ValueError, p9.serialise, 'B', (1, 9))
def test_presenter_private_properties(tc):
p9 = sgf_properties.Presenter(9, "UTF-8")
tc.assertEqual(p9.serialise('XX', "9"), ["9"])
tc.assertEqual(p9.interpret('XX', ["9"]), "9")
p9.set_private_property_type(p9.get_property_type("SZ"))
tc.assertEqual(p9.serialise('XX', 9), ["9"])
tc.assertEqual(p9.interpret('XX', ["9"]), 9)
p9.set_private_property_type(None)
tc.assertRaisesRegexp(ValueError, "unknown property",
p9.serialise, 'XX', "foo\\bar")
tc.assertRaisesRegexp(ValueError, "unknown property",
p9.interpret, 'XX', ["asd"])

View File

@ -0,0 +1,786 @@
# -*- coding: utf-8 -*-
"""Tests for sgf.py."""
from __future__ import with_statement
from textwrap import dedent
from gomill_tests import gomill_test_support
from gomill import sgf
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def test_new_sgf_game(tc):
g1 = sgf.Sgf_game(9)
tc.assertEqual(g1.get_size(), 9)
root = g1.get_root()
tc.assertEqual(root.get_raw('FF'), '4')
tc.assertEqual(root.get_raw('GM'), '1')
tc.assertEqual(root.get_raw('SZ'), '9')
tc.assertEqual(root.get_raw_property_map(), {
'FF': ['4'],
'GM': ['1'],
'SZ': ['9'],
'CA': ['UTF-8'],
});
tc.assertEqual(list(root), [])
tc.assertEqual(root.parent, None)
tc.assertIs(root.owner, g1)
def test_sgf_game_from_coarse_game_tree(tc):
class Namespace(object):
pass
coarse_game = Namespace()
coarse_game.sequence = [{'SZ' : ["9"]}, {'B' : ["aa"]}]
coarse_game.children = []
g1 = sgf.Sgf_game.from_coarse_game_tree(coarse_game)
tc.assertEqual(g1.get_size(), 9)
root = g1.get_root()
tc.assertIs(root.get_raw_property_map(), coarse_game.sequence[0])
tc.assertEqual(root.parent, None)
tc.assertIs(root.owner, g1)
tc.assertEqual(len(root), 1)
coarse_game2 = Namespace()
coarse_game2.sequence = [{'SZ' : ["0"]}, {'B' : ["aa"]}]
coarse_game2.children = []
tc.assertRaisesRegexp(ValueError, "size out of range: 0",
sgf.Sgf_game.from_coarse_game_tree, coarse_game2)
def test_sgf_game_from_string(tc):
g1 = sgf.Sgf_game.from_string("(;)")
tc.assertEqual(g1.get_size(), 19)
tc.assertRaisesRegexp(ValueError, "unexpected end of SGF data",
sgf.Sgf_game.from_string, "(;SZ[9]")
g2 = sgf.Sgf_game.from_string("(;SZ[9])")
tc.assertEqual(g2.get_size(), 9)
tc.assertRaisesRegexp(ValueError, "bad SZ property: a",
sgf.Sgf_game.from_string, "(;SZ[a])")
tc.assertRaisesRegexp(ValueError, "size out of range: 27",
sgf.Sgf_game.from_string, "(;SZ[27])")
tc.assertRaisesRegexp(ValueError, "unknown encoding: $",
sgf.Sgf_game.from_string, "(;CA[])")
def test_node(tc):
sgf_game = sgf.Sgf_game.from_string(
r"(;KM[6.5]C[sample\: comment]AB[ai][bh][ee]AE[];B[dg])")
node0 = sgf_game.get_root()
node1 = list(sgf_game.main_sequence_iter())[1]
tc.assertEqual(node0.get_size(), 19)
tc.assertEqual(node0.get_encoding(), "ISO-8859-1")
tc.assertIs(node0.has_property('KM'), True)
tc.assertIs(node0.has_property('XX'), False)
tc.assertIs(node1.has_property('KM'), False)
tc.assertEqual(set(node0.properties()), set(["KM", "C", "AB", "AE"]))
tc.assertEqual(set(node1.properties()), set(["B"]))
tc.assertEqual(node0.get_raw('C'), r"sample\: comment")
tc.assertEqual(node0.get_raw('AB'), "ai")
tc.assertEqual(node0.get_raw('AE'), "")
tc.assertRaises(KeyError, node0.get_raw, 'XX')
tc.assertEqual(node0.get_raw_list('KM'), ['6.5'])
tc.assertEqual(node0.get_raw_list('AB'), ['ai', 'bh', 'ee'])
tc.assertEqual(node0.get_raw_list('AE'), [''])
tc.assertRaises(KeyError, node0.get_raw_list, 'XX')
tc.assertRaises(KeyError, node0.get_raw, 'XX')
def test_property_combination(tc):
sgf_game = sgf.Sgf_game.from_string("(;XX[1]YY[2]XX[3]YY[4])")
node0 = sgf_game.get_root()
tc.assertEqual(node0.get_raw_list("XX"), ["1", "3"])
tc.assertEqual(node0.get_raw_list("YY"), ["2", "4"])
def test_node_get(tc):
sgf_game = sgf.Sgf_game.from_string(dedent(r"""
(;AP[testsuite:0]CA[utf-8]DT[2009-06-06]FF[4]GM[1]KM[7.5]PB[Black engine]
PL[B]PW[White engine][xs]RE[W+R]SZ[9]AB[ai][bh][ee]AW[fd][gc]AE[]BM[2]VW[]
EV[Test
event]
C[123:\)
abc]
YY[none
sense]
;B[dg]KO[]AR[ab:cd][de:fg]FG[515:first move]
LB[ac:lbl][bc:lbl2])
"""))
root = sgf_game.get_root()
node1 = list(sgf_game.main_sequence_iter())[1]
tc.assertRaises(KeyError, root.get, 'XX')
tc.assertEqual(root.get('C'), "123:)\nabc") # Text
tc.assertEqual(root.get('EV'), "Test event") # Simpletext
tc.assertEqual(root.get('BM'), 2) # Double
tc.assertEqual(root.get('YY'), "none\nsense") # unknown (Text)
tc.assertIs(node1.get('KO'), True) # None
tc.assertEqual(root.get('KM'), 7.5) # Real
tc.assertEqual(root.get('GM'), 1) # Number
tc.assertEqual(root.get('PL'), 'b') # Color
tc.assertEqual(node1.get('B'), (2, 3)) # Point
tc.assertEqual(root.get('AB'),
set([(0, 0), (1, 1), (4, 4)])) # List of Point
tc.assertEqual(root.get('VW'), set()) # Empty elist
tc.assertEqual(root.get('AP'), ("testsuite", "0")) # Application
tc.assertEqual(node1.get('AR'),
[((7, 0), (5, 2)), ((4, 3), (2, 5))]) # Arrow
tc.assertEqual(node1.get('FG'), (515, "first move")) # Figure
tc.assertEqual(node1.get('LB'),
[((6, 0), "lbl"), ((6, 1), "lbl2")]) # Label
# Check we (leniently) treat lists like elists on read
tc.assertEqual(root.get('AE'), set())
tc.assertRaisesRegexp(ValueError, "multiple values", root.get, 'PW')
def test_text_values(tc):
def check(s):
sgf_game = sgf.Sgf_game.from_string(s)
return sgf_game.get_root().get("C")
# Round-trip check of Text values through tokeniser, parser, and
# text_value().
tc.assertEqual(check(r"(;C[abc]KO[])"), r"abc")
tc.assertEqual(check(r"(;C[a\\bc]KO[])"), r"a\bc")
tc.assertEqual(check(r"(;C[a\\bc\]KO[])"), r"a\bc]KO[")
tc.assertEqual(check(r"(;C[abc\\]KO[])"), r"abc" + "\\")
tc.assertEqual(check(r"(;C[abc\\\]KO[])"), r"abc\]KO[")
tc.assertEqual(check(r"(;C[abc\\\\]KO[])"), r"abc" + "\\\\")
tc.assertEqual(check(r"(;C[abc\\\\\]KO[])"), r"abc\\]KO[")
tc.assertEqual(check(r"(;C[xxx :\) yyy]KO[])"), r"xxx :) yyy")
tc.assertEqual(check("(;C[ab\\\nc])"), "abc")
tc.assertEqual(check("(;C[ab\nc])"), "ab\nc")
SAMPLE_SGF = """\
(;AP[testsuite:0]CA[utf-8]DT[2009-06-06]FF[4]GM[1]KM[7.5]PB[Black engine]
PL[B]PW[White engine]RE[W+R]SZ[9]AB[ai][bh][ee]AW[fc][gc];B[dg];W[ef]C[comment
on two lines];B[];W[tt]C[Final comment])
"""
SAMPLE_SGF_VAR = """\
(;AP[testsuite:0]CA[utf-8]DT[2009-06-06]FF[4]GM[1]KM[7.5]PB[Black engine]
PL[B]RE[W+R]SZ[9]AB[ai][bh][ee]AW[fd][gc]VW[]
;B[dg]
;W[ef]C[comment
on two lines]
;B[]
;C[Nonfinal comment]VW[aa:bb]
(;B[ia];W[ib];B[ic])
(;B[ib];W[ic]
(;B[id])
(;B[ie])
))
"""
def test_node_string(tc):
sgf_game = sgf.Sgf_game.from_string(SAMPLE_SGF)
node = sgf_game.get_root()
tc.assertMultiLineEqual(str(node), dedent("""\
AB[ai][bh][ee]
AP[testsuite:0]
AW[fc][gc]
CA[utf-8]
DT[2009-06-06]
FF[4]
GM[1]
KM[7.5]
PB[Black engine]
PL[B]
PW[White engine]
RE[W+R]
SZ[9]
"""))
def test_node_get_move(tc):
sgf_game = sgf.Sgf_game.from_string(SAMPLE_SGF)
nodes = list(sgf_game.main_sequence_iter())
tc.assertEqual(nodes[0].get_move(), (None, None))
tc.assertEqual(nodes[1].get_move(), ('b', (2, 3)))
tc.assertEqual(nodes[2].get_move(), ('w', (3, 4)))
tc.assertEqual(nodes[3].get_move(), ('b', None))
tc.assertEqual(nodes[4].get_move(), ('w', None))
def test_node_get_setup_stones(tc):
sgf_game = sgf.Sgf_game.from_string(
r"(;KM[6.5]SZ[9]C[sample\: comment]AB[ai][bh][ee]AE[bb];B[dg])")
node0 = sgf_game.get_root()
node1 = list(sgf_game.main_sequence_iter())[1]
tc.assertIs(node0.has_setup_stones(), True)
tc.assertIs(node1.has_setup_stones(), False)
tc.assertEqual(node0.get_setup_stones(),
(set([(0, 0), (1, 1), (4, 4)]), set(), set([(7, 1)])))
tc.assertEqual(node1.get_setup_stones(),
(set(), set(), set()))
def test_sgf_game(tc):
sgf_game = sgf.Sgf_game.from_string(SAMPLE_SGF_VAR)
nodes = list(sgf_game.main_sequence_iter())
tc.assertEqual(sgf_game.get_size(), 9)
tc.assertEqual(sgf_game.get_komi(), 7.5)
tc.assertIs(sgf_game.get_handicap(), None)
tc.assertEqual(sgf_game.get_player_name('b'), "Black engine")
tc.assertIs(sgf_game.get_player_name('w'), None)
tc.assertEqual(sgf_game.get_winner(), 'w')
tc.assertEqual(nodes[2].get('C'), "comment\non two lines")
tc.assertEqual(nodes[4].get('C'), "Nonfinal comment")
g2 = sgf.Sgf_game.from_string("(;)")
tc.assertEqual(g2.get_size(), 19)
tc.assertEqual(g2.get_komi(), 0.0)
tc.assertIs(g2.get_handicap(), None)
tc.assertIs(g2.get_player_name('b'), None)
tc.assertIs(g2.get_player_name('w'), None)
tc.assertEqual(g2.get_winner(), None)
def test_tree_view(tc):
sgf_game = sgf.Sgf_game.from_string(SAMPLE_SGF_VAR)
root = sgf_game.get_root()
tc.assertIsInstance(root, sgf.Tree_node)
tc.assertIs(root.parent, None)
tc.assertIs(root.owner, sgf_game)
tc.assertEqual(len(root), 1)
tc.assertEqual(root[0].get_raw('B'), "dg")
tc.assertTrue(root)
tc.assertEqual(root.index(root[0]), 0)
branchnode = root[0][0][0][0]
tc.assertIsInstance(branchnode, sgf.Tree_node)
tc.assertIs(branchnode.parent, root[0][0][0])
tc.assertIs(branchnode.owner, sgf_game)
tc.assertEqual(len(branchnode), 2)
tc.assertIs(branchnode[1], branchnode[-1])
tc.assertEqual(branchnode[:1], [branchnode[0]])
tc.assertEqual([node for node in branchnode],
[branchnode[0], branchnode[1]])
with tc.assertRaises(IndexError):
branchnode[2]
tc.assertEqual(branchnode[0].get_raw('B'), "ia")
tc.assertEqual(branchnode[1].get_raw('B'), "ib")
tc.assertEqual(branchnode.index(branchnode[0]), 0)
tc.assertEqual(branchnode.index(branchnode[1]), 1)
tc.assertEqual(len(branchnode[1][0]), 2)
leaf = branchnode[1][0][1]
tc.assertIs(leaf.parent, branchnode[1][0])
tc.assertEqual(len(leaf), 0)
tc.assertFalse(leaf)
tc.assertIs(sgf_game.get_last_node(), root[0][0][0][0][0][0][0])
# check nothing breaks when first retrieval is by index
game2 = sgf.Sgf_game.from_string(SAMPLE_SGF)
root2 = game2.get_root()
tc.assertEqual(root2[0].get_raw('B'), "dg")
def test_serialise(tc):
# Doesn't cover transcoding
sgf_game = sgf.Sgf_game.from_string(SAMPLE_SGF_VAR)
serialised = sgf_game.serialise()
tc.assertEqual(serialised, dedent("""\
(;FF[4]AB[ai][bh][ee]AP[testsuite:0]AW[fd][gc]CA[utf-8]DT[2009-06-06]GM[1]
KM[7.5]PB[Black engine]PL[B]RE[W+R]SZ[9]VW[];B[dg];C[comment
on two lines]W[ef]
;B[];C[Nonfinal comment]VW[aa:bb](;B[ia];W[ib];B[ic])(;B[ib];W[ic](;B[id])(;
B[ie])))
"""))
sgf_game2 = sgf.Sgf_game.from_string(serialised)
tc.assertEqual(map(str, sgf_game.get_main_sequence()),
map(str, sgf_game2.get_main_sequence()))
def test_serialise_wrap(tc):
sgf_game = sgf.Sgf_game.from_string(SAMPLE_SGF_VAR)
serialised = sgf_game.serialise(wrap=None)
tc.assertEqual(serialised, dedent("""\
(;FF[4]AB[ai][bh][ee]AP[testsuite:0]AW[fd][gc]CA[utf-8]DT[2009-06-06]GM[1]KM[7.5]PB[Black engine]PL[B]RE[W+R]SZ[9]VW[];B[dg];C[comment
on two lines]W[ef];B[];C[Nonfinal comment]VW[aa:bb](;B[ia];W[ib];B[ic])(;B[ib];W[ic](;B[id])(;B[ie])))
"""))
sgf_game2 = sgf.Sgf_game.from_string(serialised)
tc.assertEqual(map(str, sgf_game.get_main_sequence()),
map(str, sgf_game2.get_main_sequence()))
def test_encoding(tc):
g1 = sgf.Sgf_game(19)
tc.assertEqual(g1.get_charset(), "UTF-8")
root = g1.get_root()
tc.assertEqual(root.get_encoding(), "UTF-8")
root.set("C", "£")
tc.assertEqual(root.get("C"), "£")
tc.assertEqual(root.get_raw("C"), "£")
tc.assertEqual(g1.serialise(), dedent("""\
(;FF[4]C[£]CA[UTF-8]GM[1]SZ[19])
"""))
g2 = sgf.Sgf_game(19, encoding="iso-8859-1")
tc.assertEqual(g2.get_charset(), "ISO-8859-1")
root = g2.get_root()
tc.assertEqual(root.get_encoding(), "ISO-8859-1")
root.set("C", "£")
tc.assertEqual(root.get("C"), "£")
tc.assertEqual(root.get_raw("C"), "\xa3")
tc.assertEqual(g2.serialise(), dedent("""\
(;FF[4]C[\xa3]CA[ISO-8859-1]GM[1]SZ[19])
"""))
tc.assertRaisesRegexp(ValueError, "unknown encoding: unknownencoding",
sgf.Sgf_game, 19, "unknownencoding")
def test_loaded_sgf_game_encoding(tc):
g1 = sgf.Sgf_game.from_string("""
(;FF[4]C[£]CA[utf-8]GM[1]SZ[19])
""")
tc.assertEqual(g1.get_charset(), "UTF-8")
root = g1.get_root()
tc.assertEqual(root.get_encoding(), "UTF-8")
tc.assertEqual(root.get("C"), "£")
tc.assertEqual(root.get_raw("C"), "£")
tc.assertEqual(g1.serialise(), dedent("""\
(;FF[4]C[£]CA[utf-8]GM[1]SZ[19])
"""))
g2 = sgf.Sgf_game.from_string("""
(;FF[4]C[\xa3]CA[iso-8859-1]GM[1]SZ[19])
""")
tc.assertEqual(g2.get_charset(), "ISO-8859-1")
root = g2.get_root()
tc.assertEqual(root.get_encoding(), "ISO-8859-1")
tc.assertEqual(root.get("C"), "£")
tc.assertEqual(root.get_raw("C"), "\xa3")
tc.assertEqual(g2.serialise(), dedent("""\
(;FF[4]C[\xa3]CA[iso-8859-1]GM[1]SZ[19])
"""))
g3 = sgf.Sgf_game.from_string("""
(;FF[4]C[\xa3]GM[1]SZ[19])
""")
tc.assertEqual(g3.get_charset(), "ISO-8859-1")
root = g3.get_root()
tc.assertEqual(root.get_encoding(), "ISO-8859-1")
tc.assertEqual(root.get("C"), "£")
tc.assertEqual(root.get_raw("C"), "\xa3")
tc.assertEqual(g3.serialise(), dedent("""\
(;FF[4]C[\xa3]GM[1]SZ[19])
"""))
# This is invalidly encoded. get() notices, but serialise() doesn't care.
g4 = sgf.Sgf_game.from_string("""
(;FF[4]C[\xa3]CA[utf-8]GM[1]SZ[19])
""")
tc.assertEqual(g4.get_charset(), "UTF-8")
root = g4.get_root()
tc.assertEqual(root.get_encoding(), "UTF-8")
tc.assertRaises(UnicodeDecodeError, root.get, "C")
tc.assertEqual(root.get_raw("C"), "\xa3")
tc.assertEqual(g4.serialise(), dedent("""\
(;FF[4]C[\xa3]CA[utf-8]GM[1]SZ[19])
"""))
tc.assertRaisesRegexp(
ValueError, "unknown encoding: unknownencoding",
sgf.Sgf_game.from_string, """
(;FF[4]CA[unknownencoding]GM[1]SZ[19])
""")
def test_override_encoding(tc):
g1 = sgf.Sgf_game.from_string("""
(;FF[4]C[£]CA[iso-8859-1]GM[1]SZ[19])
""", override_encoding="utf-8")
root = g1.get_root()
tc.assertEqual(root.get_encoding(), "UTF-8")
tc.assertEqual(root.get("C"), "£")
tc.assertEqual(root.get_raw("C"), "£")
tc.assertEqual(g1.serialise(), dedent("""\
(;FF[4]C[£]CA[UTF-8]GM[1]SZ[19])
"""))
g2 = sgf.Sgf_game.from_string("""
(;FF[4]C[\xa3]CA[utf-8]GM[1]SZ[19])
""", override_encoding="iso-8859-1")
root = g2.get_root()
tc.assertEqual(root.get_encoding(), "ISO-8859-1")
tc.assertEqual(root.get("C"), "£")
tc.assertEqual(root.get_raw("C"), "\xa3")
tc.assertEqual(g2.serialise(), dedent("""\
(;FF[4]C[\xa3]CA[ISO-8859-1]GM[1]SZ[19])
"""))
def test_serialise_transcoding(tc):
g1 = sgf.Sgf_game.from_string("""
(;FF[4]C[£]CA[utf-8]GM[1]SZ[19])
""")
tc.assertEqual(g1.serialise(), dedent("""\
(;FF[4]C[£]CA[utf-8]GM[1]SZ[19])
"""))
g1.get_root().set("CA", "latin-1")
tc.assertEqual(g1.serialise(), dedent("""\
(;FF[4]C[\xa3]CA[latin-1]GM[1]SZ[19])
"""))
g1.get_root().set("CA", "unknown")
tc.assertRaisesRegexp(ValueError, "unsupported charset: \['unknown']",
g1.serialise)
# improperly-encoded from the start
g2 = sgf.Sgf_game.from_string("""
(;FF[4]C[£]CA[ascii]GM[1]SZ[19])
""")
tc.assertEqual(g2.serialise(), dedent("""\
(;FF[4]C[£]CA[ascii]GM[1]SZ[19])
"""))
g2.get_root().set("CA", "utf-8")
tc.assertRaises(UnicodeDecodeError, g2.serialise)
g3 = sgf.Sgf_game.from_string("""
(;FF[4]C[Δ]CA[utf-8]GM[1]SZ[19])
""")
g3.get_root().unset("CA")
tc.assertRaises(UnicodeEncodeError, g3.serialise)
def test_tree_mutation(tc):
sgf_game = sgf.Sgf_game(9)
root = sgf_game.get_root()
n1 = root.new_child()
n1.set("N", "n1")
n2 = root.new_child()
n2.set("N", "n2")
n3 = n1.new_child()
n3.set("N", "n3")
n4 = root.new_child(1)
n4.set("N", "n4")
tc.assertEqual(
sgf_game.serialise(),
"(;FF[4]CA[UTF-8]GM[1]SZ[9](;N[n1];N[n3])(;N[n4])(;N[n2]))\n")
tc.assertEqual(
[node.get_raw_property_map() for node in sgf_game.main_sequence_iter()],
[node.get_raw_property_map() for node in root, root[0], n3])
tc.assertIs(sgf_game.get_last_node(), n3)
n1.delete()
tc.assertEqual(
sgf_game.serialise(),
"(;FF[4]CA[UTF-8]GM[1]SZ[9](;N[n4])(;N[n2]))\n")
tc.assertRaises(ValueError, root.delete)
def test_tree_mutation_from_coarse_game(tc):
sgf_game = sgf.Sgf_game.from_string("(;SZ[9](;N[n1];N[n3])(;N[n2]))")
root = sgf_game.get_root()
n4 = root.new_child()
n4.set("N", "n4")
n3 = root[0][0]
tc.assertEqual(n3.get("N"), "n3")
n5 = n3.new_child()
n5.set("N", "n5")
tc.assertEqual(sgf_game.serialise(),
"(;SZ[9](;N[n1];N[n3];N[n5])(;N[n2])(;N[n4]))\n")
tc.assertEqual(
[node.get_raw_property_map() for node in sgf_game.main_sequence_iter()],
[node.get_raw_property_map() for node in root, root[0], n3, n5])
tc.assertIs(sgf_game.get_last_node(), n5)
n3.delete()
tc.assertEqual(sgf_game.serialise(),
"(;SZ[9](;N[n1])(;N[n2])(;N[n4]))\n")
tc.assertRaises(ValueError, root.delete)
def test_reparent(tc):
g1 = sgf.Sgf_game.from_string("(;SZ[9](;N[n1];N[n3])(;N[n2]))")
root = g1.get_root()
# Test with unexpanded root
tc.assertRaisesRegexp(ValueError, "would create a loop",
root.reparent, root)
n1 = root[0]
n2 = root[1]
n3 = root[0][0]
tc.assertEqual(n1.get("N"), "n1")
tc.assertEqual(n2.get("N"), "n2")
tc.assertEqual(n3.get("N"), "n3")
n3.reparent(n2)
tc.assertEqual(g1.serialise(), "(;SZ[9](;N[n1])(;N[n2];N[n3]))\n")
n3.reparent(n2)
tc.assertEqual(g1.serialise(), "(;SZ[9](;N[n1])(;N[n2];N[n3]))\n")
tc.assertRaisesRegexp(ValueError, "would create a loop",
root.reparent, n3)
tc.assertRaisesRegexp(ValueError, "would create a loop",
n3.reparent, n3)
g2 = sgf.Sgf_game(9)
tc.assertRaisesRegexp(
ValueError, "new parent doesn't belong to the same game",
n3.reparent, g2.get_root())
def test_reparent_index(tc):
g1 = sgf.Sgf_game.from_string("(;SZ[9](;N[n1];N[n3])(;N[n2]))")
root = g1.get_root()
n1 = root[0]
n2 = root[1]
n3 = root[0][0]
tc.assertEqual(n1.get("N"), "n1")
tc.assertEqual(n2.get("N"), "n2")
tc.assertEqual(n3.get("N"), "n3")
n3.reparent(root, index=1)
tc.assertEqual(g1.serialise(), "(;SZ[9](;N[n1])(;N[n3])(;N[n2]))\n")
n3.reparent(root, index=1)
tc.assertEqual(g1.serialise(), "(;SZ[9](;N[n1])(;N[n3])(;N[n2]))\n")
n3.reparent(root, index=2)
tc.assertEqual(g1.serialise(), "(;SZ[9](;N[n1])(;N[n2])(;N[n3]))\n")
def test_extend_main_sequence(tc):
g1 = sgf.Sgf_game(9)
for i in xrange(6):
g1.extend_main_sequence().set("N", "e%d" % i)
tc.assertEqual(
g1.serialise(),
"(;FF[4]CA[UTF-8]GM[1]SZ[9];N[e0];N[e1];N[e2];N[e3];N[e4];N[e5])\n")
g2 = sgf.Sgf_game.from_string("(;SZ[9](;N[n1];N[n3])(;N[n2]))")
for i in xrange(6):
g2.extend_main_sequence().set("N", "e%d" % i)
tc.assertEqual(
g2.serialise(),
"(;SZ[9](;N[n1];N[n3];N[e0];N[e1];N[e2];N[e3];N[e4];N[e5])(;N[n2]))\n")
def test_get_sequence_above(tc):
sgf_game = sgf.Sgf_game.from_string(SAMPLE_SGF_VAR)
root = sgf_game.get_root()
branchnode = root[0][0][0][0]
leaf = branchnode[1][0][1]
tc.assertEqual(sgf_game.get_sequence_above(root), [])
tc.assertEqual(sgf_game.get_sequence_above(branchnode),
[root, root[0], root[0][0], root[0][0][0]])
tc.assertEqual(sgf_game.get_sequence_above(leaf),
[root, root[0], root[0][0], root[0][0][0],
branchnode, branchnode[1], branchnode[1][0]])
sgf_game2 = sgf.Sgf_game.from_string(SAMPLE_SGF_VAR)
tc.assertRaisesRegexp(ValueError, "node doesn't belong to this game",
sgf_game2.get_sequence_above, leaf)
def test_get_main_sequence_below(tc):
sgf_game = sgf.Sgf_game.from_string(SAMPLE_SGF_VAR)
root = sgf_game.get_root()
branchnode = root[0][0][0][0]
leaf = branchnode[1][0][1]
tc.assertEqual(sgf_game.get_main_sequence_below(leaf), [])
tc.assertEqual(sgf_game.get_main_sequence_below(branchnode),
[branchnode[0], branchnode[0][0], branchnode[0][0][0]])
tc.assertEqual(sgf_game.get_main_sequence_below(root),
[root[0], root[0][0], root[0][0][0], branchnode,
branchnode[0], branchnode[0][0], branchnode[0][0][0]])
sgf_game2 = sgf.Sgf_game.from_string(SAMPLE_SGF_VAR)
tc.assertRaisesRegexp(ValueError, "node doesn't belong to this game",
sgf_game2.get_main_sequence_below, branchnode)
def test_main_sequence(tc):
sgf_game = sgf.Sgf_game.from_string(SAMPLE_SGF_VAR)
root = sgf_game.get_root()
nodes = list(sgf_game.main_sequence_iter())
tc.assertEqual(len(nodes), 8)
tc.assertIs(root.get_raw_property_map(),
nodes[0].get_raw_property_map())
# Check that main_sequence_iter() optimisation has been used.
# (Have to call this before making the tree expand.)
with tc.assertRaises(AttributeError):
nodes[1].parent
tree_nodes = sgf_game.get_main_sequence()
tc.assertEqual(len(tree_nodes), 8)
tc.assertIs(root.get_raw_property_map(),
tree_nodes[0].get_raw_property_map())
tc.assertIs(tree_nodes[0], root)
tc.assertIs(tree_nodes[2].parent, tree_nodes[1])
tc.assertIs(sgf_game.get_last_node(), tree_nodes[-1])
tree_node = root
for node in nodes:
tc.assertIs(tree_node.get_raw_property_map(),
node.get_raw_property_map())
if tree_node:
tree_node = tree_node[0]
def test_find(tc):
sgf_game = sgf.Sgf_game.from_string(SAMPLE_SGF_VAR)
root = sgf_game.get_root()
branchnode = root[0][0][0][0]
leaf = branchnode[1][0][1]
tc.assertEqual(root.get("VW"), set())
tc.assertIs(root.find("VW"), root)
tc.assertRaises(KeyError, root[0].get, "VW")
tc.assertEqual(root[0].find_property("VW"), set())
tc.assertIs(root[0].find("VW"), root)
tc.assertEqual(branchnode.get("VW"),
set([(7, 0), (7, 1), (8, 0), (8, 1)]))
tc.assertIs(branchnode.find("VW"), branchnode)
tc.assertEqual(branchnode.find_property("VW"),
set([(7, 0), (7, 1), (8, 0), (8, 1)]))
tc.assertRaises(KeyError, leaf.get, "VW")
tc.assertIs(leaf.find("VW"), branchnode)
tc.assertEqual(leaf.find_property("VW"),
set([(7, 0), (7, 1), (8, 0), (8, 1)]))
tc.assertIs(leaf.find("XX"), None)
tc.assertRaises(KeyError, leaf.find_property, "XX")
def test_node_set_raw(tc):
sgf_game = sgf.Sgf_game.from_string(dedent(r"""
(;AP[testsuite:0]CA[utf-8]DT[2009-06-06]FF[4]GM[1]KM[7.5]
PB[Black engine]PW[White engine]RE[W+R]SZ[9]
AB[ai][bh][ee]AW[fd][gc]BM[2]VW[]
PL[B]
C[123abc]
;B[dg]C[first move])
"""))
root = sgf_game.get_root()
tc.assertEqual(root.get_raw('RE'), "W+R")
root.set_raw('RE', "W+2.5")
tc.assertEqual(root.get_raw('RE'), "W+2.5")
tc.assertRaises(KeyError, root.get_raw, 'XX')
root.set_raw('XX', "xyz")
tc.assertEqual(root.get_raw('XX'), "xyz")
root.set_raw_list('XX', ("abc", "def"))
tc.assertEqual(root.get_raw('XX'), "abc")
tc.assertEqual(root.get_raw_list('XX'), ["abc", "def"])
tc.assertRaisesRegexp(ValueError, "empty property list",
root.set_raw_list, 'B', [])
values = ["123", "456"]
root.set_raw_list('YY', values)
tc.assertEqual(root.get_raw_list('YY'), ["123", "456"])
values.append("789")
tc.assertEqual(root.get_raw_list('YY'), ["123", "456"])
tc.assertRaisesRegexp(ValueError, "ill-formed property identifier",
root.set_raw, 'Black', "aa")
tc.assertRaisesRegexp(ValueError, "ill-formed property identifier",
root.set_raw_list, 'Black', ["aa"])
root.set_raw('C', "foo\\]bar")
tc.assertEqual(root.get_raw('C'), "foo\\]bar")
root.set_raw('C', "abc\\\\")
tc.assertEqual(root.get_raw('C'), "abc\\\\")
tc.assertRaisesRegexp(ValueError, "ill-formed raw property value",
root.set_raw, 'C', "foo]bar")
tc.assertRaisesRegexp(ValueError, "ill-formed raw property value",
root.set_raw, 'C', "abc\\")
tc.assertRaisesRegexp(ValueError, "ill-formed raw property value",
root.set_raw_list, 'C', ["abc", "de]f"])
root.set_raw('C', "foo\\]bar\\\nbaz")
tc.assertEqual(root.get('C'), "foo]barbaz")
def test_node_aliasing(tc):
# Check that node objects retrieved by different means use the same
# property map.
sgf_game = sgf.Sgf_game.from_string(dedent(r"""
(;C[root];C[node 1])
"""))
root = sgf_game.get_root()
plain_node = list(sgf_game.main_sequence_iter())[1]
tree_node = root[0]
# Check the main_sequence_iter() optimisation was used, otherwise this test
# isn't checking what it's supposed to.
tc.assertIsNot(tree_node, plain_node)
tc.assertIs(tree_node.__class__, sgf.Tree_node)
tc.assertIs(plain_node.__class__, sgf.Node)
tc.assertEqual(tree_node.get_raw('C'), "node 1")
tree_node.set_raw('C', r"test\value")
tc.assertEqual(tree_node.get_raw('C'), r"test\value")
tc.assertEqual(plain_node.get_raw('C'), r"test\value")
plain_node.set_raw_list('XX', ["1", "2", "3"])
tc.assertEqual(tree_node.get_raw_list('XX'), ["1", "2", "3"])
def test_node_set(tc):
sgf_game = sgf.Sgf_game.from_string("(;FF[4]GM[1]SZ[9])")
root = sgf_game.get_root()
root.set("KO", True)
root.set("KM", 0.5)
root.set('DD', [(3, 4), (5, 6)])
root.set('AB', set([(0, 0), (1, 1), (4, 4)]))
root.set('TW', set())
root.set('XX', "nonsense [none]sense more n\\onsens\\e")
tc.assertEqual(sgf_game.serialise(), dedent("""\
(;FF[4]AB[ai][bh][ee]DD[ef][gd]GM[1]KM[0.5]KO[]SZ[9]TW[]
XX[nonsense [none\\]sense more n\\\\onsens\\\\e])
"""))
def test_node_unset(tc):
sgf_game = sgf.Sgf_game.from_string("(;FF[4]GM[1]SZ[9]HA[3])")
root = sgf_game.get_root()
tc.assertEqual(root.get('HA'), 3)
root.unset('HA')
tc.assertRaises(KeyError, root.unset, 'PL')
tc.assertEqual(sgf_game.serialise(),
"(;FF[4]GM[1]SZ[9])\n")
def test_set_and_unset_size(tc):
g1 = sgf.Sgf_game.from_string("(;FF[4]GM[1]SZ[9]HA[3])")
root1 = g1.get_root()
tc.assertRaisesRegexp(ValueError, "changing size is not permitted",
root1.set, "SZ", 19)
root1.set("SZ", 9)
tc.assertRaisesRegexp(ValueError, "changing size is not permitted",
root1.unset, "SZ")
g2 = sgf.Sgf_game.from_string("(;FF[4]GM[1]SZ[19]HA[3])")
root2 = g2.get_root()
root2.unset("SZ")
root2.set("SZ", 19)
def test_set_and_unset_charset(tc):
g1 = sgf.Sgf_game.from_string("(;FF[4]CA[utf-8]GM[1]SZ[9]HA[3])")
tc.assertEqual(g1.get_charset(), "UTF-8")
root1 = g1.get_root()
root1.unset("CA")
tc.assertEqual(g1.get_charset(), "ISO-8859-1")
root1.set("CA", "iso-8859-1")
tc.assertEqual(g1.get_charset(), "ISO-8859-1")
root1.set("CA", "ascii")
tc.assertEqual(g1.get_charset(), "ASCII")
root1.set("CA", "unknownencoding")
tc.assertRaisesRegexp(ValueError,
"no codec available for CA unknownencoding",
g1.get_charset)
def test_node_set_move(tc):
sgf_game = sgf.Sgf_game.from_string("(;FF[4]GM[1]SZ[9];B[aa];B[bb])")
root, n1, n2 = sgf_game.get_main_sequence()
tc.assertEqual(root.get_move(), (None, None))
root.set_move('b', (1, 1))
n1.set_move('w', (1, 2))
n2.set_move('b', None)
tc.assertEqual(root.get('B'), (1, 1))
tc.assertRaises(KeyError, root.get, 'W')
tc.assertEqual(n1.get('W'), (1, 2))
tc.assertRaises(KeyError, n1.get, 'B')
tc.assertEqual(n2.get('B'), None)
tc.assertRaises(KeyError, n2.get, 'W')
def test_node_setup_stones(tc):
sgf_game = sgf.Sgf_game.from_string("(;FF[4]GM[1]SZ[9]AW[aa:bb])")
root = sgf_game.get_root()
root.set_setup_stones(
[(1, 2), (3, 4)],
set(),
[(1, 3), (4, 5)],
)
tc.assertEqual(root.get('AB'), set([(1, 2), (3, 4)]))
tc.assertRaises(KeyError, root.get, 'AW')
tc.assertEqual(root.get('AE'), set([(1, 3), (4, 5)]))
def test_add_comment_text(tc):
sgf_game = sgf.Sgf_game(9)
root = sgf_game.get_root()
root.add_comment_text("hello\nworld")
tc.assertEqual(root.get('C'), "hello\nworld")
root.add_comment_text("hello\naga]in")
tc.assertEqual(root.get('C'), "hello\nworld\n\nhello\naga]in")

View File

@ -0,0 +1,19 @@
"""Fake GTP engine that reports info about cwd and environment.
This is used by gtp_controller_tests.test_subprocess_channel
This mustn't import any gomill or gomill_tests code.
"""
import sys
import os
def main():
sys.stderr.write("subprocess_state_reporter: testing\n")
# Read the GTP command
sys.stdin.readline()
sys.stdout.write("= cwd: %s\nGOMILL_TEST:%s\n\n" %
(os.getcwd(), os.environ.get("GOMILL_TEST")))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,158 @@
"""Generic (non-gomill-specific) test framework code."""
import sys
if sys.version_info >= (2, 7):
import unittest as unittest2
else:
try:
import unittest2
except ImportError, e:
e.unittest2_missing = True
raise
# This makes TestResult ignore lines from this module in tracebacks
__unittest = True
class SupporterError(StandardError):
"""Exception raised by support objects when something goes wrong.
This is raised to indicate things like sequencing errors detected by mock
objects.
"""
class FrameworkTestCase(unittest2.TestCase):
"""unittest2-style TestCase implementation with a few tweaks."""
# This is default in unittest2 but not python 2.7 unittest, so force it on.
longMessage = True
def assertItemsEqual(self, expected_seq, actual_seq, msg=None):
"""Variant implementation of standard assertItemsEqual.
This uses the unorderable_list_difference check even if the lists are
sortable: I prefer its output.
"""
expected = list(expected_seq)
actual = list(actual_seq)
missing, unexpected = unittest2.util.unorderable_list_difference(
expected, actual, ignore_duplicate=False
)
errors = []
if missing:
errors.append('Expected, but missing:\n %s' %
unittest2.util.safe_repr(missing))
if unexpected:
errors.append('Unexpected, but present:\n %s' %
unittest2.util.safe_repr(unexpected))
if errors:
standardMsg = '\n'.join(errors)
self.fail(self._formatMessage(msg, standardMsg))
class SimpleTestCase(FrameworkTestCase):
"""TestCase which runs a single function.
Instantiate with the test function, which takes a TestCase parameter, eg:
def test_xxx(tc):
tc.assertEqual(2+2, 4)
"""
def __init__(self, fn):
FrameworkTestCase.__init__(self)
self.fn = fn
try:
self.name = fn.__module__.split(".", 1)[-1] + "." + fn.__name__
except AttributeError:
self.name = str(fn)
def runTest(self):
self.fn(self)
def id(self):
return self.name
def shortDescription(self):
return None
def __str__(self):
return self.name
def __repr__(self):
return "<SimpleTestCase: %s>" % self.name
class ParameterisedTestCase(FrameworkTestCase):
"""Parameterised testcase.
Subclasses should define:
test_name -- short string
parameter_names -- list of identifiers
runTest
"""
def __init__(self, code, *parameters):
FrameworkTestCase.__init__(self)
self.code = code
self.name = "%s.%s:%s" % (self.__class__.__module__.split(".", 1)[-1],
self.test_name, code)
for name, value in zip(self.parameter_names, parameters):
setattr(self, name, value)
def runTest(self):
raise NotImplementedError
def id(self):
return self.name
def shortDescription(self):
return None
def __str__(self):
return self.name
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self.name)
def _function_sort_key(fn):
try:
return fn.__code__.co_firstlineno
except AttributeError:
return str(fn)
def make_simple_tests(source, prefix="test_", testcase_class=SimpleTestCase):
"""Make test cases from a module's test_xxx functions.
source -- dict (usually a module's globals()).
prefix -- string (default "test_")
testcase_class -- SimpleTestCase subclass to use
Returns a list of TestCase objects.
This makes a TestCase for each function in the values of 'source' whose
name begins with 'prefix'.
The list is in the order of function definition (using the line number
attribute).
"""
functions = [value for name, value in source.iteritems()
if name.startswith(prefix) and callable(value)]
functions.sort(key=_function_sort_key)
return [testcase_class(fn) for fn in functions]
class Fixture(object):
"""A testing fixture.
Instantiate fixture objects with a TestCase parameter.
The fixture arranges for any necessary cleanup to be performed by calling
TestCase.addCleanUp.
"""

View File

@ -0,0 +1,78 @@
"""Generic (non-gomill-specific) test support code."""
import errno
from cStringIO import StringIO
from gomill_tests.test_framework import SupporterError
class Mock_writing_pipe(object):
"""Mock writeable pipe object, with an interface like a cStringIO.
If this is 'broken', it raises IOError(EPIPE) on any further writes.
"""
def __init__(self):
self.sink = StringIO()
self.is_broken = False
def write(self, s):
if self.is_broken:
raise IOError(errno.EPIPE, "Broken pipe")
try:
self.sink.write(s)
except ValueError, e:
raise IOError(errno.EIO, str(e))
def flush(self):
self.sink.flush()
def close(self):
self.sink.close()
def simulate_broken_pipe(self):
self.is_broken = True
def getvalue(self):
return self.sink.getvalue()
class Mock_reading_pipe(object):
"""Mock readable pipe object, with an interface like a cStringIO.
Instantiate with the data to provide on the pipe.
If this is 'broken', it always returns EOF from that point on.
Set the attribute hangs_before_eof true to simulate a pipe that isn't closed
when it runs out of data.
"""
def __init__(self, response):
self.source = StringIO(response)
self.is_broken = False
self.hangs_before_eof = False
def read(self, n):
if self.is_broken:
return ""
result = self.source.read(n)
if self.hangs_before_eof and result == "":
raise SupporterError("read called with no data; this would hang")
return result
def readline(self):
if self.is_broken:
return ""
result = self.source.readline()
if self.hangs_before_eof and not result.endswith("\n"):
raise SupporterError(
"readline called with no newline; this would hang")
return result
def close(self):
self.source.close()
def simulate_broken_pipe(self):
self.is_broken = True

View File

@ -0,0 +1,63 @@
"""Tests for utils.py."""
from gomill_tests import gomill_test_support
from gomill import utils
def make_tests(suite):
suite.addTests(gomill_test_support.make_simple_tests(globals()))
def test_format_float(tc):
ff = utils.format_float
tc.assertEqual(ff(1), "1")
tc.assertEqual(ff(1.0), "1")
tc.assertEqual(ff(1.5), "1.5")
def test_format_percent(tc):
pct = utils.format_percent
tc.assertEqual(pct(1, 1), "100.00%")
tc.assertEqual(pct(1, 2), "50.00%")
tc.assertEqual(pct(1.0, 2.0), "50.00%")
tc.assertEqual(pct(1, 3), "33.33%")
tc.assertEqual(pct(0, 3), "0.00%")
tc.assertEqual(pct(2, 0), "??")
tc.assertEqual(pct(0, 0), "--")
def test_sanitise_utf8(tc):
su = utils.sanitise_utf8
tc.assertIsNone(su(None))
tc.assertEqual(su(""), "")
tc.assertEqual(su("hello world"), "hello world")
s = u"test \N{POUND SIGN}".encode("utf-8")
tc.assertIs(su(s), s)
tc.assertEqual(su(u"test \N{POUND SIGN}".encode("latin1")), "test ?")
def test_isinf(tc):
tc.assertIs(utils.isinf(0), False)
tc.assertIs(utils.isinf(0.0), False)
tc.assertIs(utils.isinf(3), False)
tc.assertIs(utils.isinf(3.0), False)
tc.assertIs(utils.isinf(1e300), False)
tc.assertIs(utils.isinf(1e400), True)
tc.assertIs(utils.isinf(-1e300), False)
tc.assertIs(utils.isinf(-1e400), True)
tc.assertIs(utils.isinf(1e-300), False)
tc.assertIs(utils.isinf(1e-400), False)
tc.assertIs(utils.isinf(float("inf")), True)
tc.assertIs(utils.isinf(float("-inf")), True)
tc.assertIs(utils.isinf(float("NaN")), False)
def test_nan(tc):
tc.assertIs(utils.isnan(0), False)
tc.assertIs(utils.isnan(0.0), False)
tc.assertIs(utils.isnan(1e300), False)
tc.assertIs(utils.isnan(1e400), False)
tc.assertIs(utils.isnan(-1e300), False)
tc.assertIs(utils.isnan(-1e400), False)
tc.assertIs(utils.isnan(1e-300), False)
tc.assertIs(utils.isnan(1e-400), False)
tc.assertIs(utils.isnan(float("inf")), False)
tc.assertIs(utils.isnan(float("-inf")), False)
tc.assertIs(utils.isnan(float("NaN")), True)