Showing posts with label Gradle. Show all posts
Showing posts with label Gradle. Show all posts

Sunday 16 October 2022

Kotlin / Ktor: G'day world from a Docker container

G'day:

Not sure what this article is going to end up being about. However I am hovering over the "New Project" button in IntelliJ, and am gonna attempt to at least get to a "G'day world" sort of situation with a Ktor-driven web service today.

Why Ktor

We need to port our monolithic CFML/CFWheels app to a more… erm… forward-thinking and well-designed solution. The existing app got us to where we are, and pays our salaries, but its design reflects a very "CFML-dev" approach to application design. We've decided to shift to Kotlin, as you know. We also need to adopt some sort of framework to implement the application atop-of, and we've chosen Ktor for a few reasons:

  • It's focus is micro-services and small footprint.
  • From what I've read, it focuses on being a framework instead of being an opinion-mill, how other frameworks can tend to be.
  • It's written for Kotlin; unlike say Spring, which is written for Java and it shows. We're using Kotlin to have the benefits of the JVM, but to steer clear the Java Way™ of doing things.
  • It's created by JetBrains, who created Kotlin, so hopefully the Ktor design team with be aligned with the Kotlin design team, so it should be a pretty Kotlin-idiomatic way of doing things.
  • Support for it is baked-in to IntelliJ, so it's a "first class citizen" in the IDE.

Also basically we need to pick something, so we're cracking on with it. If we do some quick investigation and it turns our Ktor ain't for us: I'd rather know sooner rather than later.

Let's get on with it.


Project

One can create a new Ktor project via IntelliJ ("New Project"):

I've only filled in the situation-specific stuff here, and left everything else as default. I've clicked the "Create Git repository" option: I hope if gives me the option to provide a name for it before it charges off and does it, cos I don't want it just to be called "gdayworld". So I might back out of that choice if it doesn't work for me:

Let's press "Next"…

Argh! I have to make decisions! I haven't even finished my first coffee of the day yet!

There are roughly one million plug-ins on offer here, and I don't even know what most of them are. For now, all I need this thing to do is to have testing for a greeting endpoint that says "G'day world" or something, so I doubt I'll need most of this stuff. Let's have a scan through.

OK, I've selected these ones:

  • Routing
  • DefaultHeaders
  • CallLogging
  • CallId
  • kotlinx.serialization - this also required the ContentNegotiation plug-in

After clicking "create" it got on with it, downloaded some stuff, built the project and declared everything was fine. I now have this lot:


Baseline checks

Right, let's see what tests it installed by default:

package me.adamcameron

import io.ktor.server.routing.*
import io.ktor.http.*
import io.ktor.server.plugins.callloging.*
import org.slf4j.event.*
import io.ktor.server.request.*
import io.ktor.server.plugins.callid.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlin.test.*
import io.ktor.server.testing.*
import me.adamcameron.plugins.*

class ApplicationTest {
    @Test
    fun testRoot() = testApplication {
        application {
            configureRouting()
        }
        client.get("/").apply {
            assertEquals(HttpStatusCode.OK, status)
            assertEquals("Hello World!", bodyAsText())
        }
    }
}

Most of those imports aren't necessary btw, that's a wee bit sloppy. It only claims to need these ones:

import io.ktor.http.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlin.test.*
import io.ktor.server.testing.*
import me.adamcameron.plugins.*

I'll leave it as-is for now. The test looks sound actually. Well: I've purposely not looked at the code yet, but a test that tests that a GET to / returns "Hello World!" seems reasonable. Let's run it:

Cool. OK, let's run the app then, given it looks like it'll work:

C:\Users\camer\.jdks\semeru-11.0.17\bin\java.exe […]
2022-10-16 11:01:49.119 [main]  INFO  ktor.application - Autoreload is disabled because the development mode is off.
2022-10-16 11:01:49.205 [main]  INFO  ktor.application - Application started in 0.148 seconds.
2022-10-16 11:01:49.205 [main]  INFO  ktor.application - Application started: io.ktor.server.application.Application@57312fcd
2022-10-16 11:01:50.448 [DefaultDispatcher-worker-1]  INFO  ktor.application - Responding at http://127.0.0.1:8080  

It ran. Does it actually respond on http://127.0.0.1:8080?

