Saturday, 7 May 2022

Running CFML code on trycf.com via a remote HTTP request

G'day:

This ended up being more of a rabbit hole than I expected it to be. But in the process I've learned a bit more about curl, PHP, Python, JS (client). And actually CFML too I guess.

I can't even remember why I needed to do this, but it was something to do with testing that TinyTestFramework I've been blathering about recently.

Anyhow, I decided I needed to run some code locally on my PC which would send some code off to trycf.com, run it, and send me back the response. I figured it'd be doable if I worked out what Abram was doing when I click the "Run Code" button on the trycf.com UI. As it turns out it's just an HTTP post, and I could could re-run the curl captured from my browser easily enough:

Yeah I don't care

Thanks, but before you take time to mention it: I know the code blows out to the right. It doesn't matter, no-one's expecting you to read it really, and the blow-out is just just cosmetic shite.

curl 'https://lucee5-sbx.trycf.com/lucee5/getremote.cfm' \
  -H 'authority: lucee5-sbx.trycf.com' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \
  -H 'accept-language: en-GB,en-US;q=0.9,en;q=0.8' \
  -H 'cache-control: max-age=0' \
  -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundaryaYm5TBPgCaT5n3TK' \
  -H 'cookie: _ga=GA1.2.771352503.1647902596; _gid=GA1.2.1730098774.1651605323; _gat_gtag_UA_35934323_2=1' \
  -H 'dnt: 1' \
  -H 'origin: https://trycf.com' \
  -H 'referer: https://trycf.com/' \
  -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'sec-fetch-dest: iframe' \
  -H 'sec-fetch-mode: navigate' \
  -H 'sec-fetch-site: same-site' \
  -H 'sec-fetch-user: ?1' \
  -H 'upgrade-insecure-requests: 1' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36' \
  --data-raw $'------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="setupcode"\r\n\r\n\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="zoom"\r\n\r\n\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="code"\r\n\r\n<cfscript>\r\nwriteOutput("hi")\r\n\r\n</cfscript>\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="postcode"\r\n\r\n\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="key"\r\n\r\nmain1651930152982-9b67997d-643d-3ddf-0b9f-6154282cfcf4\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="asserts"\r\n\r\n\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK--\r\n' \
  --compressed

I can run that in bash and it works fine. BTW, the actual code I'm running is buried in the middle there, There's quite a chunk of overhead to execute that code, and I reckoned the browser was probably being a bit belt-n-braces about all the headers it was sending, and I'd not need most of them. I whittled it down to this:

curl 'https://lucee5-sbx.trycf.com/lucee5/getremote.cfm' \
  -H 'content-type: multipart/form-data; boundary=----__trycf__' \
  --data-raw $'------__trycf__\r\nContent-Disposition: form-data; name="setupcode"\r\n\r\n\r\n------__trycf__\r\nContent-Disposition: form-data; name="code"\r\n\r\n<cfscript>\r\nwriteOutput("hi")\r\n</cfscript>\r\n------__trycf__\r\nContent-Disposition: form-data; name="asserts"\r\n\r\n\r\n------__trycf__--\r\n' \
  --compressed

There's a handy site that converts curls to language-specific implementations, and CFML is one of the options: https://curlconverter.com/#cfml. This was handy in theory, but the HTTP service call it created didn't work. Not its fault: it should have, but it seems CFHTTP can't handle that boundary syntax in the curl. Note that PHP's version also struggled, but the JS (fetch), Java and Python versions all worked fine.

This threw me for a while cos I'm not really that au fait with building HTTP requests by hand, but eventually I cracked it. Don't hand-crank the multipart boundary stuff: let CFML do it for you. So I came up with this proof of concept:

<cfset code = fileRead(expandPath("./code.cfm"))>

<cfhttp method="post" url="https://acf14-sbx.trycf.com/getremote.cfm" result="httpResponse">
    <cfhttpparam type="formField" name="code" value=#code#>
    <cfhttpparam type="formField" name="asserts" value="">
</cfhttp>

<cfif httpResponse.statusCode EQ "200 OK">
    <cfoutput>#httpResponse.fileContent#</cfoutput>
<cfelse>
    <cfdump var="#httpResponse#">
</cfif>

That's pretty simple. And my test code for this is just:

<cfscript>
    name = "Scott Steinbeck"
    writeOutput("G'day #name#")
</cfscript>

Results:

G'day Scott Steinbeck

(It was Scott that asked me to write this up).

That's where I've got to so far. I also want to see how I can include some set-up code. Hang on a sec whilst I watch some more HTTP traffic.

[…]

Oh OK, that was easy:

<cfset framework = fileRead(expandPath("./tinyTestFramework.cfm"))>
<cfset tests = fileRead(expandPath("./tests.cfm"))>

<cfhttp method="post" url="https://acf14-sbx.trycf.com/getremote.cfm" result="httpResponse">
    <cfhttpparam type="formField" name="setupcode" value="#framework#">
    <cfhttpparam type="formField" name="code" value="#tests#">
    <cfhttpparam type="formField" name="asserts" value="">
</cfhttp>

<cfif httpResponse.statusCode EQ "200 OK">
    <cfoutput>#httpResponse.fileContent#</cfoutput>
<cfelse>
    <cfdump var="#httpResponse#">
</cfif>

(code on github)

That example just runs the test for my test framework up on trycf instead of here on my own server. Because I can. At least now I remember why I wanted to do this in the first place, but that will be in a later article.

