SQLite中的Tcl测试脚本的一个bug

1.错误提示

       在测试Pager模块时,把日志输出的宏PAGERTRACE打开时,发现再运行pager1.test时,出现如下错误

E:\devc++\sqlite3\tclsqlite3\Debug\tclsqlite3.exe:expected boolean value but got "OPEN"

   while executing

"if { [lindex $r 0] } { error$res }"

   (procedure "testfixture" line 15)

   invoked from within

"testfixture $::code2_chan $tcl"

   (procedure "code2" line 1)

   invoked from within

"code2 { sqlite3 db2 test.db}"

   (procedure "do_multiclient_test" line 25)

   invoked from within

"do_multiclient_test tn {

 

 # Create and populate a database table using connection [db]. Check

 # that connections [db2] and [db3] can see the sche..."

(file"pager1.test" line 82)

       提示的意思是说在过程do_multiclient_test中执行code2 { sqlite3 db2 test.db }语句时,又调用了testfixture $::code2_chan$tcl过程,在执行到if { [lindex $r 0] } { error $res }时出错,本来得到的lindex $r 0应该是一个布尔变量,但是现在却得到“open”所以返回错误。

2.代码分析

       首先来看do_multiclient_test过程的实现,该过程是想实现一个多进程的测试用例。

proc do_multiclient_test {varname script} {
  foreach code [list {
    if {[info exists ::G(valgrind)]} { db close ; continue }
    set ::code2_chan [launch_testfixture]
    set ::code3_chan [launch_testfixture]
    proc code2 {tcl} { testfixture $::code2_chan $tcl }
    proc code3 {tcl} { testfixture $::code3_chan $tcl }
    set tn 1
  } {
    proc code2 {tcl} { uplevel #0 $tcl }
    proc code3 {tcl} { uplevel #0 $tcl }
    set tn 2
  }] {
    faultsim_delete_and_reopen

    proc code1 {tcl} { uplevel #0 $tcl }
  
    eval $code
    code2 { sqlite3 db2 test.db }
code3 { sqlite3 db3 test.db }
……
}
}

       code定义了一组代码列表,对于每一个code元素,将eval $code等代码执行一遍,第一次执行的是以下这组代码

    if {[info exists ::G(valgrind)]} { db close ; continue }
    set ::code2_chan [launch_testfixture]
    set ::code3_chan [launch_testfixture]
    proc code2 {tcl} { testfixture $::code2_chan $tcl }
    proc code3 {tcl} { testfixture $::code3_chan $tcl }
    set tn 1

       所以code2 { sqlite3 db2 test.db }实际执行的代码是

testfixture $::code2_chan { sqlite3 db2 test.db }

       $::code2_chan变量是执行完launch_testfixture过程后得到的返回值,我们再来看launch_testfixture的代码

proc launch_testfixture {{prg ""}} {
  write_main_loop
  if {$prg eq ""} { set prg [info nameofexec] }
  if {$prg eq ""} { set prg testfixture }
  if {[file tail $prg]==$prg} { set prg [file join . $prg] }
  set chan [open "|$prg tf_main.tcl" r+]
  fconfigure $chan -buffering line
  set rc [catch { 
    testfixture $chan "sqlite3_test_control_pending_byte $::sqlite_pending_byte"
  }]
  if {$rc} {
    testfixture $chan "set ::sqlite_pending_byte $::sqlite_pending_byte"
  }
  return $chan
}

       以上代码中write_main_loop创建了一个tf_main.tcl脚本文件,set chan [open "|$prg tf_main.tcl" r+]这行代码打开了一个管道,该管道是子进程运行tf_main.tc脚本,在我的电脑上open "|$prg tf_main.tcl"展开时就是E:/devc++/sqlite3/tclsqlite3/Debug/tclsqlite3.exe tf_main.tcl,这个过程最后返回一个管道$chan,接着code2过程干的事情就是把管道$chan和sqlite3 db2 test.db传给testfixture执行,那么到底做了什么呢,我们来看代码

proc testfixture {chan cmd args} {
  if {[llength $args] == 0} {
    fconfigure $chan -blocking 1
    puts $chan $cmd
    puts $chan OVER

    set r ""
    while { 1 } {
      set line [gets $chan]
      if { $line == "OVER" } { 
        set res [lindex $r 1]
        if { [lindex $r 0] } { error $res }
        return $res
      }
      if {[eof $chan]} {
        return "ERROR: Child process hung up"
      }
      append r $line  
    }
    return $r
  } else {
    …….
  }
}

       通过以下这2行代码通过管道把sqlite3 db2 test.db交给子进程处理

   puts $chan $cmd
   puts $chan OVER

       子进程执行完后再把返回值发回给主进程,通过gets $chan来接收,每接收完一行代码就通过append r $line语句把接收到的字符串追加到r的结尾,为了找到问题的原因,把从管道里接收的数据line和r打印出来

puts "line $line"
puts "r $r"

       前2次执行launch_testfixture过程时没有出错,而执行sqlite3 db2 test.db时管道发回来的数据如下

line OPEN 18086624 E:\devc++\tcl\tt\test\testdir\test.db
line 0 {}
line OVER
r OPEN 18086624 E:\devc++\tcl\tt\test\testdir\test.db0 {}

       而在SQLite的源代码中没加日志打印信息时并未出错,正确的执行结果应该是

line 0 {}
line OVER
r 0 {}

       接下来再看看子进程中的代码,在tf_main.tcl中:

    set script ""
    while {![eof stdin]} {
      flush stdout
      set line [gets stdin]
      if { $line == "OVER" } {
        set rc [catch {eval $script} result]
        puts [list $rc $result]
        puts OVER
        flush stdout
        set script ""
      } else {
        append script $line
        append script "\n"
      }
}

       在这里我们看到新打开的管道进程是从stdin里接收数据(即脚本),接收完后再去执行脚本set rc [catch {eval $script} result],执行完后再把执行结果发回到主进程puts [list $rc $result],使用的是puts命令,如果没有指明管道,那么默认的是往stdout发数据。而在执行sqlite3 db2 test.db时,SQLite源码里执行sqlite3PagerOpen()函数,这里面有一句打印日志的代码

 PAGERTRACE(("OPEN %d %s\n", FILEHANDLEID(pPager->fd),pPager->zFilename));

       这句代码也向stdout输出打印信息,最后被主进程收到了

OPEN 18086624 E:\devc++\tcl\tt\test\testdir\test.db

       也就是说主进程本来想收到的是子进程执行脚本后的返回结果,却收到了源代码执行时的打印信息,这就是问题的根源。

3.问题解决

       知道了问题的根源,解决起来就很容易了,最彻底的方法就是主进程和子进程定一个稍微复杂点的通信协议。

       除了定新的协议,最简单的解决问题的方法是在testfixture过程中,把append r $line该为set r $line


猜你喜欢

转载自blog.csdn.net/pfysw/article/details/80366608