Cool. OK, so I have a test that passes an an app that works. Gonna push that to GitHub as v0.2 (v0.1 was the empty repo). And I'm gonna have a shufti around the files it's created and see what's what.


Tweaking

OK, I'm not gonna look at those old-school xUnit-style tests. I'm gonna adapt them to use the more declarative BDD style I've been using so far when testing stuff with Kotlin. So this means I'm going to add some Kotest dependencies. The test is now:

@DisplayName("Tests of the / route")
class ApplicationTest {
    @Test
    fun `Tests the root route responds with the correct status and message`() = testApplication {
        application {
            configureRouting()
        }
        client.get("/").apply {
            status shouldBe HttpStatusCode.OK
            bodyAsText() shouldBe "Hello World!"
        }
    }
}

I'm also refactoring the class name and location to src/test/kotlin/acceptance/IndexRouteTest.kt. It's not testring the app, it's testing the route. Plus it's an acceptance test, and I wanna keep those separate from unit tests / integration tests etc (poss premature optimisation here I guess). I've also lost the subdirectory structure from /src/main/kotlin/me/adamcameron/Application.kt to be just /src/main/kotlin/Application.kt. Kotlin's own style guide recommends this:

In pure Kotlin projects, the recommended directory structure follows the package structure with the common root package omitted. For example, if all the code in the project is in the org.example.kotlin package and its subpackages, files with the org.example.kotlin package should be placed directly under the source root, and files in org.example.kotlin.network.socket should be in the network/socket subdirectory of the source root.

Next I feel there's a design bug in the index route, but I'm gonna push my current tweaks first, and sort that out in the next section.


Giving control to a controller

This design bug: here's the entirety of the implementation of that index route and its handling:

fun Application.configureRouting() {

    routing {
        get("/") {
            call.respondText("Hello World!")
        }
    }
}

That's in /src/main/kotlin/plugins/Routing.kt

Routing should limit itself to what it says ion the tin: routing. It should not be providing the response. It should route the request to a controller which should control how the response is handled. I know this is only example code, but example could should still follow appropriate design practices. So erm: now I have to work out how to create a controller in Ktor. I'm pleased I have a green test on that index route though, cos this is all pretty much a refactoring exercise, so whatever I do: in the end I'll know I have done good if the test still passes.

Hrm. Having not found any examples in the Ktor docs of how to extract controller code out of the routing class, I found myself reading Application structure, specifically these paras:

Different to many other server-side frameworks, it doesn't force us into a specific pattern such as having to place all cohesive routes in a single class name CustomerController for instance. While it is certainly possible, it's not required.
Frameworks such as ASP.NET MVC or Ruby on Rails, have the concept of structuring applications using three folders - Model, View, and Controllers (Routes).

My emphasis. I see. Ktor does not separate-out the idea of routing from the idea of controllers, I see. To me they're different things, but I guess I can see there's overlap. I'm not hugely enamoured with their thinking that "despite the rest of the world using the term MVC, we know better: we're gonna think of it as MVR". Just… why. If you wanna conflate routing and controllers, yeah fine. But in that case they conflate into the controller part of MVC. You don't just go "ah nah it's MVR, trust me". Remember what I said before about opinionated frameworks? This is why I don't like it when frameworks have opinions.

But anyway.

We can still separate out groups of "route-handlers" (sigh) into separate functions. ow I have this:

package routes

import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Route.indexRouting() {
    route("/") {
        get {
            call.respondText("Hello World!")
        }
    }
}

And my original configureRouting function is just this:

fun Application.configureRouting() {

    routing {
        indexRouting()
    }
}

That's good enough.


Auto-reload

One good thing my RTFMing about controllers lead me to was how to get my app to rebuild / reload when I make code changes. By default every time I changed my code I had to shut down the app (remember it's serving a web app now), rebuild, and then re-run the app. That was not the end of the world, but was pretty manual.

Ktor have thought about this, and the solution is easy.

First, I tell my app it's in development mode (in gradle.properties):

junitJupiterVersion=5.9.0
kotestVersion=5.5.0
kotlinVersion=1.7.20
ktorVersion=2.1.2
logbackVersion=1.2.11

kotlin.code.style=official

org.gradle.warning.mode=all

development=true

This in turn is picked up by code in build.gradle.kts

application {
    mainClass.set("ApplicationKt")

    val isDevelopment: Boolean = project.ext.has("development")
    applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}

(that code was already there).

Then I needed to tell the app what to pay attention to for reloading (in Application.kt):

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0", watchPaths = listOf("classes")) {
        configureMonitoring()
        configureSerialization()
        configureRouting()
    }.start(wait = true)
}