All these test so far only run on ColdFusion 2021, because that was what I was wanting to do. The other hosts are easy enough to glean just by watching the request when clicking "run code". The Lucee (latest) one is https://lucee5-sbx.trycf.com/lucee5/getremote.cfm

Anyway, not a terribly exciting one this (not like how terribly exciting the shit I write here usually is, eh? EH??), but problem solved and hopefully this will help Scott.

Righto.

--
Adam

ColdFusion: probable bug with the implementation of the rest operator

G'day:

ColdFusion 2021 added the spread and rest operators. These are implemented as two different usages of .... In this article I am going to be making an observation about how the implementation of the rest operator is incomplete and faulty. I first raised this in the CFML Slack channel, but I've now wittered on enough about it to copy it to here.

What does the rest operator do? The docs say:

[The] Rest Operator operator is similar to [the] Spread Operator but behaves in [the] opposite way, while Spread syntax expands the iterables into individual element[s], the Rest syntax collects and condenses them into a single element.

ibid.

That's not so useful out of the context of a discussion on the spread operator, that said. So a test should clarify:

function testRest(first, ...rest) {
    return arguments
}

function run() {
    describe("Testing CF's rest operator, eg: function testRest(first, ...rest)", () => {
        it("combines all latter argument values into one parameter value", () => {
            actual = testRest("first", "second", "third", "fourth")
            expect(actual).toBe([
                first = "first",
                rest = ["second", "third", "fourth"]
            ])
            
             writeDump(var=actual, label="actual/expected")
        })
    })
}

tinyTest.runTests()
  • The rest operator is ... as a prefix to the last parameter in a function signature.
  • Any arguments passed from that position on are combined into that one parameter's value.

So far so good. So what's the problem? I had a use case where I needed this to work for named arguments, not positional ones:

it("combines all latter argument values into one parameter value when using named arguments", () => {
    actual = testRest(first="first", two="second", three="third", four="fourth")
    expect(actual.first).toBe("first")
    expect(actual).toHaveKey("rest")
    expect(actual.rest).toBe({two="second", three="third", four="fourth"})
})

Same as before, just the arguments have names now. This test fails. Why? Because CF completely ignores the rest operator when one uses named arguments. This "passing" test shows the actual (wrong) behaviour:

it("doesn't work at all with named arguments", () => {
    actual = testRest(first="first", two="second", three="third", four="fourth")
    expect(actual.first).toBe("first")
    expect(actual?.rest).toBeNull()
    expect(actual.keyList().listSort("TEXTNOCASE")).toBe("FIRST,four,REST,three,two")
    
     writeDump(var=actual, label="actual")
     writeDump(var={first="first", rest={two="second", three="third", four="fourth"}}, label="expected")
})

As I said, I raised this on the CFML Slack channel. I got one useful response:

I think it's unusual to use rest with named parameters, but, CF supports named parameters as well as positional so I would expect it to work. I'd settle for it being fully documented though as only working with positional.

John Whish

Fair point, and as I said in reply:

My reasoning was remarkably similar. I started thinking "it's a bit unorthodox to use named arguments here", but then I stopped to think… why? And my conclusion was "because in other languages I use this operation there's no such thing as named arguments, so I've not been used to thinking about it", and that was the only reason I could come up with for me to think that. So I binned that thought (other than deciding to ask about it here).

The thing is there's no good reason I can think of that named arguments should not work. One cannot mix named and positional arguments which would be one wrinkle, so it's 100% reliable to take a set of named arguments, and match the argument names to the parameter names in the method signature. There is no ambiguity: any args that have the same name as a param are assigned as that parameter value. All the rest - instead of being passed in as ad-hoc arguments - are handled by the ... operation.

I cannot see a way that there's any ambiguity either. It's 100% "match the named ones, pass all others in the rest param".

What happens if the method call actually specifies a named argument that matched the name of the "rest" param? Same as if one specifies a positional argument in the position of the "rest" param: it doesn't matter. all arguments that don't match other named params are passed in the rest argument value.

I also think that if for some reason named arguments are not supported for use on function using the rest operator, then an exception should be thrown; not simply the code being ignored.

And whatever the behaviour is needs to be documented.

However one spins it, there are at least two bugs here:

  • Either it should work as I'd expect (or some variation thereof, if I have not thought of something), or it should throw an exception.
  • The behaviour should be documented.

I have not raised tickets for these as I'm not really a user of CF any more, so I don't care so much. Enough to raise it with them; not enough to care what they do about it. But CFers probably should care.

NB: I did not realise Lucee did not support the spread and rest operators at all, so I had to take a different approach to my requirement anyhow. I've not decided on the best way as yet.

There is a ticket for them to be implemented in Lucee: LDEV-2201.

The tests I wrote for this can be run on trycf.com.

Righto.

--
Adam

Wednesday, 4 May 2022

CFML: updates to my TinyTestFramework

G'day:

Just to pass the time / avoid other things I really ought to be doing instead, over the last few evenings I'm been messing around with my TinyTestFramework. I first created this as an exercise in doing some "real world" TDD for a blog article: "TDD: writing a micro testing framework, using the framework to test itself as I build it". The other intent of this work is so I can run actual tests in my code on trycf.com. This is useful when I'm both asking and answering CFML questions I encounter on the CFML Slack and other places.

The first iteration of the framework was pretty minimal. It was just this:

