Spacewar! 19 - A look at CodeaUnit.
In this section, we’ll take a look at the CodeaUnit code and see what we can learn about how it works. Having written what’s far below, I feel the need to start with some code as our own.
Objects, functions, tables
You’ve seen in our use of CodeaUnit that we get to say things like
_:expect(button.pressed).is(true)
That syntax is weird enough that it made me wonder what was going on. _:expect()
is the syntax for a method send to an object. So there must be an object around here somewhere, named _
. What is it? (Quick answer based on reading a lot of code: it’s an instance of CodeaUnit class.) There must be a method in there, called expect
. A method in Codea is a function which expects self
as its first argument, self
being the instance to which the message expect
was sent.
What about the .is()
? That is a function call (not a method call). Unlike a method call, a function call isn’t exactly sent to anyone, but the symbol “is” must be resolved somewhere.
It gets worse before it gets better. The notation X.is means, precisely, X[“is”]. It’s syntactic sugar, and nothing more. So X must be a table, because only tables understand []. More precisely, X[“is’] tells the Lua compiler to emit code that looks up “is” in what is presumed to be a table, which will have been resolved by the compiler by figuring out what X is.
In Lua, a class instance is a table. The table acts as a mapping from various symbols, like the member variables and functions, to their values. So if we have a class instance with a member variable X and a member variable Y, and a function sum(), it is represented by a table defining at least X, Y, and sum. Here’s an example:
Table = { X = 3, Y = 2, sum = function()
return Table.X + Table.Y
end}
function setup()
print(Table.X, Table.Y, Table.sum())
end
This prints, I hope unsurprisingly, “3 2 5”. Let’s go a step further and build a tiny class:
Toy = class()
function Toy:expect(aValue)
self.saved = aValue
function is(anotherValue)
if (anotherValue == self.saved ) then
print("OK")
else
print("Not OK")
end
end
return { is = is }
end
function setup()
Toy:expect(3).is(5)
Toy:expect(4).is(2+2)
end
```
This prints "Not OK" for the 3 is 5, then OK for the 4 is 2+2. Are you troubled by the return table, `{ is = is}`? It could be written this way: `return { ["is"] = is }` instead. The table returned has one element, key "is", value our "is" function. The `is` function is compiled into the class, and therefore understands self. So it can fetch self.saved and use it just fine.
Note, however, that we are sending the message `expect` to Toy, the class, not an instance! That works because to build an instance, Codea Lua just copies the class and returns it as an instance. A class in Lua is a prototype, not a factory.
However, to get an instance we can do this:
~~~lua
Toy = class()
function Toy:expect(aValue)
self.saved = aValue
function is(anotherValue)
if (anotherValue == self.saved ) then
print("OK")
else
print("Not OK")
end
end
return { ["is"] = is }
end
_ = Toy()
function setup()
_:expect(3).is(5)
_:expect(4).is(2+2)
end
Note the _ = Toy()
. That just defines the global variable named _
to be an instance of Toy, and now, voila! We have the syntax that CodeaUnit uses. Now let’s do one more thing. Our version above stores the saved value in the instance. We could instead make it local, like this:
Toy = class()
function Toy:expect(aValue)
local saved = aValue
function is(anotherValue)
if (anotherValue == saved ) then
print("OK")
else
print("Not OK")
end
end
return { ["is"] = is }
end
_ = Toy()
function setup()
_:expect(3).is(5)
_:expect(4).is(2+2)
end
Here we just define a local value, saved
, inside the expect
function definition. That local is of course available everywhere inside expect
and therefore inside is
, which uses it to check for “correctness”. Now is
is not tied to the Toy instance at all: everything is inside the function that expect
defined, and is found by is
in the table that expect
returns.
Wow. To me, this is pretty intricate. The more comfortable you are with anonymous functions and their scoping, the more comfortable you’re likely to be with this. Quite possibly you should ignore the words here, and figure out the code for yourself. Then write your own words and share them: maybe they’ll be better than mine. Certainly they’ll be better for you!
Summing up, then, CodeaUnit defines a class, and saves an instance of the class in _
. In its :expect()
function, it defines various internal functions, like is, isnt, has, throws, before, after
, with variables bound as needed, and returns from expect
a table containing those definitions. Everything else is elaboration. Let’s look at that now.
CodeaUnit
Here’s the code:
CodeaUnit = class()
function CodeaUnit:describe(feature, allTests)
self.tests = 0
self.ignored = 0
self.failures = 0
self._before = function()
end
self._after = function()
end
print(string.format("Feature: %s", feature))
allTests()
local passed = self.tests - self.failures - self.ignored
local summary = string.format("%d Passed, %d Ignored, %d Failed", passed, self.ignored, self.failures)
print(summary)
end
function CodeaUnit:before(setup)
self._before = setup
end
function CodeaUnit:after(teardown)
self._after = teardown
end
function CodeaUnit:ignore(description, scenario)
self.description = tostring(description or "")
self.tests = self.tests + 1
self.ignored = self.ignored + 1
if CodeaUnit.detailed then
print(string.format("%d: %s -- Ignored", self.tests, self.description))
end
end
function CodeaUnit:test(description, scenario)
self.description = tostring(description or "")
self.tests = self.tests + 1
self._before()
local status, err = pcall(scenario)
if err then
self.failures = self.failures + 1
print(string.format("%d: %s -- %s", self.tests, self.description, err))
end
self._after()
end
function CodeaUnit:expect(conditional)
local message = string.format("%d: %s", (self.tests or 1), self.description)
local passed = function()
if CodeaUnit.detailed then
print(string.format("%s -- OK", message))
end
end
local failed = function()
self.failures = self.failures + 1
local actual = tostring(conditional)
local expected = tostring(self.expected)
print(string.format("%s -- Actual: %s, Expected: %s", message, actual, expected))
end
local notify = function(result)
if result then
passed()
else
failed()
end
end
local is = function(expected)
self.expected = expected
notify(conditional == expected)
end
local isnt = function(expected)
self.expected = expected
notify(conditional ~= expected)
end
local has = function(expected)
self.expected = expected
local found = false
for i,v in pairs(conditional) do
if v == expected then
found = true
end
end
notify(found)
end
local throws = function(expected)
self.expected = expected
local status, error = pcall(conditional)
if not error then
conditional = "nothing thrown"
notify(false)
else
notify(string.find(error, expected, 1, true))
end
end
return {
is = is,
isnt = isnt,
has = has,
throws = throws
}
end
CodeaUnit.execute = function()
for i,v in pairs(listProjectTabs()) do
local source = readProjectTab(v)
for match in string.gmatch(source, "function%s-(test.-%(%))") do
-- print("loading", match)
loadstring(match)()
end
end
end
CodeaUnit.detailed = true
_ = CodeaUnit()
parameter.action("CodeaUnit Runner", function()
CodeaUnit.execute()
end)
Let’s begin here:
CodeaUnit.execute = function()
for i,v in pairs(listProjectTabs()) do
local source = readProjectTab(v)
for match in string.gmatch(source, "function%s-(test.-%(%))") do
-- print("loading", match)
loadstring(match)()
end
end
end
-- ...
parameter.action("CodeaUnit Runner", function()
CodeaUnit.execute()
end)
The code just above creates a test button in the Codea parameters area. The button calls execute()
. This starts the ball rolling. The execute
function loops over all project tabs, and looks at the source code for all functions whose names start with “test”, and executes what it finds. What does it find? Well, here’s a trivial test example:
function testTrivial()
CodeaUnit.detailed = true
_:describe("Trivial Test", function()
_:test("Try Doubling X", function()
local x = 2
_:expect(x*x).is(3)
end)
end)
end
CodeaUnit will find and execute testTrivial()
. That function sets CodeaUnit.detailed to true and then calls _:describe
, with the string parameter and a function definition, which in this case if a function which, if called would call _:test
. How does that get called? Let’s look at CodeaUnit describe
.
function CodeaUnit:describe(feature, allTests)
self.tests = 0
self.ignored = 0
self.failures = 0
self._before = function()
end
self._after = function()
end
print(string.format("Feature: %s", feature))
allTests()
local passed = self.tests - self.failures - self.ignored
local summary = string.format("%d Passed, %d Ignored, %d Failed", passed, self.ignored, self.failures)
print(summary)
end
The describe
function expects a feature string, and an allTests parameter which is a function. We see from our example that the function may call _:test
, and from other examples we know that it could call _:before
or _:after
. Anyway, describe
sets up some counts, sets default empty before
and after
functions, prints its message and calls allTests, the function we provided including all our tests. When our allTests function returns, describe
prints the summary information. What about our allTests
? It just calls _:test
, which looks like this:
function CodeaUnit:test(description, scenario)
self.description = tostring(description or "")
self.tests = self.tests + 1
self._before()
local status, err = pcall(scenario)
if err then
self.failures = self.failures + 1
print(string.format("%d: %s -- %s", self.tests, self.description, err))
end
self._after()
end
Well, now we’re getting down to it. We tally the test, run the _before
function, which is in our case the empty default but could have been provided, and calls the scenario function with pcall
, which ensures that errors thrown inside will return rather than stop. Thus the if err
can tally and print any failures. Then we do the _after
, if any.
Remember that our scenario looks like this:
_:test("Try Doubling X", function()
local x = 2
_:expect(x*x).is(3)
end)
Therefore what’s going to happen is that we’re going to run our little function, which will set x and then call _expect
, which looks like this:
function CodeaUnit:expect(conditional)
local message = string.format("%d: %s", (self.tests or 1), self.description)
local passed = function()
if CodeaUnit.detailed then
print(string.format("%s -- OK", message))
end
end
local failed = function()
self.failures = self.failures + 1
local actual = tostring(conditional)
local expected = tostring(self.expected)
print(string.format("%s -- Actual: %s, Expected: %s", message, actual, expected))
end
local notify = function(result)
if result then
passed()
else
failed()
end
end
local is = function(expected)
self.expected = expected
notify(conditional == expected)
end
local isnt = function(expected)
self.expected = expected
notify(conditional ~= expected)
end
local has = function(expected)
self.expected = expected
local found = false
for i,v in pairs(conditional) do
if v == expected then
found = true
end
end
notify(found)
end
local throws = function(expected)
self.expected = expected
local status, error = pcall(conditional)
if not error then
conditional = "nothing thrown"
notify(false)
else
notify(string.find(error, expected, 1, true))
end
end
return {
is = is,
isnt = isnt,
has = has,
throws = throws
}
end
Wow. Well, in our case, conditional
is the value of x*x, which I’d guess to be 4. Then, expect
defines a whole raft of functions, including passed, failed, notify, is, isn’t, and so on, each stored in variables local to this execution of expect
. We then return, from expect
, a small table defining the strings “is”, “isnt”, “has”, “throws” as those functions. So next, when we call .is(3), we’ll execute our specially bound version of is
that’s connected to our expected value, our notify, and so on.
Is this intricate? Yes, it bloody well is. But a testing framework like this generally is intricate, because each test has to be uniquely bound to its input values and expected values, which have to permeate through all the notify functions and so on. Anyway, we’re executing our little function that calls expect
and then is
. So our is
now checks conditional == expected
, which is resolved to notify(3 == 4)
and since four isn’t three, we call our previously bound failed
function, which tallies the failure and prints the bad news.