tests/harness/testsuite.cc

Go to the documentation of this file.
00001 /* testsuite.cc: a test suite engine
00002  *
00003  * Copyright 1999,2000,2001 BrightStation PLC
00004  * Copyright 2002 Ananova Ltd
00005  * Copyright 2002,2003,2004,2005,2006,2007,2008,2009 Olly Betts
00006  *
00007  * This program is free software; you can redistribute it and/or
00008  * modify it under the terms of the GNU General Public License as
00009  * published by the Free Software Foundation; either version 2 of the
00010  * License, or (at your option) any later version.
00011  *
00012  * This program is distributed in the hope that it will be useful,
00013  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00014  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00015  * GNU General Public License for more details.
00016  *
00017  * You should have received a copy of the GNU General Public License
00018  * along with this program; if not, write to the Free Software
00019  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
00020  * USA
00021  */
00022 
00023 #include <config.h>
00024 
00025 #include "testsuite.h"
00026 
00027 #ifdef HAVE_VALGRIND
00028 # include "safeerrno.h"
00029 # include <valgrind/memcheck.h>
00030 # include <stdio.h>
00031 # include <sys/types.h>
00032 # include "safefcntl.h"
00033 # include "safeunistd.h"
00034 #endif
00035 
00036 #include <algorithm>
00037 #include <iostream>
00038 
00039 #ifdef HAVE_STREAMBUF
00040 #include <streambuf>
00041 #else // HAVE_STREAMBUF
00042 #include <streambuf.h>
00043 #endif // HAVE_STREAMBUF
00044 
00045 #include <set>
00046 
00047 #include <float.h> // For DBL_DIG.
00048 #include <math.h> // For ceil, fabs, log10.
00049 #include <stdlib.h>
00050 #include <string.h>
00051 
00052 #include "gnu_getopt.h"
00053 
00054 #include <setjmp.h>
00055 #include <signal.h>
00056 
00057 #include <exception>
00058 #ifdef USE_RTTI
00059 # include <typeinfo>
00060 # if defined __GNUC__ && (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1))
00061 #  include <cxxabi.h>
00062 # endif
00063 #endif
00064 
00065 #include <xapian/error.h>
00066 #include "noreturn.h"
00067 #include "omdebug.h"
00068 #include "stringutils.h"
00069 #include "utils.h"
00070 
00071 using namespace std;
00072 
00074 bool verbose;
00075 
00076 #ifdef HAVE_VALGRIND
00077 static int vg_log_fd = -1;
00078 #endif
00079 
00081 //  We use this to attempt to diagnose when the code fails to catch an
00082 //  exception when it should (due to a compiler or runtime fault in
00083 //  GCC 2.95 it seems)
00084 const char * expected_exception = NULL;
00085 
00087 std::ostringstream tout;
00088 
00089 int test_driver::runs = 0;
00090 test_driver::result test_driver::subtotal;
00091 test_driver::result test_driver::total;
00092 string test_driver::argv0;
00093 string test_driver::opt_help;
00094 map<int, string *> test_driver::short_opts;
00095 vector<string> test_driver::test_names;
00096 bool test_driver::abort_on_error = false;
00097 string test_driver::col_red, test_driver::col_green;
00098 string test_driver::col_yellow, test_driver::col_reset;
00099 bool test_driver::use_cr = false;
00100 
00101 void
00102 test_driver::write_and_clear_tout()
00103 {
00104     const string & s = tout.str();
00105     if (!s.empty()) {
00106         out << '\n' << s;
00107         tout.str(string());
00108     }
00109 }
00110 
00111 string
00112 test_driver::get_srcdir()
00113 {
00114     char *p = getenv("srcdir");
00115     if (p != NULL) return string(p);
00116 
00117 #ifdef __WIN32__
00118     // The path on argv[0] will always use \ for the directory separator.
00119     const char ARGV0_SEP = '\\';
00120 #else
00121     const char ARGV0_SEP = '/';
00122 #endif
00123     // Default srcdir to the pathname of argv[0].
00124     string srcdir(argv0);
00125     string::size_type i = srcdir.find_last_of(ARGV0_SEP);
00126     string srcfile;
00127     if (i != string::npos) {
00128         srcfile = srcdir.substr(i + 1);
00129         srcdir.erase(i);
00130         // libtool may put the real executable in .libs.
00131         i = srcdir.find_last_of(ARGV0_SEP);
00132         if (srcdir.substr(i + 1) == ".libs") {
00133             srcdir.erase(i);
00134             // And it may have an "lt-" prefix.
00135             if (startswith(srcfile, "lt-")) srcfile.erase(0, 3);
00136         }
00137     } else {
00138         // No path of argv[0], so default srcdir to the current directory.
00139         // This may not work if libtool is involved as the true executable is
00140         // sometimes in ".libs".
00141         srcfile = srcdir;
00142         srcdir = ".";
00143     }
00144 
00145     // Remove any trailing ".exe" suffix, since some platforms add this.
00146     if (endswith(srcfile, ".exe")) srcfile.resize(srcfile.size() - 4);
00147 
00148     // Sanity check.
00149     if (!file_exists(srcdir + '/' + srcfile + ".cc")) {
00150         cout << argv0
00151              << ": srcdir is not in the environment and I can't guess it!\n"
00152                 "Run test programs using the runtest script - see HACKING for details"
00153              << endl;
00154         exit(1);
00155     }
00156     return srcdir;
00157 }
00158 
00159 test_driver::test_driver(const test_desc *tests_)
00160         : out(cout.rdbuf()), tests(tests_)
00161 {
00162 }
00163 
00164 static jmp_buf jb;
00165 static int signum = 0;
00166 
00167 /* Needs C linkage so we can pass it to signal() without problems. */
00168 extern "C" {
00169 
00170 XAPIAN_NORETURN(static void handle_sig(int signum_));
00171 static void handle_sig(int signum_)
00172 {
00173     signal(SIGSEGV, SIG_DFL);
00174     signal(SIGFPE, SIG_DFL);
00175     signal(SIGILL, SIG_DFL);
00176 #ifdef SIGBUS
00177     signal(SIGBUS, SIG_DFL);
00178 #endif
00179 #ifdef SIGSTKFLT
00180     signal(SIGSTKFLT, SIG_DFL);
00181 #endif
00182     signum = signum_;
00183     longjmp(jb, 1);
00184 }
00185 
00186 }
00187 
00188 class SignalRedirector {
00189   private:
00190     bool active;
00191   public:
00192     SignalRedirector() : active(false) { }
00193     void activate() {
00194         active = true;
00195         signal(SIGSEGV, handle_sig);
00196         signal(SIGFPE, handle_sig);
00197         signal(SIGILL, handle_sig);
00198 #ifdef SIGBUS
00199         signal(SIGBUS, handle_sig);
00200 #endif
00201 #ifdef SIGSTKFLT
00202         signal(SIGSTKFLT, handle_sig);
00203 #endif
00204     }
00205     ~SignalRedirector() {
00206         if (active) {
00207             signal(SIGSEGV, SIG_DFL);
00208             signal(SIGFPE, SIG_DFL);
00209             signal(SIGILL, SIG_DFL);
00210 #ifdef SIGBUS
00211             signal(SIGBUS, SIG_DFL);
00212 #endif
00213 #ifdef SIGSTKFLT
00214             signal(SIGSTKFLT, SIG_DFL);
00215 #endif
00216         }
00217     }
00218 };
00219 
00220 //  A wrapper around the tests to trap exceptions,
00221 //  and avoid having to catch them in every test function.
00222 //  If this test driver is used for anything other than
00223 //  Xapian tests, then this ought to be provided by
00224 //  the client, really.
00225 //  return: test_driver::PASS, test_driver::FAIL, or test_driver::SKIP
00226 test_driver::test_result
00227 test_driver::runtest(const test_desc *test)
00228 {
00229 #ifdef HAVE_VALGRIND
00230     // This is used to make a note of how many times we've run the test
00231     volatile int runcount = 0;
00232 #endif
00233 
00234     while (true) {
00235         tout.str(string());
00236         SignalRedirector sig; // use object so signal handlers are reset
00237         if (!setjmp(jb)) {
00238             static bool catch_signals =
00239                 (getenv("XAPIAN_TESTSUITE_SIG_DFL") == NULL);
00240             if (catch_signals) sig.activate();
00241             try {
00242                 expected_exception = NULL;
00243 #ifdef HAVE_VALGRIND
00244                 int vg_errs = 0;
00245                 long vg_leaks = 0, vg_dubious = 0, vg_reachable = 0;
00246                 if (vg_log_fd != -1) {
00247                     VALGRIND_DO_LEAK_CHECK;
00248                     vg_errs = VALGRIND_COUNT_ERRORS;
00249                     long dummy;
00250                     VALGRIND_COUNT_LEAKS(vg_leaks, vg_dubious, vg_reachable, dummy);
00251                     // Skip past any unread log output.
00252                     lseek(vg_log_fd, 0, SEEK_END);
00253                 }
00254 #endif
00255                 if (!test->run()) {
00256                     out << col_red << " FAILED" << col_reset;
00257                     write_and_clear_tout();
00258                     return FAIL;
00259                 }
00260 #ifdef HAVE_VALGRIND
00261                 if (vg_log_fd != -1) {
00262                     // We must empty tout before asking valgrind to perform its
00263                     // leak checks, otherwise the buffers holding the output
00264                     // may be identified as a memory leak (especially if >1K of
00265                     // output has been buffered it appears...)
00266                     tout.str(string());
00267 #define REPORT_FAIL_VG(M) do { \
00268     if (verbose) { \
00269         while (true) { \
00270             ssize_t c = read(vg_log_fd, buf, sizeof(buf)); \
00271             if (c == 0 || (c < 0 && errno != EINTR)) break; \
00272             if (c > 0) out << string(buf, c); \
00273         } \
00274     } \
00275     out << " " << col_red << M << col_reset; \
00276 } while (0)
00277                     // Record the current position so we can restore it so
00278                     // REPORT_FAIL_VG() gets the whole output.
00279                     off_t curpos = lseek(vg_log_fd, 0, SEEK_CUR);
00280                     char buf[1024];
00281                     while (true) {
00282                         ssize_t c = read(vg_log_fd, buf, sizeof(buf));
00283                         if (c == 0 || (c < 0 && errno != EINTR)) {
00284                             buf[0] = 0;
00285                             break;
00286                         }
00287                         if (c > 0) {
00288                             // Valgrind output has "==<pid>== \n" between
00289                             // report "records", so skip to the next occurrence
00290                             // of ' ' not followed by '\n'.
00291                             ssize_t i = 0;
00292                             do {
00293                                 const char * spc;
00294                                 spc = static_cast<const char *>(
00295                                         memchr(buf + i, ' ', c - i));
00296                                 if (!spc) {
00297                                     i = c;
00298                                     break;
00299                                 }
00300                                 i = spc - buf;
00301                             } while (++i < c && buf[i] == '\n');
00302 
00303                             char *start = buf + i;
00304                             c -= i;
00305                             if (c > 128) c = 128;
00306 
00307                             {
00308                                 const char *p;
00309                                 p = static_cast<const char*>(
00310                                         memchr(start, '\n', c));
00311                                 if (p != NULL) c = p - start;
00312                             }
00313 
00314                             memmove(buf, start, c);
00315                             buf[c] = '\0';
00316                             break;
00317                         }
00318                     }
00319                     lseek(vg_log_fd, curpos, SEEK_SET);
00320 
00321                     int vg_errs2 = VALGRIND_COUNT_ERRORS;
00322                     vg_errs = vg_errs2 - vg_errs;
00323                     VALGRIND_DO_LEAK_CHECK;
00324                     long vg_leaks2 = 0, vg_dubious2 = 0, vg_reachable2 = 0;
00325                     long dummy;
00326                     VALGRIND_COUNT_LEAKS(vg_leaks2, vg_dubious2, vg_reachable2,
00327                                          dummy);
00328                     vg_leaks = vg_leaks2 - vg_leaks;
00329                     vg_dubious = vg_dubious2 - vg_dubious;
00330                     vg_reachable = vg_reachable2 - vg_reachable;
00331                     if (vg_errs) {
00332                         string fail_msg(buf);
00333                         if (fail_msg.empty())
00334                             fail_msg = "VALGRIND DETECTED A PROBLEM";
00335                         REPORT_FAIL_VG(fail_msg);
00336                         return FAIL;
00337                     }
00338                     if (vg_leaks > 0) {
00339                         REPORT_FAIL_VG("LEAKED " << vg_leaks << " BYTES");
00340                         return FAIL;
00341                     }
00342                     if (vg_dubious > 0) {
00343                         // If code deliberately holds onto blocks by a pointer
00344                         // not to the start (e.g. languages/utilities.c does)
00345                         // then we need to rerun the test to see if the leak is
00346                         // real...
00347                         if (runcount == 0) {
00348                             out << col_yellow << " PROBABLY LEAKED MEMORY - RETRYING TEST" << col_reset;
00349                             ++runcount;
00350                             continue;
00351                         }
00352                         REPORT_FAIL_VG("PROBABLY LEAKED " << vg_dubious << " BYTES");
00353                         return FAIL;
00354                     }
00355                     if (vg_reachable > 0) {
00356                         // C++ STL implementations often "horde" released
00357                         // memory - for GCC 3.4 and newer the runtest script
00358                         // sets GLIBCXX_FORCE_NEW=1 which will disable this
00359                         // behaviour so we avoid this issue, but for older
00360                         // GCC and other compilers this may be an issue.
00361                         //
00362                         // See also:
00363                         // http://valgrind.org/docs/FAQ/#faq.reports
00364                         //
00365                         // For now, just use runcount to rerun the test and see
00366                         // if more is leaked - hopefully this shouldn't give
00367                         // false positives.
00368                         if (runcount == 0) {
00369                             out << col_yellow << " POSSIBLE UNRELEASED MEMORY - RETRYING TEST" << col_reset;
00370                             ++runcount;
00371                             continue;
00372                         }
00373                         REPORT_FAIL_VG("FAILED TO RELEASE " << vg_reachable << " BYTES");
00374                         return FAIL;
00375                     }
00376                 }
00377 #endif
00378             } catch (const TestFail &) {
00379                 out << col_red << " FAILED" << col_reset;
00380                 write_and_clear_tout();
00381                 return FAIL;
00382             } catch (const TestSkip &) {
00383                 out << col_yellow << " SKIPPED" << col_reset;
00384                 write_and_clear_tout();
00385                 return SKIP;
00386             } catch (const Xapian::Error &err) {
00387                 string errclass = err.get_type();
00388                 if (expected_exception && expected_exception == errclass) {
00389                     out << col_yellow << " C++ FAILED TO CATCH " << errclass << col_reset;
00390                     return SKIP;
00391                 }
00392                 out << " " << col_red << err.get_description() << col_reset;
00393                 write_and_clear_tout();
00394                 return FAIL;
00395             } catch (const string & msg) {
00396                 out << col_red << " EXCEPTION std::string " << msg << col_reset;
00397                 write_and_clear_tout();
00398                 return FAIL;
00399             } catch (const std::exception & e) {
00400                 out << " " << col_red;
00401 #ifndef USE_RTTI
00402                 out << "std::exception";
00403 #else
00404                 const char * name = typeid(e).name();
00405 # if defined __GNUC__ && (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1))
00406                 // __cxa_demangle() apparently requires GCC >= 3.1.
00407                 // Demangle the name which GCC returns for type_info::name().
00408                 int status;
00409                 char * realname = abi::__cxa_demangle(name, NULL, 0, &status);
00410                 if (realname) {
00411                     out << realname;
00412                     free(realname);
00413                 } else {
00414                     out << name;
00415                 }
00416 # else
00417                 out << name;
00418 # endif
00419 #endif
00420                 out << ": " << e.what() << col_reset;
00421                 write_and_clear_tout();
00422                 return FAIL;
00423             } catch (...) {
00424                 out << col_red << " UNKNOWN EXCEPTION" << col_reset;
00425                 write_and_clear_tout();
00426                 return FAIL;
00427             }
00428             return PASS;
00429         }
00430 
00431         // Caught a signal.
00432         const char *signame = "SIGNAL";
00433         switch (signum) {
00434             case SIGSEGV: signame = "SIGSEGV"; break;
00435             case SIGFPE: signame = "SIGFPE"; break;
00436             case SIGILL: signame = "SIGILL"; break;
00437 #ifdef SIGBUS
00438             case SIGBUS: signame = "SIGBUS"; break;
00439 #endif
00440 #ifdef SIGSTKFLT
00441             case SIGSTKFLT: signame = "SIGSTKFLT"; break;
00442 #endif
00443         }
00444         out << " " << col_red << signame << col_reset;
00445         write_and_clear_tout();
00446         return FAIL;
00447     }
00448 }
00449 
00450 test_driver::result
00451 test_driver::run_tests(vector<string>::const_iterator b,
00452                        vector<string>::const_iterator e)
00453 {
00454     return do_run_tests(b, e);
00455 }
00456 
00457 test_driver::result
00458 test_driver::run_tests()
00459 {
00460     const vector<string> blank;
00461     return do_run_tests(blank.begin(), blank.end());
00462 }
00463 
00464 test_driver::result
00465 test_driver::do_run_tests(vector<string>::const_iterator b,
00466                           vector<string>::const_iterator e)
00467 {
00468     set<string> m(b, e);
00469     bool check_name = !m.empty();
00470 
00471     test_driver::result res;
00472 
00473     for (const test_desc *test = tests; test->name; test++) {
00474         bool do_this_test = !check_name;
00475         if (!do_this_test && m.find(test->name) != m.end())
00476             do_this_test = true;
00477         if (!do_this_test) {
00478             // if this test is "foo123" see if "foo" was listed
00479             // this way "./testprog foo" can run foo1, foo2, etc.
00480             string t = test->name;
00481             string::size_type i;
00482             i = t.find_last_not_of("0123456789") + 1;
00483             if (i != string::npos) {
00484                 t.resize(i);
00485                 if (m.find(t) != m.end()) do_this_test = true;
00486             }
00487         }
00488         if (do_this_test) {
00489             out << "Running test: " << test->name << "...";
00490             out.flush();
00491             switch (runtest(test)) {
00492                 case PASS:
00493                     ++res.succeeded;
00494                     if (verbose || !use_cr) {
00495                         out << col_green << " ok" << col_reset << endl;
00496                     } else {
00497                         out << "\r                                                                               \r";
00498                     }
00499                     break;
00500                 case FAIL:
00501                     ++res.failed;
00502                     out << endl;
00503                     if (abort_on_error) {
00504                         out << "Test failed - aborting further tests." << endl;
00505                         return res;
00506                     }
00507                     break;
00508                 case SKIP:
00509                     ++res.skipped;
00510                     out << endl;
00511                     // ignore the result of this test.
00512                     break;
00513             }
00514         }
00515     }
00516     return res;
00517 }
00518 
00519 void
00520 test_driver::usage()
00521 {
00522     cout << "Usage: " << argv0 << " [-v|--verbose] [-o|--abort-on-error] " << opt_help
00523          << "[TESTNAME]..." << endl;
00524     cout << "       " << argv0 << " [-h|--help]" << endl;
00525     exit(1);
00526 }
00527 
00528 /* Needs C linkage so we can pass it to atexit() without problems. */
00529 extern "C" {
00530 // Call upon program exit if there's more than one test run.
00531 static void
00532 report_totals(void)
00533 {
00534     test_driver::report(test_driver::total, "total");
00535 }
00536 }
00537 
00538 void
00539 test_driver::report(const test_driver::result &r, const string &desc)
00540 {
00541     // Report totals at the end if we reported two or more subtotals.
00542     if (++runs == 2) atexit(report_totals);
00543 
00544     if (r.succeeded != 0 || r.failed != 0) {
00545         cout << argv0 << " " << desc << ": ";
00546 
00547         if (r.failed == 0)
00548             cout << "All ";
00549 
00550         cout << col_green << r.succeeded << col_reset << " tests passed";
00551 
00552         if (r.failed != 0)
00553             cout << ", " << col_red << r.failed << col_reset << " failed";
00554 
00555         if (r.skipped) {
00556             cout << ", " << col_yellow << r.skipped << col_reset
00557                  << " skipped." << endl;
00558         } else {
00559             cout << "." << endl;
00560         }
00561     }
00562 }
00563 
00564 void
00565 test_driver::add_command_line_option(const string &l, char s, string * arg)
00566 {
00567     short_opts.insert(make_pair<int, string *>(int(s), arg));
00568     opt_help += "[-";
00569     opt_help += s;
00570     opt_help += ' ';
00571     opt_help += l;
00572     opt_help += "] ";
00573 }
00574 
00575 void
00576 test_driver::parse_command_line(int argc, char **argv)
00577 {
00578     argv0 = argv[0];
00579 
00580 #ifndef __WIN32__
00581     bool colourise = true;
00582     const char *p = getenv("XAPIAN_TESTSUITE_OUTPUT");
00583     if (p == NULL || !*p || strcmp(p, "auto") == 0) {
00584         colourise = isatty(1);
00585     } else if (strcmp(p, "plain") == 0) {
00586         colourise = false;
00587     }
00588     if (colourise) {
00589         col_red = "\x1b[1m\x1b[31m";
00590         col_green = "\x1b[1m\x1b[32m";
00591         col_yellow = "\x1b[1m\x1b[33m";
00592         col_reset = "\x1b[0m";
00593         use_cr = true;
00594     }
00595 #endif
00596 
00597     const struct option long_opts[] = {
00598         {"verbose",             no_argument, 0, 'v'},
00599         {"abort-on-error",      no_argument, 0, 'o'},
00600         {"help",                no_argument, 0, 'h'},
00601         {NULL,                  0, 0, 0}
00602     };
00603 
00604     string short_opts_string = "voh";
00605     map<int, string *>::const_iterator i;
00606     for (i = short_opts.begin(); i != short_opts.end(); ++i) {
00607         short_opts_string += char(i->first);
00608         short_opts_string += ':';
00609     }
00610     const char * opts = short_opts_string.c_str();
00611 
00612     int c;
00613     while ((c = gnu_getopt_long(argc, argv, opts, long_opts, 0)) != -1) {
00614         switch (c) {
00615             case 'v':
00616                 verbose = true;
00617                 break;
00618             case 'o':
00619                 abort_on_error = true;
00620                 break;
00621             default: {
00622                 i = short_opts.find(c);
00623                 if (i != short_opts.end()) {
00624                     i->second->assign(optarg);
00625                     break;
00626                 }
00627                 // -h or unrecognised option
00628                 usage();
00629                 return; // usage() doesn't return ...
00630             }
00631         }
00632     }
00633 
00634     while (argv[optind]) {
00635         test_names.push_back(string(argv[optind]));
00636         optind++;
00637     }
00638 
00639 #ifdef HAVE_VALGRIND
00640     if (RUNNING_ON_VALGRIND) {
00641         if (getenv("XAPIAN_TESTSUITE_VALGRIND") != NULL) {
00642             // Open the valgrind log file, and unlink it.
00643             char fname[64];
00644             sprintf(fname, ".valgrind.log.%lu", (unsigned long)getpid());
00645             vg_log_fd = open(fname, O_RDONLY|O_NONBLOCK);
00646             if (vg_log_fd == -1 && errno == ENOENT) {
00647                 // Older valgrind versions named the log output differently.
00648                 sprintf(fname, ".valgrind.log.pid%lu", (unsigned long)getpid());
00649                 vg_log_fd = open(fname, O_RDONLY|O_NONBLOCK);
00650             }
00651             if (vg_log_fd != -1) unlink(fname);
00652         }
00653     }
00654 #endif
00655 
00656 #ifdef XAPIAN_DEBUG_VERBOSE
00657     // We need to display something before we start, or the allocation
00658     // made when the first debug message is displayed is (wrongly) picked
00659     // up on as a memory leak.
00660     DEBUGLINE(UNKNOWN, "Starting testsuite run.");
00661     om_debug.initialise();
00662 #endif /* XAPIAN_DEBUG_VERBOSE */
00663 }
00664 
00665 int
00666 test_driver::run(const test_desc *tests)
00667 {
00668     test_driver driver(tests);
00669 
00670     test_driver::result myresult;
00671     myresult = driver.run_tests(test_names.begin(), test_names.end());
00672 
00673     subtotal += myresult;
00674 
00675     return bool(myresult.failed); // if 0, then everything passed
00676 }
00677 
00678 bool
00679 TEST_EQUAL_DOUBLE_(double a, double b)
00680 {
00681     if (a == b) return true;
00682     return (ceil(log10(max(fabs(a), fabs(b)))) - log10(fabs(a - b)) > DBL_DIG);
00683 }

Documentation for Xapian (version 1.0.20).
Generated on 28 Apr 2010 by Doxygen 1.5.2.