void function describe(required string label, required function testGroup) {
    try {
        writeOutput("#label#<br>")
        testGroup()
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}
void function it(required string label, required function implementation) {
    try {
        writeOutput("#label#: ")
        implementation()
        writeOutput("OK<br>")
    } catch (TestFailedException e) {
        writeOutput("Failed<br>")
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}
function expect(required any actual) {
    return {toBe = (expected) => {
        if (actual.equals(expected)) {
            return true
        }
        throw(type="TestFailedException")
    }}
}

That's it.

But it let me write tests in a Jasmine/TestBox sort of way, right there in trycf.com:

describe("describe", () => {
    it("it is a test", () => {
        expect(true).toBe(true)
    })
}) 

And this would output:

describe
it is a test: OK

That's cool. That was a good MVP. And I actually use it on trycf.com.

However I quickly felt that only having the one toBe matcher was limiting, and made my tests less clear than they could be. Especially when I wanted to expect null or an exception. So… I messed around some more.

I'm not going to take you through the full TDD exercise of writing all this, but I assure you I TDDed almost all of it (I forgot with a coupla small tweaks, I have to admit. I'm not perfect).

But here's the code (also as a gist), for those that are interested:

<style>
    .tinyTest {background-color: black; color:white; font-family:monospace}
    .tinyTest div {margin-left: 1em}
    .tinyTest .pass {color:green;}
    .tinyTest .fail {color:red;}
    .tinyTest .error {background-color:red; color:black}
</style>
<cfscript>
function expect(required any actual) {
    return {toBe = (expected) => {
        if (actual.equals(expected)) {
            return true
        }
        throw(type="TinyTest.TestFailedException")
    }}
}

tinyTest = {
    describe = (string label, function testGroup) => {
        tinyTest.inDiv(() => {
            try {
                writeOutput("#label#<br>")
                testGroup()
            } catch (any e) {
                writeOutput("Error: #e.message#<br>")
            }
        })
    },
    it = (string label, function implementation) => {
        tinyTest.inDiv(() => {
            try {
                writeOutput("It #label#: ")
                implementation()
                tinyTest.showPass("OK<br>")
            } catch (TinyTest e) {
                tinyTest.showFail("Failed<br>")
            } catch (any e) {
                tinyTest.showError("Error: #e.message#<br>")
            }
        })
    },
    expect = (any actual) => {
        var proxy.actual = arguments?.actual
    
        return {
            toBe = (expected) => tinyTest.matchers.toBe(expected, actual),
            toBeTrue = () => tinyTest.matchers.toBe(true, actual),
            toBeFalse = () => tinyTest.matchers.toBe(false, actual),
            toBeNull = () => tinyTest.matchers.toBeNull(proxy?.actual),
            toThrow = () => tinyTest.matchers.toThrow(actual),
            toInclude = (needle) => tinyTest.matchers.toInclude(needle, actual),
            notToBe = (expected) => tinyTest.matchers.not((expected) => tinyTest.matchers.toBe(expected, actual)),
            notToBeTrue = (expected) => tinyTest.matchers.not((expected) => tinyTest.matchers.toBe(true, actual)),
            notToBeFalse = (expected) => tinyTest.matchers.not((expected) => tinyTest.matchers.toBe(false, actual)),
            notToBeNull = () => tinyTest.matchers.not(() => tinyTest.matchers.toBeNull(proxy?.actual)),
            notToThrow = () => tinyTest.matchers.not(() => tinyTest.matchers.toThrow(actual)),
            notToInclude = (needle) => tinyTest.matchers.not((needle) => tinyTest.matchers.toInclude(needle, actual))
            
        }
    },
    fail = () => {
        throw(type="TinyTest.FailureException")
    },
    matchers = {},
    inDiv = (callback)  => {
        writeOutput("<div>")
        callBack()
        writeOutput("</div>")
    },
    showPass = (message) => {
        writeOutput('<span class="pass">#message#</span>')
    },
    showFail = (message) => {
        writeOutput('<span class="fail"><em>#message#</em></span>')
    },
    showError = (message) => {
        writeOutput('<span class="error"><strong>#message#</strong></span>')
    }
    
}
tinyTest.matchers.toBe = (expected, actual) => {
    if (actual.equals(expected)) {
        return true
    }
    throw(type="TinyTest.TestFailedException")
}
tinyTest.matchers.toBeNull = (actual) => {
    if (isNull(actual)) {
        return true
    }
    throw(type="TinyTest.TestFailedException")
}
tinyTest.matchers.toThrow = (callback) => {
    try {
        callback()
        throw(type="TinyTest.TestFailedException")
    } catch (TinyTest.TestFailedException e) {
        rethrow
    } catch (any e) {
        return true
    }
}
tinyTest.matchers.toInclude = (needle, haystack) => tinyTest.matchers.toBe(true, haystack.findNoCase(needle) > 0)

tinyTest.matchers.not = (callback) => {
    try {
        callback()
        throw(type="TinyTest.NotTestFailedException")
    } catch (TinyTest.NotTestFailedException e) {
        throw(type="TinyTest.TestFailedException")
    } catch (TinyTest.TestFailedException e) {
        return true
    } catch (any e) {
        rethrow
    }
}


describe = tinyTest.describe
it = tinyTest.it
expect = tinyTest.expect
fail = tinyTest.fail
</cfscript>

120 lines now.

What functionality has all this added? Well here's the thing with BDD-style tests. I have documentation and proof that it does what it says it does:

Tests of TinyTestFramework
Tests of it
It prefixes its message with it: OK
Tests of expect
It exists: OK
It returns a struct with keys for matcher callbacks: OK
Tests of fail
It fails a test: OK
Test of test result visualisations
It specifies that a pass should have positive emphasis: OK
It specifies that a fail should have negative emphasis: OK
It specifies that an error should have more emphasis than a fail: OK
Tests of matchers
Tests of toBe
It passes if the actual and expected values are equal: OK
It fails if the actual and expected values are not equal: OK
It expects java.lang.String to work with toBe: OK
It expects java.lang.Double to work with toBe: OK
It expects java.lang.Double to work with toBe: OK
It expects java.lang.Boolean to work with toBe: OK
It expects lucee.runtime.type.ArrayImpl to work with toBe: OK
It expects lucee.runtime.type.StructImpl to work with toBe: OK
It expects lucee.runtime.type.QueryImpl to work with toBe: OK
Tests of notToBe
It passes if the actual and expected values are not equal: OK
It fails if the actual and expected values are equal: OK
It expects java.lang.String to work with notToBe: OK
It expects java.lang.Double to work with notToBe: OK
It expects java.lang.Double to work with notToBe: OK
It expects java.lang.Boolean to work with notToBe: OK
It expects lucee.runtime.type.ArrayImpl to work with notToBe: OK
It expects lucee.runtime.type.StructImpl to work with notToBe: OK
It expects lucee.runtime.type.QueryImpl to work with notToBe: OK
Tests of toBeTrue
It passes if the value is true: OK
It fails if the value is false: OK
Tests of notToBeTrue
It passes if the value is not true: OK
It fails if the value is true: OK
Tests of toBeFalse
It passes if the value is false: OK
It fails if the value is true: OK
Tests of notToBeFalse
It passes if the value is not false: OK
It fails if the value is false: OK
Tests of toThrow
It expects an exception to be thrown from its callback argument: OK
It fails if an exception is not thrown from its callback argument: OK
Tests of notToThrow
It passes if the callback does not throw an exception: OK
It fails if the callback does throw an exception: OK
Tests of toBeNull
It passes if the value is null: OK
It fails if the value is not null: OK
Tests of notToBeNull
It passes if the value is not null: OK
It fails if the value is null: OK
Tests of toInclude
It passes if the haystack contains the needle: OK
It passes if the haystack and needle exactly match: OK
It ignores case: OK
It fails if the haystack does not contain the needle: OK
Tests of notToInclude
It passes if a haystack does not contains the needle: OK
It fails if the haystack contains the needle: OK

All nicely indented and emphasised and shit.

So what have I added for this new version?

  • Tidied up the output to make it easier to read
  • Added these matchers:
    • toBeTrue
    • toBeFalse
    • toBeNull
    • toThrow
    • toInclude
  • And not versions of each of those: notToBe, notToThrow, etc
  • Tests for everything: adamcameron/testTinyTestFramework.cfm

And it's tested using itself, obviously. Interestingly / predictably, there >300 lines of test code there. The ratio is 1:2.5 code:tests

What am I gonna do next? I want to improve that toInclude matcher to work on more than just strings. I also want to have a toBeInstanceOf matcher. Also at some point I better do something with checking structs and arrays and that sorta jazz. I've not needed to actually do that stuff yet, so have not bothered to implement them. But I intend to.

Oh… and you can use this yerself in trycf.com via this URL: https://trycf.com/gist/c631c1f47c8addb2d9aa4d7dacad114f/lucee5?setupCodeGistId=816ce84fd991c2682df612dbaf1cad11&theme=monokai.

Righto.

--
Adam

Tuesday, 3 May 2022

Work with me here - revisited

G'day:

Update

This role has been filled.

Back in Jan I posted "Work with me here". Once again we are growing our team, and the equivalent job has opened again (this is in addition to the successful hire we made after the January round, not "instead of ~" ;-).

So if you're in the UK, and fancy leveraging your existing strong (strong) CFML skills to get into a position where we'll migrate your capabilities to Kotlin, this could be a good opportunity.

The narrative detail remains the same as in that last article, so go give it a read.

The formal job spec is at https://www.easydirectdebits.co.uk/job/senior-application-developer-uk/, and you can apply from there too.

Or ping me here if you need further info. Any actual applications should use the link above though.

Righto.

--
Adam

Monday, 2 May 2022

CFML: with Lucee, true isn't necessarily the same as true

G'day:

Yesterday I decided to improve my "Tiny Test Framework". I wrote an article about developing the first iterationof this as a TDD exercise six or so months ago: "TDD: writing a micro testing framework, using the framework to test itself as I build it". I use this framework on trycf.com, so I can include tests in my code samples. The first iteration of this only had the one toBe matcher, and yesterday I decided to add in toBeTrue, toBeFalse and toThrow: just to make my sample code on trycf a bit clearer.

Whilst implementing the toBeTrue matcher, I had a brain fart and ended up with this:

if (actual === true) {
    return true
}

This was the wrong approach to start with, and I'm too explicit in my check. I don't wanna be using the === operator there, this should be sufficient:

if (actual) {
    return true
}

IN CFML "truthy" should be enough for this matcher, as that's idiotmatic-CFML.

But whilst running my tests, I noticed something weird in Lucee. I had a test along these lines:

it("a passing test", () => {
    x = "something"
    expect(x.equals(x)).toBeTrue()
})

On ColdFusion, this passed fine.

But on Lucee, it failed. It seemed that apparently the same string doesn't equal itself. Although the more I dug, the weirder it got. I distilled a repro case down to:

s = "there can be only one"    
writeOutput(s.equals(s) === true) // Lucee: false; ColdFusion: true

This completely flummoxed me, and I posted something on the CFML Slack channel about it. Jonas Eriksson pointed me in the direction of an article on the topic of === that Ben had written a coupla years ago("Exploring The Triple Equals (===) Operator In Lucee CFML 5.3.4.77"). The article itself didn't tell me anything I didn't already "know" (ahem), but Brad's comment reminded me that Lucee's implementation of === is a bit shit. There's a Jira ticket for it here: "LDEV-1282: Fix === operator to check for type equality", and another one "LDEV-3132: ACF2021 - Identity/Not Identity Operator" that I have even participated in. Just a week ago!!.

The issue is that Lucee has decided to make the === operator to be an identity operator. IE: it does not do an equality and type check like ColdFusion does (full disclosure: Lucee implemented this operator before ColdFusion did), it just checks that the two operands are exactly the same object (ie: in memory).

How is this causing my problem? Well it's because not all trues are created equal, in Lucee. This gist demonstrates it:

system=createObject("java", "java.lang.System")    
    
s = "there can be only one"

e = s.equals(s)

t = true

writeDump([
    "e" = e, // true
    "t" = t, // true
    "==" = e == t, // true
    "===" = e === t, // LUCEE: FALSE; ColdFusion: true
    "e hashcode" = system.identityHashCode(e), // one thing, eg 712659136
    "t hashcode" = system.identityHashCode(t) // a different thing, eg: 558076530
])

Notice how the identity hashes for e and t are different. Therefore e === t is false on Lucee. This, I'm afraid, makes the === operator on Lucee about as useful as a chocolate teapot. In CFML it's really not important that a value occupies the same memory; but sometimes it's handy - because CFML is a loosely- & dynamically-typed language - to be able to check a value is the same and that it's actually the same type too (eg:"1" is equal to 1, but is not the same type). And this lack of joined-up-thinking in Lucee is demonstrated pretty clearly in that two instances of true are not considered the same as each other. That's just daft.

Out of interest I decided to push the boat out on my largely non-existent Java skills, check what Java does here. I came up with this:

class TestIdentiyEquality {

    public static void main(String[] args) {
        Boolean b1 = new Boolean(true); // deprecated approach to getting a Boolean
        Boolean b2 = new Boolean(true);

        System.out.println(String.format("via constructor, using equals: %b", b1.equals(b2))); // true 
        System.out.println(String.format("via constructor, using ==: %b", b1 == b2)); // false

        Boolean b3 = Boolean.parseBoolean("true");
        Boolean b4 = Boolean.valueOf("true");

        System.out.println(String.format("via parseBoolean/valueOf, using equals: %b", b3.equals(b4))); // true 
        System.out.println(String.format("via parseBoolean/valueOf, using ==: %b", b3 == b4)); // true
    }

}

It's interesting that in Java, creating a new Boolean object via its constructor is deprecated these days, in favour of using static methods that return the same Boolean each time. I guess this is Lucee's problem too: its true isn't the same object as the one the JVM is using (I presume internally Java uses the same object all over, but can't be arsed thoroughly checking this). Either way, Lucee have claimed that they have followed Java's route for the identity operator, which I think is odd given Java doesn't distinguish between value equality and identity equality via operators; it uses the equals method for checking value, and it only has the == operator other than that, and that does do an identity check. Also Java has no need to differentiate between "value equality" and "value and type equality" because it's a strongly typed language. It's not routine in Java to need to do myStringContaining1 == myNumericContaining1 like one would in a loosely typed language like CFML.

