commit 24032a29a2aff5cb64e57203afe2e765194024e5
parent daf2ea4ac2e21dc28319309ae11c8c783fb65c1b
Author: Hugo Soucy <hugo@soucy.cc>
Date: Sun, 26 Sep 2021 10:37:22 -0400
Start a new functionality to create pages for tags
Diffstat:
9 files changed, 2181 insertions(+), 0 deletions(-)
diff --git a/archetypes/tag.lua.mustache b/archetypes/tag.lua.mustache
@@ -0,0 +1,7 @@
+return {
+title = "{{ title }}",
+url = "{{{ url }}}",
+date = "{{ date }}",
+datetime = "{{ datetime }}",
+posttype = "",
+}
diff --git a/archetypes/tag.md.mustache b/archetypes/tag.md.mustache
@@ -0,0 +1 @@
+{{{ title }}}
diff --git a/bin/tags b/bin/tags
@@ -0,0 +1,54 @@
+#!/usr/bin/env lua
+do
+ --
+ package.path = package.path .. ';'.. arg[0]:match("(.*/)") ..'/?.lua'
+ --
+ local config = require 'config'
+ local dirtree = require 'utils.dirtree'
+ local file = require 'utils.file'
+ local inspect = require 'inspect'
+ local lfs = require 'lfs'
+ local lume = require 'utils.lume.lume'
+ local lustache = require 'lustache'
+
+ local tagstable = {}
+
+ -- Insert all the keywords as key of an empty subtable
+ for filepath in dirtree.get('content/') do
+ if file.is_markdown(filepath) or file.is_html(filepath) then
+ local metafile = file.get_metafile(filepath)
+
+ if metafile.keywords ~= nil then
+ for _, keyword in pairs(file.get_metafile(filepath).keywords) do
+ if keyword ~= '' then
+ tagstable[keyword] = {}
+ end
+ end
+ end
+ end
+ end
+
+ -- After insert relative links in each keyword's table
+ for filepath in dirtree.get('content/') do
+ if file.is_markdown(filepath) or file.is_html(filepath) then
+ local metafile = file.get_metafile(filepath)
+
+ if metafile.keywords ~= nil then
+ for _, keyword in pairs(file.get_metafile(filepath).keywords) do
+ if tagstable[keyword] then
+ local tagtable = {
+ title = metafile.title,
+ rellink = file.get_rellink(filepath, config.paths.content)
+ }
+
+ table.insert(tagstable[keyword], tagtable)
+ end
+ end
+ end
+ end
+ end
+
+ --print(inspect(tagstable))
+
+ return print(inspect(tagstable))
+end
diff --git a/bin/utils/dirtree.lua b/bin/utils/dirtree.lua
@@ -0,0 +1,34 @@
+-- @module dirtree
+local dirtree = {}
+--
+local lfs = require 'lfs' -- luafilesystem
+
+function dirtree.get(dir)
+ assert(dir and dir ~= '', 'directory parameter is missing or empty')
+
+ -- Removes slash if is one
+ if string.sub(dir, -1) == '/' then
+ dir = string.sub(dir, 1, -2)
+ end
+
+ -- Main function of the coroutine (recursive)
+ local function yieldtree(dir)
+ for entry in lfs.dir(dir) do
+ if entry ~= '.' and entry ~= '..' then
+ entry = dir..'/'..entry
+
+ local attr = lfs.attributes(entry)
+
+ coroutine.yield(entry,dir,attr)
+
+ if attr.mode == 'directory' then
+ yieldtree(entry)
+ end
+ end
+ end
+ end
+
+ return coroutine.wrap(function() yieldtree(dir) end)
+end
+
+return dirtree
diff --git a/bin/utils/lume/LICENSE b/bin/utils/lume/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2020 rxi
+
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/bin/utils/lume/README.md b/bin/utils/lume/README.md
@@ -0,0 +1,495 @@
+# Lume
+
+A collection of functions for Lua, geared towards game development.
+
+
+## Installation
+
+The [lume.lua](lume.lua?raw=1) file should be dropped into an existing project
+and required by it:
+
+```lua
+lume = require "lume"
+```
+
+
+## Function Reference
+
+#### lume.clamp(x, min, max)
+Returns the number `x` clamped between the numbers `min` and `max`
+
+#### lume.round(x [, increment])
+Rounds `x` to the nearest integer; rounds away from zero if we're midway
+between two integers. If `increment` is set then the number is rounded to the
+nearest increment.
+```lua
+lume.round(2.3) -- Returns 2
+lume.round(123.4567, .1) -- Returns 123.5
+```
+
+#### lume.sign(x)
+Returns `1` if `x` is 0 or above, returns `-1` when `x` is negative.
+
+#### lume.lerp(a, b, amount)
+Returns the linearly interpolated number between `a` and `b`, `amount` should
+be in the range of 0 - 1; if `amount` is outside of this range it is clamped.
+```lua
+lume.lerp(100, 200, .5) -- Returns 150
+```
+
+#### lume.smooth(a, b, amount)
+Similar to `lume.lerp()` but uses cubic interpolation instead of linear
+interpolation.
+
+#### lume.pingpong(x)
+Ping-pongs the number `x` between 0 and 1.
+
+#### lume.distance(x1, y1, x2, y2 [, squared])
+Returns the distance between the two points. If `squared` is true then the
+squared distance is returned -- this is faster to calculate and can still be
+used when comparing distances.
+
+#### lume.angle(x1, y1, x2, y2)
+Returns the angle between the two points.
+
+#### lume.vector(angle, magnitude)
+Given an `angle` and `magnitude`, returns a vector.
+```lua
+local x, y = lume.vector(0, 10) -- Returns 10, 0
+```
+
+#### lume.random([a [, b]])
+Returns a random number between `a` and `b`. If only `a` is supplied a number
+between `0` and `a` is returned. If no arguments are supplied a random number
+between `0` and `1` is returned.
+
+#### lume.randomchoice(t)
+Returns a random value from array `t`. If the array is empty an error is
+raised.
+```lua
+lume.randomchoice({true, false}) -- Returns either true or false
+```
+
+#### lume.weightedchoice(t)
+Takes the argument table `t` where the keys are the possible choices and the
+value is the choice's weight. A weight should be 0 or above, the larger the
+number the higher the probability of that choice being picked. If the table is
+empty, a weight is below zero or all the weights are 0 then an error is raised.
+```lua
+lume.weightedchoice({ ["cat"] = 10, ["dog"] = 5, ["frog"] = 0 })
+-- Returns either "cat" or "dog" with "cat" being twice as likely to be chosen.
+```
+
+#### lume.isarray(x)
+Returns `true` if `x` is an array -- the value is assumed to be an array if it
+is a table which contains a value at the index `1`. This function is used
+internally and can be overridden if you wish to use a different method to detect
+arrays.
+
+
+#### lume.push(t, ...)
+Pushes all the given values to the end of the table `t` and returns the pushed
+values. Nil values are ignored.
+```lua
+local t = { 1, 2, 3 }
+lume.push(t, 4, 5) -- `t` becomes { 1, 2, 3, 4, 5 }
+```
+
+#### lume.remove(t, x)
+Removes the first instance of the value `x` if it exists in the table `t`.
+Returns `x`.
+```lua
+local t = { 1, 2, 3 }
+lume.remove(t, 2) -- `t` becomes { 1, 3 }
+```
+
+#### lume.clear(t)
+Nils all the values in the table `t`, this renders the table empty. Returns
+`t`.
+```lua
+local t = { 1, 2, 3 }
+lume.clear(t) -- `t` becomes {}
+```
+
+#### lume.extend(t, ...)
+Copies all the fields from the source tables to the table `t` and returns `t`.
+If a key exists in multiple tables the right-most table's value is used.
+```lua
+local t = { a = 1, b = 2 }
+lume.extend(t, { b = 4, c = 6 }) -- `t` becomes { a = 1, b = 4, c = 6 }
+```
+
+#### lume.shuffle(t)
+Returns a shuffled copy of the array `t`.
+
+#### lume.sort(t [, comp])
+Returns a copy of the array `t` with all its items sorted. If `comp` is a
+function it will be used to compare the items when sorting. If `comp` is a
+string it will be used as the key to sort the items by.
+```lua
+lume.sort({ 1, 4, 3, 2, 5 }) -- Returns { 1, 2, 3, 4, 5 }
+lume.sort({ {z=2}, {z=3}, {z=1} }, "z") -- Returns { {z=1}, {z=2}, {z=3} }
+lume.sort({ 1, 3, 2 }, function(a, b) return a > b end) -- Returns { 3, 2, 1 }
+```
+
+#### lume.array(...)
+Iterates the supplied iterator and returns an array filled with the values.
+```lua
+lume.array(string.gmatch("Hello world", "%a+")) -- Returns {"Hello", "world"}
+```
+
+#### lume.each(t, fn, ...)
+Iterates the table `t` and calls the function `fn` on each value followed by
+the supplied additional arguments; if `fn` is a string the method of that name
+is called for each value. The function returns `t` unmodified.
+```lua
+lume.each({1, 2, 3}, print) -- Prints "1", "2", "3" on separate lines
+lume.each({a, b, c}, "move", 10, 20) -- Does x:move(10, 20) on each value
+```
+
+#### lume.map(t, fn)
+Applies the function `fn` to each value in table `t` and returns a new table
+with the resulting values.
+```lua
+lume.map({1, 2, 3}, function(x) return x * 2 end) -- Returns {2, 4, 6}
+```
+
+#### lume.all(t [, fn])
+Returns true if all the values in `t` table are true. If a `fn` function is
+supplied it is called on each value, true is returned if all of the calls to
+`fn` return true.
+```lua
+lume.all({1, 2, 1}, function(x) return x == 1 end) -- Returns false
+```
+
+#### lume.any(t [, fn])
+Returns true if any of the values in `t` table are true. If a `fn` function is
+supplied it is called on each value, true is returned if any of the calls to
+`fn` return true.
+```lua
+lume.any({1, 2, 1}, function(x) return x == 1 end) -- Returns true
+```
+
+#### lume.reduce(t, fn [, first])
+Applies `fn` on two arguments cumulative to the items of the array `t`, from
+left to right, so as to reduce the array to a single value. If a `first` value
+is specified the accumulator is initialised to this, otherwise the first value
+in the array is used. If the array is empty and no `first` value is specified
+an error is raised.
+```lua
+lume.reduce({1, 2, 3}, function(a, b) return a + b end) -- Returns 6
+```
+
+#### lume.unique(t)
+Returns a copy of the `t` array with all the duplicate values removed.
+```lua
+lume.unique({2, 1, 2, "cat", "cat"}) -- Returns {1, 2, "cat"}
+```
+
+#### lume.filter(t, fn [, retainkeys])
+Calls `fn` on each value of `t` table. Returns a new table with only the values
+where `fn` returned true. If `retainkeys` is true the table is not treated as
+an array and retains its original keys.
+```lua
+lume.filter({1, 2, 3, 4}, function(x) return x % 2 == 0 end) -- Returns {2, 4}
+```
+
+#### lume.reject(t, fn [, retainkeys])
+The opposite of `lume.filter()`: Calls `fn` on each value of `t` table; returns
+a new table with only the values where `fn` returned false. If `retainkeys` is
+true the table is not treated as an array and retains its original keys.
+```lua
+lume.reject({1, 2, 3, 4}, function(x) return x % 2 == 0 end) -- Returns {1, 3}
+```
+
+#### lume.merge(...)
+Returns a new table with all the given tables merged together. If a key exists
+in multiple tables the right-most table's value is used.
+```lua
+lume.merge({a=1, b=2, c=3}, {c=8, d=9}) -- Returns {a=1, b=2, c=8, d=9}
+```
+
+#### lume.concat(...)
+Returns a new array consisting of all the given arrays concatenated into one.
+```lua
+lume.concat({1, 2}, {3, 4}, {5, 6}) -- Returns {1, 2, 3, 4, 5, 6}
+```
+
+#### lume.find(t, value)
+Returns the index/key of `value` in `t`. Returns `nil` if that value does not
+exist in the table.
+```lua
+lume.find({"a", "b", "c"}, "b") -- Returns 2
+```
+
+#### lume.match(t, fn)
+Returns the value and key of the value in table `t` which returns true when
+`fn` is called on it. Returns `nil` if no such value exists.
+```lua
+lume.match({1, 5, 8, 7}, function(x) return x % 2 == 0 end) -- Returns 8, 3
+```
+
+#### lume.count(t [, fn])
+Counts the number of values in the table `t`. If a `fn` function is supplied it
+is called on each value, the number of times it returns true is counted.
+```lua
+lume.count({a = 2, b = 3, c = 4, d = 5}) -- Returns 4
+lume.count({1, 2, 4, 6}, function(x) return x % 2 == 0 end) -- Returns 3
+```
+
+#### lume.slice(t [, i [, j]])
+Mimics the behaviour of Lua's `string.sub`, but operates on an array rather
+than a string. Creates and returns a new array of the given slice.
+```lua
+lume.slice({"a", "b", "c", "d", "e"}, 2, 4) -- Returns {"b", "c", "d"}
+```
+
+#### lume.first(t [, n])
+Returns the first element of an array or nil if the array is empty. If `n` is
+specificed an array of the first `n` elements is returned.
+```lua
+lume.first({"a", "b", "c"}) -- Returns "a"
+```
+
+#### lume.last(t [, n])
+Returns the last element of an array or nil if the array is empty. If `n` is
+specificed an array of the last `n` elements is returned.
+```lua
+lume.last({"a", "b", "c"}) -- Returns "c"
+```
+
+#### lume.invert(t)
+Returns a copy of the table where the keys have become the values and the
+values the keys.
+```lua
+lume.invert({a = "x", b = "y"}) -- returns {x = "a", y = "b"}
+```
+
+#### lume.pick(t, ...)
+Returns a copy of the table filtered to only contain values for the given keys.
+```lua
+lume.pick({ a = 1, b = 2, c = 3 }, "a", "c") -- Returns { a = 1, c = 3 }
+```
+
+#### lume.keys(t)
+Returns an array containing each key of the table.
+
+#### lume.clone(t)
+Returns a shallow copy of the table `t`.
+
+#### lume.fn(fn, ...)
+Creates a wrapper function around function `fn`, automatically inserting the
+arguments into `fn` which will persist every time the wrapper is called. Any
+arguments which are passed to the returned function will be inserted after the
+already existing arguments passed to `fn`.
+```lua
+local f = lume.fn(print, "Hello")
+f("world") -- Prints "Hello world"
+```
+
+#### lume.once(fn, ...)
+Returns a wrapper function to `fn` which takes the supplied arguments. The
+wrapper function will call `fn` on the first call and do nothing on any
+subsequent calls.
+```lua
+local f = lume.once(print, "Hello")
+f() -- Prints "Hello"
+f() -- Does nothing
+```
+
+#### lume.memoize(fn)
+Returns a wrapper function to `fn` where the results for any given set of
+arguments are cached. `lume.memoize()` is useful when used on functions with
+slow-running computations.
+```lua
+fib = lume.memoize(function(n) return n < 2 and n or fib(n-1) + fib(n-2) end)
+```
+
+#### lume.combine(...)
+Creates a wrapper function which calls each supplied argument in the order they
+were passed to `lume.combine()`; nil arguments are ignored. The wrapper
+function passes its own arguments to each of its wrapped functions when it is
+called.
+```lua
+local f = lume.combine(function(a, b) print(a + b) end,
+ function(a, b) print(a * b) end)
+f(3, 4) -- Prints "7" then "12" on a new line
+```
+
+#### lume.call(fn, ...)
+Calls the given function with the provided arguments and returns its values. If
+`fn` is `nil` then no action is performed and the function returns `nil`.
+```lua
+lume.call(print, "Hello world") -- Prints "Hello world"
+```
+
+#### lume.time(fn, ...)
+Inserts the arguments into function `fn` and calls it. Returns the time in
+seconds the function `fn` took to execute followed by `fn`'s returned values.
+```lua
+lume.time(function(x) return x end, "hello") -- Returns 0, "hello"
+```
+
+#### lume.lambda(str)
+Takes a string lambda and returns a function. `str` should be a list of
+comma-separated parameters, followed by `->`, followed by the expression which
+will be evaluated and returned.
+```lua
+local f = lume.lambda "x,y -> 2*x+y"
+f(10, 5) -- Returns 25
+```
+
+#### lume.serialize(x)
+Serializes the argument `x` into a string which can be loaded again using
+`lume.deserialize()`. Only booleans, numbers, tables and strings can be
+serialized. Circular references will result in an error; all nested tables are
+serialized as unique tables.
+```lua
+lume.serialize({a = "test", b = {1, 2, 3}, false})
+-- Returns "{[1]=false,["a"]="test",["b"]={[1]=1,[2]=2,[3]=3,},}"
+```
+
+#### lume.deserialize(str)
+Deserializes a string created by `lume.serialize()` and returns the resulting
+value. This function should not be run on an untrusted string.
+```lua
+lume.deserialize("{1, 2, 3}") -- Returns {1, 2, 3}
+```
+
+#### lume.split(str [, sep])
+Returns an array of the words in the string `str`. If `sep` is provided it is
+used as the delimiter, consecutive delimiters are not grouped together and will
+delimit empty strings.
+```lua
+lume.split("One two three") -- Returns {"One", "two", "three"}
+lume.split("a,b,,c", ",") -- Returns {"a", "b", "", "c"}
+```
+
+#### lume.trim(str [, chars])
+Trims the whitespace from the start and end of the string `str` and returns the
+new string. If a `chars` value is set the characters in `chars` are trimmed
+instead of whitespace.
+```lua
+lume.trim(" Hello ") -- Returns "Hello"
+```
+
+#### lume.wordwrap(str [, limit])
+Returns `str` wrapped to `limit` number of characters per line, by default
+`limit` is `72`. `limit` can also be a function which when passed a string,
+returns `true` if it is too long for a single line.
+```lua
+-- Returns "Hello world\nThis is a\nshort string"
+lume.wordwrap("Hello world. This is a short string", 14)
+```
+
+#### lume.format(str [, vars])
+Returns a formatted string. The values of keys in the table `vars` can be
+inserted into the string by using the form `"{key}"` in `str`; numerical keys
+can also be used.
+```lua
+lume.format("{b} hi {a}", {a = "mark", b = "Oh"}) -- Returns "Oh hi mark"
+lume.format("Hello {1}!", {"world"}) -- Returns "Hello world!"
+```
+
+#### lume.trace(...)
+Prints the current filename and line number followed by each argument separated
+by a space.
+```lua
+-- Assuming the file is called "example.lua" and the next line is 12:
+lume.trace("hello", 1234) -- Prints "example.lua:12: hello 1234"
+```
+
+#### lume.dostring(str)
+Executes the lua code inside `str`.
+```lua
+lume.dostring("print('Hello!')") -- Prints "Hello!"
+```
+
+#### lume.uuid()
+Generates a random UUID string; version 4 as specified in
+[RFC 4122](http://www.ietf.org/rfc/rfc4122.txt).
+
+#### lume.hotswap(modname)
+Reloads an already loaded module in place, allowing you to immediately see the
+effects of code changes without having to restart the program. `modname` should
+be the same string used when loading the module with require(). In the case of
+an error the global environment is restored and `nil` plus an error message is
+returned.
+```lua
+lume.hotswap("lume") -- Reloads the lume module
+assert(lume.hotswap("inexistant_module")) -- Raises an error
+```
+
+#### lume.ripairs(t)
+Performs the same function as `ipairs()` but iterates in reverse; this allows
+the removal of items from the table during iteration without any items being
+skipped.
+```lua
+-- Prints "3->c", "2->b" and "1->a" on separate lines
+for i, v in lume.ripairs({ "a", "b", "c" }) do
+ print(i .. "->" .. v)
+end
+```
+
+#### lume.color(str [, mul])
+Takes color string `str` and returns 4 values, one for each color channel (`r`,
+`g`, `b` and `a`). By default the returned values are between 0 and 1; the
+values are multiplied by the number `mul` if it is provided.
+```lua
+lume.color("#ff0000") -- Returns 1, 0, 0, 1
+lume.color("rgba(255, 0, 255, .5)") -- Returns 1, 0, 1, .5
+lume.color("#00ffff", 256) -- Returns 0, 256, 256, 256
+lume.color("rgb(255, 0, 0)", 256) -- Returns 256, 0, 0, 256
+```
+
+#### lume.chain(value)
+Returns a wrapped object which allows chaining of lume functions. The function
+result() should be called at the end of the chain to return the resulting
+value.
+```lua
+lume.chain({1, 2, 3, 4})
+ :filter(function(x) return x % 2 == 0 end)
+ :map(function(x) return -x end)
+ :result() -- Returns { -2, -4 }
+```
+The table returned by the `lume` module, when called, acts in the same manner
+as calling `lume.chain()`.
+```lua
+lume({1, 2, 3}):each(print) -- Prints 1, 2 then 3 on separate lines
+```
+
+## Iteratee functions
+Several lume functions allow a `table`, `string` or `nil` to be used in place
+of their iteratee function argument. The functions that provide this behaviour
+are: `map()`, `all()`, `any()`, `filter()`, `reject()`, `match()` and
+`count()`.
+
+If the argument is `nil` then each value will return itself.
+```lua
+lume.filter({ true, true, false, true }, nil) -- { true, true, true }
+```
+
+If the argument is a `string` then each value will be assumed to be a table,
+and will return the value of the key which matches the string.
+``` lua
+local t = {{ z = "cat" }, { z = "dog" }, { z = "owl" }}
+lume.map(t, "z") -- Returns { "cat", "dog", "owl" }
+```
+
+If the argument is a `table` then each value will return `true` or `false`,
+depending on whether the values at each of the table's keys match the
+collection's value's values.
+```lua
+local t = {
+ { age = 10, type = "cat" },
+ { age = 8, type = "dog" },
+ { age = 10, type = "owl" },
+}
+lume.count(t, { age = 10 }) -- returns 2
+```
+
+
+## License
+
+This library is free software; you can redistribute it and/or modify it under
+the terms of the MIT license. See [LICENSE](LICENSE) for details.
diff --git a/bin/utils/lume/lume.lua b/bin/utils/lume/lume.lua
@@ -0,0 +1,780 @@
+--
+-- lume
+--
+-- Copyright (c) 2020 rxi
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a copy of
+-- this software and associated documentation files (the "Software"), to deal in
+-- the Software without restriction, including without limitation the rights to
+-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+-- of the Software, and to permit persons to whom the Software is furnished to do
+-- so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in all
+-- copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+-- SOFTWARE.
+--
+
+local lume = { _version = "2.3.0" }
+
+local pairs, ipairs = pairs, ipairs
+local type, assert, unpack = type, assert, unpack or table.unpack
+local tostring, tonumber = tostring, tonumber
+local math_floor = math.floor
+local math_ceil = math.ceil
+local math_atan2 = math.atan2 or math.atan
+local math_sqrt = math.sqrt
+local math_abs = math.abs
+
+local noop = function()
+end
+
+local identity = function(x)
+ return x
+end
+
+local patternescape = function(str)
+ return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1")
+end
+
+local absindex = function(len, i)
+ return i < 0 and (len + i + 1) or i
+end
+
+local iscallable = function(x)
+ if type(x) == "function" then return true end
+ local mt = getmetatable(x)
+ return mt and mt.__call ~= nil
+end
+
+local getiter = function(x)
+ if lume.isarray(x) then
+ return ipairs
+ elseif type(x) == "table" then
+ return pairs
+ end
+ error("expected table", 3)
+end
+
+local iteratee = function(x)
+ if x == nil then return identity end
+ if iscallable(x) then return x end
+ if type(x) == "table" then
+ return function(z)
+ for k, v in pairs(x) do
+ if z[k] ~= v then return false end
+ end
+ return true
+ end
+ end
+ return function(z) return z[x] end
+end
+
+
+
+function lume.clamp(x, min, max)
+ return x < min and min or (x > max and max or x)
+end
+
+
+function lume.round(x, increment)
+ if increment then return lume.round(x / increment) * increment end
+ return x >= 0 and math_floor(x + .5) or math_ceil(x - .5)
+end
+
+
+function lume.sign(x)
+ return x < 0 and -1 or 1
+end
+
+
+function lume.lerp(a, b, amount)
+ return a + (b - a) * lume.clamp(amount, 0, 1)
+end
+
+
+function lume.smooth(a, b, amount)
+ local t = lume.clamp(amount, 0, 1)
+ local m = t * t * (3 - 2 * t)
+ return a + (b - a) * m
+end
+
+
+function lume.pingpong(x)
+ return 1 - math_abs(1 - x % 2)
+end
+
+
+function lume.distance(x1, y1, x2, y2, squared)
+ local dx = x1 - x2
+ local dy = y1 - y2
+ local s = dx * dx + dy * dy
+ return squared and s or math_sqrt(s)
+end
+
+
+function lume.angle(x1, y1, x2, y2)
+ return math_atan2(y2 - y1, x2 - x1)
+end
+
+
+function lume.vector(angle, magnitude)
+ return math.cos(angle) * magnitude, math.sin(angle) * magnitude
+end
+
+
+function lume.random(a, b)
+ if not a then a, b = 0, 1 end
+ if not b then b = 0 end
+ return a + math.random() * (b - a)
+end
+
+
+function lume.randomchoice(t)
+ return t[math.random(#t)]
+end
+
+
+function lume.weightedchoice(t)
+ local sum = 0
+ for _, v in pairs(t) do
+ assert(v >= 0, "weight value less than zero")
+ sum = sum + v
+ end
+ assert(sum ~= 0, "all weights are zero")
+ local rnd = lume.random(sum)
+ for k, v in pairs(t) do
+ if rnd < v then return k end
+ rnd = rnd - v
+ end
+end
+
+
+function lume.isarray(x)
+ return type(x) == "table" and x[1] ~= nil
+end
+
+
+function lume.push(t, ...)
+ local n = select("#", ...)
+ for i = 1, n do
+ t[#t + 1] = select(i, ...)
+ end
+ return ...
+end
+
+
+function lume.remove(t, x)
+ local iter = getiter(t)
+ for i, v in iter(t) do
+ if v == x then
+ if lume.isarray(t) then
+ table.remove(t, i)
+ break
+ else
+ t[i] = nil
+ break
+ end
+ end
+ end
+ return x
+end
+
+
+function lume.clear(t)
+ local iter = getiter(t)
+ for k in iter(t) do
+ t[k] = nil
+ end
+ return t
+end
+
+
+function lume.extend(t, ...)
+ for i = 1, select("#", ...) do
+ local x = select(i, ...)
+ if x then
+ for k, v in pairs(x) do
+ t[k] = v
+ end
+ end
+ end
+ return t
+end
+
+
+function lume.shuffle(t)
+ local rtn = {}
+ for i = 1, #t do
+ local r = math.random(i)
+ if r ~= i then
+ rtn[i] = rtn[r]
+ end
+ rtn[r] = t[i]
+ end
+ return rtn
+end
+
+
+function lume.sort(t, comp)
+ local rtn = lume.clone(t)
+ if comp then
+ if type(comp) == "string" then
+ table.sort(rtn, function(a, b) return a[comp] < b[comp] end)
+ else
+ table.sort(rtn, comp)
+ end
+ else
+ table.sort(rtn)
+ end
+ return rtn
+end
+
+
+function lume.array(...)
+ local t = {}
+ for x in ... do t[#t + 1] = x end
+ return t
+end
+
+
+function lume.each(t, fn, ...)
+ local iter = getiter(t)
+ if type(fn) == "string" then
+ for _, v in iter(t) do v[fn](v, ...) end
+ else
+ for _, v in iter(t) do fn(v, ...) end
+ end
+ return t
+end
+
+
+function lume.map(t, fn)
+ fn = iteratee(fn)
+ local iter = getiter(t)
+ local rtn = {}
+ for k, v in iter(t) do rtn[k] = fn(v) end
+ return rtn
+end
+
+
+function lume.all(t, fn)
+ fn = iteratee(fn)
+ local iter = getiter(t)
+ for _, v in iter(t) do
+ if not fn(v) then return false end
+ end
+ return true
+end
+
+
+function lume.any(t, fn)
+ fn = iteratee(fn)
+ local iter = getiter(t)
+ for _, v in iter(t) do
+ if fn(v) then return true end
+ end
+ return false
+end
+
+
+function lume.reduce(t, fn, first)
+ local started = first ~= nil
+ local acc = first
+ local iter = getiter(t)
+ for _, v in iter(t) do
+ if started then
+ acc = fn(acc, v)
+ else
+ acc = v
+ started = true
+ end
+ end
+ assert(started, "reduce of an empty table with no first value")
+ return acc
+end
+
+
+function lume.unique(t)
+ local rtn = {}
+ for k in pairs(lume.invert(t)) do
+ rtn[#rtn + 1] = k
+ end
+ return rtn
+end
+
+
+function lume.filter(t, fn, retainkeys)
+ fn = iteratee(fn)
+ local iter = getiter(t)
+ local rtn = {}
+ if retainkeys then
+ for k, v in iter(t) do
+ if fn(v) then rtn[k] = v end
+ end
+ else
+ for _, v in iter(t) do
+ if fn(v) then rtn[#rtn + 1] = v end
+ end
+ end
+ return rtn
+end
+
+
+function lume.reject(t, fn, retainkeys)
+ fn = iteratee(fn)
+ local iter = getiter(t)
+ local rtn = {}
+ if retainkeys then
+ for k, v in iter(t) do
+ if not fn(v) then rtn[k] = v end
+ end
+ else
+ for _, v in iter(t) do
+ if not fn(v) then rtn[#rtn + 1] = v end
+ end
+ end
+ return rtn
+end
+
+
+function lume.merge(...)
+ local rtn = {}
+ for i = 1, select("#", ...) do
+ local t = select(i, ...)
+ local iter = getiter(t)
+ for k, v in iter(t) do
+ rtn[k] = v
+ end
+ end
+ return rtn
+end
+
+
+function lume.concat(...)
+ local rtn = {}
+ for i = 1, select("#", ...) do
+ local t = select(i, ...)
+ if t ~= nil then
+ local iter = getiter(t)
+ for _, v in iter(t) do
+ rtn[#rtn + 1] = v
+ end
+ end
+ end
+ return rtn
+end
+
+
+function lume.find(t, value)
+ local iter = getiter(t)
+ for k, v in iter(t) do
+ if v == value then return k end
+ end
+ return nil
+end
+
+
+function lume.match(t, fn)
+ fn = iteratee(fn)
+ local iter = getiter(t)
+ for k, v in iter(t) do
+ if fn(v) then return v, k end
+ end
+ return nil
+end
+
+
+function lume.count(t, fn)
+ local count = 0
+ local iter = getiter(t)
+ if fn then
+ fn = iteratee(fn)
+ for _, v in iter(t) do
+ if fn(v) then count = count + 1 end
+ end
+ else
+ if lume.isarray(t) then
+ return #t
+ end
+ for _ in iter(t) do count = count + 1 end
+ end
+ return count
+end
+
+
+function lume.slice(t, i, j)
+ i = i and absindex(#t, i) or 1
+ j = j and absindex(#t, j) or #t
+ local rtn = {}
+ for x = i < 1 and 1 or i, j > #t and #t or j do
+ rtn[#rtn + 1] = t[x]
+ end
+ return rtn
+end
+
+
+function lume.first(t, n)
+ if not n then return t[1] end
+ return lume.slice(t, 1, n)
+end
+
+
+function lume.last(t, n)
+ if not n then return t[#t] end
+ return lume.slice(t, -n, -1)
+end
+
+
+function lume.invert(t)
+ local rtn = {}
+ for k, v in pairs(t) do rtn[v] = k end
+ return rtn
+end
+
+
+function lume.pick(t, ...)
+ local rtn = {}
+ for i = 1, select("#", ...) do
+ local k = select(i, ...)
+ rtn[k] = t[k]
+ end
+ return rtn
+end
+
+
+function lume.keys(t)
+ local rtn = {}
+ local iter = getiter(t)
+ for k in iter(t) do rtn[#rtn + 1] = k end
+ return rtn
+end
+
+
+function lume.clone(t)
+ local rtn = {}
+ for k, v in pairs(t) do rtn[k] = v end
+ return rtn
+end
+
+
+function lume.fn(fn, ...)
+ assert(iscallable(fn), "expected a function as the first argument")
+ local args = { ... }
+ return function(...)
+ local a = lume.concat(args, { ... })
+ return fn(unpack(a))
+ end
+end
+
+
+function lume.once(fn, ...)
+ local f = lume.fn(fn, ...)
+ local done = false
+ return function(...)
+ if done then return end
+ done = true
+ return f(...)
+ end
+end
+
+
+local memoize_fnkey = {}
+local memoize_nil = {}
+
+function lume.memoize(fn)
+ local cache = {}
+ return function(...)
+ local c = cache
+ for i = 1, select("#", ...) do
+ local a = select(i, ...) or memoize_nil
+ c[a] = c[a] or {}
+ c = c[a]
+ end
+ c[memoize_fnkey] = c[memoize_fnkey] or {fn(...)}
+ return unpack(c[memoize_fnkey])
+ end
+end
+
+
+function lume.combine(...)
+ local n = select('#', ...)
+ if n == 0 then return noop end
+ if n == 1 then
+ local fn = select(1, ...)
+ if not fn then return noop end
+ assert(iscallable(fn), "expected a function or nil")
+ return fn
+ end
+ local funcs = {}
+ for i = 1, n do
+ local fn = select(i, ...)
+ if fn ~= nil then
+ assert(iscallable(fn), "expected a function or nil")
+ funcs[#funcs + 1] = fn
+ end
+ end
+ return function(...)
+ for _, f in ipairs(funcs) do f(...) end
+ end
+end
+
+
+function lume.call(fn, ...)
+ if fn then
+ return fn(...)
+ end
+end
+
+
+function lume.time(fn, ...)
+ local start = os.clock()
+ local rtn = {fn(...)}
+ return (os.clock() - start), unpack(rtn)
+end
+
+
+local lambda_cache = {}
+
+function lume.lambda(str)
+ if not lambda_cache[str] then
+ local args, body = str:match([[^([%w,_ ]-)%->(.-)$]])
+ assert(args and body, "bad string lambda")
+ local s = "return function(" .. args .. ")\nreturn " .. body .. "\nend"
+ lambda_cache[str] = lume.dostring(s)
+ end
+ return lambda_cache[str]
+end
+
+
+local serialize
+
+local serialize_map = {
+ [ "boolean" ] = tostring,
+ [ "nil" ] = tostring,
+ [ "string" ] = function(v) return string.format("%q", v) end,
+ [ "number" ] = function(v)
+ if v ~= v then return "0/0" -- nan
+ elseif v == 1 / 0 then return "1/0" -- inf
+ elseif v == -1 / 0 then return "-1/0" end -- -inf
+ return tostring(v)
+ end,
+ [ "table" ] = function(t, stk)
+ stk = stk or {}
+ if stk[t] then error("circular reference") end
+ local rtn = {}
+ stk[t] = true
+ for k, v in pairs(t) do
+ rtn[#rtn + 1] = "[" .. serialize(k, stk) .. "]=" .. serialize(v, stk)
+ end
+ stk[t] = nil
+ return "{" .. table.concat(rtn, ",") .. "}"
+ end
+}
+
+setmetatable(serialize_map, {
+ __index = function(_, k) error("unsupported serialize type: " .. k) end
+})
+
+serialize = function(x, stk)
+ return serialize_map[type(x)](x, stk)
+end
+
+function lume.serialize(x)
+ return serialize(x)
+end
+
+
+function lume.deserialize(str)
+ return lume.dostring("return " .. str)
+end
+
+
+function lume.split(str, sep)
+ if not sep then
+ return lume.array(str:gmatch("([%S]+)"))
+ else
+ assert(sep ~= "", "empty separator")
+ local psep = patternescape(sep)
+ return lume.array((str..sep):gmatch("(.-)("..psep..")"))
+ end
+end
+
+
+function lume.trim(str, chars)
+ if not chars then return str:match("^[%s]*(.-)[%s]*$") end
+ chars = patternescape(chars)
+ return str:match("^[" .. chars .. "]*(.-)[" .. chars .. "]*$")
+end
+
+
+function lume.wordwrap(str, limit)
+ limit = limit or 72
+ local check
+ if type(limit) == "number" then
+ check = function(s) return #s >= limit end
+ else
+ check = limit
+ end
+ local rtn = {}
+ local line = ""
+ for word, spaces in str:gmatch("(%S+)(%s*)") do
+ local s = line .. word
+ if check(s) then
+ table.insert(rtn, line .. "\n")
+ line = word
+ else
+ line = s
+ end
+ for c in spaces:gmatch(".") do
+ if c == "\n" then
+ table.insert(rtn, line .. "\n")
+ line = ""
+ else
+ line = line .. c
+ end
+ end
+ end
+ table.insert(rtn, line)
+ return table.concat(rtn)
+end
+
+
+function lume.format(str, vars)
+ if not vars then return str end
+ local f = function(x)
+ return tostring(vars[x] or vars[tonumber(x)] or "{" .. x .. "}")
+ end
+ return (str:gsub("{(.-)}", f))
+end
+
+
+function lume.trace(...)
+ local info = debug.getinfo(2, "Sl")
+ local t = { info.short_src .. ":" .. info.currentline .. ":" }
+ for i = 1, select("#", ...) do
+ local x = select(i, ...)
+ if type(x) == "number" then
+ x = string.format("%g", lume.round(x, .01))
+ end
+ t[#t + 1] = tostring(x)
+ end
+ print(table.concat(t, " "))
+end
+
+
+function lume.dostring(str)
+ return assert((loadstring or load)(str))()
+end
+
+
+function lume.uuid()
+ local fn = function(x)
+ local r = math.random(16) - 1
+ r = (x == "x") and (r + 1) or (r % 4) + 9
+ return ("0123456789abcdef"):sub(r, r)
+ end
+ return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
+end
+
+
+function lume.hotswap(modname)
+ local oldglobal = lume.clone(_G)
+ local updated = {}
+ local function update(old, new)
+ if updated[old] then return end
+ updated[old] = true
+ local oldmt, newmt = getmetatable(old), getmetatable(new)
+ if oldmt and newmt then update(oldmt, newmt) end
+ for k, v in pairs(new) do
+ if type(v) == "table" then update(old[k], v) else old[k] = v end
+ end
+ end
+ local err = nil
+ local function onerror(e)
+ for k in pairs(_G) do _G[k] = oldglobal[k] end
+ err = lume.trim(e)
+ end
+ local ok, oldmod = pcall(require, modname)
+ oldmod = ok and oldmod or nil
+ xpcall(function()
+ package.loaded[modname] = nil
+ local newmod = require(modname)
+ if type(oldmod) == "table" then update(oldmod, newmod) end
+ for k, v in pairs(oldglobal) do
+ if v ~= _G[k] and type(v) == "table" then
+ update(v, _G[k])
+ _G[k] = v
+ end
+ end
+ end, onerror)
+ package.loaded[modname] = oldmod
+ if err then return nil, err end
+ return oldmod
+end
+
+
+local ripairs_iter = function(t, i)
+ i = i - 1
+ local v = t[i]
+ if v ~= nil then
+ return i, v
+ end
+end
+
+function lume.ripairs(t)
+ return ripairs_iter, t, (#t + 1)
+end
+
+
+function lume.color(str, mul)
+ mul = mul or 1
+ local r, g, b, a
+ r, g, b = str:match("#(%x%x)(%x%x)(%x%x)")
+ if r then
+ r = tonumber(r, 16) / 0xff
+ g = tonumber(g, 16) / 0xff
+ b = tonumber(b, 16) / 0xff
+ a = 1
+ elseif str:match("rgba?%s*%([%d%s%.,]+%)") then
+ local f = str:gmatch("[%d.]+")
+ r = (f() or 0) / 0xff
+ g = (f() or 0) / 0xff
+ b = (f() or 0) / 0xff
+ a = f() or 1
+ else
+ error(("bad color string '%s'"):format(str))
+ end
+ return r * mul, g * mul, b * mul, a * mul
+end
+
+
+local chain_mt = {}
+chain_mt.__index = lume.map(lume.filter(lume, iscallable, true),
+ function(fn)
+ return function(self, ...)
+ self._value = fn(self._value, ...)
+ return self
+ end
+ end)
+chain_mt.__index.result = function(x) return x._value end
+
+function lume.chain(value)
+ return setmetatable({ _value = value }, chain_mt)
+end
+
+setmetatable(lume, {
+ __call = function(_, ...)
+ return lume.chain(...)
+ end
+})
+
+
+return lume
diff --git a/bin/utils/lume/test/test.lua b/bin/utils/lume/test/test.lua
@@ -0,0 +1,624 @@
+local tester = require "util.tester"
+
+package.path = "../?.lua;" .. package.path
+
+local lume = require "lume"
+
+local tests = {}
+local testeq = tester.test.equal
+
+-- lume.clamp
+tests["lume.clamp"] = function()
+ testeq( lume.clamp(8, 5, 10), 8 )
+ testeq( lume.clamp(12, 5, 10), 10 )
+ testeq( lume.clamp(-1, 5, 10), 5 )
+ testeq( lume.clamp(-1, -10, 10), -1 )
+ testeq( lume.clamp(-100, -10, 10), -10 )
+ testeq( lume.clamp(13, 8, 8), 8 )
+ testeq( lume.clamp(3, 8, 8), 8 )
+end
+
+-- lume.round
+tests["lume.round"] = function()
+ testeq( lume.round(.5), 1 )
+ testeq( lume.round(-.5), -1 )
+ testeq( lume.round(2.4), 2 )
+ testeq( lume.round(123, 10), 120 )
+ testeq( lume.round(129, 64), 128 )
+ testeq( lume.round(-123.45, .1), -123.5 )
+ testeq( lume.round(0), 0 )
+end
+
+-- lume.sign
+tests["lume.sign"] = function()
+ testeq( lume.sign(-10), -1 )
+ testeq( lume.sign(10), 1 )
+ testeq( lume.sign(0), 1 )
+end
+
+-- lume.lerp
+tests["lume.lerp"] = function()
+ testeq( lume.lerp(100, 200, .5), 150 )
+ testeq( lume.lerp(100, 200, .25), 125 )
+ testeq( lume.lerp(100, 200, 2), 200 )
+ testeq( lume.lerp(100, 200, -2), 100 )
+end
+
+-- lume.smooth
+tests["lume.smooth"] = function()
+ testeq( lume.smooth(100, 200, .5), 150 )
+ testeq( lume.smooth(100, 200, 0), 100 )
+ testeq( lume.smooth(100, 200, 1), 200 )
+ testeq( lume.smooth(100, 200, 2), 200 )
+ testeq( lume.smooth(100, 200, -2), 100 )
+end
+
+-- lume.pingpong
+tests["lume.pingpong"] = function()
+ testeq( lume.pingpong(0), 0 )
+ testeq( lume.pingpong(1.5), .5 )
+ testeq( lume.pingpong(-.2), .2 )
+ testeq( lume.pingpong(-1.6), .4 )
+ testeq( lume.pingpong(1.8), .2 )
+end
+
+-- lume.distance
+tests["lume.distance"] = function()
+ testeq( lume.distance(15, 20, 15, 20), 0 )
+ testeq( lume.distance(13, 44, 156, 232), 236.205419074 )
+ testeq( lume.distance(-23, 66, -232, 123), 216.633330769 )
+ local x = lume.distance(13, 15, -2, 81)
+ testeq( lume.distance(13, 15, -2, 81, true), x * x )
+end
+
+-- lume.angle
+tests["lume.angle"] = function()
+ testeq( lume.angle(10, 10, 10, 10), math.rad(0) )
+ testeq( lume.angle(10, 10, 20, 10), math.rad(0) )
+ testeq( lume.angle(10, 10, 5, 10), math.rad(180) )
+ testeq( lume.angle(10, 10, 20, 20), math.rad(45) )
+ testeq( lume.angle(10, 10, 10, 30), math.rad(90) )
+end
+
+-- lume.vector
+tests["lume.vector"] = function()
+ local function cmp(a, b) return math.abs(a - b) < 10e-6 end
+ local x, y
+ x, y = lume.vector(0, 10)
+ testeq( cmp(x, 10) and cmp(y, 0), true )
+ x, y = lume.vector(math.pi, 100)
+ testeq( cmp(x, -100) and cmp(y, 0), true )
+ x, y = lume.vector(math.pi * 0.25, 100)
+ testeq( cmp(x, 70.71067811865476) and cmp(y, 70.71067811865476), true )
+end
+
+-- lume.random
+tests["lume.random"] = function()
+ testeq( type(lume.random()), "number" )
+ testeq( type(lume.random(1)), "number" )
+ testeq( type(lume.random(1, 2)), "number" )
+end
+
+-- lume.randomchoice
+tests["lume.randomchoice"] = function()
+ local t = {}
+ for i = 0, 1000 do
+ t[lume.randomchoice({"a", "b", "c", "d"})] = true
+ end
+ testeq( t.a and t.b and t.c and t.d, true )
+ testeq( lume.randomchoice({true}), true )
+end
+
+-- lume.weightedchoice
+tests["lume.weightedchoice"] = function()
+ testeq( lume.weightedchoice( {a = 1} ), "a" )
+ testeq( lume.weightedchoice( {a = 0, b = 1} ), "b" )
+ tester.test.error( lume.weightedchoice, {} )
+ tester.test.error( lume.weightedchoice, { a = 0, b = 0 } )
+ tester.test.error( lume.weightedchoice, { a = 1, b = -1 } )
+end
+
+-- lume.push
+tests["lume.push"] = function()
+ local t = { 1, 2 }
+ lume.push(t, 3, 4)
+ testeq(t, { 1, 2, 3, 4 })
+ lume.push(t, 5, nil, 6, nil, 7)
+ testeq(t, { 1, 2, 3, 4, 5, 6, 7 })
+ lume.push(t)
+ testeq(t, { 1, 2, 3, 4, 5, 6, 7 })
+ local x, y = lume.push(t, 123, 456)
+ testeq(x, 123)
+ testeq(y, 456)
+end
+
+-- lume.remove
+tests["lume.remove"] = function()
+ local t = { 1, 2, 3, 4, 5 }
+ lume.remove(t, 3)
+ testeq(t, { 1, 2, 4, 5 })
+ lume.remove(t, 1)
+ testeq(t, { 2, 4, 5 })
+ lume.remove(t, 5)
+ testeq(t, { 2, 4 })
+ local x = lume.remove(t, 123)
+ testeq(x, 123)
+end
+
+-- lume.clear
+tests["lume.clear"] = function()
+ local t = { 1, 2, 3 }
+ lume.clear(t)
+ testeq(t, {})
+ local m = { a = 1, b = 2, c = 3 }
+ lume.clear(m)
+ testeq(m, {})
+ testeq( lume.clear(t) == t, true )
+end
+
+-- lume.extend
+tests["lume.extend"] = function()
+ local t = { a = 10, b = 20, c = 30 }
+ testeq( lume.extend(t) == t, true )
+ lume.extend(t, { d = 40 }, { e = 50 })
+ testeq( t, { a = 10, b = 20, c = 30, d = 40, e = 50 } )
+ lume.extend(t, { a = "cat", b = "dog" }, { b = "owl", c = "fox" })
+ testeq( t, { a = "cat", b = "owl", c = "fox", d = 40, e = 50 } )
+end
+
+-- lume.shuffle
+tests["lume.shuffle"] = function()
+ local t = {1, 2, 3, 4, 5}
+ t = lume.shuffle(t)
+ table.sort(t)
+ testeq( t, {1, 2, 3, 4, 5} )
+ testeq( lume.shuffle({}), {} )
+end
+
+-- lume.sort
+tests["lume.sort"] = function()
+ local t = { 1, 5, 2, 4, 3 }
+ local fn = function(a, b) return a > b end
+ testeq( t == lume.sort(t), false )
+ testeq( lume.sort(t), { 1, 2, 3, 4, 5 } )
+ testeq( lume.sort(t, fn), { 5, 4, 3, 2, 1 } )
+ testeq( t, { 1, 5, 2, 4, 3 } )
+ local t = { { id = 2 }, { id = 3 }, { id = 1 } }
+ testeq( lume.sort(t, "id"), { { id = 1 }, { id = 2 }, { id = 3 } })
+end
+
+-- lume.array
+tests["lume.array"] = function()
+ local t = lume.array(pairs({a=0, b=0, c=0}))
+ table.sort(t)
+ testeq( t, {"a", "b", "c"} )
+ testeq( lume.array(ipairs({0, 0, 0})), {1, 2, 3} )
+end
+
+-- lume.each
+tests["lume.each"] = function()
+ local acc = 1
+ lume.each({1, 2, 3}, function(x) acc = acc + x end)
+ testeq( acc, 7 )
+
+ local acc = 1
+ local f = function(o, x) acc = acc + x end
+ local f2 = function() end
+ local t = {a = {f = f}, b = {f = f}, c = {f = f2}}
+ lume.each(t, "f", 10)
+ testeq( acc, 21 )
+end
+
+-- lume.map
+tests["lume.map"] = function()
+ testeq( lume.map({1, 2, 3}, function(x) return x * 2 end), {2, 4, 6} )
+ testeq( lume.map({a=2,b=3}, function(x) return x * 2 end), {a=4,b=6} )
+ local t = {{ id = 10 }, { id = 20 }, { id = 30 }}
+ testeq( lume.map(t, "id"), { 10, 20, 30 })
+end
+
+-- lume.all
+tests["lume.all"] = function()
+ testeq( lume.all({true, true, false, true}), false )
+ testeq( lume.all({true, true, true, true}), true )
+ testeq( lume.all({2, 3, 4, 5}, function(x) return x % 2 == 0 end), false )
+ testeq( lume.all({2, 4, 6, 8}, function(x) return x % 2 == 0 end), true )
+ testeq( lume.all({{ x = 1 }, {}, { x = 3 }}, "x"), false )
+ testeq( lume.all({{ x = 1 }, { x = 2 }, { x = 3 }}, "x"), true )
+ testeq( lume.all({{ x = 1 }, { x = 2 }, { x = 3 }}, { x = 2 }), false )
+ testeq( lume.all({{ x = 2 }, { x = 2 }, { x = 2 }}, { x = 2 }), true )
+end
+
+-- lume.any
+tests["lume.any"] = function()
+ testeq( lume.any({true, true, false, true}), true )
+ testeq( lume.any({false, false, false}), false )
+ testeq( lume.any({2, 3, 4, 5}, function(x) return x % 2 == 0 end), true )
+ testeq( lume.any({1, 3, 5, 7}, function(x) return x % 2 == 0 end), false )
+ local t = {{ id = 10 }, { id = 20 }, { id = 30 }}
+ testeq( lume.any(t, { id = 10 }), true )
+ testeq( lume.any(t, { id = 40 }), false )
+end
+
+-- lume.reduce
+tests["lume.reduce"] = function()
+ local concat = function(a, b) return a .. b end
+ local add = function(a, b) return a + b end
+ local any = function(a, b) return a or b end
+ testeq( lume.reduce({"cat", "dog"}, concat, ""), "catdog" )
+ testeq( lume.reduce({"cat", "dog"}, concat, "pig"), "pigcatdog" )
+ testeq( lume.reduce({"me", "ow"}, concat), "meow" )
+ testeq( lume.reduce({1, 2, 3, 4}, add), 10 )
+ testeq( lume.reduce({1, 2, 3, 4}, add, 5), 15 )
+ testeq( lume.reduce({1}, add), 1 )
+ testeq( lume.reduce({}, concat, "potato"), "potato" )
+ testeq( lume.reduce({a=1, b=2}, add, 5), 8 )
+ testeq( lume.reduce({a=1, b=2}, add), 3 )
+ testeq( lume.reduce({false, false, false}, any), false )
+ testeq( lume.reduce({false, true, false}, any), true )
+ tester.test.error(lume.reduce, {}, add)
+end
+
+-- lume.unique
+tests["lume.unique"] = function()
+ testeq( lume.unique({}), {} )
+ local t = lume.unique({1, 2, 3, 2, 5, 6, 6})
+ table.sort(t)
+ testeq( t, {1, 2, 3, 5, 6} )
+ local t = lume.unique({"a", "b", "c", "b", "d"})
+ table.sort(t)
+ testeq( t, {"a", "b", "c", "d"} )
+end
+
+-- lume.filter
+tests["lume.filter"] = function()
+ local t = lume.filter({1, 2, 3, 4, 5}, function(x) return x % 2 == 0 end )
+ testeq( t, {2, 4} )
+ local t = lume.filter({a=1, b=2, c=3}, function(x) return x == 2 end, true)
+ testeq( t, {b=2} )
+ local t = lume.filter({{ x=1, y=1 }, { x=2, y=2 }, { x=1, y=3 }}, { x = 1 })
+ testeq( t, {{ x=1, y=1 }, {x=1, y=3}} )
+end
+
+-- lume.reject
+tests["lume.reject"] = function()
+ local t = lume.reject({1, 2, 3, 4, 5}, function(x) return x % 2 == 0 end )
+ testeq( t, {1, 3, 5} )
+ local t = lume.reject({a=1, b=2, c=3}, function(x) return x == 2 end, true)
+ testeq( t, {a=1, c=3} )
+ local t = lume.reject({{ x=1, y=1 }, { x=2, y=2 }, { x=1, y=3 }}, { x = 1 })
+ testeq( t, {{ x=2, y=2 }} )
+end
+
+-- lume.merge
+tests["lume.merge"] = function()
+ testeq( lume.merge(), {} )
+ testeq( lume.merge({x=1, y=2}), {x=1, y=2} )
+ testeq( lume.merge({a=1, b=2}, {b=3, c=4}), {a=1, b=3, c=4} )
+end
+
+-- lume.concat
+tests["lume.concat"] = function()
+ testeq( lume.concat(nil), {} )
+ testeq( lume.concat({1, 2, 3}), {1, 2, 3} )
+ testeq( lume.concat({1, 2, 3}, {4, 5, 6}), {1, 2, 3, 4, 5, 6} )
+ testeq( lume.concat({1, 2, 3}, {4, 5, 6}, nil, {7}), {1, 2, 3, 4, 5, 6, 7} )
+end
+
+-- lume.find
+tests["lume.find"] = function()
+ testeq( lume.find({"a", "b", "c"}, "b"), 2 )
+ testeq( lume.find({"a", "b", "c"}, "c"), 3 )
+ testeq( lume.find({a=1, b=5, c=7}, 5), "b" )
+end
+
+-- lume.match
+tests["lume.match"] = function()
+ local t = { "a", "b", "c", "d" }
+ local t2 = { a = 1, b = 2, c = 3, d = 4 }
+ local t3 = { {x=1, y=2}, {x=3, y=4}, {x=5, y=6} }
+ local v, k = lume.match(t, function(x) return x > "c" end)
+ testeq( v, "d" )
+ testeq( k, 4 )
+ local v, k = lume.match(t, function(x) return x < "b" end)
+ testeq( v, "a" )
+ testeq( k, 1 )
+ local v, k = lume.match(t2, function(x) return x < 2 end)
+ testeq( v, 1 )
+ testeq( k, "a" )
+ local v, k = lume.match(t2, function(x) return x > 5 end)
+ testeq( v, nil )
+ testeq( k, nil )
+ local v, k = lume.match(t3, { x = 3, y = 4 })
+ testeq( k, 2 )
+end
+
+-- lume.count
+tests["lume.count"] = function()
+ local t = { a = 1, b = 2, c = 5, [13] = 22, z = 8 }
+ testeq( lume.count(t), 5 )
+ testeq( lume.count(t, function(x) return x % 2 == 0 end ), 3 )
+ local a = { 5, 6, 7, 8, 9 }
+ testeq( lume.count(a), #a )
+ local t = { { n = 20 }, { n = 30 }, { n = 40 }, { n = 20 } }
+ testeq( lume.count(t, { n = 20 }), 2 )
+ testeq( lume.count(t, { n = 30 }), 1 )
+ testeq( lume.count(t, { n = 50 }), 0 )
+end
+
+-- lume.slice
+tests["lume.slice"] = function()
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 2, 4), {"b", "c", "d"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 2, -2), {"b", "c", "d"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 3, -1), {"c", "d", "e"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 3), {"c", "d", "e"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 4), {"d", "e"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 1, 1), {"a"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 2, 1), {} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, -3, -2), {"c", "d"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, -3, 1), {} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 0, 1), {"a"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, 0, 0), {} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, -3), {"c", "d", "e"} )
+ testeq( lume.slice({"a", "b", "c", "d", "e"}, -3, 900), {"c", "d", "e"} )
+end
+
+-- lume.first
+tests["lume.first"] = function()
+ local t = { "a", "b", "c", "d", "e" }
+ testeq( lume.first(t), "a" )
+ testeq( lume.first(t, 1), { "a" } )
+ testeq( lume.first(t, 2), { "a", "b" } )
+end
+
+-- lume.last
+tests["lume.last"] = function()
+ local t = { "a", "b", "c", "d", "e" }
+ testeq( lume.last(t), "e" )
+ testeq( lume.last(t, 1), { "e" } )
+ testeq( lume.last(t, 2), { "d", "e" } )
+end
+
+-- lume.invert
+tests["lume.invert"] = function()
+ testeq( lume.invert({}), {} )
+ testeq( lume.invert{a = "x", b = "y"}, {x = "a", y = "b"} )
+ testeq( lume.invert{a = 1, b = 2}, {"a", "b"} )
+ testeq( lume.invert(lume.invert{a = 1, b = 2}), {a = 1, b = 2} )
+end
+
+-- lume.pick
+tests["lume.pick"] = function()
+ local t = { cat = 10, dog = 20, fox = 30, owl = 40 }
+ testeq( lume.pick(t, "cat", "dog"), { cat = 10, dog = 20 } )
+ testeq( lume.pick(t, "fox", "owl"), { fox = 30, owl = 40 } )
+ testeq( lume.pick(t, "owl"), { owl = 40 } )
+ testeq( lume.pick(t), {} )
+end
+
+-- lume.keys
+tests["lume.keys"] = function()
+ testeq( lume.keys({}), {} )
+ local t = lume.keys({ aaa = 1, bbb = 2, ccc = 3 })
+ table.sort(t)
+ testeq( t, {"aaa", "bbb", "ccc"} )
+ local t = lume.keys({ "x", "x", "x" })
+ testeq( t, {1, 2, 3} )
+end
+
+-- lume.clone
+tests["lume.clone"] = function()
+ local t = {6, 7, 4, 5}
+ testeq( lume.clone(t) ~= t, true )
+ testeq( lume.clone(t), {6, 7, 4, 5} )
+ testeq( lume.clone({x=2, y="a"}), {x=2, y="a"} )
+end
+
+-- lume.fn
+tests["lume.fn"] = function()
+ local f = lume.fn(function(a, b) return a + b end, 10)
+ testeq( f(5), 15 )
+ tester.test.error( lume.fn, 123 )
+end
+
+-- lume.once
+tests["lume.once"] = function()
+ local f = lume.once(function(a, b) return a + b end, 10)
+ testeq( f(5), 15 )
+ testeq( f(5), nil )
+ tester.test.error( lume.once, 123 )
+end
+
+-- lume.memoize
+tests["lume.memoize"] = function()
+ local f = lume.memoize(
+ function(a, b, c)
+ return tostring(a) .. tostring(b) .. tostring(c)
+ end)
+ testeq( f("hello", nil, 15), "hellonil15" )
+ testeq( f("hello", nil, 15), "hellonil15" )
+ testeq( f(), "nilnilnil" )
+ testeq( f(), "nilnilnil" )
+ local f2 = lume.memoize(function() end)
+ testeq( f2(), nil )
+end
+
+-- lume.combine
+tests["lume.combine"] = function()
+ local acc = 0
+ local a = function(x, y) acc = acc + x + y end
+ local b = function(x, y) acc = acc + x * y end
+ local fn = lume.combine(a, b)
+ fn(10, 20)
+ testeq( acc, 230 )
+ acc = 0
+ fn = lume.combine(nil, a, nil, b, nil)
+ fn(10, 20)
+ testeq( acc, 230 )
+ local x = false
+ fn = lume.combine(function() x = true end)
+ fn()
+ testeq( x, true )
+ testeq( type(lume.combine(nil)), "function" )
+ testeq( type(lume.combine()), "function" )
+end
+
+-- lume.call
+tests["lume.call"] = function()
+ local add = function(a, b) return a + b end
+ testeq( lume.call(), nil )
+ testeq( lume.call(nil, 1, 2, 3), nil )
+ testeq( lume.call(add, 1, 2), 3 )
+end
+
+-- lume.time
+tests["lume.time"] = function()
+ local t, a, b, c = lume.time(function(x) return 50, 60, x end, 70)
+ testeq( type(t), "number" )
+ testeq( {a, b, c}, {50, 60, 70} )
+end
+
+-- lume.lambda
+tests["lume.lambda"] = function()
+ testeq( lume.lambda "x->x*x"(10), 100 )
+ testeq( lume.lambda "x->x*x"(20), 400 )
+ testeq( lume.lambda "x,y -> 2*x+y"(10,5), 25 )
+ testeq( lume.lambda "a, b -> a / b"(1, 2), .5 )
+ testeq( lume.lambda "a -> 'hi->' .. a"("doggy"), "hi->doggy" )
+ testeq( lume.lambda "A1,_->A1.._"("te","st"), "test" )
+ testeq( lume.lambda "->"(1,2,3), nil )
+ tester.test.error( lume.lambda, "abc" )
+ tester.test.error( lume.lambda, "" )
+ tester.test.error( lume.lambda, "a,b->a->b" )
+ tester.test.error( lume.lambda, "(a),b->a+b" )
+end
+
+-- lume.serialize / lume.deserialize
+tests["lume.serialize, lume.deserialize"] = function()
+ local t = { 1, 2, 3, 4, true, false, "cat", "dog", {1, 2, 3} }
+ local s = lume.serialize(t)
+ testeq( lume.deserialize(s), t )
+ testeq( lume.deserialize(lume.serialize(math.huge)), math.huge )
+ testeq( lume.deserialize(lume.serialize(-math.huge)), -math.huge )
+ local x = lume.deserialize(lume.serialize(0 / 0)) -- nan
+ testeq( x ~= x, true )
+end
+
+-- lume.split
+tests["lume.split"] = function()
+ testeq( lume.split("cat dog pig"), {"cat", "dog", "pig"} )
+ testeq( lume.split("cat,dog,pig", ","), {"cat", "dog", "pig"} )
+ testeq( lume.split("cat,dog;pig", ";"), {"cat,dog", "pig"} )
+ testeq( lume.split("cat,dog,,pig", ","), {"cat", "dog", "", "pig"} )
+ testeq( lume.split(";;;cat;", ";"), {"", "", "", "cat", ""} )
+ testeq( lume.split("cat.dog", "."), {"cat", "dog"} )
+ testeq( lume.split("cat%dog", "%"), {"cat", "dog"} )
+ testeq( lume.split("1<>2<>3", "<>"), {"1", "2", "3"} )
+ tester.test.error( lume.split, "abc", "" )
+end
+
+-- lume.trim
+tests["lume.trim"] = function()
+ testeq( lume.trim(" hello world "), "hello world" )
+ testeq( lume.trim("-=-hello-world===", "-="), "hello-world" )
+ testeq( lume.trim("***hello world*-*", "*"), "hello world*-" )
+ testeq( lume.trim("...hello world.", "."), "hello world" )
+ testeq( lume.trim("^.hello world]^", "^.]"), "hello world" )
+end
+
+-- lume.wordwrap
+tests["lume.wordwrap"] = function()
+ local str = "A small string with some words and then some more words"
+ local b = "A small string with \nsome words and then \nsome more words"
+ local fn = function(str) return #str >= 20 end
+ testeq( lume.wordwrap(str), str )
+ testeq( lume.wordwrap(str, 20), b )
+ testeq( lume.wordwrap(str, fn), b )
+end
+
+-- lume.format
+tests["lume.format"] = function()
+ local str = lume.format("a {a} in a {b}", {a = "mouse", b = "house"})
+ testeq( str, "a mouse in a house" )
+ testeq( lume.format("number {num}", {num = 13}), "number 13" )
+ testeq( lume.format("{missing} {keys}", {}), "{missing} {keys}" )
+ testeq( lume.format("A {missing} table"), "A {missing} table" )
+ testeq( lume.format("{1} idx {2}", {"an", "test"}), "an idx test" )
+ testeq( lume.format("bad idx {-1}", {"x"}), "bad idx {-1}" )
+ testeq( lume.format("empty {}", {"idx"}), "empty {}" )
+end
+
+-- lume.trace
+tests["lume.trace"] = function()
+ local oldprint = print
+ local file, line, msg
+ print = function(x)
+ file, line, msg = x:match("(.-):(.-): (.*)")
+ end
+ lume.trace("Hi world", 123.456, 1, nil)
+ print = oldprint
+ testeq( file:match(".lua$"), ".lua" )
+ testeq( tonumber(line) ~= nil, true )
+ testeq( msg, "Hi world 123.46 1 nil" )
+end
+
+-- lume.dostring
+tests["lume.dostring"] = function()
+ testeq( lume.dostring([[return "hello!"]]), "hello!" )
+ testeq( lume.dostring([[return 12345]]), 12345 )
+end
+
+-- lume.uuid
+tests["lume.uuid"] = function()
+ testeq( type(lume.uuid()), "string" )
+ testeq( #lume.uuid(), 36 )
+end
+
+-- lume.hotswap
+tests["lume.hotswap"] = function()
+ local ok, err = lume.hotswap("bad_module_name")
+ testeq( ok, nil )
+ testeq( type(err), "string" )
+end
+
+-- lume.ripairs
+tests["lume.ripairs"] = function()
+ local t = { "a", "b", false, "c" }
+ local r = {}
+ for i, v in lume.ripairs(t) do
+ table.insert(r, { i, v })
+ end
+ testeq( r, { { 4, "c" }, { 3, false }, { 2, "b" }, { 1, "a" } })
+ tester.test.error(lume.ripairs, nil)
+end
+
+-- lume.color
+tests["lume.color"] = function()
+ testeq({ lume.color("#ff0000") }, { 1, 0, 0, 1 } )
+ testeq({ lume.color("#00ff00") }, { 0, 1, 0, 1 } )
+ testeq({ lume.color("#0000ff") }, { 0, 0, 1, 1 } )
+ testeq({ lume.color("rgb( 255, 255, 255 )") }, { 1, 1, 1, 1 } )
+ testeq({ lume.color("rgb (0, 0, 0)") }, { 0, 0, 0, 1 } )
+ testeq({ lume.color("rgba(255, 255, 255, .5)") }, { 1, 1, 1, .5 } )
+ testeq({ lume.color("#ffffff", 2) }, { 2, 2, 2, 2 } )
+ testeq({ lume.color("rgba(255, 255, 255, 1)", 3) }, { 3, 3, 3, 3 } )
+ tester.test.error(lume.color, "#ff00f")
+ tester.test.error(lume.color, "#xyzxyz")
+ tester.test.error(lume.color, "rgba(hello)")
+ tester.test.error(lume.color, "rgba()")
+ tester.test.error(lume.color, "rgba(1, 1, 1, 1")
+end
+
+-- lume.chain
+tests["lume.chain"] = function()
+ local t = lume.chain({1, 2}):map(function(x) return x * 2 end):result()
+ testeq( t, { 2, 4 } )
+ testeq( lume.chain(10):result(), 10 )
+ local t = lume({1, 2}):map(function(x) return x * 2 end):result()
+ testeq( t, { 2, 4 } )
+end
+
+
+tester.dotests(tests)
+tester.test.global()
+tester.printresults()
diff --git a/bin/utils/lume/test/util/tester.lua b/bin/utils/lume/test/util/tester.lua
@@ -0,0 +1,166 @@
+
+
+local tester = {
+ test = {},
+ linecache = {},
+ globals = {},
+ passcount = 0,
+ failcount = 0
+}
+
+
+local function isequal(a, b)
+ if type(a) ~= type(b) then return nil end
+ local t = {}
+ function t.table(a, b)
+ for k, v in pairs(a) do if not isequal(b[k], v) then return nil end end
+ for k, v in pairs(b) do if not isequal(a[k], v) then return nil end end
+ return true
+ end
+ function t.number(a, b) return math.abs(a - b) < 10e-9 end
+ return t[type(a)] and t[type(a)](a, b) or (a == b)
+end
+
+
+local function stringify(x)
+ if type(x) == "number" then return string.format("%.2f", x) end
+ return string.format("%q", tostring(x))
+end
+
+
+local function getline(file, line)
+ if not tester.linecache[file] then
+ local t = {}
+ for line in io.lines(file) do
+ t[#t + 1] = line
+ end
+ tester.linecache[file] = t
+ end
+ return tester.linecache[file][line]
+end
+
+
+local function truncate(str, max)
+ max = max or 72
+ if #str > max then
+ return str:sub(1, max - 3) .. "..."
+ end
+ return str
+end
+
+
+local function has(t, value)
+ for k, v in pairs(t) do
+ if v == value then return true end
+ end
+ return false
+end
+
+
+local function makelogstr(passed, file, line)
+ local t = {}
+ t[#t + 1] = passed and "[\27[32mPASS\27[0m]" or "[\27[31mFAIL\27[0m]"
+ t[#t + 1] = file .. ":" .. line .. ":"
+ t[#t + 1] = getline(file, line) :gsub(" %s+", " ") :gsub("^ *", "")
+ return truncate(table.concat(t, " "))
+end
+
+
+local function dopass(file, line)
+ print(makelogstr(true, file, line))
+ tester.passcount = tester.passcount + 1
+end
+
+
+local function dofail(file, line)
+ print(makelogstr(false, file, line))
+ tester.failcount = tester.failcount + 1
+end
+
+
+local function printfailmsg(str)
+ print(string.rep(" ", 7) .. str)
+end
+
+
+
+
+function tester.init()
+ for k, v in pairs(_G) do
+ tester.globals[k] = v
+ end
+ return tester
+end
+
+
+function tester.test.global(expectedglobals)
+ expectedglobals = expectedglobals or {}
+ local info = debug.getinfo(2)
+ local unexpected = {}
+ for k in pairs(_G) do
+ if not tester.globals[k] and not has(expectedglobals, k) then
+ table.insert(unexpected, "Unexpected global '" .. k .. "'")
+ end
+ end
+ if #unexpected == 0 then
+ dopass(info.short_src, info.currentline)
+ else
+ dofail(info.short_src, info.currentline)
+ for _, v in pairs(unexpected) do printfailmsg(v) end
+ end
+end
+
+
+function tester.test.equal(result, expected)
+ local passed = isequal(result, expected)
+ local info = debug.getinfo(2)
+ if passed then
+ dopass(info.short_src, info.currentline)
+ else
+ dofail(info.short_src, info.currentline)
+ if type(expected) == "table" and type(result) == "table" then
+ printfailmsg("Tables do not match")
+ else
+ printfailmsg(string.format("Expected %s got %s",
+ stringify(expected), stringify(result) ))
+ end
+ end
+end
+
+
+function tester.test.error(fn, ...)
+ local passed = not pcall(fn, ...)
+ local info = debug.getinfo(2)
+ if passed then
+ dopass(info.short_src, info.currentline)
+ else
+ dofail(info.short_src, info.currentline)
+ printfailmsg("Expected an error to be raised")
+ end
+end
+
+
+function tester.dotests(t)
+ local keys = {}
+ for k in pairs(t) do table.insert(keys, k) end
+ table.sort(keys)
+ for _, k in pairs(keys) do
+ print("\27[33m-- " .. k .. "\27[0m")
+ t[k]()
+ end
+end
+
+
+function tester.printresults()
+ local str = table.concat{
+ "-- ",
+ string.format("Results: %d Total", tester.passcount + tester.failcount),
+ " ", string.format("%d Passed", tester.passcount),
+ " ", string.format("%d Failed", tester.failcount),
+ " --", }
+ local b = string.rep("-", #str)
+ print(table.concat{b, "\n", str, "\n", b})
+end
+
+
+return tester.init()