Wednesday, 9 December 2020

PHPUnit: get code coverage reporting working on PHP 8 / PHPUnit 9.5

 G'day:

I'm still beavering away getting a PHP dev environment working via Docker. As I mentioned in the previous article, I'm writing an article about how I'm doing that as I go, but I've had a few issues today that have made me pause that.

My current issue has been getting PHPUnit working properly. This has nothing to do with the Docker environment (as far as I can tell), it's just... me and PHPUnit not playing nicely together. The solution to my issue was not immediately apparent to me... it's taken me about three hours to sort this out. So I figured it might be worthwhile writing down.

I have a barebones "G'day World" website running in a coupla Docker containers (one for Nginx, one for PHP). That's all working fine, I can get "G'day world" being delivered to the browser via both HTML and PHP requests. Before I start writing any really-really code, I decided I better get PHPUnit working. TDD 'n' all that. I will admit I felt dirty yesterday when I wrote the gdayWorld.html and gdayWorld.php and ran them without tests; I just browsed to them and went "seems legit". Not the way I like to operate.

I sat down to write some tests. My gdayWorld.php is... basic:

<?php

$message = "G'day World";
echo $message;

I don't have any framework or anything installed as yet, so that's as good as it gets. That is in the web root. To test that I'd be needing to test that by curling it and checking the response, then I remembered Symfony has a WebTestCase for this sort of thing, then I decided I could not be arsed looking into that right now. All I need is a test to run to check PHPUnit is running. So... erm... here we go:

    /** @coversNothing */
    public function testNothing()
    {
        $this->assertTrue(false);
    }

This is enough to test PHPUnit. I'll write a better test once I know everything is working.

root@5fdd4abf5d20:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

F                                 1 / 1 (100%)

Time: 00:00.166, Memory: 6.00 MB

There was 1 failure:

1) adamCameron\fullStackExercise\test\functional\public\WebServerTest::testNothing
Failed asserting that false is true.

/usr/share/fullstackExercise/test/functional/public/WebServerTest.php:12

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
root@5fdd4abf5d20:/usr/share/fullstackExercise#

Perfect. Now I can make it pass ;-)

$this->assertTrue(true);
root@5fdd4abf5d20:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

.                                 1 / 1 (100%)

Time: 00:00.127, Memory: 6.00 MB

OK (1 test, 1 assertion)
root@5fdd4abf5d20:/usr/share/fullstackExercise#

So far, so good. My next step whilst I'm configuring stuff on PHPUnit, I decided to get the code coverage working too. I just added the code-coverage reporting bit to phpunit.xml and re-ran the tests:

root@eb28587ca66f:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

Warning: No code coverage driver available

.                                 1 / 1 (100%)

Time: 00:00.210, Memory: 6.00 MB

OK (1 test, 1 assertion)
root@eb28587ca66f:/usr/share/fullstackExercise#

Doh! Forgot about that. I need to install Xdebug.

This was way more horsing around than it could have been, but this is down to the vagaries of the Docker set-up I was using. Once I decided to change direction it was a one-liner in me Dockerfile, and all good.

root@37d06fcf9209:/usr/share/fullstackExercise# php -v
PHP 8.0.0 (cli) (built: Dec 1 2020 03:33:03) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.0-dev, Copyright (c) Zend Technologies
with Xdebug v3.0.1, Copyright (c) 2002-2020, by Derick Rethans
root@37d06fcf9209:/usr/share/fullstackExercise#

Run the tests again...

root@37d06fcf9209:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

Warning: XDEBUG_MODE=coverage or xdebug.mode=coverage has to be set

.                                 1 / 1 (100%)

Time: 00:00.220, Memory: 6.00 MB

OK (1 test, 1 assertion)
root@37d06fcf9209:/usr/share/fullstackExercise#

OK. Fine. Makes sense I s'pose. I added in the ini setting into my phpunit.xml:

<php>
  <ini name="xdebug.mode" value="coverage" />
</php>

No difference. I figured the ini setting is probably changed too late in the piece, as the xdebug.mode will need to be set before PHP starts running any code. And perhaps the environment variable approach would work such that the actual environment variable was set before hand. As it happens it ain't - all that happens is the value is added to the $_ENV array, which is hardly the same thing. From the docs:



However this step is still a relevant one cos we see a behavioural change here:

root@37d06fcf9209:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
Segmentation fault
root@37d06fcf9209:/usr/share/fullstackExercise#

Splat!

I'm gonna spare you a coupla hours of looking in to how to troubleshoot segmentation faults here. Nothing I read helped, and there's nothing useful relating to getting them with PHPUnit, other than a bunch of people getting these segmentation faults.

I started to wonder if it was an issue with Xdebug and PHP8, given PHP8 is so new and sometimes in the last I've noted that Xdebug has needed updating when there's a point release of PHP. But looking into this (at Xdebug: Supported Versions and Compatibility), the current version of Xdebug has been around for a while, and they say it's good to go with PHP8:

I was now thinking about versions of stuff, so I decided to rollback PHP to 7.4, and PHPUnit to 8.5. I realise I should have only done one at a time, but it did not occur to me when doing this that when troubleshooting things, only change one thing per test iteration.

root@41f51888adff:/usr/share/fullstackExercise# php -v
PHP 7.4.13 (cli) (built: Dec 1 2020 04:25:48) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
with Xdebug v3.0.1, Copyright (c) 2002-2020, by Derick Rethans
root@41f51888adff:/usr/share/fullstackExercise# vendor/bin/phpunit --version
PHPUnit 8.5.13 by Sebastian Bergmann and contributors.

root@41f51888adff:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 8.5.13 by Sebastian Bergmann and contributors.

.Code coverage needs to be enabled in php.ini by setting 'xdebug.mode' to 'coverage'
root@41f51888adff:/usr/share/fullstackExercise#

This verifies we are back to PHP 7.4 and PHPUnit 8.5. And we don't have the segmentation fault any more, but... now PHP is ignoring both of the settings in phpunit.xml. I have this in place:

<php>
  <env name="XDEBUG_MODE" value="coverage"/>
  <ini name="xdebug.mode" value="coverage"/>
</php>

At this point I read up on the <ini> and <env> settings in phpunit.xml, and confirmed both of them are set too late for Xdebug to read them anyhow. Quite odd the way PHPUNit 9.5 reacts to the XDEBUG_MODE being set in that case. Anyway, I need to set them before PHP is run, so I set a really-really environment variable in my Dockerfile:

ENV XDEBUG_MODE=coverage

After rebuilding my containers:

root@6053a26dc3eb:/usr/share/fullstackExercise# echo $XDEBUG_MODE
coverage
root@6053a26dc3eb:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 8.5.13 by Sebastian Bergmann and contributors.

..                                 2 / 2 (100%)

Time: 1.01 seconds, Memory: 6.00 MB

OK (2 tests, 2 assertions)

