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

36
.github/workflows/busted.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Busted
on: [push, pull_request]
jobs:
busted:
strategy:
fail-fast: false
matrix:
lua_version: ["luajit-2.1.0-beta3", "luajit-2.0.5", "luajit-openresty"]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup lua
uses: leafo/gh-actions-lua@v10
with:
luaVersion: ${{ matrix.lua_version }}
- name: Setup luarocks
uses: leafo/gh-actions-luarocks@v4
- name: Setup dependencies
run: |
luarocks install busted
luarocks install luacov-coveralls
- name: Build C library
run: make
- name: Run busted tests
run: busted -c -v
- name: Report test coverage
if: success()
continue-on-error: true
run: luacov-coveralls -e .luarocks -e spec
env:
COVERALLS_REPO_TOKEN: ${{ github.token }}

11
LICENSE Normal file
View File

@@ -0,0 +1,11 @@
Copyright 2024 John Luebs
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -248,8 +248,8 @@ void kiwi_var_set_value(KiwiVarRef var, double value) {
self->setValue(value);
}
int kiwi_var_eq(KiwiVarRef var, KiwiVarRef other) {
VariableRef self(var); // const defect in upstream
bool kiwi_var_eq(KiwiVarRef var, KiwiVarRef other) {
ConstVariableRef self(var); // const defect in upstream
const VariableRef other_ref(other);
return self && other_ref && self->equals(other_ref);

View File

@@ -56,7 +56,7 @@ const char* kiwi_var_name(KiwiVarRef var);
void kiwi_var_set_name(KiwiVarRef var, const char* name);
double kiwi_var_value(KiwiVarRef var);
void kiwi_var_set_value(KiwiVarRef var, double value);
int kiwi_var_eq(KiwiVarRef var, KiwiVarRef other);
bool kiwi_var_eq(KiwiVarRef var, KiwiVarRef other);
void kiwi_expression_del_vars(KiwiExpression* expr);

View File

@@ -48,7 +48,7 @@ typedef struct KiwiErr {
bool must_free;
} KiwiErr;
typedef struct KiwiSolver { unsigned error_mask; } KiwiSolver;
typedef struct KiwiSolver { unsigned error_mask_; } KiwiSolver;
KiwiVarRef kiwi_var_new(const char* name);
void kiwi_var_del(KiwiVarRef var);
@@ -58,7 +58,7 @@ const char* kiwi_var_name(KiwiVarRef var);
void kiwi_var_set_name(KiwiVarRef var, const char* name);
double kiwi_var_value(KiwiVarRef var);
void kiwi_var_set_value(KiwiVarRef var, double value);
int kiwi_var_eq(KiwiVarRef var, KiwiVarRef other);
bool kiwi_var_eq(KiwiVarRef var, KiwiVarRef other);
void kiwi_expression_del_vars(KiwiExpression* expr);
@@ -154,15 +154,31 @@ end
local Var = ffi.typeof("struct KiwiVarRefType") --[[@as kiwi.Var]]
kiwi.Var = Var
function kiwi.is_var(o)
return ffi_istype(Var, o)
end
local Term = ffi.typeof("struct KiwiTerm") --[[@as kiwi.Term]]
kiwi.Term = Term
function kiwi.is_term(o)
return ffi_istype(Term, o)
end
local Expression = ffi.typeof("struct KiwiExpression") --[[@as kiwi.Expression]]
kiwi.Expression = Expression
function kiwi.is_expression(o)
return ffi_istype(Expression, o)
end
local Constraint = ffi.typeof("struct KiwiConstraintRefType") --[[@as kiwi.Constraint]]
kiwi.Constraint = Constraint
function kiwi.is_constraint(o)
return ffi_istype(Constraint, o)
end
---@param expr kiwi.Expression
---@param var kiwi.Var
---@param coeff number?
@@ -404,7 +420,7 @@ do
return new_expr_pair(0.0, a, b)
end
elseif ffi_istype(Term, b) then
return new_expr_pair(0.0, b.var, a, b.coefficient)
return new_expr_pair(0.0, a, b.var, 1.0, b.coefficient)
elseif ffi_istype(Expression, b) then
return add_expr_term(b, a)
elseif type(b) == "number" then
@@ -414,7 +430,7 @@ do
end
function Var_mt.__sub(a, b)
return Var_mt.__add(a, -b)
return a + -b
end
function Var_mt:__tostring()
@@ -621,14 +637,17 @@ do
}
function Expression_mt.__new(T, terms, constant)
local e = ffi_gc(ffi_new(T, #terms), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]]
for i, t in ipairs(terms) do
local dt = e.terms_[i - 1] --[[@as kiwi.Term]]
dt.var = ckiwi.kiwi_var_clone(t.var)
dt.coefficient = t.coefficient
end
local term_count = terms and #terms or 0
local e = ffi_gc(ffi_new(T, term_count), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]]
e.term_count = term_count
e.constant = constant or 0.0
e.term_count = #terms
if terms then
for i, t in ipairs(terms) do
local dt = e.terms_[i - 1] --[[@as kiwi.Term]]
dt.var = ckiwi.kiwi_var_clone(t.var)
dt.coefficient = t.coefficient
end
end
return e
end
@@ -727,7 +746,7 @@ do
---@param solver kiwi.Solver
---@return kiwi.Constraint
function Constraint_cls:add_to(solver)
solver:add_constraints(self)
solver:add_constraint(self)
return self
end
@@ -737,7 +756,7 @@ do
---@param solver kiwi.Solver
---@return kiwi.Constraint
function Constraint_cls:remove_from(solver)
solver:remove_constraints(self)
solver:remove_constraint(self)
return self
end
@@ -881,16 +900,23 @@ do
end,
}
---@class kiwi.Error
---@field kind kiwi.ErrKind
---@field message string
---@field solver kiwi.Solver?
---@field item any?
kiwi.Error = Error_mt
function kiwi.is_error(o)
return type(o) == "table" and getmetatable(o) == Error_mt
end
---@param kind kiwi.ErrKind
---@param message string
---@param solver kiwi.Solver
---@param item any
---@return kiwi.Error
local function new_error(kind, message, solver, item)
---@class kiwi.Error
---@field kind kiwi.ErrKind
---@field message string
---@field solver kiwi.Solver?
---@field item any?
return setmetatable({
kind = kind,
message = message,
@@ -913,16 +939,16 @@ do
C.free(err)
end
local errdata = new_error(kind, message, solver, item)
local error_mask = solver and solver.error_mask_ or 0
return item,
band(solver.error_mask, lshift(1, kind --[[@as integer]])) == 0 and error(errdata)
band(error_mask, lshift(1, kind --[[@as integer]])) == 0 and error(errdata)
or errdata
end
return item
end
---@class kiwi.Solver: ffi.cdata*
---@field error_mask integer
---@overload fun(error_mask: integer?): kiwi.Solver
---@field package error_mask_ integer
---@overload fun(error_mask: (integer|(kiwi.ErrKind|number)[] )?): kiwi.Solver
local Solver_cls = {
--- Test whether a constraint is in the solver.
---@type fun(self: kiwi.Solver, constraint: kiwi.Constraint): boolean
@@ -951,6 +977,16 @@ do
dump = ckiwi.kiwi_solver_dump,
}
--- Sets the error mask for the solver.
---@param mask integer|(kiwi.ErrKind|number)[] the mask value or an array of kinds
---@param invert boolean? whether to invert the mask if an array was passed for mask
function Solver_cls:set_error_mask(mask, invert)
if type(mask) == "table" then
mask = kiwi.error_mask(mask, invert)
end
self.error_mask_ = mask
end
---@generic T
---@param solver kiwi.Solver
---@param items T|T[]
@@ -1095,10 +1131,17 @@ do
}
function Solver_mt:__new(error_mask)
if type(error_mask) == "table" then
error_mask = kiwi.error_mask(error_mask)
end
return ffi_gc(ckiwi.kiwi_solver_new(error_mask or 0), ckiwi.kiwi_solver_del)
end
kiwi.Solver = ffi.metatype("struct KiwiSolver", Solver_mt) --[[@as kiwi.Solver]]
function kiwi.is_solver(s)
return ffi_istype(kiwi.Solver, s)
end
end
return kiwi

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)