├── aockt-test ├── src │ ├── test │ │ ├── resources │ │ │ └── aockt │ │ │ │ └── y9999 │ │ │ │ ├── d02 │ │ │ │ ├── input.txt │ │ │ │ ├── solution_part2.txt │ │ │ │ └── solution_part1.txt │ │ │ │ └── d01 │ │ │ │ ├── solution_part1.txt │ │ │ │ ├── solution_part2.txt │ │ │ │ └── input.txt │ │ └── kotlin │ │ │ ├── TestConfig.kt │ │ │ ├── integration │ │ │ ├── ExecutionModes.kt │ │ │ ├── ExampleCompilesRunsAndPasses.kt │ │ │ ├── ExpensiveTest.kt │ │ │ ├── DebugMode.kt │ │ │ ├── SolutionTest.kt │ │ │ ├── MultipleSolutions.kt │ │ │ └── AocKtExtensionTest.kt │ │ │ └── internal │ │ │ ├── PuzzleTestDataTest.kt │ │ │ ├── AdventDayPartTest.kt │ │ │ ├── AocKtDisplayNameFormatterTest.kt │ │ │ ├── SpecOrdererTest.kt │ │ │ ├── AdventDayIDTest.kt │ │ │ └── DslTest.kt │ └── main │ │ └── kotlin │ │ ├── internal │ │ ├── AocKtDsl.kt │ │ ├── AdventDebugScopeImpl.kt │ │ ├── AdventPartScopeImpl.kt │ │ ├── AdventDayID.kt │ │ ├── AocKtDisplayNameFormatter.kt │ │ ├── AdventDayPart.kt │ │ ├── SpecOrderer.kt │ │ ├── AocKtException.kt │ │ ├── AdventRootScopeImpl.kt │ │ ├── AdventConfig.kt │ │ ├── TestData.kt │ │ └── AdventSpecExt.kt │ │ ├── Expensive.kt │ │ ├── AdventDebugScope.kt │ │ ├── AdventDay.kt │ │ ├── AdventPartScope.kt │ │ ├── AdventSpec.kt │ │ ├── AocKtExtension.kt │ │ └── AdventRootScope.kt ├── build.gradle.kts └── api │ └── aockt-test.api ├── .gitignore ├── aockt-core ├── build.gradle.kts ├── api │ └── aockt-core.api └── src │ ├── test │ └── kotlin │ │ └── SolutionTest.kt │ └── main │ └── kotlin │ └── Solution.kt ├── docs ├── images │ ├── workflow-run-1.png │ ├── workflow-run-2.png │ ├── workflow-run-3.png │ ├── workflow-run-4.png │ ├── workflow-run-5.png │ └── workflow-run-6.png ├── c.list ├── v.list ├── writerside.cfg ├── aockt.tree ├── topics │ ├── changelog.md │ ├── debugging.md │ ├── test-config.md │ ├── multiple-solutions.md │ ├── project-extension.md │ ├── home.topic │ ├── overview.md │ └── workflow.md └── config │ └── buildprofiles.xml ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── detekt │ ├── config │ │ ├── aockt-test.yml │ │ ├── aockt-core.yml │ │ ├── detekt-test.yml │ │ └── detekt.yml │ └── baseline │ │ ├── aockt-core-main.xml │ │ ├── aockt-core-test.xml │ │ ├── aockt-test-main.xml │ │ └── aockt-test-test.xml ├── build-logic │ ├── settings.gradle.kts │ ├── src │ │ └── main │ │ │ └── kotlin │ │ │ ├── conventions.library.gradle.kts │ │ │ ├── conventions.project.settings.gradle.kts │ │ │ ├── VersionCatalogWorkaround.kt │ │ │ ├── conventions.kotlin.gradle.kts │ │ │ ├── CompileOptions.kt │ │ │ ├── conventions.detekt.gradle.kts │ │ │ ├── conventions.kotest.gradle.kts │ │ │ ├── conventions.publish.gradle.kts │ │ │ ├── conventions.dokka.gradle.kts │ │ │ └── BuildVersion.kt │ └── build.gradle.kts └── libs.versions.toml ├── gradle.properties ├── settings.gradle.kts ├── .gitattributes ├── LICENSE.md ├── .github └── workflows │ ├── build.yml │ ├── publish.yml │ └── docs.yml ├── gradlew.bat ├── README.md └── gradlew /aockt-test/src/test/resources/aockt/y9999/d02/input.txt: -------------------------------------------------------------------------------- 1 | ABC 2 | -------------------------------------------------------------------------------- /aockt-test/src/test/resources/aockt/y9999/d01/solution_part1.txt: -------------------------------------------------------------------------------- 1 | 25 2 | -------------------------------------------------------------------------------- /aockt-test/src/test/resources/aockt/y9999/d02/solution_part2.txt: -------------------------------------------------------------------------------- 1 | 2:3 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | .kotlin 4 | build 5 | local.properties 6 | -------------------------------------------------------------------------------- /aockt-test/src/test/resources/aockt/y9999/d01/solution_part2.txt: -------------------------------------------------------------------------------- 1 | 3628800 2 | -------------------------------------------------------------------------------- /aockt-test/src/test/resources/aockt/y9999/d02/solution_part1.txt: -------------------------------------------------------------------------------- 1 | 1:ABC 2 | -------------------------------------------------------------------------------- /aockt-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("conventions.library") 3 | } 4 | -------------------------------------------------------------------------------- /aockt-test/src/test/resources/aockt/y9999/d01/input.txt: -------------------------------------------------------------------------------- 1 | 1,2,3,4,5,6,7,8,9,10 2 | -------------------------------------------------------------------------------- /docs/images/workflow-run-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jadarma/advent-of-code-kotlin/HEAD/docs/images/workflow-run-1.png -------------------------------------------------------------------------------- /docs/images/workflow-run-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jadarma/advent-of-code-kotlin/HEAD/docs/images/workflow-run-2.png -------------------------------------------------------------------------------- /docs/images/workflow-run-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jadarma/advent-of-code-kotlin/HEAD/docs/images/workflow-run-3.png -------------------------------------------------------------------------------- /docs/images/workflow-run-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jadarma/advent-of-code-kotlin/HEAD/docs/images/workflow-run-4.png -------------------------------------------------------------------------------- /docs/images/workflow-run-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jadarma/advent-of-code-kotlin/HEAD/docs/images/workflow-run-5.png -------------------------------------------------------------------------------- /docs/images/workflow-run-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jadarma/advent-of-code-kotlin/HEAD/docs/images/workflow-run-6.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jadarma/advent-of-code-kotlin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/detekt/config/aockt-test.yml: -------------------------------------------------------------------------------- 1 | naming: 2 | InvalidPackageDeclaration: 3 | rootPackage: 'io.github.jadarma.aockt.test' 4 | requireRootInDeclaration: true 5 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.caching=true 3 | org.gradle.configuration-cache=true 4 | org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m 5 | -------------------------------------------------------------------------------- /gradle/detekt/baseline/aockt-core-main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /gradle/detekt/baseline/aockt-core-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /gradle/detekt/baseline/aockt-test-main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /gradle/detekt/baseline/aockt-test-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/c.list: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/internal/AocKtDsl.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.internal 2 | 3 | @DslMarker 4 | @Retention(AnnotationRetention.SOURCE) 5 | @Target(AnnotationTarget.CLASS) 6 | internal annotation class AocKtDsl 7 | -------------------------------------------------------------------------------- /gradle/build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "build-logic" 2 | 3 | dependencyResolutionManagement { 4 | versionCatalogs { 5 | create("libs") { 6 | from(files("../libs.versions.toml")) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /gradle/detekt/config/aockt-core.yml: -------------------------------------------------------------------------------- 1 | naming: 2 | InvalidPackageDeclaration: 3 | rootPackage: 'io.github.jadarma.aockt.core' 4 | requireRootInDeclaration: true 5 | 6 | exceptions: 7 | NotImplementedDeclaration: 8 | active: false # The solution is intentionally a stub. 9 | -------------------------------------------------------------------------------- /docs/v.list: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "advent-of-code-kotlin" 2 | includeBuild("gradle/build-logic") 3 | include(":aockt-core", ":aockt-test") 4 | 5 | pluginManagement { 6 | repositories { 7 | includeBuild("gradle/build-logic") 8 | } 9 | } 10 | 11 | plugins { 12 | id("conventions.project") 13 | } 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=df67a32e86e3276d011735facb1535f64d0d88df84fa87521e90becc2d735444 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /aockt-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("conventions.library") 3 | } 4 | 5 | dependencies { 6 | api(project(":aockt-core")) 7 | implementation(libs.kotlin.reflect) 8 | implementation(libs.kotest.engine) 9 | implementation(libs.kotest.assertions) 10 | } 11 | 12 | tasks.test { 13 | systemProperty("kotest.framework.config.fqn", "io.github.jadarma.aockt.test.TestConfig") 14 | } 15 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/Expensive.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test 2 | 3 | import io.kotest.core.Tag 4 | 5 | /** 6 | * A tag that marks a spec or test as containing expensive computations. 7 | * 8 | * Useful for marking the test specs for some days (like brute force challenges) so they can be excluded conditionally 9 | * in bulk test executions. 10 | */ 11 | public object Expensive : Tag() 12 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/conventions.library.gradle.kts: -------------------------------------------------------------------------------- 1 | import CompileOptions.AocKt.GROUP_ID 2 | 3 | plugins { 4 | id("conventions.kotlin") 5 | id("conventions.kotest") 6 | id("conventions.detekt") 7 | id("conventions.dokka") 8 | id("conventions.publish") 9 | id("org.jetbrains.kotlinx.binary-compatibility-validator") 10 | } 11 | 12 | group = GROUP_ID 13 | version = buildVersion.get() 14 | -------------------------------------------------------------------------------- /gradle/build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | 10 | dependencies { 11 | implementation(libs.bundles.gradlePlugins) 12 | // Also see `src/main/kotlin/VersionCatalogWorkaround.kt 13 | // https://github.com/gradle/gradle/issues/15383 14 | implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) 15 | } 16 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/conventions.project.settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") 3 | } 4 | 5 | pluginManagement { 6 | repositories { 7 | gradlePluginPortal() 8 | } 9 | } 10 | 11 | @Suppress("UnstableApiUsage") 12 | dependencyResolutionManagement { 13 | repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS 14 | repositories { 15 | mavenCentral() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce LF ending for all files, unless otherwise specified below. 2 | * text eol=lf 3 | 4 | # Preserve CRLF for Windows only files (realistically just gradlew.bat). 5 | *.bat text eol=crlf 6 | 7 | # Use more specific diff drivers for certain files. 8 | *.kt text diff=kotlin 9 | *.kts text diff=kotlin 10 | *.md text diff=markdown 11 | 12 | # Explicitly mark binary files as such. 13 | *.jar binary 14 | *.png binary 15 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/internal/AdventDebugScopeImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.internal 2 | 3 | import io.github.jadarma.aockt.core.Solution 4 | import io.github.jadarma.aockt.test.AdventDebugScope 5 | 6 | internal class AdventDebugScopeImpl( 7 | override val solution: Solution, 8 | private val puzzleInput: PuzzleInput?, 9 | ) : AdventDebugScope { 10 | 11 | override val input: String 12 | get() = (puzzleInput ?: throw MissingInputException()).toString() 13 | } 14 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/VersionCatalogWorkaround.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.accessors.dm.LibrariesForLibs 2 | import org.gradle.api.Project 3 | import org.gradle.kotlin.dsl.getByName 4 | 5 | /** 6 | * VooDoo magic workaround for accessing Gradle Version Catalogs from within precompiled convention scripts. 7 | * Also see: https://github.com/gradle/gradle/issues/15383 8 | */ 9 | val Project.libs: LibrariesForLibs get() = 10 | rootProject.project.extensions.getByName("libs") 11 | -------------------------------------------------------------------------------- /aockt-core/api/aockt-core.api: -------------------------------------------------------------------------------- 1 | public abstract interface class io/github/jadarma/aockt/core/Solution { 2 | public fun partOne (Ljava/lang/String;)Ljava/lang/Object; 3 | public fun partTwo (Ljava/lang/String;)Ljava/lang/Object; 4 | } 5 | 6 | public final class io/github/jadarma/aockt/core/Solution$DefaultImpls { 7 | public static fun partOne (Lio/github/jadarma/aockt/core/Solution;Ljava/lang/String;)Ljava/lang/Object; 8 | public static fun partTwo (Lio/github/jadarma/aockt/core/Solution;Ljava/lang/String;)Ljava/lang/Object; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /docs/writerside.cfg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /gradle/detekt/config/detekt-test.yml: -------------------------------------------------------------------------------- 1 | # Overrides to relax some rules for the test source-sets. 2 | comments: 3 | AbsentOrWrongFileLicense: 4 | active: false 5 | UndocumentedPublicClass: 6 | active: false 7 | UndocumentedPublicFunction: 8 | active: false 9 | UndocumentedPublicProperty: 10 | active: false 11 | 12 | complexity: 13 | StringLiteralDuplication: 14 | active: false 15 | 16 | naming: 17 | BooleanPropertyNaming: 18 | active: false 19 | 20 | potential-bugs: 21 | UnnamedParameterUse: 22 | active: false 23 | 24 | style: 25 | MagicNumber: 26 | active: false 27 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/AdventDebugScope.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test 2 | 3 | import io.github.jadarma.aockt.core.Solution 4 | import io.github.jadarma.aockt.test.internal.AocKtDsl 5 | 6 | /** A DSL scope for defining an isolated run for debugging. */ 7 | @AocKtDsl 8 | public interface AdventDebugScope { 9 | 10 | /** The instance of the solution being tested. */ 11 | public val solution: Solution 12 | 13 | /** 14 | * The actual puzzle input for this puzzle. 15 | * Reading this property while the input is not available results in an error. 16 | */ 17 | public val input: String 18 | } 19 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/conventions.kotlin.gradle.kts: -------------------------------------------------------------------------------- 1 | import CompileOptions.Java 2 | import CompileOptions.Kotlin 3 | import org.gradle.kotlin.dsl.dependencies 4 | 5 | plugins { 6 | kotlin("jvm") 7 | } 8 | 9 | kotlin { 10 | jvmToolchain { 11 | languageVersion = Java.languageVersion 12 | vendor = Java.jvmVendor 13 | } 14 | 15 | compilerOptions { 16 | languageVersion = Kotlin.languageVersion 17 | apiVersion = Kotlin.apiVersion 18 | jvmTarget = Kotlin.jvmTarget 19 | allWarningsAsErrors = true 20 | } 21 | 22 | explicitApi() 23 | } 24 | 25 | dependencies { 26 | implementation(libs.kotlin.stdlib) 27 | } 28 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/AdventDay.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test 2 | 3 | /** 4 | * Marks an [AdventSpec] as being the tests of a solution to a specific advent puzzle. 5 | * 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. 9 | * @property variant Serves as disambiguation if the project contains multiple solutions for the same day. 10 | */ 11 | @Target(AnnotationTarget.CLASS) 12 | @Retention(AnnotationRetention.RUNTIME) 13 | public annotation class AdventDay( 14 | val year: Int, 15 | val day: Int, 16 | val title: String = "", 17 | val variant: String = "", 18 | ) 19 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/internal/AdventPartScopeImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.internal 2 | 3 | import io.github.jadarma.aockt.test.AdventPartScope 4 | 5 | /** A simple part scope implementation that builds a list of example inputs. */ 6 | internal class AdventPartScopeImpl : AdventPartScope { 7 | 8 | private val examples = mutableListOf>() 9 | val testCases: List> get() = examples 10 | 11 | override infix fun String.shouldOutput(expected: Any) { 12 | examples.add(PuzzleInput(this) to PuzzleAnswer(expected.toString())) 13 | } 14 | 15 | override infix fun Iterable.shouldAllOutput(expected: Any) { 16 | forEach { it shouldOutput expected } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/aockt.tree: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/TestConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test 2 | 3 | import io.kotest.core.config.AbstractProjectConfig 4 | import io.kotest.core.extensions.Extension 5 | import io.kotest.core.spec.SpecExecutionOrder 6 | import io.kotest.engine.concurrency.SpecExecutionMode 7 | import kotlin.time.Duration.Companion.milliseconds 8 | 9 | @Suppress("Unused") 10 | object TestConfig : AbstractProjectConfig() { 11 | 12 | override val extensions = listOf( 13 | AocKtExtension( 14 | efficiencyBenchmark = 100.milliseconds, 15 | ) 16 | ) 17 | 18 | override val specExecutionMode = SpecExecutionMode.LimitedConcurrency(Runtime.getRuntime().availableProcessors()) 19 | override val specExecutionOrder = SpecExecutionOrder.Lexicographic 20 | 21 | // https://kotest.io/docs/framework/test_output.html 22 | override var displayFullTestPath: Boolean? = System.getenv("CI").toBoolean() 23 | } 24 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/internal/AdventDayID.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.internal 2 | 3 | import io.github.jadarma.aockt.test.AdventDay 4 | 5 | /** 6 | * Identifies an Advent of Code problem. 7 | * 8 | * @property year The year this problem appeared in. 9 | * @property day The day associated with this problem. 10 | */ 11 | @Suppress("MagicNumber") 12 | internal data class AdventDayID(val year: Int, val day: Int) : Comparable { 13 | 14 | init { 15 | require(year in 2015..9999) { "Invalid year: '$year'." } 16 | require(day in 1..25) { "Invalid day: '$day'. " } 17 | require(year < 2025 || day <= 12) { "Invalid day: '$day' for year: '$year'." } 18 | } 19 | 20 | override fun toString(): String = "Y${year}D${day.toString().padStart(2, '0')}" 21 | override fun compareTo(other: AdventDayID): Int = compareValuesBy(a = this, b = other) { it.year * 100 + it.day } 22 | } 23 | 24 | /** The internal typesafe [AdventDayID] for this [AdventDay]. */ 25 | internal val AdventDay.id: AdventDayID get() = AdventDayID(year, day) 26 | -------------------------------------------------------------------------------- /aockt-core/src/test/kotlin/SolutionTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.core 2 | 3 | import io.kotest.assertions.throwables.shouldThrowExactly 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.shouldBe 6 | import io.kotest.matchers.types.shouldBeInstanceOf 7 | 8 | class SolutionTest : FunSpec({ 9 | 10 | test("Has default implementations.") { 11 | val stub = object : Solution {} 12 | shouldThrowExactly { stub.partOne("foo") } 13 | .message shouldBe "Part 1 not implemented." 14 | shouldThrowExactly { stub.partTwo("bar") } 15 | .message shouldBe "Part 2 not implemented." 16 | } 17 | 18 | test("Passes example implementation.") { 19 | val incrementer = object : Solution { 20 | override fun partOne(input: String): Any = input.toInt().inc() 21 | override fun partTwo(input: String): Any = input.toInt().dec() 22 | } 23 | 24 | incrementer.partOne("1").shouldBeInstanceOf().shouldBe(2) 25 | incrementer.partTwo("1").shouldBeInstanceOf().shouldBe(0) 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Dan Cristian Cîmpianu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/CompileOptions.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.jvm.toolchain.JavaLanguageVersion 2 | import org.gradle.jvm.toolchain.JvmVendorSpec 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 5 | 6 | /** 7 | * Variables to use for various compilation configurations, to allow them to be referenced from precompiled scripts and 8 | * be changed globally. 9 | */ 10 | object CompileOptions { 11 | 12 | object AocKt { 13 | const val GROUP_ID: String = "io.github.jadarma.aockt" 14 | const val CURRENT: String = "0.3.0" // Last released version. 15 | const val NEXT: String = "0.4.0" // Current snapshot. 16 | } 17 | 18 | object Java { 19 | val languageVersion: JavaLanguageVersion = JavaLanguageVersion.of(21) 20 | val jvmVendor: JvmVendorSpec = JvmVendorSpec.ADOPTIUM 21 | } 22 | 23 | object Kotlin { 24 | val languageVersion: KotlinVersion = KotlinVersion.KOTLIN_2_2 25 | val apiVersion: KotlinVersion = KotlinVersion.KOTLIN_2_0 26 | val jvmTarget: JvmTarget = JvmTarget.fromTarget(Java.languageVersion.asInt().toString()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/internal/AocKtDisplayNameFormatter.kt: -------------------------------------------------------------------------------- 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.test.TestCase 6 | import io.kotest.engine.names.DisplayNameFormatter 7 | import kotlin.reflect.KClass 8 | import kotlin.reflect.full.isSubclassOf 9 | 10 | /** A name formatter extension that adjusts the names of [AdventSpec]s with the info of their [AdventDay]. */ 11 | internal object AocKtDisplayNameFormatter : DisplayNameFormatter { 12 | 13 | // Test cases are not formatted. 14 | override fun format(testCase: TestCase) = null 15 | 16 | override fun format(kclass: KClass<*>): String? { 17 | if (!kclass.isSubclassOf(AdventSpec::class)) return null 18 | 19 | @Suppress("UNCHECKED_CAST") 20 | return with((kclass as KClass>).adventDay) { 21 | buildString { 22 | append(AdventDayID(year, day)) 23 | if (title.isNotEmpty()) append(": ", title) 24 | if (variant.isNotEmpty()) append(" (", variant, ")") 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/internal/AdventDayPart.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.internal 2 | 3 | import io.github.jadarma.aockt.core.Solution 4 | import io.github.jadarma.aockt.test.internal.AdventDayPart.One 5 | import io.github.jadarma.aockt.test.internal.AdventDayPart.Two 6 | 7 | /** Selector for a specific part of a [Solution]. */ 8 | internal enum class AdventDayPart { One, Two } 9 | 10 | /** Internal wrapper for processing a puzzle solution. */ 11 | internal typealias PartFunction = (PuzzleInput) -> PuzzleAnswer 12 | 13 | /** Convenience function to return the [PartFunction] of a specific [part] of the [Solution]. */ 14 | internal fun Solution.partFunction(part: AdventDayPart): PartFunction { 15 | val function: (String) -> Any = when (part) { 16 | One -> ::partOne 17 | Two -> ::partTwo 18 | } 19 | return { input: PuzzleInput -> PuzzleAnswer(function(input.toString()).toString()) } 20 | } 21 | 22 | /** Convenience function to return the expected output for a given [part] of the [PuzzleTestData]. */ 23 | internal fun PuzzleTestData.solutionToPart(part: AdventDayPart): PuzzleAnswer? = when (part) { 24 | One -> solutionPartOne 25 | Two -> solutionPartTwo 26 | } 27 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/integration/ExecutionModes.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.integration 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.github.jadarma.aockt.test.ExecMode 7 | import io.kotest.assertions.fail 8 | 9 | class OnlyAcceptsExamples : Solution { 10 | 11 | override fun partOne(input: String) = when (input) { 12 | "1,2,3" -> 4 13 | "1,2,3,4,5,6,7,8,9,10" -> fail("Should not run against user input.") 14 | else -> fail("Misconfigured test.") 15 | } 16 | 17 | override fun partTwo(input: String) = when (input) { 18 | "1,2,3" -> fail("Should not run against examples.") 19 | "1,2,3,4,5,6,7,8,9,10" -> 3_628_800 20 | else -> fail("Misconfigured test.") 21 | } 22 | } 23 | 24 | @AdventDay(9999, 1, "Magic Numbers", "Testing Execution Modes") 25 | class ExecutionModesTest : AdventSpec({ 26 | partOne(executionMode = ExecMode.ExamplesOnly) { 27 | "1,2,3" shouldOutput 4 28 | } 29 | partTwo(executionMode = ExecMode.SkipExamples) { 30 | "1,2,3" shouldOutput 6 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/internal/PuzzleTestDataTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.internal 2 | 3 | import io.kotest.assertions.throwables.shouldThrow 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.shouldBe 6 | 7 | class PuzzleTestDataTest : FunSpec({ 8 | 9 | context("Puzzle inputs") { 10 | 11 | test("cannot be empty") { 12 | shouldThrow { PuzzleInput("") } 13 | } 14 | 15 | test("are inlined") { 16 | PuzzleInput("aaa").toString() shouldBe "aaa" 17 | } 18 | 19 | @Suppress("StringShouldBeRawString") 20 | test("can be previewed") { 21 | val singleLine = PuzzleInput("A") 22 | val multiLine = PuzzleInput("B\n1\n2\nB") 23 | val manyLines = PuzzleInput("C\n1\n2\n3\n4\n5\n6\nC") 24 | 25 | singleLine.preview() shouldBe "A" 26 | multiLine.preview() shouldBe "\nB\n1\n2\nB\n" 27 | manyLines.preview() shouldBe "\nC\n1\n2\n...\nC\n" 28 | } 29 | } 30 | 31 | context("Puzzle answers") { 32 | test("are inlined") { 33 | PuzzleAnswer("answer").toString() shouldBe "answer" 34 | } 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/AdventPartScope.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test 2 | 3 | import io.github.jadarma.aockt.test.internal.AocKtDsl 4 | 5 | /** A DSL scope for defining example assertions for puzzle parts. */ 6 | @AocKtDsl 7 | public interface AdventPartScope { 8 | 9 | /** 10 | * Creates a new test that asserts that when given this string as input, it gets the correct [expected] answer. 11 | * The [expected] value can be anything and will be tested against its `.toString()` value. 12 | * 13 | * @receiver The example puzzle input. 14 | * @param expected The correct answer to the puzzle for the given input. 15 | */ 16 | public infix fun String.shouldOutput(expected: Any) 17 | 18 | /** 19 | * For each of the values given creates a new test that asserts that when given as input, it gets the correct 20 | * [expected] answer. 21 | * The [expected] value can be anything and will be tested against its `.toString()` value. 22 | * 23 | * _NOTE:_ This should be equivalent to calling [shouldOutput] for every input. 24 | * 25 | * @receiver The example puzzle inputs. 26 | * @param expected The correct answer to the puzzle for all given inputs. 27 | */ 28 | public infix fun Iterable.shouldAllOutput(expected: Any) 29 | } 30 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/integration/ExampleCompilesRunsAndPasses.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.integration 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 | 7 | /** A solution to a fictitious puzzle used for testing. */ 8 | class Y9999D01 : Solution { 9 | 10 | private fun parseInput(input: String): List = 11 | input 12 | .splitToSequence(',') 13 | .map(String::toInt) 14 | .toList() 15 | 16 | override fun partOne(input: String) = parseInput(input).filter { it % 2 == 1 }.sum() 17 | 18 | override fun partTwo(input: String) = parseInput(input).reduce { a, b -> a * b } 19 | } 20 | 21 | /** 22 | * A test for a fictitious puzzle. 23 | * 24 | * ```text 25 | * The input is a string of numbers separated by a comma. 26 | * Part 1: Return the sum of the odd numbers. 27 | * Part 2: Return the product of the numbers. 28 | * ``` 29 | */ 30 | @AdventDay(9999, 1, "Magic Numbers") 31 | class Y9999D01Test : AdventSpec({ 32 | 33 | partOne { 34 | "1,2,3" shouldOutput 4 35 | listOf("0", "2,4,6,8", "2,2,2,2") shouldAllOutput 0 36 | "1,2,5" shouldOutput 6 37 | } 38 | 39 | partTwo { 40 | "1,2,3" shouldOutput 6 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/internal/AdventDayPartTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.internal 2 | 3 | import io.github.jadarma.aockt.core.Solution 4 | import io.github.jadarma.aockt.test.internal.AdventDayPart.One 5 | import io.github.jadarma.aockt.test.internal.AdventDayPart.Two 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.matchers.shouldBe 8 | 9 | class AdventDayPartTest : FunSpec({ 10 | 11 | val testData = PuzzleTestData( 12 | input = PuzzleInput("input"), 13 | solutionPartOne = PuzzleAnswer("1:input"), 14 | solutionPartTwo = PuzzleAnswer("2:input"), 15 | ) 16 | 17 | test("Can extract known answer from test data") { 18 | testData.solutionToPart(One) shouldBe testData.solutionPartOne 19 | testData.solutionToPart(Two) shouldBe testData.solutionPartTwo 20 | } 21 | 22 | test("Can extract part function from a solution") { 23 | val solution = object : Solution { 24 | override fun partOne(input: String) = "1:$input" 25 | override fun partTwo(input: String) = "2:$input" 26 | } 27 | 28 | with(testData) { 29 | checkNotNull(input) 30 | solution.partFunction(One).invoke(input) shouldBe solutionPartOne 31 | solution.partFunction(Two).invoke(input) shouldBe solutionPartTwo 32 | } 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/internal/SpecOrderer.kt: -------------------------------------------------------------------------------- 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 { 16 | 17 | override fun compare(a: SpecRef, b: SpecRef): Int { 18 | val specA: KClass = a.kclass 19 | val specB: KClass = 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>).adventDay, 28 | b = (specB as KClass>).adventDay, 29 | AdventDay::year, 30 | AdventDay::day, 31 | AdventDay::title, 32 | AdventDay::variant, 33 | ) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/integration/ExpensiveTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.integration 2 | 3 | import io.github.jadarma.aockt.test.AdventDay 4 | import io.github.jadarma.aockt.test.AdventSpec 5 | import io.github.jadarma.aockt.test.Expensive 6 | import io.kotest.core.test.TestCase 7 | import io.kotest.core.test.isRootTest 8 | import io.kotest.core.test.parents 9 | import io.kotest.engine.test.TestResult 10 | import io.kotest.matchers.collections.shouldContain 11 | import io.kotest.matchers.nulls.shouldNotBeNull 12 | import io.kotest.matchers.shouldBe 13 | 14 | @AdventDay(9999, 2, "ExpensiveTest") 15 | class ExpensiveTest : AdventSpec({ 16 | partOne { "A" shouldOutput "1:A" } 17 | partTwo(expensive = true) { "A" shouldOutput "2:1" } 18 | }) { 19 | 20 | override suspend fun afterTest(testCase: TestCase, result: TestResult) { 21 | val isFromPartTwo = testCase.parents().firstOrNull()?.run { name.name == "Part Two" } 22 | val isEfficiencyTest = testCase.name.name.startsWith("Is reasonably efficient") 23 | if (isEfficiencyTest) result.isIgnored shouldBe isFromPartTwo 24 | } 25 | 26 | override suspend fun afterContainer(testCase: TestCase, result: TestResult) { 27 | if(testCase.isRootTest() && testCase.name.name == "Part Two") { 28 | testCase 29 | .config.shouldNotBeNull() 30 | .tags.shouldContain(Expensive) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/conventions.detekt.gradle.kts: -------------------------------------------------------------------------------- 1 | import dev.detekt.gradle.Detekt 2 | import org.gradle.kotlin.dsl.kotlin 3 | 4 | plugins { 5 | kotlin("jvm") 6 | id("dev.detekt") 7 | } 8 | 9 | detekt { 10 | buildUponDefaultConfig = false 11 | parallel = true 12 | baseline = file("$rootDir/gradle/detekt/baseline/${project.name}.xml") 13 | } 14 | 15 | tasks { 16 | val detektMain: Detekt by named("detektMain") { 17 | config.setFrom( 18 | "$rootDir/gradle/detekt/config/detekt.yml", 19 | "$rootDir/gradle/detekt/config/${project.name}.yml", 20 | ) 21 | } 22 | val detektTest: Detekt by named("detektTest") { 23 | config.setFrom( 24 | "$rootDir/gradle/detekt/config/detekt.yml", 25 | "$rootDir/gradle/detekt/config/${project.name}.yml", 26 | "$rootDir/gradle/detekt/config/detekt-test.yml", 27 | ) 28 | } 29 | 30 | check.configure { 31 | // Replace the default detekt dependency and instead force the use of type resolution. 32 | dependsOn(detektMain, detektTest) 33 | setDependsOn(dependsOn.filterNot { it is TaskProvider<*> && it.name == "detekt" }) 34 | } 35 | 36 | withType().configureEach { 37 | reports { 38 | html.required = true 39 | sarif.required = true 40 | markdown.required = false 41 | checkstyle.required = false 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | 3 | # Action Dependency Pins: 4 | # actions/checkout - 08c6903cd8c0fde910a37f88322edcfb5dd907a8 - v5.0.0 - https://github.com/actions/checkout/releases 5 | # actions/setup-java - dded0888837ed1f317902acf8a20df0ad188d165 - v5.0.0 - https://github.com/actions/setup-java/releases 6 | # actions/upload-artifact - 330a01c490aca151604b8cf639adc76d48f6c5d4 - v5.0.0 - https://github.com/actions/upload-artifact/releases 7 | # gradle/actions/setup-gradle - 4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 - v5.0.0 - https://github.com/gradle/actions/releases 8 | 9 | on: 10 | push: 11 | branches: [ main ] 12 | pull_request: 13 | branches: [ main ] 14 | 15 | jobs: 16 | Build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: 'Checkout' 20 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 21 | - name: 'Setup Java' 22 | uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 23 | with: 24 | java-version: '21' 25 | distribution: 'adopt' 26 | - name: 'Setup Gradle' 27 | uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 28 | - name: 'Build' 29 | run: ./gradlew build 30 | - name: 'Upload Build Reports' 31 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 32 | if: always() 33 | with: 34 | name: 'build-reports' 35 | path: 'aockt-*/build/reports' 36 | -------------------------------------------------------------------------------- /docs/topics/changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.3.0 4 | 5 | _[Released 2025-11-19](https://github.com/Jadarma/advent-of-code-kotlin/releases/tag/v0.3.0)_ 6 | 7 | - New minimum requirements: JDK 21, Kotlin `2.0`, and Kotest `6.0`. 8 | Check the [release notes](https://kotest.io/docs/release6), and how to [configure](project-extension.md) the extension. 9 | - Removed `formatAdventSpecNames` configuration property. 10 | Formatting is automatically enabled when using the project extension. 11 | - Advent specs are guaranteed to be executed chronologically when the project extension is registered. 12 | - Spec definition now uses a dedicated DSL. 13 | - Isolated debug runs can be configured from the DSL. 14 | 15 | ## 0.2.1 16 | 17 | _[Released 2024-11-17](https://github.com/Jadarma/advent-of-code-kotlin/releases/tag/v0.2.1)_ 18 | 19 | - Remove `suspend` modifier from part DSL functions. 20 | It caused a false positive error in IntelliJ, but it was not needed anyway. 21 | 22 | ## 0.2.0 23 | 24 | _[Released 2024-11-17](https://github.com/Jadarma/advent-of-code-kotlin/releases/tag/v0.2.0)_ 25 | 26 | - Updated Kotlin to `1.9.25` and Kotest to `5.9.1`. 27 | - Fixed an issue with spec name formatting. 28 | - Restricted the part DSLs to only call DSL functions. 29 | 30 | ## 0.1.0 31 | 32 | _[Released 2023-08-05](https://github.com/Jadarma/advent-of-code-kotlin/releases/tag/v0.1.0)_ 33 | 34 | This is the first public release of the AocKt packages, offering all the base functionality. 35 | The dependencies can now be fetched from `mavenCentral()`. 36 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/conventions.kotest.gradle.kts: -------------------------------------------------------------------------------- 1 | import CompileOptions.AocKt.GROUP_ID 2 | import org.gradle.api.tasks.testing.Test 3 | import org.gradle.kotlin.dsl.dependencies 4 | import org.gradle.kotlin.dsl.withType 5 | 6 | plugins { 7 | kotlin("jvm") 8 | id("org.jetbrains.kotlinx.kover") 9 | } 10 | 11 | dependencies { 12 | testImplementation(libs.kotest.runner) 13 | testImplementation(libs.kotest.property) 14 | testImplementation(libs.kotest.assertions) 15 | } 16 | 17 | kover { 18 | currentProject { 19 | sources { 20 | includedSourceSets = setOf("main") 21 | } 22 | } 23 | reports { 24 | total { 25 | html { 26 | onCheck = true 27 | charset = "UTF-8" 28 | } 29 | binary { 30 | onCheck = true 31 | } 32 | } 33 | filters { 34 | includes { 35 | classes("$GROUP_ID.*") 36 | } 37 | excludes { 38 | classes("*.*\$DefaultImpls") 39 | } 40 | } 41 | } 42 | } 43 | 44 | tasks.withType().configureEach { 45 | useJUnitPlatform() 46 | 47 | // Don't cache tests, make them run again every time. 48 | outputs.upToDateWhen { false } 49 | 50 | // Pass along system properties for Kotest. 51 | systemProperties = System.getProperties() 52 | .asIterable() 53 | .filter { it.key.toString().startsWith("kotest.") } 54 | .associate { it.key.toString() to it.value } 55 | } 56 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/internal/AocKtDisplayNameFormatterTest.kt: -------------------------------------------------------------------------------- 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.core.spec.style.FunSpec 7 | import io.kotest.matchers.nulls.shouldBeNull 8 | import io.kotest.matchers.shouldBe 9 | 10 | class AocKtDisplayNameFormatterTest : FunSpec({ 11 | 12 | test("Does not format test cases") { 13 | AocKtDisplayNameFormatter.format(testCase).shouldBeNull() 14 | } 15 | 16 | test("Does not format non-advent specs") { 17 | AocKtDisplayNameFormatter 18 | .format(AocKtDisplayNameFormatterTest::class) 19 | .shouldBeNull() 20 | } 21 | 22 | test("Formats names correctly") { 23 | mapOf( 24 | SimpleAdventSpec::class to "Y3000D02", 25 | TitledAdventSpec::class to "Y3000D02: Custom Title", 26 | VariantAdventSpec::class to "Y3000D02 (faster)", 27 | ComplexAdventSpec::class to "Y3000D02: Custom Title (faster)", 28 | ).forEach { (spec, name) -> 29 | AocKtDisplayNameFormatter.format(spec) shouldBe name 30 | } 31 | } 32 | }) 33 | 34 | @AdventDay(3000, 2) 35 | private class SimpleAdventSpec : AdventSpec() 36 | 37 | @AdventDay(3000, 2, title = "Custom Title") 38 | private class TitledAdventSpec : AdventSpec() 39 | 40 | @AdventDay(3000, 2, variant = "faster") 41 | private class VariantAdventSpec : AdventSpec() 42 | 43 | @AdventDay(3000, 2, title = "Custom Title", variant = "faster") 44 | private class ComplexAdventSpec : AdventSpec() 45 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/integration/DebugMode.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.integration 2 | 3 | import io.github.jadarma.aockt.test.AdventDay 4 | import io.github.jadarma.aockt.test.AdventSpec 5 | import io.github.jadarma.aockt.test.internal.MissingInputException 6 | import io.kotest.assertions.throwables.shouldThrowExactly 7 | import io.kotest.core.test.TestCase 8 | import io.kotest.engine.test.TestResult 9 | import io.kotest.matchers.booleans.shouldBeFalse 10 | import io.kotest.matchers.booleans.shouldBeTrue 11 | import io.kotest.matchers.shouldBe 12 | 13 | private var didExecute = false 14 | 15 | @AdventDay(9999, 2, "DebugMode") 16 | class DebugMode : AdventSpec({ 17 | 18 | partOne { 19 | "1" shouldOutput "wait, no it shouldn't output anything" 20 | } 21 | 22 | debug { 23 | didExecute = true 24 | input shouldBe "ABC" 25 | } 26 | }) { 27 | 28 | override suspend fun beforeAny(testCase: TestCase) { 29 | if(testCase.name.name == "Debug") { 30 | testCase.name.focus.shouldBeTrue() 31 | didExecute.shouldBeFalse() 32 | } else { 33 | testCase.name.focus.shouldBeFalse() 34 | } 35 | } 36 | 37 | override suspend fun afterTest(testCase: TestCase, result: TestResult) { 38 | result.isIgnored shouldBe (testCase.name.name != "Debug") 39 | if(testCase.name.name == "Debug") { 40 | didExecute.shouldBeTrue() 41 | } 42 | } 43 | } 44 | 45 | @AdventDay(3000, 6, "DebugMode", variant = "With missing input") 46 | class DebugMode2 : AdventSpec({ 47 | 48 | debug { 49 | shouldThrowExactly{ input } 50 | } 51 | }) 52 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish' 2 | 3 | # Action Dependency Pins: 4 | # actions/checkout - 08c6903cd8c0fde910a37f88322edcfb5dd907a8 - v5.0.0 - https://github.com/actions/checkout/releases 5 | # actions/setup-java - dded0888837ed1f317902acf8a20df0ad188d165 - v5.0.0 - https://github.com/actions/setup-java/releases 6 | # gradle/actions/setup-gradle - 4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 - v5.0.0 - https://github.com/gradle/actions/releases 7 | 8 | on: 9 | workflow_dispatch: 10 | inputs: 11 | isRelease: 12 | description: "Production Release" 13 | type: boolean 14 | required: false 15 | default: false 16 | 17 | jobs: 18 | Publish: 19 | runs-on: ubuntu-latest 20 | env: 21 | RELEASE_BUILD: ${{ inputs.isRelease }} 22 | PUBLISHING_ENABLED: true 23 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 24 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 25 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} 26 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} 27 | steps: 28 | - name: 'Checkout' 29 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 30 | - name: 'Setup Java' 31 | uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 32 | with: 33 | java-version: '21' 34 | distribution: 'adopt' 35 | - name: 'Setup Gradle' 36 | uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 37 | with: 38 | cache-read-only: true 39 | - name: 'Publish' 40 | run: ./gradlew --no-build-cache clean build publishToMavenCentral 41 | -------------------------------------------------------------------------------- /docs/config/buildprofiles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | https://jadarma.github.io/advent-of-code-kotlin 9 | false 10 | true 11 | 12 | 13 | forest 14 | soft 15 | 1200 16 | 17 | 18 | false 19 | true 20 | 21 | 22 | 23 |
24 | 2020-2025 Dan Cristian Cîmpianu | "Advent of Code" is a registered trademark of Eric K. Wastl in the United States. 25 | Source Code 26 | Issue Tracker 27 | Packages 28 | License 29 | Advent Of Code 30 |
31 | 32 | 33 | 34 | 35 |
36 | -------------------------------------------------------------------------------- /aockt-core/src/main/kotlin/Solution.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.core 2 | 3 | /** 4 | * An API for a solution to an Advent Day puzzle. 5 | * 6 | * An implementation **must**: 7 | * - be an `object` or have a no-arg constructor. 8 | * - have independent functions (i.e.: it should be able to solve the second part of the puzzle without having invoked 9 | * the first). 10 | * 11 | * Best practice recommendations: 12 | * - The part functions should be stateless and have no side effects. 13 | * - Keep as much of the solution within your type (except very generic helpers you don't wish to copy-paste). 14 | * - Keep all other members, inner classes, and helper functions as `private`, to simulate a black-box approach. 15 | * - Include the puzzle date in the name of the class, to make it easier to search for when sharing your solutions. 16 | * 17 | * Example: 18 | * ```kotlin 19 | * object Y9999D01 : Solution { 20 | * private fun parseInput(input: String): Sequence = input.splitToSequence(',').map(String::toInt) 21 | * override fun partOne(input: String) = input.sumOf { it >= 42 } 22 | * override fun partTwo(input: String) = input.filter { it < 0 }.count() 23 | * } 24 | * ``` 25 | */ 26 | public interface Solution { 27 | 28 | /** 29 | * Given this [input], computes and returns the resulting answer for part one of the puzzle. 30 | * The function should be pure. 31 | */ 32 | public fun partOne(input: String): Any = throw NotImplementedError("Part 1 not implemented.") 33 | 34 | /** 35 | * Given this [input], computes and returns the resulting answer for part two of the puzzle. 36 | * For days that do not have a part two (25th), this function should not be implemented. 37 | * The function should be pure. 38 | */ 39 | public fun partTwo(input: String): Any = throw NotImplementedError("Part 2 not implemented.") 40 | } 41 | -------------------------------------------------------------------------------- /docs/topics/debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging Solutions 2 | 3 | 4 | 5 | You can define isolated runs to help with debugging. 6 | 7 | 8 | Some puzzles are tricky, you are _so close_ to the right solution but there's a pesky bug hiding in plain sight. 9 | 10 | To hunt for it, it's better to make use of the debugger than to spam your code with `.also(::println)`! 11 | 12 | However, there's a problem. 13 | If you run the whole `AdventSpec` in debug mode, you might not trigger the breakpoint when you expect, because the 14 | solution is run against multiple inputs and examples. 15 | 16 | To fix this, you can define isolated runs. 17 | 18 | ## Debug Scope 19 | 20 | In your `AdventSpec`, you can use the `debug` scope, which will provide the instance of the solution, as well as 21 | your puzzle input, if it is available. 22 | When this scope is used, it defines a focused test, meaning any other parts and examples will be ignored and won't run. 23 | 24 | You can then use the `AdventSpec` gutter icon to run the test in debug mode. 25 | 26 | ```kotlin 27 | @AdventDay(9999, 1, "Magic Numbers") 28 | class Y9999D01 : AdventSpec({ 29 | 30 | debug { 31 | solution.partOne("") 32 | // or, to run on your actual input 33 | solution.partOne(input) 34 | } 35 | 36 | // Any other declarations are effectively ignored ... 37 | }) 38 | ``` 39 | 40 | ## Main Function 41 | 42 | Alternatively, you can define a main function near your solution and call it manually. 43 | However, this method requires you to provide the input yourself. 44 | 45 | IntelliJ will offer a gutter icon, right click it and run it with the debugger: 46 | 47 | ```kotlin 48 | fun main() { 49 | Y9999D01.partOne("") 50 | } 51 | 52 | object Y9999D01 : Solution { /* ... */ } 53 | ``` 54 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/conventions.publish.gradle.kts: -------------------------------------------------------------------------------- 1 | import CompileOptions.AocKt.GROUP_ID 2 | import com.vanniktech.maven.publish.JavadocJar 3 | import com.vanniktech.maven.publish.KotlinJvm 4 | 5 | plugins { 6 | id("com.vanniktech.maven.publish") 7 | } 8 | 9 | mavenPublishing { 10 | 11 | if(System.getenv("PUBLISHING_ENABLED") == "true") { 12 | publishToMavenCentral(automaticRelease = false) 13 | signAllPublications() 14 | } 15 | 16 | configure( 17 | KotlinJvm( 18 | sourcesJar = true, 19 | javadocJar = JavadocJar.Dokka("dokkaGeneratePublicationHtml"), 20 | ) 21 | ) 22 | 23 | coordinates( 24 | groupId = GROUP_ID, 25 | artifactId = project.name, 26 | version = buildVersion.get().toString(), 27 | ) 28 | 29 | pom { 30 | name = "Advent of Code Kotlin" 31 | description = "A simple library that makes running and testing your Kotlin solutions to Advent of Code puzzles a breeze." 32 | url = "https://jadarma.github.io/advent-of-code-kotlin" 33 | inceptionYear = "2020" 34 | 35 | scm { 36 | url = "https://github.com/Jadarma/advent-of-code-kotlin" 37 | connection = "scm:git:git://github.com/Jadarma/advent-of-code-kotlin.git" 38 | developerConnection = "scm:git:ssh://github.com/Jadarma/advent-of-code-kotlin.git" 39 | } 40 | 41 | developers { 42 | developer { 43 | id = "Jadarma" 44 | name = "Dan Cîmpianu" 45 | url = "https://github.com/Jadarma" 46 | email = "dancristiancimpianu@gmail.com" 47 | } 48 | } 49 | 50 | licenses { 51 | license { 52 | name = "MIT License" 53 | url = "https://opensource.org/license/mit" 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/conventions.dokka.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.internal.extensions.stdlib.capitalized 2 | import org.jetbrains.dokka.gradle.engine.parameters.DokkaSourceSetSpec 3 | import org.jetbrains.dokka.gradle.engine.parameters.KotlinPlatform 4 | import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier 5 | import java.time.LocalDate 6 | 7 | plugins { 8 | id("org.jetbrains.dokka") 9 | } 10 | 11 | dokka { 12 | moduleName = "AocKt " + project.name 13 | .removePrefix("aockt-") 14 | .replace('-', ' ') 15 | .capitalized() 16 | 17 | dokkaSourceSets.named("main") { 18 | 19 | val docVersion = buildVersion.get() 20 | if (docVersion.isRelease) { 21 | sourceLink { 22 | val sourceTree = "v${docVersion.version}" 23 | val subProject = project.name 24 | remoteUrl("https://github.com/Jadarma/advent-of-code-kotlin/tree/$sourceTree/$subProject") 25 | remoteLineSuffix = "#L" 26 | } 27 | } 28 | 29 | analysisPlatform = KotlinPlatform.JVM 30 | reportUndocumented = true 31 | documentedVisibilities = setOf(VisibilityModifier.Public) 32 | enableJdkDocumentationLink = false 33 | enableKotlinStdLibDocumentationLink = false 34 | jdkVersion = CompileOptions.Java.languageVersion.asInt() 35 | languageVersion = CompileOptions.Kotlin.languageVersion.version 36 | apiVersion = CompileOptions.Kotlin.apiVersion.version 37 | } 38 | 39 | dokkaPublications.html { 40 | offlineMode = true 41 | suppressObviousFunctions = true 42 | suppressInheritedMembers = true 43 | } 44 | 45 | pluginsConfiguration.html { 46 | separateInheritedMembers = true 47 | homepageLink = "https://jadarma.github.io/advent-of-code-kotlin" 48 | footerMessage = "(c) 2020-${LocalDate.now().year} Dan Cîmpianu" 49 | } 50 | 51 | dokkaGeneratorIsolation = ClassLoaderIsolation() 52 | } 53 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/internal/AocKtException.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.internal 2 | 3 | import io.github.jadarma.aockt.core.Solution 4 | import io.github.jadarma.aockt.test.AdventSpec 5 | import io.github.jadarma.aockt.test.AdventDay 6 | import io.github.jadarma.aockt.test.AdventRootScope 7 | import io.github.jadarma.aockt.test.AdventDebugScope 8 | import io.kotest.common.reflection.bestName 9 | import kotlin.reflect.KClass 10 | 11 | /** Base [Exception] type for all exceptions related to AocKt. */ 12 | internal sealed class AocKtException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) 13 | 14 | /** A general exception thrown upon a misconfiguration event. */ 15 | internal class ConfigurationException(message: String? = null) : AocKtException(message = message) 16 | 17 | /** 18 | * An [AdventSpec] was declared without an [AdventDay] annotation. 19 | * It is required in order to determine test input. 20 | */ 21 | internal class MissingAdventDayAnnotationException(kclass: KClass>) : AocKtException( 22 | message = "Class ${kclass.bestName()} is an AdventSpec but is missing the AdventDay annotation.", 23 | ) 24 | 25 | /** 26 | * While creating an [AdventSpec], the [Solution] to be tested is a type that does not provide a no-arg constructor, 27 | * which is required since [Solution]s should not be stateful. 28 | * Add a no-arg constructor to it or declare it as an object. 29 | */ 30 | internal class MissingNoArgConstructorException(kclass: KClass) : AocKtException( 31 | message = "Class ${kclass.bestName()} is a Solution but it is missing a no-arg constructor.", 32 | ) 33 | 34 | /** An [AdventRootScope] declared the same function twice. */ 35 | internal class DuplicateDefinitionException(spec: KClass<*>, definition: String) : AocKtException( 36 | message = "In ${spec.bestName()}, $definition has been declared twice.", 37 | ) 38 | 39 | /** An [AdventDebugScope] requested the use of the puzzle input but the input is not available. */ 40 | internal class MissingInputException : AocKtException( 41 | message = "Input requested but not provided.", 42 | ) 43 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/integration/SolutionTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.integration 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.github.jadarma.aockt.test.internal.MissingNoArgConstructorException 7 | import io.github.jadarma.aockt.test.internal.injectSolution 8 | import io.kotest.assertions.throwables.shouldThrowExactly 9 | import io.kotest.core.spec.style.FunSpec 10 | import io.kotest.matchers.shouldBe 11 | 12 | open class SolutionImpl : Solution { 13 | override fun partOne(input: String) = "1:$input" 14 | override fun partTwo(input: String) = "2:${input.length}" 15 | } 16 | 17 | class ClassSolution : SolutionImpl() 18 | object ObjectSolution : SolutionImpl() 19 | class ConstructedSolution(val arg: Int) : SolutionImpl() 20 | 21 | class SolutionTest : FunSpec({ 22 | 23 | context("Solution can be instantiated") { 24 | test("from a simple class") { 25 | val solution = ClassSolutionSpec::class.injectSolution() 26 | solution.partOne("A") shouldBe "1:A" 27 | solution.partTwo("A") shouldBe "2:1" 28 | } 29 | 30 | test("from an object class") { 31 | val solution = ObjectSolutionSpec::class.injectSolution() 32 | solution.partOne("A") shouldBe "1:A" 33 | solution.partTwo("A") shouldBe "2:1" 34 | } 35 | 36 | @Suppress("MaxLineLength") 37 | test("but not from a complex class") { 38 | shouldThrowExactly { ConstructedSolutionSpec::class.injectSolution() } 39 | .message.shouldBe("Class io.github.jadarma.aockt.test.integration.ConstructedSolution is a Solution but it is missing a no-arg constructor.") 40 | } 41 | } 42 | }) 43 | 44 | @AdventDay(3000, 1) 45 | private class ClassSolutionSpec : AdventSpec() 46 | 47 | @AdventDay(3000, 4) 48 | private class ObjectSolutionSpec : AdventSpec() 49 | 50 | @AdventDay(3000, 4) 51 | private class ConstructedSolutionSpec : AdventSpec() 52 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/integration/MultipleSolutions.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.integration 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 | 7 | /** A solution to a fictitious puzzle used for testing using collections. */ 8 | class Y9999D01UsingCollections : Solution { 9 | 10 | private fun parseInput(input: String): List = 11 | input 12 | .splitToSequence(',') 13 | .map(String::toInt) 14 | .toList() 15 | 16 | override fun partOne(input: String) = parseInput(input).filter { it % 2 == 1 }.sum() 17 | 18 | override fun partTwo(input: String) = parseInput(input).reduce { a, b -> a * b } 19 | } 20 | 21 | /** A solution to a fictitious puzzle used for testing using sequences. */ 22 | class Y9999D01UsingSequences : Solution { 23 | 24 | private fun parseInput(input: String): Sequence = 25 | input 26 | .splitToSequence(',') 27 | .map(String::toInt) 28 | 29 | override fun partOne(input: String) = parseInput(input).filter { it % 2 == 1 }.sum() 30 | 31 | override fun partTwo(input: String) = parseInput(input).reduce { a, b -> a * b } 32 | } 33 | 34 | @AdventDay(9999, 1, "Magic Numbers","Using Collections") 35 | class Y9999D01CollectionsTest : Y9999D01Spec() 36 | 37 | @AdventDay(9999, 1, "Magic Numbers", "Using Sequences") 38 | class Y9999D01SequencesTest : Y9999D01Spec() 39 | 40 | /** 41 | * A test for a fictitious puzzle. 42 | * 43 | * ```text 44 | * The input is a string of numbers separated by a comma. 45 | * Part 1: Return the sum of the odd numbers. 46 | * Part 2: Return the product of the numbers. 47 | * ``` 48 | */ 49 | @Suppress("AbstractClassCanBeInterface") 50 | abstract class Y9999D01Spec : AdventSpec({ 51 | 52 | partOne { 53 | "1,2,3" shouldOutput 4 54 | listOf("0", "2,4,6,8", "2,2,2,2") shouldAllOutput 0 55 | "1,2,5" shouldOutput 6 56 | } 57 | 58 | partTwo { 59 | "1,2,3" shouldOutput 6 60 | } 61 | }) 62 | -------------------------------------------------------------------------------- /docs/topics/test-config.md: -------------------------------------------------------------------------------- 1 | # Per-Test Config 2 | 3 | 4 | 5 | You can use optional parameters in the DSL to tweak individual puzzle runs. 6 | 7 | 8 | The puzzle part DSL accepts optional properties to allow fine-grained control over test execution and behavior. 9 | They are meant to be used as temporary knobs and dials during puzzle solving and refactoring. 10 | 11 | ## Configuration Properties 12 | 13 | ### `enabled` 14 | 15 | Set to `false` to disable all tests related to this puzzle. 16 | 17 | Useful when you want to focus on the second part once the first one is solved, or ignoring the second part when 18 | refactoring the first, without resorting to commenting out the DSL. 19 | 20 | ### `expensive` 21 | 22 | Marks a solution as known to be inefficient. 23 | 24 | There are hard days when you just want to get it over with for now, and you result to brute force, or simply not enough 25 | optimisations. 26 | Parts that are marked as expensive will skip the efficiency benchmark test. 27 | 28 | > Expensive tests are also tagged. 29 | > If you automate your test runs, you may [tell Kotest to skip some tests](https://kotest.io/docs/framework/tags.html#running-with-tags): 30 | > ```shell 31 | > gradle test -Dkotest.tags="!Expensive" 32 | > ``` 33 | > {style="note"} 34 | 35 | ### `executionMode` 36 | 37 | Override the globally set [`executionMode`](project-extension.md#executionmode). 38 | 39 | This may come in handy during refactoring. 40 | If your solution is working, but expensive, you might want to execute example inputs only first, to save time during 41 | incremental sanity check test runs. 42 | 43 | ### `efficiencyBenchmark` 44 | 45 | Override the globally set [`efficiencyBenchmark`](project-extension.md#efficiencybenchmark). 46 | 47 | If you decided to set a lower global value to challenge yourself, but you did not manage to optimize this puzzle enough, 48 | you can provide a different setting for this puzzle only. 49 | 50 | > Only use this when the solution runs relatively quickly _(say a few seconds)_. 51 | > For longer running solutions, you might want to use `expensive = true` instead. 52 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/integration/AocKtExtensionTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.integration 2 | 3 | import io.github.jadarma.aockt.test.AdventDay 4 | import io.github.jadarma.aockt.test.AdventSpec 5 | import io.github.jadarma.aockt.test.AocKtExtension 6 | import io.github.jadarma.aockt.test.ExecMode 7 | import io.github.jadarma.aockt.test.internal.AdventProjectConfig 8 | import io.github.jadarma.aockt.test.internal.ConfigurationException 9 | import io.kotest.assertions.throwables.shouldThrowExactly 10 | import io.kotest.matchers.nulls.shouldNotBeNull 11 | import io.kotest.matchers.should 12 | import io.kotest.matchers.shouldBe 13 | import kotlinx.coroutines.currentCoroutineContext 14 | import kotlin.time.Duration.Companion.milliseconds 15 | import kotlin.time.Duration.Companion.seconds 16 | 17 | @AdventDay(3000, 5, "AocKtExtensionTest") 18 | class AocKtExtensionTest : AdventSpec() { 19 | 20 | init { 21 | context("Applies config") { 22 | test("and validates it") { 23 | shouldThrowExactly { AocKtExtension(efficiencyBenchmark = (-1).seconds) } 24 | .message 25 | .shouldBe("Efficiency benchmark must be a positive value, but was: -1s") 26 | } 27 | 28 | test("by default") { 29 | AocKtExtension().configuration shouldBe AdventProjectConfig.Default 30 | } 31 | 32 | test("and applies overrides") { 33 | AocKtExtension(executionMode = ExecMode.ExamplesOnly).configuration shouldBe AdventProjectConfig( 34 | efficiencyBenchmark = AdventProjectConfig.Default.efficiencyBenchmark, 35 | executionMode = ExecMode.ExamplesOnly, 36 | ) 37 | } 38 | } 39 | 40 | test("Can be read from advent specs") { 41 | currentCoroutineContext()[AdventProjectConfig.Key] 42 | .shouldNotBeNull() 43 | .should { config -> 44 | config.executionMode shouldBe ExecMode.All 45 | config.efficiencyBenchmark shouldBe 100.milliseconds 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/topics/multiple-solutions.md: -------------------------------------------------------------------------------- 1 | # Multiple Solutions 2 | 3 | You sometimes might want to implement multiple solutions to the same puzzle. 4 | For example, to compare imperative vs. functional code styles, or test different data structures and algorithms. 5 | 6 | Since all these different implementations solve the same puzzle, they should have identical test cases. 7 | The `AdventSpec` is designed to test a single `Solution` at a time. 8 | However, that doesn't mean you need to duplicate the test code! 9 | You can instead define an abstract specification for your test cases, and use it to derive test classes for however many 10 | implementations! 11 | 12 | 13 | 14 | You can use abstract classes to define the same test cases for multiple implementations. 15 | 16 | 17 | ### Example 18 | 19 | Let's learn by _(trivial)_ example. 20 | Assume the fictitious puzzle, in which part one is supposed to add numbers, and part two to multiply them. 21 | We have two implementations _(omitted for brevity)_, `SolutionA` and `SolutionB`. 22 | 23 | The test classes are defined in the same way, each annotated by an `@AdventDay` annotation. 24 | However, instead of extending an `AdventSpec` directly, they instead extend an intermediary abstract class, which 25 | defines the test cases once. 26 | 27 | This method ensures all solutions to a puzzle run against the same tests, and you can add as many 28 | solutions as you want, at the low-low cost of two lines of boilerplate a piece. 29 | 30 | Full code sample: 31 | 32 | ```kotlin 33 | object SolutionA : Solution { /* ... */ } 34 | object SolutionB : Solution { /* ... */ } 35 | 36 | @AdventDay(9999, 1, "Magic Numbers", "Variant A") 37 | class SolutionATest : Y9999D01Spec() 38 | 39 | @AdventDay(9999, 1, "Magic Numbers", "Variant B") 40 | class SolutionBTest : Y9999D01Spec() 41 | 42 | abstract class Y9999D01Spec : AdventSpec({ 43 | partOne { "1,2,3,4" shouldOutput 10 } 44 | partTwo { "1,2,3,4" shouldOutput 24 } 45 | }) 46 | ``` 47 | 48 | > You can provide a `variant` argument to the `@AdventDay` annotation. 49 | > This is purely for aesthetics when formatting the display name. 50 | > {style="note"} 51 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: 'Documentation Site' 2 | 3 | # Action Dependency Pins: 4 | # actions/checkout - 08c6903cd8c0fde910a37f88322edcfb5dd907a8 - v5.0.0 - https://github.com/actions/checkout/releases 5 | # actions/upload-pages-artifact - 7b1f4a764d45c48632c6b24a0339c27f5614fb0b - v4.0.0 - https://github.com/actions/upload-pages-artifact/releases 6 | # actions/deploy-pages - d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e - v4.0.5 - https://github.com/actions/deploy-pages/releases 7 | # JetBrains/writerside-github-action - 73f8f377e7c6a64cce73ca73fe07bbba9a34d556 - v4 - https://github.com/JetBrains/writerside-github-action/releases 8 | # JetBrains/writerside-checker-action - 79ff89902dcd3d5c5a3f26a79bc35830377f38c5 - v1 - https://github.com/JetBrains/writerside-checker-action/releases 9 | 10 | on: 11 | workflow_dispatch: 12 | 13 | jobs: 14 | Build: 15 | runs-on: ubuntu-latest 16 | env: 17 | INSTANCE: 'docs/aockt' 18 | ARTIFACT: 'webHelpAOCKT2-all.zip' 19 | WRS_VERSION: '2025.04.8412' 20 | steps: 21 | - name: 'Checkout Repository' 22 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 23 | - name: 'Build Writerside Docs' 24 | uses: JetBrains/writerside-github-action@73f8f377e7c6a64cce73ca73fe07bbba9a34d556 25 | with: 26 | docker-version: ${{ env.WRS_VERSION }} 27 | instance: ${{ env.INSTANCE }} 28 | artifact: ${{ env.ARTIFACT }} 29 | - name: 'Test Writerside Docs' 30 | uses: JetBrains/writerside-checker-action@79ff89902dcd3d5c5a3f26a79bc35830377f38c5 31 | with: 32 | instance: artifacts/${{ env.INSTANCE }} 33 | - name: 'Unzip Artifact' 34 | run: unzip -qq 'artifacts/${{ env.ARTIFACT }}' -d public 35 | - name: 'Upload Pages Artifact' 36 | uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b 37 | with: 38 | path: ./public 39 | 40 | Deploy: 41 | needs: [ Build ] 42 | runs-on: ubuntu-latest 43 | environment: 44 | name: github-pages 45 | url: ${{ steps.deployment.outputs.page_url }} 46 | permissions: 47 | id-token: write 48 | pages: write 49 | steps: 50 | - name: 'Deploy to GitHub Pages' 51 | id: deployment 52 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e 53 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/internal/SpecOrdererTest.kt: -------------------------------------------------------------------------------- 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 = 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() 47 | 48 | @AdventDay(3000, 1, variant = "default") 49 | private class SpecB : AdventSpec() 50 | 51 | @AdventDay(3000, 1, title = "A") 52 | private class SpecC : AdventSpec() 53 | 54 | @AdventDay(3000, 1, title = "A", variant = "custom") 55 | private class SpecD : AdventSpec() 56 | 57 | @AdventDay(3000, 1, title = "A", variant = "dumb") 58 | private class SpecE : AdventSpec() 59 | 60 | @AdventDay(3000, 1, title = "B") 61 | private class SpecF : AdventSpec() 62 | 63 | @AdventDay(3000, 2) 64 | private class SpecG : AdventSpec() 65 | 66 | @AdventDay(3001, 1) 67 | private class SpecH : AdventSpec() 68 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/internal/AdventRootScopeImpl.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.internal 2 | 3 | import io.github.jadarma.aockt.core.Solution 4 | import io.github.jadarma.aockt.test.AdventDebugScope 5 | import io.github.jadarma.aockt.test.AdventPartScope 6 | import io.github.jadarma.aockt.test.AdventRootScope 7 | import io.github.jadarma.aockt.test.AdventSpec 8 | import io.github.jadarma.aockt.test.ExecMode 9 | import kotlin.reflect.KClass 10 | import kotlin.time.Duration 11 | 12 | internal class AdventRootScopeImpl( 13 | private val owner: KClass>, 14 | ) : AdventRootScope { 15 | 16 | private val solution: Solution = owner.injectSolution() 17 | 18 | var partOne: AdventTestConfig? = null 19 | var partTwo: AdventTestConfig? = null 20 | var debug: AdventDebugConfig? = null 21 | 22 | override fun partOne( 23 | enabled: Boolean, 24 | executionMode: ExecMode?, 25 | efficiencyBenchmark: Duration?, 26 | expensive: Boolean, 27 | examples: AdventPartScope.() -> Unit, 28 | ) { 29 | if (partOne != null) throw DuplicateDefinitionException(owner, "partOne") 30 | partOne = AdventTestConfig( 31 | id = owner.adventDay.id, 32 | part = AdventDayPart.One, 33 | partFunction = solution.partFunction(AdventDayPart.One), 34 | enabled = enabled, 35 | expensive = expensive, 36 | executionMode = executionMode, 37 | efficiencyBenchmark = efficiencyBenchmark, 38 | examples = AdventPartScopeImpl().apply(examples).testCases, 39 | ) 40 | } 41 | 42 | override fun partTwo( 43 | enabled: Boolean, 44 | executionMode: ExecMode?, 45 | efficiencyBenchmark: Duration?, 46 | expensive: Boolean, 47 | examples: AdventPartScope.() -> Unit, 48 | ) { 49 | if (partTwo != null) throw DuplicateDefinitionException(owner, "partTwo") 50 | partTwo = AdventTestConfig( 51 | id = owner.adventDay.id, 52 | part = AdventDayPart.Two, 53 | partFunction = solution.partFunction(AdventDayPart.Two), 54 | enabled = enabled, 55 | expensive = expensive, 56 | executionMode = executionMode, 57 | efficiencyBenchmark = efficiencyBenchmark, 58 | examples = AdventPartScopeImpl().apply(examples).testCases, 59 | ) 60 | } 61 | 62 | override fun debug(test: AdventDebugScope.() -> Unit) { 63 | if (debug != null) throw DuplicateDefinitionException(owner, "debug") 64 | debug = AdventDebugConfig( 65 | id = owner.adventDay.id, 66 | solution = solution, 67 | test = test, 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/AdventSpec.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test 2 | 3 | import io.github.jadarma.aockt.core.Solution 4 | import io.github.jadarma.aockt.test.internal.AdventRootScopeImpl 5 | import io.github.jadarma.aockt.test.internal.registerDebug 6 | import io.github.jadarma.aockt.test.internal.registerTest 7 | import io.kotest.common.ExperimentalKotest 8 | import io.kotest.core.spec.IsolationMode 9 | import io.kotest.core.spec.style.FunSpec 10 | import io.kotest.core.test.TestCaseOrder 11 | import io.kotest.engine.concurrency.TestExecutionMode 12 | import io.kotest.engine.coroutines.CoroutineDispatcherFactory 13 | import io.kotest.engine.coroutines.ThreadPerSpecCoroutineContextFactory 14 | 15 | /** 16 | * A [FunSpec] specialized for testing Advent of Code puzzle [Solution]s. 17 | * The test classes extending this should also provide information about the puzzle with an [AdventDay] annotation. 18 | * 19 | * Example: 20 | * ```kotlin 21 | * import io.github.jadarma.aockt.core.Solution 22 | * import io.github.jadarma.aockt.test.AdventSpec 23 | * import io.github.jadarma.aockt.test.AdventDay 24 | * 25 | * @AdventDay(2015, 1, "Not Quite Lisp") 26 | * class Y2015D01Test : AdventSpec({ 27 | * partOne { 28 | * listOf("(())", "()()") shouldAllOutput 0 29 | * listOf("(((", "(()(()(") shouldAllOutput 3 30 | * listOf("())", "))(") shouldAllOutput -1 31 | * listOf(")))", ")())())") shouldAllOutput -3 32 | * } 33 | * partTwo { 34 | * ")" shouldOutput 1 35 | * "()())" shouldOutput 5 36 | * } 37 | * }) 38 | * ``` 39 | * 40 | * @param T The implementation class of the [Solution] to be tested. 41 | * @param body A context in which to configure the tests. 42 | */ 43 | @Suppress("AbstractClassCanBeConcreteClass") 44 | @OptIn(ExperimentalKotest::class) 45 | public abstract class AdventSpec( 46 | body: AdventRootScope.() -> Unit = {}, 47 | ) : FunSpec() { 48 | 49 | init { 50 | AdventRootScopeImpl(owner = this::class).apply { 51 | body() 52 | partOne?.let(::registerTest) 53 | partTwo?.let(::registerTest) 54 | debug?.let(::registerDebug) 55 | } 56 | } 57 | 58 | // Enforce some configuration to ensure that all tests within one AdventSpec will be executed sequentially on a 59 | // single thread. 60 | final override fun coroutineDispatcherFactory(): CoroutineDispatcherFactory = ThreadPerSpecCoroutineContextFactory 61 | final override fun isolationMode(): IsolationMode = IsolationMode.SingleInstance 62 | final override fun testCaseOrder(): TestCaseOrder = TestCaseOrder.Sequential 63 | final override fun testExecutionMode(): TestExecutionMode = TestExecutionMode.Sequential 64 | } 65 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.2.21" # https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib 3 | kotest = "6.0.5" # https://mvnrepository.com/artifact/io.kotest/kotest-framework-engine 4 | 5 | bcv = "0.18.1" # https://mvnrepository.com/artifact/org.jetbrains.kotlinx.binary-compatibility-validator/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin 6 | detekt = "2.0.0-alpha.1"# https://mvnrepository.com/artifact/dev.detekt/detekt-gradle-plugin 7 | dokka = "2.1.0" # https://mvnrepository.com/artifact/org.jetbrains.dokka/dokka-gradle-plugin 8 | kover = "0.9.3" # https://mvnrepository.com/artifact/org.jetbrains.kotlinx.kover/org.jetbrains.kotlinx.kover.gradle.plugin 9 | publishing = "0.35.0" # https://mvnrepository.com/artifact/com.vanniktech/gradle-maven-publish-plugin 10 | toolchains = "1.0.0" # https://plugins.gradle.org/plugin/org.gradle.toolchains.foojay-resolver-convention 11 | 12 | [libraries] 13 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 14 | kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } 15 | 16 | kotest-engine = { module = "io.kotest:kotest-framework-engine-jvm", version.ref = "kotest" } 17 | kotest-runner = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } 18 | kotest-assertions = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest" } 19 | kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" } 20 | 21 | gradlePluginDep-bcv = { module = "org.jetbrains.kotlinx.binary-compatibility-validator:org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin", version.ref = "bcv" } 22 | gradlePluginDep-detekt = { module = "dev.detekt:dev.detekt.gradle.plugin", version.ref = "detekt" } 23 | gradlePluginDep-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } 24 | gradlePluginDep-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 25 | gradlePluginDep-kover = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } 26 | gradlePluginDep-publishing = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "publishing" } 27 | gradlePluginDep-toolchains = { module = "org.gradle.toolchains.foojay-resolver-convention:org.gradle.toolchains.foojay-resolver-convention.gradle.plugin", version.ref = "toolchains" } 28 | 29 | [bundles] 30 | # Bundles all plugins needed in `buildLogic` so they can be imported in bulk. 31 | gradlePlugins = [ 32 | "gradlePluginDep-bcv", 33 | "gradlePluginDep-detekt", 34 | "gradlePluginDep-dokka", 35 | "gradlePluginDep-kotlin", 36 | "gradlePluginDep-kover", 37 | "gradlePluginDep-publishing", 38 | "gradlePluginDep-toolchains", 39 | ] 40 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/internal/AdventDayIDTest.kt: -------------------------------------------------------------------------------- 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.throwables.shouldThrowExactly 7 | import io.kotest.assertions.withClue 8 | import io.kotest.core.spec.style.FunSpec 9 | import io.kotest.matchers.shouldBe 10 | import io.kotest.property.Arb 11 | import io.kotest.property.arbitrary.shuffle 12 | import io.kotest.property.checkAll 13 | 14 | class AdventDayIDTest : FunSpec({ 15 | 16 | test("Input is validated") { 17 | for (invalidYear in listOf(-2, 1, 2008, 2014, 10_000, 99_999)) { 18 | withClue("Did not reject invalid year: $invalidYear") { 19 | shouldThrowExactly { 20 | AdventDayID(invalidYear, 1) 21 | } 22 | } 23 | } 24 | 25 | for (invalidDay in listOf(-1, 0, 26, 32)) { 26 | withClue("Did not reject invalid day: $invalidDay") { 27 | shouldThrowExactly { 28 | AdventDayID(2015, invalidDay) 29 | } 30 | } 31 | } 32 | 33 | for (invalidDay in listOf(-1, 0, 13, 25)) { 34 | withClue("Did not reject invalid day: $invalidDay") { 35 | shouldThrowExactly { 36 | AdventDayID(2025, invalidDay) 37 | } 38 | } 39 | } 40 | } 41 | 42 | test("Is properly formatted") { 43 | AdventDayID(2015, 1).toString() shouldBe "Y2015D01" 44 | AdventDayID(2345, 12).toString() shouldBe "Y2345D12" 45 | } 46 | 47 | test("Is chronologically ordered") { 48 | 49 | val y2015d25 = AdventDayID(2015, 25) 50 | val y2016d02 = AdventDayID(2016, 2) 51 | val y2016d04 = AdventDayID(2016, 4) 52 | val y2017d01 = AdventDayID(2017, 1) 53 | 54 | val days = listOf(y2015d25, y2016d02, y2016d04, y2017d01) 55 | 56 | withClue("Not sorted correctly") { 57 | checkAll(iterations = 10, Arb.shuffle(days)) { shuffled -> 58 | shuffled.sorted() shouldBe days 59 | } 60 | } 61 | } 62 | 63 | test("Can be derived from AdventDay annotation") { 64 | val annotation = AdventDay(2015, 25) 65 | annotation.id shouldBe AdventDayID(2015, 25) 66 | } 67 | 68 | @Suppress("MaxLineLength") 69 | test("Must be present on AdventSpecs") { 70 | shouldThrowExactly { UnannotatedSpec::class.adventDay } 71 | .message 72 | .shouldBe("Class io.github.jadarma.aockt.test.internal.UnannotatedSpec is an AdventSpec but is missing the AdventDay annotation.") 73 | } 74 | }) 75 | 76 | private class UnannotatedSpec : AdventSpec() 77 | -------------------------------------------------------------------------------- /gradle/build-logic/src/main/kotlin/BuildVersion.kt: -------------------------------------------------------------------------------- 1 | import CompileOptions.AocKt.CURRENT 2 | import CompileOptions.AocKt.NEXT 3 | import org.gradle.api.Project 4 | import org.gradle.api.provider.Provider 5 | import org.gradle.api.provider.ValueSource 6 | import org.gradle.api.provider.ValueSourceParameters 7 | import org.gradle.kotlin.dsl.of 8 | 9 | /** 10 | * A typesafe and validated build version to be shared across the project. 11 | * 12 | * @property version The semver value without suffixes. For the actual version, use the [toString] function. 13 | * @property type The build type based on environment. 14 | */ 15 | data class BuildVersion(val version: String, val type: Type) { 16 | 17 | enum class Type { 18 | /** A pre-release build meant for local consumption only. */ 19 | LOCAL, 20 | 21 | /** A pre-release build meant to be publicly shared. */ 22 | SNAPSHOT, 23 | 24 | /** A productive build. */ 25 | RELEASE; 26 | 27 | fun format(version: String): String = when (this) { 28 | LOCAL -> "$version-LOCAL" 29 | SNAPSHOT -> "$version-SNAPSHOT" 30 | RELEASE -> version 31 | } 32 | } 33 | 34 | init { 35 | require(version.matches(semVer)) { 36 | "Invalid version: $version." 37 | } 38 | } 39 | 40 | override fun toString(): String = type.format(version) 41 | 42 | // Convenience helpers. 43 | @Suppress("unused") 44 | val isRelease: Boolean get() = type == Type.RELEASE 45 | 46 | @Suppress("unused") 47 | val isSnapshot: Boolean get() = type == Type.SNAPSHOT 48 | 49 | @Suppress("unused") 50 | val isLocal: Boolean get() = type == Type.LOCAL 51 | 52 | internal companion object { 53 | private val semVer = Regex("""^\d+\.\d+\.\d+$""") 54 | 55 | private val versions: Map = Type.values().associateWith { 56 | BuildVersion( 57 | version = if (it == Type.RELEASE) CURRENT else NEXT, 58 | type = it, 59 | ) 60 | } 61 | 62 | fun get(isRelease: Boolean, isPublished: Boolean): BuildVersion = versions.getValue( 63 | when { 64 | isRelease -> Type.RELEASE 65 | isPublished -> Type.SNAPSHOT 66 | else -> Type.LOCAL 67 | } 68 | ) 69 | } 70 | } 71 | 72 | /** Provides the build version of the project depending on environment variables. */ 73 | internal abstract class BuildVersionValueSource : ValueSource { 74 | override fun obtain(): BuildVersion = BuildVersion.get( 75 | isRelease = System.getenv("RELEASE_BUILD") == "true", 76 | isPublished = System.getenv("PUBLISHING_ENABLED") == "true", 77 | ) 78 | } 79 | 80 | /** Get the active build version of the project. */ 81 | val Project.buildVersion: Provider 82 | get() = providers.of(BuildVersionValueSource::class) {} 83 | -------------------------------------------------------------------------------- /docs/topics/project-extension.md: -------------------------------------------------------------------------------- 1 | # Project Extension 2 | 3 | 4 | 5 | You can register the AocKt extension to configure test execution and extend functionality. 6 | 7 | 8 | Registering the extension is optional, but recommended. 9 | It offers the following features: 10 | 11 | - **Global Configuration** 12 | 13 | You can configure your own defaults for [test execution](test-config.md) parameters. 14 | Otherwise, the same defaults will be used. 15 | See [below](#configuration-properties) for a detailed description of each parameter. 16 | 17 | - **Display Name Formatting** 18 | 19 | All `AdventSpec`s will have a nicely formatted display name derived from their `@AdventDay` annotation. 20 | For example, `@AdventDay(2015, 1, "Not Quite Lisp", "FP")` will become 21 | `Y2015D01: Not Quite Lisp (FP)`. 22 | All other specs and tests follow the normal Kotest formatting rules. 23 | 24 | - **Automatic Execution Ordering** 25 | 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). 29 | 30 | ## Registering The Extension 31 | 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`: 34 | 35 | ```kotlin 36 | object TestConfig : AbstractProjectConfig() { 37 | override val extensions = listOf( 38 | AocKtExtension() 39 | ) 40 | } 41 | ``` 42 | 43 | To make Kotest use this configuration, you must register the FQN as a system property for Gradle: 44 | 45 | ```kotlin 46 | tasks.test { 47 | systemProperty("kotest.framework.config.fqn", "my.aoc.TestConfig") 48 | } 49 | ``` 50 | 51 | ## Configuration Properties 52 | 53 | Preferences that can be set as constructor arguments. 54 | 55 | ### `efficiencyBenchmark` 56 | 57 | Only applies to tests against user input, not examples. 58 | If the solution completes under this time value, it will pass the efficiency test. 59 | 60 | You can lower this value if you want to further challenge yourself, but careful when going too low, as JVM 61 | execution times depend on warm-up and might lead to flaky tests. 62 | 63 | The default value is 15 seconds, a reference to the [about page](https://adventofcode.com/about), which states that 64 | _"every problem has a solution that completes in at most 15 seconds on ten-year-old hardware"_, if you go above that it 65 | usually means you did not find the intended solution. 66 | 67 | ### `executionMode` 68 | 69 | Determines which tests will run. 70 | 71 | Possible values are: 72 | 73 | - `All`: Runs all the tests it can find. The default behavior. 74 | - `ExamplesOnly`: Will not run against user inputs even if they are present. 75 | Useful when running a project with encrypted inputs _(e.g.: running a clone of someone else's solution repo)_. 76 | - `SkipExamples`: Will only run against user inputs even if examples are defined. 77 | Useful when all you care about is ensuring your solutions still give the correct answer. 78 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/internal/AdventConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.internal 2 | 3 | import io.github.jadarma.aockt.core.Solution 4 | import io.github.jadarma.aockt.test.AdventDebugScope 5 | import io.github.jadarma.aockt.test.AdventSpec 6 | import io.github.jadarma.aockt.test.ExecMode 7 | import kotlin.coroutines.AbstractCoroutineContextElement 8 | import kotlin.coroutines.CoroutineContext 9 | import kotlin.time.Duration 10 | import kotlin.time.Duration.Companion.seconds 11 | 12 | /** 13 | * Holds configurations for how an [AdventSpec] should behave by default when running tests. 14 | * 15 | * @property efficiencyBenchmark What is the maximum runtime a solution can have while being considered efficient by 16 | * the time tests. 17 | * @property executionMode The default execution mode for puzzle part definitions. 18 | */ 19 | internal data class AdventProjectConfig( 20 | val efficiencyBenchmark: Duration, 21 | val executionMode: ExecMode, 22 | ) : AbstractCoroutineContextElement(Key) { 23 | 24 | init { 25 | if (efficiencyBenchmark.isPositive().not()) { 26 | throw ConfigurationException("Efficiency benchmark must be a positive value, but was: $efficiencyBenchmark") 27 | } 28 | } 29 | 30 | companion object Key : CoroutineContext.Key { 31 | /** Sane defaults. */ 32 | val Default: AdventProjectConfig = AdventProjectConfig( 33 | efficiencyBenchmark = 15.seconds, 34 | executionMode = ExecMode.All, 35 | ) 36 | } 37 | } 38 | 39 | @Suppress("BooleanPropertyNaming") 40 | internal data class AdventTestConfig( 41 | val id: AdventDayID, 42 | val part: AdventDayPart, 43 | val partFunction: PartFunction, 44 | val enabled: Boolean, 45 | val expensive: Boolean, 46 | val executionMode: ExecMode?, 47 | val efficiencyBenchmark: Duration?, 48 | val examples: List>, 49 | ) { 50 | 51 | data class ForExamples( 52 | val enabled: Boolean, 53 | val partFunction: PartFunction, 54 | val examples: List>, 55 | ) 56 | 57 | data class ForInput( 58 | val id: AdventDayID, 59 | val part: AdventDayPart, 60 | val enabled: Boolean, 61 | val partFunction: PartFunction, 62 | val efficiencyBenchmark: Duration, 63 | ) 64 | } 65 | 66 | internal data class AdventDebugConfig( 67 | val id: AdventDayID, 68 | val solution: Solution, 69 | val test: AdventDebugScope.() -> Unit, 70 | ) 71 | 72 | internal fun AdventTestConfig.forExamples(defaults: AdventProjectConfig): AdventTestConfig.ForExamples = 73 | AdventTestConfig.ForExamples( 74 | enabled = if (!enabled) false else (executionMode ?: defaults.executionMode) != ExecMode.SkipExamples, 75 | partFunction = partFunction, 76 | examples = examples, 77 | ) 78 | 79 | internal fun AdventTestConfig.forInput(defaults: AdventProjectConfig): AdventTestConfig.ForInput = 80 | AdventTestConfig.ForInput( 81 | id = id, 82 | part = part, 83 | enabled = if (!enabled) false else (executionMode ?: defaults.executionMode) != ExecMode.ExamplesOnly, 84 | partFunction = partFunction, 85 | efficiencyBenchmark = efficiencyBenchmark ?: defaults.efficiencyBenchmark, 86 | ) 87 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/AocKtExtension.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test 2 | 3 | import io.github.jadarma.aockt.test.internal.AdventProjectConfig 4 | import io.github.jadarma.aockt.test.internal.AocKtDisplayNameFormatter 5 | import io.github.jadarma.aockt.test.internal.SpecOrderer 6 | import io.kotest.core.extensions.DisplayNameFormatterExtension 7 | import io.kotest.core.extensions.SpecExecutionOrderExtension 8 | import io.kotest.core.extensions.SpecExtension 9 | import io.kotest.core.spec.Spec 10 | import io.kotest.core.spec.SpecRef 11 | import io.kotest.engine.names.DisplayNameFormatter 12 | import kotlinx.coroutines.currentCoroutineContext 13 | import kotlinx.coroutines.withContext 14 | import kotlin.time.Duration 15 | 16 | /** 17 | * A Kotest Extension to configure the AdventSpecs. 18 | * 19 | * To register the extension: 20 | * 21 | * ```kotlin 22 | * object TestConfig : AbstractProjectConfig() { 23 | * override val extensions = listOf(AocKtExtension()) 24 | * } 25 | * ``` 26 | * 27 | * @param efficiencyBenchmark What is the maximum runtime a solution can have while being considered efficient by 28 | * the time tests. 29 | * Can be overridden on a per-test basis. 30 | * According to the AoC website, all solutions *should* be completable in 15 seconds regardless of hardware. 31 | * You may decrease this value for an increased challenge though do be aware of fluctuations in execution time due to 32 | * JVM warmup. 33 | * Default is fifteen seconds. 34 | * @param executionMode The default execution mode for puzzle part definitions. 35 | * Can be overridden on a per-test basis. 36 | * If set to `ExamplesOnly`, does not run against the true puzzle input even if present. 37 | * Useful when running the project with encrypted inputs (e.g. running a clone of someone else's solution repo). 38 | * If set to `SkipExamples`, will only test against user input. 39 | * Default is `All`. 40 | */ 41 | public class AocKtExtension( 42 | efficiencyBenchmark: Duration = AdventProjectConfig.Default.efficiencyBenchmark, 43 | executionMode: ExecMode = AdventProjectConfig.Default.executionMode, 44 | ) : DisplayNameFormatterExtension, SpecExecutionOrderExtension, SpecExtension { 45 | 46 | /** The project-level config that will apply to all [AdventSpec]s. */ 47 | internal val configuration: AdventProjectConfig = AdventProjectConfig(efficiencyBenchmark, executionMode) 48 | 49 | /** Provide the custom formatter to the extension. */ 50 | override fun formatter(): DisplayNameFormatter = AocKtDisplayNameFormatter 51 | 52 | /** Provide the custom execution order. */ 53 | override fun sort(specs: List): List = specs.sortedWith(SpecOrderer) 54 | 55 | /** Provide project-level config to scopes of all advent specs. */ 56 | override suspend fun intercept( 57 | spec: Spec, 58 | execute: suspend (Spec) -> Unit, 59 | ) { 60 | if (spec is AdventSpec<*>) { 61 | withContext(currentCoroutineContext() + configuration) { execute(spec) } 62 | } else { 63 | execute(spec) 64 | } 65 | } 66 | } 67 | 68 | /** Configures which inputs the tests will run on. */ 69 | public enum class ExecMode { 70 | /** Run both tests and the user input, if available. */ 71 | All, 72 | 73 | /** Do not run the user input, even if available. */ 74 | ExamplesOnly, 75 | 76 | /** Do not run the defined examples. */ 77 | SkipExamples; 78 | } 79 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/AdventRootScope.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test 2 | 3 | import io.github.jadarma.aockt.core.Solution 4 | import io.github.jadarma.aockt.test.internal.AocKtDsl 5 | import kotlin.time.Duration 6 | 7 | /** A DSL scope for enabling and tweaking configs of part tests. */ 8 | @AocKtDsl 9 | public interface AdventRootScope { 10 | 11 | /** 12 | * Provides a context to test the implementation a [Solution.partOne] function. 13 | * Should be called at most once per scope. 14 | * 15 | * @param enabled If set to false, the root context will be registered but not executed. 16 | * @param executionMode Specifies which tests defined for this part will be enabled as an optional override 17 | * for this test only. 18 | * If not provided, the project-default will be used. 19 | * @param efficiencyBenchmark Specifies the maximum amount of time the solution can take to finish to be considered 20 | * efficient as an optional override for this test only. 21 | * If not provided, the project-default will be used. 22 | * @param expensive Whether this part is known to produce answers in a longer timespan. 23 | * If enabled, the tests will be tagged as such, and efficiency benchmark tests will be 24 | * skipped. 25 | * @param examples Test the solution against example inputs defined in this [AdventPartScope]. 26 | */ 27 | public fun partOne( 28 | enabled: Boolean = true, 29 | executionMode: ExecMode? = null, 30 | efficiencyBenchmark: Duration? = null, 31 | expensive: Boolean = false, 32 | examples: AdventPartScope.() -> Unit = {}, 33 | ) 34 | 35 | /** 36 | * Provides a context to test the implementation a [Solution.partTwo] function. 37 | * Should be called at most once per scope. 38 | * 39 | * @param enabled If set to false, the root context will be registered but not executed. 40 | * @param executionMode Specifies which tests defined for this part will be enabled as an optional override 41 | * for this test only. 42 | * If not provided, the project-default will be used. 43 | * @param efficiencyBenchmark Specifies the maximum amount of time the solution can take to finish to be considered 44 | * efficient as an optional override for this test only. 45 | * If not provided, the project-default will be used. 46 | * @param expensive Whether this part is known to produce answers in a longer timespan. 47 | * If enabled, the tests will be tagged as such, and efficiency benchmark tests will be 48 | * skipped. 49 | * @param examples Test the solution against example inputs defined in this [AdventPartScope]. 50 | */ 51 | public fun partTwo( 52 | enabled: Boolean = true, 53 | executionMode: ExecMode? = null, 54 | efficiencyBenchmark: Duration? = null, 55 | expensive: Boolean = false, 56 | examples: AdventPartScope.() -> Unit = {}, 57 | ) 58 | 59 | /** 60 | * If used, ignores all other tests, and only runs this [test] lambda. 61 | * The solution instance being tested is exposed in the scope. 62 | * Useful when running the test in debug mode, to ensure only this test will trigger any breakpoints. 63 | */ 64 | public fun debug(test: AdventDebugScope.() -> Unit) 65 | } 66 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/internal/TestData.kt: -------------------------------------------------------------------------------- 1 | package io.github.jadarma.aockt.test.internal 2 | 3 | import io.github.jadarma.aockt.core.Solution 4 | import java.util.concurrent.ConcurrentHashMap 5 | 6 | /** 7 | * Reads [PuzzleTestData]s from classpath resources. 8 | * 9 | * Looks into the `/aockt` directory, which it expects to be split by year, then by day, then by type. 10 | * 11 | * For example, the following is the valid directory tree for `Y2015D01`: 12 | * 13 | * ```text 14 | * resources 15 | * └─ aockt 16 | * └─ y2015 17 | * └─ d01 18 | * ├─ input.txt 19 | * ├─ solution_part1.txt 20 | * └─ solution_part2.txt 21 | * ``` 22 | */ 23 | internal object TestData { 24 | 25 | private val data: ConcurrentHashMap = ConcurrentHashMap() 26 | 27 | /** Returns the available [PuzzleTestData] for a given [AdventDayID] by reading it from the resources. */ 28 | fun inputFor(adventDayID: AdventDayID): PuzzleTestData = data.computeIfAbsent(adventDayID) { 29 | val path = with(adventDayID) { "/aockt/y$year/d${day.toString().padStart(2, '0')}" } 30 | PuzzleTestData( 31 | input = readResourceAsTextOrNull("$path/input.txt").toPuzzleInput(), 32 | solutionPartOne = readResourceAsTextOrNull("$path/solution_part1.txt").toPuzzleAnswer(), 33 | solutionPartTwo = readResourceAsTextOrNull("$path/solution_part2.txt").toPuzzleAnswer(), 34 | ) 35 | } 36 | 37 | /** Reads a resource at the given [path] and returns its contents as text, if it exists. */ 38 | private fun readResourceAsTextOrNull(path: String): String? = 39 | this::class.java 40 | .getResourceAsStream(path) 41 | ?.use { String(it.readAllBytes()).trimEnd() } 42 | 43 | /** Wraps a [String] into a [PuzzleAnswer] type. */ 44 | private fun String?.toPuzzleAnswer(): PuzzleAnswer? = when (this) { 45 | null -> null 46 | else -> PuzzleAnswer(this) 47 | } 48 | 49 | /** Wraps a [String] into a [PuzzleInput] type. */ 50 | private fun String?.toPuzzleInput(): PuzzleInput? = when (this) { 51 | null -> null 52 | else -> PuzzleInput(this) 53 | } 54 | } 55 | 56 | /** 57 | * Data holder for the test data of a puzzle [Solution]. 58 | * 59 | * @property input The actual, user specific puzzle input, read from resources. 60 | * @property solutionPartOne The correct solution for part one given the input. If null, is currently unknown. 61 | * @property solutionPartTwo The correct solution for part two given the input. If null, is currently unknown. 62 | */ 63 | internal data class PuzzleTestData( 64 | val input: PuzzleInput?, 65 | val solutionPartOne: PuzzleAnswer?, 66 | val solutionPartTwo: PuzzleAnswer?, 67 | ) 68 | 69 | /** The user-specific input to a puzzle. */ 70 | @JvmInline 71 | internal value class PuzzleInput(private val input: String) { 72 | 73 | init { 74 | require(input.isNotBlank()) { "A puzzle input must be a non-blank string!" } 75 | } 76 | 77 | override fun toString(): String = input 78 | 79 | /** Formats the input in a printable friendly manner. */ 80 | @Suppress("MagicNumber") 81 | fun preview(): String = when (input.count { it == '\n' }) { 82 | 0 -> input 83 | in 1..5 -> "\n$input\n" 84 | else -> buildString { 85 | val lines = input.lines() 86 | appendLine() 87 | lines.take(3).forEach(::appendLine) 88 | appendLine("...") 89 | appendLine(lines.last()) 90 | } 91 | } 92 | } 93 | 94 | /** An answer given by a [Solution] that was given a [PuzzleInput]. */ 95 | @JvmInline 96 | internal value class PuzzleAnswer(private val answer: String) { 97 | override fun toString() = answer 98 | } 99 | -------------------------------------------------------------------------------- /aockt-test/api/aockt-test.api: -------------------------------------------------------------------------------- 1 | public abstract interface annotation class io/github/jadarma/aockt/test/AdventDay : java/lang/annotation/Annotation { 2 | public abstract fun day ()I 3 | public abstract fun title ()Ljava/lang/String; 4 | public abstract fun variant ()Ljava/lang/String; 5 | public abstract fun year ()I 6 | } 7 | 8 | public abstract interface class io/github/jadarma/aockt/test/AdventDebugScope { 9 | public abstract fun getInput ()Ljava/lang/String; 10 | public abstract fun getSolution ()Lio/github/jadarma/aockt/core/Solution; 11 | } 12 | 13 | public abstract interface class io/github/jadarma/aockt/test/AdventPartScope { 14 | public abstract fun shouldAllOutput (Ljava/lang/Iterable;Ljava/lang/Object;)V 15 | public abstract fun shouldOutput (Ljava/lang/String;Ljava/lang/Object;)V 16 | } 17 | 18 | public abstract interface class io/github/jadarma/aockt/test/AdventRootScope { 19 | public abstract fun debug (Lkotlin/jvm/functions/Function1;)V 20 | public abstract fun partOne-BZiP2OM (ZLio/github/jadarma/aockt/test/ExecMode;Lkotlin/time/Duration;ZLkotlin/jvm/functions/Function1;)V 21 | public static synthetic fun partOne-BZiP2OM$default (Lio/github/jadarma/aockt/test/AdventRootScope;ZLio/github/jadarma/aockt/test/ExecMode;Lkotlin/time/Duration;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V 22 | public abstract fun partTwo-BZiP2OM (ZLio/github/jadarma/aockt/test/ExecMode;Lkotlin/time/Duration;ZLkotlin/jvm/functions/Function1;)V 23 | public static synthetic fun partTwo-BZiP2OM$default (Lio/github/jadarma/aockt/test/AdventRootScope;ZLio/github/jadarma/aockt/test/ExecMode;Lkotlin/time/Duration;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V 24 | } 25 | 26 | public final class io/github/jadarma/aockt/test/AdventRootScope$DefaultImpls { 27 | public static synthetic fun partOne-BZiP2OM$default (Lio/github/jadarma/aockt/test/AdventRootScope;ZLio/github/jadarma/aockt/test/ExecMode;Lkotlin/time/Duration;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V 28 | public static synthetic fun partTwo-BZiP2OM$default (Lio/github/jadarma/aockt/test/AdventRootScope;ZLio/github/jadarma/aockt/test/ExecMode;Lkotlin/time/Duration;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V 29 | } 30 | 31 | public abstract class io/github/jadarma/aockt/test/AdventSpec : io/kotest/core/spec/style/FunSpec { 32 | public fun ()V 33 | public fun (Lkotlin/jvm/functions/Function1;)V 34 | public synthetic fun (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 35 | public final fun coroutineDispatcherFactory ()Lio/kotest/engine/coroutines/CoroutineDispatcherFactory; 36 | public final fun isolationMode ()Lio/kotest/core/spec/IsolationMode; 37 | public final fun testCaseOrder ()Lio/kotest/core/test/TestCaseOrder; 38 | public final fun testExecutionMode ()Lio/kotest/engine/concurrency/TestExecutionMode; 39 | } 40 | 41 | public final class io/github/jadarma/aockt/test/AocKtExtension : io/kotest/core/extensions/DisplayNameFormatterExtension, io/kotest/core/extensions/SpecExecutionOrderExtension, io/kotest/core/extensions/SpecExtension { 42 | public synthetic fun (JLio/github/jadarma/aockt/test/ExecMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 43 | public synthetic fun (JLio/github/jadarma/aockt/test/ExecMode;Lkotlin/jvm/internal/DefaultConstructorMarker;)V 44 | public fun formatter ()Lio/kotest/engine/names/DisplayNameFormatter; 45 | public fun intercept (Lio/kotest/core/spec/Spec;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 46 | public fun sort (Ljava/util/List;)Ljava/util/List; 47 | } 48 | 49 | public final class io/github/jadarma/aockt/test/ExecMode : java/lang/Enum { 50 | public static final field All Lio/github/jadarma/aockt/test/ExecMode; 51 | public static final field ExamplesOnly Lio/github/jadarma/aockt/test/ExecMode; 52 | public static final field SkipExamples Lio/github/jadarma/aockt/test/ExecMode; 53 | public static fun getEntries ()Lkotlin/enums/EnumEntries; 54 | public static fun valueOf (Ljava/lang/String;)Lio/github/jadarma/aockt/test/ExecMode; 55 | public static fun values ()[Lio/github/jadarma/aockt/test/ExecMode; 56 | } 57 | 58 | public final class io/github/jadarma/aockt/test/Expensive : io/kotest/core/Tag { 59 | public static final field INSTANCE Lio/github/jadarma/aockt/test/Expensive; 60 | } 61 | 62 | -------------------------------------------------------------------------------- /docs/topics/home.topic: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Advent of Code Kotlin (AocKt) 9 | 10 | A simple library that makes running and testing your Kotlin solutions to Advent of Code puzzles a breeze. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Start Solving Puzzles! 20 | 21 | Project Template 22 | 23 | 24 | 25 | Advanced Use-Cases 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Join The AoC Community 36 | Official AoC Website 37 | Community AoC Subreddit 38 | Community AoC Discord 39 | 40 | 41 | Learn Kotlin 42 | Kotlin Documentation 43 | Kotlin Subreddit 44 | 45 | 46 | More Resources 47 | Awesome AoC 48 | Author's Solutions 49 | 50 | 51 | 52 | 53 | Dokka API Docs 54 | io.github.jadarma.aockt:aockt-core 55 | io.github.jadarma.aockt:aockt-test 56 | 57 | 58 | Maven Central Artifacts 59 | io.github.jadarma.aockt:aockt-core 60 | io.github.jadarma.aockt:aockt-test 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/topics/overview.md: -------------------------------------------------------------------------------- 1 | # Features and Overview 2 | 3 | AocKt _(short for Advent of Code - Kotlin)_ is a simple library that makes running and testing your Kotlin solutions to 4 | [Advent of Code](https://adventofcode.com) puzzles a breeze. 5 | 6 | It is an opinionated testing framework built on [Kotest](https://kotest.io/) that defines a new `AdventSpec` specialized 7 | for testing AoC puzzle solutions with minimal boilerplate. 8 | 9 | ## ✨ Features 10 | 11 | - **Completely Offline** - Puzzle inputs and solutions are read from local files, no need for tokens. 12 | - **Test-Driven** - Run your code from unit tests for faster feedback loops and fearless refactorings. 13 | - **DSL-Driven** - Define your test cases with minimal code. 14 | - **Configurable** - You decide what runs and when using optional parameters. 15 | - **Minimal** - The test framework is the only non-Kotlin dependency. 16 | 17 | ## ⚡ Quick Start 18 | 19 | 20 | 21 | 22 | For your convenience, there is an 23 | advent-of-code-kotlin-template 24 | repository which you can use to generate your own solutions repo. 25 | It comes with a pre-configured Gradle project with all bells and whistles you might need, as well as a modified source 26 | structure for easier navigation. 27 | 28 | _(If you need a working example, check out [my solutions repo](https://github.com/Jadarma/advent-of-code-kotlin-solutions).)_ 29 | 30 | 31 | 32 | 33 | To add AocKt to your existing project, simply add the dependencies and configure your unit tests to run with Kotest: 34 | 35 | ```kotlin 36 | plugins { 37 | kotlin("jvm") version "%kotlin-version%" 38 | } 39 | 40 | repositories { 41 | mavenCentral() 42 | } 43 | 44 | dependencies { 45 | implementation("io.github.jadarma.aockt:aockt-core:%aockt-version%") 46 | testImplementation("io.github.jadarma.aockt:aockt-test:%aockt-version%") 47 | testImplementation("io.kotest:kotest-runner-junit5:%kotest-version%") 48 | } 49 | 50 | tasks.test { 51 | useJUnitPlatform() 52 | } 53 | ``` 54 | 55 | 56 | 57 | To consume pre-release builds, you must register the snapshot repository.\ 58 | Replace `x.x.x-SNAPSHOT` with the version from the badge below _(if it exists)_: 59 | 60 | ![Maven SNAPSHOT](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fcentral.sonatype.com%2Frepository%2Fmaven-snapshots%2Fio%2Fgithub%2Fjadarma%2Faockt%2Faockt-test%2Fmaven-metadata.xml&strategy=latestProperty&style=flat-square&logo=apachemaven&logoColor=orange&label=Snapshot&color=orange) 61 | 62 | ```kotlin 63 | plugins { 64 | kotlin("jvm") version "%kotlin-version%" 65 | } 66 | 67 | repositories { 68 | mavenCentral() 69 | maven { 70 | url = uri("https://central.sonatype.com/repository/maven-snapshots/") 71 | content { includeGroup("io.github.jadarma.aockt") } 72 | } 73 | } 74 | 75 | dependencies { 76 | implementation("io.github.jadarma.aockt:aockt-core:x.x.x-SNAPSHOT") 77 | testImplementation("io.github.jadarma.aockt:aockt-test:x.x.x-SNAPSHOT") 78 | testImplementation("io.kotest:kotest-runner-junit5:%kotest-version%") 79 | } 80 | 81 | tasks.test { 82 | useJUnitPlatform() 83 | } 84 | ``` 85 | 86 | **PS:** 87 | You can also [submit an issue](https://github.com/Jadarma/advent-of-code-kotlin/issues) if you find bugs or have suggestions. 88 | Thank you for trying out the latest development build! 💚 89 | 90 | 91 | 92 | 93 | ## 🧪 Test DSL Overview 94 | 95 | AocKt provides the following DSL for testing puzzle solutions: 96 | 97 | ```kotlin 98 | object Y9999D01 : Solution { // 1. 99 | override fun partOne(input: String) = spoilers() 100 | override fun partTwo(input: String) = spoilers() 101 | } 102 | 103 | @AdventDay(9999, 1, "Magic Numbers") // 2. 104 | class Y9999D01Test : AdventSpec({ // 3. 105 | partOne { // 4. 106 | "1,2,3,4" shouldOutput 4 // 5. 107 | listOf("2", "2,2", "2,4,6,8") shouldAllOutput 0 // 6. 108 | } 109 | partTwo() // 7. 110 | }) 111 | ``` 112 | 113 | 114 | In the above example: 115 | 116 | 1. Your solution should implement the `Solution` interface. 117 | 2. Each test class should be annotated with the `@AdventDay` annotation. Title is optional, but the year and day are 118 | required. 119 | 3. Rather than passing it as an instance, the `AdventSpec` takes in your solution as a type parameter. 120 | 4. Use the `partOne` and `partTwo` functions as needed. 121 | Inside the lambda you can define test cases. 122 | The `Solution` functions will only be invoked if the relevant part DSL is used. 123 | If you have not yet implemented the second part, or it doesn't exist 124 | _(e.g.: Every year, part two of the last day just requires collecting all other stars)_, 125 | then you may simply omit it. 126 | 5. To define a test case, use the `shouldOutput` function. 127 | Each usage will define another test case. 128 | The value tested against is checked against its string value, so `shouldOutput 4` and `shouldOutput "4"` are 129 | equivalent. 130 | 6. As a shorthand for defining multiple examples that should output the same thing, use the `shouldAllOutput` function. 131 | 7. If you don't have any examples, but do want to run the part against your input the lambda can be omitted. 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code - Kotlin (AocKt) 2 | 3 | [![Kotlin](https://img.shields.io/badge/Kotlin-2.2.21-%237F52FF.svg?style=flat-square&logo=kotlin&logoColor=%237F52FF)](https://kotlinlang.org/) 4 | [![Kotest](https://img.shields.io/badge/Kotest-6.0.5-%35ED35.svg?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggc3R5bGU9ImZpbGw6IzM1ZWQzNSIgZD0iTTEyIDJoNGwtOCA4IDQgNEg0di00TDAgNmg4WiIvPjwvc3ZnPg==)](https://kotest.io/) 5 | [![Build Status](https://img.shields.io/github/actions/workflow/status/Jadarma/advent-of-code-kotlin/build.yml?style=flat-square&logo=github&label=Build&logoColor=%23171515)](https://github.com/Jadarma/advent-of-code-kotlin/actions/workflows/build.yml) 6 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.jadarma.aockt/aockt-test?style=flat-square&logo=apachemaven&logoColor=blue&label=Maven%20Central&color=blue)](https://central.sonatype.com/namespace/io.github.jadarma.aockt) 7 | [![Maven SNAPSHOT](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fcentral.sonatype.com%2Frepository%2Fmaven-snapshots%2Fio%2Fgithub%2Fjadarma%2Faockt%2Faockt-test%2Fmaven-metadata.xml&strategy=latestProperty&style=flat-square&logo=apachemaven&logoColor=orange&label=Snapshot&color=orange)](https://jadarma.github.io/advent-of-code-kotlin/overview.html#snapshot) 8 | 9 | AocKt _(short for Advent of Code - Kotlin)_ is a simple library that makes running and testing your Kotlin solutions to 10 | [Advent of Code](https://adventofcode.com) puzzles a breeze. 11 | 12 | It is an opinionated testing framework built on [Kotest](https://kotest.io/) that defines a new `AdventSpec` specialized 13 | for testing AoC puzzle solutions with minimal boilerplate. 14 | 15 | ## 📑 Documentation 16 | 17 | Visit the [project website](https://jadarma.github.io/advent-of-code-kotlin) for installation instructions, 18 | DSL documentation, workflow guides, advanced configuration options, and more! 19 | 20 | ## ✨ Features 21 | 22 | - **Completely Offline** - Puzzle inputs and solutions are read from local files, no need for tokens. 23 | - **Test-Driven** - Run your code from unit tests for faster feedback loops and fearless refactorings. 24 | - **DSL-Driven** - Define your test cases with minimal code. 25 | - **Configurable** - You decide what runs and when using optional parameters. 26 | - **Minimal** - The test framework is the only non-Kotlin dependency. 27 | 28 | ## ⚡ Quick Start 29 | 30 |
31 | Project Template 32 | 33 | For your convenience, there is an 34 | [advent-of-code-kotlin-template](https://github.com/Jadarma/advent-of-code-kotlin-template) repository which you can 35 | use to generate your own solutions repo. 36 | It comes with a pre-configured Gradle project with all bells and whistles you might need, as well as a 37 | modified source structure for easier navigation. 38 | 39 | _(If you need a working example, check out [my solutions repo](https://github.com/Jadarma/advent-of-code-kotlin-solutions).)_ 40 | 41 |
42 | 43 |
44 | Standalone Gradle Project 45 | 46 | To add AocKt to your existing project, simply add the dependencies and configure your unit tests to run with Kotest: 47 | 48 | ```kotlin 49 | plugins { 50 | kotlin("jvm") version "$kotlinVersion" 51 | } 52 | 53 | repositories { 54 | mavenCentral() 55 | } 56 | 57 | dependencies { 58 | implementation("io.github.jadarma.aockt:aockt-core:$aocktVersion") 59 | testImplementation("io.github.jadarma.aockt:aockt-test:$aocktVersion") 60 | testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") 61 | } 62 | 63 | tasks.test { 64 | useJUnitPlatform() 65 | } 66 | ``` 67 |
68 | 69 | ## 🧪 Test DSL Overview 70 | 71 | AocKt provides the following DSL for testing puzzle solutions: 72 | 73 | ```kotlin 74 | object Y9999D01 : Solution { // 1. 75 | override fun partOne(input: String) = spoilers() 76 | override fun partTwo(input: String) = spoilers() 77 | } 78 | 79 | @AdventDay(9999, 1, "Magic Numbers") // 2. 80 | class Y9999D01Test : AdventSpec({ // 3. 81 | partOne { // 4. 82 | "1,2,3,4" shouldOutput 4 // 5. 83 | listOf("2", "2,2", "2,4,6,8") shouldAllOutput 0 // 6. 84 | } 85 | partTwo() // 7. 86 | }) 87 | ``` 88 | 89 | In the above example: 90 | 91 | 1. Your solution should implement the `Solution` interface. 92 | 2. Each test class should be annotated with the `@AdventDay` annotation. Title is optional, but the year and day are 93 | required. 94 | 3. Rather than passing it as an instance, the `AdventSpec` takes in your solution as a type parameter. 95 | 4. Use the `partOne` and `partTwo` functions as needed. 96 | Inside the lambda you can define test cases. 97 | The `Solution` functions will only be invoked if the relevant part DSL is used. 98 | If you have not yet implemented the second part, or it doesn't exist 99 | _(e.g.: Every year, part two of the last day just requires collecting all other stars)_, 100 | then you may simply omit it. 101 | 5. To define a test case, use the `shouldOutput` function. 102 | Each usage will define another test case. 103 | The value tested against is checked against its string value, so `shouldOutput 4` and `shouldOutput "4"` are 104 | equivalent. 105 | 6. As a shorthand for defining multiple examples that should output the same thing, use the `shouldAllOutput` function. 106 | 7. If you don't have any examples, but do want to run the part against your input the lambda can be omitted. 107 | 108 | ## 👥 Contributing 109 | 110 | If you'd like to help out: 111 | 112 | - Report bugs and submit feature requests via an [issue](https://github.com/Jadarma/advent-of-code-kotlin/issues). 113 | - If this helped you unlock some stars, consider giving one to this repo! ⭐ 114 | 115 | ## ⚖ License 116 | 117 | This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) for details.\ 118 | _Advent of Code_ is a registered trademark of Eric K. Wastl in the United States. 119 | -------------------------------------------------------------------------------- /aockt-test/src/test/kotlin/internal/DslTest.kt: -------------------------------------------------------------------------------- 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.github.jadarma.aockt.test.ExecMode 6 | import io.github.jadarma.aockt.test.integration.ObjectSolution 7 | import io.kotest.assertions.throwables.shouldThrowExactly 8 | import io.kotest.core.spec.style.FunSpec 9 | import io.kotest.matchers.booleans.shouldBeFalse 10 | import io.kotest.matchers.booleans.shouldBeTrue 11 | import io.kotest.matchers.collections.shouldBeEmpty 12 | import io.kotest.matchers.nulls.shouldBeNull 13 | import io.kotest.matchers.nulls.shouldNotBeNull 14 | import io.kotest.matchers.should 15 | import io.kotest.matchers.shouldBe 16 | import kotlin.time.Duration.Companion.seconds 17 | 18 | class DslTest : FunSpec({ 19 | 20 | context("Cannot register duplicate") { 21 | val scope = AdventRootScopeImpl(SomeSpec::class).apply { 22 | partOne() 23 | partTwo() 24 | debug {} 25 | } 26 | test("partOne scopes") { 27 | shouldThrowExactly { scope.partOne() } 28 | .message 29 | .shouldBe("In io.github.jadarma.aockt.test.internal.SomeSpec, partOne has been declared twice.") 30 | } 31 | test("partTwo scopes") { 32 | shouldThrowExactly { scope.partTwo() } 33 | .message 34 | .shouldBe("In io.github.jadarma.aockt.test.internal.SomeSpec, partTwo has been declared twice.") 35 | } 36 | test("debug scopes") { 37 | shouldThrowExactly { scope.debug {} } 38 | .message 39 | .shouldBe("In io.github.jadarma.aockt.test.internal.SomeSpec, debug has been declared twice.") 40 | } 41 | } 42 | 43 | context("Builds correct configuration") { 44 | val scope = AdventRootScopeImpl(SomeSpec::class) 45 | 46 | test("For default part entry") { 47 | scope.partOne.shouldBeNull() 48 | scope.partOne() 49 | scope.partOne.shouldNotBeNull().should { config -> 50 | config.part shouldBe AdventDayPart.One 51 | config.partFunction.invoke(PuzzleInput("A")).toString() shouldBe "1:A" 52 | config.enabled.shouldBeTrue() 53 | config.expensive.shouldBeFalse() 54 | config.executionMode.shouldBeNull() 55 | config.efficiencyBenchmark.shouldBeNull() 56 | config.examples.shouldBeEmpty() 57 | } 58 | } 59 | 60 | test("For complex part entry") { 61 | scope.partTwo.shouldBeNull() 62 | scope.partTwo( 63 | enabled = false, 64 | executionMode = ExecMode.SkipExamples, 65 | efficiencyBenchmark = 5.seconds, 66 | expensive = true, 67 | ) { 68 | "ABC" shouldOutput "2:3" 69 | listOf("A", "B", "C") shouldAllOutput "2:1" 70 | } 71 | 72 | scope.partTwo.shouldNotBeNull().should { config -> 73 | config.part shouldBe AdventDayPart.Two 74 | config.partFunction.invoke(PuzzleInput("ABC")).toString() shouldBe "2:3" 75 | config.enabled.shouldBeFalse() 76 | config.expensive.shouldBeTrue() 77 | config.executionMode shouldBe ExecMode.SkipExamples 78 | config.efficiencyBenchmark shouldBe 5.seconds 79 | config.examples shouldBe listOf( 80 | PuzzleInput("ABC") to PuzzleAnswer("2:3"), 81 | PuzzleInput("A") to PuzzleAnswer("2:1"), 82 | PuzzleInput("B") to PuzzleAnswer("2:1"), 83 | PuzzleInput("C") to PuzzleAnswer("2:1"), 84 | ) 85 | } 86 | } 87 | 88 | test("For debug entry.") { 89 | var wasInvoked = false 90 | 91 | scope.debug.shouldBeNull() 92 | scope.debug { 93 | wasInvoked = true 94 | solution shouldBe ObjectSolution 95 | input shouldBe "B" 96 | } 97 | 98 | scope.debug.shouldNotBeNull().should { config -> 99 | wasInvoked.shouldBeFalse() 100 | config.test.invoke(AdventDebugScopeImpl(config.solution, PuzzleInput("B"))) 101 | wasInvoked.shouldBeTrue() 102 | } 103 | } 104 | } 105 | 106 | context("Configurations compute final test configs") { 107 | val configA = AdventTestConfig( 108 | id = AdventDayID(9999, 2), 109 | part = AdventDayPart.One, 110 | partFunction = ObjectSolution.partFunction(AdventDayPart.One), 111 | enabled = true, 112 | expensive = true, 113 | executionMode = ExecMode.SkipExamples, 114 | efficiencyBenchmark = 10.seconds, 115 | examples = listOf(PuzzleInput("B") to PuzzleAnswer("1:B")), 116 | ) 117 | val configB = AdventTestConfig( 118 | id = AdventDayID(9999, 2), 119 | part = AdventDayPart.Two, 120 | partFunction = ObjectSolution.partFunction(AdventDayPart.Two), 121 | enabled = false, 122 | expensive = false, 123 | executionMode = null, 124 | efficiencyBenchmark = null, 125 | examples = emptyList(), 126 | ) 127 | 128 | test("for examples") { 129 | configA.forExamples(AdventProjectConfig.Default).should { finalConfig -> 130 | finalConfig.enabled shouldBe false 131 | finalConfig.examples shouldBe configA.examples 132 | finalConfig.partFunction.invoke(PuzzleInput("B")) shouldBe PuzzleAnswer("1:B") 133 | } 134 | configB.forExamples(AdventProjectConfig.Default).should { finalConfig -> 135 | finalConfig.enabled shouldBe false 136 | finalConfig.examples shouldBe emptyList() 137 | finalConfig.partFunction.invoke(PuzzleInput("B")) shouldBe PuzzleAnswer("2:1") 138 | } 139 | } 140 | 141 | test("for input") { 142 | configA.forInput(AdventProjectConfig.Default).should { finalConfig -> 143 | finalConfig.id shouldBe configA.id 144 | finalConfig.part shouldBe configA.part 145 | finalConfig.enabled shouldBe true 146 | finalConfig.partFunction.invoke(PuzzleInput("B")) shouldBe PuzzleAnswer("1:B") 147 | finalConfig.efficiencyBenchmark shouldBe configA.efficiencyBenchmark 148 | } 149 | configB.forInput(AdventProjectConfig.Default).should { finalConfig -> 150 | finalConfig.id shouldBe configB.id 151 | finalConfig.part shouldBe configB.part 152 | finalConfig.enabled shouldBe false 153 | finalConfig.partFunction.invoke(PuzzleInput("B")) shouldBe PuzzleAnswer("2:1") 154 | finalConfig.efficiencyBenchmark shouldBe AdventProjectConfig.Default.efficiencyBenchmark 155 | } 156 | } 157 | } 158 | }) 159 | 160 | @AdventDay(3000, 3) 161 | private class SomeSpec : AdventSpec() 162 | -------------------------------------------------------------------------------- /aockt-test/src/main/kotlin/internal/AdventSpecExt.kt: -------------------------------------------------------------------------------- 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.github.jadarma.aockt.test.Expensive 7 | import io.kotest.assertions.AssertionErrorBuilder 8 | import io.kotest.assertions.throwables.shouldNotThrowAny 9 | import io.kotest.assertions.throwables.shouldNotThrowAnyUnit 10 | import io.kotest.assertions.withClue 11 | import io.kotest.common.ExperimentalKotest 12 | import io.kotest.common.reflection.ReflectionInstantiations.newInstanceNoArgConstructorOrObjectInstance 13 | import io.kotest.core.spec.style.scopes.FunSpecContainerScope 14 | import io.kotest.core.test.parents 15 | import io.kotest.matchers.comparables.shouldBeLessThanOrEqualTo 16 | import io.kotest.matchers.shouldBe 17 | import kotlinx.coroutines.currentCoroutineContext 18 | import kotlin.reflect.KClass 19 | import kotlin.reflect.full.findAnnotation 20 | import kotlin.reflect.full.isSubtypeOf 21 | import kotlin.reflect.full.starProjectedType 22 | import kotlin.reflect.jvm.jvmErasure 23 | import kotlin.reflect.typeOf 24 | import kotlin.time.Duration 25 | import kotlin.time.measureTimedValue 26 | 27 | /** Return the required-to-be-registered [AdventDay] annotation for this spec. */ 28 | internal val KClass>.adventDay: AdventDay 29 | get() = findAnnotation() ?: throw MissingAdventDayAnnotationException(this) 30 | 31 | /** 32 | * Construct and inject a solution instance for this [AdventSpec]. 33 | * This function is designed to be called at spec instantiation time. 34 | * Uses reflection magic that while not that pretty, is fine for use in unit tests, and allows for a more elegant syntax 35 | * when declaring specs. 36 | */ 37 | @Suppress("UNCHECKED_CAST", "UnsafeCallOnNullableType") 38 | internal fun KClass>.injectSolution(): Solution = this 39 | .starProjectedType.jvmErasure.supertypes 40 | .first { it.isSubtypeOf(typeOf>()) } 41 | .arguments.first().type!!.jvmErasure 42 | .run { 43 | this as KClass // Must be a solution because of AdventSpec bounds. 44 | runCatching { newInstanceNoArgConstructorOrObjectInstance(this) } 45 | .getOrElse { throw MissingNoArgConstructorException(this) } 46 | } 47 | 48 | /** 49 | * Register a root context to test the implementation of one part of a [Solution]. 50 | * 51 | * Will create two sub-contexts: 52 | * - The examples, as defined by [registerExamples]. 53 | * - The solution, as defined by [registerInput]. 54 | */ 55 | internal fun AdventSpec<*>.registerTest(config: AdventTestConfig): Unit = with(config) { 56 | context("Part $part").config( 57 | enabled = enabled, 58 | tags = if (expensive) setOf(Expensive) else null, 59 | ) { 60 | val projectConfig = currentCoroutineContext()[AdventProjectConfig.Key] ?: AdventProjectConfig.Default 61 | registerExamples(config.forExamples(projectConfig)) 62 | registerInput(config.forInput(projectConfig)) 63 | } 64 | } 65 | 66 | /** 67 | * Register a focused root test to help debugging a [Solution]. 68 | * All other tests will be ignored. 69 | */ 70 | internal fun AdventSpec<*>.registerDebug(config: AdventDebugConfig): Unit = with(config) { 71 | test(name = "f:Debug") { 72 | val testData = TestData.inputFor(config.id) 73 | withClue("Debug run completed exceptionally.") { 74 | shouldNotThrowAnyUnit { 75 | AdventDebugScopeImpl(solution, testData.input).run(test) 76 | } 77 | } 78 | } 79 | } 80 | 81 | /** Define an example context, generating a separate test for each example given. */ 82 | @OptIn(ExperimentalKotest::class) 83 | @Suppress("SuspendFunWithCoroutineScopeReceiver") 84 | private suspend fun FunSpecContainerScope.registerExamples(config: AdventTestConfig.ForExamples): Unit = with(config) { 85 | if (examples.isEmpty()) return 86 | context(name = "Validates the examples").config(enabled = enabled) { 87 | examples.forEachIndexed { index, (input, expected) -> 88 | test("Example #${index + 1}") { 89 | withClue("Expected answer '$expected' for input: ${input.preview()}") { 90 | val answer = shouldNotThrowAny { partFunction(input) } 91 | answer shouldBe expected 92 | } 93 | } 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Define a context to test against the user's own input. 100 | * Sets up the following tests: 101 | * - The solution output is validated against the correct answer if known. 102 | * - Unverified solution, always ignored, only shows when the solution is computed but the correct answer is unknown. 103 | * - Efficiency benchmark, ignored if either the solution was not computed, the solution is unverified, or is marked as 104 | * expensive. 105 | */ 106 | @OptIn(ExperimentalKotest::class) 107 | @Suppress("SuspendFunWithCoroutineScopeReceiver", "CognitiveComplexMethod") 108 | private suspend fun FunSpecContainerScope.registerInput(config: AdventTestConfig.ForInput): Unit = with(config) { 109 | 110 | val data = TestData.inputFor(config.id) 111 | val input = data.input ?: return 112 | val correctAnswer = data.solutionToPart(config.part) 113 | 114 | context("The solution").config(enabled = enabled) { 115 | val isSolutionKnown = correctAnswer != null 116 | var answer: PuzzleAnswer? = null 117 | var duration: Duration? = null 118 | 119 | test(name = if (isSolutionKnown) "Is correct" else "Computes an answer") { 120 | runCatching { 121 | val measured = measureTimedValue { partFunction(input) } 122 | answer = measured.value 123 | duration = measured.duration 124 | }.onFailure { error -> 125 | AssertionErrorBuilder.create() 126 | .withMessage("The solution threw an exception before it could return an answer.") 127 | .withCause(error) 128 | .build() 129 | } 130 | 131 | if (isSolutionKnown) { 132 | withClue("Got different answer than the known solution.") { 133 | answer shouldBe correctAnswer 134 | } 135 | } 136 | } 137 | 138 | // If solution is unverified, create a dummy ignored test to display the value in the test report. 139 | if (!isSolutionKnown && answer != null) { 140 | xtest("Has unverified answer ($answer)") {} 141 | } 142 | 143 | val enableSpeedTesting = when { 144 | correctAnswer == null -> false 145 | answer != correctAnswer -> false 146 | testCase.parents().any { Expensive in it.config?.tags.orEmpty() } -> false 147 | else -> true 148 | } 149 | 150 | val benchmark = config.efficiencyBenchmark 151 | val durationSuffix = duration?.takeIf { answer != null }?.toString() ?: "N/A" 152 | test("Is reasonably efficient ($durationSuffix)").config(enabled = enableSpeedTesting) { 153 | withClue("The solution did not complete under the configured benchmark of $benchmark") { 154 | @Suppress("UnsafeCallOnNullableType") 155 | duration!! shouldBeLessThanOrEqualTo benchmark 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /docs/topics/workflow.md: -------------------------------------------------------------------------------- 1 | # Workflow 2 | 3 | After setting up your [project template](https://github.com/Jadarma/advent-of-code-kotlin-template), or configuring 4 | your own, this is the place to start learning all about using AocKt! 5 | 6 | This tutorial goes very in-depth to cover all the bases, and also be more accessible to beginners. 7 | Don't be discouraged by its length, you only need it for the first run, after which it will become 8 | reflex. 9 | 10 | Good luck! 11 | 12 | 13 | 14 | This tutorial describes the intended workflow when solving Advent of Code with AocKt. 15 | 16 | 17 | ## Prerequisite: Project Structure 18 | 19 | AocKt runs entirely locally, by reading files on disk. 20 | For each puzzle, you will end up with five files, explained later. 21 | For reference, they will be distributed as such: 22 | 23 | 24 | 25 | 26 | ```text 27 | projectDir 28 | ├── inputs 29 | │ └── aockt 30 | │ └── y2015 31 | │ └── d01 32 | │ ├── input.txt 33 | │ ├── solution_part1.txt 34 | │ └── solution_part2.txt 35 | ├── solutions 36 | │ └── aockt 37 | │ └── y2015 38 | │ └── Y2015D01.kt 39 | └── tests 40 | └── aockt 41 | └── y2015 42 | └── Y2015D01Test.kt 43 | ``` 44 | 45 | 46 | 47 | ```text 48 | projectDir 49 | ├── src/main/kotlin 50 | │ └── aockt 51 | │ └── y2015 52 | │ └── Y2015D01.kt 53 | ├── src/test/kotlin 54 | │ └── aockt 55 | │ └── y2015 56 | │ └── Y2015D01Test.kt 57 | └── src/test/resources 58 | └── aockt 59 | └── y2015 60 | └── d01 61 | └── input.txt 62 | └── solution_part1.txt 63 | └── solution_part2.txt 64 | ``` 65 | 66 | > Therein after these source sets will be referred by an alias: 67 | > - `solutions` is your `src/main/kotlin` 68 | > - `tests` is your `src/test/kotlin` 69 | > - `inputs` is your `src/test/resources` 70 | 71 | 72 | 73 | 74 | - Your implementations go into `solutions`. 75 | While it is not a requirement to split them into packages by year or have a specific naming convention, doing so 76 | helps with organising. 77 | - Unit tests go into `tests` and should have the same structure as your `solutions`. 78 | - Your puzzle inputs go into `inputs`. 79 | For these, ***the format is enforced***: 80 | - Each day has a directory as a base path: `/aockt/y[year]/d[twoDigitDay]` 81 | - In that directory, the input is in the `input.txt` file. 82 | - The solutions use `solution_part1.txt` and `solution_part2.txt`. 83 | They are added in gradually, as discovered. 84 | 85 | > ***DO NOT Commit Puzzle Inputs!*** 86 | > 87 | > _It is against the rules to redistribute your puzzle inputs!_ 88 | > 89 | > Use something like [`git-crypt`](https://github.com/AGWA/git-crypt) to ensure inputs can only be read by you if you 90 | > plan on sharing your repository publicly. 91 | > 92 | > _(In the template project, inputs are git-ignored by default.)_ 93 | > {style="warning"} 94 | 95 | ## Step 0: Read And Understand The Puzzle 96 | 97 | Have a thorough read of the puzzle. 98 | The devil's in the details, and also have a look at your puzzle input. 99 | Sometimes you'll get extra insight by observing patterns in inputs! 100 | 101 | After you think you have a clear initial understanding, move on. 102 | 103 | ## Step 1: Get Your Puzzle Input 104 | 105 | Copy your puzzle input and paste it in `inputs/aockt/y2015/d01/input.txt`. 106 | 107 | ## Step 2: Create Your Solution Stubs 108 | 109 | Create a `class` or `object` for your solution that implements the `Solution` type. 110 | 111 | 112 | In your `solutions`, create: 113 | 114 | ```kotlin 115 | package aockt.y2015 116 | 117 | import io.github.jadarma.aockt.core.Solution 118 | 119 | object Y2015D01 : Solution 120 | ``` 121 | 122 | > Your class _must have_ a zero-arg constructor. 123 | 124 | Following the same logic, create its `tests` counterpart: 125 | 126 | ```kotlin 127 | package aockt.y2015 128 | 129 | import io.github.jadarma.aockt.test.AdventDay 130 | import io.github.jadarma.aockt.test.AdventSpec 131 | 132 | @AdventDay(2015, 1, "Not Quite Lisp") 133 | class Y2015D01Test : AdventSpec({}) 134 | ``` 135 | 136 | > The `@AdventDay` annotation is required. 137 | > Pass your solution class as the type argument of the `AdventSpec`. 138 | 139 | ## Step 3: Define your Test Cases 140 | 141 | Inside the `AdventSpec`'s constructor, you pass in the configuration DSL. 142 | Each puzzle part has its own context. 143 | 144 | Let's start with part one: 145 | 146 | ```kotlin 147 | package aockt.y2015 148 | 149 | import io.github.jadarma.aockt.test.AdventDay 150 | import io.github.jadarma.aockt.test.AdventSpec 151 | 152 | @AdventDay(2015, 1, "Not Quite Lisp") 153 | class Y2015D01Test : AdventSpec({ 154 | partOne() 155 | }) 156 | ``` 157 | 158 | Like many other puzzles, this day provides you with example inputs and outputs to test your solution with. 159 | 160 | If that is the case, you can define them in a lambda. 161 | The syntax is `"string" shouldOutput "output"`, the output can be a string, or a number, it will be checked against its 162 | string representation. 163 | For multiple inputs, `listOf("string") shouldAllOutput "output"` is synonymous. 164 | 165 | ```kotlin 166 | package aockt.y2015 167 | 168 | import io.github.jadarma.aockt.test.AdventDay 169 | import io.github.jadarma.aockt.test.AdventSpec 170 | 171 | @AdventDay(2015, 1, "Not Quite Lisp") 172 | class Y2015D01Test : AdventSpec({ 173 | partOne { 174 | listOf("(())", "()()") shouldAllOutput 0 175 | listOf("(((", "(()(()(", "))(((((") shouldAllOutput 3 176 | listOf("())", "))(") shouldAllOutput -1 177 | listOf(")))", ")())())") shouldAllOutput -3 178 | } 179 | }) 180 | ``` 181 | 182 | It is recommended you use the [Kotest IntelliJ Plugin](https://kotest.io/docs/intellij/intellij-plugin.html), so you 183 | can run the test via the gutter icon. 184 | 185 | Your first run should look like this: 186 | 187 | Results of the initial run. 188 | 189 | Each example has its own test, and since you added your input, it also tests against that. 190 | 191 | > If you're not interested in running against your input until validating the examples first, you can 192 | > [ignore your input](test-config.md#executionmode) by setting: 193 | > 194 | > `partOne(executionMode = ExecMode.ExamplesOnly)` 195 | > {style="note"} 196 | 197 | ## Step 4: Implement Your Solution 198 | 199 | Go back to your solution and implement `partOne`. 200 | The function can return anything representable by a string. 201 | In practical terms, it will always be either a string or an integer. 202 | 203 | Have a try at the problem: 204 | 205 | ```kotlin 206 | package aockt.y2015 207 | 208 | import io.github.jadarma.aockt.core.Solution 209 | 210 | object Y2015D01 : Solution { 211 | override fun partOne(input: String): Int = spoilers() 212 | } 213 | ``` 214 | 215 | > In some puzzles parsing the input into some abstract data structures is necessary. 216 | > A good tip is to write parsing logic separately so that it may be used by both parts: 217 | > ```kotlin 218 | > private fun parse(input: String): Something = input.spoilers() 219 | > override fun partOne(input: String): Int = parse(input).spoilers() 220 | > ``` 221 | > {style="note"} 222 | 223 | ## Step 5: Test Your Solution 224 | 225 | Run the test again as you go. 226 | 227 | > You should be able to run the test again using the `Ctrl+F5` shortcut. 228 | > {style="note"} 229 | 230 | Results of the second run. 231 | 232 | Hmm, seems like we're missing something. 233 | Clicking on each example will show us the actual versus expected result. 234 | In this case, seems we have a sign flip issue. 235 | 236 | Results of the third run. 237 | 238 | That was it! 239 | 240 | ## Step 5: Submit Your Answer 241 | 242 | Now that you're confident your solution is correct, run it on your own input _(by reverting the `executionMode` if you 243 | set it earlier)_. 244 | 245 | Results of user input. 246 | 247 | Your solution gave an answer, but you don't know if it's correct. 248 | Go back to the website and submit it. 249 | 250 | > Unfortunately, currently you cannot copy-paste from the UI, you'll have to do it the old way. 251 | 252 | **Congratulations on collecting the first star!** ⭐ 253 | 254 | If not, try to look at what's special in your input, or [hunt down bugs](debugging.md). 255 | 256 | Once you have verified the right answer, paste it in `inputs/aockt/y2015/d01/solution_part1.txt`. 257 | Now, when you run the test, your code will be tested against the known answer: 258 | 259 | Results of verified solution. 260 | 261 | ## Step 6: Tackle Part Two 262 | 263 | There is one more star to be gained. 264 | The website will give you a second task based on the same puzzle. 265 | 266 | You can ignore part one tests for now, and [set](test-config.md#enabled) `partOne(enabled = false)`. 267 | 268 | Then repeat the same steps: 269 | - Implement `fun partTwo(input: String)` in your solution. 270 | - Define your test with `partTwo()` and define any new examples. 271 | - Run the tests until you get a solution candidate. 272 | - After getting the right answer, save it to `solution_part2.txt`. 273 | 274 | **Congratulations on collecting both stars!** ⭐⭐ 275 | 276 | ## Step 7: Refactor Fearlessly _(and Optionally)_ 277 | 278 | After completing the puzzle, your test should look like this: 279 | 280 | ```kotlin 281 | package aockt.y2015 282 | 283 | import io.github.jadarma.aockt.test.AdventDay 284 | import io.github.jadarma.aockt.test.AdventSpec 285 | 286 | @AdventDay(2015, 1, "Not Quite Lisp") 287 | class Y2015D01Test : AdventSpec({ 288 | 289 | partOne { 290 | listOf("(())", "()()") shouldAllOutput 0 291 | listOf("(((", "(()(()(") shouldAllOutput 3 292 | listOf("())", "))(") shouldAllOutput -1 293 | listOf(")))", ")())())") shouldAllOutput -3 294 | } 295 | 296 | partTwo { 297 | ")" shouldOutput 1 298 | "()())" shouldOutput 5 299 | } 300 | }) 301 | ``` 302 | 303 | And your test runs like this: 304 | 305 | Results of both verified solutions. 306 | 307 | > Your runs might be collapsed since they're all passing. 308 | > You can expand them all with Ctrl+Plus to get the above view. 309 | > {style="note"} 310 | 311 | Now you can save your changes, clean up your code, refactor some lines, if that's your thing. 312 | Run tests often, they will help you know when you made a logic-altering change. 313 | 314 | *That's it! Now relax and get ready for the next day!* 315 | 316 | 317 | 318 |
319 | 320 | 321 | 322 | -------------------------------------------------------------------------------- /gradle/detekt/config/detekt.yml: -------------------------------------------------------------------------------- 1 | # Explicit configuration to all Detekt rules. 2 | # https://detekt.dev/docs/intro 3 | 4 | # https://detekt.dev/docs/rules/comments 5 | comments: 6 | AbsentOrWrongFileLicense: 7 | active: false 8 | CommentOverPrivateFunction: 9 | active: false 10 | CommentOverPrivateProperty: 11 | active: false 12 | DeprecatedBlockTag: 13 | active: true 14 | EndOfSentenceFormat: 15 | active: true 16 | KDocReferencesNonPublicProperty: 17 | active: true 18 | OutdatedDocumentation: 19 | active: true 20 | UndocumentedPublicClass: 21 | active: true 22 | ignoreDefaultCompanionObject: true 23 | UndocumentedPublicFunction: 24 | active: true 25 | UndocumentedPublicProperty: 26 | active: true 27 | 28 | # https://detekt.dev/docs/rules/complexity 29 | complexity: 30 | CognitiveComplexMethod: 31 | active: true 32 | ComplexCondition: 33 | active: true 34 | ComplexInterface: 35 | active: true 36 | CyclomaticComplexMethod: 37 | active: true 38 | LabeledExpression: 39 | active: true 40 | LargeClass: 41 | active: true 42 | LongMethod: 43 | active: true 44 | LongParameterList: 45 | active: true 46 | ignoreDefaultParameters: true 47 | ignoreDataClasses: true 48 | MethodOverloading: 49 | active: true 50 | NamedArguments: 51 | active: false 52 | NestedBlockDepth: 53 | active: true 54 | NestedScopeFunctions: 55 | active: true 56 | allowedDepth: 2 57 | ReplaceSafeCallChainWithRun: 58 | active: true 59 | StringLiteralDuplication: 60 | active: true 61 | TooManyFunctions: 62 | active: true 63 | ignorePrivate: true 64 | ignoreInternal: true 65 | ignoreOverridden: true 66 | 67 | # https://detekt.dev/docs/rules/coroutines 68 | coroutines: 69 | CoroutineLaunchedInTestWithoutRunTest: 70 | active: false 71 | GlobalCoroutineUsage: 72 | active: true 73 | InjectDispatcher: 74 | active: true 75 | RedundantSuspendModifier: 76 | active: true 77 | SleepInsteadOfDelay: 78 | active: true 79 | SuspendFunInFinallySection: 80 | active: true 81 | SuspendFunSwallowedCancellation: 82 | active: true 83 | SuspendFunWithCoroutineScopeReceiver: 84 | active: true 85 | SuspendFunWithFlowReturnType: 86 | active: true 87 | 88 | # https://detekt.dev/docs/rules/empty-blocks 89 | empty-blocks: 90 | EmptyCatchBlock: 91 | active: true 92 | EmptyClassBlock: 93 | active: true 94 | EmptyDefaultConstructor: 95 | active: true 96 | EmptyDoWhileBlock: 97 | active: true 98 | EmptyElseBlock: 99 | active: true 100 | EmptyFinallyBlock: 101 | active: true 102 | EmptyForBlock: 103 | active: true 104 | EmptyFunctionBlock: 105 | active: true 106 | EmptyIfBlock: 107 | active: true 108 | EmptyInitBlock: 109 | active: true 110 | EmptyKotlinFile: 111 | active: true 112 | EmptySecondaryConstructor: 113 | active: true 114 | EmptyTryBlock: 115 | active: true 116 | EmptyWhenBlock: 117 | active: true 118 | EmptyWhileBlock: 119 | active: true 120 | 121 | # https://detekt.dev/docs/rules/exceptions 122 | exceptions: 123 | ErrorUsageWithThrowable: 124 | active: true 125 | ExceptionRaisedInUnexpectedLocation: 126 | active: true 127 | InstanceOfCheckForException: 128 | active: true 129 | NotImplementedDeclaration: 130 | active: true 131 | ObjectExtendsThrowable: 132 | active: true 133 | PrintStackTrace: 134 | active: true 135 | RethrowCaughtException: 136 | active: true 137 | ReturnFromFinally: 138 | active: true 139 | SwallowedException: 140 | active: true 141 | ThrowingExceptionFromFinally: 142 | active: true 143 | ThrowingExceptionInMain: 144 | active: true 145 | ThrowingExceptionsWithoutMessageOrCause: 146 | active: true 147 | ThrowingNewInstanceOfSameException: 148 | active: true 149 | TooGenericExceptionCaught: 150 | active: true 151 | TooGenericExceptionThrown: 152 | active: true 153 | 154 | # https://detekt.dev/docs/rules/naming 155 | naming: 156 | BooleanPropertyNaming: 157 | active: true 158 | allowedPattern: '^(is|has|are|enable)' 159 | ClassNaming: 160 | active: true 161 | ConstructorParameterNaming: 162 | active: true 163 | EnumNaming: 164 | active: true 165 | ForbiddenClassName: 166 | active: false 167 | FunctionNameMaxLength: 168 | active: true 169 | FunctionNameMinLength: 170 | active: false 171 | FunctionNaming: 172 | active: true 173 | FunctionParameterNaming: 174 | active: true 175 | InvalidPackageDeclaration: 176 | active: false # Configure per module. 177 | LambdaParameterNaming: 178 | active: true 179 | MatchingDeclarationName: 180 | active: true 181 | MemberNameEqualsClassName: 182 | active: true 183 | NoNameShadowing: 184 | active: true 185 | NonBooleanPropertyPrefixedWithIs: 186 | active: true 187 | ObjectPropertyNaming: 188 | active: true 189 | PackageNaming: 190 | active: true 191 | TopLevelPropertyNaming: 192 | active: true 193 | VariableMaxLength: 194 | active: true 195 | VariableMinLength: 196 | active: true 197 | VariableNaming: 198 | active: true 199 | 200 | # https://detekt.dev/docs/rules/performance 201 | performance: 202 | ArrayPrimitive: 203 | active: true 204 | CouldBeSequence: 205 | active: true 206 | ForEachOnRange: 207 | active: true 208 | SpreadOperator: 209 | active: true 210 | UnnecessaryPartOfBinaryExpression: 211 | active: true 212 | UnnecessaryTemporaryInstantiation: 213 | active: true 214 | UnnecessaryTypeCasting: 215 | active: true 216 | 217 | # https://detekt.dev/docs/rules/potential-bugs 218 | potential-bugs: 219 | AvoidReferentialEquality: 220 | active: true 221 | CastNullableToNonNullableType: 222 | active: true 223 | CastToNullableType: 224 | active: true 225 | CharArrayToStringCall: 226 | active: true 227 | Deprecation: 228 | active: true 229 | DontDowncastCollectionTypes: 230 | active: true 231 | DoubleMutabilityForCollection: 232 | active: true 233 | ElseCaseInsteadOfExhaustiveWhen: 234 | active: true 235 | EqualsAlwaysReturnsTrueOrFalse: 236 | active: true 237 | EqualsWithHashCodeExist: 238 | active: true 239 | ExitOutsideMain: 240 | active: true 241 | ExplicitGarbageCollectionCall: 242 | active: true 243 | HasPlatformType: 244 | active: true 245 | IgnoredReturnValue: 246 | active: true 247 | ImplicitDefaultLocale: 248 | active: true 249 | ImplicitUnitReturnType: 250 | active: true 251 | InvalidRange: 252 | active: true 253 | IteratorHasNextCallsNextMethod: 254 | active: true 255 | IteratorNotThrowingNoSuchElementException: 256 | active: true 257 | LateinitUsage: 258 | active: true 259 | MapGetWithNotNullAssertionOperator: 260 | active: true 261 | MissingPackageDeclaration: 262 | active: true 263 | MissingSuperCall: 264 | active: true 265 | MissingUseCall: 266 | active: true 267 | NullCheckOnMutableProperty: 268 | active: true 269 | NullableToStringCall: 270 | active: true 271 | PropertyUsedBeforeDeclaration: 272 | active: true 273 | UnconditionalJumpStatementInLoop: 274 | active: true 275 | UnnamedParameterUse: 276 | active: true 277 | UnnecessaryNotNullCheck: 278 | active: true 279 | UnnecessaryNotNullOperator: 280 | active: true 281 | UnnecessarySafeCall: 282 | active: true 283 | UnreachableCatchBlock: 284 | active: true 285 | UnreachableCode: 286 | active: true 287 | UnsafeCallOnNullableType: 288 | active: true 289 | UnsafeCast: 290 | active: true 291 | UnusedUnaryOperator: 292 | active: true 293 | UselessPostfixExpression: 294 | active: true 295 | WrongEqualsTypeParameter: 296 | active: true 297 | 298 | # https://detekt.dev/docs/rules/style 299 | style: 300 | AbstractClassCanBeConcreteClass: 301 | active: true 302 | AbstractClassCanBeInterface: 303 | active: true 304 | AlsoCouldBeApply: 305 | active: true 306 | BracesOnIfStatements: 307 | active: true 308 | BracesOnWhenStatements: 309 | active: true 310 | CanBeNonNullable: 311 | active: true 312 | CascadingCallWrapping: 313 | active: false 314 | ClassOrdering: 315 | active: true 316 | CollapsibleIfStatements: 317 | active: true 318 | DataClassContainsFunctions: 319 | active: true 320 | DataClassShouldBeImmutable: 321 | active: true 322 | DestructuringDeclarationWithTooManyEntries: 323 | active: true 324 | DoubleNegativeExpression: 325 | active: true 326 | DoubleNegativeLambda: 327 | active: true 328 | EqualsNullCall: 329 | active: true 330 | EqualsOnSignatureLine: 331 | active: true 332 | ExplicitCollectionElementAccessMethod: 333 | active: true 334 | ExplicitItLambdaMultipleParameters: 335 | active: true 336 | ExplicitItLambdaParameter: 337 | active: true 338 | ExpressionBodySyntax: 339 | active: true 340 | ForbiddenAnnotation: 341 | active: true 342 | ForbiddenComment: 343 | active: false 344 | ForbiddenImport: 345 | active: false 346 | ForbiddenMethodCall: 347 | active: true 348 | ForbiddenNamedParam: 349 | active: false 350 | ForbiddenOptIn: 351 | active: false 352 | ForbiddenSuppress: 353 | active: false 354 | ForbiddenVoid: 355 | active: true 356 | FunctionOnlyReturningConstant: 357 | active: true 358 | LoopWithTooManyJumpStatements: 359 | active: true 360 | MagicNumber: 361 | active: true 362 | MandatoryBracesLoops: 363 | active: true 364 | MaxChainedCallsOnSameLine: 365 | active: true 366 | MaxLineLength: 367 | active: true 368 | MayBeConstant: 369 | active: true 370 | ModifierOrder: 371 | active: true 372 | MultilineLambdaItParameter: 373 | active: true 374 | MultilineRawStringIndentation: 375 | active: true 376 | NestedClassesVisibility: 377 | active: true 378 | NewLineAtEndOfFile: 379 | active: true 380 | NoTabs: 381 | active: true 382 | NullableBooleanCheck: 383 | active: true 384 | ObjectLiteralToLambda: 385 | active: true 386 | OptionalAbstractKeyword: 387 | active: true 388 | OptionalUnit: 389 | active: true 390 | ProtectedMemberInFinalClass: 391 | active: true 392 | RangeUntilInsteadOfRangeTo: 393 | active: true 394 | RedundantConstructorKeyword: 395 | active: true 396 | RedundantExplicitType: 397 | active: true 398 | RedundantHigherOrderMapUsage: 399 | active: true 400 | RedundantVisibilityModifier: 401 | active: false 402 | ReturnCount: 403 | active: true 404 | max: 3 405 | SafeCast: 406 | active: true 407 | SerialVersionUIDInSerializableClass: 408 | active: true 409 | SpacingAfterPackageDeclaration: 410 | active: true 411 | StringShouldBeRawString: 412 | active: true 413 | ThrowsCount: 414 | active: true 415 | TrailingWhitespace: 416 | active: true 417 | TrimMultilineRawString: 418 | active: true 419 | UnderscoresInNumericLiterals: 420 | active: true 421 | UnnecessaryAnnotationUseSiteTarget: 422 | active: true 423 | UnnecessaryAny: 424 | active: true 425 | UnnecessaryApply: 426 | active: true 427 | UnnecessaryBackticks: 428 | active: true 429 | UnnecessaryBracesAroundTrailingLambda: 430 | active: true 431 | UnnecessaryFilter: 432 | active: true 433 | UnnecessaryInheritance: 434 | active: true 435 | UnnecessaryInnerClass: 436 | active: true 437 | UnnecessaryLet: 438 | active: true 439 | UnnecessaryParentheses: 440 | active: true 441 | allowForUnclearPrecedence: true 442 | UnnecessaryReversed: 443 | active: true 444 | UnusedImport: 445 | active: true 446 | UnusedParameter: 447 | active: true 448 | UnusedPrivateClass: 449 | active: true 450 | UnusedPrivateFunction: 451 | active: true 452 | UnusedPrivateProperty: 453 | active: true 454 | UnusedVariable: 455 | active: true 456 | UseAnyOrNoneInsteadOfFind: 457 | active: true 458 | UseArrayLiteralsInAnnotations: 459 | active: true 460 | UseCheckNotNull: 461 | active: true 462 | UseCheckOrError: 463 | active: true 464 | UseDataClass: 465 | active: true 466 | UseEmptyCounterpart: 467 | active: true 468 | UseIfEmptyOrIfBlank: 469 | active: true 470 | UseIfInsteadOfWhen: 471 | active: false 472 | UseIsNullOrEmpty: 473 | active: true 474 | UseLet: 475 | active: true 476 | UseOrEmpty: 477 | active: true 478 | UseRequire: 479 | active: true 480 | UseRequireNotNull: 481 | active: true 482 | UseSumOfInsteadOfFlatMapSize: 483 | active: true 484 | UselessCallOnNotNull: 485 | active: true 486 | UtilityClassWithPublicConstructor: 487 | active: true 488 | VarCouldBeVal: 489 | active: true 490 | WildcardImport: 491 | active: true 492 | --------------------------------------------------------------------------------