Use a PairingListener class to collect or print weights, avoid computing twice the weights during tests

This commit is contained in:
Claude Brisson
2025-07-24 15:05:51 +02:00
parent f704f3adb2
commit 3d06588889
10 changed files with 204 additions and 97 deletions

View File

@@ -20,24 +20,22 @@ class BOSP2024Test: TestBase() {
)!!.asObject()
val resp = TestAPI.post("/api/tour", tournament).asObject()
val tourId = resp.getInt("id")
Solver.weightsLogger = PrintWriter(FileWriter(getOutputFile("bosp2024-weights.txt")))
Solver.legacy_mode = true
TestAPI.post("/api/tour/$tourId/pair/3", Json.Array("all")).asArray()
val outputFile = getOutputFile("bosp2024-weights.txt")
TestAPI.post("/api/tour/$tourId/pair/3?legacy=true&weights_output=$outputFile", Json.Array("all")).asArray()
// compare weights
assertTrue(compare_weights(getOutputFile("bosp2024-weights.txt"), getTestFile("opengotha/bosp2024/bosp2024_weights_R3.txt")), "Not matching opengotha weights for BOSP test")
assertTrue(compare_weights(outputFile, getTestFile("opengotha/bosp2024/bosp2024_weights_R3.txt")), "Not matching opengotha weights for BOSP test")
TestAPI.delete("/api/tour/$tourId/pair/3", Json.Array("all"))
Solver.legacy_mode = false
val games = TestAPI.post("/api/tour/$tourId/pair/3", Json.Array("all")).asArray()
// Aksut Husrev is ID 18
val solved = games.map { it as Json.Object }.filter { game ->
val solved = games.map { it as Json.Object }.firstOrNull { game ->
// build the two-elements set of players ids
val players = game.entries.filter { (k, v) -> k == "b" || k == "w" }.map { (k, v) -> (v as Number).toInt() }.toSet()
val players =
game.entries.filter { (k, v) -> k == "b" || k == "w" }.map { (k, v) -> (v as Number).toInt() }.toSet()
// keep game with Aksut Husrev
players.contains(18)
}.firstOrNull()
}
assertNotNull(solved)

View File

@@ -1,11 +1,8 @@
package org.jeudego.pairgoth.test
import com.republicate.kson.Json
import org.jeudego.pairgoth.pairing.solver.Solver
import org.jeudego.pairgoth.test.PairingTests.Companion.compare_weights
import org.junit.jupiter.api.Test
import java.io.FileWriter
import java.io.PrintWriter
import java.nio.charset.StandardCharsets
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@@ -19,8 +16,8 @@ class MalavasiTest: TestBase() {
)!!.asObject()
val resp = TestAPI.post("/api/tour", tournament).asObject()
val tourId = resp.getInt("id")
Solver.weightsLogger = PrintWriter(FileWriter(getOutputFile("malavasi-weights.txt")))
val games = TestAPI.post("/api/tour/$tourId/pair/2", Json.Array("all")).asArray()
val outputFile = getOutputFile("malavasi-weights.txt")
val games = TestAPI.post("/api/tour/$tourId/pair/2?weights_output=$outputFile", Json.Array("all")).asArray()
// Oceane is ID 548, Valentine 549
val buggy = games.map { it as Json.Object }.filter { game ->
// build the two-elements set of players ids
@@ -33,6 +30,6 @@ class MalavasiTest: TestBase() {
assertEquals(2, buggy.size)
// compare weights
assertTrue(compare_weights(getOutputFile("malavasi-weights.txt"), getTestFile("opengotha/malavasi/malavasi_weights_R2.txt")), "Not matching opengotha weights for Malavasi test")
assertTrue(compare_weights(outputFile, getTestFile("opengotha/malavasi/malavasi_weights_R2.txt")), "Not matching opengotha weights for Malavasi test")
}
}

View File

