Wednesday, 31 August 2022

CFML: outputting text from within inline Java in CFML

G'day:

I saw an odd question today. Me mate Ray was messing around with the Java integration in ColdFusion (that allows one to write Java code directly in one's CFML code), and asked how to write out content to the screen from within the Java code. IE: the equiv of emiting some text in one's CFML response… it ends up on the client browser "screen" (this is not my wording).

My initial reaction was "isn't the answer System.out.println("G'day Ray!")?". But it's not. System.out writes to stdout. Not the response buffer. In my Docker container if I do that println, I get it in my Docker log, eg:

OK so it's not a dumb question.

I knew the answer would be "stick it in the PageContext's BodyContent", but I had no idea of how to get a reference to that via Java. Especially basically a Java snippet running inside a ColdFusion request. I googled a lot and did not find anything I understood (my Java is shit).

So I cheated. ColdFusion knows how to get the current PageContext: via getPageContext(). I'll just pass that in to my Java code:

Warning

This code runs fine in the version of CF that is installed from the adobecoldfusion/coldfusion2021:latest Docker image, but does not run on a local install of CF (even the "exact same version"). It will give you a The setPageContext method was not found. error. I'm not sure why, but I managed to grab the ear of one of the Adobe bods, and they are actively looking into it. I'll report back when we get an answer on this.

messager = java {

    import javax.servlet.jsp.PageContext;

    public class Outputer {

        PageContext pageContext;

        public void output(String text) throws java.io.IOException {
            this.pageContext.getOut().print(text);
        }

        public void setPageContext(PageContext pageContext) {
            this.pageContext = pageContext;
        }
    }
}

messager.setPageContext(getPageContext())
writeOutput("Before stuff added from Java<br>")
messager.output("G'day Ray!<br>")
writeOutput("After stuff added from Java<br>")

And this works:

Before stuff added from Java
G'day Ray!
After stuff added from Java

I dunno if it's a good way of doing it, but it's "a" way.

I'd still prefer knowing how to get the PageContext directly from the Java code, if anyone knows…?

Righto.

--
Adam

Saturday, 27 August 2022

CFML: testing applicationStop behaviour. Again.

G'day:

As the title suggests, I've done this before: Investigation into applicationStop(). However this time I've tested on Lucee as well. It's interesting to see how my code differs from 2012 and now.

This investigation came up cos someone asked on the CFML Slack channel how applicationStop behaves vis-a-vis other already-running requests. The suspicion being that the request calling applicationStop will break other already running requests that still need to rely on the application state as it was when their requests start. I was pretty sure ColdFusion had thought of that, and it made sure running requests continued with the "correct" application state, even if it's been reset during their run. I have to admit when I started looking into it, it did not occur to me that I might have already checked for this. Ah well. Anyway: I'm just writing this article so I can link to something in Slack.

The code is simple:

// Application.cfc
component {
    this.name = "testApp"
    this.applicationTimeout = createTimespan(0, 0, 0, 30)

    function onApplicationStart() {
        application.uuid = createUuid()
        logMessage(getFunctionCalledName())
    }

    function onApplicationEnd(applicationScope) {
        logMessage(getFunctionCalledName())
    }

    function logMessage(handlerName) {
        writeLog(
            file = "appStopDebug",
            text = handlerName
                & " (#getFileFromPath(getBaseTemplatePath())#)"
                & " (query_string: #CGI.query_string#)"
                & " (application?.uuid: [#application?.uuid#])")
    }
}

This sets a UUID into the application scope when the application starts, and logs a line when the application starts and ends.


// beforeStop.cfm
writeLog(file="appStopDebug", text="START beforeStop.cfm (query_string: #CGI.query_string#) (application?.uuid: [#application?.uuid#])")
sleep(5000)
writeLog(file="appStopDebug", text="END beforeStop.cfm (query_string: #CGI.query_string#) (application?.uuid: [#application?.uuid#])")

My test requests this file three times, just to get some slow (note the sleep call there) requests running before I stop the app.


// applicationStop.cfm
writeLog(file="appStopDebug", text="START applicationStop.cfm (application?.uuid: [#application?.uuid#])")
applicationStop()
writeLog(file="appStopDebug", text="END applicationStop.cfm (application?.uuid: [#application?.uuid#])")

After kicking off the first three requests, the test calls this one to stop the application. Note that the first three requests will still be running when this is called. So the application will end whilst they are still needing their application state.


// afterStop.cfm
writeLog(file="appStopDebug", text="START afterStop.cfm (application?.uuid: [#application?.uuid#])")
sleep(5000)
writeLog(file="appStopDebug", text="END afterStop.cfm (application?.uuid: [#application?.uuid#])")

This is called after the application is stopped, just to restart it again; and application.uuid will be re-created with a new value by the onApplicationStart handler.


And this next one is the thing that runs the test:

// test.cfm
writeLog(file="appStopDebug", text="==================== TEST STARTED ====================")

port = server.coldfusion.productname contains "ColdFusion" ? 8500 : 8888;

for (i=1; i <= 3; i++) {
    testUrl = "http://localhost:#port#/cfml/applicationFramework/applicationStop/testApp/beforeStop.cfm?i=#i#"
    writeOutput("Calling #testUrl#<br>")
    thread name="t#i#" action="run" testUrl=testUrl {
        cfhttp(url=attributes.testUrl, timeout=0);
    }
}

sleep(500)
writeOutput("Running applicationStop.cfm<br>")
cfhttp(url="http://localhost:#port#/cfml/applicationFramework/applicationStop/testApp/applicationStop.cfm", timeout=0);

sleep(500)
writeOutput("Running afterStop.cfm<br>")
cfhttp(url="http://localhost:#port#/cfml/applicationFramework/applicationStop/testApp/afterStop.cfm", timeout=0);

Note that the test.cfm file is in a separate directory and runs in a different application to the files it calls.

I've summarised what test.cfm does, above. Note how it calls beforeStop.cfm in their own threads so that I can get those underway, but not have to wait until they finish before calling applicationStop.cfm. The thing with the port variable is just cos I run the exact same code in my CF and Lucee containers, and they're both listening on different ports.

Here's the log from CF2021:

14:01:08	TESTERAPP	==================== TEST STARTED ====================
14:01:08	TESTAPP	onApplicationStart (beforeStop.cfm) (query_string: i=1) (application?.uuid: [BC105138-F6A3-62EC-146CC1456BB982D8])
14:01:08	TESTAPP	START beforeStop.cfm (query_string: i=3) (application?.uuid: [BC105138-F6A3-62EC-146CC1456BB982D8])
14:01:08	TESTAPP	START beforeStop.cfm (query_string: i=1) (application?.uuid: [BC105138-F6A3-62EC-146CC1456BB982D8])
14:01:08	TESTAPP	START beforeStop.cfm (query_string: i=2) (application?.uuid: [BC105138-F6A3-62EC-146CC1456BB982D8])
14:01:09	TESTAPP	START applicationStop.cfm (application?.uuid: [BC105138-F6A3-62EC-146CC1456BB982D8])
14:01:09	TESTAPP	onApplicationEnd (applicationStop.cfm) (query_string: ) (application?.uuid: [])
14:01:09	testApp	END applicationStop.cfm (application?.uuid: [BC105138-F6A3-62EC-146CC1456BB982D8])
14:01:09	TESTAPP	onApplicationStart (afterStop.cfm) (query_string: ) (application?.uuid: [BC107AA8-BCA5-9CD9-3F2E0D20E47D0F60])
14:01:09	TESTAPP	START afterStop.cfm (application?.uuid: [BC107AA8-BCA5-9CD9-3F2E0D20E47D0F60])
14:01:13	TESTAPP	END beforeStop.cfm (query_string: i=3) (application?.uuid: [BC105138-F6A3-62EC-146CC1456BB982D8])
14:01:13	TESTAPP	END beforeStop.cfm (query_string: i=1) (application?.uuid: [BC105138-F6A3-62EC-146CC1456BB982D8])
14:01:13	TESTAPP	END beforeStop.cfm (query_string: i=2) (application?.uuid: [BC105138-F6A3-62EC-146CC1456BB982D8])
14:01:14	TESTAPP	END afterStop.cfm (application?.uuid: [BC107AA8-BCA5-9CD9-3F2E0D20E47D0F60])
14:02:21	TESTAPP	onApplicationEnd (Application.cfc) (query_string: ) (application?.uuid: [])

Here you can see that the app starts and sets application.uuid to be BC105138-F6A3-62EC-146CC1456BB982D8, and that value sticks through until the end of the beforeStop.cfm runs, even though in the middle the application has been stopped and restarted again (and in the new application, application.uuid is BC107AA8-BCA5-9CD9-3F2E0D20E47D0F60. This is what one would want. Well done ColdFusion.

Not so much for Lucee:

13:57:08	testerApp	==================== TEST STARTED ====================
13:57:08	testApp	onApplicationStart (beforeStop.cfm) (query_string: i=1) (application?.uuid: [FDC7FFBB-538B-4630-9894DA7F8E82726F])
13:57:08	testApp	START beforeStop.cfm (query_string: i=1) (application?.uuid: [FDC7FFBB-538B-4630-9894DA7F8E82726F])
13:57:08	testApp	START beforeStop.cfm (query_string: i=2) (application?.uuid: [FDC7FFBB-538B-4630-9894DA7F8E82726F])
13:57:08	testApp	START beforeStop.cfm (query_string: i=3) (application?.uuid: [FDC7FFBB-538B-4630-9894DA7F8E82726F])
13:57:08	testApp	START applicationStop.cfm (application?.uuid: [FDC7FFBB-538B-4630-9894DA7F8E82726F])
13:57:08	testApp	onApplicationEnd (applicationStop.cfm) (query_string: ) (application?.uuid: [FDC7FFBB-538B-4630-9894DA7F8E82726F])
13:57:08	testApp	END applicationStop.cfm (application?.uuid: [])
13:57:09	testApp	onApplicationStart (afterStop.cfm) (query_string: ) (application?.uuid: [74D5E31E-46D2-4884-A3D14A6D5A31EAB8])
13:57:09	testApp	START afterStop.cfm (application?.uuid: [74D5E31E-46D2-4884-A3D14A6D5A31EAB8])
13:57:13	testApp	END beforeStop.cfm (query_string: i=1) (application?.uuid: [])
13:57:13	testApp	END beforeStop.cfm (query_string: i=2) (application?.uuid: [])
13:57:13	testApp	END beforeStop.cfm (query_string: i=3) (application?.uuid: [])
13:57:14	testApp	END afterStop.cfm (application?.uuid: [74D5E31E-46D2-4884-A3D14A6D5A31EAB8])
13:57:58	testApp	onApplicationEnd (Application.cfc) (query_string: ) (application?.uuid: [74D5E31E-46D2-4884-A3D14A6D5A31EAB8])

In Lucee's case, as soon as applicationStop is called, the application state of running requests is banjanxed: note that in the END beforeStop.cfm entries, application.uuid is just gone. This is not very well-reasoned on Lucee's part, I think.


Oh, why did applicationStop come up in the first place? Once again someone was suggesting to another person that to restart an application, one just needs to call onApplicationStart. This is not correct. onApplicationStart is called as a result of the application starting; it doesn't in itself cause the application to (re)start. I cover this in another earlier article: CFML: The difference between events and event handlers. If you want the application to restart in a controlled fashion, you must call applicationStop(), and let the CFML server restart the app in an orderly fashion. Simply running the event handler that's called after the application has been started is not the same thing, and can be dangerous, and lead to an application being unstable.

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.

Tuesday, 16 August 2022

Kotlin: creating a project and getting some code (and tests) to run

G'day

This whole thing is gonna be a cross between a note-to-self and a pseudo-stream-of-consciousness as I set up a new Kotlin project in IntelliJ IDEA, and demonstrate to myself I can make some code run, as well as some tests. Why am I doing this? Well because in the last eight months I've been through this exercise half a dozen times, with a month or two between each one, and every time it's been a right pain in the arse to work out what I need to do. Context: I am 100% new to Kotlin, and I am 100% new to Gradle, and I am my past has been the luxury of writing apps in scripting languages (CFML, PHP) that need close to zero messing about to get to the "G'day World" stage (and tests thereof) into production. It's easy. Any idiot can do it (ahem). So it's annoying that I need to do all this faffing about. I hasten to add I am not going as far as "getting into production" here: I don't even know what I need to put into production if I wanted to. I'm just getting to the point where code runs in the IDE.

I worked through all this yesterday evening and early this evening, and I think I know the steps now. So I've blitzed my previous project and am starting again.

New Project

I've opened IntelliJ IDEA and clicked "New Project", and completed the form on the ensuing screen:

And I click "Create".

Project files

Having done that, I get this lot:

/mnt/c/src/kotlin/scratch$ tree
.
├── build.gradle.kts
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
    ├── main
    │   ├── kotlin
    │   │   └── Main.kt
    │   └── resources
    └── test
        ├── kotlin
        └── resources

9 directories, 8 files

scratch/src/main/kotlin/Main.kt

It's created a G'day world function for me, but there's a typo in it:

fun main(args: Array<String>) {
    println("Hello World!")

    // Try adding program arguments via Run/Debug configuration.
    // Learn more about running applications: https://www.jetbrains.com/help/idea/running-applications.html.
    println("Program arguments: ${args.joinToString()}")
}

Fixed:

println("G'day World!")


scratch/gradle.properties

kotlin.code.style=official

OK. Shrug.

scratch/settings.gradle.kts

rootProject.name = "scratch"

Yup. Another shrug.

scratch/gradlew.bat / scratch/gradlew

These are shell scripts Gradle seems to create to "do stuff". It's boilerplate and I won't be touching it.

scratch/.gradle / scratch/.idea / scratch/gradle directories

More boilerplace that I don't seem to need to know about ATM. Well the .idea dir is the project config dir for Intellif IDEA, I know that.

scratch/build.gradle.kts

This is the file I seem to spend all my time in. The default contents are:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

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

group = "me.adamcameron"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation(kotlin("test"))
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "1.8"
}

application {
    mainClass.set("MainKt")
}

That's all lovely. Other than recognising some of the words, I have NFI what's going on really. Well I superficially do, but exactly the reason it is the way it is: NFI. As far as I can tell this is the Gradle equivalent of composer.json or packages.json or some such. Except it's doing a bunch more than just package management.

Build and run

Anyway, I guess the new project wizard knows what it's doing, so I'm gonna try to build and run the project.

And in the build window I get this lot:

  21:24:31: Executing ':classes :testClasses'...

> Task :wrapper

BUILD SUCCESSFUL in 295ms
1 actionable task: 1 executed
> Task :processResources NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :compileKotlin
> Task :compileJava NO-SOURCE
> Task :classes UP-TO-DATE
> Task :compileTestKotlin NO-SOURCE
> Task :compileTestJava NO-SOURCE
> Task :testClasses UP-TO-DATE

BUILD SUCCESSFUL in 4s
1 actionable task: 1 executed
21:24:37: Execution finished ':classes :testClasses'.

OK. That seems promising in that it's not screaming at me that I've done it all wrong, so. Um. I go back to Main.kt and run it:

Result:

C:\Users\camer\.jdks\openjdk-17.0.1\bin\java.exe …
G'day World!
Program arguments: 

Process finished with exit code 0

Excellent. The default project config will run.

Installing Kotest

It would not be me if I didn't do some testing. I actually feel a bit bad running the app before having tests for it, but… small steps.

I wanna use Kotest for my testing, so I'll work out how to install that.

Distilling the info from that quickstart, I've made these changes to my build.gradle.kts:


// ...
dependencies {
    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:5.4.2")
    testImplementation("io.kotest:kotest-assertions-core:5.4.2")
}

tasks.test {
    useJUnitPlatform()
}
tasks.withType<Test>().configureEach {
    useJUnitPlatform()
}

Note that the docs actually said to use literally $version but after having done that and it trying to use my app's version:

version = "1.0-SNAPSHOT"

I inferred that that was just unfortunate shorthand, and they mean "the version you want to install". Checking io.kotest:kotest-runner-junit5 and io.kotest:kotest-assertions-core in the Maven repo, it's 5.4.2 for each, so that's why I'm using that for the version.

That rebuilt OK.

Testing Kotest install

So some tests!

// scratch/src/test/kotlin/SystemTest.kt
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldStartWith

class SystemTest : DescribeSpec({
    describe("Tests of kotest installation") {
        it("should return the size of a string") {
            "hello".length shouldBe 5
        }
        it("should test for the prefix of a string") {
            "world".shouldStartWith("wor")
        }
    }
})

These are just testing that Kotest will do something, and use a couple of the assertions from its assertion lib.

Unfortunately I've gone backwards and forwards with this file whilst testing what I was saying above, but I don't think I have missed any steps here. Anyways, after a rebuild I was able to tell IntelliJ to run the tests:

And the results:

I also have the Kotest plugin for IntelliJ installed, so I can run them from there too:

Testing main

And now I need to get back to the "red" part of "red-green-refactor" of my initial main method. This is a tricky one cos it doesn't return a value, it outputs to stdout (println("G'day World!") etc). I thougth it must be possible to capture stdout from a test and test it, so I sniffed around.

I came across this project on Github (can't remember how I found it): stefanbirkner / system-lambda. This was an easy install:

dependencies {
    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:5.4.+")
    testImplementation("io.kotest:kotest-assertions-core:5.4.+")
    testImplementation("com.github.stefanbirkner:system-lambda:1.2.+")
}

(Also note how I've worked out that version number is a constraint, so I've changed the all to get the latest in the given point release, with the +).

After a rebuild I can add a quick system test in to test the installation of the module:

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldStartWith
import com.github.stefanbirkner.systemlambda.SystemLambda.*

class SystemTest : DescribeSpec({
    describe("Tests of kotest installation") {
        it("should return the size of a string") {
            "hello".length shouldBe 5
        }
        it("should test for the prefix of a string") {
            "world".shouldStartWith("wor")
        }
    }
    describe("tests of system-lambda") {
        it("checks system-lambda is working OK") {
            var testString = "string to capture"
            val output = tapSystemOut {
                print(testString)
            }
            output shouldBe testString
        }
    }
})

It's quite pleasing how the Kotest plugin picked up the new test automatically:

And see it passing:

And now a test actually testing main:

import com.github.stefanbirkner.systemlambda.SystemLambda
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe

class MainTest : DescribeSpec ({
    describe("Tests of Main class") {
        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!\r\nProgram arguments: some arg, some other arg"
        }
    }
})

And watch it pass:

Whilst writing that, I figured I should have a test where I don't pass any args. Not least of all cos I had no idea how to make an empty typed array in Kotlin. It's dead easy as it turns out:

it("works OK with no args") {
    val testArgs = arrayOf<String>()
    val output = SystemLambda.tapSystemOut {
        main(testArgs)
    }
    output.trim() shouldBe "G'day World!\r\nProgram arguments:"
}

Done

And that's about it. I've been through this process three times in the last 24hrs now, and I'm having fewer problems. I still don't really "get" what Gradle is doing (or how), but I know enough to be getting on with some language testing now anyhow. And I'm sure the more I need to mess around with stuff, the more Gradle expertise I will pick-up.

And I'm quite pleased to have a "Kotlin" blog article. Finally. Even though it's just the travails of a noob.

All the code in the project is here: https://github.com/adamcameron/kotlin_scratch/tree/1.0. I have not yet re-cloned it and created a project from that to see if it all works A-OK. I will report back later. Time for one last beer and bed now though.

Righto.

--
Adam

Work with me here - revisited ...again

G'day:

Update

These roles have been filled.

I've done this twice before:

Once again, we're continuing to build the team, and we're recruiting for two roles this time:

The latter role - Senior Application Developer - is the same as the earlier roles we filled: I'm after a senior CFML dev who has strong CFML skills, and who is interested in moving off CFML and onto Kotlin instead. This time around we're also keen to hear from people who possibly has some exposure to React.js or similar. We have some front-end reimplementation coming up. This is a "nice to have" though, so don't be put off if yer purely a strong back-end CFML dev. I discuss this role at length in that first "Work with me here" article I link to above.

The "Software Development Engineer in Test" is an entirely new role for us. We will have a dev team of five, and it's time we did a better job of our QA. Currently QA is done by a mix of the devs and the end-users, which is less good than it could be; our end-to-end automation is nil. We've been living with this on the legacy system, but it's no place to be when we replatform. The person filling this role will be fundamental in planning and executing how we get from zero test automation in an application not designed with test automation in mind; through to having us confident that when we release work, we know it will be stable. And also leading the similar move on the new platform. It's important to note that this is absolutely not a CFML role. Currently we're thinking of using Cypress for this, but even that decision hasn't been made yet. It'll be a decision largely informed by/lead by the person in this new role.

Details of how to apply are at those links.

Righto.

--
Adam

Sunday, 14 August 2022

JS: Server-sent events

G'day:

Yes, it's very odd for me to have something to say about JS stuff. I don't imagine there's anything new here for people that actually do JS development, but I don't, so this feature is new to me. Maybe it'll be new to some of my readers too.

I was looking at a question on Stack Overflow tagged with "ColdFusion": Server Side Events and Polling Database for new records with Lucee/Coldfusion. There was no answer, and I didn't know what the questioner was on about, so I decided to have a look.

Firstly, I RTFMed the JS words I didn't recognise:

The from ferreting around in the docs further, I found a PHP example of the server-side part: MDN › References › Web APIs › Server-sent events › Using server-sent events

From that lot it was easy to knock-together a CFML example.

<!-- test.html -->

<div id="result"></div>

<script>
var source = new EventSource('testInLoop.cfm');

source.addEventListener('message', function(e){
    document.body.innerHTML += `${e.data}<br>`
});
</script>
// testInLoop.cfm

header name="Content-Type" value="text/event-stream";

requestStartedAt = now()
for (i=1; i <= 10; i++) {
    data = {
        "uuid" = createUuid(),
        "now" = now().timeFormat("HH:mm:ss"),
        "requestStartedAt" = requestStartedAt.timeFormat("HH:mm:ss")
    }
    writeOutput("event: message" & chr(10))
    writeOutput('data: #serializeJson(data)#' & chr(10) & chr(10))
    flush;
    sleep(1000)
}
writeOutput("event: message" & chr(10))
writeOutput('data: #serializeJson({"complete": true})#' & chr(10) & chr(10))
flush;

Miraculaously, this works:

{"uuid":"4E1A4D17-6E4A-410B-B9C26B420A0DD5B4","now":"11:15:40","requestStartedAt":"11:15:40"}
{"uuid":"65C3F605-8823-4BF5-B32550FEABD0CECC","now":"11:15:41","requestStartedAt":"11:15:40"}
{"uuid":"D22E86D0-872F-4E5F-A89B29F44314BE05","now":"11:15:42","requestStartedAt":"11:15:40"}
{"uuid":"C4B6C1A6-0EF6-4CFD-AC9D4791F14A74AA","now":"11:15:43","requestStartedAt":"11:15:40"}
{"uuid":"FE0DB9CA-3535-416C-B63066EC4913E87D","now":"11:15:44","requestStartedAt":"11:15:40"}
{"uuid":"3E4E2DED-5B57-40AB-B8F5886DC7983053","now":"11:15:45","requestStartedAt":"11:15:40"}
{"uuid":"19611965-34D0-4ED4-9B9260B7EE3D8941","now":"11:15:46","requestStartedAt":"11:15:40"}
{"uuid":"81C7066F-55AD-47F1-BBBEC6EDAAA7A7C4","now":"11:15:47","requestStartedAt":"11:15:40"}
{"uuid":"2477899D-50EB-4CB0-AAF8E681DF168087","now":"11:15:48","requestStartedAt":"11:15:40"}
{"uuid":"7ABF54BD-1618-458D-A6BBF5D07812564D","now":"11:15:49","requestStartedAt":"11:15:40"}
{"complete":true}
{"uuid":"44AA5416-8EA9-451B-95C60E2BC1F4CEF8","now":"11:15:53","requestStartedAt":"11:15:53"}
{"uuid":"8829A145-93D5-4175-9305414B32CBE83E","now":"11:15:54","requestStartedAt":"11:15:53"}
{"uuid":"15F96712-263C-4798-ABCDBDC0C0AA68F7","now":"11:15:55","requestStartedAt":"11:15:53"}
{"uuid":"AF3A1831-8817-400E-B1292B4FCBD8BDDC","now":"11:15:56","requestStartedAt":"11:15:53"}
{"uuid":"C3D9D100-C8A9-4A92-87F6E62138DB5C6F","now":"11:15:57","requestStartedAt":"11:15:53"}
{"uuid":"1E60C5D6-E16A-429C-804D32B278272872","now":"11:15:58","requestStartedAt":"11:15:53"}
{"uuid":"DDB2DA3C-D064-4F6E-80227E337B41D657","now":"11:15:59","requestStartedAt":"11:15:53"}
{"uuid":"E7034B05-7912-4971-9ECD76E07403AD49","now":"11:16:00","requestStartedAt":"11:15:53"}
{"uuid":"D0D17279-6531-42B9-AB21FFB76DA5D939","now":"11:16:01","requestStartedAt":"11:15:53"}
{"uuid":"D804394F-DCB7-4FE3-BB6AAD86A4865D7D","now":"11:16:02","requestStartedAt":"11:15:53"}
{"complete":true}
{"uuid":"C25CF442-18FB-4BDD-8B55811E6DDCCD13","now":"11:16:06","requestStartedAt":"11:16:06"}

One thing to note is that once the server-side request completes, the browser (Chrome for me) re-polls the server after seemingly 4sec. I could not find anything in the docs about this (or how to set the interval). I've just tested in Edge and Firefox too, and their behaviour is the same as Chrome, except Firefox's interval seemed to be 6sec not 4sec.

That's all I have to say on this. Pleased to know about it, and pleased to be able to answer that Stack Overflow question now.

Righto.

--
Adam

Thursday, 11 August 2022

Lucee: Creating a log file programmatically

G'day:

This will be super short as there's not much to say, I just want to note the code down for posterity, and to cross-reference elsewhere.

We need to create some log files in our application. I knew in ColdFusion one can just create a file ad-hoc, thus:

writeLog(file="anythingYouLike", text="some log message")

However the Lucee docs for writeLog were claiming that the file option was not implemented, and was also deprecated (this has since been rectified in the docs). It actually is implemented, but I was not so sure about the deprecation status. I asked and got no straight answer really. Anyway, the docs now say it's all good, so that's something.

Whilst this works, the log file is not managed by Lucee: it doesn't show up in Lucee Admin, so one cannot set things like log level / file size / file retention etc. This is fine for some situation, but was not fine for us. We need to be able to control the log level.

I had a sniff around, and found org.lucee.cfml.Administrator, and looking some more, found Configure Lucee within your application. From that, I concocted this proof of concept:

local.admin = new Administrator("web", server.system.environment.ADMINPASSWORD)
admin.updateLogSettings(
    name = "AdamTest",
    level = "ERROR",
    appenderClass = "lucee.commons.io.log.log4j2.appender.ResourceAppender",
    layoutClass = "lucee.commons.io.log.log4j2.layout.ClassicLayout",
    appenderArgs = {
        maxfiles = 20,
        maxfilesize = 1073741824
    }
)
writeLog(log="AdamTest", type="ERROR", text="ERROR SHOULD BE LOGGED")
writeLog(log="AdamTest", type="INFO", text="INFO SHOULD NOT BE LOGGED")

And - hurrah - this does exactly what it looks like it does: all the settings are set correctly and respected. Most importantly for us is that the log level works: in this case logging an ERROR works, but any logs at level INFO are not logged. We have some debugging stuff we want to leave log entries in the codebase, but in general set the log to be ERROR so they don't log until we need to do some debugging.

I had to guess at the appenderArgs key values, but on a whim I assumed they'd be the same as the form fields in Lucee admin, and they kinda were:

<input name="custom_3_appender_maxfiles" type="text" class="large" value="0">
<!-- … -->
<input name="custom_3_appender_maxfilesize" type="text" class="large" value="16">

It also shows up in Admin correctly:

Perfect.

And that's all I have to say on this.

Righto.

--
Adam