I did a check of some other languages that operate in a similar space to CFML (this is being a bit kind to CFML, but hey). My findings were:

  • Lucee, Python, Groovy, Kotlin treat === as reference equality check
  • CF, PHP, JS, Ruby treat it as a value & type check (in Ruby even == checks type too)

The code for the comparisons is in this gist.

I think CFML more closely fits with PHP, JS and - to a lesser extent - Ruby here, and I think ColdFusion has got the implementation of === right. Lucee should sort theirs out to work the same.


Oh, and where did I get to with my improvements to my tiny testing framework? This Twitter message sums-up my afternoon:

I spent so much time looking at various bugs in Lucee and spilling over into ColdFusion as I checked my code for the Lucee problem and found other bugs on ColdFusion too that I lost the taste for doing CFML, and just drank beer instead. It's a better use of my time, I think. The same could be applied to writing daft blog articles, so I'm off to find a beer instead.

Righto.

--
Adam


Update

OKOK, I did check what Java does about comparing two different true expressions:

class TestEquals {

    public static void main(String[] args) {
        String s1 = "there can be only one";
        String s2 = "maybe there can be another one, actually";
        Boolean b1 = s1.equals(s1);
        Boolean b2 = s2.equals(s2);

        System.out.println(String.format("b1: %b; b2: %b; b1 == b2: %b", b1, b2, b1 == b2)); // b1: true; b2: true; b1 == b2: true
    }

}

