All kinds of things

- Add modified BSD license
- Initial test runner github action
- Add is_{type} functions
- export kiwi.Error metatable such that it appears like a class.
- Allow Expression.new to take nil for terms
- Initial set of specs
This commit is contained in:
2024-02-17 21:53:34 -06:00
parent d85796a038
commit 2ffc5a333b
8 changed files with 777 additions and 25 deletions

111
spec/constraint_spec.lua Normal file
View File

@@ -0,0 +1,111 @@
expose("module", function()
require("kiwi")
end)
describe("Constraint", function()
local kiwi = require("kiwi")
describe("construction", function()
local v, lhs
before_each(function()
v = kiwi.Var("foo")
lhs = v + 1
end)
it("has correct type", function()
assert.True(kiwi.is_constraint(kiwi.Constraint()))
assert.False(kiwi.is_constraint(v))
end)
it("default op and strength", function()
local c = kiwi.Constraint(lhs)
assert.equal("EQ", c:op())
assert.equal(kiwi.strength.REQUIRED, c:strength())
end)
it("configure op", function()
local c = kiwi.Constraint(lhs, nil, "LE")
assert.equal("LE", c:op())
end)
it("configure strength", function()
local c = kiwi.Constraint(lhs, nil, "GE", kiwi.strength.STRONG)
assert.equal(kiwi.strength.STRONG, c:strength())
end)
it("formats well", function()
local c = kiwi.Constraint(lhs)
assert.equal("1 foo + 1 == 0 | required", tostring(c))
c = kiwi.Constraint(lhs * 2, nil, "GE", kiwi.strength.STRONG)
assert.equal("2 foo + 2 >= 0 | strong", tostring(c))
c = kiwi.Constraint(lhs / 2, nil, "LE", kiwi.strength.MEDIUM)
assert.equal("0.5 foo + 0.5 <= 0 | medium", tostring(c))
c = kiwi.Constraint(lhs, kiwi.Expression(nil, 3), "GE", kiwi.strength.WEAK)
assert.equal("1 foo + -2 >= 0 | weak", tostring(c))
end)
it("rejects invalid args", function()
assert.error(function()
local _ = kiwi.Constraint(1)
end)
assert.error(function()
local _ = kiwi.Constraint(lhs, 1)
end)
assert.error(function()
local _ = kiwi.Constraint("")
end)
assert.error(function()
local _ = kiwi.Constraint(lhs, "")
end)
assert.error(function()
local _ = kiwi.Constraint(lhs, nil, "foo")
end)
assert.error(function()
local _ = kiwi.Constraint(lhs, nil, "LE", "foo")
end)
end)
it("combines lhs and rhs", function()
local v2 = kiwi.Var("bar")
local rhs = kiwi.Expression({ 5 * v2, 3 * v }, 3)
local c = kiwi.Constraint(lhs, rhs)
local e = c:expression()
local t = e:terms()
assert.equal(2, #t)
if t[1].var ~= v then
t[1], t[2] = t[2], t[1]
end
assert.equal(v, t[1].var)
assert.equal(-2.0, t[1].coefficient)
assert.equal(v2, t[2].var)
assert.equal(-5.0, t[2].coefficient)
assert.equal(-2.0, e.constant)
end)
end)
describe("method", function()
local c, v
before_each(function()
v = kiwi.Var("foo")
c = kiwi.Constraint(2 * v + 1)
end)
it("violated", function()
assert.True(c:violated())
v:set(-0.5)
assert.False(c:violated())
end)
it("add/remove constraint", function()
local s = kiwi.Solver()
c:add_to(s)
assert.True(s:has_constraint(c))
c:remove_from(s)
assert.False(s:has_constraint(c))
end)
end)
end)

365
spec/solver_spec.lua Normal file
View File

@@ -0,0 +1,365 @@
expose("module", function()
require("kiwi")
end)
describe("solver", function()
local kiwi = require("kiwi")
---@type kiwi.Solver
local solver
before_each(function()
solver = kiwi.Solver()
end)
it("should create a solver", function()
assert.True(kiwi.is_solver(solver))
assert.False(kiwi.is_solver(kiwi.Term()))
end)
describe("edit variables", function()
local v1, v2, v3
before_each(function()
v1 = kiwi.Var("foo")
v2 = kiwi.Var("bar")
v3 = kiwi.Var("baz")
end)
describe("add_edit_var", function()
it("should add a variable", function()
solver:add_edit_var(v1, kiwi.strength.STRONG)
assert.True(solver:has_edit_var(v1))
end)
it("should return the argument", function()
assert.equal(v1, solver:add_edit_var(v1, kiwi.strength.STRONG))
end)
it("should error on incorrect type", function()
assert.error(function()
solver:add_edit_var("", kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch
end)
assert.error(function()
solver:add_edit_var(v1, "") ---@diagnostic disable-line: param-type-mismatch
end)
end)
it("should require a strength argument", function()
assert.error(function()
solver:add_edit_var(v1) ---@diagnostic disable-line: missing-parameter
end)
end)
it("should error on duplicate variable", function()
solver:add_edit_var(v1, kiwi.strength.STRONG)
local _, err = pcall(function()
return solver:add_edit_var(v1, kiwi.strength.STRONG)
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v1, err.item)
assert.equal("KiwiErrDuplicateEditVariable", err.kind)
assert.equal("The edit variable has already been added to the solver.", err.message)
end)
it("should error on invalid strength", function()
local _, err = pcall(function()
return solver:add_edit_var(v1, kiwi.strength.REQUIRED)
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v1, err.item)
assert.equal("KiwiErrBadRequiredStrength", err.kind)
assert.equal("A required strength cannot be used in this context.", err.message)
end)
it("should return errors for duplicate variables", function()
solver:set_error_mask({ "KiwiErrDuplicateEditVariable", "KiwiErrBadRequiredStrength" })
local ret, err = solver:add_edit_var(v1, kiwi.strength.STRONG)
assert.Nil(err)
ret, err = solver:add_edit_var(v1, kiwi.strength.STRONG)
assert.equal(v1, ret)
assert.True(kiwi.is_error(err))
---@diagnostic disable: need-check-nil
assert.True(kiwi.is_solver(err.solver))
assert.equal(v1, err.item)
assert.equal("KiwiErrDuplicateEditVariable", err.kind)
assert.equal("The edit variable has already been added to the solver.", err.message)
---@diagnostic enable: need-check-nil
end)
it("should return errors for invalid strength", function()
solver:set_error_mask({ "KiwiErrDuplicateEditVariable", "KiwiErrBadRequiredStrength" })
---@diagnostic disable: need-check-nil
local ret, err = solver:add_edit_var(v2, kiwi.strength.REQUIRED)
assert.equal(v2, ret)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v2, err.item)
assert.equal("KiwiErrBadRequiredStrength", err.kind)
assert.equal("A required strength cannot be used in this context.", err.message)
---@diagnostic enable: need-check-nil
end)
it("tolerates a nil self", function()
local _, err = pcall(function()
return kiwi.Solver.add_edit_var(nil, v1, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch
end)
assert.True(kiwi.is_error(err))
assert.Nil(err.solver)
assert.equal(v1, err.item)
assert.equal("KiwiErrNullObject", err.kind)
assert.equal("null object passed as argument #0 (self)", err.message)
end)
it("tolerates a nil var", function()
local _, err = pcall(function()
return solver:add_edit_var(nil, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.Nil(err.item)
assert.equal("KiwiErrNullObject", err.kind)
assert.equal("null object passed as argument #1", err.message)
end)
end)
describe("add_edit_vars", function()
it("should add variables", function()
solver:add_edit_vars({ v1, v2 }, kiwi.strength.STRONG)
assert.True(solver:has_edit_var(v1))
assert.True(solver:has_edit_var(v2))
assert.False(solver:has_edit_var(v3))
end)
it("should return the argument", function()
local arg = { v1, v2, v3 }
assert.equal(arg, solver:add_edit_vars(arg, kiwi.strength.STRONG))
end)
it("should error on incorrect type", function()
assert.error(function()
solver:add_edit_vars(v1, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch
end)
assert.error(function()
solver:add_edit_vars("", kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch
end)
assert.error(function()
solver:add_edit_vars(v1, "") ---@diagnostic disable-line: param-type-mismatch
end)
end)
it("should require a strength argument", function()
assert.error(function()
solver:add_edit_vars({ v1, v2 }) ---@diagnostic disable-line: missing-parameter
end, "bad argument #3 to 'f' (cannot convert 'nil' to 'double')")
end)
it("should error on duplicate variable", function()
local _, err = pcall(function()
return solver:add_edit_vars({ v1, v2, v3, v2, v3 }, kiwi.strength.STRONG)
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v2, err.item)
assert.equal("KiwiErrDuplicateEditVariable", err.kind)
assert.equal("The edit variable has already been added to the solver.", err.message)
end)
it("should error on invalid strength", function()
local _, err = pcall(function()
return solver:add_edit_vars({ v1, v2 }, kiwi.strength.REQUIRED)
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v1, err.item)
assert.equal("KiwiErrBadRequiredStrength", err.kind)
assert.equal("A required strength cannot be used in this context.", err.message)
end)
it("should return errors for duplicate variables", function()
solver:set_error_mask({ "KiwiErrDuplicateEditVariable", "KiwiErrBadRequiredStrength" })
local ret, err = solver:add_edit_vars({ v1, v2, v3 }, kiwi.strength.STRONG)
assert.Nil(err)
local arg = { v1, v2, v3 }
ret, err = solver:add_edit_vars(arg, kiwi.strength.STRONG)
assert.equal(arg, ret)
assert.True(kiwi.is_error(err))
---@diagnostic disable: need-check-nil
assert.True(kiwi.is_solver(err.solver))
assert.equal(v1, err.item)
assert.equal("KiwiErrDuplicateEditVariable", err.kind)
assert.equal("The edit variable has already been added to the solver.", err.message)
---@diagnostic enable: need-check-nil
end)
it("should return errors for invalid strength", function()
solver:set_error_mask({ "KiwiErrDuplicateEditVariable", "KiwiErrBadRequiredStrength" })
arg = { v2, v3 }
local ret, err = solver:add_edit_vars(arg, kiwi.strength.REQUIRED)
assert.equal(arg, ret)
assert.True(kiwi.is_error(err))
---@diagnostic disable: need-check-nil
assert.True(kiwi.is_solver(err.solver))
assert.equal(v2, err.item)
assert.equal("KiwiErrBadRequiredStrength", err.kind)
assert.equal("A required strength cannot be used in this context.", err.message)
---@diagnostic enable: need-check-nil
end)
it("tolerates a nil self", function()
local _, err = pcall(function()
return kiwi.Solver.add_edit_vars(nil, { v1, v2 }, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch
end)
assert.True(kiwi.is_error(err))
assert.Nil(err.solver)
assert.equal(v1, err.item)
assert.equal("KiwiErrNullObject", err.kind)
assert.equal("null object passed as argument #0 (self)", err.message)
end)
end)
describe("remove_edit_var", function()
it("should remove a variable", function()
solver:add_edit_vars({ v1, v2, v3 }, kiwi.strength.STRONG)
assert.True(solver:has_edit_var(v2))
solver:remove_edit_var(v2)
assert.True(solver:has_edit_var(v1))
assert.False(solver:has_edit_var(v2))
assert.True(solver:has_edit_var(v3))
end)
it("should return the argument", function()
solver:add_edit_var(v1, kiwi.strength.STRONG)
assert.equal(v1, solver:remove_edit_var(v1))
end)
it("should error on incorrect type", function()
assert.error(function()
solver:remove_edit_var("") ---@diagnostic disable-line: param-type-mismatch
end)
assert.error(function()
solver:remove_edit_var({ v1 }) ---@diagnostic disable-line: param-type-mismatch
end)
end)
it("should error on unknown variable", function()
solver:add_edit_var(v1, kiwi.strength.STRONG)
local _, err = pcall(function()
return solver:remove_edit_var(v2)
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v2, err.item)
assert.equal("KiwiErrUnknownEditVariable", err.kind)
assert.equal("The edit variable has not been added to the solver.", err.message)
end)
it("should return errors if requested", function()
solver:set_error_mask({ "KiwiErrDuplicateEditVariable", "KiwiErrUnknownEditVariable" })
local ret, err = solver:remove_edit_var(v1)
assert.equal(v1, ret)
assert.True(kiwi.is_error(err))
---@diagnostic disable: need-check-nil
assert.True(kiwi.is_solver(err.solver))
assert.equal(v1, err.item)
assert.equal("KiwiErrUnknownEditVariable", err.kind)
assert.equal("The edit variable has not been added to the solver.", err.message)
---@diagnostic enable: need-check-nil
end)
it("tolerates a nil self", function()
local _, err = pcall(function()
return kiwi.Solver.remove_edit_var(nil, v1) ---@diagnostic disable-line: param-type-mismatch
end)
assert.True(kiwi.is_error(err))
assert.Nil(err.solver)
assert.equal(v1, err.item)
assert.equal("KiwiErrNullObject", err.kind)
assert.equal("null object passed as argument #0 (self)", err.message)
end)
it("tolerates a nil var", function()
local _, err = pcall(function()
return solver:remove_edit_var(nil) ---@diagnostic disable-line: param-type-mismatch
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.Nil(err.item)
assert.equal("KiwiErrNullObject", err.kind)
assert.equal("null object passed as argument #1", err.message)
end)
end)
describe("remove_edit_vars", function()
it("should remove variables", function()
solver:add_edit_vars({ v1, v2, v3 }, kiwi.strength.STRONG)
assert.True(solver:has_edit_var(v2))
assert.True(solver:has_edit_var(v3))
solver:remove_edit_vars({ v2, v3 })
assert.False(solver:has_edit_var(v2))
assert.False(solver:has_edit_var(v3))
end)
it("should return the argument", function()
local arg = { v1, v2, v3 }
solver:add_edit_vars(arg, kiwi.strength.STRONG)
assert.equal(arg, solver:remove_edit_vars(arg))
end)
it("should error on incorrect type", function()
assert.error(function()
solver:remove_edit_vars(v1) ---@diagnostic disable-line: param-type-mismatch
end)
assert.error(function()
solver:remove_edit_vars("") ---@diagnostic disable-line: param-type-mismatch
end)
end)
it("should error on unknown variables", function()
local _, err = pcall(function()
return solver:remove_edit_vars({ v2, v1 })
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v2, err.item)
assert.equal("KiwiErrUnknownEditVariable", err.kind)
assert.equal("The edit variable has not been added to the solver.", err.message)
end)
it("should return errors for unknown variables", function()
solver:set_error_mask({ "KiwiErrDuplicateEditVariable", "KiwiErrUnknownEditVariable" })
local ret, err = solver:add_edit_vars({ v1, v2 }, kiwi.strength.STRONG)
assert.Nil(err)
local arg = { v1, v2, v3 }
ret, err = solver:remove_edit_vars(arg)
assert.equal(arg, ret)
assert.True(kiwi.is_error(err))
---@diagnostic disable: need-check-nil
assert.True(kiwi.is_solver(err.solver))
assert.equal(v3, err.item)
assert.equal("KiwiErrUnknownEditVariable", err.kind)
assert.equal("The edit variable has not been added to the solver.", err.message)
---@diagnostic enable: need-check-nil
end)
it("tolerates a nil self", function()
local _, err = pcall(function()
return kiwi.Solver.remove_edit_vars(nil, { v1, v2 }) ---@diagnostic disable-line: param-type-mismatch
end)
assert.True(kiwi.is_error(err))
assert.Nil(err.solver)
assert.equal(v1, err.item)
assert.equal("KiwiErrNullObject", err.kind)
assert.equal("null object passed as argument #0 (self)", err.message)
end)
end)
end)
end)

186
spec/var_spec.lua Normal file
View File

@@ -0,0 +1,186 @@
expose("module", function()
require("kiwi")
end)
describe("Var", function()
local kiwi = require("kiwi")
it("construction", function()
assert.True(kiwi.is_var(kiwi.Var()))
assert.False(kiwi.is_var(kiwi.Constraint()))
assert.error(function()
kiwi.Var(1)
end)
end)
describe("method", function()
local v
before_each(function()
v = kiwi.Var("goo")
end)
it("has settable name", function()
assert.equal("goo", v:name())
v:set_name("Δ")
assert.equal("Δ", v:name())
assert.error(function()
v:set_name(1)
end)
end)
it("has a initial value of 0.0", function()
assert.equal(0.0, v:value())
end)
it("has a settable value", function()
v:set(47.0)
assert.equal(47.0, v:value())
end)
it("neg", function()
local neg = -v --[[@as kiwi.Term]]
assert.True(kiwi.is_term(neg))
assert.equal(v, neg.var)
assert.equal(-1.0, neg.coefficient)
end)
describe("bin op", function()
local v2
before_each(function()
v2 = kiwi.Var("foo")
end)
it("mul", function()
for _, prod in ipairs({ v * 2.0, 2 * v }) do
assert.True(kiwi.is_term(prod))
assert.equal(v, prod.var)
assert.equal(2.0, prod.coefficient)
end
assert.error(function()
local _ = v * v2
end)
end)
it("div", function()
local quot = v / 2.0
assert.True(kiwi.is_term(quot))
assert.equal(v, quot.var)
assert.equal(0.5, quot.coefficient)
assert.error(function()
local _ = v / v2
end)
end)
it("add", function()
for _, sum in ipairs({ v + 2.0, 2 + v }) do
assert.True(kiwi.is_expression(sum))
assert.equal(2.0, sum.constant)
local terms = sum:terms()
assert.equal(1, #terms)
assert.equal(1.0, terms[1].coefficient)
assert.equal(v, terms[1].var)
end
local sum = v + v2
assert.True(kiwi.is_expression(sum))
assert.equal(0, sum.constant)
local terms = sum:terms()
assert.equal(2, #terms)
assert.equal(v, terms[1].var)
assert.equal(1.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(1.0, terms[2].coefficient)
assert.error(function()
local _ = v + "foo"
end)
assert.error(function()
local _ = v + {}
end)
end)
it("sub", function()
local constants = { -2, 2 }
for i, diff in ipairs({ v - 2.0, 2 - v }) do
local constant = constants[i]
assert.True(kiwi.is_expression(diff))
assert.equal(constant, diff.constant)
local terms = diff:terms()
assert.equal(1, #terms)
assert.equal(v, terms[1].var)
assert.equal(constant < 0 and 1 or -1, terms[1].coefficient)
end
local diff = v - v2
assert.True(kiwi.is_expression(diff))
assert.equal(0, diff.constant)
local terms = diff:terms()
assert.equal(2, #terms)
assert.equal(v, terms[1].var)
assert.equal(1.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(-1.0, terms[2].coefficient)
assert.error(function()
local _ = v - "foo"
end)
assert.error(function()
local _ = v - {}
end)
end)
it("constraint var op expr", function()
local ops = { "LE", "EQ", "GE" }
for i, meth in ipairs({ "le", "eq", "ge" }) do
local c = v[meth](v, v2 + 1)
assert.True(kiwi.is_constraint(c))
local e = c:expression()
local t = e:terms()
assert.equal(2, #t)
-- order can be randomized due to use of map
if t[1].var ~= v then
t[1], t[2] = t[2], t[1]
end
assert.equal(v, t[1].var)
assert.equal(1.0, t[1].coefficient)
assert.equal(v2, t[2].var)
assert.equal(-1.0, t[2].coefficient)
assert.equal(-1, e.constant)
assert.equal(ops[i], c:op())
assert.equal(kiwi.strength.REQUIRED, c:strength())
end
end)
it("constraint var op var", function()
for i, meth in ipairs({ "le", "eq", "ge" }) do
local c = v[meth](v, v2)
assert.True(kiwi.is_constraint(c))
local e = c:expression()
local t = e:terms()
assert.equal(2, #t)
-- order can be randomized due to use of map
if t[1].var ~= v then
t[1], t[2] = t[2], t[1]
end
assert.equal(v, t[1].var)
assert.equal(1.0, t[1].coefficient)
assert.equal(v2, t[2].var)
assert.equal(-1.0, t[2].coefficient)
assert.equal(0, e.constant)
end
end)
end)
end)
end)