Generating code coverage report in HTML format ... done [596 ms]
root@6053a26dc3eb:/usr/share/fullstackExercise#

Hurrah! Let's have a look at the report:

This is correct: I'm not testing that code in /public yet.

I also quickly tried to use the xdebug.mode ini file setting. I dropped a /usr/local/etc/php/conf.d/phpunit-code-coverage-xdebug.ini with just xdebug.mode=coverage in it into my container's file system:

root@f3cd4a0715a5:/usr/share/fullstackExercise# cat /usr/local/etc/php/conf.d/phpunit-code-coverage-xdebug.ini
xdebug.mode=coverage
root@f3cd4a0715a5:/usr/share/fullstackExercise# php --ini
Configuration File (php.ini) Path: /usr/local/etc/php
Loaded Configuration File: (none)
Scan for additional .ini files in: /usr/local/etc/php/conf.d
Additional .ini files parsed: /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini,
/usr/local/etc/php/conf.d/docker-php-ext-sodium.ini,
/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini,
/usr/local/etc/php/conf.d/phpunit-code-coverage-xdebug.ini

root@f3cd4a0715a5:/usr/share/fullstackExercise# php -i | grep xdebug.mode
xdebug.mode => coverage => coverage
root@f3cd4a0715a5:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 8.5.13 by Sebastian Bergmann and contributors.

..                                 2 / 2 (100%)

Time: 1.07 seconds, Memory: 6.00 MB

OK (2 tests, 2 assertions)

Generating code coverage report in HTML format ... done [869 ms]
root@f3cd4a0715a5:/usr/share/fullstackExercise#

Perfect.

My supposition here is that the issue all along was that I needed to set one of either the ini file option, or the environment variable option before PHPUnit runs. PHPUnit 8 correctly sez "OI!" if I don't; whereas PHPUnit 9 doesn't check correctly, and instead messes things up. Let's roll forward to PHP8 and PHPUnit 9.5 again and see:

root@f7d29f40ad62:/usr/share/fullstackExercise# php -v
PHP 8.0.0 (cli) (built: Dec 1 2020 03:33:03) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.0-dev, Copyright (c) Zend Technologies
with Xdebug v3.0.1, Copyright (c) 2002-2020, by Derick Rethans
root@f7d29f40ad62:/usr/share/fullstackExercise# vendor/bin/phpunit --version
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.

root@f7d29f40ad62:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
..                                 2 / 2 (100%)

Time: 00:01.535, Memory: 10.00 MB

OK (2 tests, 2 assertions)

Generating code coverage report in HTML format ... done [00:00.605]
root@f7d29f40ad62:/usr/share/fullstackExercise#

Hoo-frickin-ray!!

So that supposition was correct then.