The equivalent in CFML is:

s1 = "there can be only one"
s2 = "maybe there can be another one, actually"
b1 = s1.equals(s1)
b2 = s2.equals(s2)

writeOutput("b1: #b1#; b2: #b2#; b1 == b2: #b1 === b2#") // b1: true; b2: true; b1 == b2: false (true on ColdFusion)

Even in the context of booleans "we do it how Java does it" doesn't stand-up to too much scrunity with Lucee. I think its implementation is just wrong.

Monday, 18 April 2022

CFML: Adding a LogBox logger to a CFWheels app via dependency injection

G'day:

This follows on from CFML: implementing dependency injection in a CFWheels web site. In that article I got the DI working, but only with a test scenario. For the sake of completeness, I'm gonna continue with the whole point of the exercise, which is getting a logger service into my model objects, via DI.


0.6 adding LogBox

I ran into some "issues" yesterday whilst trying to write this article. Documented here: "A day in the life of trying to write a blog article in the CFML ecosystem", and there's some file changes committed as 0.5.1. So that killed my motivation to continue this work once I'd waded through that. I appear to be back on track now though.

Right so I'll add a quick test to check LogBox is installed. It's a bit of a contrived no-brainer, but this is playing to my strengths. So be it:

describe("Tests the LogBox install", () => {
    it("is instantiable", () => {
        expect(() => getComponentMetadata("LogBox")).notToThrow(regex="^.*can't find component \[LogBox\].*$")
    })
})

