Allow dynamic RPC method names in the 'rpc' macro
This commit is contained in:
parent
c455198d4f
commit
7ff4559cc0
|
@ -141,14 +141,8 @@ macro rpc*(server: RpcRouter, path: string, body: untyped): untyped =
|
||||||
parameters = body.findChild(it.kind == nnkFormalParams)
|
parameters = body.findChild(it.kind == nnkFormalParams)
|
||||||
# all remote calls have a single parameter: `params: JsonNode`
|
# all remote calls have a single parameter: `params: JsonNode`
|
||||||
paramsIdent = newIdentNode"params"
|
paramsIdent = newIdentNode"params"
|
||||||
# procs are generated from the stripped path
|
rpcProcImpl = genSym(nskProc)
|
||||||
pathStr = $path
|
rpcProcWrapper = genSym(nskProc)
|
||||||
# strip non alphanumeric
|
|
||||||
procNameStr = pathStr.makeProcName
|
|
||||||
# public rpc proc
|
|
||||||
procName = newIdentNode(procNameStr)
|
|
||||||
# when parameters present: proc that contains our rpc body
|
|
||||||
doMain = newIdentNode(procNameStr & "DoMain")
|
|
||||||
var
|
var
|
||||||
setup = jsonToNim(parameters, paramsIdent)
|
setup = jsonToNim(parameters, paramsIdent)
|
||||||
procBody = if body.kind == nnkStmtList: body else: body.body
|
procBody = if body.kind == nnkStmtList: body else: body.body
|
||||||
|
@ -158,26 +152,26 @@ macro rpc*(server: RpcRouter, path: string, body: untyped): untyped =
|
||||||
|
|
||||||
# delegate async proc allows return and setting of result as native type
|
# delegate async proc allows return and setting of result as native type
|
||||||
result.add quote do:
|
result.add quote do:
|
||||||
proc `doMain`(`paramsIdent`: JsonNode): Future[`ReturnType`] {.async.} =
|
proc `rpcProcImpl`(`paramsIdent`: JsonNode): Future[`ReturnType`] {.async.} =
|
||||||
`setup`
|
`setup`
|
||||||
`procBody`
|
`procBody`
|
||||||
|
|
||||||
if ReturnType == ident"JsonNode":
|
if ReturnType == ident"JsonNode":
|
||||||
# `JsonNode` results don't need conversion
|
# `JsonNode` results don't need conversion
|
||||||
result.add quote do:
|
result.add quote do:
|
||||||
proc `procName`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} =
|
proc `rpcProcWrapper`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} =
|
||||||
return StringOfJson($(await `doMain`(`paramsIdent`)))
|
return StringOfJson($(await `rpcProcImpl`(`paramsIdent`)))
|
||||||
elif ReturnType == ident"StringOfJson":
|
elif ReturnType == ident"StringOfJson":
|
||||||
result.add quote do:
|
result.add quote do:
|
||||||
proc `procName`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} =
|
proc `rpcProcWrapper`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} =
|
||||||
return await `doMain`(`paramsIdent`)
|
return await `rpcProcImpl`(`paramsIdent`)
|
||||||
else:
|
else:
|
||||||
result.add quote do:
|
result.add quote do:
|
||||||
proc `procName`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} =
|
proc `rpcProcWrapper`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} =
|
||||||
return StringOfJson($(%(await `doMain`(`paramsIdent`))))
|
return StringOfJson($(%(await `rpcProcImpl`(`paramsIdent`))))
|
||||||
|
|
||||||
result.add quote do:
|
result.add quote do:
|
||||||
`server`.register(`path`, `procName`)
|
`server`.register(`path`, `rpcProcWrapper`)
|
||||||
|
|
||||||
when defined(nimDumpRpcs):
|
when defined(nimDumpRpcs):
|
||||||
echo "\n", pathStr, ": ", result.repr
|
echo "\n", pathStr, ": ", result.repr
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import
|
import
|
||||||
|
std/tables,
|
||||||
chronos,
|
chronos,
|
||||||
./router,
|
./router,
|
||||||
./jsonmarshal
|
./jsonmarshal
|
||||||
|
@ -20,6 +21,11 @@ template rpc*(server: RpcServer, path: string, body: untyped): untyped =
|
||||||
template hasMethod*(server: RpcServer, methodName: string): bool =
|
template hasMethod*(server: RpcServer, methodName: string): bool =
|
||||||
server.router.hasMethod(methodName)
|
server.router.hasMethod(methodName)
|
||||||
|
|
||||||
|
proc executeMethod*(server: RpcServer,
|
||||||
|
methodName: string,
|
||||||
|
args: JsonNode): Future[StringOfJson] =
|
||||||
|
server.router.procs[methodName](args)
|
||||||
|
|
||||||
# Wrapper for message processing
|
# Wrapper for message processing
|
||||||
|
|
||||||
proc route*(server: RpcServer, line: string): Future[string] {.gcsafe.} =
|
proc route*(server: RpcServer, line: string): Future[string] {.gcsafe.} =
|
||||||
|
|
|
@ -17,13 +17,16 @@ server.addEthRpcs()
|
||||||
## Generate client convenience marshalling wrappers from forward declarations
|
## Generate client convenience marshalling wrappers from forward declarations
|
||||||
createRpcSigs(RpcSocketClient, sourceDir & DirSep & "ethcallsigs.nim")
|
createRpcSigs(RpcSocketClient, sourceDir & DirSep & "ethcallsigs.nim")
|
||||||
|
|
||||||
|
func rpcDynamicName(name: string): string =
|
||||||
|
"rpc." & name
|
||||||
|
|
||||||
## Create custom RPC with StUint input parameter
|
## Create custom RPC with StUint input parameter
|
||||||
server.rpc("rpc.uint256param") do(i: UInt256):
|
server.rpc(rpcDynamicName "uint256Param") do(i: UInt256):
|
||||||
let r = i + 1.stUint(256)
|
let r = i + 1.stUint(256)
|
||||||
return %r
|
return %r
|
||||||
|
|
||||||
## Create custom RPC with StUInt return parameter
|
## Create custom RPC with StUInt return parameter
|
||||||
server.rpc("rpc.testreturnuint256") do() -> UInt256:
|
server.rpc(rpcDynamicName "testReturnUint256") do() -> UInt256:
|
||||||
let r: UInt256 = "0x1234567890abcdef".parse(UInt256, 16)
|
let r: UInt256 = "0x1234567890abcdef".parse(UInt256, 16)
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
@ -31,15 +34,15 @@ proc testLocalCalls: Future[seq[StringOfJson]] =
|
||||||
## Call RPCs created with `rpc` locally.
|
## Call RPCs created with `rpc` locally.
|
||||||
## This simply demonstrates async calls of the procs generated by the `rpc` macro.
|
## This simply demonstrates async calls of the procs generated by the `rpc` macro.
|
||||||
let
|
let
|
||||||
uint256Param = rpcUInt256Param(%[%"0x1234567890"])
|
uint256Param = server.executeMethod("rpc.uint256Param", %[%"0x1234567890"])
|
||||||
returnUint256 = rpcTestReturnUInt256(%[])
|
returnUint256 = server.executeMethod("rpc.testReturnUint256", %[])
|
||||||
return all(uint256Param, returnUint256)
|
return all(uint256Param, returnUint256)
|
||||||
|
|
||||||
proc testRemoteUInt256: Future[seq[Response]] =
|
proc testRemoteUInt256: Future[seq[Response]] =
|
||||||
## Call function remotely on server, testing `stint` types
|
## Call function remotely on server, testing `stint` types
|
||||||
let
|
let
|
||||||
uint256Param = client.call("rpc.uint256param", %[%"0x1234567890"])
|
uint256Param = client.call("rpc.uint256Param", %[%"0x1234567890"])
|
||||||
returnUint256 = client.call("rpc.testreturnuint256", %[])
|
returnUint256 = client.call("rpc.testReturnUint256", %[])
|
||||||
return all(uint256Param, returnUint256)
|
return all(uint256Param, returnUint256)
|
||||||
|
|
||||||
proc testSigCalls: Future[seq[string]] =
|
proc testSigCalls: Future[seq[string]] =
|
||||||
|
|
|
@ -37,37 +37,37 @@ let
|
||||||
var s = newRpcSocketServer(["localhost:8545"])
|
var s = newRpcSocketServer(["localhost:8545"])
|
||||||
|
|
||||||
# RPC definitions
|
# RPC definitions
|
||||||
s.rpc("rpc.simplepath"):
|
s.rpc("rpc.simplePath"):
|
||||||
return %1
|
return %1
|
||||||
|
|
||||||
s.rpc("rpc.differentparams") do(a: int, b: string):
|
s.rpc("rpc.differentParams") do(a: int, b: string):
|
||||||
return %[%a, %b]
|
return %[%a, %b]
|
||||||
|
|
||||||
s.rpc("rpc.arrayparam") do(arr: array[0..5, byte], b: string):
|
s.rpc("rpc.arrayParam") do(arr: array[0..5, byte], b: string):
|
||||||
var res = %arr
|
var res = %arr
|
||||||
res.add %b
|
res.add %b
|
||||||
return %res
|
return %res
|
||||||
|
|
||||||
s.rpc("rpc.seqparam") do(a: string, s: seq[int]):
|
s.rpc("rpc.seqParam") do(a: string, s: seq[int]):
|
||||||
var res = newJArray()
|
var res = newJArray()
|
||||||
res.add %a
|
res.add %a
|
||||||
for item in s:
|
for item in s:
|
||||||
res.add %int(item)
|
res.add %int(item)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
s.rpc("rpc.objparam") do(a: string, obj: MyObject):
|
s.rpc("rpc.objParam") do(a: string, obj: MyObject):
|
||||||
return %obj
|
return %obj
|
||||||
|
|
||||||
s.rpc("rpc.returntypesimple") do(i: int) -> int:
|
s.rpc("rpc.returnTypeSimple") do(i: int) -> int:
|
||||||
return i
|
return i
|
||||||
|
|
||||||
s.rpc("rpc.returntypecomplex") do(i: int) -> Test2:
|
s.rpc("rpc.returnTypeComplex") do(i: int) -> Test2:
|
||||||
return Test2(x: [1, i, 3], y: "test")
|
return Test2(x: [1, i, 3], y: "test")
|
||||||
|
|
||||||
s.rpc("rpc.testreturns") do() -> int:
|
s.rpc("rpc.testReturns") do() -> int:
|
||||||
return 1234
|
return 1234
|
||||||
|
|
||||||
s.rpc("rpc.multivarsofonetype") do(a, b: string) -> string:
|
s.rpc("rpc.multiVarsOfOneType") do(a, b: string) -> string:
|
||||||
return a & " " & b
|
return a & " " & b
|
||||||
|
|
||||||
s.rpc("rpc.optional") do(obj: MyOptional) -> MyOptional:
|
s.rpc("rpc.optional") do(obj: MyOptional) -> MyOptional:
|
||||||
|
@ -122,102 +122,102 @@ s.rpc("rpc.optInObj") do(data: string, options: Option[MaybeOptions]) -> int:
|
||||||
# Tests
|
# Tests
|
||||||
suite "Server types":
|
suite "Server types":
|
||||||
test "On macro registration":
|
test "On macro registration":
|
||||||
check s.hasMethod("rpc.simplepath")
|
check s.hasMethod("rpc.simplePath")
|
||||||
check s.hasMethod("rpc.differentparams")
|
check s.hasMethod("rpc.differentParams")
|
||||||
check s.hasMethod("rpc.arrayparam")
|
check s.hasMethod("rpc.arrayParam")
|
||||||
check s.hasMethod("rpc.seqparam")
|
check s.hasMethod("rpc.seqParam")
|
||||||
check s.hasMethod("rpc.objparam")
|
check s.hasMethod("rpc.objParam")
|
||||||
check s.hasMethod("rpc.returntypesimple")
|
check s.hasMethod("rpc.returnTypeSimple")
|
||||||
check s.hasMethod("rpc.returntypecomplex")
|
check s.hasMethod("rpc.returnTypeComplex")
|
||||||
check s.hasMethod("rpc.testreturns")
|
check s.hasMethod("rpc.testReturns")
|
||||||
check s.hasMethod("rpc.multivarsofonetype")
|
check s.hasMethod("rpc.multiVarsOfOneType")
|
||||||
check s.hasMethod("rpc.optionalArg")
|
check s.hasMethod("rpc.optionalArg")
|
||||||
check s.hasMethod("rpc.mixedOptionalArg")
|
check s.hasMethod("rpc.mixedOptionalArg")
|
||||||
check s.hasMethod("rpc.optionalArgNotBuiltin")
|
check s.hasMethod("rpc.optionalArgNotBuiltin")
|
||||||
check s.hasMethod("rpc.optInObj")
|
check s.hasMethod("rpc.optInObj")
|
||||||
|
|
||||||
test "Simple paths":
|
test "Simple paths":
|
||||||
let r = waitFor rpcSimplePath(%[])
|
let r = waitFor s.executeMethod("rpc.simplePath", %[])
|
||||||
check r == "1"
|
check r == "1"
|
||||||
|
|
||||||
test "Different param types":
|
test "Different param types":
|
||||||
let
|
let
|
||||||
inp = %[%1, %"abc"]
|
inp = %[%1, %"abc"]
|
||||||
r = waitFor rpcDifferentParams(inp)
|
r = waitFor s.executeMethod("rpc.differentParams", inp)
|
||||||
check r == inp
|
check r == inp
|
||||||
|
|
||||||
test "Array parameters":
|
test "Array parameters":
|
||||||
let r1 = waitfor rpcArrayParam(%[%[1, 2, 3], %"hello"])
|
let r1 = waitfor s.executeMethod("rpc.arrayParam", %[%[1, 2, 3], %"hello"])
|
||||||
var ckR1 = %[1, 2, 3, 0, 0, 0]
|
var ckR1 = %[1, 2, 3, 0, 0, 0]
|
||||||
ckR1.elems.add %"hello"
|
ckR1.elems.add %"hello"
|
||||||
check r1 == ckR1
|
check r1 == ckR1
|
||||||
|
|
||||||
test "Seq parameters":
|
test "Seq parameters":
|
||||||
let r2 = waitfor rpcSeqParam(%[%"abc", %[1, 2, 3, 4, 5]])
|
let r2 = waitfor s.executeMethod("rpc.seqParam", %[%"abc", %[1, 2, 3, 4, 5]])
|
||||||
var ckR2 = %["abc"]
|
var ckR2 = %["abc"]
|
||||||
for i in 0..4: ckR2.add %(i + 1)
|
for i in 0..4: ckR2.add %(i + 1)
|
||||||
check r2 == ckR2
|
check r2 == ckR2
|
||||||
|
|
||||||
test "Object parameters":
|
test "Object parameters":
|
||||||
let r = waitfor rpcObjParam(%[%"abc", testObj])
|
let r = waitfor s.executeMethod("rpc.objParam", %[%"abc", testObj])
|
||||||
check r == testObj
|
check r == testObj
|
||||||
|
|
||||||
test "Simple return types":
|
test "Simple return types":
|
||||||
let
|
let
|
||||||
inp = %99
|
inp = %99
|
||||||
r1 = waitfor rpcReturnTypeSimple(%[%inp])
|
r1 = waitfor s.executeMethod("rpc.returnTypeSimple", %[%inp])
|
||||||
check r1 == inp
|
check r1 == inp
|
||||||
|
|
||||||
test "Complex return types":
|
test "Complex return types":
|
||||||
let
|
let
|
||||||
inp = 99
|
inp = 99
|
||||||
r1 = waitfor rpcReturnTypeComplex(%[%inp])
|
r1 = waitfor s.executeMethod("rpc.returnTypeComplex", %[%inp])
|
||||||
check r1 == %*{"x": %[1, inp, 3], "y": "test"}
|
check r1 == %*{"x": %[1, inp, 3], "y": "test"}
|
||||||
|
|
||||||
test "Option types":
|
test "Option types":
|
||||||
let
|
let
|
||||||
inp1 = MyOptional(maybeInt: some(75))
|
inp1 = MyOptional(maybeInt: some(75))
|
||||||
inp2 = MyOptional()
|
inp2 = MyOptional()
|
||||||
r1 = waitfor rpcOptional(%[%inp1])
|
r1 = waitfor s.executeMethod("rpc.optional", %[%inp1])
|
||||||
r2 = waitfor rpcOptional(%[%inp2])
|
r2 = waitfor s.executeMethod("rpc.optional", %[%inp2])
|
||||||
check r1 == %inp1
|
check r1 == %inp1
|
||||||
check r2 == %inp2
|
check r2 == %inp2
|
||||||
|
|
||||||
test "Return statement":
|
test "Return statement":
|
||||||
let r = waitFor rpcTestReturns(%[])
|
let r = waitFor s.executeMethod("rpc.testReturns", %[])
|
||||||
check r == %1234
|
check r == %1234
|
||||||
|
|
||||||
test "Runtime errors":
|
test "Runtime errors":
|
||||||
expect ValueError:
|
expect ValueError:
|
||||||
# root param not array
|
# root param not array
|
||||||
discard waitfor rpcArrayParam(%"test")
|
discard waitfor s.executeMethod("rpc.arrayParam", %"test")
|
||||||
expect ValueError:
|
expect ValueError:
|
||||||
# too big for array
|
# too big for array
|
||||||
discard waitfor rpcArrayParam(%[%[0, 1, 2, 3, 4, 5, 6], %"hello"])
|
discard waitfor s.executeMethod("rpc.arrayParam", %[%[0, 1, 2, 3, 4, 5, 6], %"hello"])
|
||||||
expect ValueError:
|
expect ValueError:
|
||||||
# wrong sub parameter type
|
# wrong sub parameter type
|
||||||
discard waitfor rpcArrayParam(%[%"test", %"hello"])
|
discard waitfor s.executeMethod("rpc.arrayParam", %[%"test", %"hello"])
|
||||||
expect ValueError:
|
expect ValueError:
|
||||||
# wrong param type
|
# wrong param type
|
||||||
discard waitFor rpcDifferentParams(%[%"abc", %1])
|
discard waitFor s.executeMethod("rpc.differentParams", %[%"abc", %1])
|
||||||
|
|
||||||
test "Multiple variables of one type":
|
test "Multiple variables of one type":
|
||||||
let r = waitfor rpcMultiVarsOfOneType(%[%"hello", %"world"])
|
let r = waitfor s.executeMethod("rpc.multiVarsOfOneType", %[%"hello", %"world"])
|
||||||
check r == %"hello world"
|
check r == %"hello world"
|
||||||
|
|
||||||
test "Optional arg":
|
test "Optional arg":
|
||||||
let
|
let
|
||||||
int1 = MyOptional(maybeInt: some(75))
|
int1 = MyOptional(maybeInt: some(75))
|
||||||
int2 = MyOptional(maybeInt: some(117))
|
int2 = MyOptional(maybeInt: some(117))
|
||||||
r1 = waitFor rpcOptionalArg(%[%117, %int1])
|
r1 = waitFor s.executeMethod("rpc.optionalArg", %[%117, %int1])
|
||||||
r2 = waitFor rpcOptionalArg(%[%117])
|
r2 = waitFor s.executeMethod("rpc.optionalArg", %[%117])
|
||||||
check r1 == %int1
|
check r1 == %int1
|
||||||
check r2 == %int2
|
check r2 == %int2
|
||||||
|
|
||||||
test "Mixed optional arg":
|
test "Mixed optional arg":
|
||||||
var ax = waitFor rpcMixedOptionalArg(%[%10, %11, %"hello", %12, %"world"])
|
var ax = waitFor s.executeMethod("rpc.mixedOptionalArg", %[%10, %11, %"hello", %12, %"world"])
|
||||||
check ax == %OptionalFields(a: 10, b: some(11), c: "hello", d: some(12), e: some("world"))
|
check ax == %OptionalFields(a: 10, b: some(11), c: "hello", d: some(12), e: some("world"))
|
||||||
var bx = waitFor rpcMixedOptionalArg(%[%10, newJNull(), %"hello"])
|
var bx = waitFor s.executeMethod("rpc.mixedOptionalArg", %[%10, newJNull(), %"hello"])
|
||||||
check bx == %OptionalFields(a: 10, c: "hello")
|
check bx == %OptionalFields(a: 10, c: "hello")
|
||||||
|
|
||||||
test "Non-built-in optional types":
|
test "Non-built-in optional types":
|
||||||
|
@ -225,33 +225,33 @@ suite "Server types":
|
||||||
t2 = Test2(x: [1, 2, 3], y: "Hello")
|
t2 = Test2(x: [1, 2, 3], y: "Hello")
|
||||||
testOpts1 = MyOptionalNotBuiltin(val: some(t2))
|
testOpts1 = MyOptionalNotBuiltin(val: some(t2))
|
||||||
testOpts2 = MyOptionalNotBuiltin()
|
testOpts2 = MyOptionalNotBuiltin()
|
||||||
var r = waitFor rpcOptionalArgNotBuiltin(%[%testOpts1])
|
var r = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[%testOpts1])
|
||||||
check r == %t2.y
|
check r == %t2.y
|
||||||
var r2 = waitFor rpcOptionalArgNotBuiltin(%[])
|
var r2 = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[])
|
||||||
check r2 == %"Empty1"
|
check r2 == %"Empty1"
|
||||||
var r3 = waitFor rpcOptionalArgNotBuiltin(%[%testOpts2])
|
var r3 = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[%testOpts2])
|
||||||
check r3 == %"Empty2"
|
check r3 == %"Empty2"
|
||||||
|
|
||||||
test "Manually set up JSON for optionals":
|
test "Manually set up JSON for optionals":
|
||||||
# Check manual set up json with optionals
|
# Check manual set up json with optionals
|
||||||
let opts1 = parseJson("""{"o1": true}""")
|
let opts1 = parseJson("""{"o1": true}""")
|
||||||
var r1 = waitFor rpcOptInObj(%[%"0x31ded", opts1])
|
var r1 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts1])
|
||||||
check r1 == %1
|
check r1 == %1
|
||||||
let opts2 = parseJson("""{"o2": true}""")
|
let opts2 = parseJson("""{"o2": true}""")
|
||||||
var r2 = waitFor rpcOptInObj(%[%"0x31ded", opts2])
|
var r2 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts2])
|
||||||
check r2 == %2
|
check r2 == %2
|
||||||
let opts3 = parseJson("""{"o3": true}""")
|
let opts3 = parseJson("""{"o3": true}""")
|
||||||
var r3 = waitFor rpcOptInObj(%[%"0x31ded", opts3])
|
var r3 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts3])
|
||||||
check r3 == %4
|
check r3 == %4
|
||||||
# Combinations
|
# Combinations
|
||||||
let opts4 = parseJson("""{"o1": true, "o3": true}""")
|
let opts4 = parseJson("""{"o1": true, "o3": true}""")
|
||||||
var r4 = waitFor rpcOptInObj(%[%"0x31ded", opts4])
|
var r4 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts4])
|
||||||
check r4 == %5
|
check r4 == %5
|
||||||
let opts5 = parseJson("""{"o2": true, "o3": true}""")
|
let opts5 = parseJson("""{"o2": true, "o3": true}""")
|
||||||
var r5 = waitFor rpcOptInObj(%[%"0x31ded", opts5])
|
var r5 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts5])
|
||||||
check r5 == %6
|
check r5 == %6
|
||||||
let opts6 = parseJson("""{"o1": true, "o2": true}""")
|
let opts6 = parseJson("""{"o1": true, "o2": true}""")
|
||||||
var r6 = waitFor rpcOptInObj(%[%"0x31ded", opts6])
|
var r6 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts6])
|
||||||
check r6 == %3
|
check r6 == %3
|
||||||
|
|
||||||
s.stop()
|
s.stop()
|
||||||
|
|
Loading…
Reference in New Issue