Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix passing a client object and allow operations to be an any callable Lua object #13

Merged
merged 3 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.repository
strategy:
matrix:
LUA: ['luajit-2.0.5', 'tarantool']
LUA: ['luajit', 'tarantool']
fail-fast: false
runs-on: [ubuntu-latest]
steps:
Expand All @@ -25,10 +25,8 @@ jobs:
tarantool-version: '2.10'

- name: Setup LuaJIT (${{ matrix.LUA }})
if: matrix.LUA != 'tarantool'
uses: leafo/gh-actions-lua@v8
with:
luaVersion: ${{ matrix.LUA }}
if: matrix.LUA == 'luajit'
run: sudo apt install -y luajit

- name: Setup luarocks
run: sudo apt install -y luarocks
Expand All @@ -49,5 +47,5 @@ jobs:
DEV: ON

- name: Run tests with LuaJIT (${{ matrix.LUA }})
if: matrix.LUA != 'tarantool'
run: LUAJIT_BIN=$(pwd)/.lua/bin/luajit DEV=ON make test-luajit
if: matrix.LUA == 'luajit'
run: LUAJIT_BIN=luajit DEV=ON make test-luajit
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bump luacheck version.
- Allow chaining `luafun` iterators with iterators defined in Molly and vice versa.
- Using of SQL prepared statements in test examples.
- Generated operation can be any callable Lua object.

### Removed

### Fixed

