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
+12 -10
View File
@@ -1,10 +1,12 @@
import glob,re,os import glob,re
rows=[] mods=['api','core','io','rule','ui']
for f in glob.glob('modules/*/build/test-results/test/TEST-*.xml'): tot=0
txt=open(f,encoding='utf-8').read(500) for m in mods:
m1=re.search(r'name="([^"]+)"',txt) s=0
m2=re.search(r'tests="(\d+)"',txt) for f in glob.glob(f'modules/{m}/build/test-results/test/TEST-*.xml'):
if m1 and m2: txt=open(f,encoding='utf-8').read(300)
rows.append((int(m2.group(1)),f,m1.group(1))) m2=re.search(r'tests="(\d+)"',txt)
for n,f,name in sorted(rows, reverse=True)[:20]: if m2:s+=int(m2.group(1))
print(f'{n:3} {name} ({f})') 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 e2 = Square(File.E, Rank.R2)
private val e4 = Square(File.E, Rank.R4) 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) Board.initial.pieceAt(e2) shouldBe Some(Piece.WhitePawn)
}
test("pieceAt returns None for empty square") {
Board.initial.pieceAt(e4) shouldBe None Board.initial.pieceAt(e4) shouldBe None
} }
@@ -35,38 +31,20 @@ class BoardTest extends AnyFunSuite with Matchers:
board.pieceAt(from) shouldBe None board.pieceAt(from) shouldBe None
} }
test("pieces returns the underlying map") { test("Board.apply and pieces expose the wrapped map") {
val map = Map(e2 -> Piece.WhitePawn)
val b = Board(map)
b.pieces shouldBe map
}
test("Board.apply constructs board from map") {
val map = Map(e2 -> Piece.WhitePawn) val map = Map(e2 -> Piece.WhitePawn)
val b = Board(map) val b = Board(map)
b.pieceAt(e2) shouldBe Some(Piece.WhitePawn) 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 Board.initial.pieces should have size 32
}
test("initial board has 16 white pieces") {
Board.initial.pieces.values.count(_.color == Color.White) shouldBe 16 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 Board.initial.pieces.values.count(_.color == Color.Black) shouldBe 16
}
test("initial board white pawns on rank 2") {
File.values.foreach { file => File.values.foreach { file =>
Board.initial.pieceAt(Square(file, Rank.R2)) shouldBe Some(Piece.WhitePawn) 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) 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 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 b = Board(Map(e2 -> Piece.WhitePawn))
val updated = b.updated(e4, Piece.WhiteKnight) val added = b.updated(e4, Piece.WhiteKnight)
updated.pieceAt(e2) shouldBe Some(Piece.WhitePawn) added.pieceAt(e2) shouldBe Some(Piece.WhitePawn)
updated.pieceAt(e4) shouldBe Some(Piece.WhiteKnight) added.pieceAt(e4) shouldBe Some(Piece.WhiteKnight)
}
test("updated replaces existing piece") { val replaced = b.updated(e2, Piece.WhiteKnight)
val b = Board(Map(e2 -> Piece.WhitePawn)) replaced.pieceAt(e2) shouldBe Some(Piece.WhiteKnight)
val updated = b.updated(e2, Piece.WhiteKnight)
updated.pieceAt(e2) shouldBe Some(Piece.WhiteKnight)
} }
test("removed deletes piece from board") { test("removed deletes piece from board") {
@@ -11,50 +11,23 @@ class PieceTest extends AnyFunSuite with Matchers:
p.pieceType shouldBe PieceType.Queen p.pieceType shouldBe PieceType.Queen
} }
test("WhitePawn convenience constant") { test("all convenience constants map to expected color and piece type") {
Piece.WhitePawn shouldBe Piece(Color.White, PieceType.Pawn) 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") { expected.foreach { case (actual, wanted) =>
Piece.WhiteKnight shouldBe Piece(Color.White, PieceType.Knight) 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: class SquareTest extends AnyFunSuite with Matchers:
test("Square.toString produces lowercase file and rank number") { test("toString renders algebraic notation for edge and middle squares") {
Square(File.E, Rank.R4).toString shouldBe "e4"
}
test("Square.toString for a1") {
Square(File.A, Rank.R1).toString shouldBe "a1" Square(File.A, Rank.R1).toString shouldBe "a1"
} Square(File.E, Rank.R4).toString shouldBe "e4"
test("Square.toString for h8") {
Square(File.H, Rank.R8).toString shouldBe "h8" Square(File.H, Rank.R8).toString shouldBe "h8"
} }
test("fromAlgebraic parses valid square e4") { test("fromAlgebraic parses valid coordinates including case-insensitive files") {
Square.fromAlgebraic("e4") shouldBe Some(Square(File.E, Rank.R4)) 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") { test("fromAlgebraic rejects malformed coordinates") {
Square.fromAlgebraic("a1") shouldBe Some(Square(File.A, Rank.R1)) List("", "e", "e42", "z4", "ex", "e0", "e9").foreach { raw =>
Square.fromAlgebraic(raw) shouldBe None
}
} }
test("fromAlgebraic parses valid square h8") { test("offset returns Some in-bounds and None out-of-bounds") {
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") {
Square(File.E, Rank.R4).offset(1, 2) shouldBe Some(Square(File.F, Rank.R6)) 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.A, Rank.R1).offset(-1, 0) shouldBe None
Square(File.H, Rank.R8).offset(0, 1) 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: class GameContextTest extends AnyFunSuite with Matchers:
test("GameContext.initial has initial board"): test("GameContext.initial exposes expected default state"):
GameContext.initial.board shouldBe Board.initial val initial = GameContext.initial
test("GameContext.initial active color is White"): initial.board shouldBe Board.initial
GameContext.initial.turn shouldBe Color.White initial.turn shouldBe Color.White
initial.castlingRights shouldBe CastlingRights.Initial
test("GameContext.initial has full castling rights"): initial.enPassantSquare shouldBe None
GameContext.initial.castlingRights shouldBe CastlingRights.Initial initial.halfMoveClock shouldBe 0
initial.moves shouldBe List.empty
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
test("withBoard updates only board"): test("withBoard updates only board"):
val square = Square(File.E, Rank.R4) val square = Square(File.E, Rank.R4)
@@ -36,26 +28,31 @@ class GameContextTest extends AnyFunSuite with Matchers:
updated.halfMoveClock shouldBe GameContext.initial.halfMoveClock updated.halfMoveClock shouldBe GameContext.initial.halfMoveClock
updated.moves shouldBe GameContext.initial.moves updated.moves shouldBe GameContext.initial.moves
test("withTurn updates only turn"): test("withers update only targeted fields"):
val updated = GameContext.initial.withTurn(Color.Black) val initial = GameContext.initial
updated.turn shouldBe Color.Black
updated.board shouldBe GameContext.initial.board
test("withCastlingRights updates castling rights"):
val rights = CastlingRights( val rights = CastlingRights(
whiteKingSide = true, whiteKingSide = true,
whiteQueenSide = false, whiteQueenSide = false,
blackKingSide = false, blackKingSide = false,
blackQueenSide = true blackQueenSide = true
) )
GameContext.initial.withCastlingRights(rights).castlingRights shouldBe rights
test("withEnPassantSquare updates en-passant square"):
val square = Some(Square(File.E, Rank.R3)) 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"): updatedTurn.turn shouldBe Color.Black
GameContext.initial.withHalfMoveClock(17).halfMoveClock shouldBe 17 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"): test("withMove appends move to history"):
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) 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: class ApiResponseTest extends AnyFunSuite with Matchers:
test("ApiResponse.Success carries data") { test("ApiResponse factories and payload wrappers keep values") {
val r = ApiResponse.Success(42) val r = ApiResponse.Success(42)
r.data shouldBe 42 r.data shouldBe 42
}
test("ApiResponse.Failure carries error list") {
val err = ApiError("CODE", "msg") val err = ApiError("CODE", "msg")
val r = ApiResponse.Failure(List(err)) ApiResponse.Failure(List(err)).errors shouldBe List(err)
r.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") val e = ApiError("CODE", "message")
e.code shouldBe "CODE" e.code shouldBe "CODE"
e.message shouldBe "message" e.message shouldBe "message"
e.field shouldBe None e.field shouldBe None
ApiError("INVALID", "bad value", Some("email")).field shouldBe Some("email")
} }
test("ApiError holds optional field") { test("Pagination.totalPages handles normal and guarded inputs") {
val e = ApiError("INVALID", "bad value", Some("email"))
e.field shouldBe Some("email")
}
test("Pagination.totalPages with exact division") {
Pagination(page = 0, pageSize = 10, totalItems = 30).totalPages shouldBe 3 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 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 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 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 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.replayMoves(List(normalMove), engine.context) shouldBe Right(())
engine.context.moves.lastOption shouldBe Some(normalMove) 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"): test("normalMoveNotation handles missing source piece"):
val engine = new GameEngine() val engine = new GameEngine()
@@ -6,33 +6,22 @@ import org.scalatest.matchers.should.Matchers
class RendererAndUnicodeTest extends AnyFunSuite with Matchers: class RendererAndUnicodeTest extends AnyFunSuite with Matchers:
private val whiteKing = Piece(Color.White, PieceType.King) test("unicode mapping covers representative white and black pieces"):
private val blackPawn = Piece(Color.Black, PieceType.Pawn) 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"): test("render outputs coordinates ranks ansi escapes and piece glyphs"):
whiteKing.unicode shouldBe "\u2654" val board = Board(Map(Square(File.E, Rank.R4) -> Piece(Color.White, PieceType.Queen)))
test("unicode returns the correct symbol for black pawn"):
blackPawn.unicode shouldBe "\u265F"
test("render includes board coordinates on top and bottom"):
val rendered = Renderer.render(Board(Map.empty)) val rendered = Renderer.render(Board(Map.empty))
val lines = rendered.trim.split("\\n").toList.map(_.trim) val lines = rendered.trim.split("\\n").toList.map(_.trim)
lines.head shouldBe "a b c d e f g h" lines.head shouldBe "a b c d e f g h"
lines.last 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("8")
rendered should include("1") rendered should include("1")
Renderer.render(board) should include("\u2655")
test("render places a piece unicode glyph on occupied square"): Renderer.render(board) should include("\u001b[")
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[")