If LogBox is where it's supposed to be: it'll pass. Initially I had a new LogBox() in there, but it needs some config to work, and that requires a bit of horsing around, so I'll deal with that next. For now: is it installed? Test sez "no":

Test sez… yes.

OK. That was unexpected. Why did that pass? I have checked that LogBox is not installed, so WTH??

After a coupla hours of horsing about looking at TestBox code, I worked out there's a logic… um… shortfall (shall I say) in its implementation of that regex param, which is a bit wayward. The code in question is this (from /system/Assertion.cfc):

 if (
    len( arguments.regex ) AND
    (
        !arrayLen( reMatchNoCase( arguments.regex, e.message ) )
        OR
        !arrayLen( reMatchNoCase( arguments.regex, e.detail ) )
    )
) {
    return this;
}

Basically this requires both of message and detail to not match the regex for it to be considered "the same" exception. This is a bit rigorous as it's really unlikely for this to be the case in the real world. I've raised it with Ortus (TESTBOX-349), but for now I'll just work around it. Oh yeah, there's a Lucee bug interfering with this too. When an exception does have the same message and details, Lucee ignores the details. I've not raised a bug for this yet: I'm waiting for them to fee-back as to whether I'm missing something. When there's a ticket, I'll cross-post it here.

Anyway, moving on, I'll just check for any exception, and that'll do:

expect(() => getComponentMetadata("LogBox")).notToThrow()

That was a lot more faff to get to this point than I expected. Anyway. Now I install LogBox:

root@60d2920f4c60:/var/www# box install logbox
√ | Installing package [forgebox:logbox]

0.7 wiring LogBox into the DependencyInjectionService

One of the reasons the previous step really didn't push the boat out with testing if LogBox was working, is that to actually create a working LogBox logger takes some messing about; and I wanted to separate that from the installation. And also to give me some time to come up with the next test case. I want to avoid this sort of thing:

Possibly © 9gag.com - I am using it without permission. If you are the copyright owner and wish me to remove this, please let me know.

I don't want to skip to a test that is "it can log stuff that happens in the Test model object". I guess it is part ofthe requiremnt that the logger is handled via dependency injection into the model, so we can first get it set up and ready to go in the DependencyInjectionService. I mean the whole thing here is about DI: the logger is just an example usage. I think the next step is legit.

I've never used LogBox before, so I am RTFMing all this as I type (docs: "Configuring LogBox"). It seems I need to pass a Config object to my LogBox object, then get the root logger from said object… and that's my logger. Al of that can go in a factory method in configureDependencies, adn I'll just put the resultant logger into the IoC container.

it("loads a logger", () => {
    diService = new DependencyInjectionService()

    logger = diService.getBean("Logger")

    expect(logger).toBeInstanceOf("logbox.system.logging.Logger")

    expect(() => logger.info("TEST")).notToThrow()
})

I'm grabbing a logger and logging a message with it. The expectation is simply that the act of logging doesn't error. For now.

First here's the most minimal config I seem to be able to get away with:

component {
    function configure() {
        variables.logBox = {
            appenders = {
                DummyAppender = {
                    class = "logbox.system.logging.appenders.DummyAppender"
                }
            }
        }
    }
}

The docs ("LogBox DSL ") seemed to indicate I only needed the logBox struct, but it errored when I used it unless I had at least one appender. I'm just using a dummy one for now because I'm testing config, not operations. And there's nothing to test there: it's all implementation, so I think it's fine to create that in the "green" phase of "red-green-refactor" from that test above (currently red). With TDD the red phase is just to do the minimum code to make the test pass. That doesn't mean it needs to be one line of code, or one function or whatever. If my code needed to call a method on this Config object: then I'd test that separately. But I'm happy that this is - well - config. It's just data.

Once we have that I can write my factory method on DependencyInjectionService:

private function configureDependencies() {
    variables.container.declareBean("DependencyInjectionService", "services.DependencyInjectionService")
    variables.container.declareBean("TestDependency", "services.TestDependency")

    variables.container.factoryBean("Logger", () => {
        config = new Config()
        logboxConfig = new LogBoxConfig(config)

        logbox = new LogBox(logboxConfig)
        logger = logbox.getRootLogger()

        return logger
    })
}

I got all that from the docs, and I have nothing to add: it's pretty straight forward. Let's see if the test passes:

Cool.

Now I need to get my Test model to inject the logger into itself, and verify I can use it:

