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:
36
.github/workflows/busted.yml
vendored
Normal file
36
.github/workflows/busted.yml
vendored
Normal 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
11
LICENSE
Normal 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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
79
kiwi.lua
79
kiwi.lua
@@ -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]]
|
||||
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
|
||||
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
|
||||
e.constant = constant or 0.0
|
||||
e.term_count = #terms
|
||||
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,
|
||||
}
|
||||
|
||||
---@param kind kiwi.ErrKind
|
||||
---@param message string
|
||||
---@param solver kiwi.Solver
|
||||
---@param item any
|
||||
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?
|
||||
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)
|
||||
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
111
spec/constraint_spec.lua
Normal 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
365
spec/solver_spec.lua
Normal 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
186
spec/var_spec.lua
Normal 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)
|
||||
Reference in New Issue
Block a user