| 1 | /* |
| 2 | ** Copyright (c) 2008 D. Richard Hipp |
| 3 | ** |
| 4 | ** This program is free software; you can redistribute it and/or |
| 5 | ** modify it under the terms of the GNU General Public |
| 6 | ** License version 2 as published by the Free Software Foundation. |
| 7 | ** |
| 8 | ** This program is distributed in the hope that it will be useful, |
| 9 | ** but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 | ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 11 | ** General Public License for more details. |
| 12 | ** |
| 13 | ** You should have received a copy of the GNU General Public |
| 14 | ** License along with this library; if not, write to the |
| 15 | ** Free Software Foundation, Inc., 59 Temple Place - Suite 330, |
| 16 | ** Boston, MA 02111-1307, USA. |
| 17 | ** |
| 18 | ** Author contact information: |
| 19 | ** drh@hwaci.com |
| 20 | ** http://www.hwaci.com/drh/ |
| 21 | ** |
| 22 | ******************************************************************************* |
| 23 | ** |
| 24 | ** This main driver for the sqllogictest program. |
| 25 | */ |
| 26 | #include "catch.hpp" |
| 27 | #include "sqllogictest.hpp" |
| 28 | |
| 29 | #include <ctype.h> |
| 30 | #include <stdio.h> |
| 31 | #include <stdlib.h> |
| 32 | #ifndef _WIN32 |
| 33 | #include <unistd.h> |
| 34 | #define stricmp strcasecmp |
| 35 | #endif |
| 36 | #include "slt_duckdb.hpp" |
| 37 | |
| 38 | #include <algorithm> |
| 39 | #include <dirent.h> |
| 40 | #include <functional> |
| 41 | #include <string.h> |
| 42 | #include <string> |
| 43 | #include <vector> |
| 44 | |
| 45 | using namespace std; |
| 46 | |
| 47 | #define DEFAULT_HASH_THRESHOLD 8 |
| 48 | |
| 49 | /* |
| 50 | ** A structure to keep track of the state of scanning the input script. |
| 51 | */ |
| 52 | typedef struct Script Script; |
| 53 | struct Script { |
| 54 | char *zScript; /* Complete text of the input script */ |
| 55 | int iCur; /* Index in zScript of start of current line */ |
| 56 | char *zLine; /* Pointer to start of current line */ |
| 57 | int len; /* Length of current line */ |
| 58 | int iNext; /* index of start of next line */ |
| 59 | int nLine; /* line number for the current line */ |
| 60 | int iEnd; /* Index in zScript of '\000' at end of script */ |
| 61 | int startLine; /* Line number of start of current record */ |
| 62 | int copyFlag; /* If true, copy lines to output as they are read */ |
| 63 | char azToken[4][200]; /* tokenization of a line */ |
| 64 | }; |
| 65 | |
| 66 | // stub because not used |
| 67 | void sqllogictestRegisterEngine(const DbEngine *p) { |
| 68 | } |
| 69 | |
| 70 | /* |
| 71 | ** Advance the cursor to the start of the next non-comment line of the |
| 72 | ** script. Make p->zLine point to the start of the line. Make p->len |
| 73 | ** be the length of the line. Zero-terminate the line. Any \r at the |
| 74 | ** end of the line is removed. |
| 75 | ** |
| 76 | ** Return 1 on success. Return 0 and no-op at end-of-file. |
| 77 | */ |
| 78 | static int nextLine(Script *p) { |
| 79 | int i; |
| 80 | |
| 81 | /* Loop until a non-comment line is found, or until end-of-file */ |
| 82 | while (1) { |
| 83 | /* When we reach end-of-file, return 0 */ |
| 84 | if (p->iNext >= p->iEnd) { |
| 85 | p->iCur = p->iEnd; |
| 86 | p->zLine = &p->zScript[p->iEnd]; |
| 87 | p->len = 0; |
| 88 | return 0; |
| 89 | } |
| 90 | |
| 91 | /* Advance the cursor to the next line */ |
| 92 | p->iCur = p->iNext; |
| 93 | p->nLine++; |
| 94 | p->zLine = &p->zScript[p->iCur]; |
| 95 | for (i = p->iCur; i < p->iEnd && p->zScript[i] != '\n'; i++) { |
| 96 | } |
| 97 | p->zScript[i] = 0; |
| 98 | p->len = i - p->iCur; |
| 99 | p->iNext = i + 1; |
| 100 | |
| 101 | /* If the current line ends in a \r then remove the \r. */ |
| 102 | if (p->len > 0 && p->zScript[i - 1] == '\r') { |
| 103 | p->len--; |
| 104 | i--; |
| 105 | p->zScript[i] = 0; |
| 106 | } |
| 107 | |
| 108 | /* If the line consists of all spaces, make it an empty line */ |
| 109 | for (i = i - 1; i >= p->iCur && isspace(p->zScript[i]); i--) { |
| 110 | } |
| 111 | if (i < p->iCur) { |
| 112 | p->zLine[0] = 0; |
| 113 | } |
| 114 | |
| 115 | /* If the copy flag is set, write the line to standard output */ |
| 116 | if (p->copyFlag) { |
| 117 | printf("%s\n" , p->zLine); |
| 118 | } |
| 119 | |
| 120 | /* If the line is not a comment line, then we are finished, so break |
| 121 | ** out of the loop. If the line is a comment, the loop will repeat in |
| 122 | ** order to skip this line. */ |
| 123 | if (p->zLine[0] != '#') |
| 124 | break; |
| 125 | } |
| 126 | return 1; |
| 127 | } |
| 128 | |
| 129 | /* |
| 130 | ** Look ahead to the next line and return TRUE if it is a blank line. |
| 131 | ** But do not advance to the next line yet. |
| 132 | */ |
| 133 | static int nextIsBlank(Script *p) { |
| 134 | int i = p->iNext; |
| 135 | if (i >= p->iEnd) |
| 136 | return 1; |
| 137 | while (i < p->iEnd && isspace(p->zScript[i])) { |
| 138 | if (p->zScript[i] == '\n') |
| 139 | return 1; |
| 140 | i++; |
| 141 | } |
| 142 | return 0; |
| 143 | } |
| 144 | |
| 145 | /* |
| 146 | ** Advance the cursor to the start of the next record. To do this, |
| 147 | ** first skip over the tail section of the record in which we are |
| 148 | ** currently located, then skip over blank lines. |
| 149 | ** |
| 150 | ** Return 1 on success. Return 0 at end-of-file. |
| 151 | */ |
| 152 | static int findStartOfNextRecord(Script *p) { |
| 153 | /* Skip over any existing content to find a blank line */ |
| 154 | if (p->iCur > 0) { |
| 155 | while (p->zLine[0] && p->iCur < p->iEnd) { |
| 156 | nextLine(p); |
| 157 | } |
| 158 | } else { |
| 159 | nextLine(p); |
| 160 | } |
| 161 | |
| 162 | /* Skip over one or more blank lines to find the first line of the |
| 163 | ** new record */ |
| 164 | while (p->zLine[0] == 0 && p->iCur < p->iEnd) { |
| 165 | nextLine(p); |
| 166 | } |
| 167 | |
| 168 | /* Return 1 if we have not reached end of file. */ |
| 169 | return p->iCur < p->iEnd; |
| 170 | } |
| 171 | |
| 172 | /* |
| 173 | ** Find a single token in a string. Return the index of the start |
| 174 | ** of the token and the length of the token. |
| 175 | */ |
| 176 | static void findToken(const char *z, int *piStart, int *pLen) { |
| 177 | int i; |
| 178 | int iStart; |
| 179 | for (i = 0; isspace(z[i]); i++) { |
| 180 | } |
| 181 | *piStart = iStart = i; |
| 182 | while (z[i] && !isspace(z[i])) { |
| 183 | i++; |
| 184 | } |
| 185 | *pLen = i - iStart; |
| 186 | } |
| 187 | |
| 188 | #define count(X) (sizeof(X) / sizeof(X[0])) |
| 189 | |
| 190 | /* |
| 191 | ** tokenize the current line in up to 3 tokens and store those values |
| 192 | ** into p->azToken[0], p->azToken[1], and p->azToken[2]. Record the |
| 193 | ** current line in p->startLine. |
| 194 | */ |
| 195 | static void tokenizeLine(Script *p) { |
| 196 | int i, j, k; |
| 197 | int len, n; |
| 198 | for (i = 0; i < (int)count(p->azToken); i++) |
| 199 | p->azToken[i][0] = 0; |
| 200 | p->startLine = p->nLine; |
| 201 | for (i = j = 0; j < p->len && i < (int)count(p->azToken); i++) { |
| 202 | findToken(&p->zLine[j], &k, &len); |
| 203 | j += k; |
| 204 | n = len; |
| 205 | if (n >= (int)sizeof(p->azToken[0])) { |
| 206 | n = sizeof(p->azToken[0]) - 1; |
| 207 | } |
| 208 | memcpy(p->azToken[i], &p->zLine[j], n); |
| 209 | p->azToken[i][n] = 0; |
| 210 | j += n + 1; |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | /* |
| 215 | ** The number columns in a row of the current result set |
| 216 | */ |
| 217 | static int nColumn = 0; |
| 218 | |
| 219 | /* |
| 220 | ** Comparison function for sorting the result set. |
| 221 | */ |
| 222 | static int rowCompare(const void *pA, const void *pB) { |
| 223 | const char **azA = (const char **)pA; |
| 224 | const char **azB = (const char **)pB; |
| 225 | int c = 0, i; |
| 226 | for (i = 0; c == 0 && i < nColumn; i++) { |
| 227 | c = strcmp(azA[i], azB[i]); |
| 228 | } |
| 229 | return c; |
| 230 | } |
| 231 | |
| 232 | /* |
| 233 | ** Entry in a hash table of prior results |
| 234 | */ |
| 235 | typedef struct HashEntry HashEntry; |
| 236 | struct HashEntry { |
| 237 | char zKey[24]; /* The search key */ |
| 238 | char zHash[33]; /* The hash value stored */ |
| 239 | HashEntry *pNext; /* Next with same hash */ |
| 240 | HashEntry *pAll; /* Next overall */ |
| 241 | }; |
| 242 | |
| 243 | /* |
| 244 | ** The hash table |
| 245 | */ |
| 246 | #define NHASH 1009 |
| 247 | static HashEntry *aHash[NHASH]; |
| 248 | static HashEntry *pAll; |
| 249 | |
| 250 | /* |
| 251 | ** Try to look up the value zKey in the hash table. If the value |
| 252 | ** does not exist, create it and return 0. If the value does already |
| 253 | ** exist return 0 if hash matches and 1 if the hash is different. |
| 254 | */ |
| 255 | static int checkValue(const char *zKey, const char *zHash) { |
| 256 | unsigned int h; |
| 257 | HashEntry *p; |
| 258 | unsigned int i; |
| 259 | |
| 260 | h = 0; |
| 261 | for (i = 0; zKey[i] && i < sizeof(p->zKey); i++) { |
| 262 | h = h << 3 ^ h ^ zKey[i]; |
| 263 | } |
| 264 | h = h % NHASH; |
| 265 | for (p = aHash[h]; p; p = p->pNext) { |
| 266 | if (strcmp(p->zKey, zKey) == 0) { |
| 267 | return strcmp(p->zHash, zHash) != 0; |
| 268 | } |
| 269 | } |
| 270 | p = (HashEntry *)malloc(sizeof(*p)); |
| 271 | if (p == 0) { |
| 272 | fprintf(stderr, "out of memory at %s:%d\n" , __FILE__, __LINE__); |
| 273 | exit(1); |
| 274 | } |
| 275 | for (i = 0; zKey[i] && i < sizeof(p->zKey) - 1; i++) { |
| 276 | p->zKey[i] = zKey[i]; |
| 277 | } |
| 278 | p->zKey[i] = 0; |
| 279 | for (i = 0; zHash[i] && i < sizeof(p->zHash) - 1; i++) { |
| 280 | p->zHash[i] = zHash[i]; |
| 281 | } |
| 282 | p->zHash[i] = 0; |
| 283 | p->pAll = pAll; |
| 284 | pAll = p; |
| 285 | p->pNext = aHash[h]; |
| 286 | aHash[h] = p; |
| 287 | return 0; |
| 288 | } |
| 289 | |
| 290 | #define IFAIL() \ |
| 291 | { \ |
| 292 | if (zScript) \ |
| 293 | free(zScript); \ |
| 294 | if (pConn) \ |
| 295 | pEngine->xDisconnect(pConn); \ |
| 296 | REQUIRE(false); \ |
| 297 | return; \ |
| 298 | } |
| 299 | |
| 300 | static void execute_file(string script) { |
| 301 | int haltOnError = 0; /* Stop on first error if true */ |
| 302 | int enableTrace = 0; /* Trace SQL statements if true */ |
| 303 | const char *zScriptFile = 0; /* Input script filename */ |
| 304 | const char *zDbEngine = "DuckDB" ; /* Name of database engine */ |
| 305 | const char *zConnection = 0; /* Connection string on DB engine */ |
| 306 | const DbEngine *pEngine = 0; /* Pointer to DbEngine object */ |
| 307 | int i; /* Loop counter */ |
| 308 | char *zScript; /* Content of the script */ |
| 309 | long nScript; /* Size of the script in bytes */ |
| 310 | long nGot; /* Number of bytes read */ |
| 311 | void *pConn = nullptr; /* Connection to the database engine */ |
| 312 | int rc; /* Result code from subroutine call */ |
| 313 | int nErr = 0; /* Number of errors */ |
| 314 | int nCmd = 0; /* Number of SQL statements processed */ |
| 315 | int nSkipped = 0; /* Number of SQL statements skipped */ |
| 316 | int nResult; /* Number of query results */ |
| 317 | char **azResult; /* Query result vector */ |
| 318 | Script sScript; /* Script parsing status */ |
| 319 | FILE *in; /* For reading script */ |
| 320 | char zHash[100]; /* Storage space for hash results */ |
| 321 | int hashThreshold = DEFAULT_HASH_THRESHOLD; /* Threshold for hashing res */ |
| 322 | int bHt = 0; /* True if -ht command-line option */ |
| 323 | const char *zParam = 0; /* Argument to -parameters */ |
| 324 | |
| 325 | const DbEngine duckdbEngine = { |
| 326 | "DuckDB" , /* zName */ |
| 327 | 0, /* pAuxData */ |
| 328 | duckdbConnect, /* xConnect */ |
| 329 | duckdbGetEngineName, /* xGetEngineName */ |
| 330 | duckdbStatement, /* xStatement */ |
| 331 | duckdbQuery, /* xQuery */ |
| 332 | duckdbFreeResults, /* xFreeResults */ |
| 333 | duckdbDisconnect /* xDisconnect */ |
| 334 | }; |
| 335 | pEngine = &duckdbEngine; |
| 336 | |
| 337 | REQUIRE(pEngine); |
| 338 | /* |
| 339 | ** Read the entire script file contents into memory |
| 340 | */ |
| 341 | |
| 342 | zScriptFile = script.c_str(); |
| 343 | in = fopen(zScriptFile, "rb" ); |
| 344 | if (!in) { |
| 345 | FAIL("Could not find test script '" + script + "'. Perhaps run `make sqlite`. " ); |
| 346 | } |
| 347 | REQUIRE(in); |
| 348 | fseek(in, 0L, SEEK_END); |
| 349 | nScript = ftell(in); |
| 350 | REQUIRE(nScript > 0); |
| 351 | zScript = (char *)malloc(nScript + 1); |
| 352 | if (!zScript) { |
| 353 | IFAIL(); |
| 354 | } |
| 355 | fseek(in, 0L, SEEK_SET); |
| 356 | nGot = fread(zScript, 1, nScript, in); |
| 357 | fclose(in); |
| 358 | REQUIRE(nGot <= nScript); |
| 359 | zScript[nGot] = 0; |
| 360 | |
| 361 | // zap hash table as result labels are only valid within one test file |
| 362 | memset(aHash, 0, sizeof(aHash)); |
| 363 | |
| 364 | /* Initialize the sScript structure so that the cursor will be pointing |
| 365 | ** to the start of the first line in the file after nextLine() is called |
| 366 | ** once. */ |
| 367 | memset(&sScript, 0, sizeof(sScript)); |
| 368 | sScript.zScript = zScript; |
| 369 | sScript.zLine = zScript; |
| 370 | sScript.iEnd = nScript; |
| 371 | sScript.copyFlag = 0; |
| 372 | |
| 373 | /* Open the database engine under test |
| 374 | */ |
| 375 | rc = pEngine->xConnect(pEngine->pAuxData, zConnection, &pConn, zParam); |
| 376 | REQUIRE(rc == 0); |
| 377 | |
| 378 | /* Loop over all records in the file */ |
| 379 | while ((nErr == 0 || !haltOnError) && findStartOfNextRecord(&sScript)) { |
| 380 | int bSkip = false; /* True if we should skip the current record. */ |
| 381 | |
| 382 | /* Tokenizer the first line of the record. This also records the |
| 383 | ** line number of the first record in sScript.startLine */ |
| 384 | tokenizeLine(&sScript); |
| 385 | |
| 386 | bSkip = false; |
| 387 | while (strcmp(sScript.azToken[0], "skipif" ) == 0 || strcmp(sScript.azToken[0], "onlyif" ) == 0) { |
| 388 | int bMatch; |
| 389 | /* The "skipif" and "onlyif" modifiers allow skipping or using |
| 390 | ** statement or query record for a particular database engine. |
| 391 | ** In this way, SQL features implmented by a majority of the |
| 392 | ** engines can be tested without causing spurious errors for |
| 393 | ** engines that don't support it. |
| 394 | ** |
| 395 | ** Once this record is encountered, an the current selected |
| 396 | ** db interface matches the db engine specified in the record, |
| 397 | ** the we skip this rest of this record for "skipif" or for |
| 398 | ** "onlyif" we skip the record if the record does not match. |
| 399 | */ |
| 400 | bMatch = stricmp(sScript.azToken[1], zDbEngine) == 0; |
| 401 | if (sScript.azToken[0][0] == 's') { |
| 402 | if (bMatch) |
| 403 | bSkip = true; |
| 404 | } else { |
| 405 | if (!bMatch) |
| 406 | bSkip = true; |
| 407 | } |
| 408 | nextLine(&sScript); |
| 409 | tokenizeLine(&sScript); |
| 410 | } |
| 411 | if (bSkip) { |
| 412 | int n; |
| 413 | nSkipped++; |
| 414 | if (strcmp(sScript.azToken[0], "query" ) != 0) |
| 415 | continue; |
| 416 | if (sScript.azToken[3][0] == 0) |
| 417 | continue; |
| 418 | |
| 419 | /* We are skipping this record. But we observe that it is a |
| 420 | *query |
| 421 | ** with a named hash value and we are in verify mode. Even |
| 422 | *though |
| 423 | ** we are going to skip the SQL evaluation, we might as well |
| 424 | *check |
| 425 | ** the hash of the result. |
| 426 | */ |
| 427 | while (!nextIsBlank(&sScript) && nextLine(&sScript) && strcmp(sScript.zLine, "----" ) != 0) { |
| 428 | /* Skip over the SQL text */ |
| 429 | } |
| 430 | if (strcmp(sScript.zLine, "----" ) == 0) |
| 431 | nextLine(&sScript); |
| 432 | if (sScript.zLine[0] == 0) |
| 433 | continue; |
| 434 | n = sscanf(sScript.zLine, "%*d values hashing to %32s" , zHash); |
| 435 | if (n != 1) { |
| 436 | md5_add(sScript.zLine); |
| 437 | md5_add("\n" ); |
| 438 | while (!nextIsBlank(&sScript) && nextLine(&sScript)) { |
| 439 | md5_add(sScript.zLine); |
| 440 | md5_add("\n" ); |
| 441 | } |
| 442 | strcpy(zHash, md5_finish()); |
| 443 | } |
| 444 | if (checkValue(sScript.azToken[3], zHash)) { |
| 445 | fprintf(stderr, |
| 446 | "%s:%d: labeled result [%s] does not agree with " |
| 447 | "previous values\n" , |
| 448 | zScriptFile, sScript.startLine, sScript.azToken[3]); |
| 449 | IFAIL(); |
| 450 | } |
| 451 | continue; |
| 452 | } |
| 453 | |
| 454 | /* Figure out the record type and do appropriate processing */ |
| 455 | if (strcmp(sScript.azToken[0], "statement" ) == 0) { |
| 456 | int k = 0; |
| 457 | int bExpectOk = 0; |
| 458 | int bExpectError = 0; |
| 459 | |
| 460 | /* Extract the SQL from second and subsequent lines of the |
| 461 | ** record. Copy the SQL into contiguous memory at the beginning |
| 462 | ** of zScript - we are guaranteed to have enough space there. */ |
| 463 | while (nextLine(&sScript) && sScript.zLine[0]) { |
| 464 | if (k > 0) |
| 465 | zScript[k++] = '\n'; |
| 466 | memmove(&zScript[k], sScript.zLine, sScript.len); |
| 467 | k += sScript.len; |
| 468 | } |
| 469 | zScript[k] = 0; |
| 470 | |
| 471 | bExpectOk = strcmp(sScript.azToken[1], "ok" ) == 0; |
| 472 | bExpectError = strcmp(sScript.azToken[1], "error" ) == 0; |
| 473 | |
| 474 | /* Run the statement. Remember the results |
| 475 | ** If we're expecting an error, pass true to suppress |
| 476 | ** printing of any errors. |
| 477 | */ |
| 478 | if (enableTrace) |
| 479 | printf("%s;\n" , zScript); |
| 480 | rc = pEngine->xStatement(pConn, zScript, bExpectError); |
| 481 | nCmd++; |
| 482 | |
| 483 | /* Check to see if we are expecting success or failure */ |
| 484 | if (bExpectOk) { |
| 485 | /* do nothing if we expect success */ |
| 486 | } else if (bExpectError) { |
| 487 | /* Invert the result if we expect failure */ |
| 488 | rc = !rc; |
| 489 | } else { |
| 490 | fprintf(stderr, "%s:%d: statement argument should be 'ok' or 'error'\n" , zScriptFile, |
| 491 | sScript.startLine); |
| 492 | IFAIL(); |
| 493 | } |
| 494 | |
| 495 | /* Report an error if the results do not match expectation */ |
| 496 | if (rc) { |
| 497 | fprintf(stderr, "%s:%d: statement error\n" , zScriptFile, sScript.startLine); |
| 498 | IFAIL(); |
| 499 | } |
| 500 | } else if (strcmp(sScript.azToken[0], "query" ) == 0) { |
| 501 | int k = 0; |
| 502 | int c; |
| 503 | |
| 504 | /* Verify that the type string consists of one or more |
| 505 | *characters |
| 506 | ** from the set "TIR". */ |
| 507 | for (k = 0; (c = sScript.azToken[1][k]) != 0; k++) { |
| 508 | if (c != 'T' && c != 'I' && c != 'R') { |
| 509 | fprintf(stderr, |
| 510 | "%s:%d: unknown type character '%c' in type " |
| 511 | "string\n" , |
| 512 | zScriptFile, sScript.startLine, c); |
| 513 | nErr++; |
| 514 | break; |
| 515 | } |
| 516 | } |
| 517 | if (c != 0) |
| 518 | continue; |
| 519 | if (k <= 0) { |
| 520 | fprintf(stderr, "%s:%d: missing type string\n" , zScriptFile, sScript.startLine); |
| 521 | IFAIL(); |
| 522 | } |
| 523 | |
| 524 | /* Extract the SQL from second and subsequent lines of the |
| 525 | *record |
| 526 | ** until the first "----" line or until end of record. |
| 527 | */ |
| 528 | k = 0; |
| 529 | while (!nextIsBlank(&sScript) && nextLine(&sScript) && sScript.zLine[0] && |
| 530 | strcmp(sScript.zLine, "----" ) != 0) { |
| 531 | if (k > 0) |
| 532 | zScript[k++] = '\n'; |
| 533 | memmove(&zScript[k], sScript.zLine, sScript.len); |
| 534 | k += sScript.len; |
| 535 | } |
| 536 | zScript[k] = 0; |
| 537 | |
| 538 | /* Run the query */ |
| 539 | nResult = 0; |
| 540 | azResult = 0; |
| 541 | if (enableTrace) |
| 542 | printf("%s;\n" , zScript); |
| 543 | rc = pEngine->xQuery(pConn, zScript, sScript.azToken[1], &azResult, &nResult); |
| 544 | nCmd++; |
| 545 | if (rc) { |
| 546 | fprintf(stderr, "%s:%d: query failed\n" , zScriptFile, sScript.startLine); |
| 547 | pEngine->xFreeResults(pConn, azResult, nResult); |
| 548 | IFAIL(); |
| 549 | } |
| 550 | |
| 551 | /* Do any required sorting of query results */ |
| 552 | if (sScript.azToken[2][0] == 0 || strcmp(sScript.azToken[2], "nosort" ) == 0) { |
| 553 | /* Do no sorting */ |
| 554 | } else if (strcmp(sScript.azToken[2], "rowsort" ) == 0) { |
| 555 | /* Row-oriented sorting */ |
| 556 | nColumn = (int)strlen(sScript.azToken[1]); |
| 557 | qsort(azResult, nResult / nColumn, sizeof(azResult[0]) * nColumn, rowCompare); |
| 558 | } else if (strcmp(sScript.azToken[2], "valuesort" ) == 0) { |
| 559 | /* Sort all values independently */ |
| 560 | nColumn = 1; |
| 561 | qsort(azResult, nResult, sizeof(azResult[0]), rowCompare); |
| 562 | } else { |
| 563 | fprintf(stderr, "%s:%d: unknown sort method: '%s'\n" , zScriptFile, sScript.startLine, |
| 564 | sScript.azToken[2]); |
| 565 | IFAIL(); |
| 566 | } |
| 567 | |
| 568 | /* Hash the results if we are over the hash threshold or if we |
| 569 | ** there is a hash label */ |
| 570 | if (sScript.azToken[3][0] || (hashThreshold > 0 && nResult > hashThreshold)) { |
| 571 | md5_add("" ); /* make sure md5 is reset, even if no results */ |
| 572 | for (i = 0; i < nResult; i++) { |
| 573 | md5_add(azResult[i]); |
| 574 | md5_add("\n" ); |
| 575 | } |
| 576 | snprintf(zHash, sizeof(zHash), "%d values hashing to %s" , nResult, md5_finish()); |
| 577 | sScript.azToken[3][20] = 0; |
| 578 | if (sScript.azToken[3][0] && checkValue(sScript.azToken[3], md5_finish())) { |
| 579 | fprintf(stderr, |
| 580 | "%s:%d: labeled result [%s] does not agree with " |
| 581 | "previous values\n" , |
| 582 | zScriptFile, sScript.startLine, sScript.azToken[3]); |
| 583 | IFAIL(); |
| 584 | } |
| 585 | } |
| 586 | |
| 587 | /* In verify mode, first skip over the ---- line if we are |
| 588 | *still |
| 589 | ** pointing at it. */ |
| 590 | if (strcmp(sScript.zLine, "----" ) == 0) |
| 591 | nextLine(&sScript); |
| 592 | |
| 593 | /* Compare subsequent lines of the script against the |
| 594 | *results |
| 595 | ** from the query. Report an error if any differences are |
| 596 | *found. |
| 597 | */ |
| 598 | if (hashThreshold == 0 || nResult <= hashThreshold) { |
| 599 | for (i = 0; i < nResult && sScript.zLine[0]; nextLine(&sScript), i++) { |
| 600 | if (strcmp(sScript.zLine, azResult[i]) != 0) { |
| 601 | fprintf(stderr, "%s:%d: wrong result\n" , zScriptFile, sScript.nLine); |
| 602 | |
| 603 | fprintf(stderr, "%s <> %s\n" , sScript.zLine, azResult[i]); |
| 604 | IFAIL(); |
| 605 | } |
| 606 | // we check this already but this inflates the test |
| 607 | // case count as desired |
| 608 | REQUIRE(strcmp(sScript.zLine, azResult[i]) == 0); |
| 609 | } |
| 610 | } else { |
| 611 | if (strcmp(sScript.zLine, zHash) != 0) { |
| 612 | fprintf(stderr, "%s:%d: wrong result hash\n" , zScriptFile, sScript.nLine); |
| 613 | IFAIL(); |
| 614 | } |
| 615 | } |
| 616 | |
| 617 | /* Free the query results */ |
| 618 | pEngine->xFreeResults(pConn, azResult, nResult); |
| 619 | } else if (strcmp(sScript.azToken[0], "hash-threshold" ) == 0) { |
| 620 | /* Set the maximum number of result values that will be accepted |
| 621 | ** for a query. If the number of result values exceeds this |
| 622 | *number, |
| 623 | ** then an MD5 hash is computed of all values, and the resulting |
| 624 | *hash |
| 625 | ** is the only result. |
| 626 | ** |
| 627 | ** If the threshold is 0, then hashing is never used. |
| 628 | ** |
| 629 | ** If a threshold was specified on the command line, ignore |
| 630 | ** any specifed in the script. |
| 631 | */ |
| 632 | if (!bHt) { |
| 633 | hashThreshold = atoi(sScript.azToken[1]); |
| 634 | } |
| 635 | } else if (strcmp(sScript.azToken[0], "halt" ) == 0) { |
| 636 | /* Used for debugging. Stop reading the test script and shut |
| 637 | *down. |
| 638 | ** A "halt" record can be inserted in the middle of a test |
| 639 | *script in |
| 640 | ** to run the script up to a particular point that is giving a |
| 641 | ** faulty result, then terminate at that point for analysis. |
| 642 | */ |
| 643 | fprintf(stderr, "%s:%d: halt\n" , zScriptFile, sScript.startLine); |
| 644 | break; |
| 645 | } else { |
| 646 | /* An unrecognized record type is an error */ |
| 647 | fprintf(stderr, "%s:%d: unknown record type: '%s'\n" , zScriptFile, sScript.startLine, sScript.azToken[0]); |
| 648 | IFAIL(); |
| 649 | } |
| 650 | } |
| 651 | |
| 652 | /* Shutdown the database connection. |
| 653 | */ |
| 654 | pEngine->xDisconnect(pConn); |
| 655 | free(zScript); |
| 656 | } |
| 657 | |
| 658 | // code below traverses the test directory and makes individual test cases out |
| 659 | // of each script |
| 660 | static void listFiles(const string &path, std::function<void(const string &)> cb) { |
| 661 | #ifndef SUN |
| 662 | if (auto dir = opendir(path.c_str())) { |
| 663 | while (auto f = readdir(dir)) { |
| 664 | if (f->d_name[0] == '.') |
| 665 | continue; |
| 666 | if (f->d_type == DT_DIR) |
| 667 | listFiles(path + f->d_name + "/" , cb); |
| 668 | |
| 669 | if (f->d_type == DT_REG) |
| 670 | cb(path + f->d_name); |
| 671 | } |
| 672 | closedir(dir); |
| 673 | } |
| 674 | #endif |
| 675 | } |
| 676 | |
| 677 | static bool endsWith(const string &mainStr, const string &toMatch) { |
| 678 | return (mainStr.size() >= toMatch.size() && |
| 679 | mainStr.compare(mainStr.size() - toMatch.size(), toMatch.size(), toMatch) == 0); |
| 680 | } |
| 681 | |
| 682 | static void testRunner() { |
| 683 | // this is an ugly hack that uses the test case name to pass the script file |
| 684 | // name if someone has a better idea... |
| 685 | auto name = Catch::getResultCapture().getCurrentTestName(); |
| 686 | fprintf(stderr, "%s\n" , name.c_str()); |
| 687 | execute_file(name); |
| 688 | } |
| 689 | |
| 690 | TEST_CASE("SQLite select1" , "[sqlitelogic]" ) { |
| 691 | execute_file("test/sqlite/select1.test" ); |
| 692 | } |
| 693 | |
| 694 | TEST_CASE("SQLite select2" , "[sqlitelogic]" ) { |
| 695 | execute_file("test/sqlite/select2.test" ); |
| 696 | } |
| 697 | |
| 698 | TEST_CASE("SQLite select3" , "[sqlitelogic]" ) { |
| 699 | execute_file("test/sqlite/select3.test" ); |
| 700 | } |
| 701 | |
| 702 | TEST_CASE("SQLite select4" , "[sqlitelogic][.]" ) { |
| 703 | execute_file("test/sqlite/select4.test" ); |
| 704 | } |
| 705 | |
| 706 | struct AutoRegTests { |
| 707 | AutoRegTests() { |
| 708 | vector<string> excludes = { |
| 709 | "test/select1.test" , // tested separately |
| 710 | "test/select2.test" , "test/select3.test" , "test/select4.test" , |
| 711 | "test/index" , // no index yet |
| 712 | "random/groupby/" , // having column binding issue with first |
| 713 | "random/select/slt_good_70.test" , // join on not between |
| 714 | "random/expr/slt_good_10.test" , // these all fail because the AVG |
| 715 | // decimal rewrite |
| 716 | "random/expr/slt_good_102.test" , "random/expr/slt_good_107.test" , "random/expr/slt_good_108.test" , |
| 717 | "random/expr/slt_good_109.test" , "random/expr/slt_good_111.test" , "random/expr/slt_good_112.test" , |
| 718 | "random/expr/slt_good_113.test" , "random/expr/slt_good_115.test" , "random/expr/slt_good_116.test" , |
| 719 | "random/expr/slt_good_117.test" , "random/expr/slt_good_13.test" , "random/expr/slt_good_15.test" , |
| 720 | "random/expr/slt_good_16.test" , "random/expr/slt_good_17.test" , "random/expr/slt_good_19.test" , |
| 721 | "random/expr/slt_good_21.test" , "random/expr/slt_good_22.test" , "random/expr/slt_good_24.test" , |
| 722 | "random/expr/slt_good_28.test" , "random/expr/slt_good_29.test" , "random/expr/slt_good_3.test" , |
| 723 | "random/expr/slt_good_30.test" , "random/expr/slt_good_34.test" , "random/expr/slt_good_38.test" , |
| 724 | "random/expr/slt_good_4.test" , "random/expr/slt_good_41.test" , "random/expr/slt_good_44.test" , |
| 725 | "random/expr/slt_good_45.test" , "random/expr/slt_good_49.test" , "random/expr/slt_good_52.test" , |
| 726 | "random/expr/slt_good_53.test" , "random/expr/slt_good_55.test" , "random/expr/slt_good_59.test" , |
| 727 | "random/expr/slt_good_6.test" , "random/expr/slt_good_60.test" , "random/expr/slt_good_63.test" , |
| 728 | "random/expr/slt_good_64.test" , "random/expr/slt_good_67.test" , "random/expr/slt_good_69.test" , |
| 729 | "random/expr/slt_good_7.test" , "random/expr/slt_good_71.test" , "random/expr/slt_good_72.test" , |
| 730 | "random/expr/slt_good_8.test" , "random/expr/slt_good_80.test" , "random/expr/slt_good_82.test" , |
| 731 | "random/expr/slt_good_85.test" , "random/expr/slt_good_9.test" , "random/expr/slt_good_90.test" , |
| 732 | "random/expr/slt_good_91.test" , "random/expr/slt_good_94.test" , "random/expr/slt_good_95.test" , |
| 733 | "random/expr/slt_good_96.test" , "random/expr/slt_good_99.test" , "random/aggregates/slt_good_2.test" , |
| 734 | "random/aggregates/slt_good_5.test" , "random/aggregates/slt_good_7.test" , |
| 735 | "random/aggregates/slt_good_9.test" , "random/aggregates/slt_good_17.test" , |
| 736 | "random/aggregates/slt_good_28.test" , "random/aggregates/slt_good_45.test" , |
| 737 | "random/aggregates/slt_good_50.test" , "random/aggregates/slt_good_52.test" , |
| 738 | "random/aggregates/slt_good_58.test" , "random/aggregates/slt_good_65.test" , |
| 739 | "random/aggregates/slt_good_66.test" , "random/aggregates/slt_good_76.test" , |
| 740 | "random/aggregates/slt_good_81.test" , "random/aggregates/slt_good_90.test" , |
| 741 | "random/aggregates/slt_good_96.test" , "random/aggregates/slt_good_102.test" , |
| 742 | "random/aggregates/slt_good_106.test" , "random/aggregates/slt_good_112.test" , |
| 743 | "random/aggregates/slt_good_118.test" , |
| 744 | "third_party/sqllogictest/test/evidence/in1.test" , // UNIQUE index on text |
| 745 | "evidence/slt_lang_replace.test" , // feature not supported |
| 746 | "evidence/slt_lang_reindex.test" , // " |
| 747 | "evidence/slt_lang_dropindex.test" , // " |
| 748 | "evidence/slt_lang_createtrigger.test" , // " |
| 749 | "evidence/slt_lang_droptrigger.test" // " |
| 750 | }; |
| 751 | listFiles("third_party/sqllogictest/test/" , [excludes](const string &path) { |
| 752 | if (endsWith(path, ".test" )) { |
| 753 | for (auto excl : excludes) { |
| 754 | if (path.find(excl) != string::npos) { |
| 755 | return; |
| 756 | } |
| 757 | } |
| 758 | REGISTER_TEST_CASE(testRunner, path, "[sqlitelogic][.]" ); |
| 759 | } |
| 760 | }); |
| 761 | } |
| 762 | }; |
| 763 | AutoRegTests autoreg; |
| 764 | |