Use a PairingListener class to collect or print weights, avoid computing twice the weights during tests
This commit is contained in:
@@ -13,7 +13,10 @@ import org.jeudego.pairgoth.model.Tournament
|
|||||||
import org.jeudego.pairgoth.model.getID
|
import org.jeudego.pairgoth.model.getID
|
||||||
import org.jeudego.pairgoth.model.toID
|
import org.jeudego.pairgoth.model.toID
|
||||||
import org.jeudego.pairgoth.model.toJson
|
import org.jeudego.pairgoth.model.toJson
|
||||||
|
import org.jeudego.pairgoth.pairing.solver.LoggingListener
|
||||||
import org.jeudego.pairgoth.server.Event.*
|
import org.jeudego.pairgoth.server.Event.*
|
||||||
|
import java.io.FileWriter
|
||||||
|
import java.io.PrintWriter
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
import javax.servlet.http.HttpServletResponse
|
import javax.servlet.http.HttpServletResponse
|
||||||
|
|
||||||
@@ -67,7 +70,15 @@ object PairingHandler: PairgothApiHandler {
|
|||||||
if (playing.contains(it.id)) badRequest("pairable #$id already plays round $round")
|
if (playing.contains(it.id)) badRequest("pairable #$id already plays round $round")
|
||||||
} ?: badRequest("invalid pairable id: #$id")
|
} ?: badRequest("invalid pairable id: #$id")
|
||||||
}
|
}
|
||||||
val games = tournament.pair(round, pairables)
|
|
||||||
|
// POST pair/$round accepts a few parameters to help tests
|
||||||
|
val legacy = request.getParameter("legacy")?.toBoolean() ?: false
|
||||||
|
val weightsLogger = request.getParameter("weights_output")?.let {
|
||||||
|
val append = request.getParameter("append")?.toBoolean() ?: false
|
||||||
|
LoggingListener(PrintWriter(FileWriter(it, append)))
|
||||||
|
}
|
||||||
|
|
||||||
|
val games = tournament.pair(round, pairables, legacy, weightsLogger)
|
||||||
|
|
||||||
val ret = games.map { it.toJson() }.toJsonArray()
|
val ret = games.map { it.toJson() }.toJsonArray()
|
||||||
tournament.dispatchEvent(GamesAdded, request, Json.Object("round" to round, "games" to ret))
|
tournament.dispatchEvent(GamesAdded, request, Json.Object("round" to round, "games" to ret))
|
||||||
|
@@ -7,6 +7,7 @@ import org.jeudego.pairgoth.model.PairingType.*
|
|||||||
import org.jeudego.pairgoth.pairing.HistoryHelper
|
import org.jeudego.pairgoth.pairing.HistoryHelper
|
||||||
import org.jeudego.pairgoth.pairing.solver.Solver
|
import org.jeudego.pairgoth.pairing.solver.Solver
|
||||||
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
|
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
|
||||||
|
import org.jeudego.pairgoth.pairing.solver.PairingListener
|
||||||
import org.jeudego.pairgoth.pairing.solver.SwissSolver
|
import org.jeudego.pairgoth.pairing.solver.SwissSolver
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -133,8 +134,15 @@ sealed class Pairing(
|
|||||||
val placementParams: PlacementParams) {
|
val placementParams: PlacementParams) {
|
||||||
companion object {}
|
companion object {}
|
||||||
abstract fun solver(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): Solver
|
abstract fun solver(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): Solver
|
||||||
fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
|
fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>, legacyMode: Boolean = false, listener: PairingListener? = null): List<Game> {
|
||||||
return solver(tournament, round, pairables).pair()
|
return solver(tournament, round, pairables)
|
||||||
|
.also { solver ->
|
||||||
|
solver.legacyMode = legacyMode
|
||||||
|
listener?.let {
|
||||||
|
solver.pairingListener = listener
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pair()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,6 +9,7 @@ import java.time.LocalDate
|
|||||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||||
import org.jeudego.pairgoth.pairing.HistoryHelper
|
import org.jeudego.pairgoth.pairing.HistoryHelper
|
||||||
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
|
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
|
||||||
|
import org.jeudego.pairgoth.pairing.solver.PairingListener
|
||||||
import org.jeudego.pairgoth.store.nextGameId
|
import org.jeudego.pairgoth.store.nextGameId
|
||||||
import org.jeudego.pairgoth.store.nextPlayerId
|
import org.jeudego.pairgoth.store.nextPlayerId
|
||||||
import org.jeudego.pairgoth.store.nextTournamentId
|
import org.jeudego.pairgoth.store.nextTournamentId
|
||||||
@@ -64,7 +65,7 @@ sealed class Tournament <P: Pairable>(
|
|||||||
var frozen: Json.Array? = null
|
var frozen: Json.Array? = null
|
||||||
|
|
||||||
// pairing
|
// pairing
|
||||||
open fun pair(round: Int, pairables: List<Pairable>): List<Game> {
|
open fun pair(round: Int, pairables: List<Pairable>, legacyMode: Boolean = false, listener: PairingListener? = null): List<Game> {
|
||||||
// Minimal check on round number.
|
// Minimal check on round number.
|
||||||
// CB TODO - the complete check should verify, for each player, that he was either non pairable or implied in the previous round
|
// CB TODO - the complete check should verify, for each player, that he was either non pairable or implied in the previous round
|
||||||
if (round > games.size + 1) badRequest("previous round not paired")
|
if (round > games.size + 1) badRequest("previous round not paired")
|
||||||
@@ -72,7 +73,7 @@ sealed class Tournament <P: Pairable>(
|
|||||||
val evenPairables =
|
val evenPairables =
|
||||||
if (pairables.size % 2 == 0) pairables
|
if (pairables.size % 2 == 0) pairables
|
||||||
else pairables.toMutableList().also { it.add(ByePlayer) }
|
else pairables.toMutableList().also { it.add(ByePlayer) }
|
||||||
return pairing.pair(this, round, evenPairables).also { newGames ->
|
return pairing.pair(this, round, evenPairables, legacyMode, listener).also { newGames ->
|
||||||
if (games.size < round) games.add(mutableMapOf())
|
if (games.size < round) games.add(mutableMapOf())
|
||||||
games[round - 1].putAll( newGames.associateBy { it.id } )
|
games[round - 1].putAll( newGames.associateBy { it.id } )
|
||||||
}
|
}
|
||||||
@@ -291,8 +292,8 @@ class TeamTournament(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pair(round: Int, pairables: List<Pairable>) =
|
override fun pair(round: Int, pairables: List<Pairable>, legacyMode: Boolean, listener: PairingListener?) =
|
||||||
super.pair(round, pairables).also { games ->
|
super.pair(round, pairables, legacyMode, listener).also { games ->
|
||||||
if (type.individual) {
|
if (type.individual) {
|
||||||
games.forEach { game ->
|
games.forEach { game ->
|
||||||
pairIndividualGames(round, game)
|
pairIndividualGames(round, game)
|
||||||
|
@@ -43,8 +43,7 @@ class MacMahonSolver(round: Int,
|
|||||||
return 2 * p.score
|
return 2 * p.score
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun SecondaryCritParams.apply(p1: Pairable, p2: Pairable): Double {
|
override fun SecondaryCritParams.playersMeetCriteria(p1: Pairable, p2: Pairable): Int {
|
||||||
|
|
||||||
// playersMeetCriteria = 0 : No player is above thresholds -> apply the full weight
|
// playersMeetCriteria = 0 : No player is above thresholds -> apply the full weight
|
||||||
// playersMeetCriteria = 1 : 1 player is above thresholds -> apply half the weight
|
// playersMeetCriteria = 1 : 1 player is above thresholds -> apply half the weight
|
||||||
// playersMeetCriteria = 2 : Both players are above thresholds -> do not apply weight
|
// playersMeetCriteria = 2 : Both players are above thresholds -> do not apply weight
|
||||||
@@ -67,7 +66,7 @@ class MacMahonSolver(round: Int,
|
|||||||
|| barThresholdActive && (p2.mmBase >= mmBar - Pairable.MIN_RANK)
|
|| barThresholdActive && (p2.mmBase >= mmBar - Pairable.MIN_RANK)
|
||||||
|| p2.mms >= rankSecThreshold - Pairable.MIN_RANK) playersMeetCriteria++
|
|| p2.mms >= rankSecThreshold - Pairable.MIN_RANK) playersMeetCriteria++
|
||||||
|
|
||||||
return pairing.geo.apply(p1, p2, playersMeetCriteria)
|
return playersMeetCriteria
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun HandicapParams.pseudoRank(pairable: Pairable): Int {
|
override fun HandicapParams.pseudoRank(pairable: Pairable): Int {
|
||||||
|
@@ -0,0 +1,75 @@
|
|||||||
|
package org.jeudego.pairgoth.pairing.solver
|
||||||
|
|
||||||
|
import org.jeudego.pairgoth.model.ID
|
||||||
|
import org.jeudego.pairgoth.model.Pairable
|
||||||
|
import org.jeudego.pairgoth.model.Tournament
|
||||||
|
import java.io.PrintWriter
|
||||||
|
|
||||||
|
interface PairingListener {
|
||||||
|
|
||||||
|
fun start(round: Int) {}
|
||||||
|
fun startPair(white: Pairable, black: Pairable) {}
|
||||||
|
fun endPair(white: Pairable, black: Pairable) {}
|
||||||
|
fun addWeight(name: String, weight: Double)
|
||||||
|
fun end() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoggingListener(val out: PrintWriter) : PairingListener {
|
||||||
|
|
||||||
|
var currentOpenGothaWeight: Double = 0.0
|
||||||
|
|
||||||
|
override fun start(round: Int) {
|
||||||
|
out.println("Round $round")
|
||||||
|
out.println("Costs")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startPair(white: Pairable, black: Pairable) {
|
||||||
|
currentOpenGothaWeight = 0.0
|
||||||
|
out.println("Player1Name=${white.fullName()}")
|
||||||
|
out.println("Player2Name=${black.fullName()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addWeight(name: String, weight: Double) {
|
||||||
|
// Try hard to stay in sync with current reference files of OpenGotha conformance tests
|
||||||
|
val key = when (name) {
|
||||||
|
// TODO - Change to propagate to test reference files
|
||||||
|
"baseColorBalance" -> "baseBWBalance"
|
||||||
|
// Pairgoth-specific part of the color balance, not considered in conformance tests
|
||||||
|
"secColorBalance" -> return
|
||||||
|
else -> name
|
||||||
|
}
|
||||||
|
val value = when (name) {
|
||||||
|
// TODO - This cost is always zero in reference files, seems unused
|
||||||
|
"secHandi" -> 0.0
|
||||||
|
else -> weight
|
||||||
|
}
|
||||||
|
currentOpenGothaWeight += value
|
||||||
|
out.println("${key}Cost=$value")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endPair(white: Pairable, black: Pairable) {
|
||||||
|
out.println("totalCost=$currentOpenGothaWeight")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun end() {
|
||||||
|
out.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollectingListener() : PairingListener {
|
||||||
|
|
||||||
|
val out = mutableMapOf<Pair<ID, ID>, MutableMap<String, Double>>()
|
||||||
|
var white: Pairable? = null
|
||||||
|
var black: Pairable? = null
|
||||||
|
|
||||||
|
override fun startPair(white: Pairable, black: Pairable) {
|
||||||
|
this.white = white
|
||||||
|
this.black = black
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addWeight(name: String, weight: Double) {
|
||||||
|
val key = Pair(white!!.id, black!!.id)
|
||||||
|
val weights = out.computeIfAbsent(key) { mutableMapOf() }
|
||||||
|
weights[name] = weight
|
||||||
|
}
|
||||||
|
}
|
@@ -32,11 +32,12 @@ sealed class Solver(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val rand = Random(/* seed from properties - TODO */)
|
val rand = Random(/* seed from properties - TODO */)
|
||||||
// Used in tests
|
|
||||||
var weightsLogger: PrintWriter? = null
|
|
||||||
var legacy_mode = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For tests and explain feature
|
||||||
|
var legacyMode = false
|
||||||
|
var pairingListener: PairingListener? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
history.scoresFactory = this::mainScoreMapFactory
|
history.scoresFactory = this::mainScoreMapFactory
|
||||||
history.scoresXFactory = this::scoreXMapFactory
|
history.scoresXFactory = this::scoreXMapFactory
|
||||||
@@ -59,7 +60,7 @@ sealed class Solver(
|
|||||||
abstract fun missedRoundSosMapFactory(): Map<ID, Double>
|
abstract fun missedRoundSosMapFactory(): Map<ID, Double>
|
||||||
|
|
||||||
open fun openGothaWeight(p1: Pairable, p2: Pairable) =
|
open fun openGothaWeight(p1: Pairable, p2: Pairable) =
|
||||||
1.0 + // 1 is minimum value because 0 means "no matching allowed"
|
1.0 + // 1 is the minimum value because 0 means "no matching allowed"
|
||||||
pairing.base.apply(p1, p2) +
|
pairing.base.apply(p1, p2) +
|
||||||
pairing.main.apply(p1, p2) +
|
pairing.main.apply(p1, p2) +
|
||||||
pairing.secondary.apply(p1, p2)
|
pairing.secondary.apply(p1, p2)
|
||||||
@@ -71,13 +72,19 @@ sealed class Solver(
|
|||||||
else 0.0
|
else 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun weight(p1: Pairable, p2: Pairable) =
|
open fun weight(p1: Pairable, p2: Pairable): Double {
|
||||||
|
pairingListener?.startPair(p1, p2)
|
||||||
|
return (
|
||||||
openGothaWeight(p1, p2) +
|
openGothaWeight(p1, p2) +
|
||||||
pairgothBlackWhite(p1, p2) +
|
pairgothBlackWhite(p1, p2).also { pairingListener?.addWeight("secColorBalance", it) } +
|
||||||
// pairing.base.applyByeWeight(p1, p2) +
|
// pairing.base.applyByeWeight(p1, p2) +
|
||||||
pairing.handicap.color(p1, p2)
|
pairing.handicap.color(p1, p2).also { pairingListener?.addWeight("secHandi", it) }
|
||||||
|
).also {
|
||||||
|
pairingListener?.endPair(p1, p2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open fun computeWeightForBye(p: Pairable): Double{
|
open fun computeWeightForBye(p: Pairable): Double {
|
||||||
// The weightForBye function depends on the system type (Mac-Mahon or Swiss), default value is 0.0
|
// The weightForBye function depends on the system type (Mac-Mahon or Swiss), default value is 0.0
|
||||||
return 0.0
|
return 0.0
|
||||||
}
|
}
|
||||||
@@ -92,10 +99,7 @@ sealed class Solver(
|
|||||||
val logger = LoggerFactory.getLogger("debug")
|
val logger = LoggerFactory.getLogger("debug")
|
||||||
val debug = false
|
val debug = false
|
||||||
|
|
||||||
weightsLogger?.apply {
|
pairingListener?.start(round)
|
||||||
this.println("Round $round")
|
|
||||||
this.println("Costs")
|
|
||||||
}
|
|
||||||
|
|
||||||
var chosenByePlayer: Pairable = ByePlayer
|
var chosenByePlayer: Pairable = ByePlayer
|
||||||
// Choose bye player and remove from pairables
|
// Choose bye player and remove from pairables
|
||||||
@@ -124,21 +128,6 @@ sealed class Solver(
|
|||||||
val q = nameSortedPairables[j]
|
val q = nameSortedPairables[j]
|
||||||
weight(p, q).let { if (it != Double.NaN) builder.addEdge(p, q, it/1e6) }
|
weight(p, q).let { if (it != Double.NaN) builder.addEdge(p, q, it/1e6) }
|
||||||
weight(q, p).let { if (it != Double.NaN) builder.addEdge(q, p, it/1e6) }
|
weight(q, p).let { if (it != Double.NaN) builder.addEdge(q, p, it/1e6) }
|
||||||
weightsLogger?.apply {
|
|
||||||
this.println("Player1Name=${p.fullName()}")
|
|
||||||
this.println("Player2Name=${q.fullName()}")
|
|
||||||
this.println("baseDuplicateGameCost=${dec.format(pairing.base.avoidDuplicatingGames(p, q))}")
|
|
||||||
this.println("baseRandomCost=${dec.format(pairing.base.applyRandom(p, q))}")
|
|
||||||
this.println("baseBWBalanceCost=${dec.format(pairing.base.applyColorBalance(p, q))}")
|
|
||||||
this.println("mainCategoryCost=${dec.format(pairing.main.avoidMixingCategory(p, q))}")
|
|
||||||
this.println("mainScoreDiffCost=${dec.format(pairing.main.minimizeScoreDifference(p, q))}")
|
|
||||||
this.println("mainDUDDCost=${dec.format(pairing.main.applyDUDD(p, q))}")
|
|
||||||
this.println("mainSeedCost=${dec.format(pairing.main.applySeeding(p, q))}")
|
|
||||||
this.println("secHandiCost=${dec.format(pairing.handicap.handicap(p, q))}")
|
|
||||||
this.println("secGeoCost=${dec.format(pairing.secondary.apply(p, q))}")
|
|
||||||
this.println("totalCost=${dec.format(openGothaWeight(p,q))}")
|
|
||||||
//File(WEIGHTS_FILE).appendText("ByeCost="+dec.format(pairing.base.applyByeWeight(p,q))+"\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val graph = builder.build()
|
val graph = builder.build()
|
||||||
@@ -153,6 +142,8 @@ sealed class Solver(
|
|||||||
// add game for ByePlayer
|
// add game for ByePlayer
|
||||||
if (chosenByePlayer != ByePlayer) result += Game(id = nextGameId, table = 0, white = chosenByePlayer.id, black = ByePlayer.id, result = Game.Result.fromSymbol('w'))
|
if (chosenByePlayer != ByePlayer) result += Game(id = nextGameId, table = 0, white = chosenByePlayer.id, black = ByePlayer.id, result = Game.Result.fromSymbol('w'))
|
||||||
|
|
||||||
|
pairingListener?.end()
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
var sumOfWeights = 0.0
|
var sumOfWeights = 0.0
|
||||||
|
|
||||||
@@ -216,11 +207,11 @@ sealed class Solver(
|
|||||||
var score = 0.0
|
var score = 0.0
|
||||||
// Base Criterion 1 : Avoid Duplicating Game
|
// Base Criterion 1 : Avoid Duplicating Game
|
||||||
// Did p1 and p2 already play ?
|
// Did p1 and p2 already play ?
|
||||||
score += avoidDuplicatingGames(p1, p2)
|
score += avoidDuplicatingGames(p1, p2).also { pairingListener?.addWeight("baseDuplicateGame", it) }
|
||||||
// Base Criterion 2 : Random
|
// Base Criterion 2 : Random
|
||||||
score += applyRandom(p1, p2)
|
score += applyRandom(p1, p2).also { pairingListener?.addWeight("baseRandom", it) }
|
||||||
// Base Criterion 3 : Balance W and B
|
// Base Criterion 3 : Balance W and B
|
||||||
score += applyColorBalance(p1, p2)
|
score += applyColorBalance(p1, p2).also { pairingListener?.addWeight("baseColorBalance", it) }
|
||||||
|
|
||||||
return score
|
return score
|
||||||
}
|
}
|
||||||
@@ -280,16 +271,16 @@ sealed class Solver(
|
|||||||
var score = 0.0
|
var score = 0.0
|
||||||
|
|
||||||
// Main criterion 1 avoid mixing category is moved to Swiss with category
|
// Main criterion 1 avoid mixing category is moved to Swiss with category
|
||||||
score += avoidMixingCategory(p1, p2)
|
score += avoidMixingCategory(p1, p2).also { pairingListener?.addWeight("mainCategory", it) }
|
||||||
|
|
||||||
// Main criterion 2 minimize score difference
|
// Main criterion 2 minimize score difference
|
||||||
score += minimizeScoreDifference(p1, p2)
|
score += minimizeScoreDifference(p1, p2).also { pairingListener?.addWeight("mainScoreDiff", it) }
|
||||||
|
|
||||||
// Main criterion 3 If different groups, make a directed Draw-up/Draw-down
|
// Main criterion 3 If different groups, make a directed Draw-up/Draw-down
|
||||||
score += applyDUDD(p1, p2)
|
score += applyDUDD(p1, p2).also { pairingListener?.addWeight("mainDUDD", it) }
|
||||||
|
|
||||||
// Main criterion 4 seeding
|
// Main criterion 4 seeding
|
||||||
score += applySeeding(p1, p2)
|
score += applySeeding(p1, p2).also { pairingListener?.addWeight("mainSeed", it) }
|
||||||
|
|
||||||
return score
|
return score
|
||||||
}
|
}
|
||||||
@@ -411,7 +402,7 @@ sealed class Solver(
|
|||||||
val randRange = maxSeedingWeight * 0.2
|
val randRange = maxSeedingWeight * 0.2
|
||||||
// for old tests to pass
|
// for old tests to pass
|
||||||
val rand =
|
val rand =
|
||||||
if (legacy_mode && p1.fullName() > p2.fullName()) {
|
if (legacyMode && p1.fullName() > p2.fullName()) {
|
||||||
// for old tests to pass
|
// for old tests to pass
|
||||||
detRandom(randRange, p2, p1, false)
|
detRandom(randRange, p2, p1, false)
|
||||||
} else {
|
} else {
|
||||||
@@ -427,8 +418,7 @@ sealed class Solver(
|
|||||||
return Math.round(score).toDouble()
|
return Math.round(score).toDouble()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun SecondaryCritParams.apply(p1: Pairable, p2: Pairable): Double {
|
open fun SecondaryCritParams.playersMeetCriteria(p1: Pairable, p2: Pairable): Int {
|
||||||
|
|
||||||
// playersMeetCriteria = 0 : No player is above thresholds -> apply secondary criteria
|
// playersMeetCriteria = 0 : No player is above thresholds -> apply secondary criteria
|
||||||
// playersMeetCriteria = 1 : 1 player is above thresholds -> apply half the weight
|
// playersMeetCriteria = 1 : 1 player is above thresholds -> apply half the weight
|
||||||
// playersMeetCriteria = 2 : Both players are above thresholds -> apply the full weight
|
// playersMeetCriteria = 2 : Both players are above thresholds -> apply the full weight
|
||||||
@@ -441,7 +431,11 @@ sealed class Solver(
|
|||||||
if (2*p1.nbW >= nbw2Threshold) playersMeetCriteria++
|
if (2*p1.nbW >= nbw2Threshold) playersMeetCriteria++
|
||||||
if (2*p2.nbW >= nbw2Threshold) playersMeetCriteria++
|
if (2*p2.nbW >= nbw2Threshold) playersMeetCriteria++
|
||||||
|
|
||||||
return pairing.geo.apply(p1, p2, playersMeetCriteria)
|
return playersMeetCriteria
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SecondaryCritParams.apply(p1: Pairable, p2: Pairable): Double {
|
||||||
|
return pairing.geo.apply(p1, p2, playersMeetCriteria(p1, p2))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun GeographicalParams.apply(p1: Pairable, p2: Pairable, playersMeetCriteria: Int): Double {
|
fun GeographicalParams.apply(p1: Pairable, p2: Pairable, playersMeetCriteria: Int): Double {
|
||||||
@@ -449,11 +443,11 @@ sealed class Solver(
|
|||||||
|
|
||||||
val geoMaxCost = pairing.geo.avoidSameGeo
|
val geoMaxCost = pairing.geo.avoidSameGeo
|
||||||
|
|
||||||
val countryFactor: Int = if (legacy_mode || biggestCountrySize.toDouble() / pairables.size <= proportionMainClubThreshold)
|
val countryFactor: Int = if (legacyMode || biggestCountrySize.toDouble() / pairables.size <= proportionMainClubThreshold)
|
||||||
preferMMSDiffRatherThanSameCountry
|
preferMMSDiffRatherThanSameCountry
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
val clubFactor: Int = if (legacy_mode || biggestClubSize.toDouble() / pairables.size <= proportionMainClubThreshold)
|
val clubFactor: Int = if (legacyMode || biggestClubSize.toDouble() / pairables.size <= proportionMainClubThreshold)
|
||||||
preferMMSDiffRatherThanSameClub
|
preferMMSDiffRatherThanSameClub
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
@@ -511,7 +505,7 @@ sealed class Solver(
|
|||||||
2 -> geoMaxCost
|
2 -> geoMaxCost
|
||||||
1 -> 0.5 * (geoNominalCost + geoMaxCost)
|
1 -> 0.5 * (geoNominalCost + geoMaxCost)
|
||||||
else -> geoNominalCost
|
else -> geoNominalCost
|
||||||
}
|
}.also { pairingListener?.addWeight("secGeo", it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handicap functions
|
// Handicap functions
|
||||||
@@ -559,7 +553,7 @@ sealed class Solver(
|
|||||||
} else if (p1.colorBalance < p2.colorBalance) {
|
} else if (p1.colorBalance < p2.colorBalance) {
|
||||||
score = 1.0
|
score = 1.0
|
||||||
} else { // choose color from a det random
|
} else { // choose color from a det random
|
||||||
if (detRandom(1.0, p1, p2, false) === 0.0) {
|
if (detRandom(1.0, p1, p2, false) == 0.0) {
|
||||||
score = 1.0
|
score = 1.0
|
||||||
} else {
|
} else {
|
||||||
score = -1.0
|
score = -1.0
|
||||||
|
@@ -20,24 +20,22 @@ class BOSP2024Test: TestBase() {
|
|||||||
)!!.asObject()
|
)!!.asObject()
|
||||||
val resp = TestAPI.post("/api/tour", tournament).asObject()
|
val resp = TestAPI.post("/api/tour", tournament).asObject()
|
||||||
val tourId = resp.getInt("id")
|
val tourId = resp.getInt("id")
|
||||||
Solver.weightsLogger = PrintWriter(FileWriter(getOutputFile("bosp2024-weights.txt")))
|
val outputFile = getOutputFile("bosp2024-weights.txt")
|
||||||
|
TestAPI.post("/api/tour/$tourId/pair/3?legacy=true&weights_output=$outputFile", Json.Array("all")).asArray()
|
||||||
Solver.legacy_mode = true
|
|
||||||
TestAPI.post("/api/tour/$tourId/pair/3", Json.Array("all")).asArray()
|
|
||||||
|
|
||||||
// compare weights
|
// 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"))
|
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()
|
val games = TestAPI.post("/api/tour/$tourId/pair/3", Json.Array("all")).asArray()
|
||||||
// Aksut Husrev is ID 18
|
// 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
|
// 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
|
// keep game with Aksut Husrev
|
||||||
players.contains(18)
|
players.contains(18)
|
||||||
}.firstOrNull()
|
}
|
||||||
|
|
||||||
assertNotNull(solved)
|
assertNotNull(solved)
|
||||||
|
|
||||||
|
@@ -1,11 +1,8 @@
|
|||||||
package org.jeudego.pairgoth.test
|
package org.jeudego.pairgoth.test
|
||||||
|
|
||||||
import com.republicate.kson.Json
|
import com.republicate.kson.Json
|
||||||
import org.jeudego.pairgoth.pairing.solver.Solver
|
|
||||||
import org.jeudego.pairgoth.test.PairingTests.Companion.compare_weights
|
import org.jeudego.pairgoth.test.PairingTests.Companion.compare_weights
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.io.FileWriter
|
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
@@ -19,8 +16,8 @@ class MalavasiTest: TestBase() {
|
|||||||
)!!.asObject()
|
)!!.asObject()
|
||||||
val resp = TestAPI.post("/api/tour", tournament).asObject()
|
val resp = TestAPI.post("/api/tour", tournament).asObject()
|
||||||
val tourId = resp.getInt("id")
|
val tourId = resp.getInt("id")
|
||||||
Solver.weightsLogger = PrintWriter(FileWriter(getOutputFile("malavasi-weights.txt")))
|
val outputFile = getOutputFile("malavasi-weights.txt")
|
||||||
val games = TestAPI.post("/api/tour/$tourId/pair/2", Json.Array("all")).asArray()
|
val games = TestAPI.post("/api/tour/$tourId/pair/2?weights_output=$outputFile", Json.Array("all")).asArray()
|
||||||
// Oceane is ID 548, Valentine 549
|
// Oceane is ID 548, Valentine 549
|
||||||
val buggy = games.map { it as Json.Object }.filter { game ->
|
val buggy = games.map { it as Json.Object }.filter { game ->
|
||||||
// build the two-elements set of players ids
|
// build the two-elements set of players ids
|
||||||
@@ -33,6 +30,6 @@ class MalavasiTest: TestBase() {
|
|||||||
assertEquals(2, buggy.size)
|
assertEquals(2, buggy.size)
|
||||||
|
|
||||||
// compare weights
|
// 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,8 +8,6 @@ import org.jeudego.pairgoth.store.lastPlayerId
|
|||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileWriter
|
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@@ -59,7 +57,6 @@ class PairingTests: TestBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun compare_weights(file1: File, file2: File, skipSeeding: Boolean = false):Boolean {
|
fun compare_weights(file1: File, file2: File, skipSeeding: Boolean = false):Boolean {
|
||||||
Solver.weightsLogger!!.flush()
|
|
||||||
// Maps to store name pairs and costs
|
// Maps to store name pairs and costs
|
||||||
val map1 = create_weights_map(file1)
|
val map1 = create_weights_map(file1)
|
||||||
val map2 = create_weights_map(file2)
|
val map2 = create_weights_map(file2)
|
||||||
@@ -165,6 +162,7 @@ class PairingTests: TestBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun test_from_XML(name: String, forcePairing:List<Int>) {
|
fun test_from_XML(name: String, forcePairing:List<Int>) {
|
||||||
|
// Let pairgoth use the legacy asymmetric detRandom()
|
||||||
test_from_XML_internal(name, forcePairing, true)
|
test_from_XML_internal(name, forcePairing, true)
|
||||||
// Non-legacy tests inhibited for now: pairings differ for Toulouse and SimpleMM
|
// Non-legacy tests inhibited for now: pairings differ for Toulouse and SimpleMM
|
||||||
// test_from_XML_internal(name, forcePairing, false)
|
// 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) {
|
fun test_from_XML_internal(name: String, forcePairing:List<Int>, legacy: Boolean) {
|
||||||
// Let pairgoth use the legacy asymmetric detRandom()
|
// Let pairgoth use the legacy asymmetric detRandom()
|
||||||
Solver.legacy_mode = legacy
|
|
||||||
// read tournament with pairing
|
// read tournament with pairing
|
||||||
val file = getTestFile("opengotha/pairings/$name.xml")
|
val tourFile = getTestFile("opengotha/pairings/$name.xml")
|
||||||
logger.info("read from file $file")
|
logger.info("read from file $tourFile")
|
||||||
val resource = file.readText(StandardCharsets.UTF_8)
|
val resource = tourFile.readText(StandardCharsets.UTF_8)
|
||||||
var resp = TestAPI.post("/api/tour", resource)
|
var resp = TestAPI.post("/api/tour", resource)
|
||||||
val id = resp.asObject().getInt("id")
|
val id = resp.asObject().getInt("id")
|
||||||
val tournament = TestAPI.get("/api/tour/$id").asObject()
|
val tournament = TestAPI.get("/api/tour/$id").asObject()
|
||||||
@@ -203,15 +200,15 @@ class PairingTests: TestBase() {
|
|||||||
for (round in 1..tournament.getInt("rounds")!!) {
|
for (round in 1..tournament.getInt("rounds")!!) {
|
||||||
val sumOfWeightsOG = compute_sumOfWeight_OG(getTestFile("opengotha/$name/$name" + "_weights_R$round.txt"), pairingsOG[round-1], players)
|
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
|
// 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("sumOfWeightOG = " + dec.format(sumOfWeightsOG))
|
||||||
logger.info("games for round $round: {}", games.toString())
|
logger.info("games for round $round: {}", games.toString())
|
||||||
|
|
||||||
// Compare weights with OpenGotha if legacy mode
|
// Compare weights with OpenGotha if legacy mode
|
||||||
if (legacy) {
|
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) {
|
if (round in forcePairing) {
|
||||||
@@ -223,7 +220,7 @@ class PairingTests: TestBase() {
|
|||||||
val gameOG = pairingsOG[round - 1].getJson(i)!!.asObject()// ["r"] as String?
|
val gameOG = pairingsOG[round - 1].getJson(i)!!.asObject()// ["r"] as String?
|
||||||
val whiteId = gameOG["w"] as Long?
|
val whiteId = gameOG["w"] as Long?
|
||||||
val blackId = gameOG["b"] 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()
|
games = TestAPI.get("/api/tour/$id/res/$round").asArray()
|
||||||
}
|
}
|
||||||
@@ -273,11 +270,10 @@ class PairingTests: TestBase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `SwissTest simpleSwiss`() {
|
fun `SwissTest simpleSwiss`() {
|
||||||
Solver.legacy_mode = true
|
|
||||||
// read tournament with pairing
|
// read tournament with pairing
|
||||||
var file = getTestFile("opengotha/pairings/simpleswiss.xml")
|
var tourFile = getTestFile("opengotha/pairings/simpleswiss.xml")
|
||||||
logger.info("read from file $file")
|
logger.info("read from file $tourFile")
|
||||||
val resource = file.readText(StandardCharsets.UTF_8)
|
val resource = tourFile.readText(StandardCharsets.UTF_8)
|
||||||
var resp = TestAPI.post("/api/tour", resource)
|
var resp = TestAPI.post("/api/tour", resource)
|
||||||
val id = resp.asObject().getInt("id")
|
val id = resp.asObject().getInt("id")
|
||||||
val tournament = TestAPI.get("/api/tour/$id").asObject()
|
val tournament = TestAPI.get("/api/tour/$id").asObject()
|
||||||
@@ -315,10 +311,10 @@ class PairingTests: TestBase() {
|
|||||||
var firstGameID: Int
|
var firstGameID: Int
|
||||||
|
|
||||||
for (round in 1..7) {
|
for (round in 1..7) {
|
||||||
Solver.weightsLogger = PrintWriter(FileWriter(getOutputFile("weights.txt")))
|
val outputFile = getOutputFile("weights.txt")
|
||||||
games = TestAPI.post("/api/tour/$id/pair/$round", Json.Array("all")).asArray()
|
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) + "...")
|
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")
|
assertTrue(compare_games(games, Json.parse(pairingsOG[round - 1])!!.asArray()),"pairings for round $round differ")
|
||||||
logger.info("Pairings for round $round match OpenGotha")
|
logger.info("Pairings for round $round match OpenGotha")
|
||||||
|
|
||||||
@@ -354,12 +350,12 @@ class PairingTests: TestBase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `SwissTest KPMCSplitbug`() {
|
fun `SwissTest KPMCSplitbug`() {
|
||||||
// Let pairgoth use the legacy asymmetric detRandom()
|
// Let pairgoth use the legacy asymmetric detRandom()
|
||||||
Solver.legacy_mode = true
|
val legacy = true
|
||||||
// read tournament with pairing
|
// read tournament with pairing
|
||||||
val name = "20240921-KPMC-Splitbug"
|
val name = "20240921-KPMC-Splitbug"
|
||||||
val file = getTestFile("opengotha/pairings/$name.xml")
|
val tourFile = getTestFile("opengotha/pairings/$name.xml")
|
||||||
logger.info("read from file $file")
|
logger.info("read from file $tourFile")
|
||||||
val resource = file.readText(StandardCharsets.UTF_8)
|
val resource = tourFile.readText(StandardCharsets.UTF_8)
|
||||||
var resp = TestAPI.post("/api/tour", resource)
|
var resp = TestAPI.post("/api/tour", resource)
|
||||||
val id = resp.asObject().getInt("id")
|
val id = resp.asObject().getInt("id")
|
||||||
val tournament = TestAPI.get("/api/tour/$id").asObject()
|
val tournament = TestAPI.get("/api/tour/$id").asObject()
|
||||||
@@ -387,13 +383,13 @@ class PairingTests: TestBase() {
|
|||||||
|
|
||||||
var games: Json.Array
|
var games: Json.Array
|
||||||
var firstGameID: Int
|
var firstGameID: Int
|
||||||
|
val outputFile = getOutputFile("weights.txt")
|
||||||
|
|
||||||
for (round in minRound..maxRound) {
|
for (round in minRound..maxRound) {
|
||||||
val sumOfWeightsOG = compute_sumOfWeight_OG(getTestFile("opengotha/$name/$name" + "_weights_R$round.txt"), pairingsOG[round - minRound], players)
|
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
|
// 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("sumOfWeightOG = " + dec.format(sumOfWeightsOG))
|
||||||
logger.info("games for round $round: {}", games.toString().slice(0..50) + "...")
|
logger.info("games for round $round: {}", games.toString().slice(0..50) + "...")
|
||||||
@@ -401,7 +397,7 @@ class PairingTests: TestBase() {
|
|||||||
// Compare weights with OpenGotha
|
// Compare weights with OpenGotha
|
||||||
assertTrue(
|
assertTrue(
|
||||||
compare_weights(
|
compare_weights(
|
||||||
getOutputFile("weights.txt"),
|
outputFile,
|
||||||
getTestFile("opengotha/$name/$name" + "_weights_R$round.txt")
|
getTestFile("opengotha/$name/$name" + "_weights_R$round.txt")
|
||||||
), "Not matching opengotha weights for round $round"
|
), "Not matching opengotha weights for round $round"
|
||||||
)
|
)
|
||||||
|
@@ -7,6 +7,8 @@ import org.jeudego.pairgoth.server.SSEServlet
|
|||||||
import org.jeudego.pairgoth.server.WebappManager
|
import org.jeudego.pairgoth.server.WebappManager
|
||||||
import org.mockito.kotlin.*
|
import org.mockito.kotlin.*
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.URLDecoder
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.servlet.ReadListener
|
import javax.servlet.ReadListener
|
||||||
@@ -21,20 +23,45 @@ object TestAPI {
|
|||||||
|
|
||||||
fun Any?.toUnit() = Unit
|
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 apiServlet = ApiServlet()
|
||||||
private val sseServlet = SSEServlet()
|
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["auth"] = "none"
|
||||||
WebappManager.properties["store"] = "memory"
|
WebappManager.properties["store"] = "memory"
|
||||||
WebappManager.properties["webapp.env"] = "test"
|
WebappManager.properties["webapp.env"] = "test"
|
||||||
|
|
||||||
|
val (uri, parameters) = parseURL(url)
|
||||||
|
|
||||||
// mock request
|
// mock request
|
||||||
val myHeaderNames = if (reqMethod == "GET") emptyList() else listOf("Content-Type")
|
val myHeaderNames = if (reqMethod == "GET") emptyList() else listOf("Content-Type")
|
||||||
val selector = argumentCaptor<String>()
|
val selector = argumentCaptor<String>()
|
||||||
val subSelector = argumentCaptor<String>()
|
val subSelector = argumentCaptor<String>()
|
||||||
val reqPayload = argumentCaptor<String>()
|
val reqPayload = argumentCaptor<String>()
|
||||||
|
val parameter = argumentCaptor<String>()
|
||||||
val myInputStream = payload?.let { DelegatingServletInputStream(payload.toString().byteInputStream(StandardCharsets.UTF_8)) }
|
val myInputStream = payload?.let { DelegatingServletInputStream(payload.toString().byteInputStream(StandardCharsets.UTF_8)) }
|
||||||
val myReader = payload?.let { BufferedReader(StringReader(payload.toString())) }
|
val myReader = payload?.let { BufferedReader(StringReader(payload.toString())) }
|
||||||
val req = mock<HttpServletRequest> {
|
val req = mock<HttpServletRequest> {
|
||||||
@@ -59,6 +86,7 @@ object TestAPI {
|
|||||||
}
|
}
|
||||||
on { headerNames } doReturn Collections.enumeration(myHeaderNames)
|
on { headerNames } doReturn Collections.enumeration(myHeaderNames)
|
||||||
on { getHeader(eq("Accept")) } doReturn accept
|
on { getHeader(eq("Accept")) } doReturn accept
|
||||||
|
on { getParameter(parameter.capture()) } doAnswer { parameters[parameter.lastValue] }
|
||||||
}
|
}
|
||||||
|
|
||||||
// mock response
|
// mock response
|
||||||
@@ -77,7 +105,7 @@ object TestAPI {
|
|||||||
"DELETE" -> apiServlet.doDelete(req, resp)
|
"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")
|
fun get(uri: String): Json = Json.parse(testRequest<Void>("GET", uri)) ?: throw Error("no payload")
|
||||||
|
Reference in New Issue
Block a user