- Executing `close` method in a `Client` instance (#2).
- list-append generator (#3).
- Passing a client object to a client's methods (#9).

[Unreleased]: https://github.com/ligurio/molly/compare/0.1.0...HEAD

Expand Down
69 changes: 45 additions & 24 deletions molly/client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ local clock = require('molly.clock')
local dev_checks = require('molly.dev_checks')
local log = require('molly.log')
local op_lib = require('molly.op')
local utils = require('molly.utils')

local shared_gen_state
local op_index = 1

local function process_operation(client, history, op, thread_id_str, thread_id)
dev_checks('<client>', '<history>', 'function|table', 'string', 'number')
local function process_operation(client, history, op, thread_id_str, thread_id, client_data)
dev_checks('<client>', '<history>', 'any', 'string', 'number')

if type(op) == 'function' then -- FIXME: check for callable object
if utils.is_callable(op) then
op = op()
end

Expand All @@ -28,7 +29,7 @@ local function process_operation(client, history, op, thread_id_str, thread_id)
op.time = clock.monotonic64()
log.debug('%-4s %s', thread_id_str, op_lib.to_string(op))
history:add(op)
local ok, res = pcall(client.invoke, client, op)
local ok, res = pcall(client.invoke, client, op, client_data)
if not ok then
log.warn('Process %d crashed (%s)', thread_id, res)
res.type = 'fail'
Expand All @@ -55,15 +56,16 @@ local function run_client(thread_id, opts)
local nth = math.random(1, table.getn(opts.nodes)) -- TODO: Use fun.cycle() and closure.
local addr = opts.nodes[nth]

local client_data = {}
log.debug('Opening connection by thread %d to DB (%s)', thread_id, addr)
local ok, err = pcall(client.open, client, addr)
local ok, err = pcall(client.open, client, addr, client_data)
if not ok then
log.info('ERROR: %s', err)
return false, err
end

log.debug('Setting up DB (%s) by thread %d', addr, thread_id)
ok, err = pcall(client.setup, client)
ok, err = pcall(client.setup, client, client_data)
if not ok then
log.info('ERROR: %s', err)
return false, err
Expand All @@ -81,7 +83,7 @@ local function run_client(thread_id, opts)
break
end
shared_gen_state = state
ok, err = pcall(process_operation, client, history, op, thread_id_str, thread_id)
ok, err = pcall(process_operation, client, history, op, thread_id_str, thread_id, client_data)
if ok == false then
error('Failed to process an operation', err)
end
Expand All @@ -92,14 +94,14 @@ local function run_client(thread_id, opts)
-- TODO: Add barrier here.

log.debug('Tearing down DB (%s) by thread %d', addr, thread_id)
ok, err = pcall(client.teardown, client)
ok, err = pcall(client.teardown, client, client_data)
if not ok then
log.info('ERROR: %s', err)
return false, err
end

log.debug('Closing connection to DB (%s) by thread %d', addr, thread_id)
ok, err = pcall(client.close, client)
ok, err = pcall(client.close, client, client_data)
if not ok then
log.info('ERROR: %s', err)
return false, err
Expand Down Expand Up @@ -128,26 +130,41 @@ local client_mt = {
--
-- Client must implement the following methods:
--
-- **open** - function that open a connection to a database instance. Function
-- must return a boolean value, true in case of success and false otherwise.
-- **open** - function that open a connection to a database
-- instance. Function must return a boolean value, true in case of
-- success and false otherwise. Two arguments are passed to the
-- function:
-- - `address` - an 'address' of the remote node
-- - `client_data` - a table with client's data
--
-- **setup** - function that set up a database instance. Function must return a
-- boolean value, true in case of success and false otherwise.
-- **setup** - function that set up a database instance. Function
-- must return a boolean value, true in case of success and false
-- otherwise. Single argument is passed to the function:
-- - `client_data` - a table with client's data
--
-- **invoke** - function that accept an operation and invoke it on database
-- instance, function should process user-defined types of operations and
-- execute intended actions on databases. Function must return an operation
-- after invokation.
-- **invoke** - function that accept an operation and invoke it
-- on database instance, function should process user-defined
-- types of operations and execute intended actions on databases.
-- Function must return an operation after invocation.
-- Two arguments are passed to the function:
-- - `op` - an operation generated by a test generator
-- - `client_data` - a table with client's data
--
-- **teardown** - function that tear down a database instance. Function must
-- return a boolean value, true in case of success and false otherwise.
-- **teardown** - function that tear down a database instance.
-- Function must return a boolean value, true in case of success
-- and false otherwise. Single argument is passed to the function:
-- - `client_data` - a table with client's data
--
-- **close** - function that close connection to a database instance. Function
-- must return a boolean value, true in case of success and false otherwise.
-- **close** - function that close connection to a database
-- instance. Function must return a boolean value, true in case of
-- success and false otherwise. and false otherwise. Single
-- argument is passed to the function:
-- - `client_data` - a table with client's data
--
-- In general it is recommended to raise an error in case of fatal errors like
-- failed database setup, teardown or connection and set status of operation to
-- 'fail' when key is not found in database table etc.
-- In general it is recommended to raise an error in case of fatal
-- errors like failed database setup, teardown or connection and
-- set status of operation to 'fail' when key is not found in
-- database table etc.
--
-- @return client
-- @usage
Expand All @@ -156,6 +173,10 @@ local client_mt = {
-- return true
-- end
--
-- @see molly.tests
-- @see molly.gen
-- @see molly.op
--
-- @function new
local function new()
return setmetatable({
Expand Down
3 changes: 2 additions & 1 deletion molly/op.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
-- not perform another operation; the invocation remains open for the rest of
-- the history.
--
-- We define each operation as a table, that contains following keys:
-- We define each operation as a table or a callable object that return
-- a table, that contains following keys:
--
-- - state, can be nil (invoke), true (ok) or false (fail);
-- - f is an action defined in a test, for example 'transfer', 'read' or
Expand Down
33 changes: 33 additions & 0 deletions molly/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,41 @@ local function pack(...)
}
end

local function is_callable(object)
if type(object) == 'function' then
return true
end

-- All objects with type `cdata` are allowed because there is
-- no easy way to get metatable.__call of object with type
-- `cdata`.
if type(object) == 'cdata' then
return true
end

local object_metatable = getmetatable(object)
if (type(object) == 'table' or type(object) == 'userdata') then
-- If metatable type is not `table` -> metatable is
-- protected -> cannot detect metamethod `__call` exists.
if object_metatable and
type(object_metatable) ~= 'table' then
return true
end

-- The `__call` metamethod can be only the `function`
-- and cannot be a `table`, `userdata` or `cdata`
-- with `__call` methamethod on its own.
if object_metatable and object_metatable.__call then
return type(object_metatable.__call) == 'function'
end
end

return false
end

return {
basename = basename,
is_callable = is_callable,
chdir = chdir,
cwd = cwd,
setenv = setenv,
Expand Down
5 changes: 5 additions & 0 deletions test/examples/sqlite-list-append.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ end
local sqlite_list_append = molly.client.new()

sqlite_list_append.open = function(self)
assert(type(self) == 'table')
self.db = assert(sqlite3.open_memory(), 'database handle is nil')
if self.db == nil then
error('database handle is nil')
Expand All @@ -49,6 +50,7 @@ sqlite_list_append.open = function(self)
end

sqlite_list_append.setup = function(self)
assert(type(self) == 'table')
assert(sqlite3.OK == self.db:exec('CREATE TABLE IF NOT EXISTS list_append (key INT NOT NULL, val INT)'))
self.insert_stmt = assert(self.db:prepare('INSERT INTO list_append VALUES (?, ?)'))
self.select_stmt = assert(self.db:prepare('SELECT key, val FROM list_append ORDER BY key'))
Expand All @@ -60,6 +62,7 @@ local IDX_MOP_KEY = 2
local IDX_MOP_VAL = 3

sqlite_list_append.invoke = function(self, op)
assert(type(self) == 'table')
local mop = op.value[1] -- TODO: Support more than one mop in operation.
local mop_key = mop[IDX_MOP_KEY]
local type = 'ok'
Expand All @@ -83,10 +86,12 @@ sqlite_list_append.invoke = function(self, op)
end

sqlite_list_append.teardown = function(self)
assert(type(self) == 'table')
return true
end

sqlite_list_append.close = function(self)
assert(type(self) == 'table')
-- Close database. All SQL statements prepared using
-- db:prepare() should have been finalized before this
-- function is called. The function returns sqlite3.OK on
Expand Down
5 changes: 5 additions & 0 deletions test/examples/sqlite-rw-register.lua
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ end
local sqlite_rw_register = molly.client.new()

sqlite_rw_register.open = function(self)
assert(type(self) == 'table')
self.db = assert(sqlite3.open_memory(), 'database handle is nil')
-- For explanation see https://www.sqlite.org/pragma.html
assert(sqlite3.OK == self.db:exec('PRAGMA journal_mode = WAL'))
Expand All @@ -58,6 +59,7 @@ sqlite_rw_register.open = function(self)
end

sqlite_rw_register.setup = function(self)
assert(type(self) == 'table')
assert(sqlite3.OK == self.db:exec('CREATE TABLE IF NOT EXISTS rw_register (id, val)'))
self.insert_stmt = assert(self.db:prepare('INSERT INTO rw_register VALUES (?, ?)'), 'statement prepare')
self.select_stmt = assert(self.db:prepare('SELECT val FROM rw_register WHERE id = ?'), 'statement prepare')
Expand All @@ -71,6 +73,7 @@ local OP_VAL = 3
local KEY_ID = 1

sqlite_rw_register.invoke = function(self, op)
assert(type(self) == 'table')
local val = op.value[1]
local type = 'ok'
if val[OP_TYPE] == 'r' then
Expand Down Expand Up @@ -99,10 +102,12 @@ sqlite_rw_register.invoke = function(self, op)
end

sqlite_rw_register.teardown = function(self)
assert(type(self) == 'table')
return true
end

sqlite_rw_register.close = function(self)
assert(type(self) == 'table')
-- Close database. All SQL statements prepared using
-- db:prepare() should have been finalized before this
-- function is called. The function returns sqlite3.OK on
Expand Down
50 changes: 49 additions & 1 deletion test/tests.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require('test.coverage').enable()

local math = require('math')
local os = require('os')
local ffi = require('ffi')

local helpers = require('test.helpers')
local test = require('test.tap').test('molly')
Expand All @@ -27,7 +28,7 @@ local utils = molly.utils
local seed = os.time()
math.randomseed(seed)

test:plan(13)
test:plan(14)

test:test('clock', function(test)
test:plan(5)
Expand Down Expand Up @@ -136,6 +137,53 @@ test:test('utils', function(test)
test:is(utils.basename('/home/sergeyb/sources/molly/README.md'), 'README.md', "utils.basename()")
end)

test:test("utils.is_callable", function(test)
test:plan(8)

local string_not_callable = 'str'
test:is(utils.is_callable(string_not_callable), false,
"string is not callable")

local number_not_callable = 9
test:is(utils.is_callable(number_not_callable), false,
"number is not callable")

local func_callable = function() end
test:is(utils.is_callable(func_callable), true, "func is callable")

local table_callable = setmetatable({}, {
__call = function() return 'op' end
})
test:is(utils.is_callable(table_callable), true, "table is callable")

local table_not_callable = setmetatable({}, {})
test:is(utils.is_callable(table_not_callable), false,
"table is not callable")

local userdata_callable = newproxy(true)
local mt = getmetatable(userdata_callable)
mt.__call = function() return 'op' end
test:is(utils.is_callable(userdata_callable), true, "userdata is callable")

local userdata_not_callable = newproxy(true)
mt = getmetatable(userdata_not_callable)
mt.__call = {}
test:is(utils.is_callable(userdata_not_callable), false,
"userdata is callable")

ffi.cdef[[
typedef struct
{
int data;
} test_check_struct_t;
]]
ffi.metatype('test_check_struct_t', {
__call = function(_, key) return key end
})
local cdata_callable = ffi.new('test_check_struct_t')
test:is(utils.is_callable(cdata_callable), true, "cdata is callable")
end)

local OP_TYPE = 1
local OP_VAL = 3

Expand Down
Loading