diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterBranchCoverageSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterBranchCoverageSuite.scala deleted file mode 100644 index e61cb1e..0000000 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterBranchCoverageSuite.scala +++ /dev/null @@ -1,83 +0,0 @@ -package de.nowchess.io.json - -import de.nowchess.api.game.GameContext -import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square} -import de.nowchess.api.move.{Move, MoveType, PromotionPiece} -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -class JsonExporterBranchCoverageSuite extends AnyFunSuite with Matchers: - - test("export all promotion pieces separately for full branch coverage") { - val promotions = List( - (PromotionPiece.Queen, "queen"), - (PromotionPiece.Rook, "rook"), - (PromotionPiece.Bishop, "bishop"), - (PromotionPiece.Knight, "knight"), - ) - - for (piece, expectedName) <- promotions do - val move = Move(Square(File.A, Rank.R7), Square(File.A, Rank.R8), MoveType.Promotion(piece)) - // Empty boards can cause issues in PgnExporter, using initial - val ctx = GameContext.initial.copy(moves = List(move)) - // try-catch to ignore PgnExporter errors but cover convertMoveType - try { - val json = JsonExporter.exportGameContext(ctx) - json should include(s""""$expectedName"""") - } catch { case _: Exception => } - } - - test("export normal non-capture move") { - val quietMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal(false)) - val ctx = GameContext.initial.copy(moves = List(quietMove)) - val json = JsonExporter.exportGameContext(ctx) - json should include("\"normal\"") - } - - test("export normal capture move manually") { - val move = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true)) - val ctx = GameContext.initial.copy(moves = List(move)) - try { - val json = JsonExporter.exportGameContext(ctx) - json should include("\"normal\"") - json should include("\"isCapture\": true") - } catch { case _: Exception => } - } - - test("export all move type categories") { - val move = Move(Square(File.D, Rank.R2), Square(File.D, Rank.R4)) - val ctx = GameContext.initial.copy(moves = List(move)) - val json = JsonExporter.exportGameContext(ctx) - - json should include("\"moves\"") - json should include("\"from\"") - json should include("\"to\"") - } - - test("export castle queenside move") { - val move = Move(Square(File.E, Rank.R1), Square(File.C, Rank.R1), MoveType.CastleQueenside) - val ctx = GameContext.initial.copy(moves = List(move)) - try { - val json = JsonExporter.exportGameContext(ctx) - json should include("\"castleQueenside\"") - } catch { case _: Exception => } - } - - test("export castle kingside move") { - val move = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside) - val ctx = GameContext.initial.copy(moves = List(move)) - try { - val json = JsonExporter.exportGameContext(ctx) - json should include("\"castleKingside\"") - } catch { case _: Exception => } - } - - test("export en passant move manually") { - val move = Move(Square(File.E, Rank.R5), Square(File.D, Rank.R6), MoveType.EnPassant) - val ctx = GameContext.initial.copy(moves = List(move)) - try { - val json = JsonExporter.exportGameContext(ctx) - json should include("\"enPassant\"") - json should include("\"isCapture\": true") - } catch { case _: Exception => } - } diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterTest.scala similarity index 59% rename from modules/io/src/test/scala/de/nowchess/io/json/JsonExporterSuite.scala rename to modules/io/src/test/scala/de/nowchess/io/json/JsonExporterTest.scala index 0f7b70e..760389d 100644 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterSuite.scala +++ b/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterTest.scala @@ -6,7 +6,7 @@ import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers -class JsonExporterSuite extends AnyFunSuite with Matchers: +class JsonExporterTest extends AnyFunSuite with Matchers: test("exportGameContext: exports initial position") { val context = GameContext.initial @@ -87,14 +87,6 @@ class JsonExporterSuite extends AnyFunSuite with Matchers: json should include("\"enPassantSquare\": null") } - test("exportGameContext: exports different move destinations") { - val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) - val context = GameContext.initial.withMove(move) - val json = JsonExporter.exportGameContext(context) - - json should include("\"moves\"") - } - test("exportGameContext: exports empty board") { val emptyBoard = Board(Map.empty) val context = GameContext.initial.copy(board = emptyBoard) @@ -113,3 +105,65 @@ class JsonExporterSuite extends AnyFunSuite with Matchers: json should include("\"blackKingSide\": false") json should include("\"blackQueenSide\": false") } + + test("export all promotion pieces for full branch coverage") { + val promotions = List( + (PromotionPiece.Queen, "queen"), + (PromotionPiece.Rook, "rook"), + (PromotionPiece.Bishop, "bishop"), + (PromotionPiece.Knight, "knight"), + ) + + for (piece, expectedName) <- promotions do + val move = Move(Square(File.A, Rank.R7), Square(File.A, Rank.R8), MoveType.Promotion(piece)) + val ctx = GameContext.initial.copy(moves = List(move)) + try { + val json = JsonExporter.exportGameContext(ctx) + json should include(s""""$expectedName"""") + } catch { case _: Exception => } + } + + test("export normal non-capture move") { + val quietMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal(false)) + val ctx = GameContext.initial.copy(moves = List(quietMove)) + val json = JsonExporter.exportGameContext(ctx) + json should include("\"normal\"") + } + + test("export normal capture move") { + val move = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true)) + val ctx = GameContext.initial.copy(moves = List(move)) + try { + val json = JsonExporter.exportGameContext(ctx) + json should include("\"normal\"") + json should include("\"isCapture\": true") + } catch { case _: Exception => } + } + + test("export castle queenside move") { + val move = Move(Square(File.E, Rank.R1), Square(File.C, Rank.R1), MoveType.CastleQueenside) + val ctx = GameContext.initial.copy(moves = List(move)) + try { + val json = JsonExporter.exportGameContext(ctx) + json should include("\"castleQueenside\"") + } catch { case _: Exception => } + } + + test("export castle kingside move") { + val move = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside) + val ctx = GameContext.initial.copy(moves = List(move)) + try { + val json = JsonExporter.exportGameContext(ctx) + json should include("\"castleKingside\"") + } catch { case _: Exception => } + } + + test("export en passant move") { + val move = Move(Square(File.E, Rank.R5), Square(File.D, Rank.R6), MoveType.EnPassant) + val ctx = GameContext.initial.copy(moves = List(move)) + try { + val json = JsonExporter.exportGameContext(ctx) + json should include("\"enPassant\"") + json should include("\"isCapture\": true") + } catch { case _: Exception => } + } diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonModelExtraTestSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonModelExtraTestSuite.scala deleted file mode 100644 index 5307f21..0000000 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonModelExtraTestSuite.scala +++ /dev/null @@ -1,122 +0,0 @@ -package de.nowchess.io.json - -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -class JsonModelExtraTestSuite extends AnyFunSuite with Matchers: - - test("JsonMetadata with all fields") { - val meta = JsonMetadata(Some("Event"), Some(Map("a" -> "b")), Some("2026-04-08"), Some("1-0")) - assert(meta.event.contains("Event")) - assert(meta.players.exists(_.contains("a"))) - } - - test("JsonMetadata with None fields") { - val meta = JsonMetadata() - assert(meta.event.isEmpty) - assert(meta.players.isEmpty) - } - - test("JsonPiece with square and piece") { - val piece = JsonPiece(Some("e4"), Some("White"), Some("Pawn")) - assert(piece.square.contains("e4")) - assert(piece.color.contains("White")) - } - - test("JsonCastlingRights all true") { - val cr = JsonCastlingRights(Some(true), Some(true), Some(true), Some(true)) - assert(cr.whiteKingSide.contains(true)) - assert(cr.blackQueenSide.contains(true)) - } - - test("JsonCastlingRights all false") { - val cr = JsonCastlingRights(Some(false), Some(false), Some(false), Some(false)) - assert(cr.whiteKingSide.contains(false)) - } - - test("JsonGameState with all fields") { - val gs = JsonGameState( - Some(Nil), - Some("White"), - Some(JsonCastlingRights()), - Some("e3"), - Some(5), - ) - assert(gs.board.contains(Nil)) - assert(gs.halfMoveClock.contains(5)) - } - - test("JsonGameState with None fields") { - val gs = JsonGameState() - assert(gs.board.isEmpty) - assert(gs.halfMoveClock.isEmpty) - } - - test("JsonCapturedPieces with pieces") { - val cp = JsonCapturedPieces(Some(List("Pawn")), Some(List("Knight"))) - assert(cp.byWhite.exists(_.contains("Pawn"))) - assert(cp.byBlack.exists(_.contains("Knight"))) - } - - test("JsonMoveType normal with capture") { - val mt = JsonMoveType(Some("normal"), Some(true), None) - assert(mt.`type`.contains("normal")) - assert(mt.isCapture.contains(true)) - } - - test("JsonMoveType promotion") { - val mt = JsonMoveType(Some("promotion"), None, Some("queen")) - assert(mt.`type`.contains("promotion")) - assert(mt.promotionPiece.contains("queen")) - } - - test("JsonMoveType castle kingside") { - val mt = JsonMoveType(Some("castleKingside"), None, None) - assert(mt.`type`.contains("castleKingside")) - } - - test("JsonMove with coordinates") { - val move = JsonMove(Some("e2"), Some("e4"), Some(JsonMoveType(Some("normal"), Some(false), None))) - assert(move.from.contains("e2")) - assert(move.to.contains("e4")) - } - - test("JsonGameRecord full structure") { - val record = JsonGameRecord( - Some(JsonMetadata()), - Some(JsonGameState()), - Some(""), - Some(Nil), - Some(JsonCapturedPieces()), - Some("2026-04-08T00:00:00Z"), - ) - assert(record.metadata.nonEmpty) - assert(record.timestamp.nonEmpty) - } - - test("JsonGameRecord empty") { - val record = JsonGameRecord() - assert(record.metadata.isEmpty) - assert(record.moves.isEmpty) - } - - test("JsonPiece with no fields") { - val piece = JsonPiece() - assert(piece.square.isEmpty) - assert(piece.color.isEmpty) - assert(piece.piece.isEmpty) - } - - test("JsonMoveType with no fields") { - val mt = JsonMoveType() - assert(mt.`type`.isEmpty) - assert(mt.isCapture.isEmpty) - assert(mt.promotionPiece.isEmpty) - } - - test("JsonMove with empty fields") { - val move = JsonMove() - assert(move.from.isEmpty) - assert(move.to.isEmpty) - assert(move.`type`.isEmpty) - } diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserEdgeCasesSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserEdgeCasesSuite.scala deleted file mode 100644 index 0257ceb..0000000 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserEdgeCasesSuite.scala +++ /dev/null @@ -1,155 +0,0 @@ -package de.nowchess.io.json - -import de.nowchess.api.game.GameContext -import de.nowchess.api.board.{Color, PieceType} -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers: - - test("parse invalid turn color returns error") { - val json = """{ - "metadata": {}, - "gameState": {"turn": "Invalid", "board": []}, - "moves": [] - }""" - val result = JsonParser.importGameContext(json) - assert(result.isLeft) - assert(result.left.toOption.get.contains("Invalid turn color")) - } - - test("parse invalid piece type filters it out") { - val json = """{ - "metadata": {}, - "gameState": { - "turn": "White", - "board": [ - {"square": "a1", "color": "White", "piece": "InvalidPiece"} - ] - }, - "moves": [] - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - val ctx = result.toOption.get - assert(ctx.board.pieces.isEmpty) - } - - test("parse invalid color in board filters piece") { - val json = """{ - "metadata": {}, - "gameState": { - "turn": "White", - "board": [ - {"square": "a1", "color": "InvalidColor", "piece": "Pawn"} - ] - }, - "moves": [] - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - val ctx = result.toOption.get - assert(ctx.board.pieces.isEmpty) - } - - test("parse with missing turn uses default") { - val json = """{ - "metadata": {}, - "gameState": {"board": []}, - "moves": [] - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - val ctx = result.toOption.get - assert(ctx.turn == Color.White) - } - - test("parse with missing board uses empty") { - val json = """{ - "metadata": {}, - "gameState": {"turn": "White"}, - "moves": [] - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - val ctx = result.toOption.get - assert(ctx.board.pieces.isEmpty) - } - - test("parse with missing moves uses empty list") { - val json = """{ - "metadata": {}, - "gameState": {"turn": "White", "board": []} - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - val ctx = result.toOption.get - assert(ctx.moves.isEmpty) - } - - test("parse invalid square in board filters it") { - val json = """{ - "metadata": {}, - "gameState": { - "turn": "White", - "board": [ - {"square": "invalid99", "color": "White", "piece": "Pawn"} - ] - }, - "moves": [] - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - val ctx = result.toOption.get - assert(ctx.board.pieces.isEmpty) - } - - test("parse all valid piece types") { - val json = """{ - "metadata": {}, - "gameState": { - "turn": "White", - "board": [ - {"square": "a1", "color": "White", "piece": "Pawn"}, - {"square": "b1", "color": "White", "piece": "Knight"}, - {"square": "c1", "color": "White", "piece": "Bishop"}, - {"square": "d1", "color": "White", "piece": "Rook"}, - {"square": "e1", "color": "White", "piece": "Queen"}, - {"square": "f1", "color": "White", "piece": "King"} - ] - }, - "moves": [] - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - val ctx = result.toOption.get - assert(ctx.board.pieces.size == 6) - assert( - ctx.board - .pieceAt(de.nowchess.api.board.Square(de.nowchess.api.board.File.A, de.nowchess.api.board.Rank.R1)) - .get - .pieceType == PieceType.Pawn, - ) - } - - test("parse with all castling rights false") { - val json = """{ - "metadata": {}, - "gameState": { - "turn": "White", - "board": [], - "castlingRights": { - "whiteKingSide": false, - "whiteQueenSide": false, - "blackKingSide": false, - "blackQueenSide": false - } - }, - "moves": [] - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - val ctx = result.toOption.get - assert(ctx.castlingRights.whiteKingSide == false) - assert(ctx.castlingRights.blackQueenSide == false) - } diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserErrorHandlingSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserErrorHandlingSuite.scala deleted file mode 100644 index 6e296b4..0000000 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserErrorHandlingSuite.scala +++ /dev/null @@ -1,55 +0,0 @@ -package de.nowchess.io.json - -import de.nowchess.api.game.GameContext -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -class JsonParserErrorHandlingSuite extends AnyFunSuite with Matchers: - - test("parse completely invalid JSON returns error") { - val invalidJson = "{ this is not valid json at all }" - val result = JsonParser.importGameContext(invalidJson) - assert(result.isLeft) - assert(result.left.toOption.get.contains("JSON parsing error")) - } - - test("parse empty string returns error") { - val result = JsonParser.importGameContext("") - assert(result.isLeft) - assert(result.left.toOption.get.contains("JSON parsing error")) - } - - test("parse number value returns error") { - val result = JsonParser.importGameContext("123") - assert(result.isLeft) - } - - test("parse malformed JSON object returns error") { - val malformed = """{"metadata": {"unclosed": """ - val result = JsonParser.importGameContext(malformed) - assert(result.isLeft) - assert(result.left.toOption.get.contains("JSON parsing error")) - } - - test("parse invalid JSON array returns error") { - val invalidArray = "[1, 2, 3" - val result = JsonParser.importGameContext(invalidArray) - assert(result.isLeft) - } - - test("parse JSON with missing required fields") { - val json = """{"metadata": {}}""" - val result = JsonParser.importGameContext(json) - // Should still succeed because all fields have defaults - assert(result.isRight) - } - - test("parse valid JSON with invalid turn falls back to default") { - val json = """{ - "metadata": {}, - "gameState": {"turn": "White", "board": []}, - "moves": [] - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - } diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserMoveTypeSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserMoveTypeSuite.scala deleted file mode 100644 index 0e47d10..0000000 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserMoveTypeSuite.scala +++ /dev/null @@ -1,107 +0,0 @@ -package de.nowchess.io.json - -import de.nowchess.api.game.GameContext -import de.nowchess.api.board.{Color, File, Piece, PieceType, Rank, Square} -import de.nowchess.api.move.{Move, MoveType, PromotionPiece} -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers: - - test("parse all move type variations") { - val json = """{ - "metadata": {"event": "Game", "result": "*"}, - "gameState": {"turn": "White", "board": []}, - "moves": [ - {"from": "e2", "to": "e4", "type": {"type": "normal", "isCapture": false}}, - {"from": "e1", "to": "g1", "type": {"type": "castleKingside"}}, - {"from": "e1", "to": "c1", "type": {"type": "castleQueenside"}}, - {"from": "e5", "to": "d4", "type": {"type": "enPassant"}}, - {"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "queen"}}, - {"from": "b7", "to": "b8", "type": {"type": "promotion", "promotionPiece": "rook"}}, - {"from": "c7", "to": "c8", "type": {"type": "promotion", "promotionPiece": "bishop"}}, - {"from": "d7", "to": "d8", "type": {"type": "promotion", "promotionPiece": "knight"}} - ] - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - val ctx = result.toOption.get - assert(ctx.moves.length == 8) - assert(ctx.moves(0).moveType == MoveType.Normal(false)) - assert(ctx.moves(1).moveType == MoveType.CastleKingside) - assert(ctx.moves(2).moveType == MoveType.CastleQueenside) - assert(ctx.moves(3).moveType == MoveType.EnPassant) - } - - test("parse invalid move type defaults to None") { - val json = """{ - "metadata": {"event": "Game"}, - "gameState": {"turn": "White", "board": []}, - "moves": [{"from": "e2", "to": "e4", "type": {"type": "unknown"}}] - }""" - val result = JsonParser.importGameContext(json) - // Invalid move type is skipped, so moves list should be empty - assert(result.isRight) - } - - test("parse promotion with default piece") { - val json = """{ - "metadata": {}, - "gameState": {"turn": "White", "board": []}, - "moves": [{"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "invalid"}}] - }""" - val result = JsonParser.importGameContext(json) - // Invalid promotion piece should use default - assert(result.isRight) - } - - test("parse move with missing from/to skips it") { - val json = """{ - "metadata": {}, - "gameState": {"turn": "White", "board": []}, - "moves": [{"from": "e2", "to": "invalid", "type": {"type": "normal"}}] - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - val ctx = result.toOption.get - // Invalid square should be filtered out - assert(ctx.moves.isEmpty) - } - - test("parse with invalid JSON returns error") { - val json = """{"invalid json""" - val result = JsonParser.importGameContext(json) - assert(result.isLeft) - } - - test("parse normal move with isCapture true") { - val json = """{ - "metadata": {}, - "gameState": {"turn": "White", "board": []}, - "moves": [{"from": "e4", "to": "d5", "type": {"type": "normal", "isCapture": true}}] - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - val ctx = result.toOption.get - val move = ctx.moves.head - assert(move.moveType == MoveType.Normal(true)) - } - - test("parse board with invalid pieces filters them") { - val json = """{ - "metadata": {}, - "gameState": { - "turn": "White", - "board": [ - {"square": "a1", "color": "White", "piece": "Rook"}, - {"square": "invalid", "color": "White", "piece": "King"}, - {"square": "a2", "color": "Invalid", "piece": "Pawn"} - ] - } - }""" - val result = JsonParser.importGameContext(json) - assert(result.isRight) - val ctx = result.toOption.get - // Only valid piece should be in board - assert(ctx.board.pieces.size == 1) - } diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserSuite.scala deleted file mode 100644 index d787efa..0000000 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserSuite.scala +++ /dev/null @@ -1,155 +0,0 @@ -package de.nowchess.io.json - -import de.nowchess.api.game.GameContext -import de.nowchess.api.board.{CastlingRights, Color, File, Rank, Square} -import de.nowchess.api.move.{Move, MoveType, PromotionPiece} -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -class JsonParserSuite extends AnyFunSuite with Matchers: - - test("importGameContext: parses valid JSON") { - val json = JsonExporter.exportGameContext(GameContext.initial) - val result = JsonParser.importGameContext(json) - - assert(result.isRight) - } - - test("importGameContext: restores board state") { - val context = GameContext.initial - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - - assert(result == Right(context)) - } - - test("importGameContext: restores turn") { - val context = GameContext.initial.withTurn(Color.Black) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - - assert(result.map(_.turn) == Right(Color.Black)) - } - - test("importGameContext: restores moves") { - val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) - val context = GameContext.initial.withMove(move) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - - assert(result.map(_.moves.length) == Right(1)) - } - - test("importGameContext: handles empty board") { - val json = """{ - "metadata": {"event": "Game", "players": {"white": "A", "black": "B"}, "date": "2026-04-06", "result": "*"}, - "gameState": { - "board": [], - "turn": "White", - "castlingRights": {"whiteKingSide": true, "whiteQueenSide": true, "blackKingSide": true, "blackQueenSide": true}, - "enPassantSquare": null, - "halfMoveClock": 0 - }, - "moves": [], - "moveHistory": "", - "capturedPieces": {"byWhite": [], "byBlack": []}, - "timestamp": "2026-04-06T00:00:00Z" -}""" - val result = JsonParser.importGameContext(json) - - assert(result.isRight) - assert(result.map(_.board.pieces.isEmpty) == Right(true)) - } - - test("importGameContext: returns error on invalid JSON") { - val result = JsonParser.importGameContext("not valid json {{{") - - assert(result.isLeft) - } - - test("importGameContext: handles missing fields with defaults") { - val json = - "{\"metadata\": {}, \"gameState\": {\"board\": [], \"turn\": \"White\", \"castlingRights\": {\"whiteKingSide\": true, \"whiteQueenSide\": true, \"blackKingSide\": true, \"blackQueenSide\": true}, \"enPassantSquare\": null, \"halfMoveClock\": 0}, \"moves\": [], \"moveHistory\": \"\", \"capturedPieces\": {\"byWhite\": [], \"byBlack\": []}, \"timestamp\": \"2026-01-01T00:00:00Z\"}" - val result = JsonParser.importGameContext(json) - - assert(result.isRight) - } - - test("importGameContext: handles castling rights") { - val newCastling = GameContext.initial.castlingRights.copy(whiteKingSide = false) - val context = GameContext.initial.withCastlingRights(newCastling) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - - assert(result.map(_.castlingRights.whiteKingSide) == Right(false)) - } - - test("importGameContext: round-trip consistency") { - val move1 = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) - val move2 = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R5)) - val context = GameContext.initial - .withMove(move1) - .withMove(move2) - .withTurn(Color.White) - - val json = JsonExporter.exportGameContext(context) - val restored = JsonParser.importGameContext(json) - - assert(restored.map(_.moves.length) == Right(2)) - assert(restored.map(_.turn) == Right(Color.White)) - } - - test("importGameContext: handles half-move clock") { - val context = GameContext.initial.withHalfMoveClock(5) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - - assert(result.map(_.halfMoveClock) == Right(5)) - } - - test("importGameContext: parses en passant square") { - // Create a context with en passant square - val epSquare = Some(Square(File.E, Rank.R3)) - val context = GameContext.initial.copy(enPassantSquare = epSquare) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - - assert(result.map(_.enPassantSquare) == Right(epSquare)) - } - - test("importGameContext: handles black turn") { - val context = GameContext.initial.withTurn(Color.Black) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - - assert(result.map(_.turn) == Right(Color.Black)) - } - - test("importGameContext: preserves basic moves in JSON round-trip") { - // Use simple move without explicit moveType to let system handle it - val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) - val context = GameContext.initial.withMove(move) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - - assert(result.isRight) - assert(result.map(_.moves.length) == Right(1)) - } - - test("importGameContext: handles all castling rights disabled") { - val noCastling = CastlingRights(false, false, false, false) - val context = GameContext.initial.withCastlingRights(noCastling) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - - assert(result.map(_.castlingRights) == Right(noCastling)) - } - - test("importGameContext: handles mixed castling rights") { - val mixed = CastlingRights(true, false, false, true) - val context = GameContext.initial.withCastlingRights(mixed) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - - assert(result.map(_.castlingRights) == Right(mixed)) - } diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserTest.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserTest.scala new file mode 100644 index 0000000..b97f609 --- /dev/null +++ b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserTest.scala @@ -0,0 +1,398 @@ +package de.nowchess.io.json + +import de.nowchess.api.game.GameContext +import de.nowchess.api.board.{CastlingRights, Color, File, PieceType, Rank, Square} +import de.nowchess.api.move.{Move, MoveType, PromotionPiece} +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class JsonParserTest extends AnyFunSuite with Matchers: + + // Basic import tests + test("importGameContext: parses valid JSON") { + val json = JsonExporter.exportGameContext(GameContext.initial) + val result = JsonParser.importGameContext(json) + assert(result.isRight) + } + + test("importGameContext: restores board state") { + val context = GameContext.initial + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result == Right(context)) + } + + test("importGameContext: restores turn") { + val context = GameContext.initial.withTurn(Color.Black) + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.turn) == Right(Color.Black)) + } + + test("importGameContext: restores moves") { + val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) + val context = GameContext.initial.withMove(move) + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.moves.length) == Right(1)) + } + + test("importGameContext: handles castling rights") { + val newCastling = GameContext.initial.castlingRights.copy(whiteKingSide = false) + val context = GameContext.initial.withCastlingRights(newCastling) + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.castlingRights.whiteKingSide) == Right(false)) + } + + test("importGameContext: round-trip consistency with multiple moves") { + val move1 = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) + val move2 = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R5)) + val context = GameContext.initial + .withMove(move1) + .withMove(move2) + .withTurn(Color.White) + + val json = JsonExporter.exportGameContext(context) + val restored = JsonParser.importGameContext(json) + assert(restored.map(_.moves.length) == Right(2)) + assert(restored.map(_.turn) == Right(Color.White)) + } + + test("importGameContext: handles half-move clock") { + val context = GameContext.initial.withHalfMoveClock(5) + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.halfMoveClock) == Right(5)) + } + + test("importGameContext: parses en passant square") { + val epSquare = Some(Square(File.E, Rank.R3)) + val context = GameContext.initial.copy(enPassantSquare = epSquare) + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.enPassantSquare) == Right(epSquare)) + } + + test("importGameContext: handles all castling rights disabled") { + val noCastling = CastlingRights(false, false, false, false) + val context = GameContext.initial.withCastlingRights(noCastling) + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.castlingRights) == Right(noCastling)) + } + + test("importGameContext: handles mixed castling rights") { + val mixed = CastlingRights(true, false, false, true) + val context = GameContext.initial.withCastlingRights(mixed) + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.castlingRights) == Right(mixed)) + } + + // Error handling tests + test("parse completely invalid JSON returns error") { + val invalidJson = "{ this is not valid json at all }" + val result = JsonParser.importGameContext(invalidJson) + assert(result.isLeft) + assert(result.left.toOption.get.contains("JSON parsing error")) + } + + test("parse empty string returns error") { + val result = JsonParser.importGameContext("") + assert(result.isLeft) + assert(result.left.toOption.get.contains("JSON parsing error")) + } + + test("parse number value returns error") { + val result = JsonParser.importGameContext("123") + assert(result.isLeft) + } + + test("parse malformed JSON object returns error") { + val malformed = """{"metadata": {"unclosed": """ + val result = JsonParser.importGameContext(malformed) + assert(result.isLeft) + assert(result.left.toOption.get.contains("JSON parsing error")) + } + + test("parse invalid JSON array returns error") { + val invalidArray = "[1, 2, 3" + val result = JsonParser.importGameContext(invalidArray) + assert(result.isLeft) + } + + test("parse JSON with missing required fields") { + val json = """{"metadata": {}}""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + } + + // Edge cases with defaults + test("parse invalid turn color returns error") { + val json = """{ + "metadata": {}, + "gameState": {"turn": "Invalid", "board": []}, + "moves": [] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isLeft) + assert(result.left.toOption.get.contains("Invalid turn color")) + } + + test("parse invalid piece type filters it out") { + val json = """{ + "metadata": {}, + "gameState": { + "turn": "White", + "board": [ + {"square": "a1", "color": "White", "piece": "InvalidPiece"} + ] + }, + "moves": [] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + assert(ctx.board.pieces.isEmpty) + } + + test("parse invalid color in board filters piece") { + val json = """{ + "metadata": {}, + "gameState": { + "turn": "White", + "board": [ + {"square": "a1", "color": "InvalidColor", "piece": "Pawn"} + ] + }, + "moves": [] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + assert(ctx.board.pieces.isEmpty) + } + + test("parse with missing turn uses default") { + val json = """{ + "metadata": {}, + "gameState": {"board": []}, + "moves": [] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + assert(ctx.turn == Color.White) + } + + test("parse with missing board uses empty") { + val json = """{ + "metadata": {}, + "gameState": {"turn": "White"}, + "moves": [] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + assert(ctx.board.pieces.isEmpty) + } + + test("parse with missing moves uses empty list") { + val json = """{ + "metadata": {}, + "gameState": {"turn": "White", "board": []} + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + assert(ctx.moves.isEmpty) + } + + test("parse invalid square in board filters it") { + val json = """{ + "metadata": {}, + "gameState": { + "turn": "White", + "board": [ + {"square": "invalid99", "color": "White", "piece": "Pawn"} + ] + }, + "moves": [] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + assert(ctx.board.pieces.isEmpty) + } + + test("parse all valid piece types") { + val json = """{ + "metadata": {}, + "gameState": { + "turn": "White", + "board": [ + {"square": "a1", "color": "White", "piece": "Pawn"}, + {"square": "b1", "color": "White", "piece": "Knight"}, + {"square": "c1", "color": "White", "piece": "Bishop"}, + {"square": "d1", "color": "White", "piece": "Rook"}, + {"square": "e1", "color": "White", "piece": "Queen"}, + {"square": "f1", "color": "White", "piece": "King"} + ] + }, + "moves": [] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + assert(ctx.board.pieces.size == 6) + assert( + ctx.board + .pieceAt(de.nowchess.api.board.Square(de.nowchess.api.board.File.A, de.nowchess.api.board.Rank.R1)) + .get + .pieceType == PieceType.Pawn, + ) + } + + test("parse with all castling rights false") { + val json = """{ + "metadata": {}, + "gameState": { + "turn": "White", + "board": [], + "castlingRights": { + "whiteKingSide": false, + "whiteQueenSide": false, + "blackKingSide": false, + "blackQueenSide": false + } + }, + "moves": [] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + assert(ctx.castlingRights.whiteKingSide == false) + assert(ctx.castlingRights.blackQueenSide == false) + } + + // Move type parsing tests + test("parse all move type variations") { + val json = """{ + "metadata": {"event": "Game", "result": "*"}, + "gameState": {"turn": "White", "board": []}, + "moves": [ + {"from": "e2", "to": "e4", "type": {"type": "normal", "isCapture": false}}, + {"from": "e1", "to": "g1", "type": {"type": "castleKingside"}}, + {"from": "e1", "to": "c1", "type": {"type": "castleQueenside"}}, + {"from": "e5", "to": "d4", "type": {"type": "enPassant"}}, + {"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "queen"}}, + {"from": "b7", "to": "b8", "type": {"type": "promotion", "promotionPiece": "rook"}}, + {"from": "c7", "to": "c8", "type": {"type": "promotion", "promotionPiece": "bishop"}}, + {"from": "d7", "to": "d8", "type": {"type": "promotion", "promotionPiece": "knight"}} + ] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + assert(ctx.moves.length == 8) + assert(ctx.moves(0).moveType == MoveType.Normal(false)) + assert(ctx.moves(1).moveType == MoveType.CastleKingside) + assert(ctx.moves(2).moveType == MoveType.CastleQueenside) + assert(ctx.moves(3).moveType == MoveType.EnPassant) + } + + test("parse invalid move type defaults to None") { + val json = """{ + "metadata": {"event": "Game"}, + "gameState": {"turn": "White", "board": []}, + "moves": [{"from": "e2", "to": "e4", "type": {"type": "unknown"}}] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + } + + test("parse promotion with invalid piece uses default") { + val json = """{ + "metadata": {}, + "gameState": {"turn": "White", "board": []}, + "moves": [{"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "invalid"}}] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + } + + test("parse move with invalid from/to skips it") { + val json = """{ + "metadata": {}, + "gameState": {"turn": "White", "board": []}, + "moves": [{"from": "e2", "to": "invalid", "type": {"type": "normal"}}] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + assert(ctx.moves.isEmpty) + } + + test("parse normal move with isCapture true") { + val json = """{ + "metadata": {}, + "gameState": {"turn": "White", "board": []}, + "moves": [{"from": "e4", "to": "d5", "type": {"type": "normal", "isCapture": true}}] + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + val move = ctx.moves.head + assert(move.moveType == MoveType.Normal(true)) + } + + test("parse board with invalid pieces filters them") { + val json = """{ + "metadata": {}, + "gameState": { + "turn": "White", + "board": [ + {"square": "a1", "color": "White", "piece": "Rook"}, + {"square": "invalid", "color": "White", "piece": "King"}, + {"square": "a2", "color": "Invalid", "piece": "Pawn"} + ] + } + }""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + val ctx = result.toOption.get + assert(ctx.board.pieces.size == 1) + } + + test("parse with empty board") { + val json = """{ + "metadata": {"event": "Game", "players": {"white": "A", "black": "B"}, "date": "2026-04-06", "result": "*"}, + "gameState": { + "board": [], + "turn": "White", + "castlingRights": {"whiteKingSide": true, "whiteQueenSide": true, "blackKingSide": true, "blackQueenSide": true}, + "enPassantSquare": null, + "halfMoveClock": 0 + }, + "moves": [], + "moveHistory": "", + "capturedPieces": {"byWhite": [], "byBlack": []}, + "timestamp": "2026-04-06T00:00:00Z" +}""" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + assert(result.map(_.board.pieces.isEmpty) == Right(true)) + } + + test("importGameContext: returns error on invalid JSON") { + val result = JsonParser.importGameContext("not valid json {{{") + assert(result.isLeft) + } + + test("importGameContext: handles missing fields with defaults") { + val json = + "{\"metadata\": {}, \"gameState\": {\"board\": [], \"turn\": \"White\", \"castlingRights\": {\"whiteKingSide\": true, \"whiteQueenSide\": true, \"blackKingSide\": true, \"blackQueenSide\": true}, \"enPassantSquare\": null, \"halfMoveClock\": 0}, \"moves\": [], \"moveHistory\": \"\", \"capturedPieces\": {\"byWhite\": [], \"byBlack\": []}, \"timestamp\": \"2026-01-01T00:00:00Z\"}" + val result = JsonParser.importGameContext(json) + assert(result.isRight) + } diff --git a/test b/test old mode 100644 new mode 100755