refactor(tests): enhance test coverage for move application and piece movement logic

This commit is contained in:
2026-04-05 22:03:40 +02:00
parent 4cf39e3e97
commit 3cec5b8898
14 changed files with 773 additions and 97 deletions
@@ -0,0 +1,101 @@
package de.nowchess.io
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.io.fen.{FenExporter, FenParser}
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class IoCoverageRegressionTest extends AnyFunSuite with Matchers:
private def sq(alg: String): Square =
Square.fromAlgebraic(alg).getOrElse(fail(s"Invalid square in test: $alg"))
test("FenParser rejects malformed board shapes and invalid piece symbols"):
FenParser.parseBoard("8/8/8/8/8/8/8") shouldBe None
FenParser.parseBoard("9/8/8/8/8/8/8/8") shouldBe None
FenParser.parseBoard("8p/8/8/8/8/8/8/8") shouldBe None
FenParser.parseBoard("7/8/8/8/8/8/8/8") shouldBe None
FenParser.parseBoard("8/8/8/8/8/8/8/7X") shouldBe None
test("FenExporter exportGameContext forwards to gameContextToFen"):
val context = GameContext.initial
FenExporter.exportGameContext(context) shouldBe FenExporter.gameContextToFen(context)
test("PgnParser rejects too-short notation and invalid piece letters"):
val initial = GameContext.initial
PgnParser.parseAlgebraicMove("e", initial, Color.White) shouldBe None
PgnParser.parseAlgebraicMove("Xe5", initial, Color.White) shouldBe None
test("PgnParser rejects notation with invalid promotion piece"):
val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").getOrElse(fail("valid board expected"))
val context = GameContext.initial.withBoard(board)
PgnParser.parseAlgebraicMove("e7e8=X", context, Color.White) shouldBe None
test("PgnExporter emits notation for all normal piece types and captures"):
val moves = List(
Move(sq("e2"), sq("e4")),
Move(sq("a7"), sq("a6")),
Move(sq("g1"), sq("f3")),
Move(sq("b7"), sq("b6")),
Move(sq("f1"), sq("b5"), MoveType.Normal(true)),
Move(sq("g8"), sq("f6")),
Move(sq("a1"), sq("a8"), MoveType.Normal(true)),
Move(sq("c7"), sq("c6")),
Move(sq("d1"), sq("d7"), MoveType.Normal(true)),
Move(sq("d8"), sq("d7"), MoveType.Normal(true)),
Move(sq("e1"), sq("e2"), MoveType.Normal(true))
)
val pgn = PgnExporter.exportGame(Map("Result" -> "*"), moves)
pgn should include("e4")
pgn should include("Nf3")
pgn should include("Bxb5")
pgn should include("Rxa8")
pgn should include("Qxd7")
pgn should include("Kxe2")
test("PgnExporter emits en-passant and promotion capture notation"):
val enPassant = Move(sq("e2"), sq("d3"), MoveType.EnPassant)
val promotionCapture = Move(sq("e7"), sq("f8"), MoveType.Promotion(PromotionPiece.Queen))
val pawnCapture = Move(sq("e2"), sq("d3"), MoveType.Normal(isCapture = true))
val promotionQuietSetup = Move(sq("e8"), sq("e7"))
val promotionQuiet = Move(sq("e2"), sq("e8"), MoveType.Promotion(PromotionPiece.Queen))
val pgn = PgnExporter.exportGame(Map.empty, List(enPassant, promotionCapture))
val pawnCapturePgn = PgnExporter.exportGame(Map.empty, List(pawnCapture))
val quietPromotionPgn = PgnExporter.exportGame(Map.empty, List(promotionQuietSetup, promotionQuiet))
pgn should include("exd3")
pgn should include("exf8=Q")
pawnCapturePgn should include("exd3")
quietPromotionPgn should include("e8=Q")
test("PgnExporter emits all promotion suffixes"):
val promotions = List(
Move(sq("e2"), sq("e1"), MoveType.Promotion(PromotionPiece.Queen)),
Move(sq("e2"), sq("e1"), MoveType.Promotion(PromotionPiece.Rook)),
Move(sq("e2"), sq("e1"), MoveType.Promotion(PromotionPiece.Bishop)),
Move(sq("e2"), sq("e1"), MoveType.Promotion(PromotionPiece.Knight))
)
val pgn = PgnExporter.exportGame(Map.empty, promotions)
pgn should include("=Q")
pgn should include("=R")
pgn should include("=B")
pgn should include("=N")
test("PgnParser parsePgn silently skips unknown tokens"):
val parsed = PgnParser.parsePgn("1. e4 ??? e5")
parsed.map(_.moves.size) shouldBe Some(2)
@@ -167,3 +167,28 @@ class PgnParserTest extends AnyFunSuite with Matchers:
val result = PgnParser.importGameContext(invalidPgn)
// Empty PGN is still valid (no moves), so check for reasonable parsing
result.isRight shouldBe true
test("parseAlgebraicMove: uppercase file token still fails when destination is unreachable"):
val result = PgnParser.parseAlgebraicMove("E5", GameContext.initial, Color.White)
result shouldBe None
test("parseAlgebraicMove: non-file/rank hint characters are ignored"):
val result = PgnParser.parseAlgebraicMove("N?f3", GameContext.initial, Color.White)
result.isDefined shouldBe true
result.get.to shouldBe Square(File.F, Rank.R3)
test("extractPromotion returns None for unsupported promotion letter"):
PgnParser.extractPromotion("e7e8=X") shouldBe None
test("parseAlgebraicMove rejects promotion target without promotion suffix"):
val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get
val result = PgnParser.parseAlgebraicMove("e8", GameContext.initial.withBoard(board), Color.White)
result shouldBe None
test("parseAlgebraicMove: king notation resolves a legal king move"):
val board = FenParser.parseBoard("4k3/8/8/8/8/8/8/4K3").get
val result = PgnParser.parseAlgebraicMove("Ke2", GameContext.initial.withBoard(board), Color.White)
result.isDefined shouldBe true
result.get.from shouldBe Square(File.E, Rank.R1)
result.get.to shouldBe Square(File.E, Rank.R2)