I guess I could have saved myself a bunch of time here by just setting the .ini setting or the environment variable in the correct environment right from the outset. Then again had PHPUnit 9 not changed its warnings, I would have known that straight away: PHPUnit 8 was pretty clear on this. The problem and the solution here were pretty mundane, but I learned a lot about how to do things with Docker (which will all go into the other article I'm writing at the same time as this one), some stuff about Xdebug, some stuff about PHP and some stuff about PHPUnit. So it was a worthwhile afternoon of scratching my head. I'm also pleased I've sat here and done 8hrs of "work" for the second day running. Hopefully this will stick and I'll work my way out of my unemployed malaise before long. We'll see.

PHPUnit 9.5 on PHP 7.4

As I said earlier, I should not have changed two things at once when testing this, as it makes it harder to draw accurate conclusions. I omitted to say I did then go back and test PHPUnit 9.5 on PHP 7.4, so as to check whether the issue was due to PHPUnit 9.5, or whether it was some combination of that and PHP version. Here are the test results when running PHPUnit 9.5 on PHP 7.4 with XDEBUG_MODE set in phpunit.xml and not in the environment. xdebug.mode is not set anywhere:

root@81c63ae50626:/usr/share/fullstackExercise# php -v
PHP 7.4.13 (cli) (built: Dec 1 2020 04:25:48) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
with Xdebug v3.0.1, Copyright (c) 2002-2020, by Derick Rethans
root@81c63ae50626:/usr/share/fullstackExercise# vendor/bin/phpunit --version
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
root@81c63ae50626:/usr/share/fullstackExercise# vendor/bin/phpunit
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
Segmentation fault
root@81c63ae50626:/usr/share/fullstackExercise#

To me this demonstrates the issue is with PHPUnit 9.5 and nothing else.

Anyway... now for a beer.

Righto.


--
Adam

Docker: resolving "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?" issue

G'day:

Firstly some framing. This morning I sat down to do some more Docker stuff, and started to get this issue again:

adam@DESKTOP-QV1A45U:/mnt/c/src/fullstackExercise/docker$ docker-compose down
Traceback (most recent call last):
File "bin/docker-compose", line 3, in <module>
File "compose/cli/main.py", line 67, in main
File "compose/cli/main.py", line 123, in perform_command
File "compose/cli/command.py", line 69, in project_from_options
File "compose/cli/command.py", line 125, in get_project
File "compose/cli/command.py", line 184, in get_project_name
File "posixpath.py", line 383, in abspath
FileNotFoundError: [Errno 2] No such file or directory
[2052] Failed to execute script docker-compose
adam@DESKTOP-QV1A45U:/mnt/c/src/fullstackExercise/docker$

(I say "again", cos I have been getting this a lot whilst working on another article that's taken me a coupla days to write so far, and won't be done until tomorrow. Note: this issue is not the one I am looking at in this article… I'm looking at that "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?" issue.)

OK, so enough context. Back to "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?".

From a lot of googling, most suggestions start with a de-install/re-install Docker Desktop, so decided to try that first. It's important to note that a lot of people had tried this, it had worked for a while, but then it started again. I think it's addressing a symptom not the problem, but I might as well be on the most recent version of Docker Desktop anyhow.

After doing the upgrade I happened to have PhpStorm open, and I absent-mindedly clicked the "refresh" button on the config for the connection to Docker I had. It errored out saying "got no PHP container mate", which was true because the de-install of Docker Desktop got rid of the containers I had. To verify this, I hit the shell:

adam@DESKTOP-QV1A45U:~$ docker container ls
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
adam@DESKTOP-QV1A45U:~$

Erm… OK, that's new. now I had been messing with the Docker daemon settings in Docker Desktop trying to get PhpStorm connecting, so went back in there and re-dis-enabled(!) Docker daemon with no TLS support. I mean this setting:

PhpStorm would stop connecting, but in the mean time I could get my containers up and running. I hit the shell again:

adam@DESKTOP-QV1A45U:~$ docker container ls
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
adam@DESKTOP-QV1A45U:~$

Bugger.

adam@DESKTOP-QV1A45U:~$ docker ps
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
adam@DESKTOP-QV1A45U:~$

[I said more than bugger this time]

Great. In troubleshooting one issue, I have caused another one. Excellent. Thanks Docker. Thanks me.

After a bunch of googling and restarting randomly and shaking my head at everyone's solution for this issue which seemed to largely be "switch it off and switch it back on again", etc, I landed on someone commenting "weird thing is though… it's all still fine in Powershell. It's just broke in WS". I checked:

PS C:\Users\camer> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
PS C:\Users\camer>

Interesting. Something to do with WSL then. Then I remembered a setting I thought seemed odd in Docker Desktop when I gave it the once-over after re-install:

I did not think much of this at the time, because Ubuntu is my default WSL distro, so I figured that was fine. After all… everything had been working fine like that. On a whim, I decided to switch those two toggles back on though:

And now back to bash:

adam@DESKTOP-QV1A45U:/mnt/c/src/fullstackExercise/docker$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES adam@DESKTOP-QV1A45U:/mnt/c/src/fullstackExercise/docker$

Hurrah. So basically when I de-/re-installed Docker Desktop, it did not maintain its existing settings, and defaulted those two to be off? Weird, but I'm guessing so.

Anyway, no answer I saw via my googlings had this particular solution, so I figrued I'd write it up. This might be obvious to everyone who already knows, but there's enough people out there who clearly don't already know for it to possibly be useful. Perhaps. I'm also not suggesting it's the solution to everyone getting this "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?" issue, but it's something to check. Ah anyway it's a far easier and quicker article to write than the one I'd been working on. Quick win 'n' all. I'll have the longer article done tomorrow I hope.

Righto.

--
Adam

Tuesday, 1 December 2020

Getting Docker, Clojure and VSCode all playing nice (well... almost...)

G'day:
I'm trying to re-motivate myself to do some tech type stuff in my spare time. I have a lot of spare time at the moment. 24h of the day. I still haven't really looked for a job having been made redundant a few months back (coughJunecough), and I've been largely wasting my time sleeping, napping (yes, two distinct things), and playing computer games. On one hand 2020 is a bit of a rubbish year, and a new work environment is likely to be a subpar experience, even if I can find work. On the other hand I can afford to not work for quite a while thanks to my redundancy pay out, but on still another hand (yes, fine, I have three hands for the purposes of this story), I really am not taking advantage of this spare time I have. That's why I had a look at Mingo's CF problem the other day ("looking at an issue Mingo had with ehcache and cachePut and cacheGet"). Just trying to do anything marginally "productive".

I've been wanting to learn a bit of Clojure for a while now. Not for any particular reason, and I'm not looking to shift to doing that over PHP, but just cos it's a completely different language from anything I've used before - which have all been C-ish "curly brace" type things - and my mate Sean has banged on about how good it is. I think at his suggestion I bought Living Clojure by Carin Meier a while back when I had some down time in NZ. I read the first few pages and then realised Auckland has a lot of pubs, so did that instead. This would have been about four(?) years ago. I ain't touched it since.

Here I am, sporadically trying to get to grips with Docker, and this seemed like a good mini-project... get Clojure up and running within a container, set up a project, integrate it with an IDE, and get to the point where I have a running (and passing) test for some sort of "G'day world" function. In the past I would install whatever language I was wanting to mess with natively on my PC, but I decided running it in a container is the way to go.

Note that this is pretty much just a record of what I needed to do to get to the point I could write the code and run the tests. I have very little experience with Docker, and hardly any experience with Clojure. What this means is that this is def a newbie account of what I did, and I really would not take it as instructions of how to do stuff. All I will say is that I needed to do a whole bunch of googling to work shit out, and I will summarise the results of the googling here. This is more an exercise for me to verify what I've done actually works, as having done it once... I'm now getting-rid and starting from scratch.

OK. I am starting with a minimal "empty" repo, living-clojure:

adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$ tree -aF --dirsfirst -L 1 .
.
├── .git/
├── .gitignore*
├── LICENSE*
└── README.md*

1 directory, 3 files
adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$

This just gives me a directory I can mount in the container to expose my code in there.

Now I need the container. Looking at dockerhub/clojure, the options there are just for running the app, not doing development, so ain't much help. I was hoping to be able to copy and paste something like "a container with Clojure and Leiningen in it wot you can as a shell for building yer project, testing it, developing it, and running it". Nothing to copy and paste and run, so I'm gonna have to work out what to do. Remember I'm a complete noob with Docker.

Getting the image down was easy enough:

adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$ docker pull clojure
Using default tag: latest
latest: Pulling from library/clojure
852e50cd189d: Already exists
ef17c1a94464: Pull complete
477589359411: Pull complete
0a40767d8190: Downloading [==================> ] 71.81MB/197.1MB
434967525bea: Download complete
c89e9081a4db: Download complete
b7cd84bcb910: Download complete
214999e8c5eb: Download complete
0bdd8adb02ca: Download complete

(Just showing it mid-pull there)

adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
php rc-cli 657d0925a963 6 days ago 407MB
clojure latest 846a444b258f 6 days ago 586MB
[etc]

Just to start with, I just need a container based on the Clojure image, but doing nothing else. I just need to run a shell with it so I can create my Clojure project. I came up with this:

adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$ docker create --name living-clojure --mount type=bind,source=/mnt/c/src/living-clojure,target=/usr/src/living-clojure --workdir /usr/src/living-clojure --interactive --tty clojure /bin/bash
a1be067ee435adb6851ac06b53bb97545f38dabced73a19255f1e273b0337d1e
adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$
  • I'm mounting my host's project directory within the container;
  • and setting that to be my working dir in there too;
  • I don't completely get these two, but I wanna be able to type shit into my shell that I'm running, and this seems to make it happen;
  • and it's bash I want to interact with.

And I start this:

adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$ docker start --interactive living-clojure
root@a1be067ee435:/usr/src/living-clojure#

Hurrah! This seems promising: I'm in a different shell, and I'm in the working directory I specified when doing the docker create before.

Let's see if I can create a new Clojure project (which I'm lifting from the the Leiningen docs):

root@a1be067ee435:/usr/src/living-clojure# cd ..
root@a1be067ee435:/usr/src# lein new app living-clojure --force
Generating a project called living-clojure based on the 'app' template.
root@a1be067ee435:/usr/src#

I need to go up a directory level and to use --force because I already have the project directory in place, and lein new assumes one doesn't.

And this all seems to work fine (well… in that it's done "stuff"):

root@a1be067ee435:/usr/src# tree -F -a --dirsfirst -L 1 living-clojure/
living-clojure/
├── .git/
├── doc/
├── resources/
├── src/
├── test/
├── .gitignore*
├── .hgignore*
├── CHANGELOG.md*
├── LICENSE*
├── README.md*
└── project.clj*

5 directories, 6 files
root@a1be067ee435:/usr/src#

git status shows something interesting though:

adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .gitignore
modified: LICENSE
modified: README.md

Untracked files:
(use "git add <file>..." to include in what will be committed)
CHANGELOG.md
doc/
project.clj
src/
test/

no changes added to commit (use "git add" and/or "git commit -a")
adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$

Note how .gitignore, LICENSE and README.md have all changed. Such is the price of doing that --force I guess. I'm gonna revert the LICENSE and README.md, but leave the .gitignore; I presume the Leiningen bods have a better idea of what git should ignore in the context of a Clojure project than GitHub's boilerplate one does.

Next… what's the code in the default project actually do?

(ns living-clojure.core
(:gen-class))

(defn -main
"I don't do a whole lot ... yet."
[& args]
(println "Hello, World!"))

That will do for a start. Let's see if it runs:

root@a1be067ee435:/usr/src# cd living-clojure/
root@a1be067ee435:/usr/src/living-clojure# lein run
Hello, World!
root@a1be067ee435:/usr/src/living-clojure#

Cool! Is it bad I'm so pleased with a computer saying "Hello, World!"? Ha.

I'm being a bit naughty because I'm running the code before I've tested it. But I figure... not my code, so it's OK. Anyway, there is a test, although it does not test the code in the app. Sigh. But anyhow:

root@a1be067ee435:/usr/src/living-clojure# cat test/living_clojure/core_test.clj
(ns living-clojure.core-test
(:require [clojure.test :refer :all]
[living-clojure.core :refer :all]))

(deftest a-test
(testing "FIXME, I fail."
(is (= 0 1))))
root@a1be067ee435:/usr/src/living-clojure# lein test

lein test living-clojure.core-test

lein test :only living-clojure.core-test/a-test

FAIL in (a-test) (core_test.clj:7)
FIXME, I fail.
expected: (= 0 1)
actual: (not (= 0 1))

Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
Tests failed.
root@a1be067ee435:/usr/src/living-clojure#

I guess this is in-keeping with "start with a failing test". Fair enough then. At least the test runs correctly, which is good.

The last bit of checking the baseline project is… will it compile and run as a jar?

root@a1be067ee435:/usr/src/living-clojure# lein uberjar
Compiling living-clojure.core
Created /usr/src/living-clojure/target/uberjar/living-clojure-0.1.0-SNAPSHOT.jar
Created /usr/src/living-clojure/target/uberjar/living-clojure-0.1.0-SNAPSHOT-standalone.jar
root@a1be067ee435:/usr/src/living-clojure# java -jar target/uberjar/living-clojure-0.1.0-SNAPSHOT-standalone.jar
Hello, World!
root@a1be067ee435:/usr/src/living-clojure#

Perfect.

Right. Now I actually need to write some code. I wanna change that main function to read the first argument from the command-line - if present - and say "G'day [value]" (eg: "G'day Zachary"), or if there's no argument, then stick with just "G'day World".

Given that's all very simple, I could just do it with a text editor, but I'll hook an IDE up to this lot too. I googled around and found that VSCode and the Calva Clojure plug-in. I've already set all that up and can't be arsed redoing it for the purposes of this article. One issue I had… and still have… is that Calva is supposed to be able to connect to an external REPL, instead of using its own one. Whilst experimenting trying to get this working, I tweaked my docker create statement to run a headless REPL instead of bash:

docker create --name living-clojure --mount type=bind,source=/mnt/c/src/living-clojure,target=/usr/src/living-clojure --expose 56505 -p 56505:56505 --workdir /usr/src/living-clojure --interactive --tty clojure lein repl :headless :host 0.0.0.0 :port 56505

The new stuff is as follows:

  • I probably don't need the --expose here for what I'm doing, but the -p exposes the container's port 56505 to the host PC (56505 has no significance… it was just one of the ports a previous REPL had used, so I copied that).
  • I start a headless REPL (so basically a REPL as a service, that just listens for REPL connections) that listens on that port 56505
  • I have to use 0.0.0.0 here because when I tried to use 127.0.0.1, that worked fine from within the container, but was no good on the host machine. It took me a bit of googling to crack that one.

I start this one without being interactive:

adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$ docker start living-clojure
living-clojure
adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$

Doing it this way I am exposing a REPL to other sessions, whilst still being able to bash my way along too:

adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$ docker exec --interactive --tty living-clojure lein repl :connect localhost:56505
Connecting to nREPL at localhost:56505
REPL-y 0.4.4, nREPL 0.6.0
Clojure 1.10.1
OpenJDK 64-Bit Server VM 11.0.9.1+1
Docs: (doc function-name-here)
(find-doc "part-of-name-here")
Source: (source function-name-here)
Javadoc: (javadoc java-object-or-class-here)
Exit: Control+D or (exit) or (quit)
Results: Stored in vars *1, *2, *3, an exception in *e

living-clojure.core=> (println "G'day world")
G'day world
nil
living-clojure.core=> quit
Bye for now!
adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$ docker exec --interactive --tty living-clojure /bin/bash
root@222940f1eafd:/usr/src/living-clojure# ls
CHANGELOG.md LICENSE README.md doc project.clj resources src target test
root@222940f1eafd:/usr/src/living-clojure# exit
exit
adam@DESKTOP-QV1A45U:/mnt/c/src/living-clojure$

I could get VSCode on my desktop PC to see this REPL:


But I could not get it to do stuff like running tests, which it was able to do using the built-in REPL. So that sucked. I googled a bit and I am not the only person with this problem when using external REPLs, so I have just decided to give up on that. I'll cheat and use the built-in REPL.

Oh god. I now need to write some Clojure code (I've now caught up with my previous progress, and am typing this article "live" now… Bear with me for a bit).

Time passes…

More time passes and there is swearing…

OK, got there. I abandoned using VSCode and Calva because Calva seems to have this strange idea that when I go to delete things (like extra or misplaced parentheses), that it knows better and doesn't just do what it's frickin' told. It's probably me not knowing some special trick that people accustomed to using it know, but… for the love of god if I ask a frickin' text editor to do something in the edit window, I expect it to just do it even if I'm asking it to do something less than ideal. I will seek out a different plugin later. Once I dropped back to using Notepad++, things went better. I have done a lot of googling in the last two hours though. Sigh.

Anyhow, here are my tests:

(ns living-clojure.core-test
  (:require [clojure.test :refer :all]
    [living-clojure.core :refer :all]))

(deftest test-greet-default-behaviour
  (testing "it should return G'day world if no name is passed"
    (is (= "G'day World" (greet [])))))

(deftest test-greet-by-single-name
  (testing "it should return G'day [name] if just that name is passed"
  (is (= "G'day Zachary" (greet ["Zachary"])))))

(deftest test-greet-by-multiple-name
  (testing "it should return G'day [name] if more than one names are passed"
  (is (= "G'day Zachary" (greet ["Zachary" "Cameron" "Lynch"])))))

And here is the code:

(ns living-clojure.core
  (:gen-class))

(defn greet
  ([args]
    (str "G'day " (or (first args) "World"))))
  
(defn -main
  "I greet someone or everyone"
  [& args]
  (println (greet args)))

And here's the output of the tests, running the code, and compiling and running the code:

root@222940f1eafd:/usr/src/living-clojure# lein test

lein test living-clojure.core-test

Ran 3 tests containing 3 assertions.
0 failures, 0 errors.


root@222940f1eafd:/usr/src/living-clojure# lein run
G'day World


root@222940f1eafd:/usr/src/living-clojure# lein run Zachary
G'day Zachary


root@222940f1eafd:/usr/src/living-clojure# lein run Zachary Cameron Lynch
G'day Zachary


root@222940f1eafd:/usr/src/living-clojure# lein uberjar
Compiling living-clojure.core
Created /usr/src/living-clojure/target/uberjar/living-clojure-0.1.0-SNAPSHOT.jar
Created /usr/src/living-clojure/target/uberjar/living-clojure-0.1.0-SNAPSHOT-standalone.jar


root@222940f1eafd:/usr/src/living-clojure# java -jar target/uberjar/living-clojure-0.1.0-SNAPSHOT-standalone.jar
G'day World


root@222940f1eafd:/usr/src/living-clojure# java -jar target/uberjar/living-clojure-0.1.0-SNAPSHOT-standalone.jar Zachary
G'day Zachary


root@222940f1eafd:/usr/src/living-clojure# java -jar target/uberjar/living-clojure-0.1.0-SNAPSHOT-standalone.jar Zachary Cameron Lynch
G'day Zachary


root@222940f1eafd:/usr/src/living-clojure#

I got a bit snagged on the variadic parameters syntax for a while, largely cos to me that just says "it's a reference", so I need to unlearn that in this context. And then cos I've been staring at the screen too long now, I had a brain-disconnect on only the parameters of main were variadic… by the time they got to greet it was just an array (or a list or whatever Clojure calls it). So due to dumbarsey on my part I variously had the tests passing but the behaviour at runtime slightly off, or the tests failing but the program actually running correctly. What this says is that I probably should have been testing main, not greet. My bad.

OK. It's 22:30 and I've been at this on and off for quite a few hours now. I'm pretty pleased with the exercise as a whole - I learned a lot - but now I want to go shoot things. Parentheses. I want to shoot parentheses. But I guess I will settle for shooting raiders in Fall Out 4.

Righto.

--
Adam

Friday, 23 October 2020

Quickly running ColdFusion via Docker

G'day:
Firstly, don't take this as some sort of informed tutorial as to how things should be done. I am completely new to Docker, and don't yet know my arse from my elbow with it. Today I decided to look at an issue my mate was having with ColdFusion (see "ColdFusion: looking at an issue Mingo had with ehcache and cachePut and cacheGet"). These days I do not have CF installed anywhere, so to look into this issue, I needed to run it somehow. I was not gonna do an actual full install just for this. Also I've been trying - slowly -  to get up to speed with Docker, and this seemed to be a sitter for doing something newish with it. Also note that this is not the sort of issue I could just use trycf.com to run some code, as it required looking at some server config.

All I'm gonna do here is document what I had to do today to solve the problem in that other article.

I am running on Windows 10 Pro, I have WSL installed, and I'm running an Ubuntu distribution, and using bash within that. I could use powershell, but I so seldom use it, I'm actually slightly more comfortable with bash. For general Windows command-line stuff I usually just use the default shell, and it didn't even occur to me until just now to check if the docker CLI actually works in that shell, given all the docs I have read use either Powershell or some *nix shell. I just checked and seems it does work in the default shell too. Ha. Ah well, I'm gonna stick with bash for this.

I've also already got Docker Desktop & CLI installed and are running. I've worked through the Getting Started material on the Docker website, and that's really about it, other than a coupla additional experiments.

Right. I'd been made aware that Adobe maintain some Docker images for ColdFusion, but no idea where they are or that sort of jazz, so just googled it ("docker coldfusion image"). The first match takes me to "Docker images for ColdFusion", and on there are the exact Docker commands to run CF / code in CF in various ways. What I wanted to do was to run the ColdFusion REPL, and mess around with its config files. The closest option to what I wanted was to run ColdFusions built-in web server to serve a website. I figured it'd use an image with a complete ColdFusion install, so whilst I didn't want or need the web part of it, I could SSH into the container and use the REPL that way. So the option for me was this one:

Run ColdFusion image in daemon mode (that link is just a deep link to the previous one above). That gives the command:

docker run -dt -p 8500:8500 -v c:/wwwroot/:/app \
-e acceptEULA=YES -e password=ColdFusion123 -e enableSecureProfile=true \
eaps-docker-coldfusion.bintray.io/cf/coldfusion:latest

(I've just split that over multiple lines for readability).

Here we're gonna be running the container detached (-d), so once the container runs, control returns to my shell. I'm not at all clear why they're using the pseudo-TTY option here (-t / --tty). From all my reading (basically this Q&A on Stack Overflow: "Confused about Docker -t option to Allocate a pseudo-TTY"), it's more for when not in detached mode?

We're also exposing (publishing) the container's port 8500 to the host. IE: when I make HTTP requests to port 8500 in my PC's browser, it will be routed to the container (and CF's inbuilt web server in the container will be listening).

Lastly on that line we're mounting C:\wwwroot as a volume (-v) in the container as /app. IE: files in my PC's C:\wwwroot dir will be availed within the container in the /app dir.

The second line is just a bunch of environment variables (-e) that get set in the container... the ColdFusion installation process are expecting these I guess.

And the last line is the address of where the CF image is.

I'm not going to use exactly this command actually. I'm on Ubuntu so I don't have a C:\wwwroot, and I don't even care what's in the web root for this exercise (I'm just wanting the REPL, remember), so I'm just going to point it to a temp directory in my home dir (~/tmp). Also I see no reason to enable the secure profile. That's just a pain in the arse. Although it would probably not matter one way or the other.

Also I want to give my container a name (--name) for easy reference.

So here's what I've got:

docker run --name cf -dt -p 8500:8500 -v ~/tmp:/app \
-e acceptEULA=YES -e password=ColdFusion123 -e enableSecureProfile=false \
eaps-docker-coldfusion.bintray.io/cf/coldfusion:latest

Let's run that:

adam@DESKTOP-QV1A45U:~$ docker run --name cf -dt -p 8500:8500 -v ~/tmp:/app \
> -e acceptEULA=YES -e password=ColdFusion123 -e enableSecureProfile=false \
> eaps-docker-coldfusion.bintray.io/cf/coldfusion:latest
Unable to find image 'eaps-docker-coldfusion.bintray.io/cf/coldfusion:latest' locally
latest: Pulling from cf/coldfusion
b234f539f7a1: Pull complete
55172d420b43: Pull complete
5ba5bbeb6b91: Pull complete
43ae2841ad7a: Pull complete
f6c9c6de4190: Pull complete
b3121a6492e2: Pull complete
cf911c0e9bab: Pull complete
ef2cca3cfd53: Pull complete
f7a96a442bce: Pull complete
5f2632eaddf0: Pull complete
f4563a5d4acb: Pull complete
ad1e188590ee: Pull complete
Digest: sha256:12e7dc19bb642a0f67dba9a6d0a2e93f805de0a968c6f92fdcb7c9c418e1f1e8
Status: Downloaded newer image for eaps-docker-coldfusion.bintray.io/cf/coldfusion:latest
c7b703635a5358f0ea91c3f2549d8ec4a308812c44b2476bd6ad4c00b2e32c12
adam@DESKTOP-QV1A45U:~$

This takes a frew minutes to run cos the ColdFusion image is 570MB and it also downloads a bunch of other stuff. As it goes, you see progress like this:

...
ef2cca3cfd53: Pull complete
f7a96a442bce: Downloading [=================================================> ]  560.9MB/570.4MB
f7a96a442bce: Pull complete
...

In theory ColdFusion should now be installed and listening on http://localhost:8500, and...


Cool. I've had some issues running the REPL before if the CFAdmin installation step hasn't been run at least once, so I let that go (the password is the one from the environment variable, yeah? ColdFusion123).

I can see the image and the container in docker now too:

adam@DESKTOP-QV1A45U:~$
adam@DESKTOP-QV1A45U:~$ docker image ls
REPOSITORY                                        TAG                 IMAGE ID            CREATED             SIZE
eaps-docker-coldfusion.bintray.io/cf/coldfusion   latest              31a6d984cf30        2 months ago        1.03GB
adam@DESKTOP-QV1A45U:~$
adam@DESKTOP-QV1A45U:~$
adam@DESKTOP-QV1A45U:~$ docker container ls
CONTAINER ID        IMAGE                                                    COMMAND                  CREATED             STATUS                    PORTS                                         NAMES
c7b703635a53        eaps-docker-coldfusion.bintray.io/cf/coldfusion:latest   "sh /opt/startup/sta…"   10 minutes ago      Up 10 minutes (healthy)   8016/tcp, 45564/tcp, 0.0.0.0:8500->8500/tcp   cf
adam@DESKTOP-QV1A45U:~$
adam@DESKTOP-QV1A45U:~$

Next we need to SSH into the container.

Tip from Pete Freitag

In his comment below, Pete points out that given I'm doing all this on the same physical machine, I don't need to use ssh, I can just do this:

docker exec -it cf /bin/bash

And this works fine:

adam@DESKTOP-QV1A45U:~$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
adam@DESKTOP-QV1A45U:~$
adam@DESKTOP-QV1A45U:~$
adam@DESKTOP-QV1A45U:~$ docker start cf
cf
adam@DESKTOP-QV1A45U:~$ docker container ls
CONTAINER ID        IMAGE                                                    COMMAND                  CREATED             STATUS                             PORTS                                         NAMES
c7b703635a53        eaps-docker-coldfusion.bintray.io/cf/coldfusion:latest   "sh /opt/startup/sta…"   3 days ago          Up 13 seconds (health: starting)   8016/tcp, 45564/tcp, 0.0.0.0:8500->8500/tcp   cf
adam@DESKTOP-QV1A45U:~$
adam@DESKTOP-QV1A45U:~$
adam@DESKTOP-QV1A45U:~$ curl -s -o /dev/null -I -w "%{http_code}"  http://localhost:8500/CFIDE/administrator/
200
adam@DESKTOP-QV1A45U:~$
adam@DESKTOP-QV1A45U:~$
adam@DESKTOP-QV1A45U:~$ docker exec -it cf /bin/bash
root@c7b703635a53:/opt#
root@c7b703635a53:/opt#
root@c7b703635a53:/opt# sed -n '484,485'p /opt/coldfusion/cfusion/lib/ehcache.xml
        copyOnWrite="true"
            />
root@c7b703635a53:/opt#
root@c7b703635a53:/opt#
root@c7b703635a53:/opt# exit
exit
adam@DESKTOP-QV1A45U:~$


Here I show:
  • ColdFusion container not running
  • Starting the ColdFusion container
  • Showing the container started
  • Showing it is up and running and responding to requests with a 200
  • Using Pete's docker exec approach
  • Showing the change to the file I needed to make to get the caching to work as expected
  • Exiting the container's bash, back to my own PC's bash

It never occurred to me to try to do something like this, given traditionally I am never physically positioned on the boxes I am working on, so need to use ssh.

Cheers Pete!


For that I'm using some geezer called Jeroen Peters' docker-ssh container. I found this from a bunch of googling, and people saying how not do use SSH from within containers, and instead have the SSH server in a container of its own. Seems legit.

I've modified his docker command slightly to a) point to my cf container, also to detach once it's started:

adam@DESKTOP-QV1A45U:~$
adam@DESKTOP-QV1A45U:~$ docker run --detach -e FILTERS={\"name\":[\"^/cf$\"]} -e AUTH_MECHANISM=noAuth \
--name sshd-web-server1 -p 2222:22  --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
jeroenpeeters/docker-ssh
5447950ce4d4d7ced8ef61696a64b8af9c4c01139c4fde58fa558ef25ba89b58
adam@DESKTOP-QV1A45U:~$
adam@DESKTOP-QV1A45U:~$

I can now SSH into the container:

adam@DESKTOP-QV1A45U:~$ ssh localhost -p 2222

 ###############################################################
 ## Docker SSH ~ Because every container should be accessible ##
 ############################################################### 
 ## container | /cf                                           ##
 ###############################################################

/opt $

I'm in! Cool!

Just to prove to myself further that I am actually "inside" the container, I jumped over to another bash session on my PC, and stuck a new file in that ~/tmp directory:

adam@DESKTOP-QV1A45U:~$ cd tmp
adam@DESKTOP-QV1A45U:~/tmp$ cat > new_file
G'day world
^Z
[2]+  Stopped                 cat > new_file
adam@DESKTOP-QV1A45U:~/tmp$ ll
total 20
drwxr-xr-x 3 adam adam 4096 Oct 23 19:10 ./
drwxr-xr-x 6 adam adam 4096 Oct 23 19:09 ../
drwxr-xr-x 4  999  999 4096 Oct 23 18:25 WEB-INF/
-rwxr-xr-x 1  999 root  370 Oct 23 18:24 crossdomain.xml*
-rw-r--r-- 1 adam adam   12 Oct 23 19:10 new_file
adam@DESKTOP-QV1A45U:~/tmp$
adam@DESKTOP-QV1A45U:~/tmp$ cat new_file
G'day world
adam@DESKTOP-QV1A45U:~/tmp$

And checked it from within the container:

/opt $ cd /app
/app $ # before

/app $ ll
total 16
drwxr-xr-x 3   1000   1000 4096 Oct 23 18:09 ./
drwxr-xr-x 1 root   root   4096 Oct 23 17:24 ../
drwxr-xr-x 4 cfuser cfuser 4096 Oct 23 17:25 WEB-INF/
-rwxr-xr-x 1 cfuser root    370 Oct 23 17:24 crossdomain.xml*
/app $
/app $ # after
/app $ ll
total 20
drwxr-xr-x 3   1000   1000 4096 Oct 23 18:10 ./
drwxr-xr-x 1 root   root   4096 Oct 23 17:24 ../
drwxr-xr-x 4 cfuser cfuser 4096 Oct 23 17:25 WEB-INF/
-rwxr-xr-x 1 cfuser root    370 Oct 23 17:24 crossdomain.xml*
-rw-r--r-- 1   1000   1000   12 Oct 23 18:10 new_file
/app $
/app $ cat new_file

G'day world

/app $

OK so I think I'm happy with that. So... now to run the code...

/app $ cd /opt/coldfusion/cfusion/bin/
/opt/coldfusion/cfusion/bin $ ./cf.sh
ColdFusion started in interactive mode. Type 'q' to quit.
cf-cli>foo = { bar = 1 };
cachePut( 'foobar', foo );

foo.bar = 2;

writeDump( cacheGet( 'foobar' ) );
struct

BAR: 1

cf-cli>
cf-cli>cf-cli>2
cf-cli>cf-cli>struct

BAR: 2

cf-cli>^Z
[1]+  Stopped                 ./cf.sh
/opt/coldfusion/cfusion/bin $

All this is discussed in the previous article (ColdFusion: looking at an issue Mingo had with ehcache and cachePut and cacheGet). This is showing the problem though... the answer should be 1 not 2

I have to fix this cache setting...

/opt/coldfusion/cfusion/bin $ cd ../lib
/opt/coldfusion/cfusion/lib $ ll ehcache.xml
-rwxr-xr-x 1 cfuser bin 25056 Jun 25  2018 ehcache.xml*
/opt/coldfusion/cfusion/lib $ vi ehcache.xml
bash: vi: command not found
/opt/coldfusion/cfusion/lib $

Erm... wat??

Wow OK so this is a really slimmed down container (except the 100s of MB of ColdFusion stuff I mean). I can fix this...

/opt/coldfusion/cfusion/lib $ sudo apt-get update
bash: sudo: command not found
/opt/coldfusion/cfusion/lib $

WAAAAAH. OK this is getting annoying now. On a whim I just tried without sudo

/opt/coldfusion/cfusion/lib $ apt-get update
Get:1 http://archive.ubuntu.com/ubuntu xenial InRelease [247 kB]
[etc]
Fetched 29.4 MB in 10s (2758 kB/s)
Reading package lists... Done
/opt/coldfusion/cfusion/lib $
/opt/coldfusion/cfusion/lib $ apt-get install vi
Reading package lists... Done
Building dependency tree
Reading state information... Done
E: Unable to locate package vi
/opt/coldfusion/cfusion/lib $

Fine. One step forward, one step sideways. Grumble. Another whim:

/opt/coldfusion/cfusion/lib $ apt-get install vim
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
[heaps of stuff]
/opt/coldfusion/cfusion/lib $

Ooh! Progress. But... does it work? (hard to get proof of this as vi takes over the shell, but... yes it worked).

I now just have to restart the CF container, and test the fix:

/opt/coldfusion/cfusion/lib $ exit
exit
There are stopped jobs.
/opt/coldfusion/cfusion/lib $ exit
exit
Connection to localhost closed.
adam@DESKTOP-QV1A45U:~$ docker stop cf
cf
adam@DESKTOP-QV1A45U:~$ docker start cf
cf
adam@DESKTOP-QV1A45U:~$ ssh localhost -p 2222
 ###############################################################
 ## Docker SSH ~ Because every container should be accessible ##
 ###############################################################
 ## container | /cf                                           ##
 ###############################################################

/opt $ cd coldfusion/cfusion/bin/
/opt/coldfusion/cfusion/bin $ ./cf.sh
ColdFusion started in interactive mode. Type 'q' to quit.
cf-cli>foo = { bar = 1 };
cachePut( 'foobar', foo );

foo.bar = 2;

writeDump( cacheGet( 'foobar' ) );
struct

BAR: 1


cf-cli>
cf-cli>cf-cli>2
cf-cli>cf-cli>struct

BAR: 1

cf-cli>

WOOHOO! I managed to do something useful with Docker (and then reproduce it all again when writing this article). Am quite happy about that.

Final thing... if yer reading this and are going "frickin' hell Cameron, you shouldn't do [whatever] like [how I did it]", please let me know. This is very much a learning exercise for me.

Anyhow... this is the most work I've done in about four months now, and I need a... well... another beer.

Righto.

--
Adam

ColdFusion: looking at an issue Mingo had with ehcache and cachePut and cacheGet

 G'day:

Bloody Coldfusion. OK so why am I writing about a ColdFusion issue? Well about 80% of it is "not having a great deal else to do today", about 10% of being interested in this issue Mingo found. And 10% it being an excuse to mess around with Docker a bit. I am currently - and slowly - teaching myself about Docker, so there's some practise for me getting a ColdFusion instance up and running on this PC (which I no-longer have any type of CFML engine installed on).

OK so what's the issue. Mingo posted this on Twitter:


Just in case you wanna run that code, here it is for copy and paste:
foo = { bar = 1 };
cachePut( 'foobar', foo );

foo.bar = 2;

writeDump( cacheGet( 'foobar' ) );


Obviously (?) what one would expect here is {bar:1}. What gets put into cache would be a copy of the struct right?

Well... um... here goes...

/opt/coldfusion/cfusion/bin $ ./cf.sh
ColdFusion started in interactive mode. Type 'q' to quit.
cf-cli>foo = { bar = 1 };
struct
BAR: 1

cf-cli>cachePut( 'foobar', foo );
cf-cli>foo.bar = 2;
2
cf-cli>writeDump( cacheGet( 'foobar' ) );
struct
BAR: 2

cf-cli>

... errr... what?

It looks like ColdFusion is like only putting a reference to the struct into cache. So any code changing the data in the struct is changing it in CFML as well as changing it in cache. This does not seem right.

I did a quick google and found a ticket in Adobe's system about this: CF-3989480 - cacheGet returns values by reference. The important bit to note is that it's closed with


Not a great explanation from Adobe there, fortunately Rob Bilson had commented further up with a link to a cached version of an old blog article of his, explaining what's going on: "Issue with Ehcache and ColdFusion Query Objects". It's a slightly different situation, but it's the same underlying "issue". Just to copy and paste the relevant bit from his article:

Update: It looks like this is actually expected behavior in Ehcache. Unfortunately, it's not documented in the ColdFusion documentation anywhere, but Ehcache actually has two configurable parameters (as of v. 2.10) called copyOnRead and copyOnWrite that determine whether values returned from the cache are by reference or copies of the original values. By default, items are returned by reference. Unfortunately we can't take advantage of these parameters right now as CF 9.0.1 implements Ehcache 2.0.


I decided to have a look what we could do about this on ColdFusion 2018, hoping that its embedded implementation of Ehcache has been updated since Rob wrote that in 2010.

Firstly I checked the Ehcache docs for these two settings: copyOnWrite and copyOnRead. This is straight forward (from "copyOnRead and copyOnWrite cache configuration"):


<cache name="copyCache"
    maxEntriesLocalHeap="10"
    eternal="false"
    timeToIdleSeconds="5"
    timeToLiveSeconds="10"
    copyOnRead="true"
    copyOnWrite="true">
  <persistence strategy="none"/>
  <copyStrategy class="com.company.ehcache.MyCopyStrategy"/>
</cache>


The docs also confirm these are off by default

Next where's the file?

/opt/coldfusion $ find . -name ehcache.xml
./cfusion/lib/ehcache.xml
/opt/coldfusion $

Cool. BTW I just guessed at that file name.

So in there we have this (way down at line 471):


<!--
Mandatory Default Cache configuration. These settings will be applied to caches
created programmtically using CacheManager.add(String cacheName).

The defaultCache has an implicit name "default" which is a reserved cache name.
-->
<defaultCache
    maxElementsInMemory="10000"
    eternal="false"
    timeToIdleSeconds="86400"
    timeToLiveSeconds="86400"
    overflowToDisk="false"
    diskSpoolBufferSizeMB="30"
    maxElementsOnDisk="10000000"
    diskPersistent="false"
    diskExpiryThreadIntervalSeconds="3600"
    memoryStoreEvictionPolicy="LRU"
    clearOnFlush="true"
    statistics="true"
/>


That looked promising, so I updated it to use copyOnWrite:



<defaultCache
    maxElementsInMemory="10000"
    eternal="false"
    timeToIdleSeconds="86400"
    timeToLiveSeconds="86400"
    overflowToDisk="false"
    diskSpoolBufferSizeMB="30"
    maxElementsOnDisk="10000000"
    diskPersistent="false"
    diskExpiryThreadIntervalSeconds="3600"
    memoryStoreEvictionPolicy="LRU"
    clearOnFlush="true"
    statistics="true"
    copyOnWrite="false"
/>


Whatever I put into cache, I want it to be decoupled from the code immediately, hence doing the copy-on-write.

I restarted CF and ran the code again:

/opt/coldfusion/cfusion/bin $ ./cf.sh
ColdFusion started in interactive mode. Type 'q' to quit.
cf-cli>foo = { bar = 1 };
struct

BAR: 1

cf-cli>cachePut( 'foobar', foo );
cf-cli>foo.bar = 2;
2

cf-cli>writeDump( cacheGet( 'foobar' ) );

struct
BAR: 1

cf-cli>


Yay! We are getting the "expected" result now: 1

Don't really have much else to say about this. I'm mulling over writing down what I did to get ColdFusion 2018 up and running via Docker instead of installing it. Let's see if I can be arsed...

Righto.

-- 

Adam

Friday, 16 October 2020

Ben Brumm writes an interesting article on Hierarchical Data in SQL

G'day:

This is solely a heads-up regarding some potential reading for you.

A bloke called Ben Brumm came across one of my old articles, "CFML: an exercise in converting a nested set tree into an adjacency list tree". He's hit me up and asked me to add a link to that to an article he's written "Hierarchical Data in SQL: The Ultimate Guide". I've done that, but that article is buried in the mists of time back in 2015. I figured he could benefit from a more contemporary nudge too. So here it is. Me nudging.

In other news people have been urging me to get back to this blog, but I'm afraid I still am not finding anything new to write about. So... erm... yeah. If something leaps out at me I do fully intend to come back to this, but... not for now.

Righto.

-- 

Adam

Thursday, 9 April 2020

Aaaaah... *Me*

G'day
OK to continue this occasional series of commenting on ppl who have impacted my tenure at work, as they leave. Time to do another one.

Adam Cameron.

What a prick. Always nagging about doing TDD, always being pedantic about code reviews. Banging on about clean code. Asking for stuff to be refactored and simplified. Being stroppy. Then being even more stroppy.

So it's a bloody relief that after 10 years he's finally leaving, and getting out of everyone's hair.

Cameron is reluctantly leaving behind a whole bunch of people who have helped him grow as a professional (one way or another), enriched his work experience and who he genuinely admires. Both professionally and personally. He'll probably stay in touch with a bunch of them. And knowing him he'll be showing up in Porto to have beers with his squad as soon as he can.

He's been in a coupla good squads in his time, but this one that he's been leading for the last year is the best. He was lucky to get that lot as his first team as "Tech Lead". I doubt he'd've turned out reasonably OK at that role if it wasn't for them. The good thing is they hardly need leading, so will continue to do their excellent work even without him around.

10 years. Fuck me.

See ya.

--
Adam

Friday, 3 April 2020

Brian

G'day:
So this really sux.

For the bulk of the last decade I've been working alongside Brian Sadler. We first worked with each other when he joined hostelbookers.com when I'd been there for a coupla years, back in fuck-knows-when. I can recall thinking "shit... someone as old as I am, this should be interesting", in a room of devs (and managers) who were between 5-15 years younger than the both of us. Not that chronological experience necessarily means anything in any way that talent can be measured, but I had been used to being the oldest and most experienced geezer in the teams I'd been involved in. I've always been old, basically. So this new "Brian" guy seemed an interesting colleague to acquire.

My instinct was right.

[I've typed-in a paragraph of over-written shite four times now, and deleted the lot. I need to get to the point]

I have been a reasonably good programmer, technically. I'm OK with saying that.

What I've learned from Brian is that is only a small part of being a good team operative. I've always known and respected that programming is an act of communicating with humans, not the computer, but Brian really drove this home to me.

He introduced me to the concept of Clean Code.

He introduced me to the concept of TDD.

He schooled me in various concepts of refactoring, in that latter stage of Red Green Refactor. I'm still reading Martin Fowler's "Refactoring" as a result.

Every time I am looking at a code problem and I know I am not quite getting it, I hit him up and say "right, come on then... what am I missing...?" and he'll pull out a design pattern, or some OOP concept I'd forgotten about, or just has the ability to "see" what I can't in a code conundrum, and explain it to me.

Every time I ask "how can we make this code easier for our team to work with in future?", Brian's had the answer.

For a few years Brian's has been our Agile advocate, and listening to him and following his guidance has been an immeasurable benefit to my capabilities and my work focus.

I've also seen Brian help everyone else in my immediate team; other teams; and our leaders.

He's probably the most significant force for good I have had in my career.

He's also a really fuckin' good mate, for a lot of reasons that are not relevant for this sort of environment.

For reasons that are also irrelevant to this blog, today was the last day for the time being that Brian and I will be working with each other, and I am genuinely sad about that.

Genuinely sad.

Thanks bro.

--
Adam