classes there is a reference to build/classes in the project file system.

Then get Gradle to rebuild when any source code changes:

PS C:\src\kotlin\ktor\gdayworld> ./gradlew --continuous :build
BUILD SUCCESSFUL in 1s
13 actionable tasks: 13 up-to-date

Waiting for changes to input files... (ctrl-d then enter to exit)
<-------------> 0% WAITING
> IDLE

And another instance of Gradle to run the app with it watching the build results:

PS C:\src\kotlin\ktor\gdayworld> ./gradlew :run                  
> Task :run
2022-10-16 14:21:19.374 [main]  DEBUG ktor.application - Java Home: C:\apps\openjdk\EclipseAdoptium
2022-10-16 14:21:19.374 [main]  DEBUG ktor.application - Class Loader: jdk.internal.loader.ClassLoaders$AppClassLoader@73d16e93:...]
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main\me\adamcameron\plugins for changes.
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main\routes for changes.
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main\me for changes.
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main for changes.
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main\META-INF for changes.
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main\me\adamcameron for changes.
2022-10-16 14:21:19.390 [main]  DEBUG ktor.application - Watching C:\src\kotlin\ktor\gdayworld\build\classes\kotlin\main\plugins for changes.
2022-10-16 14:21:19.562 [main]  INFO  ktor.application - Application started in 0.298 seconds.
2022-10-16 14:21:19.562 [main]  INFO  ktor.application - Application started: io.ktor.server.application.Application@5a45133e
2022-10-16 14:21:19.937 [main]  INFO  ktor.application - Responding at http://127.0.0.1:8080
<===========--> 85% EXECUTING [15s]
> :run

When I change any source code now, the project rebuilds, and the app notices the recompiled classes, and restarts itself:

modified: C:\src\kotlin\ktor\gdayworld\src\main\kotlin\routes\IndexRoutes.kt
Change detected, executing build...


BUILD SUCCESSFUL in 6s
13 actionable tasks: 12 executed, 1 up-to-date

Waiting for changes to input files... (ctrl-d then enter to exit)
<=============> 100% EXECUTING [8m 55s]
2022-10-16 14:26:20.438 [eventLoopGroupProxy-4-2]  INFO  ktor.application - 200 OK: GET - /
2022-10-16 14:26:34.861 [eventLoopGroupProxy-3-1]  INFO  ktor.application - Changes in application detected.
2022-10-16 14:26:35.073 [eventLoopGroupProxy-3-1]  DEBUG ktor.application - Changes to 18 files caused application restart.
[...]
2022-10-16 14:26:35.106 [eventLoopGroupProxy-3-1]  INFO  ktor.application - Application auto-reloaded in 0.012 seconds.
2022-10-16 14:26:35.106 [eventLoopGroupProxy-3-1]  INFO  ktor.application - Application started: io.ktor.server.application.Application@33747fec
2022-10-16 14:26:35.107 [eventLoopGroupProxy-4-2]  INFO  ktor.application - 200 OK: GET - /
<===========--> 85% EXECUTING [5m 32s]
> :run

Note how the app doesn't restart until I actually use it, which is good thinking.

One might as why I have dropped down to a shell to do this autoload stuff? As far as I can tell it's not baked into IntelliJ yet, so needs to be handled directly by Gradle for now. It's not a hardship. I mean: the shells I am running there are being run from within IntelliJ, it's just slightly more complicated than a key-combo or some mouseclicks.

OK. That's all good progress. I'm gonna take a break and come back and create my own controller / response / etc, which is what the object of the exercise was today.


Docker

Ktor's way

I was not expecting this to be the next step, but I just spotted some stuff about Docker in the Ktor docs ("Docker"), so I decided to see what they said.

[time passes whilst I do battle with the docs]

OK, screw that. It's a very perfunctory handling of it. I can build a jar and create an image that will run it, and then run the container - and it all works - but it's… a bit… "proof of concept". From reading the docs and the code snippets that link from the docs (Deployment - Ktor plugin › Build and run a Docker image).

