Skip to content

Commit 01ef653

Browse files
committed
The AocKtExtension now handles spec execution order.
1 parent a0cedad commit 01ef653

File tree

10 files changed

+145
-45
lines changed

10 files changed

+145
-45
lines changed

aockt-test/api/aockt-test.api

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ public abstract class io/github/jadarma/aockt/test/AdventSpec : io/kotest/core/s
2525
public final fun testExecutionMode ()Lio/kotest/engine/concurrency/TestExecutionMode;
2626
}
2727

28-
public final class io/github/jadarma/aockt/test/AocKtExtension : io/kotest/core/extensions/DisplayNameFormatterExtension, io/kotest/core/extensions/SpecExtension {
28+
public final class io/github/jadarma/aockt/test/AocKtExtension : io/kotest/core/extensions/DisplayNameFormatterExtension, io/kotest/core/extensions/SpecExecutionOrderExtension, io/kotest/core/extensions/SpecExtension {
2929
public synthetic fun <init> (JLio/github/jadarma/aockt/test/ExecMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
3030
public synthetic fun <init> (JLio/github/jadarma/aockt/test/ExecMode;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
3131
public fun formatter ()Lio/kotest/engine/names/DisplayNameFormatter;
3232
public fun intercept (Lio/kotest/core/spec/Spec;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
33+
public fun sort (Ljava/util/List;)Ljava/util/List;
3334
}
3435

3536
public final class io/github/jadarma/aockt/test/ExecMode : java/lang/Enum {

aockt-test/src/main/kotlin/AdventDay.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package io.github.jadarma.aockt.test
33
/**
44
* Marks an [AdventSpec] as being the tests of a solution to a specific advent puzzle.
55
*
6-
* @property year The year this puzzle appeared in.
7-
* @property day The day this puzzle appeared in.
8-
* @property title The title of the puzzle. If unspecified will default to the date.
6+
* @property year The year this puzzle appeared in.
7+
* @property day The day this puzzle appeared in.
8+
* @property title The title of the puzzle. If unspecified will default to the date.
99
* @property variant Serves as disambiguation if the project contains multiple solutions for the same day.
1010
*/
1111
@Target(AnnotationTarget.CLASS)
@@ -14,5 +14,5 @@ public annotation class AdventDay(
1414
val year: Int,
1515
val day: Int,
1616
val title: String = "",
17-
val variant: String = "default",
17+
val variant: String = "",
1818
)

aockt-test/src/main/kotlin/AdventSpec.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ import io.github.jadarma.aockt.test.internal.AdventDayPart
55
import io.github.jadarma.aockt.test.internal.AdventDayPart.One
66
import io.github.jadarma.aockt.test.internal.AdventDayPart.Two
77
import io.github.jadarma.aockt.test.internal.AocktDsl
8-
import io.github.jadarma.aockt.test.internal.MissingAdventDayAnnotationException
98
import io.github.jadarma.aockt.test.internal.PuzzleTestData
109
import io.github.jadarma.aockt.test.internal.TestData
10+
import io.github.jadarma.aockt.test.internal.adventDay
1111
import io.github.jadarma.aockt.test.internal.definePart
1212
import io.github.jadarma.aockt.test.internal.id
1313
import io.github.jadarma.aockt.test.internal.injectSolution
1414
import io.kotest.common.ExperimentalKotest
15-
import io.kotest.common.reflection.annotation
1615
import io.kotest.core.spec.IsolationMode
1716
import io.kotest.core.spec.style.FunSpec
1817
import io.kotest.core.test.TestCaseOrder
@@ -64,7 +63,7 @@ public abstract class AdventSpec<T : Solution>(
6463
public val solution: Solution
6564

6665
init {
67-
val adventDay = this::class.annotation<AdventDay>() ?: throw MissingAdventDayAnnotationException(this::class)
66+
val adventDay = this::class.adventDay
6867
solution = injectSolution()
6968
testData = TestData.inputFor(adventDay.id)
7069
body()

aockt-test/src/main/kotlin/AocKtExtension.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package io.github.jadarma.aockt.test
22

33
import io.github.jadarma.aockt.test.internal.AdventSpecConfig
4-
import io.github.jadarma.aockt.test.internal.AocktDisplayNameFormatter
4+
import io.github.jadarma.aockt.test.internal.AocKtDisplayNameFormatter
5+
import io.github.jadarma.aockt.test.internal.SpecOrderer
56
import io.kotest.core.extensions.DisplayNameFormatterExtension
7+
import io.kotest.core.extensions.SpecExecutionOrderExtension
68
import io.kotest.core.extensions.SpecExtension
79
import io.kotest.core.spec.Spec
10+
import io.kotest.core.spec.SpecRef
811
import io.kotest.engine.names.DisplayNameFormatter
912
import kotlinx.coroutines.currentCoroutineContext
1013
import kotlinx.coroutines.withContext
@@ -38,17 +41,11 @@ import kotlin.time.Duration
3841
public class AocKtExtension(
3942
efficiencyBenchmark: Duration = AdventSpecConfig.Default.efficiencyBenchmark,
4043
executionMode: ExecMode = AdventSpecConfig.Default.executionMode,
41-
) : SpecExtension, DisplayNameFormatterExtension {
44+
) : SpecExtension, DisplayNameFormatterExtension, SpecExecutionOrderExtension {
4245

4346
/** The project-level config that will apply to all [AdventSpec]s. */
4447
private val configuration: AdventSpecConfig = AdventSpecConfig(efficiencyBenchmark, executionMode)
4548

46-
/** The formatter to use for [AdventSpec] names. */
47-
private val displayNameFormatter = AocktDisplayNameFormatter
48-
49-
/** Provide the custom formatter to the extension. */
50-
override fun formatter(): DisplayNameFormatter = displayNameFormatter
51-
5249
/**
5350
* Intercept the [spec] execution.
5451
* If it is an [AdventSpec], add the project-level config to its coroutine context.
@@ -60,6 +57,12 @@ public class AocKtExtension(
6057
execute(spec)
6158
}
6259
}
60+
61+
/** Provide the custom formatter to the extension. */
62+
override fun formatter(): DisplayNameFormatter = AocKtDisplayNameFormatter
63+
64+
/** Provide the custom execution order. */
65+
override fun sort(specs: List<SpecRef>): List<SpecRef> = specs.sortedWith(SpecOrderer)
6366
}
6467

6568
/** Configures which inputs the tests will run on. */

aockt-test/src/main/kotlin/internal/AdventSpecExt.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.jadarma.aockt.test.internal
22

33
import io.github.jadarma.aockt.core.Solution
4+
import io.github.jadarma.aockt.test.AdventDay
45
import io.github.jadarma.aockt.test.AdventPartScope
56
import io.github.jadarma.aockt.test.AdventSpec
67
import io.github.jadarma.aockt.test.AocKtExtension
@@ -16,13 +17,18 @@ import io.kotest.matchers.comparables.shouldBeLessThanOrEqualTo
1617
import io.kotest.matchers.shouldBe
1718
import kotlinx.coroutines.currentCoroutineContext
1819
import kotlin.reflect.KClass
20+
import kotlin.reflect.full.findAnnotation
1921
import kotlin.reflect.full.isSubtypeOf
2022
import kotlin.reflect.full.starProjectedType
2123
import kotlin.reflect.jvm.jvmErasure
2224
import kotlin.reflect.typeOf
2325
import kotlin.time.Duration
2426
import kotlin.time.measureTimedValue
2527

28+
/** Return the required-to-be-registered [AdventDay] annotation for this spec. */
29+
internal val KClass<out AdventSpec<*>>.adventDay: AdventDay
30+
get() = findAnnotation<AdventDay>() ?: throw MissingAdventDayAnnotationException(this)
31+
2632
/**
2733
* Construct and inject a solution instance for this [AdventSpec].
2834
* This function is designed to be called at spec instantiation time.

aockt-test/src/main/kotlin/internal/AocktDisplayNameFormatter.kt renamed to aockt-test/src/main/kotlin/internal/AocKtDisplayNameFormatter.kt

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,26 @@ package io.github.jadarma.aockt.test.internal
22

33
import io.github.jadarma.aockt.test.AdventDay
44
import io.github.jadarma.aockt.test.AdventSpec
5-
import io.kotest.common.reflection.annotation
65
import io.kotest.core.test.TestCase
76
import io.kotest.engine.names.DisplayNameFormatter
87
import kotlin.reflect.KClass
98
import kotlin.reflect.full.isSubclassOf
109

1110
/** A name formatter extension that adjusts the names of [AdventSpec]s with the info of their [AdventDay]. */
12-
internal object AocktDisplayNameFormatter : DisplayNameFormatter {
11+
internal object AocKtDisplayNameFormatter : DisplayNameFormatter {
1312

1413
// Test cases are not formatted.
1514
override fun format(testCase: TestCase) = null
1615

1716
override fun format(kclass: KClass<*>): String? {
18-
val annotation = kclass.annotation<AdventDay>() ?: return null
1917
if (!kclass.isSubclassOf(AdventSpec::class)) return null
2018

21-
return with(annotation) {
19+
@Suppress("UNCHECKED_CAST")
20+
return with((kclass as KClass<out AdventSpec<*>>).adventDay) {
2221
buildString {
2322
append(AdventDayID(year, day))
24-
if (title.isNotEmpty()) {
25-
append(": ")
26-
append(title)
27-
}
28-
if (variant != "default") {
29-
append(" (")
30-
append(variant)
31-
append(')')
32-
}
23+
if (title.isNotEmpty()) append(": ", title)
24+
if (variant.isNotEmpty()) append(" (", variant, ")")
3325
}
3426
}
3527
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.github.jadarma.aockt.test.internal
2+
3+
import io.github.jadarma.aockt.test.AdventDay
4+
import io.github.jadarma.aockt.test.AdventSpec
5+
import io.kotest.core.spec.Spec
6+
import io.kotest.core.spec.SpecRef
7+
import kotlin.reflect.KClass
8+
import kotlin.reflect.full.isSubclassOf
9+
10+
/**
11+
* Defines the execution order of [SpecRef]s.
12+
* All non-[AdventSpec] are executed first in the order they were discovered.
13+
* Advent specs are then executed in chronological order.
14+
*/
15+
internal object SpecOrderer : Comparator<SpecRef> {
16+
17+
override fun compare(a: SpecRef, b: SpecRef): Int {
18+
val specA: KClass<out Spec> = a.kclass
19+
val specB: KClass<out Spec> = b.kclass
20+
21+
@Suppress("UNCHECKED_CAST")
22+
return when (specA.isSubclassOf(AdventSpec::class) to specB.isSubclassOf(AdventSpec::class)) {
23+
true to false -> 1
24+
false to true -> -1
25+
false to false -> 0
26+
else -> compareValuesBy(
27+
a = (specA as KClass<out AdventSpec<*>>).adventDay,
28+
b = (specB as KClass<out AdventSpec<*>>).adventDay,
29+
AdventDay::year,
30+
AdventDay::day,
31+
AdventDay::title,
32+
AdventDay::variant,
33+
)
34+
}
35+
}
36+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.github.jadarma.aockt.test.internal
2+
3+
import io.github.jadarma.aockt.core.Solution
4+
import io.github.jadarma.aockt.test.AdventDay
5+
import io.github.jadarma.aockt.test.AdventSpec
6+
import io.kotest.assertions.withClue
7+
import io.kotest.core.spec.SpecRef
8+
import io.kotest.core.spec.style.FunSpec
9+
import io.kotest.matchers.collections.shouldContainInOrder
10+
import io.kotest.matchers.shouldBe
11+
import io.kotest.property.Arb
12+
import io.kotest.property.arbitrary.shuffle
13+
import io.kotest.property.checkAll
14+
15+
class SpecOrdererTest : FunSpec({
16+
17+
test("Orders specs correctly") {
18+
val otherSpecs = listOf(OtherA::class, OtherB::class).map(SpecRef::Reference)
19+
val adventSpecs = listOf(
20+
SpecA::class, SpecB::class, SpecC::class, SpecD::class,
21+
SpecE::class, SpecF::class, SpecG::class, SpecH::class,
22+
).map(SpecRef::Reference)
23+
val allSpecs: List<SpecRef> = adventSpecs + otherSpecs
24+
25+
checkAll(iterations = 128, Arb.shuffle(allSpecs)) { discoveryOrder ->
26+
val otherOrder = discoveryOrder.filter { it in otherSpecs }
27+
val sortedOrder = discoveryOrder.sortedWith(SpecOrderer)
28+
29+
withClue("Sorting changed the relative order of non-AdventSpecs") {
30+
sortedOrder.shouldContainInOrder(otherOrder)
31+
}
32+
33+
withClue("Final sort order is incorrect.") {
34+
val expectedOrder = otherOrder + adventSpecs
35+
sortedOrder.shouldBe(expectedOrder)
36+
}
37+
}
38+
}
39+
})
40+
41+
// Some inactive specs to test with.
42+
private class OtherA : FunSpec()
43+
private class OtherB : FunSpec()
44+
45+
@AdventDay(3000, 1)
46+
private class SpecA : AdventSpec<Solution>()
47+
48+
@AdventDay(3000, 1, variant = "default")
49+
private class SpecB : AdventSpec<Solution>()
50+
51+
@AdventDay(3000, 1, title = "A")
52+
private class SpecC : AdventSpec<Solution>()
53+
54+
@AdventDay(3000, 1, title = "A", variant = "custom")
55+
private class SpecD : AdventSpec<Solution>()
56+
57+
@AdventDay(3000, 1, title = "A", variant = "dumb")
58+
private class SpecE : AdventSpec<Solution>()
59+
60+
@AdventDay(3000, 1, title = "B")
61+
private class SpecF : AdventSpec<Solution>()
62+
63+
@AdventDay(3000, 2)
64+
private class SpecG : AdventSpec<Solution>()
65+
66+
@AdventDay(3001, 1)
67+
private class SpecH : AdventSpec<Solution>()

docs/topics/changelog.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22

33
## 0.3.0-SNAPSHOT
44

5-
**Breaking Changes:**
6-
- Minimum JDK is now 21.
7-
- Minimum Kotlin version is now `2.0`.
8-
- Minimum Kotest version is now `6.0`. Check the [release notes](https://kotest.io/docs/release6), and how to
9-
[configure](project-extension.md) the extension.
5+
- New minimum requirements: JDK 21, Kotlin `2.0`, and Kotest `6.0`.
6+
Check the [release notes](https://kotest.io/docs/release6), and how to [configure](project-extension.md) the extension.
107
- Removed `formatAdventSpecNames` configuration property.
11-
Formatting is automatically enabled when using the project extension.
8+
Formatting is automatically enabled when using the project extension.
9+
- Advent specs are guaranteed to be executed chronologically when the project extension is registered.
1210

1311
## 0.2.1
1412

docs/topics/project-extension.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,18 @@ It offers the following features:
2121
`Y2015D01: Not Quite Lisp (FP)`.
2222
All other specs and tests follow the normal Kotest formatting rules.
2323

24-
## Registering The Extension
24+
- **Automatic Execution Ordering**
2525

26-
To register it, add it to your Kotest [project level config](https://kotest.io/docs/framework/project-config.html):
26+
`AdventSpec`s will always execute in chronological order.
27+
All other specs will run before them, in the order they were discovered.
28+
Note that this overrides Kotest's own [spec ordering`](https://kotest.io/docs/framework/spec-ordering.html).
2729

28-
```kotlin
29-
package my.aoc
30+
## Registering The Extension
3031

31-
import io.github.jadarma.aockt.test.AocKtExtension
32-
import io.github.jadarma.aockt.test.ExecMode
33-
import io.kotest.core.config.AbstractProjectConfig
34-
import io.kotest.core.extensions.Extension
32+
To register it, add it to your Kotest [project level config](https://kotest.io/docs/framework/project-config.html), for
33+
example in `src/test/my/aoc/TestConfig.kt`:
3534

35+
```kotlin
3636
object TestConfig : AbstractProjectConfig() {
3737
override val extensions = listOf<Extension>(
3838
AocKtExtension()
@@ -58,8 +58,6 @@ tasks.test {
5858
}
5959
```
6060

61-
62-
6361
## Configuration Properties
6462

6563
Preferences that can be set as constructor arguments.

0 commit comments

Comments
 (0)