Skip to content

Commit

Permalink
Add tests for splitting and chunking scrambles in PDF (#881)
Browse files Browse the repository at this point in the history
* Migrate Enum.values() to Enum.entries

* Add test cases for PDF chunk splitting

* Yay code reuse

* Add back tailrec modifiers
  • Loading branch information
gregorbg committed Dec 17, 2023
1 parent 767c33f commit 89690e8
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 22 deletions.
Expand Up @@ -22,7 +22,7 @@ enum class EventData(val id: String, val description: String, val scrambler: Puz
constructor(scrambler: PuzzleData, legalFormats: Set<FormatData>) : this(scrambler.id, scrambler.description, scrambler, legalFormats)

companion object {
val WCA_EVENTS = values().associateBy { it.id }.toSortedMap()
val WCA_EVENTS = entries.associateBy { it.id }.toSortedMap()

val ONE_HOUR_EVENTS = setOf(THREE_FM, THREE_MULTI_BLD)
val ATTEMPT_BASED_EVENTS = setOf(THREE_FM, THREE_MULTI_BLD)
Expand Down
Expand Up @@ -8,7 +8,7 @@ enum class FormatData(val key: String, val description: String, val tag: String,
BEST_OF_1("1", "Best of 1", "Bo1", 1);

companion object {
val WCA_FORMATS = values().associateBy { it.key }.toSortedMap()
val WCA_FORMATS = entries.associateBy { it.key }.toSortedMap()

val BIG_AVERAGE_FORMATS = sortedSetOf(AVERAGE_OF_5, BEST_OF_3, BEST_OF_2, BEST_OF_1)
val SMALL_AVERAGE_FORMATS = sortedSetOf(MEAN_OF_3, BEST_OF_2, BEST_OF_1)
Expand Down
Expand Up @@ -62,6 +62,6 @@ enum class PuzzleData(

private val SCRAMBLE_CACHERS = mutableMapOf<String, CoroutineScrambleCacher>()

val WCA_PUZZLES = values().associateBy { it.id }.toSortedMap()
val WCA_PUZZLES = entries.associateBy { it.id }.toSortedMap()
}
}
Expand Up @@ -88,7 +88,7 @@ object FontUtil {
return lineTokens.map { it.joinToStringWithPadding(chunkGlue, padding) }
}

fun splitAtPossibleBreaks(
tailrec fun splitAtPossibleBreaks(
chunksWithBreakFlags: List<Pair<String, Boolean>>,
currentPhraseAccu: List<String> = emptyList(),
accu: List<List<String>> = emptyList()
Expand Down Expand Up @@ -120,7 +120,7 @@ object FontUtil {
}
}

fun splitToMaxFontSizeLines(
tailrec fun splitToMaxFontSizeLines(
chunkSections: List<List<String>>,
boxHeight: Float,
boxWidth: Float,
Expand Down Expand Up @@ -166,7 +166,7 @@ object FontUtil {
return accu.toMutableList().apply { add(element) }
}

private fun splitChunksToLines(
private tailrec fun splitChunksToLines(
chunks: List<List<String>>,
lineRelWidth: Float,
chunkGlue: String,
Expand All @@ -189,7 +189,7 @@ object FontUtil {
}
}

private fun takeChunksThatFitOneLine(
private tailrec fun takeChunksThatFitOneLine(
chunkSections: List<List<String>>,
lineRelativeWidth: Float,
chunkGlue: String,
Expand Down
Expand Up @@ -6,12 +6,12 @@ import kotlin.math.ceil
object ScrambleStringUtil {
const val MOVES_DELIMITER = " "

val NBSP_STRING = Typography.nbsp.toString()
const val NBSP_STRING = Typography.nbsp.toString()

val MIN_ONE_LINE_FONT_SIZE = 15f
val MAX_PHRASE_FONT_SIZE = 20f
const val MIN_ONE_LINE_FONT_SIZE = 15f
const val MAX_PHRASE_FONT_SIZE = 20f

fun padTurnsUniformly(scramble: String, padding: String = NBSP_STRING): String {
private fun padTurnsUniformly(scramble: String, padding: String = NBSP_STRING): String {
val maxTurnLength = scramble.split("\\s+".toRegex()).maxOfOrNull { it.length } ?: 0
val lines = scramble.lines().dropLastWhile { it.isEmpty() }

Expand Down
Expand Up @@ -15,7 +15,7 @@ object ReadmeHandler : RouteHandler {
val scramblesReadmeStream = ReadmeHandler.javaClass.getResourceAsStream("/wca/readme-scramble.md")
val rawReadme = scramblesReadmeStream.bufferedReader().readText()

val scrambleFilteringInfo = PuzzleData.values()
val scrambleFilteringInfo = PuzzleData.entries
.map { it.scrambler }
.joinToString("\n") {
// those 2 spaces at the end are no accident: http://meta.stackoverflow.com/questions/26011/should-the-markdown-renderer-treat-a-single-line-break-as-br
Expand Down
Expand Up @@ -13,7 +13,7 @@ object ApplicationDataHandler : RouteHandler {
override fun install(router: Route) {
router.route("data") {
get("events") {
val eventData = EventData.values().map(EventFrontendData.Companion::fromDataModel)
val eventData = EventData.entries.map(EventFrontendData.Companion::fromDataModel)
call.respond(eventData)
}

Expand Down
Expand Up @@ -9,10 +9,10 @@ enum class Gender(val wcaString: String) {
OTHER("o");

companion object : SingletonStringEncoder<Gender>("Gender") {
fun fromWCAString(wcaString: String) = values().find { it.wcaString == wcaString }
fun fromWCAString(wcaString: String) = entries.find { it.wcaString == wcaString }

override fun encodeInstance(instance: Gender) = instance.wcaString
override fun makeInstance(deserialized: String) = fromWCAString(deserialized)
?: BadWcifParameterException.error("Unknown WCIF spec Gender: '$deserialized'. Valid types: ${values().map { it.wcaString }}")
?: BadWcifParameterException.error("Unknown WCIF spec Gender: '$deserialized'. Valid types: ${entries.map { it.wcaString }}")
}
}
Expand Up @@ -9,10 +9,10 @@ enum class RegistrationStatus(val wcaString: String) {
DELETED("deleted");

companion object : SingletonStringEncoder<RegistrationStatus>("RegistrationStatus") {
fun fromWCAString(wcaString: String) = values().find { it.wcaString == wcaString }
fun fromWCAString(wcaString: String) = entries.find { it.wcaString == wcaString }

override fun encodeInstance(instance: RegistrationStatus) = instance.wcaString
override fun makeInstance(deserialized: String) = fromWCAString(deserialized)
?: BadWcifParameterException.error("Unknown WCIF spec RegistrationStatus: '$deserialized'. Valid types: ${values().map { it.wcaString }}")
?: BadWcifParameterException.error("Unknown WCIF spec RegistrationStatus: '$deserialized'. Valid types: ${entries.map { it.wcaString }}")
}
}
Expand Up @@ -8,10 +8,10 @@ enum class ResultType(val wcaString: String) {
AVERAGE("average");

companion object : SingletonStringEncoder<ResultType>("ResultType") {
fun fromWCAString(wcaString: String) = values().find { it.wcaString == wcaString }
fun fromWCAString(wcaString: String) = entries.find { it.wcaString == wcaString }

override fun encodeInstance(instance: ResultType) = instance.wcaString
override fun makeInstance(deserialized: String) = fromWCAString(deserialized)
?: BadWcifParameterException.error("Unknown WCIF spec ResultType: '$deserialized'. Valid types: ${values().map { it.wcaString }}")
?: BadWcifParameterException.error("Unknown WCIF spec ResultType: '$deserialized'. Valid types: ${entries.map { it.wcaString }}")
}
}
Expand Up @@ -54,7 +54,7 @@ class PdfRenderingTest {

@Test
fun `test that 3+2 scrambles get displayed on one page`() {
for (event in EventData.values()) {
for (event in EventData.entries) {
println("Rendering 3+2 layout for ${event.id}")

repeat(SCRAMBLE_REPETITIONS) {
Expand All @@ -66,7 +66,7 @@ class PdfRenderingTest {

@Test
fun `test that 5+2 scrambles get displayed on one page`() {
for (event in EventData.values()) {
for (event in EventData.entries) {
println("Rendering 5+2 layout for ${event.id}")

repeat(SCRAMBLE_REPETITIONS) {
Expand All @@ -78,7 +78,7 @@ class PdfRenderingTest {

@Test
fun `test that 7+0 scrambles get displayed on one page`() {
for (event in EventData.values()) {
for (event in EventData.entries) {
println("Rendering 7+0 layout for ${event.id}")

repeat(SCRAMBLE_REPETITIONS) {
Expand Down
@@ -0,0 +1,129 @@
package org.worldcubeassociation.tnoodle.server.webscrambles

import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.worldcubeassociation.tnoodle.server.model.EventData
import org.worldcubeassociation.tnoodle.server.webscrambles.pdf.util.FontUtil
import org.worldcubeassociation.tnoodle.server.webscrambles.pdf.util.ScrambleStringUtil

class TokenizerTest {
@Test
fun `test that tokenizing scramble strings does not lose information`() {
for (event in EventData.entries) {
println("Generating random scrambles for ${event.id}")

event.scrambler.generateEfficientScrambles(SCRAMBLE_REPETITIONS) {
val tokenizedScramble = ScrambleStringUtil.splitToTokens(it)
val gluedTogetherScramble = tokenizedScramble.joinToString(ScrambleStringUtil.MOVES_DELIMITER) { (str, _) -> str.trim() }

assertScramblesEqual(event, it, gluedTogetherScramble)
}
}
}

@Test
fun `test that splitting scramble strings does not lose information`() {
for (event in EventData.entries) {
println("Generating random scrambles for ${event.id}")

event.scrambler.generateEfficientScrambles(SCRAMBLE_REPETITIONS) {
val tokenizedScramble = ScrambleStringUtil.split(it)
val gluedTogetherScramble = tokenizedScramble.joinToString(ScrambleStringUtil.MOVES_DELIMITER)

assertScramblesEqual(event, it, gluedTogetherScramble)
}
}
}

@Test
fun `test that splitting to lines does not lose information`() {
for (event in EventData.entries) {
println("Generating random scrambles for ${event.id}")

event.scrambler.generateEfficientScrambles(SCRAMBLE_REPETITIONS) {
val tokenizedScramble = ScrambleStringUtil.splitToTokens(it)
val lineSplitScramble = FontUtil.splitAtPossibleBreaks(tokenizedScramble)

val gluedTogetherScramble = lineSplitScramble.joinToString(ScrambleStringUtil.MOVES_DELIMITER) { line ->
line.joinToString(ScrambleStringUtil.MOVES_DELIMITER) { str -> str.trim() }
}

assertScramblesEqual(event, it, gluedTogetherScramble)
}
}
}

@Test
fun `test that splitting to fixed size does not lose information`() {
for (event in EventData.entries) {
println("Generating random scrambles for ${event.id}")

event.scrambler.generateEfficientScrambles(SCRAMBLE_REPETITIONS) {
val tokenizedScramble = ScrambleStringUtil.splitToTokens(it)
val lineSplitScramble = FontUtil.splitAtPossibleBreaks(tokenizedScramble)

for (fontSize in listOf(3f, 12f, 120f)) {
for (lineWidth in listOf(12f, 120f, 1200f)) {
for (unitToInches in listOf(.2f, 1f, 2f, 2000f)) {
val lines = FontUtil.splitToFixedSizeLines(
lineSplitScramble,
fontSize,
lineWidth,
unitToInches,
ScrambleStringUtil.MOVES_DELIMITER
)

val gluedTogetherScramble = lines.joinToString(ScrambleStringUtil.MOVES_DELIMITER) { str -> str.trim() }
.replace(ScrambleStringUtil.NBSP_STRING, "")

assertScramblesEqual(event, it, gluedTogetherScramble)
}
}
}
}
}
}

@Test
fun `test that splitting to max size does not lose information`() {
for (event in EventData.entries) {
println("Generating random scrambles for ${event.id}")

event.scrambler.generateEfficientScrambles(SCRAMBLE_REPETITIONS) {
val tokenizedScramble = ScrambleStringUtil.splitToTokens(it)
val lineSplitScramble = FontUtil.splitAtPossibleBreaks(tokenizedScramble)

for (boxHeight in listOf(12f, 120f, 1200f)) {
for (boxWidth in listOf(12f, 120f, 1200f)) {
for (leadingFactor in listOf(.9f, 1.2f, 2f)) {
val lines = FontUtil.splitToMaxFontSizeLines(
lineSplitScramble,
boxHeight,
boxWidth,
leadingFactor,
ScrambleStringUtil.MOVES_DELIMITER
)

val gluedTogetherScramble = lines.joinToString(ScrambleStringUtil.MOVES_DELIMITER) { str -> str.trim() }
.replace(ScrambleStringUtil.NBSP_STRING, "")

assertScramblesEqual(event, it, gluedTogetherScramble)
}
}
}
}
}
}

companion object {
const val SCRAMBLE_REPETITIONS = 20

fun assertScramblesEqual(event: EventData, original: String, reconstructed: String) {
// With Megaminx, it is a bit bothersome to reconstruct where the original newlines were.
// Since they are cosmetical anyways, we just ignore them even in the original scramble.
val originalScramble = if (event.id == "minx") original.replace("\n", " ") else original

Assertions.assertEquals(originalScramble, reconstructed)
}
}
}

0 comments on commit 89690e8

Please sign in to comment.