I had to add this to my build.gradle.kts file:

ktor {
    fatJar {
        archiveFileName.set("gday-world-ktor.jar")
    }
    docker {
        jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)
        localImageName.set("gday-world-ktor")
        imageTag.set("${project.version}-preview")
        portMappings.set(listOf(
            io.ktor.plugin.features.DockerPortMapping(
                8080,
                8080,
                io.ktor.plugin.features.DockerPortMappingProtocol.TCP
            )
        ))
    }
}

And then from the shell I could run this lot:

PS C:\src\kotlin\ktor\gdayworld> ./gradlew :buildFatJar     
[…]
PS C:\src\kotlin\ktor\gdayworld> ./gradlew :runDocker

And I would indeed end up with a running Docker container. Which is handy, but I had no control over what params were passed to docker run, so I couldn't even give the container a name, so I just ended up with one of Docker's random ones. That's a bit amateurish. I checked to see if I was missing anything with the plugin, but this is the code (from Ktor's repo on GitHub):

private abstract class RunDockerTask : DefaultTask() {
    @get:Inject
    abstract val execOperations: ExecOperations

    @get:Input
    abstract val fullImageName: Property<String>

    @TaskAction
    fun execute() {
        val dockerExtension = project.getKtorExtension<DockerExtension>()
        execOperations.exec {
            it.commandLine(buildList {
                add("docker")
                add("run")
                for (portMapping in dockerExtension.portMappings.get()) {
                    add("-p")
                    with(portMapping) {
                        add("${outsideDocker}:${insideDocker}/${protocol.name.lowercase()}")
                    }
                }
                add(fullImageName.get())
            })
        }
    }
}

It looks to me like it simply builds a string docker run [port mappings] [image name], and that's it. No scope for me to specify any other of docker run's parameters in my build config.

So: nah, not doing that; I'll DIY. It's at least shown me what I need to do in a DockerFile and I can organise my own docker-compose.yml file.


My way

[…]

I have this docker/Dockerfile:

FROM gradle:7-jdk17 AS build
COPY --chown=gradle:gradle .. /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle test --no-daemon
RUN gradle buildFatJar --no-daemon

