From 65adea3cf601525c32279a5ae36a23c1cde6a387 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Fri, 29 Jan 2021 15:06:27 +0100 Subject: Started implementing tests for gameBoard. --- tests/test_gameBoard.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/test_gameBoard.py (limited to 'tests') diff --git a/tests/test_gameBoard.py b/tests/test_gameBoard.py new file mode 100644 index 0000000..2fa1037 --- /dev/null +++ b/tests/test_gameBoard.py @@ -0,0 +1,30 @@ +"""Tests for gameBoard module.""" + +import unittest + +from imago.data.enums import Player +from imago.gameLogic.gameBoard import GameBoard + +#from imago.data.enums import Player + +TEST_BOARD_SIZE = 19 + +class TestGameBoard(unittest.TestCase): + """Test gameBoard module.""" + + def testGetGroupLiberties(self): + """Test calculation of group liberties.""" + board = GameBoard(TEST_BOARD_SIZE) + + #Empty cell liberties + self.assertEqual(board.getGroupLiberties(0,0), {}) + self.assertEqual(board.getGroupLibertiesCount(0,0), 0) + + # Lone stone liberties + board.board[3][3] = Player.WHITE + self.assertEqual(board.getGroupLiberties(3,3), + {(2,3), (3,2), (4,3), (3,4)}) + self.assertEqual(board.getGroupLibertiesCount(3,3), 4) + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.1 From 4a39a8fd07e49db5feb0c403b784423f0b673f97 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sat, 12 Jun 2021 10:58:21 +0200 Subject: First MonteCarlo match simulations. --- tests/test_gameBoard.py | 84 +++++++++++++++++++++++++++++++++++++++++++++--- tests/test_gameMove.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_monteCarlo.py | 22 +++++++++++++ 3 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 tests/test_gameMove.py create mode 100644 tests/test_monteCarlo.py (limited to 'tests') diff --git a/tests/test_gameBoard.py b/tests/test_gameBoard.py index 2fa1037..3a4d5a0 100644 --- a/tests/test_gameBoard.py +++ b/tests/test_gameBoard.py @@ -5,8 +5,6 @@ import unittest from imago.data.enums import Player from imago.gameLogic.gameBoard import GameBoard -#from imago.data.enums import Player - TEST_BOARD_SIZE = 19 class TestGameBoard(unittest.TestCase): @@ -14,10 +12,10 @@ class TestGameBoard(unittest.TestCase): def testGetGroupLiberties(self): """Test calculation of group liberties.""" - board = GameBoard(TEST_BOARD_SIZE) + board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) #Empty cell liberties - self.assertEqual(board.getGroupLiberties(0,0), {}) + self.assertEqual(board.getGroupLiberties(0,0), set()) self.assertEqual(board.getGroupLibertiesCount(0,0), 0) # Lone stone liberties @@ -26,5 +24,83 @@ class TestGameBoard(unittest.TestCase): {(2,3), (3,2), (4,3), (3,4)}) self.assertEqual(board.getGroupLibertiesCount(3,3), 4) + def testIsCellEye(self): + """Tests the isCellEye method.""" + board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) + + # Empty board is eye + self.assertEqual(Player.EMPTY, board.isCellEye(0, 0)) + self.assertEqual(Player.EMPTY, board.isCellEye(3, 3)) + self.assertEqual(Player.EMPTY, board.isCellEye(TEST_BOARD_SIZE-1, TEST_BOARD_SIZE-1)) + + # Board with 1 stone is eye + board.board[5][6] = Player.WHITE + self.assertEqual(Player.WHITE, board.isCellEye(3, 3)) + + # Board with 2 stones of different color is not eye + board.board[9][9] = Player.BLACK + self.assertEqual(Player.EMPTY, board.isCellEye(3, 3)) + + # Surrounded cell is eye + board.board[6][5] = Player.WHITE + board.board[6][7] = Player.WHITE + board.board[7][6] = Player.WHITE + + self.assertEqual(Player.WHITE, board.isCellEye(6, 6)) + + # Surrounded cell with 2 different colors is not eye + board.board[6][5] = Player.BLACK + self.assertEqual(Player.EMPTY, board.isCellEye(6, 6)) + + def testScore(self): + """Tests the score method.""" + board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) + + # Empty board has no score. + self.assertEqual((0, 0), board.score()) + + # Board with 1 black stone has totalVertices-1 points for black. + board.board[3][3] = Player.BLACK + self.assertEqual((TEST_BOARD_SIZE*TEST_BOARD_SIZE-1, 0), board.score()) + + # Board with 2 black stones has totalVertices-2 points for black. + board.board[5][5] = Player.BLACK + self.assertEqual((TEST_BOARD_SIZE*TEST_BOARD_SIZE-2, 0), board.score()) + + # Board with lone stones of different colors has no score. + board.board[7][7] = Player.WHITE + self.assertEqual((0, 0), board.score()) + + # Black group with surrounded territory. + board.board[2][3] = Player.BLACK + board.board[1][3] = Player.BLACK + board.board[0][3] = Player.BLACK + board.board[3][2] = Player.BLACK + board.board[3][1] = Player.BLACK + board.board[3][0] = Player.BLACK + self.assertEqual((9, 0), board.score()) + + # White group besides black group. + board.board[6][7] = Player.WHITE + board.board[5][7] = Player.WHITE + board.board[5][6] = Player.WHITE + board.board[5][5] = Player.WHITE + board.board[5][4] = Player.WHITE + board.board[5][3] = Player.WHITE + board.board[5][2] = Player.WHITE + board.board[5][1] = Player.WHITE + board.board[5][0] = Player.WHITE + board.board[8][7] = Player.WHITE + board.board[9][7] = Player.WHITE + board.board[9][6] = Player.WHITE + board.board[9][5] = Player.WHITE + board.board[9][4] = Player.WHITE + board.board[9][3] = Player.WHITE + board.board[9][2] = Player.WHITE + board.board[9][1] = Player.WHITE + board.board[9][0] = Player.WHITE + self.assertEqual((9, 21), board.score()) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_gameMove.py b/tests/test_gameMove.py new file mode 100644 index 0000000..6569c5b --- /dev/null +++ b/tests/test_gameMove.py @@ -0,0 +1,78 @@ +"""Tests for gameMove module.""" + +import unittest + +from imago.data.enums import Player +from imago.gameLogic.gameBoard import GameBoard +from imago.gameLogic.gameMove import GameMove + +TEST_BOARD_SIZE = 19 + +class TestGameMove(unittest.TestCase): + """Test gameMove module.""" + + def testAddMove(self): + """Test adding new moves to existing moves.""" + board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) + firstMove = GameMove(board) + + self.assertIsNone(firstMove.coords) + + secondMove = firstMove.addMove(1, 2) + + self.assertIsNone(firstMove.coords) + self.assertEqual(secondMove.coords[0], 1) + self.assertEqual(secondMove.coords[1], 2) + + thirdMove = secondMove.addMove(5, 7) + + self.assertIsNone(firstMove.coords) + self.assertIsNone(thirdMove.previousMove.previousMove.coords) + + self.assertEqual(secondMove.coords[0], 1) + self.assertEqual(secondMove.coords[1], 2) + self.assertEqual(thirdMove.previousMove.coords[0], 1) + self.assertEqual(thirdMove.previousMove.coords[1], 2) + + self.assertEqual(thirdMove.coords[0], 5) + self.assertEqual(thirdMove.coords[1], 7) + self.assertEqual(firstMove + .nextMoves[0] + .nextMoves[0] + .coords[0], 5) + self.assertEqual(firstMove + .nextMoves[0] + .nextMoves[0] + .coords[1], 7) + + self.assertEqual(firstMove.board.getBoard()[1][2], Player.EMPTY) + self.assertEqual(secondMove.board.getBoard()[1][2], Player.BLACK) + self.assertEqual(thirdMove.board.getBoard()[1][2], Player.BLACK) + + self.assertEqual(firstMove.board.getBoard()[5][7], Player.EMPTY) + self.assertEqual(secondMove.board.getBoard()[5][7], Player.EMPTY) + self.assertEqual(thirdMove.board.getBoard()[5][7], Player.WHITE) + + def testGetPlayableVertices(self): + """Test getting the set of valid moves.""" + boardSize = 3 + board = GameBoard(boardSize, boardSize) + + firstMove = GameMove(board) + self.assertSetEqual( + firstMove.getPlayableVertices(), + set(((0,0), (0,1), (0,2), + (1,0), (1,1), (1,2), + (2,0), (2,1), (2,2))) + ) + + secondMove = firstMove.addMove(1, 2) + self.assertSetEqual( + secondMove.getPlayableVertices(), + set(((0,0), (0,1), (0,2), + (1,0), (1,1), + (2,0), (2,1), (2,2))) + ) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_monteCarlo.py b/tests/test_monteCarlo.py new file mode 100644 index 0000000..8fe6f47 --- /dev/null +++ b/tests/test_monteCarlo.py @@ -0,0 +1,22 @@ +"""Tests for the MonteCarlo algorithm.""" + +import unittest + +from imago.gameLogic.gameBoard import GameBoard +from imago.gameLogic.gameMove import GameMove +from imago.engine.monteCarlo import MCTSNode + +TEST_BOARD_SIZE = 19 + +class TestMonteCarlo(unittest.TestCase): + """Test MonteCarlo algorithm.""" + + def testSimulation(self): + """Test calculation of group liberties.""" + board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) + move = GameMove(board) + node = MCTSNode(move, None) + node.simulation() + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.1 From 25e4e5fb3a77c8da65a2e7b42b7d77ba8f1b21a1 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Thu, 17 Jun 2021 20:13:41 +0200 Subject: Engine now uses MCTS algorithm. --- tests/test_monteCarlo.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) (limited to 'tests') diff --git a/tests/test_monteCarlo.py b/tests/test_monteCarlo.py index 8fe6f47..283c657 100644 --- a/tests/test_monteCarlo.py +++ b/tests/test_monteCarlo.py @@ -2,21 +2,29 @@ import unittest -from imago.gameLogic.gameBoard import GameBoard -from imago.gameLogic.gameMove import GameMove +from imago.gameLogic.gameState import GameState +from imago.engine.monteCarlo import MCTS from imago.engine.monteCarlo import MCTSNode -TEST_BOARD_SIZE = 19 +TEST_BOARD_SIZE = 9 class TestMonteCarlo(unittest.TestCase): """Test MonteCarlo algorithm.""" - def testSimulation(self): - """Test calculation of group liberties.""" - board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) - move = GameMove(board) - node = MCTSNode(move, None) - node.simulation() + def testPickMove(self): + """Test picking a move.""" + state = GameState(TEST_BOARD_SIZE) + tree = MCTS(state) + print(tree.pickMove().toString()) + + #def testSimulation(self): + # """Test calculation of group liberties.""" + # board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) + # move = GameMove(board) + # node = MCTSNode(move, None) + # nMatches = 100 + # scoreDiffHeur = 15 + # node.simulation(nMatches, scoreDiffHeur) if __name__ == '__main__': unittest.main() -- cgit v1.2.1 From 79a2fa6eba1648bf2beaaf534d1fa3247c0d6a01 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 1 Jun 2022 20:14:15 +0200 Subject: Some redesigning. --- tests/test_monteCarlo.py | 4 ++-- tests/test_parseHelpers.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/test_monteCarlo.py b/tests/test_monteCarlo.py index 283c657..b217cf9 100644 --- a/tests/test_monteCarlo.py +++ b/tests/test_monteCarlo.py @@ -14,8 +14,8 @@ class TestMonteCarlo(unittest.TestCase): def testPickMove(self): """Test picking a move.""" state = GameState(TEST_BOARD_SIZE) - tree = MCTS(state) - print(tree.pickMove().toString()) + tree = MCTS(state.lastMove) + #print(tree.pickMove().toString()) #def testSimulation(self): # """Test calculation of group liberties.""" diff --git a/tests/test_parseHelpers.py b/tests/test_parseHelpers.py index cf1fa9f..d03f253 100644 --- a/tests/test_parseHelpers.py +++ b/tests/test_parseHelpers.py @@ -20,6 +20,7 @@ class TestParseHelpers(unittest.TestCase): self.assertEqual(parseHelpers.parseMove(["B", "A1", "W"], TEST_BOARD_SIZE), parseHelpers.ParseCodes.ERROR) + parsedMove = parseHelpers.parseMove(["B", "t1"], TEST_BOARD_SIZE) targetMove = parseHelpers.GtpMove(Player.BLACK, [18, 18]) @@ -67,6 +68,10 @@ class TestParseHelpers(unittest.TestCase): "A19", TEST_BOARD_SIZE), [0,0]) + self.assertEqual(parseHelpers.parseVertex( + "pass", TEST_BOARD_SIZE), + "pass") + def testVertexToString(self): """Test converting vertices to strings.""" self.assertEqual(parseHelpers.vertexToString([0,0], TEST_BOARD_SIZE), "A19") @@ -78,6 +83,8 @@ class TestParseHelpers(unittest.TestCase): self.assertEqual(parseHelpers.vertexToString([18,0], TEST_BOARD_SIZE), "A1") self.assertEqual(parseHelpers.vertexToString([18,18], TEST_BOARD_SIZE), "T1") + self.assertEqual(parseHelpers.vertexToString("pass", TEST_BOARD_SIZE), "pass") + self.assertEqual(parseHelpers.vertexToString([-1,0], TEST_BOARD_SIZE), parseHelpers.ParseCodes.ERROR) self.assertEqual(parseHelpers.vertexToString([0,-1], TEST_BOARD_SIZE), -- cgit v1.2.1 From 080ef02ab7abc46eae1e2a550d26fdcb6c87450f Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 29 Jun 2022 14:32:57 +0200 Subject: Updated some tests. --- tests/test_enums.py | 15 +++++++++++ tests/test_gameBoard.py | 20 ++++++++++++--- tests/test_parseHelpers.py | 63 +++++++++++++++++++++++++--------------------- 3 files changed, 66 insertions(+), 32 deletions(-) create mode 100644 tests/test_enums.py (limited to 'tests') diff --git a/tests/test_enums.py b/tests/test_enums.py new file mode 100644 index 0000000..73f4009 --- /dev/null +++ b/tests/test_enums.py @@ -0,0 +1,15 @@ +"""Tests for enums module.""" + +import unittest + +from imago.data.enums import Player + +class TestEnums(unittest.TestCase): + """Test parseHelpers module.""" + + def testOtherPlayer(self): + """Test method to get the other player""" + + self.assertEqual(Player.otherPlayer(Player.BLACK), Player.WHITE) + self.assertEqual(Player.otherPlayer(Player.WHITE), Player.BLACK) + self.assertEqual(Player.otherPlayer(''), Player.EMPTY) diff --git a/tests/test_gameBoard.py b/tests/test_gameBoard.py index 3a4d5a0..8a7b127 100644 --- a/tests/test_gameBoard.py +++ b/tests/test_gameBoard.py @@ -10,20 +10,33 @@ TEST_BOARD_SIZE = 19 class TestGameBoard(unittest.TestCase): """Test gameBoard module.""" - def testGetGroupLiberties(self): - """Test calculation of group liberties.""" + def testGetGroupCounts(self): + """Test calculation of group stones and liberties.""" board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) #Empty cell liberties self.assertEqual(board.getGroupLiberties(0,0), set()) self.assertEqual(board.getGroupLibertiesCount(0,0), 0) - # Lone stone liberties + # Lone stone board.board[3][3] = Player.WHITE + self.assertEqual(board.getGroupVertices(3,3), + {(3,3)}) + self.assertEqual(board.getGroupVerticesCount(3,3), 1) self.assertEqual(board.getGroupLiberties(3,3), {(2,3), (3,2), (4,3), (3,4)}) self.assertEqual(board.getGroupLibertiesCount(3,3), 4) + # L group (don't compute twice liberty inside L) + board.board[3][4] = Player.WHITE + board.board[2][4] = Player.WHITE + self.assertEqual(board.getGroupVertices(3,3), + {(3,3), (3,4), (2,4)}) + self.assertEqual(board.getGroupVerticesCount(3,3), 3) + self.assertEqual(board.getGroupLiberties(3,3), + {(2,3), (3,2), (4,3), (4, 4), (3,5), (2,5), (1,4)}) + self.assertEqual(board.getGroupLibertiesCount(3,3), 7) + def testIsCellEye(self): """Tests the isCellEye method.""" board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) @@ -101,6 +114,5 @@ class TestGameBoard(unittest.TestCase): board.board[9][0] = Player.WHITE self.assertEqual((9, 21), board.score()) - if __name__ == '__main__': unittest.main() diff --git a/tests/test_parseHelpers.py b/tests/test_parseHelpers.py index d03f253..7bbf152 100644 --- a/tests/test_parseHelpers.py +++ b/tests/test_parseHelpers.py @@ -13,16 +13,19 @@ class TestParseHelpers(unittest.TestCase): def testParseMove(self): """Test parsing of GtpMove""" - self.assertEqual(parseHelpers.parseMove(["B"], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.parseMove(["A1"], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.parseMove(["B", "A1", "W"], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - + wrongMoves = [ + ["B"], + ["A1"], + ["B", "A1", "W"] + ] + for move in wrongMoves: + self.assertRaises( + RuntimeError, + parseHelpers.parseMove, + move, TEST_BOARD_SIZE + ) parsedMove = parseHelpers.parseMove(["B", "t1"], TEST_BOARD_SIZE) - targetMove = parseHelpers.GtpMove(Player.BLACK, [18, 18]) self.assertEqual(parsedMove.color, targetMove.color) self.assertEqual(parsedMove.vertex, targetMove.vertex) @@ -33,15 +36,19 @@ class TestParseHelpers(unittest.TestCase): self.assertEqual(parseHelpers.parseColor("B"), Player.BLACK) self.assertEqual(parseHelpers.parseColor("w"), Player.WHITE) self.assertEqual(parseHelpers.parseColor("W"), Player.WHITE) - self.assertEqual(parseHelpers.parseColor("bw"), parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.parseColor("wb"), parseHelpers.ParseCodes.ERROR) + + self.assertRaises(RuntimeError, parseHelpers.parseColor, "bw") + self.assertRaises(RuntimeError, parseHelpers.parseColor, "wb") def testParseVertexWrongInputs(self): """Test wrong inputs for parseVertex.""" inputs = ( "a", "1", "1a", "aa1", "a0", "u1", "a"+str(TEST_BOARD_SIZE+1) ) for text in inputs: - self.assertEqual(parseHelpers.parseVertex(text, TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) + self.assertRaises( + RuntimeError, + parseHelpers.parseVertex, + text, TEST_BOARD_SIZE + ) def testParseVertexCorrectInputs(self): """Test correct inputs and their resulting coordinates for parseVertex.""" @@ -85,22 +92,22 @@ class TestParseHelpers(unittest.TestCase): self.assertEqual(parseHelpers.vertexToString("pass", TEST_BOARD_SIZE), "pass") - self.assertEqual(parseHelpers.vertexToString([-1,0], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([0,-1], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([-1,-1], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([19,0], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([0,19], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([19,19], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([0], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) - self.assertEqual(parseHelpers.vertexToString([0,0,0], TEST_BOARD_SIZE), - parseHelpers.ParseCodes.ERROR) + wrongVertices = [ + [-1,0], + [0,-1], + [-1,-1], + [19,0], + [0,19], + [19,19], + [0], + [0,0,0] + ] + for vertex in wrongVertices: + self.assertRaises( + RuntimeError, + parseHelpers.vertexToString, + vertex, TEST_BOARD_SIZE + ) if __name__ == '__main__': unittest.main() -- cgit v1.2.1 From 824d5e3b6954407a694f5739cbeb40f66324c8d7 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 14 Dec 2022 15:41:49 +0100 Subject: Developing Unit Testing. --- tests/test_gameMove.py | 6 ++-- tests/test_gameState.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/test_imagoIO.py | 40 ++++++++++++++++++++++ tests/test_monteCarlo.py | 48 ++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 tests/test_gameState.py create mode 100644 tests/test_imagoIO.py (limited to 'tests') diff --git a/tests/test_gameMove.py b/tests/test_gameMove.py index 6569c5b..a7edfab 100644 --- a/tests/test_gameMove.py +++ b/tests/test_gameMove.py @@ -18,13 +18,13 @@ class TestGameMove(unittest.TestCase): self.assertIsNone(firstMove.coords) - secondMove = firstMove.addMove(1, 2) + secondMove = firstMove.addMoveByCoords(1, 2) self.assertIsNone(firstMove.coords) self.assertEqual(secondMove.coords[0], 1) self.assertEqual(secondMove.coords[1], 2) - thirdMove = secondMove.addMove(5, 7) + thirdMove = secondMove.addMoveByCoords(5, 7) self.assertIsNone(firstMove.coords) self.assertIsNone(thirdMove.previousMove.previousMove.coords) @@ -66,7 +66,7 @@ class TestGameMove(unittest.TestCase): (2,0), (2,1), (2,2))) ) - secondMove = firstMove.addMove(1, 2) + secondMove = firstMove.addMoveByCoords(1, 2) self.assertSetEqual( secondMove.getPlayableVertices(), set(((0,0), (0,1), (0,2), diff --git a/tests/test_gameState.py b/tests/test_gameState.py new file mode 100644 index 0000000..638e269 --- /dev/null +++ b/tests/test_gameState.py @@ -0,0 +1,89 @@ +"""Tests for the input/output component.""" + +import unittest + +from imago.data.enums import Player +from imago.gameLogic.gameState import GameState + +class TestGameState(unittest.TestCase): + """Test GameState component.""" + + def testCurrentPlayer(self): + """Test simple commands.""" + size = 9 + state = GameState(size) + + self.assertEqual(state.getCurrentPlayer(), Player.BLACK) + self.assertEqual(state.getPlayerCode(), 'B') + + state.playMove(0, 0) + self.assertEqual(state.getCurrentPlayer(), Player.WHITE) + self.assertEqual(state.getPlayerCode(), 'W') + + def testPlays(self): + """Test simple commands.""" + size = 3 + state = GameState(size) + + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + state.playMove(1, 1) + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.BLACK, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + state.playPass() + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.BLACK, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + state.undo() + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.BLACK, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + state.undo() + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + state.passForPlayer(Player.WHITE) + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + + self.assertFalse(state.playMove(-1, -1)) + self.assertEqual(state.getBoard().getBoard(), + [ + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY], + [Player.EMPTY, Player.EMPTY, Player.EMPTY] + ]) + + state.lastMove = None + self.assertEqual(state.getCurrentPlayer(), Player.BLACK) + self.assertRaises(RuntimeError, state.playMove, 0, 0) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_imagoIO.py b/tests/test_imagoIO.py new file mode 100644 index 0000000..c3c6fda --- /dev/null +++ b/tests/test_imagoIO.py @@ -0,0 +1,40 @@ +"""Tests for the input/output component.""" + +import unittest + +import io +import sys + +from imago.engine.imagoIO import ImagoIO + +class TestImagoIO(unittest.TestCase): + """Test ImagoIO component.""" + + @unittest.mock.patch('imago.engine.imagoIO.input', create=True) + + def testSimpleCommands(self, mocked_input): + """Test simple commands.""" + + mocked_input.side_effect = [ + 'name\n', + 'version\n', + 'protocol_version\n', + 'quit\n' + ] + + testout = io.StringIO() + imagoIO = ImagoIO(outputStream=testout) + + imagoIO.start() + value = testout.getvalue() + self.assertEqual( + '= Imago\n\n' + + '= 0.0.0\n\n' + + '= 2\n\n', + value + ) + + testout.close() + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_monteCarlo.py b/tests/test_monteCarlo.py index b217cf9..496c073 100644 --- a/tests/test_monteCarlo.py +++ b/tests/test_monteCarlo.py @@ -2,6 +2,7 @@ import unittest +from imago.engine.decisionAlgorithm import DecisionAlgorithm from imago.gameLogic.gameState import GameState from imago.engine.monteCarlo import MCTS from imago.engine.monteCarlo import MCTSNode @@ -17,6 +18,53 @@ class TestMonteCarlo(unittest.TestCase): tree = MCTS(state.lastMove) #print(tree.pickMove().toString()) + def testForceNextMove(self): + """Test forcing next move.""" + + # Next move before expansion (no children nodes) + state = GameState(TEST_BOARD_SIZE) + tree = MCTS(state.lastMove) + self.assertEqual(set(), tree.root.children) + tree.forceNextMove((0, 1)) + self.assertEqual(set(), tree.root.children) + + # Next move after expansion (with children nodes) + tree.expansions = 2 + tree.simulationsPerExpansion = 2 + tree.pickMove() + self.assertEqual(tree.expansions, len(tree.root.children)) + nextMoveCoords = list(tree.root.children)[0].move.coords + tree.forceNextMove(nextMoveCoords) + + def testPass(self): + """Test passing as next move.""" + state = GameState(TEST_BOARD_SIZE) + tree = MCTS(state.lastMove) + self.assertFalse(tree.root.move.isPass) + tree.forceNextMove("pass") + self.assertTrue(tree.root.move.isPass) + + def testClearBoard(self): + """Test clearing board returns root to original and retains information.""" + state = GameState(TEST_BOARD_SIZE) + tree = MCTS(state.lastMove) + + firstMoveCoords = (0,0) + secondMoveCoords = (1,0) + thirdMoveCoords = (0,1) + + tree.forceNextMove(firstMoveCoords) + tree.forceNextMove(secondMoveCoords) + tree.forceNextMove(thirdMoveCoords) + tree.clearBoard() + + nextNode = list(tree.root.children)[0] + self.assertEqual(firstMoveCoords, nextNode.move.coords) + nextNode = list(nextNode.children)[0] + self.assertEqual(secondMoveCoords, nextNode.move.coords) + nextNode = list(nextNode.children)[0] + self.assertEqual(thirdMoveCoords, nextNode.move.coords) + #def testSimulation(self): # """Test calculation of group liberties.""" # board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) -- cgit v1.2.1 From 80c4cca827ff80c0508c27cd9b6a37ffa2ea17e5 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 11 Jan 2023 19:20:06 +0100 Subject: 100% coverage on imagoIO module. --- tests/test_imagoIO.py | 164 ++++++++++++++++++++++++++++++++++++++++++++- tests/test_parseHelpers.py | 36 +++++----- 2 files changed, 181 insertions(+), 19 deletions(-) (limited to 'tests') diff --git a/tests/test_imagoIO.py b/tests/test_imagoIO.py index c3c6fda..4f2d7bd 100644 --- a/tests/test_imagoIO.py +++ b/tests/test_imagoIO.py @@ -5,36 +5,194 @@ import unittest import io import sys +from imago.data.enums import DecisionAlgorithms from imago.engine.imagoIO import ImagoIO +from imago.engine.parseHelpers import parseVertex class TestImagoIO(unittest.TestCase): """Test ImagoIO component.""" @unittest.mock.patch('imago.engine.imagoIO.input', create=True) - def testSimpleCommands(self, mocked_input): """Test simple commands.""" + self.maxDiff = None + mocked_input.side_effect = [ + '\n', 'name\n', 'version\n', 'protocol_version\n', + 'clear_board\n', + '\n', + 'known_command\n', + 'known_command name\n', + 'known_command version\n', + 'known_command wrongcommand\n', + '\n', + 'boardsize\n', + 'boardsize 10\n', + '\n', + 'komi\n', + 'komi 5.5\n', + '\n', + 'play\n', + 'play 1\n', + 'play 1 2\n', + 'play b a1\n', + '\n', + 'undo\n', + 'undo\n', + '\n', + 'wrongcommand\n', + '\n', 'quit\n' ] testout = io.StringIO() - imagoIO = ImagoIO(outputStream=testout) + imagoIO = ImagoIO( + decisionAlgorithmId=DecisionAlgorithms.MONTECARLO, + outputStream=testout + ) imagoIO.start() value = testout.getvalue() self.assertEqual( '= Imago\n\n' + '= 0.0.0\n\n' + - '= 2\n\n', + '= 2\n\n' + + '= \n\n' + + '? Wrong number of arguments\n' + + '? Usage: known_command COMMAND_NAME\n\n' + + '= true\n\n' + + '= true\n\n' + + '= false\n\n' + + '? Wrong number of arguments\n' + + '? Usage: boardsize \n\n' + + '= \n\n' + + '? Wrong number of arguments\n' + + '? Usage: komi \n\n' + + '= \n\n' + + '? Wrong number of arguments\n' + + '? Usage: play \n\n' + + '? Wrong number of arguments\n' + + '? Usage: play \n\n' + + '? Invalid move: Unknown color [1].\n\n' + + '= \n\n' + + '= \n\n' + + '= \n\n' + + '? unknown command\n\n', + value + ) + + testout.close() + + + @unittest.mock.patch('imago.engine.imagoIO.input', create=True) + def testListsCommands(self, mocked_input): + """Test command for listing all commands.""" + + mocked_input.side_effect = [ + 'list_commands\n', + 'quit\n' + ] + + testout = io.StringIO() + imagoIO = ImagoIO( + decisionAlgorithmId=DecisionAlgorithms.MONTECARLO, + outputStream=testout + ) + + commandsString = "\n".join(list(map( + lambda cmd: "%s - %s" % (cmd.__name__, cmd.__doc__), + imagoIO.commands_set))) + + imagoIO.start() + value = testout.getvalue() + self.assertEqual( + '= ' + + commandsString + + '\n\n\n', value ) testout.close() + + @unittest.mock.patch('imago.engine.imagoIO.input', create=True) + def testFixedHandicap(self, mocked_input): + """Test command for setting fixed handicap stones.""" + + mocked_input.side_effect = [ + 'fixed_handicap\n', + 'fixed_handicap 2\n', + 'quit\n' + ] + + testout = io.StringIO() + imagoIO = ImagoIO( + decisionAlgorithmId=DecisionAlgorithms.MONTECARLO, + outputStream=testout + ) + + imagoIO.start() + value = testout.getvalue() + self.assertEqual( + '? Wrong number of arguments\n' + + '? Usage: fixed_handicap \n\n' + + '= A1 A2\n\n', + value + ) + + testout.close() + + +# @unittest.mock.patch('imago.engine.imagoIO.input', create=True) +# def testGenmove(self, mocked_input): +# """Test command for generating a move.""" +# +# mocked_input.side_effect = [ +# 'genmove\n', +# 'genmove w w\n', +# 'genmove 2\n', +# 'quit\n' +# ] +# +# testout = io.StringIO() +# imagoIO = ImagoIO( +# decisionAlgorithmId=DecisionAlgorithms.MONTECARLO, +# outputStream=testout +# ) +# +# imagoIO.start() +# value = testout.getvalue() +# self.assertEqual( +# '? Wrong number of arguments\n' + +# '? Usage: genmove \n\n' + +# '? Wrong number of arguments\n' + +# '? Usage: genmove \n\n' + +# '? Error: Unknown color [2].\n\n', +# value +# ) +# +# mocked_input.side_effect = [ +# 'genmove w\n', +# 'quit\n'] +# +# testout = io.StringIO() +# imagoIO = ImagoIO( +# decisionAlgorithmId=DecisionAlgorithms.MONTECARLO, +# outputStream=testout +# ) +# imagoIO.start() +# value = testout.getvalue() +# vertexValue = value.split(' ')[1].split('\n')[0] +# +# # Test parsing vertex does not raise an error +# parseVertex(vertexValue, imagoIO.gameEngine.gameState.size) +# +# testout.close() + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_parseHelpers.py b/tests/test_parseHelpers.py index 7bbf152..c1405fb 100644 --- a/tests/test_parseHelpers.py +++ b/tests/test_parseHelpers.py @@ -26,7 +26,7 @@ class TestParseHelpers(unittest.TestCase): ) parsedMove = parseHelpers.parseMove(["B", "t1"], TEST_BOARD_SIZE) - targetMove = parseHelpers.GtpMove(Player.BLACK, [18, 18]) + targetMove = parseHelpers.GtpMove(Player.BLACK, (18, 18)) self.assertEqual(parsedMove.color, targetMove.color) self.assertEqual(parsedMove.vertex, targetMove.vertex) @@ -54,26 +54,26 @@ class TestParseHelpers(unittest.TestCase): """Test correct inputs and their resulting coordinates for parseVertex.""" self.assertEqual(parseHelpers.parseVertex( "a1", TEST_BOARD_SIZE), - [18,0]) + (18,0)) self.assertEqual(parseHelpers.parseVertex( "b1", TEST_BOARD_SIZE), - [18,1]) + (18,1)) self.assertEqual(parseHelpers.parseVertex( "a2", TEST_BOARD_SIZE), - [17,0]) + (17,0)) self.assertEqual(parseHelpers.parseVertex( "b2", TEST_BOARD_SIZE), - [17,1]) + (17,1)) self.assertEqual(parseHelpers.parseVertex( "T1", TEST_BOARD_SIZE), - [18,18]) + (18,18)) self.assertEqual(parseHelpers.parseVertex( "T19", TEST_BOARD_SIZE), - [0,18]) + (0,18)) self.assertEqual(parseHelpers.parseVertex( "A19", TEST_BOARD_SIZE), - [0,0]) + (0,0)) self.assertEqual(parseHelpers.parseVertex( "pass", TEST_BOARD_SIZE), @@ -81,10 +81,14 @@ class TestParseHelpers(unittest.TestCase): def testVertexToString(self): """Test converting vertices to strings.""" - self.assertEqual(parseHelpers.vertexToString([0,0], TEST_BOARD_SIZE), "A19") - self.assertEqual(parseHelpers.vertexToString([1,0], TEST_BOARD_SIZE), "A18") - self.assertEqual(parseHelpers.vertexToString([2,0], TEST_BOARD_SIZE), "A17") - self.assertEqual(parseHelpers.vertexToString([0,1], TEST_BOARD_SIZE), "B19") + + # Try with vertices as tuples + self.assertEqual(parseHelpers.vertexToString((0,0), TEST_BOARD_SIZE), "A19") + self.assertEqual(parseHelpers.vertexToString((1,0), TEST_BOARD_SIZE), "A18") + self.assertEqual(parseHelpers.vertexToString((2,0), TEST_BOARD_SIZE), "A17") + self.assertEqual(parseHelpers.vertexToString((0,1), TEST_BOARD_SIZE), "B19") + + # Try with vertices as arrays self.assertEqual(parseHelpers.vertexToString([0,2], TEST_BOARD_SIZE), "C19") self.assertEqual(parseHelpers.vertexToString([0,18], TEST_BOARD_SIZE), "T19") self.assertEqual(parseHelpers.vertexToString([18,0], TEST_BOARD_SIZE), "A1") @@ -93,10 +97,10 @@ class TestParseHelpers(unittest.TestCase): self.assertEqual(parseHelpers.vertexToString("pass", TEST_BOARD_SIZE), "pass") wrongVertices = [ - [-1,0], - [0,-1], - [-1,-1], - [19,0], + (-1,0), + (0,-1), + (-1,-1), + (19,0), [0,19], [19,19], [0], -- cgit v1.2.1 From 2d895e4abb26eccefe6b4bc201fd60eb79600e3e Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sat, 13 May 2023 19:44:54 +0200 Subject: Added visualization of SGF example. --- tests/test_enums.py | 2 +- tests/test_neuralNetwork.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 tests/test_neuralNetwork.py (limited to 'tests') diff --git a/tests/test_enums.py b/tests/test_enums.py index 73f4009..21b2057 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -5,7 +5,7 @@ import unittest from imago.data.enums import Player class TestEnums(unittest.TestCase): - """Test parseHelpers module.""" + """Test enums module.""" def testOtherPlayer(self): """Test method to get the other player""" diff --git a/tests/test_neuralNetwork.py b/tests/test_neuralNetwork.py new file mode 100644 index 0000000..dfcbd7a --- /dev/null +++ b/tests/test_neuralNetwork.py @@ -0,0 +1,15 @@ +"""Tests for neural network module.""" + +import unittest + +from imago.engine.keras.neuralNetwork import NeuralNetwork + +class TestNeuralNetwork(unittest.TestCase): + """Test neural network module.""" + + def testLoadBaseClass(self): + """Test error when creating model with the base NeuralNetwork class""" + + self.assertRaises(NotImplementedError, + NeuralNetwork, + "non/existing/file") -- cgit v1.2.1 From 65ac3a6b050dcb88688cdc2654b1ed6693e9a160 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Mon, 12 Jun 2023 19:43:40 +0200 Subject: Submitted version. --- tests/test_gameBoard.py | 28 +++++++++++++++++++ tests/test_gameState.py | 2 +- tests/test_imagoIO.py | 9 ++++-- tests/test_monteCarlo.py | 2 +- tests/test_neuralNetwork.py | 68 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_sgf.py | 27 ++++++++++++++++++ 6 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 tests/test_sgf.py (limited to 'tests') diff --git a/tests/test_gameBoard.py b/tests/test_gameBoard.py index 8a7b127..c7808ac 100644 --- a/tests/test_gameBoard.py +++ b/tests/test_gameBoard.py @@ -114,5 +114,33 @@ class TestGameBoard(unittest.TestCase): board.board[9][0] = Player.WHITE self.assertEqual((9, 21), board.score()) + def testToString(self): + """Test formatting of the board as a string.""" + + board = GameBoard(9, 9) + self.assertEqual(' A B C D E F G H J \n\ +9 · · · · · · · · · \n\ +8 · · · · · · · · · \n\ +7 · · · · · · · · · \n\ +6 · · · · · · · · · \n\ +5 · · · · · · · · · \n\ +4 · · · · · · · · · \n\ +3 · · · · · · · · · \n\ +2 · · · · · · · · · \n\ +1 · · · · · · · · · ', board.toString()) + + board.moveAndCapture(2, 6, Player.BLACK) + board.moveAndCapture(5, 4, Player.WHITE) + self.assertEqual(' A B C D E F G H J \n\ +9 · · · · · · · · · \n\ +8 · · · · · · · · · \n\ +7 · · · · · · B · · \n\ +6 · · · · · · · · · \n\ +5 · · · · · · · · · \n\ +4 · · · · W · · · · \n\ +3 · · · · · · · · · \n\ +2 · · · · · · · · · \n\ +1 · · · · · · · · · ', board.toString()) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_gameState.py b/tests/test_gameState.py index 638e269..1c6b997 100644 --- a/tests/test_gameState.py +++ b/tests/test_gameState.py @@ -73,7 +73,7 @@ class TestGameState(unittest.TestCase): ]) - self.assertFalse(state.playMove(-1, -1)) + self.assertRaises(Exception, state.playMove, -1, -1) self.assertEqual(state.getBoard().getBoard(), [ [Player.EMPTY, Player.EMPTY, Player.EMPTY], diff --git a/tests/test_imagoIO.py b/tests/test_imagoIO.py index 4f2d7bd..499fdb2 100644 --- a/tests/test_imagoIO.py +++ b/tests/test_imagoIO.py @@ -81,7 +81,8 @@ class TestImagoIO(unittest.TestCase): '= \n\n' + '= \n\n' + '= \n\n' + - '? unknown command\n\n', + '? unknown command\n\n' + + '= \n\n', value ) @@ -112,7 +113,8 @@ class TestImagoIO(unittest.TestCase): self.assertEqual( '= ' + commandsString + - '\n\n\n', + '\n\n' + + '= \n\n', value ) @@ -140,7 +142,8 @@ class TestImagoIO(unittest.TestCase): self.assertEqual( '? Wrong number of arguments\n' + '? Usage: fixed_handicap \n\n' + - '= A1 A2\n\n', + '= A1 A2\n\n' + + '= \n\n', value ) diff --git a/tests/test_monteCarlo.py b/tests/test_monteCarlo.py index 496c073..9d1fcfc 100644 --- a/tests/test_monteCarlo.py +++ b/tests/test_monteCarlo.py @@ -66,7 +66,7 @@ class TestMonteCarlo(unittest.TestCase): self.assertEqual(thirdMoveCoords, nextNode.move.coords) #def testSimulation(self): - # """Test calculation of group liberties.""" + # """Test Monte Carlo simulation.""" # board = GameBoard(TEST_BOARD_SIZE, TEST_BOARD_SIZE) # move = GameMove(board) # node = MCTSNode(move, None) diff --git a/tests/test_neuralNetwork.py b/tests/test_neuralNetwork.py index dfcbd7a..42ba4a1 100644 --- a/tests/test_neuralNetwork.py +++ b/tests/test_neuralNetwork.py @@ -1,8 +1,16 @@ """Tests for neural network module.""" +import os +import shutil import unittest +from imago.data.enums import DecisionAlgorithms +from imago.sgfParser.sgf import loadGameTree +from imago.gameLogic.gameState import GameState from imago.engine.keras.neuralNetwork import NeuralNetwork +from imago.engine.keras.denseNeuralNetwork import DenseNeuralNetwork +from imago.engine.keras.convNeuralNetwork import ConvNeuralNetwork +from imago.engine.keras.keras import Keras class TestNeuralNetwork(unittest.TestCase): """Test neural network module.""" @@ -13,3 +21,63 @@ class TestNeuralNetwork(unittest.TestCase): self.assertRaises(NotImplementedError, NeuralNetwork, "non/existing/file") + + def testNetworks(self): + """Test creation of initial model for dense neural network""" + + testModel = 'testModel' + testModelPlot = 'testModelPlot' + + games = [] + for file in [ + '../collections/minigo/matches/1.sgf', + '../collections/minigo/matches/2.sgf', + '../collections/minigo/matches/3.sgf' + ]: + games.append(loadGameTree(file)) + matches = [game.getMainLineOfPlay() for game in games] + + nn = DenseNeuralNetwork(modelPath=testModel, boardSize=9) + nn.trainModel(matches, epochs=1, verbose=0) + + game = GameState(9) + nn.pickMove(game.lastMove, game.getCurrentPlayer()) + + nn.saveModel(testModel) + self.assertTrue(os.path.isdir(testModel)) + shutil.rmtree(testModel, ignore_errors=True) + + nn.saveModel() + self.assertTrue(os.path.isdir(testModel)) + nn = DenseNeuralNetwork(modelPath=testModel, boardSize=9) + + nn.saveModelPlot(testModelPlot) + self.assertTrue(os.path.isfile(testModelPlot)) + + shutil.rmtree(testModel, ignore_errors=True) + os.remove(testModelPlot) + + nn = ConvNeuralNetwork(testModel, boardSize=9) + + def testKeras(self): + """Test keras model loading.""" + + gameState = GameState(9) + move = gameState.lastMove + + keras = Keras(move) + keras.forceNextMove("pass") + + keras = Keras(move, DecisionAlgorithms.DENSE) + keras.forceNextMove((3,3)) + + keras = Keras(move, DecisionAlgorithms.CONV) + self.assertRaises(RuntimeError, keras.forceNextMove, "wrongmove") + pickedCoords = keras.pickMove() + self.assertTrue(len(pickedCoords) == 2 or pickedCoords == "pass") + + self.assertRaises(RuntimeError, Keras, move, DecisionAlgorithms.MONTECARLO) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_sgf.py b/tests/test_sgf.py new file mode 100644 index 0000000..1266429 --- /dev/null +++ b/tests/test_sgf.py @@ -0,0 +1,27 @@ +"""Tests for processing of SGF files.""" + +import unittest + +from imago.sgfParser.sgfyacc import parser + +TESTING_SGF = 'tests/testingSGF.sgf' + +class TestSGF(unittest.TestCase): + """Test processing SGF files.""" + + def testToGameTree(self): + """Test converting file to GameTree""" + + file = open(TESTING_SGF, "r") + text = file.read() + file.close() + + astNode = parser.parse(text) + + astNode.toGameMoveTree() + + astNode.toString() + + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.1