| OLD | NEW |
| (Empty) |
| 1 # 2001 October 12 | |
| 2 # | |
| 3 # The author disclaims copyright to this source code. In place of | |
| 4 # a legal notice, here is a blessing: | |
| 5 # | |
| 6 # May you do good and not evil. | |
| 7 # May you find forgiveness for yourself and forgive others. | |
| 8 # May you share freely, never taking more than you give. | |
| 9 # | |
| 10 #*********************************************************************** | |
| 11 # This file implements regression tests for SQLite library. The | |
| 12 # focus of this file is testing for correct handling of I/O errors | |
| 13 # such as writes failing because the disk is full. | |
| 14 # | |
| 15 # The tests in this file use special facilities that are only | |
| 16 # available in the SQLite test fixture. | |
| 17 # | |
| 18 # $Id: ioerr.test,v 1.43 2009/04/06 17:50:03 danielk1977 Exp $ | |
| 19 | |
| 20 set testdir [file dirname $argv0] | |
| 21 source $testdir/tester.tcl | |
| 22 | |
| 23 # If SQLITE_DEFAULT_AUTOVACUUM is set to true, then a simulated IO error | |
| 24 # on the 8th IO operation in the SQL script below doesn't report an error. | |
| 25 # | |
| 26 # This is because the 8th IO call attempts to read page 2 of the database | |
| 27 # file when the file on disk is only 1 page. The pager layer detects that | |
| 28 # this has happened and suppresses the error returned by the OS layer. | |
| 29 # | |
| 30 do_ioerr_test ioerr-1 -erc 1 -ckrefcount 1 -sqlprep { | |
| 31 SELECT * FROM sqlite_master; | |
| 32 } -sqlbody { | |
| 33 CREATE TABLE t1(a,b,c); | |
| 34 SELECT * FROM sqlite_master; | |
| 35 BEGIN TRANSACTION; | |
| 36 INSERT INTO t1 VALUES(1,2,3); | |
| 37 INSERT INTO t1 VALUES(4,5,6); | |
| 38 ROLLBACK; | |
| 39 SELECT * FROM t1; | |
| 40 BEGIN TRANSACTION; | |
| 41 INSERT INTO t1 VALUES(1,2,3); | |
| 42 INSERT INTO t1 VALUES(4,5,6); | |
| 43 COMMIT; | |
| 44 SELECT * FROM t1; | |
| 45 DELETE FROM t1 WHERE a<100; | |
| 46 } -exclude [expr [string match [execsql {pragma auto_vacuum}] 1] ? 4 : 0] | |
| 47 | |
| 48 # Test for IO errors during a VACUUM. | |
| 49 # | |
| 50 # The first IO call is excluded from the test. This call attempts to read | |
| 51 # the file-header of the temporary database used by VACUUM. Since the | |
| 52 # database doesn't exist at that point, the IO error is not detected. | |
| 53 # | |
| 54 # Additionally, if auto-vacuum is enabled, the 12th IO error is not | |
| 55 # detected. Same reason as the 8th in the test case above. | |
| 56 # | |
| 57 ifcapable vacuum { | |
| 58 do_ioerr_test ioerr-2 -cksum true -ckrefcount true -sqlprep { | |
| 59 BEGIN; | |
| 60 CREATE TABLE t1(a, b, c); | |
| 61 INSERT INTO t1 VALUES(1, randstr(50,50), randstr(50,50)); | |
| 62 INSERT INTO t1 SELECT a+2, b||'-'||rowid, c||'-'||rowid FROM t1; | |
| 63 INSERT INTO t1 SELECT a+4, b||'-'||rowid, c||'-'||rowid FROM t1; | |
| 64 INSERT INTO t1 SELECT a+8, b||'-'||rowid, c||'-'||rowid FROM t1; | |
| 65 INSERT INTO t1 SELECT a+16, b||'-'||rowid, c||'-'||rowid FROM t1; | |
| 66 INSERT INTO t1 SELECT a+32, b||'-'||rowid, c||'-'||rowid FROM t1; | |
| 67 INSERT INTO t1 SELECT a+64, b||'-'||rowid, c||'-'||rowid FROM t1; | |
| 68 INSERT INTO t1 SELECT a+128, b||'-'||rowid, c||'-'||rowid FROM t1; | |
| 69 INSERT INTO t1 VALUES(1, randstr(600,600), randstr(600,600)); | |
| 70 CREATE TABLE t2 AS SELECT * FROM t1; | |
| 71 CREATE TABLE t3 AS SELECT * FROM t1; | |
| 72 COMMIT; | |
| 73 DROP TABLE t2; | |
| 74 } -sqlbody { | |
| 75 VACUUM; | |
| 76 } -exclude [list \ | |
| 77 1 [expr [string match [execsql {pragma auto_vacuum}] 1]?9:-1]] | |
| 78 } | |
| 79 | |
| 80 do_ioerr_test ioerr-3 -ckrefcount true -tclprep { | |
| 81 execsql { | |
| 82 PRAGMA cache_size = 10; | |
| 83 BEGIN; | |
| 84 CREATE TABLE abc(a); | |
| 85 INSERT INTO abc VALUES(randstr(1500,1500)); -- Page 4 is overflow | |
| 86 } | |
| 87 for {set i 0} {$i<150} {incr i} { | |
| 88 execsql { | |
| 89 INSERT INTO abc VALUES(randstr(100,100)); | |
| 90 } | |
| 91 } | |
| 92 execsql COMMIT | |
| 93 } -sqlbody { | |
| 94 CREATE TABLE abc2(a); | |
| 95 BEGIN; | |
| 96 DELETE FROM abc WHERE length(a)>100; | |
| 97 UPDATE abc SET a = randstr(90,90); | |
| 98 COMMIT; | |
| 99 CREATE TABLE abc3(a); | |
| 100 } | |
| 101 | |
| 102 # Test IO errors that can occur retrieving a record header that flows over | |
| 103 # onto an overflow page. | |
| 104 do_ioerr_test ioerr-4 -ckrefcount true -tclprep { | |
| 105 set sql "CREATE TABLE abc(a1" | |
| 106 for {set i 2} {$i<1300} {incr i} { | |
| 107 append sql ", a$i" | |
| 108 } | |
| 109 append sql ");" | |
| 110 execsql $sql | |
| 111 execsql {INSERT INTO abc (a1) VALUES(NULL)} | |
| 112 } -sqlbody { | |
| 113 SELECT * FROM abc; | |
| 114 } | |
| 115 | |
| 116 | |
| 117 # Test IO errors that may occur during a multi-file commit. | |
| 118 # | |
| 119 # Tests 8 and 17 are excluded when auto-vacuum is enabled for the same | |
| 120 # reason as in test cases ioerr-1.XXX | |
| 121 ifcapable attach { | |
| 122 set ex "" | |
| 123 if {[string match [execsql {pragma auto_vacuum}] 1]} { | |
| 124 set ex [list 4 17] | |
| 125 } | |
| 126 do_ioerr_test ioerr-5 -restoreprng 0 -ckrefcount true -sqlprep { | |
| 127 ATTACH 'test2.db' AS test2; | |
| 128 } -sqlbody { | |
| 129 BEGIN; | |
| 130 CREATE TABLE t1(a,b,c); | |
| 131 CREATE TABLE test2.t2(a,b,c); | |
| 132 COMMIT; | |
| 133 } -exclude $ex | |
| 134 } | |
| 135 | |
| 136 # Test IO errors when replaying two hot journals from a 2-file | |
| 137 # transaction. This test only runs on UNIX. | |
| 138 ifcapable crashtest&&attach { | |
| 139 if {![catch {sqlite3 -has_codec} r] && !$r} { | |
| 140 do_ioerr_test ioerr-6 -ckrefcount true -tclprep { | |
| 141 execsql { | |
| 142 ATTACH 'test2.db' as aux; | |
| 143 CREATE TABLE tx(a, b); | |
| 144 CREATE TABLE aux.ty(a, b); | |
| 145 } | |
| 146 set rc [crashsql -delay 2 -file test2.db-journal { | |
| 147 ATTACH 'test2.db' as aux; | |
| 148 PRAGMA cache_size = 10; | |
| 149 BEGIN; | |
| 150 CREATE TABLE aux.t2(a, b, c); | |
| 151 CREATE TABLE t1(a, b, c); | |
| 152 COMMIT; | |
| 153 }] | |
| 154 if {$rc!="1 {child process exited abnormally}"} { | |
| 155 error "Wrong error message: $rc" | |
| 156 } | |
| 157 } -sqlbody { | |
| 158 SELECT * FROM sqlite_master; | |
| 159 SELECT * FROM aux.sqlite_master; | |
| 160 } | |
| 161 } | |
| 162 } | |
| 163 | |
| 164 # Test handling of IO errors that occur while rolling back hot journal | |
| 165 # files. | |
| 166 # | |
| 167 # These tests can't be run on windows because the windows version of | |
| 168 # SQLite holds a mandatory exclusive lock on journal files it has open. | |
| 169 # | |
| 170 if {$tcl_platform(platform)!="windows"} { | |
| 171 do_ioerr_test ioerr-7 -tclprep { | |
| 172 db close | |
| 173 sqlite3 db2 test2.db | |
| 174 db2 eval { | |
| 175 PRAGMA synchronous = 0; | |
| 176 CREATE TABLE t1(a, b); | |
| 177 INSERT INTO t1 VALUES(1, 2); | |
| 178 BEGIN; | |
| 179 INSERT INTO t1 VALUES(3, 4); | |
| 180 } | |
| 181 copy_file test2.db test.db | |
| 182 copy_file test2.db-journal test.db-journal | |
| 183 db2 close | |
| 184 } -tclbody { | |
| 185 sqlite3 db test.db | |
| 186 db eval { | |
| 187 SELECT * FROM t1; | |
| 188 } | |
| 189 } -exclude 1 | |
| 190 } | |
| 191 | |
| 192 # For test coverage: Cause an I/O failure while trying to read a | |
| 193 # short field (one that fits into a Mem buffer without mallocing | |
| 194 # for space). | |
| 195 # | |
| 196 do_ioerr_test ioerr-8 -ckrefcount true -tclprep { | |
| 197 execsql { | |
| 198 CREATE TABLE t1(a,b,c); | |
| 199 INSERT INTO t1 VALUES(randstr(200,200), randstr(1000,1000), 2); | |
| 200 } | |
| 201 db close | |
| 202 sqlite3 db test.db | |
| 203 } -sqlbody { | |
| 204 SELECT c FROM t1; | |
| 205 } | |
| 206 | |
| 207 # For test coverage: Cause an IO error whilst reading the master-journal | |
| 208 # name from a journal file. | |
| 209 if {$tcl_platform(platform)=="unix"} { | |
| 210 do_ioerr_test ioerr-9 -ckrefcount true -tclprep { | |
| 211 execsql { | |
| 212 CREATE TABLE t1(a,b,c); | |
| 213 INSERT INTO t1 VALUES(randstr(200,200), randstr(1000,1000), 2); | |
| 214 BEGIN; | |
| 215 INSERT INTO t1 VALUES(randstr(200,200), randstr(1000,1000), 2); | |
| 216 } | |
| 217 copy_file test.db-journal test2.db-journal | |
| 218 execsql { | |
| 219 COMMIT; | |
| 220 } | |
| 221 copy_file test2.db-journal test.db-journal | |
| 222 set f [open test.db-journal a] | |
| 223 fconfigure $f -encoding binary | |
| 224 puts -nonewline $f "hello" | |
| 225 puts -nonewline $f "\x00\x00\x00\x05\x01\x02\x03\x04" | |
| 226 puts -nonewline $f "\xd9\xd5\x05\xf9\x20\xa1\x63\xd7" | |
| 227 close $f | |
| 228 } -sqlbody { | |
| 229 SELECT a FROM t1; | |
| 230 } | |
| 231 } | |
| 232 | |
| 233 # For test coverage: Cause an IO error during statement playback (i.e. | |
| 234 # a constraint). | |
| 235 do_ioerr_test ioerr-10 -ckrefcount true -tclprep { | |
| 236 execsql { | |
| 237 BEGIN; | |
| 238 CREATE TABLE t1(a PRIMARY KEY, b); | |
| 239 } | |
| 240 for {set i 0} {$i < 500} {incr i} { | |
| 241 execsql {INSERT INTO t1 VALUES(:i, 'hello world');} | |
| 242 } | |
| 243 execsql { | |
| 244 COMMIT; | |
| 245 } | |
| 246 } -tclbody { | |
| 247 | |
| 248 catch {execsql { | |
| 249 BEGIN; | |
| 250 INSERT INTO t1 VALUES('abc', 123); | |
| 251 INSERT INTO t1 VALUES('def', 123); | |
| 252 INSERT INTO t1 VALUES('ghi', 123); | |
| 253 INSERT INTO t1 SELECT (a+500)%900, 'good string' FROM t1; | |
| 254 }} msg | |
| 255 | |
| 256 if {$msg != "column a is not unique"} { | |
| 257 error $msg | |
| 258 } | |
| 259 } | |
| 260 | |
| 261 # Assertion fault bug reported by alex dimitrov. | |
| 262 # | |
| 263 do_ioerr_test ioerr-11 -ckrefcount true -erc 1 -sqlprep { | |
| 264 CREATE TABLE A(Id INTEGER, Name TEXT); | |
| 265 INSERT INTO A(Id, Name) VALUES(1, 'Name'); | |
| 266 } -sqlbody { | |
| 267 UPDATE A SET Id = 2, Name = 'Name2' WHERE Id = 1; | |
| 268 } | |
| 269 | |
| 270 # Test that an io error encountered in a sync() caused by a call to | |
| 271 # sqlite3_release_memory() is handled Ok. Only try this if | |
| 272 # memory-management is enabled. | |
| 273 # | |
| 274 ifcapable memorymanage { | |
| 275 do_ioerr_test memmanage-ioerr1 -ckrefcount true -sqlprep { | |
| 276 BEGIN; | |
| 277 CREATE TABLE t1(a, b, c); | |
| 278 INSERT INTO t1 VALUES(randstr(50,50), randstr(100,100), randstr(10,10)); | |
| 279 INSERT INTO t1 SELECT randstr(50,50), randstr(9,9), randstr(90,90) FROM t1; | |
| 280 INSERT INTO t1 SELECT randstr(50,50), randstr(9,9), randstr(90,90) FROM t1; | |
| 281 INSERT INTO t1 SELECT randstr(50,50), randstr(9,9), randstr(90,90) FROM t1; | |
| 282 INSERT INTO t1 SELECT randstr(50,50), randstr(9,9), randstr(90,90) FROM t1; | |
| 283 INSERT INTO t1 SELECT randstr(50,50), randstr(9,9), randstr(90,90) FROM t1; | |
| 284 } -tclbody { | |
| 285 sqlite3_release_memory | |
| 286 } -sqlbody { | |
| 287 COMMIT; | |
| 288 } | |
| 289 } | |
| 290 | |
| 291 ifcapable pager_pragmas&&autovacuum { | |
| 292 do_ioerr_test ioerr-12 -ckrefcount true -erc 1 -sqlprep { | |
| 293 PRAGMA page_size = 512; | |
| 294 PRAGMA auto_vacuum = incremental; | |
| 295 CREATE TABLE t1(x); | |
| 296 INSERT INTO t1 VALUES( randomblob(1 * (512-4)) ); | |
| 297 INSERT INTO t1 VALUES( randomblob(110 * (512-4)) ); | |
| 298 INSERT INTO t1 VALUES( randomblob(2 * (512-4)) ); | |
| 299 INSERT INTO t1 VALUES( randomblob(110 * (512-4)) ); | |
| 300 INSERT INTO t1 VALUES( randomblob(3 * (512-4)) ); | |
| 301 DELETE FROM t1 WHERE rowid = 3; | |
| 302 PRAGMA incremental_vacuum = 2; | |
| 303 DELETE FROM t1 WHERE rowid = 1; | |
| 304 } -sqlbody { | |
| 305 PRAGMA incremental_vacuum = 1; | |
| 306 } | |
| 307 } | |
| 308 | |
| 309 # Usually, after a new page is allocated from the end of the file, it does | |
| 310 # not need to be written to the journal. The exception is when the new page | |
| 311 # shares its sector with an existing page that does need to be journalled. | |
| 312 # This test case provokes this condition to test for the sake of coverage | |
| 313 # that an IO error while journalling the coresident page is handled correctly. | |
| 314 # | |
| 315 sqlite3_simulate_device -char {} -sectorsize 2048 | |
| 316 do_ioerr_test ioerr-12 -ckrefcount true -erc 1 -tclprep { | |
| 317 db close | |
| 318 sqlite3 db test.db -vfs devsym | |
| 319 | |
| 320 # Create a test database. Page 2 is the root page of table t1. The only | |
| 321 # row inserted into t1 has an overflow page - page 3. Page 3 will be | |
| 322 # coresident on the 2048 byte sector with the next page to be allocated. | |
| 323 # | |
| 324 db eval { PRAGMA page_size = 1024 } | |
| 325 db eval { CREATE TABLE t1(x) } | |
| 326 db eval { INSERT INTO t1 VALUES(randomblob(1100)); } | |
| 327 } -tclbody { | |
| 328 db eval { INSERT INTO t1 VALUES(randomblob(2000)); } | |
| 329 } | |
| 330 sqlite3_simulate_device -char {} -sectorsize 0 | |
| 331 catch {db close} | |
| 332 | |
| 333 do_ioerr_test ioerr-13 -ckrefcount true -erc 1 -sqlprep { | |
| 334 PRAGMA auto_vacuum = incremental; | |
| 335 CREATE TABLE t1(x); | |
| 336 CREATE TABLE t2(x); | |
| 337 INSERT INTO t2 VALUES(randomblob(1500)); | |
| 338 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 339 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 340 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 341 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 342 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 343 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 344 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 345 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 346 INSERT INTO t1 VALUES(randomblob(20)); | |
| 347 INSERT INTO t1 SELECT x FROM t1; | |
| 348 INSERT INTO t1 SELECT x FROM t1; | |
| 349 INSERT INTO t1 SELECT x FROM t1; | |
| 350 INSERT INTO t1 SELECT x FROM t1; | |
| 351 INSERT INTO t1 SELECT x FROM t1; | |
| 352 INSERT INTO t1 SELECT x FROM t1; /* 64 entries in t1 */ | |
| 353 INSERT INTO t1 SELECT x FROM t1 LIMIT 14; /* 78 entries in t1 */ | |
| 354 DELETE FROM t2 WHERE rowid = 3; | |
| 355 } -sqlbody { | |
| 356 -- This statement uses the balance_quick() optimization. The new page | |
| 357 -- is appended to the database file. But the overflow page used by | |
| 358 -- the new record will be positioned near the start of the database | |
| 359 -- file, in the gap left by the "DELETE FROM t2 WHERE rowid=3" statement | |
| 360 -- above. | |
| 361 -- | |
| 362 -- The point of this is that the statement wil need to update two pointer | |
| 363 -- map pages. Which introduces another opportunity for an IO error. | |
| 364 -- | |
| 365 INSERT INTO t1 VALUES(randomblob(2000)); | |
| 366 } | |
| 367 | |
| 368 do_ioerr_test ioerr-14 -ckrefcount true -erc 1 -sqlprep { | |
| 369 PRAGMA auto_vacuum = incremental; | |
| 370 CREATE TABLE t1(x); | |
| 371 CREATE TABLE t2(x); | |
| 372 INSERT INTO t2 VALUES(randomblob(1500)); | |
| 373 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 374 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 375 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 376 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 377 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 378 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 379 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 380 INSERT INTO t2 SELECT randomblob(1500) FROM t2; | |
| 381 | |
| 382 -- This statement inserts a row into t1 with an overflow page at the | |
| 383 -- end of the file. A long way from its parent (the root of t1). | |
| 384 INSERT INTO t1 VALUES(randomblob(1500)); | |
| 385 DELETE FROM t2 WHERE rowid<10; | |
| 386 } -sqlbody { | |
| 387 -- This transaction will cause the root-page of table t1 to divide | |
| 388 -- (by calling balance_deeper()). When it does, the "parent" page of the | |
| 389 -- overflow page inserted in the -sqlprep block above will change and | |
| 390 -- the corresponding pointer map page be updated. This test case attempts | |
| 391 -- to cause an IO error during the pointer map page update. | |
| 392 -- | |
| 393 BEGIN; | |
| 394 INSERT INTO t1 VALUES(randomblob(100)); | |
| 395 INSERT INTO t1 VALUES(randomblob(100)); | |
| 396 INSERT INTO t1 VALUES(randomblob(100)); | |
| 397 INSERT INTO t1 VALUES(randomblob(100)); | |
| 398 INSERT INTO t1 VALUES(randomblob(100)); | |
| 399 INSERT INTO t1 VALUES(randomblob(100)); | |
| 400 INSERT INTO t1 VALUES(randomblob(100)); | |
| 401 INSERT INTO t1 VALUES(randomblob(100)); | |
| 402 INSERT INTO t1 VALUES(randomblob(100)); | |
| 403 INSERT INTO t1 VALUES(randomblob(100)); | |
| 404 COMMIT; | |
| 405 } | |
| 406 | |
| 407 do_ioerr_test ioerr-15 -tclprep { | |
| 408 db eval { | |
| 409 BEGIN; | |
| 410 PRAGMA cache_size = 10; | |
| 411 CREATE TABLE t1(a); | |
| 412 CREATE INDEX i1 ON t1(a); | |
| 413 CREATE TABLE t2(a); | |
| 414 } | |
| 415 for {set ii 1} {$ii < 100} {incr ii} { | |
| 416 set v [string range [string repeat [format %.3d $ii] 200] 0 220] | |
| 417 db eval {INSERT INTO t1 VALUES($v)} | |
| 418 } | |
| 419 db eval { | |
| 420 DELETE FROM t1 WHERE oid > 85; | |
| 421 COMMIT; | |
| 422 } | |
| 423 } -sqlbody { | |
| 424 BEGIN; | |
| 425 INSERT INTO t2 VALUES(randstr(22000,22000)); | |
| 426 DELETE FROM t1 WHERE oid = 83; | |
| 427 COMMIT; | |
| 428 } | |
| 429 | |
| 430 # This test verifies that IO errors that occur within the obscure branch | |
| 431 # of code executed by tkt3762.test are correctly reported. | |
| 432 # | |
| 433 ifcapable vacuum&&autovacuum&&pragma { | |
| 434 do_ioerr_test ioerr-16 -erc 1 -ckrefcount 1 -sqlprep { | |
| 435 PRAGMA auto_vacuum=INCREMENTAL; | |
| 436 PRAGMA page_size=1024; | |
| 437 BEGIN; | |
| 438 CREATE TABLE t1(x); | |
| 439 INSERT INTO t1 VALUES(zeroblob(900)); | |
| 440 INSERT INTO t1 VALUES(zeroblob(900)); | |
| 441 INSERT INTO t1 SELECT x FROM t1; | |
| 442 INSERT INTO t1 SELECT x FROM t1; | |
| 443 INSERT INTO t1 SELECT x FROM t1; | |
| 444 INSERT INTO t1 SELECT x FROM t1; | |
| 445 INSERT INTO t1 SELECT x FROM t1; | |
| 446 INSERT INTO t1 SELECT x FROM t1; | |
| 447 INSERT INTO t1 SELECT x FROM t1; | |
| 448 DELETE FROM t1 WHERE rowid>202; | |
| 449 COMMIT; | |
| 450 VACUUM; | |
| 451 PRAGMA cache_size = 10; | |
| 452 BEGIN; | |
| 453 DELETE FROM t1 WHERE rowid IN (10,11,12) ; | |
| 454 } -sqlbody { | |
| 455 PRAGMA incremental_vacuum(10); | |
| 456 COMMIT; | |
| 457 } | |
| 458 } | |
| 459 | |
| 460 finish_test | |
| OLD | NEW |