it("logs calls", () => {
    test = model("Test").new()
    prepareMock(test)
    logger = test.$getProperty("logger")
    prepareMock(logger)
    logger.$("debug")

    test.getMessage()

    loggerCallLog = logger.$callLog()
    expect(loggerCallLog).toHaveKey("debug")
    expect(loggerCallLog.debug).toHaveLength(1)
    expect(loggerCallLog.debug[1]).toBe(["getMessage was called"])
})

Here I am mocking the logger's debug method, just so I can check it's being called, and with what. Having done this, I am now wondering about "don't mock what you don't own", but I suspect in this case I'm OK because whilst the nomenclature is all "mock", I'm actually just spying on the method that "I don't own". IE: it's LogBox's method, not my application's method. I'll have to think about that a bit.

And the implementation for this is way easier than the test:

// models/Test.cfc
private function setDependencies() {
    variables.dependency = variables.diService.getBean("TestDependency")
    variables.logger = variables.diService.getBean("Logger")
}

public function getMessage() {
    variables.logger.debug("getMessage was called")
    return variables.dependency.getMessage()
}

Just for the hell of it, I also wrote a functional test to check the append was getting the expected info:

it("logs via the correct appender", () => {
    test = model("Test").new()

    prepareMock(test)
    logger = test.$getProperty("logger")

    appenders = logger.getAppenders()
    expect(appenders).toHaveKey("DummyAppender", "Logger is not configured with the correct appender. Test aborted.")

    appender = logger.getAppenders().DummyAppender
    prepareMock(appender)
    appender.$("logMessage").$results(appender)

    test.getMessage()

    appenderCallLog = appender.$callLog()

    expect(appenderCallLog).toHaveKey("logMessage")
    expect(appenderCallLog.logMessage).toHaveLength(1)
    expect(appenderCallLog.logMessage[1]).toSatisfy((actual) => {
        expect(actual[1].getMessage()).toBe("getMessage was called")
        expect(actual[1].getSeverity()).toBe(logger.logLevels.DEBUG)
        expect(actual[1].getTimestamp()).toBeCloseTo(now(), 2, "s")
        return true
    }, "Log entry is not correct")
})

It's largely the same as the unit test, except it spies on the appender instead of the logger. There's no good reason for doing this, I was just messing around.


And that's it. I'm pushing that up to Git as 0.7…

… and go to bed.

Righto.

--
Adam

Sunday, 17 April 2022

A day in the life of trying to write a blog article in the CFML ecosystem

G'day:

This is not the article I intended to write today. That article was gonna be titled "CFML: Adding a LogBox logger to a CFWheels app via dependency injection", but I'll need to get to that another day now.

Here's how far that article got before the wheels fell off:

And that was it.

Why? Well I started by writing an integration test just to check that box install logbox did what I expected:

import test.BaseSpec
import logbox.system.logging.LogBox

component extends=BaseSpec {

    function run() {
        describe("Tests the LogBox install", () => {
            it("is instantiable", () => {
                expect(() => getComponentMetadata("LogBox")).notToThrow()
            })
        })
    }
}

Simple enough. It'll throws an exception if LogBox ain't there, and I'm expecting that. It's a dumb test but it's a reasonable first step to build on.

I run the test:

Err… come again? I ain't installed it yet. I lifted the code from the expect callback out and run it "raw" in the body ofthe test case: predictable exception. I put it back in the callback. Test passes. I change the matcher to be toThrow. Test still passed. So this code both throws and exception and doesn't throw an exception. This is pleasingly Schrödingeresque, but not helpful.

