summaryrefslogtreecommitdiff
path: root/examples/redis-unstable/tests/unit/scripting.tcl
diff options
context:
space:
mode:
authorMitja Felicijan <mitja.felicijan@gmail.com>2026-01-21 22:40:55 +0100
committerMitja Felicijan <mitja.felicijan@gmail.com>2026-01-21 22:40:55 +0100
commit5d8dfe892a2ea89f706ee140c3bdcfd89fe03fda (patch)
tree1acdfa5220cd13b7be43a2a01368e80d306473ca /examples/redis-unstable/tests/unit/scripting.tcl
parentc7ab12bba64d9c20ccd79b132dac475f7bc3923e (diff)
downloadcrep-5d8dfe892a2ea89f706ee140c3bdcfd89fe03fda.tar.gz
Add Redis source code for testing
Diffstat (limited to 'examples/redis-unstable/tests/unit/scripting.tcl')
-rw-r--r--examples/redis-unstable/tests/unit/scripting.tcl2688
1 files changed, 2688 insertions, 0 deletions
diff --git a/examples/redis-unstable/tests/unit/scripting.tcl b/examples/redis-unstable/tests/unit/scripting.tcl
new file mode 100644
index 0000000..911f114
--- /dev/null
+++ b/examples/redis-unstable/tests/unit/scripting.tcl
@@ -0,0 +1,2688 @@
+#
+# Copyright (c) 2009-Present, Redis Ltd.
+# All rights reserved.
+#
+# Copyright (c) 2024-present, Valkey contributors.
+# All rights reserved.
+#
+# Licensed under your choice of (a) the Redis Source Available License 2.0
+# (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+# GNU Affero General Public License v3 (AGPLv3).
+#
+# Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information.
+#
+
+foreach is_eval {0 1} {
+
+if {$is_eval == 1} {
+ proc run_script {args} {
+ r eval {*}$args
+ }
+ proc run_script_ro {args} {
+ r eval_ro {*}$args
+ }
+ proc run_script_on_connection {args} {
+ [lindex $args 0] eval {*}[lrange $args 1 end]
+ }
+ proc kill_script {args} {
+ r script kill
+ }
+} else {
+ proc run_script {args} {
+ r function load replace [format "#!lua name=test\nredis.register_function('test', function(KEYS, ARGV)\n %s \nend)" [lindex $args 0]]
+ if {[r readingraw] eq 1} {
+ # read name
+ assert_equal {test} [r read]
+ }
+ r fcall test {*}[lrange $args 1 end]
+ }
+ proc run_script_ro {args} {
+ r function load replace [format "#!lua name=test\nredis.register_function{function_name='test', callback=function(KEYS, ARGV)\n %s \nend, flags={'no-writes'}}" [lindex $args 0]]
+ if {[r readingraw] eq 1} {
+ # read name
+ assert_equal {test} [r read]
+ }
+ r fcall_ro test {*}[lrange $args 1 end]
+ }
+ proc run_script_on_connection {args} {
+ set rd [lindex $args 0]
+ $rd function load replace [format "#!lua name=test\nredis.register_function('test', function(KEYS, ARGV)\n %s \nend)" [lindex $args 1]]
+ # read name
+ $rd read
+ $rd fcall test {*}[lrange $args 2 end]
+ }
+ proc kill_script {args} {
+ r function kill
+ }
+}
+
+start_server {tags {"scripting"}} {
+
+ if {$is_eval eq 1} {
+ test {Script - disallow write on OOM} {
+ r config set maxmemory 1
+
+ catch {[r eval "redis.call('set', 'x', 1)" 0]} e
+ assert_match {*command not allowed when used memory*} $e
+
+ r config set maxmemory 0
+ } {OK} {needs:config-maxmemory}
+ } ;# is_eval
+
+ test {EVAL - Does Lua interpreter replies to our requests?} {
+ run_script {return 'hello'} 0
+ } {hello}
+
+ test {EVAL - Return _G} {
+ run_script {return _G} 0
+ } {}
+
+ test {EVAL - Return table with a metatable that raise error} {
+ run_script {local a = {}; setmetatable(a,{__index=function() foo() end}) return a} 0
+ } {}
+
+ test {EVAL - Return table with a metatable that call redis} {
+ run_script {local a = {}; setmetatable(a,{__index=function() redis.call('set', 'x', '1') end}) return a} 1 x
+ # make sure x was not set
+ r get x
+ } {}
+
+ test {EVAL - Lua integer -> Redis protocol type conversion} {
+ run_script {return 100.5} 0
+ } {100}
+
+ test {EVAL - Lua string -> Redis protocol type conversion} {
+ run_script {return 'hello world'} 0
+ } {hello world}
+
+ test {EVAL - Lua true boolean -> Redis protocol type conversion} {
+ run_script {return true} 0
+ } {1}
+
+ test {EVAL - Lua false boolean -> Redis protocol type conversion} {
+ run_script {return false} 0
+ } {}
+
+ test {EVAL - Lua status code reply -> Redis protocol type conversion} {
+ run_script {return {ok='fine'}} 0
+ } {fine}
+
+ test {EVAL - Lua error reply -> Redis protocol type conversion} {
+ catch {
+ run_script {return {err='ERR this is an error'}} 0
+ } e
+ set _ $e
+ } {ERR this is an error}
+
+ test {EVAL - Lua table -> Redis protocol type conversion} {
+ run_script {return {1,2,3,'ciao',{1,2}}} 0
+ } {1 2 3 ciao {1 2}}
+
+ test {EVAL - Are the KEYS and ARGV arrays populated correctly?} {
+ run_script {return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}} 2 a{t} b{t} c{t} d{t}
+ } {a{t} b{t} c{t} d{t}}
+
+ test {EVAL - is Lua able to call Redis API?} {
+ r set mykey myval
+ run_script {return redis.call('get',KEYS[1])} 1 mykey
+ } {myval}
+
+ if {$is_eval eq 1} {
+ # eval sha is only relevant for is_eval Lua
+ test {EVALSHA - Can we call a SHA1 if already defined?} {
+ r evalsha fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey
+ } {myval}
+
+ test {EVALSHA_RO - Can we call a SHA1 if already defined?} {
+ r evalsha_ro fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey
+ } {myval}
+
+ test {EVALSHA - Can we call a SHA1 in uppercase?} {
+ r evalsha FD758D1589D044DD850A6F05D52F2EEFD27F033F 1 mykey
+ } {myval}
+
+ test {EVALSHA - Do we get an error on invalid SHA1?} {
+ catch {r evalsha NotValidShaSUM 0} e
+ set _ $e
+ } {NOSCRIPT*}
+
+ test {EVALSHA - Do we get an error on non defined SHA1?} {
+ catch {r evalsha ffd632c7d33e571e9f24556ebed26c3479a87130 0} e
+ set _ $e
+ } {NOSCRIPT*}
+ } ;# is_eval
+
+ test {EVAL - Redis integer -> Lua type conversion} {
+ r set x 0
+ run_script {
+ local foo = redis.pcall('incr',KEYS[1])
+ return {type(foo),foo}
+ } 1 x
+ } {number 1}
+
+ test {EVAL - Lua number -> Redis integer conversion} {
+ r del hash
+ run_script {
+ local foo = redis.pcall('hincrby','hash','field',200000000)
+ return {type(foo),foo}
+ } 0
+ } {number 200000000}
+
+ test {EVAL - Redis bulk -> Lua type conversion} {
+ r set mykey myval
+ run_script {
+ local foo = redis.pcall('get',KEYS[1])
+ return {type(foo),foo}
+ } 1 mykey
+ } {string myval}
+
+ test {EVAL - Redis multi bulk -> Lua type conversion} {
+ r del mylist
+ r rpush mylist a
+ r rpush mylist b
+ r rpush mylist c
+ run_script {
+ local foo = redis.pcall('lrange',KEYS[1],0,-1)
+ return {type(foo),foo[1],foo[2],foo[3],# foo}
+ } 1 mylist
+ } {table a b c 3}
+
+ test {EVAL - Redis status reply -> Lua type conversion} {
+ run_script {
+ local foo = redis.pcall('set',KEYS[1],'myval')
+ return {type(foo),foo['ok']}
+ } 1 mykey
+ } {table OK}
+
+ test {EVAL - Redis error reply -> Lua type conversion} {
+ r set mykey myval
+ run_script {
+ local foo = redis.pcall('incr',KEYS[1])
+ return {type(foo),foo['err']}
+ } 1 mykey
+ } {table {ERR value is not an integer or out of range}}
+
+ test {EVAL - Redis nil bulk reply -> Lua type conversion} {
+ r del mykey
+ run_script {
+ local foo = redis.pcall('get',KEYS[1])
+ return {type(foo),foo == false}
+ } 1 mykey
+ } {boolean 1}
+
+ test {EVAL - Is the Lua client using the currently selected DB?} {
+ r set mykey "this is DB 9"
+ r select 10
+ r set mykey "this is DB 10"
+ run_script {return redis.pcall('get',KEYS[1])} 1 mykey
+ } {this is DB 10} {singledb:skip}
+
+ test {EVAL - SELECT inside Lua should not affect the caller} {
+ # here we DB 10 is selected
+ r set mykey "original value"
+ run_script {return redis.pcall('select','9')} 0
+ set res [r get mykey]
+ r select 9
+ set res
+ } {original value} {singledb:skip}
+
+ if 0 {
+ test {EVAL - Script can't run more than configured time limit} {
+ r config set lua-time-limit 1
+ catch {
+ run_script {
+ local i = 0
+ while true do i=i+1 end
+ } 0
+ } e
+ set _ $e
+ } {*execution time*}
+ }
+
+ test {EVAL - Scripts do not block on blpop command} {
+ r lpush l 1
+ r lpop l
+ run_script {return redis.pcall('blpop','l',0)} 1 l
+ } {}
+
+ test {EVAL - Scripts do not block on brpop command} {
+ r lpush l 1
+ r lpop l
+ run_script {return redis.pcall('brpop','l',0)} 1 l
+ } {}
+
+ test {EVAL - Scripts do not block on brpoplpush command} {
+ r lpush empty_list1{t} 1
+ r lpop empty_list1{t}
+ run_script {return redis.pcall('brpoplpush','empty_list1{t}', 'empty_list2{t}',0)} 2 empty_list1{t} empty_list2{t}
+ } {}
+
+ test {EVAL - Scripts do not block on blmove command} {
+ r lpush empty_list1{t} 1
+ r lpop empty_list1{t}
+ run_script {return redis.pcall('blmove','empty_list1{t}', 'empty_list2{t}', 'LEFT', 'LEFT', 0)} 2 empty_list1{t} empty_list2{t}
+ } {}
+
+ test {EVAL - Scripts do not block on bzpopmin command} {
+ r zadd empty_zset 10 foo
+ r zmpop 1 empty_zset MIN
+ run_script {return redis.pcall('bzpopmin','empty_zset', 0)} 1 empty_zset
+ } {}
+
+ test {EVAL - Scripts do not block on bzpopmax command} {
+ r zadd empty_zset 10 foo
+ r zmpop 1 empty_zset MIN
+ run_script {return redis.pcall('bzpopmax','empty_zset', 0)} 1 empty_zset
+ } {}
+
+ test {EVAL - Scripts do not block on wait} {
+ run_script {return redis.pcall('wait','1','0')} 0
+ } {0}
+
+ test {EVAL - Scripts do not block on waitaof} {
+ r config set appendonly no
+ run_script {return redis.pcall('waitaof','0','1','0')} 0
+ } {0 0}
+
+ test {EVAL - Scripts do not block on XREAD with BLOCK option} {
+ r del s
+ r xgroup create s g $ MKSTREAM
+ set res [run_script {return redis.pcall('xread','STREAMS','s','$')} 1 s]
+ assert {$res eq {}}
+ run_script {return redis.pcall('xread','BLOCK',0,'STREAMS','s','$')} 1 s
+ } {}
+
+ test {EVAL - Scripts do not block on XREADGROUP with BLOCK option} {
+ set res [run_script {return redis.pcall('xreadgroup','group','g','c','STREAMS','s','>')} 1 s]
+ assert {$res eq {}}
+ run_script {return redis.pcall('xreadgroup','group','g','c','BLOCK',0,'STREAMS','s','>')} 1 s
+ } {}
+
+ test {EVAL - Scripts do not block on XREAD with BLOCK option -- non empty stream} {
+ r XADD s * a 1
+ set res [run_script {return redis.pcall('xread','BLOCK',0,'STREAMS','s','$')} 1 s]
+ assert {$res eq {}}
+
+ set res [run_script {return redis.pcall('xread','BLOCK',0,'STREAMS','s','0-0')} 1 s]
+ assert {[lrange [lindex $res 0 1 0 1] 0 1] eq {a 1}}
+ }
+
+ test {EVAL - Scripts do not block on XREADGROUP with BLOCK option -- non empty stream} {
+ r XADD s * b 2
+ set res [
+ run_script {return redis.pcall('xreadgroup','group','g','c','BLOCK',0,'STREAMS','s','>')} 1 s
+ ]
+ assert {[llength [lindex $res 0 1]] == 2}
+ lindex $res 0 1 0 1
+ } {a 1}
+
+ test {EVAL - Scripts can run non-deterministic commands} {
+ set e {}
+ catch {
+ run_script {redis.pcall('randomkey'); return redis.pcall('set','x','ciao')} 1 x
+ } e
+ set e
+ } {*OK*}
+
+ test {EVAL - No arguments to redis.call/pcall is considered an error} {
+ set e {}
+ catch {run_script {return redis.call()} 0} e
+ set e
+ } {*one argument*}
+
+ test {EVAL - redis.call variant raises a Lua error on Redis cmd error (1)} {
+ set e {}
+ catch {
+ run_script "redis.call('nosuchcommand')" 0
+ } e
+ set e
+ } {*Unknown Redis*}
+
+ test {EVAL - redis.call variant raises a Lua error on Redis cmd error (1)} {
+ set e {}
+ catch {
+ run_script "redis.call('get','a','b','c')" 0
+ } e
+ set e
+ } {*number of args*}
+
+ test {EVAL - redis.call variant raises a Lua error on Redis cmd error (1)} {
+ set e {}
+ r set foo bar
+ catch {
+ run_script {redis.call('lpush',KEYS[1],'val')} 1 foo
+ } e
+ set e
+ } {*against a key*}
+
+ test {EVAL - Test table unpack with invalid indexes} {
+ catch {run_script { return {unpack({1,2,3}, -2, 2147483647)} } 0} e
+ assert_match {*too many results to unpack*} $e
+ catch {run_script { return {unpack({1,2,3}, 0, 2147483647)} } 0} e
+ assert_match {*too many results to unpack*} $e
+ catch {run_script { return {unpack({1,2,3}, -2147483648, -2)} } 0} e
+ assert_match {*too many results to unpack*} $e
+ set res [run_script { return {unpack({1,2,3}, -1, -2)} } 0]
+ assert_match {} $res
+ set res [run_script { return {unpack({1,2,3}, 1, -1)} } 0]
+ assert_match {} $res
+
+ # unpack with range -1 to 5, verify nil indexes
+ set res [run_script {
+ local function unpack_to_list(t, i, j)
+ local n, v = select('#', unpack(t, i, j)), {unpack(t, i, j)}
+ for i = 1, n do v[i] = v[i] or '_NIL_' end
+ v.n = n
+ return v
+ end
+
+ return unpack_to_list({1,2,3}, -1, 5)
+ } 0]
+ assert_match {_NIL_ _NIL_ 1 2 3 _NIL_ _NIL_} $res
+
+ # unpack with negative range, verify nil indexes
+ set res [run_script {
+ local function unpack_to_list(t, i, j)
+ local n, v = select('#', unpack(t, i, j)), {unpack(t, i, j)}
+ for i = 1, n do v[i] = v[i] or '_NIL_' end
+ v.n = n
+ return v
+ end
+
+ return unpack_to_list({1,2,3}, -2147483648, -2147483646)
+ } 0]
+ assert_match {_NIL_ _NIL_ _NIL_} $res
+ } {}
+
+ test {EVAL - JSON numeric decoding} {
+ # We must return the table as a string because otherwise
+ # Redis converts floats to ints and we get 0 and 1023 instead
+ # of 0.0003 and 1023.2 as the parsed output.
+ run_script {return
+ table.concat(
+ cjson.decode(
+ "[0.0, -5e3, -1, 0.3e-3, 1023.2, 0e10]"), " ")
+ } 0
+ } {0 -5000 -1 0.0003 1023.2 0}
+
+ test {EVAL - JSON string decoding} {
+ run_script {local decoded = cjson.decode('{"keya": "a", "keyb": "b"}')
+ return {decoded.keya, decoded.keyb}
+ } 0
+ } {a b}
+
+ test {EVAL - JSON empty array decoding} {
+ # Default behavior
+ assert_equal "{}" [run_script {
+ return cjson.encode(cjson.decode('[]'))
+ } 0]
+ assert_equal "{}" [run_script {
+ cjson.decode_array_with_array_mt(false)
+ return cjson.encode(cjson.decode('[]'))
+ } 0]
+ assert_equal "{\"item\":{}}" [run_script {
+ cjson.decode_array_with_array_mt(false)
+ return cjson.encode(cjson.decode('{"item": []}'))
+ } 0]
+
+ # With array metatable
+ assert_equal "\[\]" [run_script {
+ cjson.decode_array_with_array_mt(true)
+ return cjson.encode(cjson.decode('[]'))
+ } 0]
+ assert_equal "{\"item\":\[\]}" [run_script {
+ cjson.decode_array_with_array_mt(true)
+ return cjson.encode(cjson.decode('{"item": []}'))
+ } 0]
+ }
+
+ test {EVAL - JSON empty array decoding after element removal} {
+ # Default: emptied array becomes object
+ assert_equal "{}" [run_script {
+ cjson.decode_array_with_array_mt(false)
+ local t = cjson.decode('[1, 2]')
+ -- emptying the array
+ t[1] = nil
+ t[2] = nil
+ return cjson.encode(t)
+ } 0]
+
+ # With array metatable: emptied array stays array
+ assert_equal "\[\]" [run_script {
+ cjson.decode_array_with_array_mt(true)
+ local t = cjson.decode('[1, 2]')
+ -- emptying the array
+ t[1] = nil
+ t[2] = nil
+ return cjson.encode(t)
+ } 0]
+ }
+
+ test {EVAL - cjson array metatable modification should be readonly} {
+ catch {
+ run_script {
+ cjson.decode_array_with_array_mt(true)
+ local t = cjson.decode('[]')
+ getmetatable(t).__is_cjson_array = function() return 1 end
+ return cjson.encode(t)
+ } 0
+ } e
+ set _ $e
+ } {*Attempt to modify a readonly table*}
+
+ test {EVAL - JSON smoke test} {
+ run_script {
+ local some_map = {
+ s1="Some string",
+ n1=100,
+ a1={"Some","String","Array"},
+ nil1=nil,
+ b1=true,
+ b2=false}
+ local encoded = cjson.encode(some_map)
+ local decoded = cjson.decode(encoded)
+ assert(table.concat(some_map) == table.concat(decoded))
+
+ cjson.encode_keep_buffer(false)
+ encoded = cjson.encode(some_map)
+ decoded = cjson.decode(encoded)
+ assert(table.concat(some_map) == table.concat(decoded))
+
+ -- Table with numeric keys
+ local table1 = {one="one", [1]="one"}
+ encoded = cjson.encode(table1)
+ decoded = cjson.decode(encoded)
+ assert(decoded["one"] == table1["one"])
+ assert(decoded["1"] == table1[1])
+
+ -- Array
+ local array1 = {[1]="one", [2]="two"}
+ encoded = cjson.encode(array1)
+ decoded = cjson.decode(encoded)
+ assert(table.concat(array1) == table.concat(decoded))
+
+ -- Invalid keys
+ local invalid_map = {}
+ invalid_map[false] = "false"
+ local ok, encoded = pcall(cjson.encode, invalid_map)
+ assert(ok == false)
+
+ -- Max depth
+ cjson.encode_max_depth(1)
+ ok, encoded = pcall(cjson.encode, some_map)
+ assert(ok == false)
+
+ cjson.decode_max_depth(1)
+ ok, decoded = pcall(cjson.decode, '{"obj": {"array": [1,2,3,4]}}')
+ assert(ok == false)
+
+ -- Invalid numbers
+ ok, encoded = pcall(cjson.encode, {num1=0/0})
+ assert(ok == false)
+ cjson.encode_invalid_numbers(true)
+ ok, encoded = pcall(cjson.encode, {num1=0/0})
+ assert(ok == true)
+
+ -- Restore defaults
+ cjson.decode_max_depth(1000)
+ cjson.encode_max_depth(1000)
+ cjson.encode_invalid_numbers(false)
+ } 0
+ }
+
+ test {EVAL - cmsgpack can pack double?} {
+ run_script {local encoded = cmsgpack.pack(0.1)
+ local h = ""
+ for i = 1, #encoded do
+ h = h .. string.format("%02x",string.byte(encoded,i))
+ end
+ return h
+ } 0
+ } {cb3fb999999999999a}
+
+ test {EVAL - cmsgpack can pack negative int64?} {
+ run_script {local encoded = cmsgpack.pack(-1099511627776)
+ local h = ""
+ for i = 1, #encoded do
+ h = h .. string.format("%02x",string.byte(encoded,i))
+ end
+ return h
+ } 0
+ } {d3ffffff0000000000}
+
+ test {EVAL - cmsgpack pack/unpack smoke test} {
+ run_script {
+ local str_lt_32 = string.rep("x", 30)
+ local str_lt_255 = string.rep("x", 250)
+ local str_lt_65535 = string.rep("x", 65530)
+ local str_long = string.rep("x", 100000)
+ local array_lt_15 = {1, 2, 3, 4, 5}
+ local array_lt_65535 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}
+ local array_big = {}
+ for i=1, 100000 do
+ array_big[i] = i
+ end
+ local map_lt_15 = {a=1, b=2}
+ local map_big = {}
+ for i=1, 100000 do
+ map_big[tostring(i)] = i
+ end
+ local some_map = {
+ s1=str_lt_32,
+ s2=str_lt_255,
+ s3=str_lt_65535,
+ s4=str_long,
+ d1=0.1,
+ i1=1,
+ i2=250,
+ i3=65530,
+ i4=100000,
+ i5=2^40,
+ i6=-1,
+ i7=-120,
+ i8=-32000,
+ i9=-100000,
+ i10=-3147483648,
+ a1=array_lt_15,
+ a2=array_lt_65535,
+ a3=array_big,
+ m1=map_lt_15,
+ m2=map_big,
+ b1=false,
+ b2=true,
+ n=nil
+ }
+ local encoded = cmsgpack.pack(some_map)
+ local decoded = cmsgpack.unpack(encoded)
+ assert(table.concat(some_map) == table.concat(decoded))
+ local offset, decoded_one = cmsgpack.unpack_one(encoded, 0)
+ assert(table.concat(some_map) == table.concat(decoded_one))
+ assert(offset == -1)
+
+ local encoded_multiple = cmsgpack.pack(str_lt_32, str_lt_255, str_lt_65535, str_long)
+ local offset, obj = cmsgpack.unpack_limit(encoded_multiple, 1, 0)
+ assert(obj == str_lt_32)
+ offset, obj = cmsgpack.unpack_limit(encoded_multiple, 1, offset)
+ assert(obj == str_lt_255)
+ offset, obj = cmsgpack.unpack_limit(encoded_multiple, 1, offset)
+ assert(obj == str_lt_65535)
+ offset, obj = cmsgpack.unpack_limit(encoded_multiple, 1, offset)
+ assert(obj == str_long)
+ assert(offset == -1)
+ } 0
+ }
+
+ test {EVAL - cmsgpack can pack and unpack circular references?} {
+ run_script {local a = {x=nil,y=5}
+ local b = {x=a}
+ a['x'] = b
+ local encoded = cmsgpack.pack(a)
+ local h = ""
+ -- cmsgpack encodes to a depth of 16, but can't encode
+ -- references, so the encoded object has a deep copy recursive
+ -- depth of 16.
+ for i = 1, #encoded do
+ h = h .. string.format("%02x",string.byte(encoded,i))
+ end
+ -- when unpacked, re.x.x != re because the unpack creates
+ -- individual tables down to a depth of 16.
+ -- (that's why the encoded output is so large)
+ local re = cmsgpack.unpack(encoded)
+ assert(re)
+ assert(re.x)
+ assert(re.x.x.y == re.y)
+ assert(re.x.x.x.x.y == re.y)
+ assert(re.x.x.x.x.x.x.y == re.y)
+ assert(re.x.x.x.x.x.x.x.x.x.x.y == re.y)
+ -- maximum working depth:
+ assert(re.x.x.x.x.x.x.x.x.x.x.x.x.x.x.y == re.y)
+ -- now the last x would be b above and has no y
+ assert(re.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x)
+ -- so, the final x.x is at the depth limit and was assigned nil
+ assert(re.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x == nil)
+ return {h, re.x.x.x.x.x.x.x.x.y == re.y, re.y == 5}
+ } 0
+ } {82a17905a17881a17882a17905a17881a17882a17905a17881a17882a17905a17881a17882a17905a17881a17882a17905a17881a17882a17905a17881a17882a17905a17881a178c0 1 1}
+
+ test {EVAL - Numerical sanity check from bitop} {
+ run_script {assert(0x7fffffff == 2147483647, "broken hex literals");
+ assert(0xffffffff == -1 or 0xffffffff == 2^32-1,
+ "broken hex literals");
+ assert(tostring(-1) == "-1", "broken tostring()");
+ assert(tostring(0xffffffff) == "-1" or
+ tostring(0xffffffff) == "4294967295",
+ "broken tostring()")
+ } 0
+ } {}
+
+ test {EVAL - Verify minimal bitop functionality} {
+ run_script {assert(bit.tobit(1) == 1);
+ assert(bit.band(1) == 1);
+ assert(bit.bxor(1,2) == 3);
+ assert(bit.bor(1,2,4,8,16,32,64,128) == 255)
+ } 0
+ } {}
+
+ test {EVAL - Able to parse trailing comments} {
+ run_script {return 'hello' --trailing comment} 0
+ } {hello}
+
+ test {EVAL_RO - Successful case} {
+ r set foo bar
+ assert_equal bar [run_script_ro {return redis.call('get', KEYS[1]);} 1 foo]
+ }
+
+ test {EVAL_RO - Cannot run write commands} {
+ r set foo bar
+ catch {run_script_ro {redis.call('del', KEYS[1]);} 1 foo} e
+ set e
+ } {ERR Write commands are not allowed from read-only scripts*}
+
+ if {$is_eval eq 1} {
+ # script command is only relevant for is_eval Lua
+ test {SCRIPTING FLUSH - is able to clear the scripts cache?} {
+ r set mykey myval
+
+ r script load {return redis.call('get',KEYS[1])}
+ set v [r evalsha fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey]
+ assert_equal $v myval
+ r script flush
+ assert_error {NOSCRIPT*} {r evalsha fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey}
+
+ r eval {return redis.call('get',KEYS[1])} 1 mykey
+ set v [r evalsha fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey]
+ assert_equal $v myval
+ r script flush
+ assert_error {NOSCRIPT*} {r evalsha fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey}
+ }
+
+ test {SCRIPTING FLUSH ASYNC} {
+ for {set j 0} {$j < 100} {incr j} {
+ r script load "return $j"
+ }
+ assert { [string match "*number_of_cached_scripts:100*" [r info Memory]] }
+ r script flush async
+ assert { [string match "*number_of_cached_scripts:0*" [r info Memory]] }
+ }
+
+ test {SCRIPT EXISTS - can detect already defined scripts?} {
+ r eval "return 1+1" 0
+ r script exists a27e7e8a43702b7046d4f6a7ccf5b60cef6b9bd9 a27e7e8a43702b7046d4f6a7ccf5b60cef6b9bda
+ } {1 0}
+
+ test {SCRIPT LOAD - is able to register scripts in the scripting cache} {
+ list \
+ [r script load "return 'loaded'"] \
+ [r evalsha b534286061d4b9e4026607613b95c06c06015ae8 0]
+ } {b534286061d4b9e4026607613b95c06c06015ae8 loaded}
+
+ test "SORT is normally not alpha re-ordered for the scripting engine" {
+ r del myset
+ r sadd myset 1 2 3 4 10
+ r eval {return redis.call('sort',KEYS[1],'desc')} 1 myset
+ } {10 4 3 2 1} {cluster:skip}
+
+ test "SORT BY <constant> output gets ordered for scripting" {
+ r del myset
+ r sadd myset a b c d e f g h i l m n o p q r s t u v z aa aaa azz
+ r eval {return redis.call('sort',KEYS[1],'by','_')} 1 myset
+ } {a aa aaa azz b c d e f g h i l m n o p q r s t u v z} {cluster:skip}
+
+ test "SORT BY <constant> with GET gets ordered for scripting" {
+ r del myset
+ r sadd myset a b c
+ r eval {return redis.call('sort',KEYS[1],'by','_','get','#','get','_:*')} 1 myset
+ } {a {} b {} c {}} {cluster:skip}
+ } ;# is_eval
+
+ test "redis.sha1hex() implementation" {
+ list [run_script {return redis.sha1hex('')} 0] \
+ [run_script {return redis.sha1hex('Pizza & Mandolino')} 0]
+ } {da39a3ee5e6b4b0d3255bfef95601890afd80709 74822d82031af7493c20eefa13bd07ec4fada82f}
+
+ test "Measures elapsed time os.clock()" {
+ set escaped [run_script {
+ local start = os.clock()
+ while os.clock() - start < 1 do end
+ return {double = os.clock() - start}
+ } 0]
+ assert_morethan_equal $escaped 1 ;# 1 second
+ }
+
+ test "Prohibit dangerous lua methods in sandbox" {
+ assert_equal "" [run_script {
+ local allowed_methods = {"clock"}
+ -- Find a value from a tuple and return the position.
+ local indexOf = function(tuple, value)
+ for i, v in ipairs(tuple) do
+ if v == value then return i end
+ end
+ return nil
+ end
+ -- Check for disallowed methods and verify all allowed methods exist.
+ -- If an allowed method is found, it's removed from 'allowed_methods'.
+ -- If 'allowed_methods' is empty at the end, all allowed methods were found.
+ for key, value in pairs(os) do
+ local index = indexOf(allowed_methods, key)
+ if index == nil or type(value) ~= "function" then
+ return "Disallowed "..type(value)..":"..key
+ end
+ table.remove(allowed_methods, index)
+ end
+ if #allowed_methods ~= 0 then
+ return "Expected method not found: "..table.concat(allowed_methods, ",")
+ end
+ return ""
+ } 0]
+ }
+
+ test "Verify execution of prohibit dangerous Lua methods will fail" {
+ assert_error {ERR *attempt to call field 'execute'*} {run_script {os.execute()} 0}
+ assert_error {ERR *attempt to call field 'exit'*} {run_script {os.exit()} 0}
+ assert_error {ERR *attempt to call field 'getenv'*} {run_script {os.getenv()} 0}
+ assert_error {ERR *attempt to call field 'remove'*} {run_script {os.remove()} 0}
+ assert_error {ERR *attempt to call field 'rename'*} {run_script {os.rename()} 0}
+ assert_error {ERR *attempt to call field 'setlocale'*} {run_script {os.setlocale()} 0}
+ assert_error {ERR *attempt to call field 'tmpname'*} {run_script {os.tmpname()} 0}
+ }
+
+ test {Globals protection reading an undeclared global variable} {
+ catch {run_script {return a} 0} e
+ set e
+ } {ERR *attempted to access * global*}
+
+ test {Globals protection setting an undeclared global*} {
+ catch {run_script {a=10} 0} e
+ set e
+ } {ERR *Attempt to modify a readonly table*}
+
+ test {lua bit.tohex bug} {
+ set res [run_script {return bit.tohex(65535, -2147483648)} 0]
+ r ping
+ set res
+ } {0000FFFF}
+
+ test {Test an example script DECR_IF_GT} {
+ set decr_if_gt {
+ local current
+
+ current = redis.call('get',KEYS[1])
+ if not current then return nil end
+ if current > ARGV[1] then
+ return redis.call('decr',KEYS[1])
+ else
+ return redis.call('get',KEYS[1])
+ end
+ }
+ r set foo 5
+ set res {}
+ lappend res [run_script $decr_if_gt 1 foo 2]
+ lappend res [run_script $decr_if_gt 1 foo 2]
+ lappend res [run_script $decr_if_gt 1 foo 2]
+ lappend res [run_script $decr_if_gt 1 foo 2]
+ lappend res [run_script $decr_if_gt 1 foo 2]
+ set res
+ } {4 3 2 2 2}
+
+ if {$is_eval eq 1} {
+ # random handling is only relevant for is_eval Lua
+ test {random numbers are random now} {
+ set rand1 [r eval {return tostring(math.random())} 0]
+ wait_for_condition 100 1 {
+ $rand1 ne [r eval {return tostring(math.random())} 0]
+ } else {
+ fail "random numbers should be random, now it's fixed value"
+ }
+ }
+
+ test {Scripting engine PRNG can be seeded correctly} {
+ set rand1 [r eval {
+ math.randomseed(ARGV[1]); return tostring(math.random())
+ } 0 10]
+ set rand2 [r eval {
+ math.randomseed(ARGV[1]); return tostring(math.random())
+ } 0 10]
+ set rand3 [r eval {
+ math.randomseed(ARGV[1]); return tostring(math.random())
+ } 0 20]
+ assert_equal $rand1 $rand2
+ assert {$rand2 ne $rand3}
+ }
+ } ;# is_eval
+
+ test {EVAL does not leak in the Lua stack} {
+ r script flush ;# reset Lua VM
+ r set x 0
+ # Use a non blocking client to speedup the loop.
+ set rd [redis_deferring_client]
+ for {set j 0} {$j < 10000} {incr j} {
+ run_script_on_connection $rd {return redis.call("incr",KEYS[1])} 1 x
+ }
+ for {set j 0} {$j < 10000} {incr j} {
+ $rd read
+ }
+ assert {[s used_memory_lua] < 1024*100}
+ $rd close
+ r get x
+ } {10000}
+
+ if {$is_eval eq 1} {
+ test {SPOP: We can call scripts rewriting client->argv from Lua} {
+ set repl [attach_to_replication_stream]
+ #this sadd operation is for external-cluster test. If myset doesn't exist, 'del myset' won't get propagated.
+ r sadd myset ppp
+ r del myset
+ r sadd myset a b c
+ assert {[r eval {return redis.call('spop', 'myset')} 0] ne {}}
+ assert {[r eval {return redis.call('spop', 'myset', 1)} 0] ne {}}
+ assert {[r eval {return redis.call('spop', KEYS[1])} 1 myset] ne {}}
+ # this one below should not be replicated
+ assert {[r eval {return redis.call('spop', KEYS[1])} 1 myset] eq {}}
+ r set trailingkey 1
+ assert_replication_stream $repl {
+ {select *}
+ {sadd *}
+ {del *}
+ {sadd *}
+ {srem myset *}
+ {srem myset *}
+ {srem myset *}
+ {set *}
+ }
+ close_replication_stream $repl
+ } {} {needs:repl}
+
+ test {MGET: mget shouldn't be propagated in Lua} {
+ set repl [attach_to_replication_stream]
+ r mset a{t} 1 b{t} 2 c{t} 3 d{t} 4
+ #read-only, won't be replicated
+ assert {[r eval {return redis.call('mget', 'a{t}', 'b{t}', 'c{t}', 'd{t}')} 0] eq {1 2 3 4}}
+ r set trailingkey 2
+ assert_replication_stream $repl {
+ {select *}
+ {mset *}
+ {set *}
+ }
+ close_replication_stream $repl
+ } {} {needs:repl}
+
+ test {EXPIRE: We can call scripts rewriting client->argv from Lua} {
+ set repl [attach_to_replication_stream]
+ r set expirekey 1
+ #should be replicated as EXPIREAT
+ assert {[r eval {return redis.call('expire', KEYS[1], ARGV[1])} 1 expirekey 3] eq 1}
+
+ assert_replication_stream $repl {
+ {select *}
+ {set *}
+ {pexpireat expirekey *}
+ }
+ close_replication_stream $repl
+ } {} {needs:repl}
+
+ test {INCRBYFLOAT: We can call scripts expanding client->argv from Lua} {
+ # coverage for scripts calling commands that expand the argv array
+ # an attempt to add coverage for a possible bug in luaArgsToRedisArgv
+ # this test needs a fresh server so that lua_argv_size is 0.
+ # glibc realloc can return the same pointer even when the size changes
+ # still this test isn't able to trigger the issue, but we keep it anyway.
+ start_server {tags {"scripting"}} {
+ set repl [attach_to_replication_stream]
+ # a command with 5 argsument
+ r eval {redis.call('hmget', KEYS[1], 1, 2, 3)} 1 key
+ # then a command with 3 that is replicated as one with 4
+ r eval {redis.call('incrbyfloat', KEYS[1], 1)} 1 key
+ # then a command with 4 args
+ r eval {redis.call('set', KEYS[1], '1', 'KEEPTTL')} 1 key
+
+ assert_replication_stream $repl {
+ {select *}
+ {set key 1 KEEPTTL}
+ {set key 1 KEEPTTL}
+ }
+ close_replication_stream $repl
+ }
+ } {} {needs:repl}
+
+ } ;# is_eval
+
+ test {Call Redis command with many args from Lua (issue #1764)} {
+ run_script {
+ local i
+ local x={}
+ redis.call('del','mylist')
+ for i=1,100 do
+ table.insert(x,i)
+ end
+ redis.call('rpush','mylist',unpack(x))
+ return redis.call('lrange','mylist',0,-1)
+ } 1 mylist
+ } {1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100}
+
+ test {Number conversion precision test (issue #1118)} {
+ run_script {
+ local value = 9007199254740991
+ redis.call("set","foo",value)
+ return redis.call("get","foo")
+ } 1 foo
+ } {9007199254740991}
+
+ test {String containing number precision test (regression of issue #1118)} {
+ run_script {
+ redis.call("set", "key", "12039611435714932082")
+ return redis.call("get", "key")
+ } 1 key
+ } {12039611435714932082}
+
+ test {Verify negative arg count is error instead of crash (issue #1842)} {
+ catch { run_script { return "hello" } -12 } e
+ set e
+ } {ERR Number of keys can't be negative}
+
+ test {Scripts can handle commands with incorrect arity} {
+ assert_error "ERR Wrong number of args calling Redis command from script*" {run_script "redis.call('set','invalid')" 0}
+ assert_error "ERR Wrong number of args calling Redis command from script*" {run_script "redis.call('incr')" 0}
+ }
+
+ test {Correct handling of reused argv (issue #1939)} {
+ run_script {
+ for i = 0, 10 do
+ redis.call('SET', 'a{t}', '1')
+ redis.call('MGET', 'a{t}', 'b{t}', 'c{t}')
+ redis.call('EXPIRE', 'a{t}', 0)
+ redis.call('GET', 'a{t}')
+ redis.call('MGET', 'a{t}', 'b{t}', 'c{t}')
+ end
+ } 3 a{t} b{t} c{t}
+ }
+
+ test {Functions in the Redis namespace are able to report errors} {
+ catch {
+ run_script {
+ redis.sha1hex()
+ } 0
+ } e
+ set e
+ } {*wrong number*}
+
+ test {CLUSTER RESET can not be invoke from within a script} {
+ catch {
+ run_script {
+ redis.call('cluster', 'reset', 'hard')
+ } 0
+ } e
+ set _ $e
+ } {*command is not allowed*}
+
+ test {Script with RESP3 map} {
+ set expected_dict [dict create field value]
+ set expected_list [list field value]
+
+ # Sanity test for RESP3 without scripts
+ r HELLO 3
+ r hset hash field value
+ set res [r hgetall hash]
+ assert_equal $res $expected_dict
+
+ # Test RESP3 client with script in both RESP2 and RESP3 modes
+ set res [run_script {redis.setresp(3); return redis.call('hgetall', KEYS[1])} 1 hash]
+ assert_equal $res $expected_dict
+ set res [run_script {redis.setresp(2); return redis.call('hgetall', KEYS[1])} 1 hash]
+ assert_equal $res $expected_list
+
+ # Test RESP2 client with script in both RESP2 and RESP3 modes
+ r HELLO 2
+ set res [run_script {redis.setresp(3); return redis.call('hgetall', KEYS[1])} 1 hash]
+ assert_equal $res $expected_list
+ set res [run_script {redis.setresp(2); return redis.call('hgetall', KEYS[1])} 1 hash]
+ assert_equal $res $expected_list
+ } {} {resp3}
+
+ if {!$::log_req_res} { # this test creates a huge nested array which python can't handle (RecursionError: maximum recursion depth exceeded in comparison)
+ test {Script return recursive object} {
+ r readraw 1
+ set res [run_script {local a = {}; local b = {a}; a[1] = b; return a} 0]
+ # drain the response
+ while {true} {
+ if {$res == "-ERR reached lua stack limit"} {
+ break
+ }
+ assert_equal $res "*1"
+ set res [r read]
+ }
+ r readraw 0
+ # make sure the connection is still valid
+ assert_equal [r ping] {PONG}
+ }
+ }
+
+ test {Script check unpack with massive arguments} {
+ run_script {
+ local a = {}
+ for i=1,7999 do
+ a[i] = 1
+ end
+ return redis.call("lpush", "l", unpack(a))
+ } 1 l
+ } {7999}
+
+ test "Script read key with expiration set" {
+ r SET key value EX 10
+ assert_equal [run_script {
+ if redis.call("EXISTS", "key") then
+ return redis.call("GET", "key")
+ else
+ return redis.call("EXISTS", "key")
+ end
+ } 1 key] "value"
+ }
+
+ test "Script del key with expiration set" {
+ r SET key value EX 10
+ assert_equal [run_script {
+ redis.call("DEL", "key")
+ return redis.call("EXISTS", "key")
+ } 1 key] 0
+ }
+
+ test "Script ACL check" {
+ r acl setuser bob on {>123} {+@scripting} {+set} {~x*}
+ assert_equal [r auth bob 123] {OK}
+
+ # Check permission granted
+ assert_equal [run_script {
+ return redis.acl_check_cmd('set','xx',1)
+ } 1 xx] 1
+
+ # Check permission denied unauthorised command
+ assert_equal [run_script {
+ return redis.acl_check_cmd('hset','xx','f',1)
+ } 1 xx] {}
+
+ # Check permission denied unauthorised key
+ # Note: we don't pass the "yy" key as an argument to the script so key acl checks won't block the script
+ assert_equal [run_script {
+ return redis.acl_check_cmd('set','yy',1)
+ } 0] {}
+
+ # Check error due to invalid command
+ assert_error {ERR *Invalid command passed to redis.acl_check_cmd()*} {run_script {
+ return redis.acl_check_cmd('invalid-cmd','arg')
+ } 0}
+ }
+
+ test "Binary code loading failed" {
+ assert_error {ERR *attempt to call a nil value*} {run_script {
+ return loadstring(string.dump(function() return 1 end))()
+ } 0}
+ }
+
+ test "Try trick global protection 1" {
+ catch {
+ run_script {
+ setmetatable(_G, {})
+ } 0
+ } e
+ set _ $e
+ } {*Attempt to modify a readonly table*}
+
+ test "Try trick global protection 2" {
+ catch {
+ run_script {
+ local g = getmetatable(_G)
+ g.__index = {}
+ } 0
+ } e
+ set _ $e
+ } {*Attempt to modify a readonly table*}
+
+ test "Try trick global protection 3" {
+ catch {
+ run_script {
+ redis = function() return 1 end
+ } 0
+ } e
+ set _ $e
+ } {*Attempt to modify a readonly table*}
+
+ test "Try trick global protection 4" {
+ catch {
+ run_script {
+ _G = {}
+ } 0
+ } e
+ set _ $e
+ } {*Attempt to modify a readonly table*}
+
+ test "Try trick readonly table on redis table" {
+ catch {
+ run_script {
+ redis.call = function() return 1 end
+ } 0
+ } e
+ set _ $e
+ } {*Attempt to modify a readonly table*}
+
+ test "Try trick readonly table on json table" {
+ catch {
+ run_script {
+ cjson.encode = function() return 1 end
+ } 0
+ } e
+ set _ $e
+ } {*Attempt to modify a readonly table*}
+
+ test "Try trick readonly table on cmsgpack table" {
+ catch {
+ run_script {
+ cmsgpack.pack = function() return 1 end
+ } 0
+ } e
+ set _ $e
+ } {*Attempt to modify a readonly table*}
+
+ test "Try trick readonly table on bit table" {
+ catch {
+ run_script {
+ bit.lshift = function() return 1 end
+ } 0
+ } e
+ set _ $e
+ } {*Attempt to modify a readonly table*}
+
+ test "Try trick readonly table on basic types metatable" {
+ # Run the following scripts for basic types. Either getmetatable()
+ # should return nil or the metatable must be readonly.
+ set scripts {
+ {getmetatable(nil).__index = function() return 1 end}
+ {getmetatable('').__index = function() return 1 end}
+ {getmetatable(123.222).__index = function() return 1 end}
+ {getmetatable(true).__index = function() return 1 end}
+ {getmetatable(function() return 1 end).__index = function() return 1 end}
+ {getmetatable(coroutine.create(function() return 1 end)).__index = function() return 1 end}
+ }
+
+ foreach code $scripts {
+ catch {run_script $code 0} e
+ assert {
+ [string match "*attempt to index a nil value script*" $e] ||
+ [string match "*Attempt to modify a readonly table*" $e]
+ }
+ }
+ }
+
+ test "Test loadfile are not available" {
+ catch {
+ run_script {
+ loadfile('some file')
+ } 0
+ } e
+ set _ $e
+ } {*Script attempted to access nonexistent global variable 'loadfile'*}
+
+ test "Test dofile are not available" {
+ catch {
+ run_script {
+ dofile('some file')
+ } 0
+ } e
+ set _ $e
+ } {*Script attempted to access nonexistent global variable 'dofile'*}
+
+ test "Test print are not available" {
+ catch {
+ run_script {
+ print('some data')
+ } 0
+ } e
+ set _ $e
+ } {*Script attempted to access nonexistent global variable 'print'*}
+}
+
+# start a new server to test the large-memory tests
+start_server {tags {"scripting external:skip large-memory"}} {
+ test {EVAL - JSON string encoding a string larger than 2GB} {
+ run_script {
+ local s = string.rep("a", 1024 * 1024 * 1024)
+ return #cjson.encode(s..s..s)
+ } 0
+ } {3221225474} ;# length includes two double quotes at both ends
+
+ test {EVAL - Test long escape sequences for strings} {
+ run_script {
+ -- Generate 1gb '==...==' separator
+ local s = string.rep('=', 1024 * 1024)
+ local t = {} for i=1,1024 do t[i] = s end
+ local sep = table.concat(t)
+ collectgarbage('collect')
+
+ local code = table.concat({'return [',sep,'[x]',sep,']'})
+ collectgarbage('collect')
+
+ -- Load the code and run it. Script will return the string length.
+ -- Escape sequence: [=....=[ to ]=...=] will be ignored
+ -- Actual string is a single character: 'x'. Script will return 1
+ local func = loadstring(code)
+ return #func()
+ } 0
+ } {1}
+
+ test {EVAL - Lua can parse string with too many new lines} {
+ # Create a long string consisting only of newline characters. When Lua
+ # fails to parse a string, it typically includes a snippet like
+ # "... near ..." in the error message to indicate the last recognizable
+ # token. In this test, since the input contains only newlines, there
+ # should be no identifiable token, so the error message should contain
+ # only the actual error, without a near clause.
+
+ run_script {
+ local s = string.rep('\n', 1024 * 1024)
+ local t = {} for i=1,2048 do t[#t+1] = s end
+ local lines = table.concat(t)
+ local fn, err = loadstring(lines)
+ return err
+ } 0
+ } {*chunk has too many lines}
+}
+
+# Start a new server to test lua-enable-deprecated-api config
+foreach enabled {no yes} {
+start_server [subst {tags {"scripting external:skip"} overrides {lua-enable-deprecated-api $enabled}}] {
+ test "Test setfenv availability lua-enable-deprecated-api=$enabled" {
+ catch {
+ run_script {
+ local f = function() return 1 end
+ setfenv(f, {})
+ return 0
+ } 0
+ } e
+ if {$enabled} {
+ assert_equal $e 0
+ } else {
+ assert_match {*Script attempted to access nonexistent global variable 'setfenv'*} $e
+ }
+ }
+
+ test "Test getfenv availability lua-enable-deprecated-api=$enabled" {
+ catch {
+ run_script {
+ local f = function() return 1 end
+ getfenv(f)
+ return 0
+ } 0
+ } e
+ if {$enabled} {
+ assert_equal $e 0
+ } else {
+ assert_match {*Script attempted to access nonexistent global variable 'getfenv'*} $e
+ }
+ }
+
+ test "Test newproxy availability lua-enable-deprecated-api=$enabled" {
+ catch {
+ run_script {
+ getmetatable(newproxy(true)).__gc = function() return 1 end
+ return 0
+ } 0
+ } e
+ if {$enabled} {
+ assert_equal $e 0
+ } else {
+ assert_match {*Script attempted to access nonexistent global variable 'newproxy'*} $e
+ }
+ }
+}
+}
+
+# Start a new server since the last test in this stanza will kill the
+# instance at all.
+start_server {tags {"scripting"}} {
+ test {Timedout read-only scripts can be killed by SCRIPT KILL} {
+ set rd [redis_deferring_client]
+ r config set lua-time-limit 10
+ run_script_on_connection $rd {while true do end} 0
+ after 200
+ catch {r ping} e
+ assert_match {BUSY*} $e
+ kill_script
+ after 200 ; # Give some time to Lua to call the hook again...
+ assert_equal [r ping] "PONG"
+ $rd close
+ }
+
+ test {Timedout read-only scripts can be killed by SCRIPT KILL even when use pcall} {
+ set rd [redis_deferring_client]
+ r config set lua-time-limit 10
+ run_script_on_connection $rd {local f = function() while 1 do redis.call('ping') end end while 1 do pcall(f) end} 0
+
+ wait_for_condition 50 100 {
+ [catch {r ping} e] == 1
+ } else {
+ fail "Can't wait for script to start running"
+ }
+ catch {r ping} e
+ assert_match {BUSY*} $e
+
+ kill_script
+
+ wait_for_condition 50 100 {
+ [catch {r ping} e] == 0
+ } else {
+ fail "Can't wait for script to be killed"
+ }
+ assert_equal [r ping] "PONG"
+
+ catch {$rd read} res
+ $rd close
+
+ assert_match {*killed by user*} $res
+ }
+
+ test {Timedout script does not cause a false dead client} {
+ set rd [redis_deferring_client]
+ r config set lua-time-limit 10
+
+ # senging (in a pipeline):
+ # 1. eval "while 1 do redis.call('ping') end" 0
+ # 2. ping
+ if {$is_eval == 1} {
+ set buf "*3\r\n\$4\r\neval\r\n\$33\r\nwhile 1 do redis.call('ping') end\r\n\$1\r\n0\r\n"
+ append buf "*1\r\n\$4\r\nping\r\n"
+ } else {
+ set buf "*4\r\n\$8\r\nfunction\r\n\$4\r\nload\r\n\$7\r\nreplace\r\n\$97\r\n#!lua name=test\nredis.register_function('test', function() while 1 do redis.call('ping') end end)\r\n"
+ append buf "*3\r\n\$5\r\nfcall\r\n\$4\r\ntest\r\n\$1\r\n0\r\n"
+ append buf "*1\r\n\$4\r\nping\r\n"
+ }
+ $rd write $buf
+ $rd flush
+
+ wait_for_condition 50 100 {
+ [catch {r ping} e] == 1
+ } else {
+ fail "Can't wait for script to start running"
+ }
+ catch {r ping} e
+ assert_match {BUSY*} $e
+
+ kill_script
+ wait_for_condition 50 100 {
+ [catch {r ping} e] == 0
+ } else {
+ fail "Can't wait for script to be killed"
+ }
+ assert_equal [r ping] "PONG"
+
+ if {$is_eval == 0} {
+ # read the function name
+ assert_match {test} [$rd read]
+ }
+
+ catch {$rd read} res
+ assert_match {*killed by user*} $res
+
+ set res [$rd read]
+ assert_match {*PONG*} $res
+
+ $rd close
+ }
+
+ test {Timedout script link is still usable after Lua returns} {
+ r config set lua-time-limit 10
+ run_script {for i=1,100000 do redis.call('ping') end return 'ok'} 0
+ r ping
+ } {PONG}
+
+ test {Timedout scripts and unblocked command} {
+ # make sure a command that's allowed during BUSY doesn't trigger an unblocked command
+
+ # enable AOF to also expose an assertion if the bug would happen
+ r flushall
+ r config set appendonly yes
+
+ # create clients, and set one to block waiting for key 'x'
+ set rd [redis_deferring_client]
+ set rd2 [redis_deferring_client]
+ set r3 [redis_client]
+ $rd2 blpop x 0
+ wait_for_blocked_clients_count 1
+
+ # hack: allow the script to use client list command so that we can control when it aborts
+ r DEBUG set-disable-deny-scripts 1
+ r config set lua-time-limit 10
+ run_script_on_connection $rd {
+ local clients
+ redis.call('lpush',KEYS[1],'y');
+ while true do
+ clients = redis.call('client','list')
+ if string.find(clients, 'abortscript') ~= nil then break end
+ end
+ redis.call('lpush',KEYS[1],'z');
+ return clients
+ } 1 x
+
+ # wait for the script to be busy
+ after 200
+ catch {r ping} e
+ assert_match {BUSY*} $e
+
+ # run cause the script to abort, and run a command that could have processed
+ # unblocked clients (due to a bug)
+ $r3 hello 2 setname abortscript
+
+ # make sure the script completed before the pop was processed
+ assert_equal [$rd2 read] {x z}
+ assert_match {*abortscript*} [$rd read]
+
+ $rd close
+ $rd2 close
+ $r3 close
+ r DEBUG set-disable-deny-scripts 0
+ } {OK} {external:skip needs:debug}
+
+ test {Timedout scripts that modified data can't be killed by SCRIPT KILL} {
+ set rd [redis_deferring_client]
+ r config set lua-time-limit 10
+ run_script_on_connection $rd {redis.call('set',KEYS[1],'y'); while true do end} 1 x
+ after 200
+ catch {r ping} e
+ assert_match {BUSY*} $e
+ catch {kill_script} e
+ assert_match {UNKILLABLE*} $e
+ catch {r ping} e
+ assert_match {BUSY*} $e
+ } {} {external:skip}
+
+ # Note: keep this test at the end of this server stanza because it
+ # kills the server.
+ test {SHUTDOWN NOSAVE can kill a timedout script anyway} {
+ # The server should be still unresponding to normal commands.
+ catch {r ping} e
+ assert_match {BUSY*} $e
+ catch {r shutdown nosave}
+ # Make sure the server was killed
+ catch {set rd [redis_deferring_client]} e
+ assert_match {*connection refused*} $e
+ } {} {external:skip}
+}
+
+ start_server {tags {"scripting repl needs:debug external:skip"}} {
+ start_server {} {
+ test "Before the replica connects we issue two EVAL commands" {
+ # One with an error, but still executing a command.
+ # SHA is: 67164fc43fa971f76fd1aaeeaf60c1c178d25876
+ catch {
+ run_script {redis.call('incr',KEYS[1]); redis.call('nonexisting')} 1 x
+ }
+ # One command is correct:
+ # SHA is: 6f5ade10a69975e903c6d07b10ea44c6382381a5
+ run_script {return redis.call('incr',KEYS[1])} 1 x
+ } {2}
+
+ test "Connect a replica to the master instance" {
+ r -1 slaveof [srv 0 host] [srv 0 port]
+ wait_for_condition 50 100 {
+ [s -1 role] eq {slave} &&
+ [string match {*master_link_status:up*} [r -1 info replication]]
+ } else {
+ fail "Can't turn the instance into a replica"
+ }
+ }
+
+ if {$is_eval eq 1} {
+ test "Now use EVALSHA against the master, with both SHAs" {
+ # The server should replicate successful and unsuccessful
+ # commands as EVAL instead of EVALSHA.
+ catch {
+ r evalsha 67164fc43fa971f76fd1aaeeaf60c1c178d25876 1 x
+ }
+ r evalsha 6f5ade10a69975e903c6d07b10ea44c6382381a5 1 x
+ } {4}
+
+ test "'x' should be '4' for EVALSHA being replicated by effects" {
+ wait_for_condition 50 100 {
+ [r -1 get x] eq {4}
+ } else {
+ fail "Expected 4 in x, but value is '[r -1 get x]'"
+ }
+ }
+ } ;# is_eval
+
+ test "Replication of script multiple pushes to list with BLPOP" {
+ set rd [redis_deferring_client]
+ $rd brpop a 0
+ run_script {
+ redis.call("lpush",KEYS[1],"1");
+ redis.call("lpush",KEYS[1],"2");
+ } 1 a
+ set res [$rd read]
+ $rd close
+ wait_for_condition 50 100 {
+ [r -1 lrange a 0 -1] eq [r lrange a 0 -1]
+ } else {
+ fail "Expected list 'a' in replica and master to be the same, but they are respectively '[r -1 lrange a 0 -1]' and '[r lrange a 0 -1]'"
+ }
+ set res
+ } {a 1}
+
+ if {$is_eval eq 1} {
+ test "EVALSHA replication when first call is readonly" {
+ r del x
+ r eval {if tonumber(ARGV[1]) > 0 then redis.call('incr', KEYS[1]) end} 1 x 0
+ r evalsha 6e0e2745aa546d0b50b801a20983b70710aef3ce 1 x 0
+ r evalsha 6e0e2745aa546d0b50b801a20983b70710aef3ce 1 x 1
+ wait_for_condition 50 100 {
+ [r -1 get x] eq {1}
+ } else {
+ fail "Expected 1 in x, but value is '[r -1 get x]'"
+ }
+ }
+ } ;# is_eval
+
+ test "Lua scripts using SELECT are replicated correctly" {
+ run_script {
+ redis.call("set","foo1","bar1")
+ redis.call("select","10")
+ redis.call("incr","x")
+ redis.call("select","11")
+ redis.call("incr","z")
+ } 3 foo1 x z
+ run_script {
+ redis.call("set","foo1","bar1")
+ redis.call("select","10")
+ redis.call("incr","x")
+ redis.call("select","11")
+ redis.call("incr","z")
+ } 3 foo1 x z
+ wait_for_condition 50 100 {
+ [debug_digest -1] eq [debug_digest]
+ } else {
+ fail "Master-Replica desync after Lua script using SELECT."
+ }
+ } {} {singledb:skip}
+ }
+ }
+
+start_server {tags {"scripting repl external:skip"}} {
+ start_server {overrides {appendonly yes aof-use-rdb-preamble no}} {
+ test "Connect a replica to the master instance" {
+ r -1 slaveof [srv 0 host] [srv 0 port]
+ wait_for_condition 50 100 {
+ [s -1 role] eq {slave} &&
+ [string match {*master_link_status:up*} [r -1 info replication]]
+ } else {
+ fail "Can't turn the instance into a replica"
+ }
+ }
+
+ # replicate_commands is the default on Redis Function
+ test "Redis.replicate_commands() can be issued anywhere now" {
+ r eval {
+ redis.call('set','foo','bar');
+ return redis.replicate_commands();
+ } 0
+ } {1}
+
+ test "Redis.set_repl() can be issued before replicate_commands() now" {
+ catch {
+ r eval {
+ redis.set_repl(redis.REPL_ALL);
+ } 0
+ } e
+ set e
+ } {}
+
+ test "Redis.set_repl() don't accept invalid values" {
+ catch {
+ run_script {
+ redis.set_repl(12345);
+ } 0
+ } e
+ set e
+ } {*Invalid*flags*}
+
+ test "Test selective replication of certain Redis commands from Lua" {
+ r del a b c d
+ run_script {
+ redis.call('set','a','1');
+ redis.set_repl(redis.REPL_NONE);
+ redis.call('set','b','2');
+ redis.set_repl(redis.REPL_AOF);
+ redis.call('set','c','3');
+ redis.set_repl(redis.REPL_ALL);
+ redis.call('set','d','4');
+ } 4 a b c d
+
+ wait_for_condition 50 100 {
+ [r -1 mget a b c d] eq {1 {} {} 4}
+ } else {
+ fail "Only a and d should be replicated to replica"
+ }
+
+ # Master should have everything right now
+ assert {[r mget a b c d] eq {1 2 3 4}}
+
+ # After an AOF reload only a, c and d should exist
+ r debug loadaof
+
+ assert {[r mget a b c d] eq {1 {} 3 4}}
+ }
+
+ test "PRNG is seeded randomly for command replication" {
+ if {$is_eval eq 1} {
+ # on is_eval Lua we need to call redis.replicate_commands() to get real randomization
+ set a [
+ run_script {
+ redis.replicate_commands()
+ return math.random()*100000;
+ } 0
+ ]
+ set b [
+ run_script {
+ redis.replicate_commands()
+ return math.random()*100000;
+ } 0
+ ]
+ } else {
+ set a [
+ run_script {
+ return math.random()*100000;
+ } 0
+ ]
+ set b [
+ run_script {
+ return math.random()*100000;
+ } 0
+ ]
+ }
+ assert {$a ne $b}
+ }
+
+ test "Using side effects is not a problem with command replication" {
+ run_script {
+ redis.call('set','time',redis.call('time')[1])
+ } 0
+
+ assert {[r get time] ne {}}
+
+ wait_for_condition 50 100 {
+ [r get time] eq [r -1 get time]
+ } else {
+ fail "Time key does not match between master and replica"
+ }
+ }
+ }
+}
+
+if {$is_eval eq 1} {
+start_server {tags {"scripting external:skip"}} {
+ r script debug sync
+ r eval {return 'hello'} 0
+ r eval {return 'hello'} 0
+}
+
+start_server {tags {"scripting needs:debug external:skip"}} {
+ test {Test scripting debug protocol parsing} {
+ r script debug sync
+ r eval {return 'hello'} 0
+ catch {r 'hello\0world'} e
+ assert_match {*Unknown Redis Lua debugger command*} $e
+ catch {r 'hello\0'} e
+ assert_match {*Unknown Redis Lua debugger command*} $e
+ catch {r '\0hello'} e
+ assert_match {*Unknown Redis Lua debugger command*} $e
+ catch {r '\0hello\0'} e
+ assert_match {*Unknown Redis Lua debugger command*} $e
+ }
+
+ test {Test scripting debug lua stack overflow} {
+ r script debug sync
+ r eval {return 'hello'} 0
+ set cmd "*101\r\n\$5\r\nredis\r\n"
+ append cmd [string repeat "\$4\r\ntest\r\n" 100]
+ r write $cmd
+ r flush
+ set ret [r read]
+ assert_match {*Unknown Redis command called from script*} $ret
+ # make sure the server is still ok
+ reconnect
+ assert_equal [r ping] {PONG}
+ }
+}
+
+start_server {tags {"scripting external:skip"}} {
+ test {Lua scripts eviction does not generate many scripts} {
+ r script flush
+ r config resetstat
+
+ # "return 1" sha is: e0e1f9fabfc9d4800c877a703b823ac0578ff8db
+ # "return 500" sha is: 98fe65896b61b785c5ed328a5a0a1421f4f1490c
+ for {set j 1} {$j <= 250} {incr j} {
+ r eval "return $j" 0
+ }
+ for {set j 251} {$j <= 500} {incr j} {
+ r eval_ro "return $j" 0
+ }
+ assert_equal [s number_of_cached_scripts] 500
+ assert_equal 1 [r evalsha e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0]
+ assert_equal 1 [r evalsha_ro e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0]
+ assert_equal 500 [r evalsha 98fe65896b61b785c5ed328a5a0a1421f4f1490c 0]
+ assert_equal 500 [r evalsha_ro 98fe65896b61b785c5ed328a5a0a1421f4f1490c 0]
+
+ # Scripts between "return 1" and "return 500" are evicted
+ for {set j 501} {$j <= 750} {incr j} {
+ r eval "return $j" 0
+ }
+ for {set j 751} {$j <= 1000} {incr j} {
+ r eval "return $j" 0
+ }
+ assert_error {NOSCRIPT*} {r evalsha e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0}
+ assert_error {NOSCRIPT*} {r evalsha_ro e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0}
+ assert_error {NOSCRIPT*} {r evalsha 98fe65896b61b785c5ed328a5a0a1421f4f1490c 0}
+ assert_error {NOSCRIPT*} {r evalsha_ro 98fe65896b61b785c5ed328a5a0a1421f4f1490c 0}
+
+ assert_equal [s evicted_scripts] 500
+ assert_equal [s number_of_cached_scripts] 500
+ }
+
+ test {Lua scripts eviction is plain LRU} {
+ r script flush
+ r config resetstat
+
+ # "return 1" sha is: e0e1f9fabfc9d4800c877a703b823ac0578ff8db
+ # "return 2" sha is: 7f923f79fe76194c868d7e1d0820de36700eb649
+ # "return 3" sha is: 09d3822de862f46d784e6a36848b4f0736dda47a
+ # "return 500" sha is: 98fe65896b61b785c5ed328a5a0a1421f4f1490c
+ # "return 1000" sha is: 94f1a7bc9f985a1a1d5a826a85579137d9d840c8
+ for {set j 1} {$j <= 500} {incr j} {
+ r eval "return $j" 0
+ }
+
+ # Call "return 1" to move it to the tail.
+ r eval "return 1" 0
+ # Call "return 2" to move it to the tail.
+ r evalsha 7f923f79fe76194c868d7e1d0820de36700eb649 0
+ # Create a new script, "return 3" will be evicted.
+ r eval "return 1000" 0
+ # "return 1" is ok since it was moved to tail.
+ assert_equal 1 [r evalsha e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0]
+ # "return 2" is ok since it was moved to tail.
+ assert_equal 1 [r evalsha e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0]
+ # "return 3" was evicted.
+ assert_error {NOSCRIPT*} {r evalsha 09d3822de862f46d784e6a36848b4f0736dda47a 0}
+ # Others are ok.
+ assert_equal 500 [r evalsha 98fe65896b61b785c5ed328a5a0a1421f4f1490c 0]
+ assert_equal 1000 [r evalsha 94f1a7bc9f985a1a1d5a826a85579137d9d840c8 0]
+
+ assert_equal [s evicted_scripts] 1
+ assert_equal [s number_of_cached_scripts] 500
+ }
+
+ test {Lua scripts eviction does not affect script load} {
+ r script flush
+ r config resetstat
+
+ set num [randomRange 500 1000]
+ for {set j 1} {$j <= $num} {incr j} {
+ r script load "return $j"
+ r eval "return 'str_$j'" 0
+ }
+ set evicted [s evicted_scripts]
+ set cached [s number_of_cached_scripts]
+ # evicted = num eval scripts - 500 eval scripts
+ assert_equal $evicted [expr $num-500]
+ # cached = num load scripts + 500 eval scripts
+ assert_equal $cached [expr $num+500]
+ }
+}
+
+} ;# is_eval
+
+start_server {tags {"scripting needs:debug"}} {
+ r debug set-disable-deny-scripts 1
+
+ for {set i 2} {$i <= 3} {incr i} {
+ for {set client_proto 2} {$client_proto <= 3} {incr client_proto} {
+ if {[lsearch $::denytags "resp3"] >= 0} {
+ if {$client_proto == 3} {continue}
+ } elseif {$::force_resp3} {
+ if {$client_proto == 2} {continue}
+ }
+ r hello $client_proto
+ set extra "RESP$i/$client_proto"
+ r readraw 1
+
+ test "test $extra big number protocol parsing" {
+ set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'bignum')" 0]
+ if {$client_proto == 2 || $i == 2} {
+ # if either Lua or the client is RESP2 the reply will be RESP2
+ assert_equal $ret {$37}
+ assert_equal [r read] {1234567999999999999999999999999999999}
+ } else {
+ assert_equal $ret {(1234567999999999999999999999999999999}
+ }
+ }
+
+ test "test $extra malformed big number protocol parsing" {
+ set ret [run_script "return {big_number='123\\r\\n123'}" 0]
+ if {$client_proto == 2} {
+ # if either Lua or the client is RESP2 the reply will be RESP2
+ assert_equal $ret {$8}
+ assert_equal [r read] {123 123}
+ } else {
+ assert_equal $ret {(123 123}
+ }
+ }
+
+ test "test $extra map protocol parsing" {
+ set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'map')" 0]
+ if {$client_proto == 2 || $i == 2} {
+ # if either Lua or the client is RESP2 the reply will be RESP2
+ assert_equal $ret {*6}
+ } else {
+ assert_equal $ret {%3}
+ }
+ for {set j 0} {$j < 6} {incr j} {
+ r read
+ }
+ }
+
+ test "test $extra set protocol parsing" {
+ set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'set')" 0]
+ if {$client_proto == 2 || $i == 2} {
+ # if either Lua or the client is RESP2 the reply will be RESP2
+ assert_equal $ret {*3}
+ } else {
+ assert_equal $ret {~3}
+ }
+ for {set j 0} {$j < 3} {incr j} {
+ r read
+ }
+ }
+
+ test "test $extra double protocol parsing" {
+ set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'double')" 0]
+ if {$client_proto == 2 || $i == 2} {
+ # if either Lua or the client is RESP2 the reply will be RESP2
+ assert_equal $ret {$5}
+ assert_equal [r read] {3.141}
+ } else {
+ assert_equal $ret {,3.141}
+ }
+ }
+
+ test "test $extra null protocol parsing" {
+ set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'null')" 0]
+ if {$client_proto == 2} {
+ # null is a special case in which a Lua client format does not effect the reply to the client
+ assert_equal $ret {$-1}
+ } else {
+ assert_equal $ret {_}
+ }
+ } {}
+
+ test "test $extra verbatim protocol parsing" {
+ set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'verbatim')" 0]
+ if {$client_proto == 2 || $i == 2} {
+ # if either Lua or the client is RESP2 the reply will be RESP2
+ assert_equal $ret {$25}
+ assert_equal [r read] {This is a verbatim}
+ assert_equal [r read] {string}
+ } else {
+ assert_equal $ret {=29}
+ assert_equal [r read] {txt:This is a verbatim}
+ assert_equal [r read] {string}
+ }
+ }
+
+ test "test $extra true protocol parsing" {
+ set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'true')" 0]
+ if {$client_proto == 2 || $i == 2} {
+ # if either Lua or the client is RESP2 the reply will be RESP2
+ assert_equal $ret {:1}
+ } else {
+ assert_equal $ret {#t}
+ }
+ }
+
+ test "test $extra false protocol parsing" {
+ set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'false')" 0]
+ if {$client_proto == 2 || $i == 2} {
+ # if either Lua or the client is RESP2 the reply will be RESP2
+ assert_equal $ret {:0}
+ } else {
+ assert_equal $ret {#f}
+ }
+ }
+
+ r readraw 0
+ r hello 2
+ }
+ }
+
+ # attribute is not relevant to test with resp2
+ test {test resp3 attribute protocol parsing} {
+ # attributes are not (yet) expose to the script
+ # So here we just check the parser handles them and they are ignored.
+ run_script "redis.setresp(3);return redis.call('debug', 'protocol', 'attrib')" 0
+ } {Some real reply following the attribute}
+
+ test "Script block the time during execution" {
+ assert_equal [run_script {
+ redis.call("SET", "key", "value", "PX", "1")
+ redis.call("DEBUG", "SLEEP", 0.01)
+ return redis.call("EXISTS", "key")
+ } 1 key] 1
+
+ assert_equal 0 [r EXISTS key]
+ }
+
+ test "Script delete the expired key" {
+ r DEBUG set-active-expire 0
+ r SET key value PX 1
+ after 2
+
+ # use DEBUG OBJECT to make sure it doesn't error (means the key still exists)
+ r DEBUG OBJECT key
+
+ assert_equal [run_script {return redis.call('EXISTS', 'key')} 1 key] 0
+ assert_equal 0 [r EXISTS key]
+ r DEBUG set-active-expire 1
+ }
+
+ test "TIME command using cached time" {
+ set res [run_script {
+ local result1 = {redis.call("TIME")}
+ redis.call("DEBUG", "SLEEP", 0.01)
+ local result2 = {redis.call("TIME")}
+ return {result1, result2}
+ } 0]
+ assert_equal [lindex $res 0] [lindex $res 1]
+ }
+
+ test "Script block the time in some expiration related commands" {
+ # The test uses different commands to set the "same" expiration time for different keys,
+ # and interspersed with "DEBUG SLEEP", to verify that time is frozen in script.
+ # The commands involved are [P]TTL / SET EX[PX] / [P]EXPIRE / GETEX / [P]SETEX / [P]EXPIRETIME
+ set res [run_script {
+ redis.call("SET", "key1{t}", "value", "EX", 1)
+ redis.call("DEBUG", "SLEEP", 0.01)
+
+ redis.call("SET", "key2{t}", "value", "PX", 1000)
+ redis.call("DEBUG", "SLEEP", 0.01)
+
+ redis.call("SET", "key3{t}", "value")
+ redis.call("EXPIRE", "key3{t}", 1)
+ redis.call("DEBUG", "SLEEP", 0.01)
+
+ redis.call("SET", "key4{t}", "value")
+ redis.call("PEXPIRE", "key4{t}", 1000)
+ redis.call("DEBUG", "SLEEP", 0.01)
+
+ redis.call("SETEX", "key5{t}", 1, "value")
+ redis.call("DEBUG", "SLEEP", 0.01)
+
+ redis.call("PSETEX", "key6{t}", 1000, "value")
+ redis.call("DEBUG", "SLEEP", 0.01)
+
+ redis.call("SET", "key7{t}", "value")
+ redis.call("GETEX", "key7{t}", "EX", 1)
+ redis.call("DEBUG", "SLEEP", 0.01)
+
+ redis.call("SET", "key8{t}", "value")
+ redis.call("GETEX", "key8{t}", "PX", 1000)
+ redis.call("DEBUG", "SLEEP", 0.01)
+
+ local ttl_results = {redis.call("TTL", "key1{t}"),
+ redis.call("TTL", "key2{t}"),
+ redis.call("TTL", "key3{t}"),
+ redis.call("TTL", "key4{t}"),
+ redis.call("TTL", "key5{t}"),
+ redis.call("TTL", "key6{t}"),
+ redis.call("TTL", "key7{t}"),
+ redis.call("TTL", "key8{t}")}
+
+ local pttl_results = {redis.call("PTTL", "key1{t}"),
+ redis.call("PTTL", "key2{t}"),
+ redis.call("PTTL", "key3{t}"),
+ redis.call("PTTL", "key4{t}"),
+ redis.call("PTTL", "key5{t}"),
+ redis.call("PTTL", "key6{t}"),
+ redis.call("PTTL", "key7{t}"),
+ redis.call("PTTL", "key8{t}")}
+
+ local expiretime_results = {redis.call("EXPIRETIME", "key1{t}"),
+ redis.call("EXPIRETIME", "key2{t}"),
+ redis.call("EXPIRETIME", "key3{t}"),
+ redis.call("EXPIRETIME", "key4{t}"),
+ redis.call("EXPIRETIME", "key5{t}"),
+ redis.call("EXPIRETIME", "key6{t}"),
+ redis.call("EXPIRETIME", "key7{t}"),
+ redis.call("EXPIRETIME", "key8{t}")}
+
+ local pexpiretime_results = {redis.call("PEXPIRETIME", "key1{t}"),
+ redis.call("PEXPIRETIME", "key2{t}"),
+ redis.call("PEXPIRETIME", "key3{t}"),
+ redis.call("PEXPIRETIME", "key4{t}"),
+ redis.call("PEXPIRETIME", "key5{t}"),
+ redis.call("PEXPIRETIME", "key6{t}"),
+ redis.call("PEXPIRETIME", "key7{t}"),
+ redis.call("PEXPIRETIME", "key8{t}")}
+
+ return {ttl_results, pttl_results, expiretime_results, pexpiretime_results}
+ } 8 key1{t} key2{t} key3{t} key4{t} key5{t} key6{t} key7{t} key8{t}]
+
+ # The elements in each list are equal.
+ assert_equal 1 [llength [lsort -unique [lindex $res 0]]]
+ assert_equal 1 [llength [lsort -unique [lindex $res 1]]]
+ assert_equal 1 [llength [lsort -unique [lindex $res 2]]]
+ assert_equal 1 [llength [lsort -unique [lindex $res 3]]]
+
+ # Then we check that the expiration time is set successfully.
+ assert_morethan [lindex $res 0] 0
+ assert_morethan [lindex $res 1] 0
+ assert_morethan [lindex $res 2] 0
+ assert_morethan [lindex $res 3] 0
+ }
+
+ test "RESTORE expired keys with expiration time" {
+ set res [run_script {
+ redis.call("SET", "key1{t}", "value")
+ local encoded = redis.call("DUMP", "key1{t}")
+
+ redis.call("RESTORE", "key2{t}", 1, encoded, "REPLACE")
+ redis.call("DEBUG", "SLEEP", 0.01)
+ redis.call("RESTORE", "key3{t}", 1, encoded, "REPLACE")
+
+ return {redis.call("PEXPIRETIME", "key2{t}"), redis.call("PEXPIRETIME", "key3{t}")}
+ } 3 key1{t} key2{t} key3{t}]
+
+ # Can get the expiration time and they are all equal.
+ assert_morethan [lindex $res 0] 0
+ assert_equal [lindex $res 0] [lindex $res 1]
+ }
+
+ r debug set-disable-deny-scripts 0
+}
+
+start_server {tags {"scripting"}} {
+ test "Test script flush will not leak memory - script:$is_eval" {
+ r flushall
+ r script flush
+ r function flush
+
+ # This is a best-effort test to check we don't leak some resources on
+ # script flush and function flush commands. For lua vm, we create a
+ # jemalloc thread cache. On each script flush command, thread cache is
+ # destroyed and we create a new one. In this test, running script flush
+ # many times to verify there is no increase in the memory usage while
+ # re-creating some of the resources for lua vm.
+ set used_memory [s used_memory]
+ set allocator_allocated [s allocator_allocated]
+
+ r multi
+ for {set j 1} {$j <= 500} {incr j} {
+ if {$is_eval} {
+ r SCRIPT FLUSH
+ } else {
+ r FUNCTION FLUSH
+ }
+ }
+ r exec
+
+ # Verify used memory is not (much) higher.
+ assert_lessthan [s used_memory] [expr $used_memory*1.5]
+ assert_lessthan [s allocator_allocated] [expr $allocator_allocated*1.5]
+ }
+
+ test "Verify Lua performs GC correctly after script loading" {
+ set dummy_script "--[string repeat x 10]\nreturn "
+ set n 50000
+ for {set i 0} {$i < $n} {incr i} {
+ set script "$dummy_script[format "%06d" $i]"
+ if {$is_eval} {
+ r script load $script
+ } else {
+ r function load "#!lua name=test$i\nredis.register_function('test$i', function(KEYS, ARGV)\n $script \nend)"
+ }
+ }
+
+ if {$is_eval} {
+ assert_lessthan [s used_memory_lua] 17500000
+ } else {
+ assert_lessthan [s used_memory_vm_functions] 14500000
+ }
+ } {} {debug_defrag:skip}
+}
+} ;# foreach is_eval
+
+
+# Scripting "shebang" notation tests
+start_server {tags {"scripting"}} {
+ test "Shebang support for lua engine" {
+ catch {
+ r eval {#!not-lua
+ return 1
+ } 0
+ } e
+ assert_match {*Unexpected engine in script shebang*} $e
+
+ assert_equal [r eval {#!lua
+ return 1
+ } 0] 1
+ }
+
+ test "Unknown shebang option" {
+ catch {
+ r eval {#!lua badger=data
+ return 1
+ } 0
+ } e
+ assert_match {*Unknown lua shebang option*} $e
+ }
+
+ test "Unknown shebang flag" {
+ catch {
+ r eval {#!lua flags=allow-oom,what?
+ return 1
+ } 0
+ } e
+ assert_match {*Unexpected flag in script shebang*} $e
+ }
+
+ test "allow-oom shebang flag" {
+ r set x 123
+
+ r config set maxmemory 1
+
+ # Fail to execute deny-oom command in OOM condition (backwards compatibility mode without flags)
+ assert_error {OOM command not allowed when used memory > 'maxmemory'*} {
+ r eval {
+ redis.call('set','x',1)
+ return 1
+ } 1 x
+ }
+ # Can execute non deny-oom commands in OOM condition (backwards compatibility mode without flags)
+ assert_equal [
+ r eval {
+ return redis.call('get','x')
+ } 1 x
+ ] {123}
+
+ # Fail to execute regardless of script content when we use default flags in OOM condition
+ assert_error {OOM *} {
+ r eval {#!lua flags=
+ return 1
+ } 0
+ }
+
+ # Script with allow-oom can write despite being in OOM state
+ assert_equal [
+ r eval {#!lua flags=allow-oom
+ redis.call('set','x',1)
+ return 1
+ } 1 x
+ ] 1
+
+ # read-only scripts implies allow-oom
+ assert_equal [
+ r eval {#!lua flags=no-writes
+ redis.call('get','x')
+ return 1
+ } 0
+ ] 1
+ assert_equal [
+ r eval_ro {#!lua flags=no-writes
+ redis.call('get','x')
+ return 1
+ } 1 x
+ ] 1
+
+ # Script with no shebang can read in OOM state
+ assert_equal [
+ r eval {
+ redis.call('get','x')
+ return 1
+ } 1 x
+ ] 1
+
+ # Script with no shebang can read in OOM state (eval_ro variant)
+ assert_equal [
+ r eval_ro {
+ redis.call('get','x')
+ return 1
+ } 1 x
+ ] 1
+
+ r config set maxmemory 0
+ } {OK} {needs:config-maxmemory}
+
+ test "no-writes shebang flag" {
+ assert_error {ERR Write commands are not allowed from read-only scripts*} {
+ r eval {#!lua flags=no-writes
+ redis.call('set','x',1)
+ return 1
+ } 1 x
+ }
+ }
+
+ start_server {tags {"external:skip"}} {
+ r -1 set x "some value"
+ test "no-writes shebang flag on replica" {
+ r replicaof [srv -1 host] [srv -1 port]
+ wait_for_condition 50 100 {
+ [s role] eq {slave} &&
+ [string match {*master_link_status:up*} [r info replication]]
+ } else {
+ fail "Can't turn the instance into a replica"
+ }
+
+ assert_equal [
+ r eval {#!lua flags=no-writes
+ return redis.call('get','x')
+ } 1 x
+ ] "some value"
+
+ assert_error {READONLY You can't write against a read only replica.} {
+ r eval {#!lua
+ return redis.call('get','x')
+ } 1 x
+ }
+
+ # test no-write inside multi-exec
+ r multi
+ r eval {#!lua flags=no-writes
+ redis.call('get','x')
+ return 1
+ } 1 x
+ assert_equal [r exec] 1
+
+ # test no shebang without write inside multi-exec
+ r multi
+ r eval {
+ redis.call('get','x')
+ return 1
+ } 1 x
+ assert_equal [r exec] 1
+
+ # temporarily set the server to master, so it doesn't block the queuing
+ # and we can test the evaluation of the flags on exec
+ r replicaof no one
+ set rr [redis_client]
+ set rr2 [redis_client]
+ $rr multi
+ $rr2 multi
+
+ # test write inside multi-exec
+ # we don't need to do any actual write
+ $rr eval {#!lua
+ return 1
+ } 0
+
+ # test no shebang with write inside multi-exec
+ $rr2 eval {
+ redis.call('set','x',1)
+ return 1
+ } 1 x
+
+ r replicaof [srv -1 host] [srv -1 port]
+
+ # To avoid -LOADING reply, wait until replica syncs with master.
+ wait_for_condition 50 100 {
+ [s master_link_status] eq {up}
+ } else {
+ fail "Replica did not sync in time."
+ }
+
+ assert_error {EXECABORT Transaction discarded because of: READONLY *} {$rr exec}
+ assert_error {READONLY You can't write against a read only replica. script: *} {$rr2 exec}
+ $rr close
+ $rr2 close
+ }
+ }
+
+ test "not enough good replicas" {
+ r set x "some value"
+ r config set min-replicas-to-write 1
+
+ assert_equal [
+ r eval {#!lua flags=no-writes
+ return redis.call('get','x')
+ } 1 x
+ ] "some value"
+
+ assert_equal [
+ r eval {
+ return redis.call('get','x')
+ } 1 x
+ ] "some value"
+
+ assert_error {NOREPLICAS *} {
+ r eval {#!lua
+ return redis.call('get','x')
+ } 1 x
+ }
+
+ assert_error {NOREPLICAS *} {
+ r eval {
+ return redis.call('set','x', 1)
+ } 1 x
+ }
+
+ r config set min-replicas-to-write 0
+ }
+
+ test "not enough good replicas state change during long script" {
+ r set x "pre-script value"
+ r config set min-replicas-to-write 1
+ r config set lua-time-limit 10
+ start_server {tags {"external:skip"}} {
+ # add a replica and wait for the master to recognize it's online
+ r slaveof [srv -1 host] [srv -1 port]
+ wait_replica_online [srv -1 client]
+
+ # run a slow script that does one write, then waits for INFO to indicate
+ # that the replica dropped, and then runs another write
+ set rd [redis_deferring_client -1]
+ $rd eval {
+ redis.call('set','x',"script value")
+ while true do
+ local info = redis.call('info','replication')
+ if (string.match(info, "connected_slaves:0")) then
+ redis.call('set','x',info)
+ break
+ end
+ end
+ return 1
+ } 1 x
+
+ # wait for the script to time out and yield
+ wait_for_condition 100 100 {
+ [catch {r -1 ping} e] == 1
+ } else {
+ fail "Can't wait for script to start running"
+ }
+ catch {r -1 ping} e
+ assert_match {BUSY*} $e
+
+ # cause the replica to disconnect (triggering the busy script to exit)
+ r slaveof no one
+
+ # make sure the script was able to write after the replica dropped
+ assert_equal [$rd read] 1
+ assert_match {*connected_slaves:0*} [r -1 get x]
+
+ $rd close
+ }
+ r config set min-replicas-to-write 0
+ r config set lua-time-limit 5000
+ } {OK} {external:skip needs:repl}
+
+ test "allow-stale shebang flag" {
+ r config set replica-serve-stale-data no
+ r replicaof 127.0.0.1 1
+
+ assert_error {MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.} {
+ r eval {
+ return redis.call('get','x')
+ } 1 x
+ }
+
+ assert_error {MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.} {
+ r eval {#!lua flags=no-writes
+ return 1
+ } 0
+ }
+
+ assert_equal [
+ r eval {#!lua flags=allow-stale,no-writes
+ return 1
+ } 0
+ ] 1
+
+
+ assert_error {*Can not execute the command on a stale replica*} {
+ r eval {#!lua flags=allow-stale,no-writes
+ return redis.call('get','x')
+ } 1 x
+ }
+
+ assert_match {foobar} [
+ r eval {#!lua flags=allow-stale,no-writes
+ return redis.call('echo','foobar')
+ } 0
+ ]
+
+ # Test again with EVALSHA
+ set sha [
+ r script load {#!lua flags=allow-stale,no-writes
+ return redis.call('echo','foobar')
+ }
+ ]
+ assert_match {foobar} [r evalsha $sha 0]
+
+ r replicaof no one
+ r config set replica-serve-stale-data yes
+ set _ {}
+ } {} {external:skip}
+
+ test "reject script do not cause a Lua stack leak" {
+ r config set maxmemory 1
+ for {set i 0} {$i < 50} {incr i} {
+ assert_error {OOM *} {r eval {#!lua
+ return 1
+ } 0}
+ }
+ r config set maxmemory 0
+ assert_equal [r eval {#!lua
+ return 1
+ } 0] 1
+ }
+}
+
+# Additional eval only tests
+start_server {tags {"scripting"}} {
+ test "Consistent eval error reporting" {
+ r config resetstat
+ r config set maxmemory 1
+ # Script aborted due to Redis state (OOM) should report script execution error with detailed internal error
+ assert_error {OOM command not allowed when used memory > 'maxmemory'*} {
+ r eval {return redis.call('set','x','y')} 1 x
+ }
+ assert_equal [errorrstat OOM r] {count=1}
+ assert_equal [s total_error_replies] {1}
+ assert_match {calls=0*rejected_calls=1,failed_calls=0*} [cmdrstat set r]
+ assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat eval r]
+
+ # redis.pcall() failure due to Redis state (OOM) returns lua error table with Redis error message without '-' prefix
+ r config resetstat
+ assert_equal [
+ r eval {
+ local t = redis.pcall('set','x','y')
+ if t['err'] == "OOM command not allowed when used memory > 'maxmemory'." then
+ return 1
+ else
+ return 0
+ end
+ } 1 x
+ ] 1
+ # error stats were not incremented
+ assert_equal [errorrstat ERR r] {}
+ assert_equal [errorrstat OOM r] {count=1}
+ assert_equal [s total_error_replies] {1}
+ assert_match {calls=0*rejected_calls=1,failed_calls=0*} [cmdrstat set r]
+ assert_match {calls=1*rejected_calls=0,failed_calls=0*} [cmdrstat eval r]
+
+ # Returning an error object from lua is handled as a valid RESP error result.
+ r config resetstat
+ assert_error {OOM command not allowed when used memory > 'maxmemory'.} {
+ r eval { return redis.pcall('set','x','y') } 1 x
+ }
+ assert_equal [errorrstat ERR r] {}
+ assert_equal [errorrstat OOM r] {count=1}
+ assert_equal [s total_error_replies] {1}
+ assert_match {calls=0*rejected_calls=1,failed_calls=0*} [cmdrstat set r]
+ assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat eval r]
+
+ r config set maxmemory 0
+ r config resetstat
+ # Script aborted due to error result of Redis command
+ assert_error {ERR DB index is out of range*} {
+ r eval {return redis.call('select',99)} 0
+ }
+ assert_equal [errorrstat ERR r] {count=1}
+ assert_equal [s total_error_replies] {1}
+ assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat select r]
+ assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat eval r]
+
+ # redis.pcall() failure due to error in Redis command returns lua error table with redis error message without '-' prefix
+ r config resetstat
+ assert_equal [
+ r eval {
+ local t = redis.pcall('select',99)
+ if t['err'] == "ERR DB index is out of range" then
+ return 1
+ else
+ return 0
+ end
+ } 0
+ ] 1
+ assert_equal [errorrstat ERR r] {count=1} ;
+ assert_equal [s total_error_replies] {1}
+ assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat select r]
+ assert_match {calls=1*rejected_calls=0,failed_calls=0*} [cmdrstat eval r]
+
+ # Script aborted due to scripting specific error state (write cmd with eval_ro) should report script execution error with detailed internal error
+ r config resetstat
+ assert_error {ERR Write commands are not allowed from read-only scripts*} {
+ r eval_ro {return redis.call('set','x','y')} 1 x
+ }
+ assert_equal [errorrstat ERR r] {count=1}
+ assert_equal [s total_error_replies] {1}
+ assert_match {calls=0*rejected_calls=1,failed_calls=0*} [cmdrstat set r]
+ assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat eval_ro r]
+
+ # redis.pcall() failure due to scripting specific error state (write cmd with eval_ro) returns lua error table with Redis error message without '-' prefix
+ r config resetstat
+ assert_equal [
+ r eval_ro {
+ local t = redis.pcall('set','x','y')
+ if t['err'] == "ERR Write commands are not allowed from read-only scripts." then
+ return 1
+ else
+ return 0
+ end
+ } 1 x
+ ] 1
+ assert_equal [errorrstat ERR r] {count=1}
+ assert_equal [s total_error_replies] {1}
+ assert_match {calls=0*rejected_calls=1,failed_calls=0*} [cmdrstat set r]
+ assert_match {calls=1*rejected_calls=0,failed_calls=0*} [cmdrstat eval_ro r]
+
+ r config resetstat
+ # make sure geoadd will failed
+ r set Sicily 1
+ assert_error {WRONGTYPE Operation against a key holding the wrong kind of value*} {
+ r eval {return redis.call('GEOADD', 'Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania')} 1 x
+ }
+ assert_equal [errorrstat WRONGTYPE r] {count=1}
+ assert_equal [s total_error_replies] {1}
+ assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat geoadd r]
+ assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat eval r]
+ } {} {cluster:skip}
+
+ test "LUA redis.error_reply API" {
+ r config resetstat
+ assert_error {MY_ERR_CODE custom msg} {
+ r eval {return redis.error_reply("MY_ERR_CODE custom msg")} 0
+ }
+ assert_equal [errorrstat MY_ERR_CODE r] {count=1}
+ }
+
+ test "LUA redis.error_reply API with empty string" {
+ r config resetstat
+ assert_error {ERR} {
+ r eval {return redis.error_reply("")} 0
+ }
+ assert_equal [errorrstat ERR r] {count=1}
+ }
+
+ test "LUA redis.status_reply API" {
+ r config resetstat
+ r readraw 1
+ assert_equal [
+ r eval {return redis.status_reply("MY_OK_CODE custom msg")} 0
+ ] {+MY_OK_CODE custom msg}
+ r readraw 0
+ assert_equal [errorrstat MY_ERR_CODE r] {} ;# error stats were not incremented
+ }
+
+ test "LUA test pcall" {
+ assert_equal [
+ r eval {local status, res = pcall(function() return 1 end); return 'status: ' .. tostring(status) .. ' result: ' .. res} 0
+ ] {status: true result: 1}
+ }
+
+ test "LUA test pcall with error" {
+ assert_match {status: false result:*Script attempted to access nonexistent global variable 'foo'} [
+ r eval {local status, res = pcall(function() return foo end); return 'status: ' .. tostring(status) .. ' result: ' .. res} 0
+ ]
+ }
+
+ test "LUA test pcall with non string/integer arg" {
+ assert_error "ERR Lua redis lib command arguments must be strings or integers*" {
+ r eval {
+ local x={}
+ return redis.call("ping", x)
+ } 0
+ }
+ # run another command, to make sure the cached argv array survived
+ assert_equal [
+ r eval {
+ return redis.call("ping", "asdf")
+ } 0
+ ] {asdf}
+ }
+
+ test "LUA test trim string as expected" {
+ # this test may fail if we use different memory allocator than jemalloc, as libc for example may keep the old size on realloc.
+ if {[string match {*jemalloc*} [s mem_allocator]]} {
+ # test that when using LUA cache mechanism, if there is free space in the argv array, the string is trimmed.
+ r set foo [string repeat "a" 45]
+ set expected_memory [r memory usage foo]
+
+ # Jemalloc will allocate for the requested 63 bytes, 80 bytes.
+ # We can't test for larger sizes because LUA_CMD_OBJCACHE_MAX_LEN is 64.
+ # This value will be recycled to be used in the next argument.
+ # We use SETNX to avoid saving the string which will prevent us to reuse it in the next command.
+ r eval {
+ return redis.call("SETNX", "foo", string.rep("a", 63))
+ } 0
+
+ # Jemalloc will allocate for the request 45 bytes, 56 bytes.
+ # we can't test for smaller sizes because OBJ_ENCODING_EMBSTR_SIZE_LIMIT is 44 where no trim is done.
+ r eval {
+ return redis.call("SET", "foo", string.rep("a", 45))
+ } 0
+
+ # Assert the string has been trimmed and the 80 bytes from the previous alloc were not kept.
+ assert { [r memory usage foo] <= $expected_memory};
+ }
+ }
+
+ test {EVAL - explicit error() call handling} {
+ # error("simple string error")
+ assert_error {ERR user_script:1: simple string error script: *} {
+ r eval "error('simple string error')" 0
+ }
+
+ # error({"err": "ERR table error"})
+ assert_error {ERR table error script: *} {
+ r eval "error({err='ERR table error'})" 0
+ }
+
+ # error({})
+ assert_error {ERR unknown error script: *} {
+ r eval "error({})" 0
+ }
+ }
+}