@@ -8,8 +8,6 @@ import org.jeudego.pairgoth.store.lastPlayerId
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.io.File
import java.io.FileWriter
import java.io.PrintWriter
import java.nio.charset.StandardCharsets
import java.text.DecimalFormat
import kotlin.math.abs
@@ -59,7 +57,6 @@ class PairingTests: TestBase() {
}
fun compare_weights(file1: File, file2: File, skipSeeding: Boolean = false):Boolean {
Solver.weightsLogger!!.flush()
// Maps to store name pairs and costs
val map1 = create_weights_map(file1)
val map2 = create_weights_map(file2)
@@ -165,6 +162,7 @@ class PairingTests: TestBase() {
}
fun test_from_XML(name: String, forcePairing:List<Int>) {
// Let pairgoth use the legacy asymmetric detRandom()
test_from_XML_internal(name, forcePairing, true)
// Non-legacy tests inhibited for now: pairings differ for Toulouse and SimpleMM
// test_from_XML_internal(name, forcePairing, false)
@@ -172,11 +170,10 @@ class PairingTests: TestBase() {
fun test_from_XML_internal(name: String, forcePairing:List<Int>, legacy: Boolean) {
// Let pairgoth use the legacy asymmetric detRandom()
Solver.legacy_mode = legacy
// read tournament with pairing
val file = getTestFile("opengotha/pairings/$name.xml")
logger.info("read from file $file")
val resource = file.readText(StandardCharsets.UTF_8)
val tourFile = getTestFile("opengotha/pairings/$name.xml")
logger.info("read from file $tourFile")
val resource = tourFile.readText(StandardCharsets.UTF_8)
var resp = TestAPI.post("/api/tour", resource)
val id = resp.asObject().getInt("id")
val tournament = TestAPI.get("/api/tour/$id").asObject()
@@ -203,15 +200,15 @@ class PairingTests: TestBase() {
for (round in 1..tournament.getInt("rounds")!!) {
val sumOfWeightsOG = compute_sumOfWeight_OG(getTestFile("opengotha/$name/$name" + "_weights_R$round.txt"), pairingsOG[round-1], players)
Solver.weightsLogger = PrintWriter(FileWriter(getOutputFile("weights.txt")))
val outputFile = getOutputFile("weights.txt")
// Call Pairgoth pairing solver to generate games
games = TestAPI.post("/api/tour/$id/pair/$round", Json.Array("all")).asArray()
games = TestAPI.post("/api/tour/$id/pair/$round?legacy=$legacy&weights_output=$outputFile", Json.Array("all")).asArray()
logger.info("sumOfWeightOG = " + dec.format(sumOfWeightsOG))
logger.info("games for round $round: {}", games.toString())
// Compare weights with OpenGotha if legacy mode
if (legacy) {
assertTrue(compare_weights(getOutputFile("weights.txt"), getTestFile("opengotha/$name/$name"+"_weights_R$round.txt")), "Not matching opengotha weights for round $round")
assertTrue(compare_weights(outputFile, getTestFile("opengotha/$name/$name"+"_weights_R$round.txt")), "Not matching opengotha weights for round $round")
}
if (round in forcePairing) {
@@ -223,7 +220,7 @@ class PairingTests: TestBase() {
val gameOG = pairingsOG[round - 1].getJson(i)!!.asObject()// ["r"] as String?
val whiteId = gameOG["w"] as Long?
val blackId = gameOG["b"] as Long?
TestAPI.put("/api/tour/$id/pair/$round", Json.parse("""{"id":$gameID,"w":$whiteId,"b":$blackId}""")).asObject()
TestAPI.put("/api/tour/$id/pair/$round?legacy=$legacy&weights_output=$outputFile&append=true", Json.parse("""{"id":$gameID,"w":$whiteId,"b":$blackId}""")).asObject()
}
games = TestAPI.get("/api/tour/$id/res/$round").asArray()
}
@@ -273,11 +270,10 @@ class PairingTests: TestBase() {
@Test
fun `SwissTest simpleSwiss`() {
Solver.legacy_mode = true
// read tournament with pairing
var file = getTestFile("opengotha/pairings/simpleswiss.xml")
logger.info("read from file $file")
val resource = file.readText(StandardCharsets.UTF_8)
var tourFile = getTestFile("opengotha/pairings/simpleswiss.xml")
logger.info("read from file $tourFile")
val resource = tourFile.readText(StandardCharsets.UTF_8)
var resp = TestAPI.post("/api/tour", resource)
val id = resp.asObject().getInt("id")
val tournament = TestAPI.get("/api/tour/$id").asObject()
@@ -315,10 +311,10 @@ class PairingTests: TestBase() {
var firstGameID: Int
for (round in 1..7) {
Solver.weightsLogger = PrintWriter(FileWriter(getOutputFile("weights.txt")))
games = TestAPI.post("/api/tour/$id/pair/$round", Json.Array("all")).asArray()
val outputFile = getOutputFile("weights.txt")
games = TestAPI.post("/api/tour/$id/pair/$round?legacy=true&weights_output=$outputFile", Json.Array("all")).asArray()
logger.info("games for round $round: {}", games.toString().slice(0..50) + "...")
assertTrue(compare_weights(getOutputFile("weights.txt"), getTestFile("opengotha/simpleswiss/simpleswiss_weights_R$round.txt")), "Not matching opengotha weights for round $round")
assertTrue(compare_weights(outputFile, getTestFile("opengotha/simpleswiss/simpleswiss_weights_R$round.txt")), "Not matching opengotha weights for round $round")
assertTrue(compare_games(games, Json.parse(pairingsOG[round - 1])!!.asArray()),"pairings for round $round differ")
logger.info("Pairings for round $round match OpenGotha")
@@ -354,12 +350,12 @@ class PairingTests: TestBase() {
@Test
fun `SwissTest KPMCSplitbug`() {
// Let pairgoth use the legacy asymmetric detRandom()
Solver.legacy_mode = true
val legacy = true
// read tournament with pairing
val name = "20240921-KPMC-Splitbug"
val file = getTestFile("opengotha/pairings/$name.xml")
logger.info("read from file $file")
val resource = file.readText(StandardCharsets.UTF_8)
val tourFile = getTestFile("opengotha/pairings/$name.xml")
logger.info("read from file $tourFile")
val resource = tourFile.readText(StandardCharsets.UTF_8)
var resp = TestAPI.post("/api/tour", resource)
val id = resp.asObject().getInt("id")
val tournament = TestAPI.get("/api/tour/$id").asObject()
@@ -387,13 +383,13 @@ class PairingTests: TestBase() {
var games: Json.Array
var firstGameID: Int
val outputFile = getOutputFile("weights.txt")
for (round in minRound..maxRound) {
val sumOfWeightsOG = compute_sumOfWeight_OG(getTestFile("opengotha/$name/$name" + "_weights_R$round.txt"), pairingsOG[round - minRound], players)
Solver.weightsLogger = PrintWriter(FileWriter(getOutputFile("weights.txt")))
// Call Pairgoth pairing solver to generate games
games = TestAPI.post("/api/tour/$id/pair/$round", Json.Array("all")).asArray()
games = TestAPI.post("/api/tour/$id/pair/$round?legacy=$legacy&weights_ouput=$outputFile&append=${round > 1}", Json.Array("all")).asArray()
logger.info("sumOfWeightOG = " + dec.format(sumOfWeightsOG))
logger.info("games for round $round: {}", games.toString().slice(0..50) + "...")
@@ -401,7 +397,7 @@ class PairingTests: TestBase() {
// Compare weights with OpenGotha
assertTrue(
compare_weights(
getOutputFile("weights.txt"),
outputFile,
getTestFile("opengotha/$name/$name" + "_weights_R$round.txt")
), "Not matching opengotha weights for round $round"
)

View File

@@ -7,6 +7,8 @@ import org.jeudego.pairgoth.server.SSEServlet
import org.jeudego.pairgoth.server.WebappManager
import org.mockito.kotlin.*
import java.io.*
import java.net.URL
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.util.*
import javax.servlet.ReadListener
@@ -21,20 +23,45 @@ object TestAPI {
fun Any?.toUnit() = Unit
fun parseURL(url: String): Pair<String, Map<String, String>> {
val qm = url.indexOf('?')
if (qm == -1) {
return url to emptyMap()
}
val uri = url.substring(0, qm)
val params = url.substring(qm + 1)
.split('&')
.map { it.split('=') }
.mapNotNull {
when (it.size) {
1 -> it[0].decodeUTF8() to ""
2 -> it[0].decodeUTF8() to it[1].decodeUTF8()
else -> null
}
}
.toMap()
return uri to params
}
private fun String.decodeUTF8() = URLDecoder.decode(this, "UTF-8") // decode page=%22ABC%22 to page="ABC"
private val apiServlet = ApiServlet()
private val sseServlet = SSEServlet()
private fun <T> testRequest(reqMethod: String, uri: String, accept: String = "application/json", payload: T? = null): String {
private fun <T> testRequest(reqMethod: String, url: String, accept: String = "application/json", payload: T? = null): String {
WebappManager.properties["auth"] = "none"
WebappManager.properties["store"] = "memory"
WebappManager.properties["webapp.env"] = "test"
val (uri, parameters) = parseURL(url)
// mock request
val myHeaderNames = if (reqMethod == "GET") emptyList() else listOf("Content-Type")
val selector = argumentCaptor<String>()
val subSelector = argumentCaptor<String>()
val reqPayload = argumentCaptor<String>()
val parameter = argumentCaptor<String>()
val myInputStream = payload?.let { DelegatingServletInputStream(payload.toString().byteInputStream(StandardCharsets.UTF_8)) }
val myReader = payload?.let { BufferedReader(StringReader(payload.toString())) }
val req = mock<HttpServletRequest> {
@@ -59,6 +86,7 @@ object TestAPI {
}
on { headerNames } doReturn Collections.enumeration(myHeaderNames)
on { getHeader(eq("Accept")) } doReturn accept
on { getParameter(parameter.capture()) } doAnswer { parameters[parameter.lastValue] }
}
// mock response
@@ -77,7 +105,7 @@ object TestAPI {
"DELETE" -> apiServlet.doDelete(req, resp)
}
return buffer.toString() ?: throw Error("no response payload")
return buffer.toString()
}
fun get(uri: String): Json = Json.parse(testRequest<Void>("GET", uri)) ?: throw Error("no payload")