The weird thing is I know this is not a bug in TestBox, cos we use notToThrow in our tests at work. I port the test over to my work codebase: test fails (remember: this is what I want ATM, we're still at the "red" of "red-green-refactor").

I noticed that we were running a slightly different version of Testbox in the work codebase: 4.4.0-snapshot compared to my 4.5.0+5. Maybe there's been a regression. I changed my TestBox version in box.json and - without thinking things through - went box install again (not just box install testbox which is all I really needed to do), and was greeted with this:

That's reasonably bemusing as I had just used box install fw1 to install it in the first place, and that went fine. And I have not touched it since. I checked what version I already had installed (in framework/box.json), and it claims 4.3.0. So… ForgeBox… I beg to differ pal. You found this version y/day, why can't you find it today? I check on ForgeBox, and for 4.x I see versions 4.0.0, 4.1.0, 4.2.0, 4.5.0-SNAPSHOT. OK, so granted: no 4.3.0. Except that's what it installed for me yesterday. Maybe 4.3.0 has issues and got taken down in the last 24h (doubtful, but hey), so I blow away my /framework directory, and remove the entry from box.json, and box install fw1 again. This is interesting:

root@280d80cf28c6:/var/www# box install fw1
√ | Installing package [forgebox:fw1]
|------------------------------------------------
| Verifying package 'fw1' in forgebox, please wait...
| Installing version [4.2.0].

4.2.0. But its entry in its own box.json is 4.3.0, and the constraint it put in my box.json is ^4.3.0.

I do not have time or inclination for any of this, so I just stick a constraint of ~4.2.0 in my box.json, and that seems to have solved it. I mean the error went away: it's still installing 4.3.0. Even with a hard-coded 4.2.0 it's still installing 4.3.0.

Brad Wood from Ortus/CommandBox had a look at this, nutted-out that there was something wrong with the way the FW/1 package on ForgeBox was configured, and he in turn pinged Steve Neiland who looks after FW/1 these days, and he got this sorted. I'm now on 4.3.0, and it says it's 4.2.0. And box install no longer complains at me. Cheers fellas.

Then I noticed that because of the stupid way CFWheels "organises" itself in the file system, I have inadvertantly overwritten a bunch of my own CFWheels files. Sigh. CFWheels doesn't bother to package itself up as "app" (its stuff) and "implementation" (my code that uses their app), it just has "here's some files: some you should change (anything outside the wheels subdirectory), some you probably shouldn't (the stuff in the wheels subdirectory)", but there's no differentiation when it comes to installation: all the files are deployed. Overwriting all the user-space files with their original defaults. Sorry but this is just dumbarsey. Hurrah for source control and small commit iterations is all I can say, as I could just revert some files and I was all good.

Right so now I have the same version of TestBox installed here as in our app at work (remember how this was all I was tring to do? Update testbox. Nothing to do with FW/1, and nothing to do with CFWheels. But there's an hour gone cocking around with that lot).

And the test still doesn't work. Ballocks.

I notice the Lucee version is also different. We're locked into an older version of Lucee at work due to bugs and incompats in newer versions that we're still waiting on to be fixed, so the work app is running 5.3.7.47, and I am on 5.3.8.206. Surely it's not that? I rolled my app's Lucee version back to 5.3.7.47 and the test started failing (correctly). OK, so it's a Lucee issue.

I spent about an hour messing around doing a binary search of Lucee container versions until I identified the last version that wasn't broken (5.3.8.3) and the next version - a big jump here - 5.3.8.42 that was broken. I looked at a diff of the code but nothing leapt out at me. This was slightly daft as I had no idea what I was looking for, so that was probably half an hour of time looking at Lucee's source code in an aimless fashion. I actually saw the change that was the problem, but didn't clock that that is what caused it at the time.

Having drawn a blank, I slapped my forehead, called myself a dick, and went back to the code in TestBox that was behaving differently. That would obviously tell me where to look for the issue.

I tracked the problem down to here, in system/Assertion.cfc:

    // Message+Detail regex must not match
    if (
        len( arguments.regex ) AND
        (
            !arrayLen( reMatchNoCase( arguments.regex, e.message ) ) OR !arrayLen(
                reMatchNoCase( arguments.regex, e.detail )
            )
        )
    ) {
        return this;
    }

    fail( arguments.message );
}

I distilled that down into a portable repro case:

s = ""
pattern = ".*"
writeDump([
    reMatch = s.reMatchNoCase(pattern),
    matches = s.matches(pattern),
    split = s.split(pattern)
])

There are some Java method calls there to act as controls, but on Lucee's current version, we get this:

And on earlier versions it's this:

(Full disclosure: I'm using Lucee 4.5 on trycf.com for that second dump, but it's the same results in earlier versions of Lucee 5, up to the point where it starts going wrong)

Note how previously a regex match of .* matches an empty string? This is correct. It does. In all regex engines I know of. Yet in Lucee's current versions, it returns a completely empty array. This indicates no match, and it's wrong. Simple as that. So there's the bug.

I was pointed in the direction of an existing issue for this: LDEV-3703. Depsite being a regression they know they caused, Lucee have decided to only fix it in 6.x. Not the version they actually broke. Less than ideal, but so be it.

There were a coupla of Regex issues dealt with between those Lucee versions I mentioned before. Here's a Jira search for "fixversion >= 5.3.8.4 and fixversion < 5.3.8.42 and text ~ regex". I couldn't be arsed tracking back through the code, but I did find something in LDEV-3009 mentioning a new Application.cfc setting:

this.useJavaAsRegexEngine = true

This is documented for ColdFusion in Application variables, and… absolutely frickin' nowhere in the Lucee docs, as far as I can see.

On a whim I stuck that setting in my Application.cfc and re-ran the test. If the setting was false: the test doesn't work. If it was true: the test does work. That's something, but Lucee is not off the hook here. The behaviour of that regex match does not change between the old and new regex engines! .* always matches an empty string! So there's still a bug.

However, being pragmatic, I figured "problem solved" (for now), and moved on. For some reason I restarted my container, and re-hit my tests:

I switched the setting to this.useJavaAsRegexEngine = false and the tests ran again (failed incorrectly, but ran). So… let me get this straight. For TestBox to work, I need to set that setting to true. To get CFWheels to work, I need to set it to false.

For pete's sake.

As I said on the Lucee subchannel on the CFML Slack:

Do ppl recall how I've always said these stupid flags to change behaviour of the language were a fuckin dumb idea, and are basically unusable in a day and age where we all rely on third-party libs to do our jobs?

Exhibit. Fucking. A.

Every single one of these stupid, toxic, setting doubles the overhead for library providers to make their code work. I do not fault TestBox or CFWheels one bit here. They can't be expected to support the exponential number of variations each one of those settings accumulates. I can firmly say that no CFML code should ever be written that depends on any of these settings. And no library or third-party code should ever be tested with the setting variation on. Just ignore them. The settings should not exist. Anyway: this is an editorial digression. "We are where we are" as the over-used saying goes.


Screw all this. Seriously. All I wanted to do is to do a blog article about perhaps 50-odd new lines of code in my example app. Instead I spent four hours untangling this shite. And my blog article has not progressed.

Here's what I needed to do to my app to work around these various issues:

This is all committed to GitHub as 0.5.1

Writing this sure was cathartic. I think I was my own audience for this one. Ah well. Good on you if you got to here.

Righto.

--
Adam