├── 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 |
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 | 
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 | [](https://kotlinlang.org/)
4 | [](https://kotest.io/)
5 | [](https://github.com/Jadarma/advent-of-code-kotlin/actions/workflows/build.yml)
6 | [](https://central.sonatype.com/namespace/io.github.jadarma.aockt)
7 | [](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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------