pygo/gomill/gomill_tests/game_job_tests.py

459 lines
17 KiB
Python

"""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"])