mirror of
https://github.com/sqlite/sqlite.git
synced 2025-08-05 15:55:57 +03:00
Add the new "bind_fallback" method to the "sqlite3" object in the TCL
interface. FossilOrigin-Name: c7f70b6d96338dba201e005104e7f7148c1a8cd767ab05e35b44617c4c797bc5
This commit is contained in:
14
manifest
14
manifest
@@ -1,5 +1,5 @@
|
|||||||
C New\stest\scase\sloaded\sinto\stest/fuzzdata8.db.
|
C Add\sthe\snew\s"bind_fallback"\smethod\sto\sthe\s"sqlite3"\sobject\sin\sthe\sTCL\ninterface.
|
||||||
D 2019-02-28T14:09:14.893
|
D 2019-02-28T17:29:19.212
|
||||||
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
|
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
|
||||||
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
|
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
|
||||||
F Makefile.in 1ad7263f38329c0ecea543c80f30af839ee714ea77fc391bf1a3fbb919a5b6b5
|
F Makefile.in 1ad7263f38329c0ecea543c80f30af839ee714ea77fc391bf1a3fbb919a5b6b5
|
||||||
@@ -524,7 +524,7 @@ F src/sqliteInt.h f253c4ec15e577a293a462e5049f8ea1d0c7a31819b3a88acdd24698df8f4d
|
|||||||
F src/sqliteLimit.h 1513bfb7b20378aa0041e7022d04acb73525de35b80b252f1b83fedb4de6a76b
|
F src/sqliteLimit.h 1513bfb7b20378aa0041e7022d04acb73525de35b80b252f1b83fedb4de6a76b
|
||||||
F src/status.c 46e7aec11f79dad50965a5ca5fa9de009f7d6bde08be2156f1538a0a296d4d0e
|
F src/status.c 46e7aec11f79dad50965a5ca5fa9de009f7d6bde08be2156f1538a0a296d4d0e
|
||||||
F src/table.c b46ad567748f24a326d9de40e5b9659f96ffff34
|
F src/table.c b46ad567748f24a326d9de40e5b9659f96ffff34
|
||||||
F src/tclsqlite.c de81c50e5112a8106da871b4d2dfef7748fe7625e148f85cc89ec7499b8e4de5
|
F src/tclsqlite.c cfe7f93daf9d8787f65e099efb67d7cdfc2c35236dec5d3f6758520bd3519424
|
||||||
F src/test1.c 353b066e7ec761c4c715c1c20b888e0e7a0b0c0eda7f68c110e032d63713cade
|
F src/test1.c 353b066e7ec761c4c715c1c20b888e0e7a0b0c0eda7f68c110e032d63713cade
|
||||||
F src/test2.c 3efb99ab7f1fc8d154933e02ae1378bac9637da5
|
F src/test2.c 3efb99ab7f1fc8d154933e02ae1378bac9637da5
|
||||||
F src/test3.c 61798bb0d38b915067a8c8e03f5a534b431181f802659a6616f9b4ff7d872644
|
F src/test3.c 61798bb0d38b915067a8c8e03f5a534b431181f802659a6616f9b4ff7d872644
|
||||||
@@ -1366,7 +1366,7 @@ F test/tabfunc01.test 20e98ffe55f35d8d33fd834ca8bf9d4b637e560af8fcd00464b4154d90
|
|||||||
F test/table.test eb3463b7add9f16a5bb836badf118cf391b809d09fdccd1f79684600d07ec132
|
F test/table.test eb3463b7add9f16a5bb836badf118cf391b809d09fdccd1f79684600d07ec132
|
||||||
F test/tableapi.test ecbcc29c4ab62c1912c3717c48ea5c5e59f7d64e4a91034e6148bd2b82f177f4
|
F test/tableapi.test ecbcc29c4ab62c1912c3717c48ea5c5e59f7d64e4a91034e6148bd2b82f177f4
|
||||||
F test/tableopts.test dba698ba97251017b7c80d738c198d39ab747930
|
F test/tableopts.test dba698ba97251017b7c80d738c198d39ab747930
|
||||||
F test/tclsqlite.test 0037c0ca7fd3da08202a807f7b76590019841edb9f459fcfcf52aed7212bf853
|
F test/tclsqlite.test 5a06962d8f18edf4703931f6b7dacd83678d02fa5c8ced9a7958c007ad58626a
|
||||||
F test/tempdb.test 4cdaa23ddd8acb4d79cbb1b68ccdfd09b0537aaba909ca69a876157c2a2cbd08
|
F test/tempdb.test 4cdaa23ddd8acb4d79cbb1b68ccdfd09b0537aaba909ca69a876157c2a2cbd08
|
||||||
F test/tempdb2.test 2479226e4cb96f4c663eccd2d12c077cf6bda29ca5cc69a8a58a06127105dd62
|
F test/tempdb2.test 2479226e4cb96f4c663eccd2d12c077cf6bda29ca5cc69a8a58a06127105dd62
|
||||||
F test/tempfault.test 0c0d349c9a99bf5f374655742577f8712c647900
|
F test/tempfault.test 0c0d349c9a99bf5f374655742577f8712c647900
|
||||||
@@ -1805,7 +1805,7 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
|
|||||||
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
|
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
|
||||||
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
|
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
|
||||||
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
|
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
|
||||||
P d5250db6322103326f0d5782ba049996d9ce8784f9e53a3112fb6f09f888f1c3
|
P 00ae0c6c4815366bd2f36bc054b13bc7b568dd0a3caceddf0eba4db33f010ee4
|
||||||
R 76ee1637810bdaf539a762a9c646979d
|
R 448ca78435be9ef8ac1ee1c7bdd459a0
|
||||||
U drh
|
U drh
|
||||||
Z ac7f7b28caabfd1523a4f739ee67d612
|
Z 8dd6bd727b5289a2522f6d5168203921
|
||||||
|
@@ -1 +1 @@
|
|||||||
00ae0c6c4815366bd2f36bc054b13bc7b568dd0a3caceddf0eba4db33f010ee4
|
c7f70b6d96338dba201e005104e7f7148c1a8cd767ab05e35b44617c4c797bc5
|
127
src/tclsqlite.c
127
src/tclsqlite.c
@@ -159,6 +159,7 @@ struct SqliteDb {
|
|||||||
char *zTraceV2; /* The trace_v2 callback routine */
|
char *zTraceV2; /* The trace_v2 callback routine */
|
||||||
char *zProfile; /* The profile callback routine */
|
char *zProfile; /* The profile callback routine */
|
||||||
char *zProgress; /* The progress callback routine */
|
char *zProgress; /* The progress callback routine */
|
||||||
|
char *zBindFallback; /* Callback to invoke on a binding miss */
|
||||||
char *zAuth; /* The authorization callback routine */
|
char *zAuth; /* The authorization callback routine */
|
||||||
int disableAuth; /* Disable the authorizer if it exists */
|
int disableAuth; /* Disable the authorizer if it exists */
|
||||||
char *zNull; /* Text to substitute for an SQL NULL value */
|
char *zNull; /* Text to substitute for an SQL NULL value */
|
||||||
@@ -549,6 +550,9 @@ static void SQLITE_TCLAPI DbDeleteCmd(void *db){
|
|||||||
if( pDb->zProfile ){
|
if( pDb->zProfile ){
|
||||||
Tcl_Free(pDb->zProfile);
|
Tcl_Free(pDb->zProfile);
|
||||||
}
|
}
|
||||||
|
if( pDb->zBindFallback ){
|
||||||
|
Tcl_Free(pDb->zBindFallback);
|
||||||
|
}
|
||||||
if( pDb->zAuth ){
|
if( pDb->zAuth ){
|
||||||
Tcl_Free(pDb->zAuth);
|
Tcl_Free(pDb->zAuth);
|
||||||
}
|
}
|
||||||
@@ -1301,6 +1305,8 @@ static int dbPrepareAndBind(
|
|||||||
int iParm = 0; /* Next free entry in apParm */
|
int iParm = 0; /* Next free entry in apParm */
|
||||||
char c;
|
char c;
|
||||||
int i;
|
int i;
|
||||||
|
int needResultReset = 0; /* Need to invoke Tcl_ResetResult() */
|
||||||
|
int rc = SQLITE_OK; /* Value to return */
|
||||||
Tcl_Interp *interp = pDb->interp;
|
Tcl_Interp *interp = pDb->interp;
|
||||||
|
|
||||||
*ppPreStmt = 0;
|
*ppPreStmt = 0;
|
||||||
@@ -1388,6 +1394,25 @@ static int dbPrepareAndBind(
|
|||||||
const char *zVar = sqlite3_bind_parameter_name(pStmt, i);
|
const char *zVar = sqlite3_bind_parameter_name(pStmt, i);
|
||||||
if( zVar!=0 && (zVar[0]=='$' || zVar[0]==':' || zVar[0]=='@') ){
|
if( zVar!=0 && (zVar[0]=='$' || zVar[0]==':' || zVar[0]=='@') ){
|
||||||
Tcl_Obj *pVar = Tcl_GetVar2Ex(interp, &zVar[1], 0, 0);
|
Tcl_Obj *pVar = Tcl_GetVar2Ex(interp, &zVar[1], 0, 0);
|
||||||
|
if( pVar==0 && pDb->zBindFallback!=0 ){
|
||||||
|
Tcl_Obj *pCmd;
|
||||||
|
int rx;
|
||||||
|
pCmd = Tcl_NewStringObj(pDb->zBindFallback, -1);
|
||||||
|
Tcl_IncrRefCount(pCmd);
|
||||||
|
Tcl_ListObjAppendElement(interp, pCmd, Tcl_NewStringObj(zVar,-1));
|
||||||
|
if( needResultReset ) Tcl_ResetResult(interp);
|
||||||
|
needResultReset = 1;
|
||||||
|
rx = Tcl_EvalObjEx(interp, pCmd, TCL_EVAL_DIRECT);
|
||||||
|
Tcl_DecrRefCount(pCmd);
|
||||||
|
if( rx==TCL_OK ){
|
||||||
|
pVar = Tcl_GetObjResult(interp);
|
||||||
|
}else if( rx==TCL_ERROR ){
|
||||||
|
rc = TCL_ERROR;
|
||||||
|
break;
|
||||||
|
}else{
|
||||||
|
pVar = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
if( pVar ){
|
if( pVar ){
|
||||||
int n;
|
int n;
|
||||||
u8 *data;
|
u8 *data;
|
||||||
@@ -1423,12 +1448,14 @@ static int dbPrepareAndBind(
|
|||||||
}else{
|
}else{
|
||||||
sqlite3_bind_null(pStmt, i);
|
sqlite3_bind_null(pStmt, i);
|
||||||
}
|
}
|
||||||
|
if( needResultReset ) Tcl_ResetResult(pDb->interp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pPreStmt->nParm = iParm;
|
pPreStmt->nParm = iParm;
|
||||||
*ppPreStmt = pPreStmt;
|
*ppPreStmt = pPreStmt;
|
||||||
|
if( needResultReset && rc==TCL_OK ) Tcl_ResetResult(pDb->interp);
|
||||||
|
|
||||||
return TCL_OK;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1887,35 +1914,36 @@ static int SQLITE_TCLAPI DbObjCmd(
|
|||||||
int choice;
|
int choice;
|
||||||
int rc = TCL_OK;
|
int rc = TCL_OK;
|
||||||
static const char *DB_strs[] = {
|
static const char *DB_strs[] = {
|
||||||
"authorizer", "backup", "busy",
|
"authorizer", "backup", "bind_fallback",
|
||||||
"cache", "changes", "close",
|
"busy", "cache", "changes",
|
||||||
"collate", "collation_needed", "commit_hook",
|
"close", "collate", "collation_needed",
|
||||||
"complete", "copy", "deserialize",
|
"commit_hook", "complete", "copy",
|
||||||
"enable_load_extension", "errorcode", "eval",
|
"deserialize", "enable_load_extension", "errorcode",
|
||||||
"exists", "function", "incrblob",
|
"eval", "exists", "function",
|
||||||
"interrupt", "last_insert_rowid", "nullvalue",
|
"incrblob", "interrupt", "last_insert_rowid",
|
||||||
"onecolumn", "preupdate", "profile",
|
"nullvalue", "onecolumn", "preupdate",
|
||||||
"progress", "rekey", "restore",
|
"profile", "progress", "rekey",
|
||||||
"rollback_hook", "serialize", "status",
|
"restore", "rollback_hook", "serialize",
|
||||||
"timeout", "total_changes", "trace",
|
"status", "timeout", "total_changes",
|
||||||
"trace_v2", "transaction", "unlock_notify",
|
"trace", "trace_v2", "transaction",
|
||||||
"update_hook", "version", "wal_hook",
|
"unlock_notify", "update_hook", "version",
|
||||||
0
|
"wal_hook", 0
|
||||||
};
|
};
|
||||||
enum DB_enum {
|
enum DB_enum {
|
||||||
DB_AUTHORIZER, DB_BACKUP, DB_BUSY,
|
DB_AUTHORIZER, DB_BACKUP, DB_BIND_FALLBACK,
|
||||||
DB_CACHE, DB_CHANGES, DB_CLOSE,
|
DB_BUSY, DB_CACHE, DB_CHANGES,
|
||||||
DB_COLLATE, DB_COLLATION_NEEDED, DB_COMMIT_HOOK,
|
DB_CLOSE, DB_COLLATE, DB_COLLATION_NEEDED,
|
||||||
DB_COMPLETE, DB_COPY, DB_DESERIALIZE,
|
DB_COMMIT_HOOK, DB_COMPLETE, DB_COPY,
|
||||||
DB_ENABLE_LOAD_EXTENSION, DB_ERRORCODE, DB_EVAL,
|
DB_DESERIALIZE, DB_ENABLE_LOAD_EXTENSION,DB_ERRORCODE,
|
||||||
DB_EXISTS, DB_FUNCTION, DB_INCRBLOB,
|
DB_EVAL, DB_EXISTS, DB_FUNCTION,
|
||||||
DB_INTERRUPT, DB_LAST_INSERT_ROWID, DB_NULLVALUE,
|
DB_INCRBLOB, DB_INTERRUPT, DB_LAST_INSERT_ROWID,
|
||||||
DB_ONECOLUMN, DB_PREUPDATE, DB_PROFILE,
|
DB_NULLVALUE, DB_ONECOLUMN, DB_PREUPDATE,
|
||||||
DB_PROGRESS, DB_REKEY, DB_RESTORE,
|
DB_PROFILE, DB_PROGRESS, DB_REKEY,
|
||||||
DB_ROLLBACK_HOOK, DB_SERIALIZE, DB_STATUS,
|
DB_RESTORE, DB_ROLLBACK_HOOK, DB_SERIALIZE,
|
||||||
DB_TIMEOUT, DB_TOTAL_CHANGES, DB_TRACE,
|
DB_STATUS, DB_TIMEOUT, DB_TOTAL_CHANGES,
|
||||||
DB_TRACE_V2, DB_TRANSACTION, DB_UNLOCK_NOTIFY,
|
DB_TRACE, DB_TRACE_V2, DB_TRANSACTION,
|
||||||
DB_UPDATE_HOOK, DB_VERSION, DB_WAL_HOOK
|
DB_UNLOCK_NOTIFY, DB_UPDATE_HOOK, DB_VERSION,
|
||||||
|
DB_WAL_HOOK
|
||||||
};
|
};
|
||||||
/* don't leave trailing commas on DB_enum, it confuses the AIX xlc compiler */
|
/* don't leave trailing commas on DB_enum, it confuses the AIX xlc compiler */
|
||||||
|
|
||||||
@@ -2037,6 +2065,49 @@ static int SQLITE_TCLAPI DbObjCmd(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* $db bind_fallback ?CALLBACK?
|
||||||
|
**
|
||||||
|
** When resolving bind parameters in an SQL statement, if the parameter
|
||||||
|
** cannot be associated with a TCL variable then invoke CALLBACK with a
|
||||||
|
** single argument that is the name of the parameter and use the return
|
||||||
|
** value of the CALLBACK as the binding. If CALLBACK returns something
|
||||||
|
** other than TCL_OK or TCL_ERROR then bind a NULL.
|
||||||
|
**
|
||||||
|
** If CALLBACK is an empty string, then revert to the default behavior
|
||||||
|
** which is to set the binding to NULL.
|
||||||
|
**
|
||||||
|
** If CALLBACK returns an error, that causes the statement execution to
|
||||||
|
** abort. Hence, to configure a connection so that it throws an error
|
||||||
|
** on an attempt to bind an unknown variable, do something like this:
|
||||||
|
**
|
||||||
|
** proc bind_error {name} {error "no such variable: $name"}
|
||||||
|
** db bind_fallback bind_error
|
||||||
|
*/
|
||||||
|
case DB_BIND_FALLBACK: {
|
||||||
|
if( objc>3 ){
|
||||||
|
Tcl_WrongNumArgs(interp, 2, objv, "?CALLBACK?");
|
||||||
|
return TCL_ERROR;
|
||||||
|
}else if( objc==2 ){
|
||||||
|
if( pDb->zBindFallback ){
|
||||||
|
Tcl_AppendResult(interp, pDb->zBindFallback, (char*)0);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
char *zCallback;
|
||||||
|
int len;
|
||||||
|
if( pDb->zBindFallback ){
|
||||||
|
Tcl_Free(pDb->zBindFallback);
|
||||||
|
}
|
||||||
|
zCallback = Tcl_GetStringFromObj(objv[2], &len);
|
||||||
|
if( zCallback && len>0 ){
|
||||||
|
pDb->zBindFallback = Tcl_Alloc( len + 1 );
|
||||||
|
memcpy(pDb->zBindFallback, zCallback, len+1);
|
||||||
|
}else{
|
||||||
|
pDb->zBindFallback = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
/* $db busy ?CALLBACK?
|
/* $db busy ?CALLBACK?
|
||||||
**
|
**
|
||||||
** Invoke the given callback if an SQL statement attempts to open
|
** Invoke the given callback if an SQL statement attempts to open
|
||||||
|
@@ -42,7 +42,7 @@ do_test tcl-1.1.1 {
|
|||||||
do_test tcl-1.2 {
|
do_test tcl-1.2 {
|
||||||
set v [catch {db bogus} msg]
|
set v [catch {db bogus} msg]
|
||||||
lappend v $msg
|
lappend v $msg
|
||||||
} {1 {bad option "bogus": must be authorizer, backup, busy, cache, changes, close, collate, collation_needed, commit_hook, complete, copy, deserialize, enable_load_extension, errorcode, eval, exists, function, incrblob, interrupt, last_insert_rowid, nullvalue, onecolumn, preupdate, profile, progress, rekey, restore, rollback_hook, serialize, status, timeout, total_changes, trace, trace_v2, transaction, unlock_notify, update_hook, version, or wal_hook}}
|
} {1 {bad option "bogus": must be authorizer, backup, bind_fallback, busy, cache, changes, close, collate, collation_needed, commit_hook, complete, copy, deserialize, enable_load_extension, errorcode, eval, exists, function, incrblob, interrupt, last_insert_rowid, nullvalue, onecolumn, preupdate, profile, progress, rekey, restore, rollback_hook, serialize, status, timeout, total_changes, trace, trace_v2, transaction, unlock_notify, update_hook, version, or wal_hook}}
|
||||||
do_test tcl-1.2.1 {
|
do_test tcl-1.2.1 {
|
||||||
set v [catch {db cache bogus} msg]
|
set v [catch {db cache bogus} msg]
|
||||||
lappend v $msg
|
lappend v $msg
|
||||||
@@ -791,5 +791,60 @@ do_test 17.6.3 {
|
|||||||
list [catch { db function xyz -n object ret } msg] $msg
|
list [catch { db function xyz -n object ret } msg] $msg
|
||||||
} {1 {bad option "-n": must be -argcount, -deterministic or -returntype}}
|
} {1 {bad option "-n": must be -argcount, -deterministic or -returntype}}
|
||||||
|
|
||||||
finish_test
|
# 2019-02-28: The "bind_fallback" command.
|
||||||
|
#
|
||||||
|
do_test 18.100 {
|
||||||
|
unset -nocomplain bindings abc def ghi jkl mno e01 e02
|
||||||
|
set bindings(abc) [expr {1+2}]
|
||||||
|
set bindings(def) {hello}
|
||||||
|
set bindings(ghi) [expr {3.1415926*1.0}]
|
||||||
|
proc bind_callback {nm} {
|
||||||
|
global bindings
|
||||||
|
set n2 [string range $nm 1 end]
|
||||||
|
if {[info exists bindings($n2)]} {
|
||||||
|
return $bindings($n2)
|
||||||
|
}
|
||||||
|
if {[string match e* $n2]} {
|
||||||
|
error "no such variable: $nm"
|
||||||
|
}
|
||||||
|
return -code return {}
|
||||||
|
}
|
||||||
|
db bind_fallback bind_callback
|
||||||
|
db eval {SELECT $abc, typeof($abc), $def, typeof($def), $ghi, typeof($ghi)}
|
||||||
|
} {3 integer hello text 3.1415926 real}
|
||||||
|
do_test 18.110 {
|
||||||
|
db eval {SELECT quote(@def), typeof(@def)}
|
||||||
|
} {X'68656C6C6F' blob}
|
||||||
|
do_execsql_test 18.120 {
|
||||||
|
SELECT typeof($mno);
|
||||||
|
} {null}
|
||||||
|
do_catchsql_test 18.130 {
|
||||||
|
SELECT $e01;
|
||||||
|
} {1 {no such variable: $e01}}
|
||||||
|
do_test 18.140 {
|
||||||
|
db bind_fallback
|
||||||
|
} {bind_callback}
|
||||||
|
do_test 18.200 {
|
||||||
|
db bind_fallback {}
|
||||||
|
db eval {SELECT $abc, typeof($abc), $def, typeof($def), $ghi, typeof($ghi)}
|
||||||
|
} {{} null {} null {} null}
|
||||||
|
do_test 18.300 {
|
||||||
|
unset -nocomplain bindings
|
||||||
|
proc bind_callback {nm} {lappend ::bindings $nm}
|
||||||
|
db bind_fallback bind_callback
|
||||||
|
db eval {SELECT $abc, @def, $ghi(123), :mno}
|
||||||
|
set bindings
|
||||||
|
} {{$abc} @def {$ghi(123)} :mno}
|
||||||
|
do_test 18.900 {
|
||||||
|
set rc [catch {db bind_fallback a b} msg]
|
||||||
|
lappend rc $msg
|
||||||
|
} {1 {wrong # args: should be "db bind_fallback ?CALLBACK?"}}
|
||||||
|
do_test 18.910 {
|
||||||
|
db bind_fallback bind_fallback_does_not_exist
|
||||||
|
} {}
|
||||||
|
do_catchsql_test 19.911 {
|
||||||
|
SELECT $abc, typeof($abc), $def, typeof($def), $ghi, typeof($ghi);
|
||||||
|
} {1 {invalid command name "bind_fallback_does_not_exist"}}
|
||||||
|
db bind_fallback {}
|
||||||
|
|
||||||
|
finish_test
|
||||||
|
Reference in New Issue
Block a user