FROM openjdk:17
EXPOSE 8080:8080
RUN mkdir /app
COPY --from=build /home/gradle/src/build/libs/*.jar /app/gday-world-ktor.jar
ENTRYPOINT ["java","-jar","/app/gday-world-ktor.jar"]

This is pretty much lifted from the Ktor Docker › Prepare Docker image docs I linked to above, I've just added the test-round in first.

And this docker/docker-compose.yml:

version: '3'

services:
  gday-world-ktor:
    build:
      context: ..
      dockerfile: docker/Dockerfile
    ports:
      - "8080:8080"
    stdin_open: true
    tty: true

And when I run docker-compose up --build --detach, after a couple of minutes, I get an up and running container with my app in it. Bonus: it halts if my tests don't first pass.

I'm not enamoured with the "after a couple of minutes" part of this: seems really slow for what it needs to do. I am "sure" there must be a way of telling Gradle to do the build, test-run and jar-build all on one operation. However I'm over googling things starting with "gradle" today, so I'm gonna leave it for now.

I'm pretty happy with the progress I made today.

Righto.

--
Adam

Saturday 8 October 2022

Kotlin/Gradle: abstracting versions into a config file, and wondering what delegated properties are

G'day:

Currently I have just been following the instructions fairly slavishly when it comes to my dependency management in my application. If some readme file says "put testImplementation("io.kotest:kotest-runner-junit5:5.5.0") in yer Gradle build file to include this dependency", then that's exactly what I do. Sometimes I monkey with the version if IntelliJ says there's a newer version. So I've ended up with this lot:

dependencies {
    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:5.5.0")
    testImplementation("io.kotest:kotest-assertions-core:5.5.0")
    testImplementation("com.github.stefanbirkner:system-lambda:1.2.1")
    testImplementation("org.apache.commons:commons-lang3:3.12.0")
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.0")
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.0")
    testImplementation("io.kotest:kotest-framework-datatest:5.5.0")
    implementation("io.ktor:ktor-client-core:2.1.2")
    implementation("io.ktor:ktor-client-cio:2.1.2")
}

That's fine. However then I looked at the sample project that the Ktor Project Generator generates, and I see this in my its build file:

val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project

// ...

dependencies {
    implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
    implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
    implementation("ch.qos.logback:logback-classic:$logback_version")
    testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}

What's all this by project carry-on? I looked for a string match for ktor_version, and found this lot in gradle.properties:

ktor_version=2.1.2
kotlin_version=1.7.20
logback_version=1.2.11
kotlin.code.style=official

(Mine currently only has that last line)

OK so that seems like a good idea, I'm gonna run with this approach for the dependencies in my scratch project too:

kotlin.code.style=official
kotest_version=5.5.0
kotlin_version=1.7.10
system_lambda_version=1.2.1
commons_lang_3_version=3.12.0
junit_jupiter_version=5.9.0
ktor_client_core_version=2.1.2
dependencies {
    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:$kotest_version")
    testImplementation("io.kotest:kotest-assertions-core:$kotest_version")
    testImplementation("io.kotest:kotest-framework-datatest:$kotest_version")
    testImplementation("com.github.stefanbirkner:system-lambda:$system_lambda_version")
    testImplementation("org.apache.commons:commons-lang3:$commons_lang_3_version")
    testImplementation("org.junit.jupiter:junit-jupiter:$junit_jupiter_version")
    testImplementation("org.junit.jupiter:junit-jupiter:$junit_jupiter_version")
    implementation("io.ktor:ktor-client-core:$ktor_client_core_version")
    implementation("io.ktor:ktor-client-cio:$ktor_client_core_version")
}

When I rebuilt: it all went fine, and all my tests still pass.

I noted in the Ktor build file the Kotlin plug-in version was still a static inline string:

plugins {
    application
    kotlin("jvm") version "1.7.20"
    // …
}

I tried to make that dynamic as well:

plugins {
    kotlin("jvm") version kotlin_version
    application
}

But INtelliJ barfed at this saying:

'val kotlin_version: String' can't be called in this context by implicit receiver. Use the explicit one if necessary

I googled that, and landed on Why can’t I use val inside Plugins {}? which pointed me to this: Using Gradle Plugins › Limitations of the plugins DSL, and the important bit there is:

This requires that plugins be specified in a way that Gradle can easily and quickly extract, before executing the rest of the build script. It also requires that the definition of plugins to use be somewhat static.

(My emphasis).

Fair cop. I'll not try to do that then.

All this is lovely, but how does this work. What magic is "by project" doing? I guessed that by is some inline function, and project is its argument, similar to in a test where I have x shouldBe y. I can drill down on by, and it takes me to org.gradle.kotlin.dsl.ProjectExtensions:

operator fun Project.provideDelegate(any: Any?, property: KProperty<*>): PropertyDelegate =
    propertyDelegateFor(this, property)

I'm none-the-wiser looking at that, but it gave me something to google (googling on phrases where "by" is the important word was not a particularly edifying or fruitful endeavour). I found Delegated properties, the summary of which is:

With some common kinds of properties, even though you can implement them manually every time you need them, it is more helpful to implement them once, add them to a library, and reuse them later.

Kotlin supports delegated properties:

class Example {
    var p: String by Delegate()
}

OK, that answers that. There is also a bunch more interesting stuff in that article which I will need to look at later. Oh and what's the "project" part of "by project"? It's just an object that wraps up the values I set in gradle.properties. Slick.

I'm really glad to understand what's going on there, and also looking forward to looking at "delegated properties" some more. Later.


That was painless and short. I quite like that I have de-duped my versions there, and separated them from their usage. The changes made for this exercise are in GitHub @ kotlin_scratch, tag 1.12. Not that it's very intertesting code today, I s'pose ;-)

Righto.

--
Adam

Sunday 21 August 2022

Kotlin / Gradle / Kotest random exploration

G'day:

I am currently wondering whether this one will ever see light-of-day. It's very much what it says on the tin: I've been messing with my Kotlin ecosystem again, and I've tried and suceeded with some random things, so thought I'd write them down. There's nothing groundbreaking here, I warn you. But it's all new to me.

This follows on from the previous article: "Kotlin: creating a project and getting some code (and tests) to run". This is the very next time I have sat down and opened IntelliJ and gone "OK so now what?".


What I actually sat down to do was to work out what all the shit in the build.gradle.kts file actually means, so I had a google and found Gradle Kotlin DSL Primer. This seemed like a good start. However I only got as far as a mention of "Run ./gradle tasks to get more details" and I thought "Oh yeah, I should probably know how to use the thing from the shell as well as just pushing buttons in the IDE", so dropped down to my shell and ran ./gradlew.bat tasks (note the docs said gradle not gradlew but I did not notice that at first. And the .bat bit is just cos I'm running this in Powershell not in Bash).

I googled the difference between gradle and gradlew, and found this: gradle vs. gradlew – what’s the difference?. Basically gradle runs the underlying Gradle application, whereas gradlew runs the "Gradle Wrapper" which is the app's stand-alone Gradle runner which bundles up a bunch of app-related stuff,a nd indeed will install Gradle if it's not already installed. It's just a "self-containment" sort of thing. Best to read the article to get that clear.

Anyhow, running gradlew.bat tasks output a bunch of stuff which I won't be going back to in this article (told you it was random):

Because I noticed the magic word:

test - Runs the test suite.

(You have to picture me sitting at my keyboard clapping twice to myself and voicing "yay" with more glee than I really ought to be admitting to).


Right so let's run my tests:

PS C:\src\kotlin\scratch> .\gradlew.bat test

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 up-to-date
PS C:\src\kotlin\scratch>

OK, that's not as exciting as it could be. Indeed I dunno even if it ran some tests or not. Let's red-green-refactor this: I'll break a test…

PS C:\src\kotlin\scratch> .\gradlew.bat test

> Task :test

MainTest > Tests of Main class > MainTest.outputs G'day World & its args FAILED
    io.kotest.assertions.AssertionFailedError at MainTest.kt:12

5 tests completed, 1 failed

> Task :test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///C:/src/kotlin/scratch/build/reports/tests/test/index.html

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 4s
3 actionable tasks: 2 executed, 1 up-to-date
PS C:\src\kotlin\scratch>

Whoa. OK. I guess if the build is green: all tests pass. If not: it'll let me know.

But what's this report it speaks of:

> There were failing tests. See the report at: file:///C:/src/kotlin/scratch/build/reports/tests/test/index.html

Let's have a look at that:

I like that. And I can drill down into my failed test too:

Let's fix the test and look at the passing report:


Cool. My next random thought was "hrmph: PowerShell". I don't like PowerShell. Largely cos I've never bothered to learn to use it, either just using the old school Windows shell, or more often when I'm needing to use a shell it's in a *nix environment, and I use Bash. So let's see if I can do all this in Bash as well:

adam@DESKTOP-QV1A45Uadam@DESKTOP-QV1A45U:~$ cd /mnt/c/src/kotlin/scratch/
adam@DESKTOP-QV1A45U:/mnt/c/src/kotlin/scratch$ ./gradlew test
Starting a Gradle Daemon (subsequent builds will be faster)

> Task :test

MainTest > Tests of Main class > MainTest.outputs G'day World & its args FAILED
    io.kotest.assertions.AssertionFailedError at MainTest.kt:12

MainTest > Tests of Main class > MainTest.works OK with no args FAILED
    io.kotest.assertions.AssertionFailedError at MainTest.kt:19

5 tests completed, 2 failed

> Task :test FAILED

Oh. :-(

However checking the report and drilling down into the failures, it made sense:

io.kotest.assertions.AssertionFailedError:
(contents match, but line-breaks differ; output has been escaped to show line-breaks)
expected:<G'day World!\r\nProgram arguments: some arg, some other arg>
but was:<G'day World!\nProgram arguments: some arg, some other arg>

It's the old "Windows uses CRLF / *nix uses LF" thing, and my test has hard-coded CRLF in it:

output.trim() shouldBe "G'day World!\r\nProgram arguments: some arg, some other arg"

I'm sure there's a constant or something in Java that is the current EOL marker for the given operating system. I googled around and found this: Stack Overflow › Is there a Newline constant defined in Java like Environment.Newline in C#?, and the answer is System.lineSeparator() (integrated into my tests now):

class MainTest : DescribeSpec ({
    describe("Tests of Main class") {
        val EOL = System.lineSeparator()

        it("outputs G'day World & its args") {
            val testArgs = arrayOf("some arg", "some other arg")
            val output = SystemLambda.tapSystemOut {
                main(testArgs)
            }
            output.trim() shouldBe "G'day World!${EOL}Program arguments: some arg, some other arg"
        }
        it("works OK with no args") {
            val testArgs = arrayOf<String>()
            val output = SystemLambda.tapSystemOut {
                main(testArgs)
            }
            output.trim() shouldBe "G'day World!${EOL}Program arguments:"
        }
    }
})

Right so lets see if the tests work in both environments now. Powershell:

BUILD SUCCESSFUL in 1s

Bash:

BUILD SUCCESSFUL in 1s

Cool.


Oh I need to back up a bit. I also had to go find out how to do interpolated strings in Kotlin as well for that:

output.trim() shouldBe "G'day World!${EOL}Program arguments:"

I googled-up this page: How Does String Interpolation Work in Kotlin?, which says one just prefixes the variable name with $. As my EOL variable usage is hard-up against another string literal, I needed to (guess) that one should surround it in curly braces to make it clear where variable name stopped and string literal started. Otherwise I got this: Unresolved reference: EOLProgram.


The last thing I thought about was "but what if there was no generic solution for the test, and I had to only conditionally run a test given some criterion?" Kotest can do this: Conditional tests with enabled flags. I put together a new test to show myself this enabled flag in action:

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import org.apache.commons.lang3.SystemUtils

class KotestTest : DescribeSpec( {
    describe("Testing test conditionality") {
        it ("only runs on linux").config(enabled = SystemUtils.IS_OS_LINUX) {
            SystemUtils.IS_OS_LINUX shouldBe true
        }
    }
})

To use SystemUtils I had to include Apache Commons as a dependency in build.gradle.kts:

dependencies {
    implementation("org.apache.commons:commons-lang3:3.12.0")

    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:5.4.2")
    testImplementation("io.kotest:kotest-assertions-core:5.4.2")
    testImplementation("com.github.stefanbirkner:system-lambda:1.2.1")
}

BTW I'm finding the package name by just googling something like "apache commons maven", and it leads me to a page like this: https://mvnrepository.com/artifact/org.apache.commons/commons-lang3/3.12.0:

That even gives me the statement to use in my build file. In hindsight I should probably only be using that as a test dependency, so I'll change that:

testImplementation("org.apache.commons:commons-lang3:3.12.0")

Also note that after discussion with SeanC in the Working Code Podcast Discord channel, I have stopped using the ranged constraints for my dependencies (eg: something like 3.12.+), but using concrete ones (eg: something like 3.12.0) like I have here. I've changed all the other ones I alerady had, too. This is just "the Java way" apparently. Duly noted.

OK so now I run the tests, and check if the conditionality works. The difference is best seen in the test report:

Bash:

Powershell:

Excellent.


At this point I had decided I had had enough, and also thought maybe I could get a blog article out of it (still not sure I have, but hey… the bar is set pretty low around here so I'm gonna press "publish" anyhow).

Just to recap, this is what I learned in this exercise:

  • I found that docs page on the Kotlin Gradle DSL which I will come back to (Gradle Kotlin DSL Primer),
  • I had the most superficial of looks at the gradlew shell runner thingey.
  • I got a better understanding of gradle vs gradlew.
  • I found out how to run the tests on same.
  • And discovered that auto-generated HTML test results page.
  • The tests will run just fine on Powershell and Bash.
  • System.lineSeparator() contains the platform-specific line ending.
  • How to do interpolated strings, eg: "G'day World!${EOL}Program arguments:".
  • How to set a a condition on a Kotest test to control whether it's ignored.
  • How to find what the dependency string and version is for the implementation/testImplementation statement is, from Maven.

There's no rocket science there, nor anything complicated. But I've only just started looking at this stuff so can't expect too much. Everything was really easy to find, and worked with a minimum of fuss, too.

Let me know if this article is too "all over the place" or it held any interest or not. I actually found it quite useful myself for formalising the stuff I had looked at, so I guess it's at least useful to me if not for anyone else.

Righto.

--
Adam


PS: Oh! I also learned how to pass args to my app when I run it. Cos like I didn't just test it, I also ran it:

adam@DESKTOP-QV1A45U:/mnt/c/src/kotlin/scratch$ ./gradlew run --args "first second third"

> Task :run
G'day World!
Program arguments: first, second, third  

PPS: the code for this is at https://github.com/adamcameron/kotlin_scratch/tree/1.1.