refactor(tests): streamline test cases for ApiResponse, Board, GameContext, Piece, and Square

This commit is contained in:
2026-04-05 22:10:13 +02:00
parent 3cec5b8898
commit 03c3b90d06
8 changed files with 96 additions and 227 deletions
+11 -9
View File
@@ -1,10 +1,12 @@
import glob,re,os
rows=[]
for f in glob.glob('modules/*/build/test-results/test/TEST-*.xml'):
txt=open(f,encoding='utf-8').read(500)
m1=re.search(r'name="([^"]+)"',txt)
import glob,re
mods=['api','core','io','rule','ui']
tot=0
for m in mods:
s=0
for f in glob.glob(f'modules/{m}/build/test-results/test/TEST-*.xml'):
txt=open(f,encoding='utf-8').read(300)
m2=re.search(r'tests="(\d+)"',txt)
if m1 and m2:
rows.append((int(m2.group(1)),f,m1.group(1)))
for n,f,name in sorted(rows, reverse=True)[:20]:
print(f'{n:3} {name} ({f})')
if m2:s+=int(m2.group(1))
print(f'{m}: {s}')
tot+=s
print('overall:',tot)
@@ -8,13 +8,9 @@ class BoardTest extends AnyFunSuite with Matchers:
private val e2 = Square(File.E, Rank.R2)
private val e4 = Square(File.E, Rank.R4)
private val d7 = Square(File.D, Rank.R7)
test("pieceAt returns Some for occupied square") {
test("pieceAt resolves occupied and empty squares") {
Board.initial.pieceAt(e2) shouldBe Some(Piece.WhitePawn)
}
test("pieceAt returns None for empty square") {
Board.initial.pieceAt(e4) shouldBe None
}
@@ -35,38 +31,20 @@ class BoardTest extends AnyFunSuite with Matchers:
board.pieceAt(from) shouldBe None
}
test("pieces returns the underlying map") {
val map = Map(e2 -> Piece.WhitePawn)
val b = Board(map)
b.pieces shouldBe map
}
test("Board.apply constructs board from map") {
test("Board.apply and pieces expose the wrapped map") {
val map = Map(e2 -> Piece.WhitePawn)
val b = Board(map)
b.pieceAt(e2) shouldBe Some(Piece.WhitePawn)
b.pieces shouldBe map
}
test("initial board has 32 pieces") {
test("initial board has expected material and pawn placement") {
Board.initial.pieces should have size 32
}
test("initial board has 16 white pieces") {
Board.initial.pieces.values.count(_.color == Color.White) shouldBe 16
}
test("initial board has 16 black pieces") {
Board.initial.pieces.values.count(_.color == Color.Black) shouldBe 16
}
test("initial board white pawns on rank 2") {
File.values.foreach { file =>
Board.initial.pieceAt(Square(file, Rank.R2)) shouldBe Some(Piece.WhitePawn)
}
}
test("initial board black pawns on rank 7") {
File.values.foreach { file =>
Board.initial.pieceAt(Square(file, Rank.R7)) shouldBe Some(Piece.BlackPawn)
}
}
@@ -102,17 +80,14 @@ class BoardTest extends AnyFunSuite with Matchers:
Board.initial.pieceAt(Square(file, rank)) shouldBe None
}
test("updated adds or replaces piece at square") {
test("updated adds and replaces piece at squares") {
val b = Board(Map(e2 -> Piece.WhitePawn))
val updated = b.updated(e4, Piece.WhiteKnight)
updated.pieceAt(e2) shouldBe Some(Piece.WhitePawn)
updated.pieceAt(e4) shouldBe Some(Piece.WhiteKnight)
}
val added = b.updated(e4, Piece.WhiteKnight)
added.pieceAt(e2) shouldBe Some(Piece.WhitePawn)
added.pieceAt(e4) shouldBe Some(Piece.WhiteKnight)
test("updated replaces existing piece") {
val b = Board(Map(e2 -> Piece.WhitePawn))
val updated = b.updated(e2, Piece.WhiteKnight)
updated.pieceAt(e2) shouldBe Some(Piece.WhiteKnight)
val replaced = b.updated(e2, Piece.WhiteKnight)
replaced.pieceAt(e2) shouldBe Some(Piece.WhiteKnight)
}
test("removed deletes piece from board") {
@@ -11,50 +11,23 @@ class PieceTest extends AnyFunSuite with Matchers:
p.pieceType shouldBe PieceType.Queen
}
test("WhitePawn convenience constant") {
Piece.WhitePawn shouldBe Piece(Color.White, PieceType.Pawn)
}
test("all convenience constants map to expected color and piece type") {
val expected = List(
Piece.WhitePawn -> Piece(Color.White, PieceType.Pawn),
Piece.WhiteKnight -> Piece(Color.White, PieceType.Knight),
Piece.WhiteBishop -> Piece(Color.White, PieceType.Bishop),
Piece.WhiteRook -> Piece(Color.White, PieceType.Rook),
Piece.WhiteQueen -> Piece(Color.White, PieceType.Queen),
Piece.WhiteKing -> Piece(Color.White, PieceType.King),
Piece.BlackPawn -> Piece(Color.Black, PieceType.Pawn),
Piece.BlackKnight -> Piece(Color.Black, PieceType.Knight),
Piece.BlackBishop -> Piece(Color.Black, PieceType.Bishop),
Piece.BlackRook -> Piece(Color.Black, PieceType.Rook),
Piece.BlackQueen -> Piece(Color.Black, PieceType.Queen),
Piece.BlackKing -> Piece(Color.Black, PieceType.King)
)
test("WhiteKnight convenience constant") {
Piece.WhiteKnight shouldBe Piece(Color.White, PieceType.Knight)
expected.foreach { case (actual, wanted) =>
actual shouldBe wanted
}
test("WhiteBishop convenience constant") {
Piece.WhiteBishop shouldBe Piece(Color.White, PieceType.Bishop)
}
test("WhiteRook convenience constant") {
Piece.WhiteRook shouldBe Piece(Color.White, PieceType.Rook)
}
test("WhiteQueen convenience constant") {
Piece.WhiteQueen shouldBe Piece(Color.White, PieceType.Queen)
}
test("WhiteKing convenience constant") {
Piece.WhiteKing shouldBe Piece(Color.White, PieceType.King)
}
test("BlackPawn convenience constant") {
Piece.BlackPawn shouldBe Piece(Color.Black, PieceType.Pawn)
}
test("BlackKnight convenience constant") {
Piece.BlackKnight shouldBe Piece(Color.Black, PieceType.Knight)
}
test("BlackBishop convenience constant") {
Piece.BlackBishop shouldBe Piece(Color.Black, PieceType.Bishop)
}
test("BlackRook convenience constant") {
Piece.BlackRook shouldBe Piece(Color.Black, PieceType.Rook)
}
test("BlackQueen convenience constant") {
Piece.BlackQueen shouldBe Piece(Color.Black, PieceType.Queen)
}
test("BlackKing convenience constant") {
Piece.BlackKing shouldBe Piece(Color.Black, PieceType.King)
}
@@ -5,67 +5,32 @@ import org.scalatest.matchers.should.Matchers
class SquareTest extends AnyFunSuite with Matchers:
test("Square.toString produces lowercase file and rank number") {
Square(File.E, Rank.R4).toString shouldBe "e4"
}
test("Square.toString for a1") {
test("toString renders algebraic notation for edge and middle squares") {
Square(File.A, Rank.R1).toString shouldBe "a1"
}
test("Square.toString for h8") {
Square(File.E, Rank.R4).toString shouldBe "e4"
Square(File.H, Rank.R8).toString shouldBe "h8"
}
test("fromAlgebraic parses valid square e4") {
Square.fromAlgebraic("e4") shouldBe Some(Square(File.E, Rank.R4))
test("fromAlgebraic parses valid coordinates including case-insensitive files") {
val expected = List(
"a1" -> Square(File.A, Rank.R1),
"e4" -> Square(File.E, Rank.R4),
"h8" -> Square(File.H, Rank.R8),
"E4" -> Square(File.E, Rank.R4)
)
expected.foreach { case (raw, sq) =>
Square.fromAlgebraic(raw) shouldBe Some(sq)
}
}
test("fromAlgebraic parses valid square a1") {
Square.fromAlgebraic("a1") shouldBe Some(Square(File.A, Rank.R1))
test("fromAlgebraic rejects malformed coordinates") {
List("", "e", "e42", "z4", "ex", "e0", "e9").foreach { raw =>
Square.fromAlgebraic(raw) shouldBe None
}
}
test("fromAlgebraic parses valid square h8") {
Square.fromAlgebraic("h8") shouldBe Some(Square(File.H, Rank.R8))
}
test("fromAlgebraic is case-insensitive for file") {
Square.fromAlgebraic("E4") shouldBe Some(Square(File.E, Rank.R4))
}
test("fromAlgebraic returns None for empty string") {
Square.fromAlgebraic("") shouldBe None
}
test("fromAlgebraic returns None for string too short") {
Square.fromAlgebraic("e") shouldBe None
}
test("fromAlgebraic returns None for string too long") {
Square.fromAlgebraic("e42") shouldBe None
}
test("fromAlgebraic returns None for invalid file character") {
Square.fromAlgebraic("z4") shouldBe None
}
test("fromAlgebraic returns None for non-digit rank") {
Square.fromAlgebraic("ex") shouldBe None
}
test("fromAlgebraic returns None for rank 0") {
Square.fromAlgebraic("e0") shouldBe None
}
test("fromAlgebraic returns None for rank 9") {
Square.fromAlgebraic("e9") shouldBe None
}
test("offset returns target square for in-bounds delta") {
test("offset returns Some in-bounds and None out-of-bounds") {
Square(File.E, Rank.R4).offset(1, 2) shouldBe Some(Square(File.F, Rank.R6))
}
test("offset returns None for out-of-bounds delta") {
Square(File.A, Rank.R1).offset(-1, 0) shouldBe None
Square(File.H, Rank.R8).offset(0, 1) shouldBe None
}
@@ -7,23 +7,15 @@ import org.scalatest.matchers.should.Matchers
class GameContextTest extends AnyFunSuite with Matchers:
test("GameContext.initial has initial board"):
GameContext.initial.board shouldBe Board.initial
test("GameContext.initial exposes expected default state"):
val initial = GameContext.initial
test("GameContext.initial active color is White"):
GameContext.initial.turn shouldBe Color.White
test("GameContext.initial has full castling rights"):
GameContext.initial.castlingRights shouldBe CastlingRights.Initial
test("GameContext.initial en-passant square is None"):
GameContext.initial.enPassantSquare shouldBe None
test("GameContext.initial half-move clock is 0"):
GameContext.initial.halfMoveClock shouldBe 0
test("GameContext.initial move history is empty"):
GameContext.initial.moves shouldBe List.empty
initial.board shouldBe Board.initial
initial.turn shouldBe Color.White
initial.castlingRights shouldBe CastlingRights.Initial
initial.enPassantSquare shouldBe None
initial.halfMoveClock shouldBe 0
initial.moves shouldBe List.empty
test("withBoard updates only board"):
val square = Square(File.E, Rank.R4)
@@ -36,26 +28,31 @@ class GameContextTest extends AnyFunSuite with Matchers:
updated.halfMoveClock shouldBe GameContext.initial.halfMoveClock
updated.moves shouldBe GameContext.initial.moves
test("withTurn updates only turn"):
val updated = GameContext.initial.withTurn(Color.Black)
updated.turn shouldBe Color.Black
updated.board shouldBe GameContext.initial.board
test("withCastlingRights updates castling rights"):
test("withers update only targeted fields"):
val initial = GameContext.initial
val rights = CastlingRights(
whiteKingSide = true,
whiteQueenSide = false,
blackKingSide = false,
blackQueenSide = true
)
GameContext.initial.withCastlingRights(rights).castlingRights shouldBe rights
test("withEnPassantSquare updates en-passant square"):
val square = Some(Square(File.E, Rank.R3))
GameContext.initial.withEnPassantSquare(square).enPassantSquare shouldBe square
val updatedTurn = initial.withTurn(Color.Black)
val updatedRights = initial.withCastlingRights(rights)
val updatedEp = initial.withEnPassantSquare(square)
val updatedClock = initial.withHalfMoveClock(17)
test("withHalfMoveClock updates half-move clock"):
GameContext.initial.withHalfMoveClock(17).halfMoveClock shouldBe 17
updatedTurn.turn shouldBe Color.Black
updatedTurn.board shouldBe initial.board
updatedRights.castlingRights shouldBe rights
updatedRights.turn shouldBe initial.turn
updatedEp.enPassantSquare shouldBe square
updatedEp.castlingRights shouldBe initial.castlingRights
updatedClock.halfMoveClock shouldBe 17
updatedClock.moves shouldBe initial.moves
test("withMove appends move to history"):
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
@@ -5,52 +5,26 @@ import org.scalatest.matchers.should.Matchers
class ApiResponseTest extends AnyFunSuite with Matchers:
test("ApiResponse.Success carries data") {
test("ApiResponse factories and payload wrappers keep values") {
val r = ApiResponse.Success(42)
r.data shouldBe 42
}
test("ApiResponse.Failure carries error list") {
val err = ApiError("CODE", "msg")
val r = ApiResponse.Failure(List(err))
r.errors shouldBe List(err)
}
ApiResponse.Failure(List(err)).errors shouldBe List(err)
ApiResponse.error(err) shouldBe ApiResponse.Failure(List(err))
test("ApiResponse.error creates single-error Failure") {
val err = ApiError("NOT_FOUND", "not found")
val f = ApiResponse.error(err)
f shouldBe ApiResponse.Failure(List(err))
}
test("ApiError holds code and message") {
val e = ApiError("CODE", "message")
e.code shouldBe "CODE"
e.message shouldBe "message"
e.field shouldBe None
ApiError("INVALID", "bad value", Some("email")).field shouldBe Some("email")
}
test("ApiError holds optional field") {
val e = ApiError("INVALID", "bad value", Some("email"))
e.field shouldBe Some("email")
}
test("Pagination.totalPages with exact division") {
test("Pagination.totalPages handles normal and guarded inputs") {
Pagination(page = 0, pageSize = 10, totalItems = 30).totalPages shouldBe 3
}
test("Pagination.totalPages rounds up") {
Pagination(page = 0, pageSize = 10, totalItems = 25).totalPages shouldBe 3
}
test("Pagination.totalPages is 0 when totalItems is 0") {
Pagination(page = 0, pageSize = 10, totalItems = 0).totalPages shouldBe 0
}
test("Pagination.totalPages is 0 when pageSize is 0") {
Pagination(page = 0, pageSize = 0, totalItems = 100).totalPages shouldBe 0
}
test("Pagination.totalPages is 0 when pageSize is negative") {
Pagination(page = 0, pageSize = -1, totalItems = 100).totalPages shouldBe 0
}
@@ -141,12 +141,6 @@ class GameEngineCoverageRegressionTest extends AnyFunSuite with Matchers:
engine.replayMoves(List(normalMove), engine.context) shouldBe Right(())
engine.context.moves.lastOption shouldBe Some(normalMove)
test("loadGame replay will stop on errors"):
val normalMove = Move(sq("e2"), sq("e4"), MoveType.Normal())
val engine = new GameEngine()
engine.replayMoves(List(normalMove), engine.context) shouldBe Right(())
engine.context.moves.lastOption shouldBe Some(normalMove)
test("normalMoveNotation handles missing source piece"):
val engine = new GameEngine()
@@ -6,33 +6,22 @@ import org.scalatest.matchers.should.Matchers
class RendererAndUnicodeTest extends AnyFunSuite with Matchers:
private val whiteKing = Piece(Color.White, PieceType.King)
private val blackPawn = Piece(Color.Black, PieceType.Pawn)
test("unicode mapping covers representative white and black pieces"):
Piece(Color.White, PieceType.King).unicode shouldBe "\u2654"
Piece(Color.White, PieceType.Queen).unicode shouldBe "\u2655"
Piece(Color.Black, PieceType.King).unicode shouldBe "\u265A"
Piece(Color.Black, PieceType.Pawn).unicode shouldBe "\u265F"
test("unicode returns the correct symbol for white king"):
whiteKing.unicode shouldBe "\u2654"
test("unicode returns the correct symbol for black pawn"):
blackPawn.unicode shouldBe "\u265F"
test("render includes board coordinates on top and bottom"):
test("render outputs coordinates ranks ansi escapes and piece glyphs"):
val board = Board(Map(Square(File.E, Rank.R4) -> Piece(Color.White, PieceType.Queen)))
val rendered = Renderer.render(Board(Map.empty))
val lines = rendered.trim.split("\\n").toList.map(_.trim)
lines.head shouldBe "a b c d e f g h"
lines.last shouldBe "a b c d e f g h"
test("render includes rank labels from 8 down to 1"):
val rendered = Renderer.render(Board(Map.empty))
rendered should include("8")
rendered should include("1")
test("render places a piece unicode glyph on occupied square"):
val board = Board(Map(Square(File.E, Rank.R4) -> Piece(Color.White, PieceType.Queen)))
val rendered = Renderer.render(board)
rendered should include("\u2655")
rendered should include("\u001b[")
Renderer.render(board) should include("\u2655")
Renderer.render(board) should include("\u001b[")