tests.h (19843B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #ifndef jsapi_tests_tests_h 8 #define jsapi_tests_tests_h 9 10 #include "mozilla/Sprintf.h" 11 12 #include <errno.h> 13 #include <stdio.h> 14 #include <stdlib.h> 15 #include <string.h> 16 #include <type_traits> 17 18 #include "jsapi.h" 19 20 #include "gc/GC.h" 21 #include "js/AllocPolicy.h" 22 #include "js/ArrayBuffer.h" 23 #include "js/CharacterEncoding.h" 24 #include "js/Conversions.h" 25 #include "js/Equality.h" // JS::SameValue 26 #include "js/GlobalObject.h" // JS::DefaultGlobalClassOps 27 #include "js/RegExpFlags.h" // JS::RegExpFlags 28 #include "js/Vector.h" 29 #include "js/Warnings.h" // JS::SetWarningReporter 30 #include "vm/JSContext.h" 31 32 /* Note: Aborts on OOM. */ 33 class JSAPITestString { 34 js::Vector<char, 0, js::SystemAllocPolicy> chars; 35 36 public: 37 JSAPITestString() {} 38 explicit JSAPITestString(const char* s) { *this += s; } 39 JSAPITestString(const JSAPITestString& s) { *this += s; } 40 41 const char* begin() const { return chars.begin(); } 42 const char* end() const { return chars.end(); } 43 size_t length() const { return chars.length(); } 44 void clear() { chars.clearAndFree(); } 45 46 JSAPITestString& operator+=(const char* s) { 47 if (!chars.append(s, strlen(s))) { 48 abort(); 49 } 50 return *this; 51 } 52 53 JSAPITestString& operator+=(const JSAPITestString& s) { 54 if (!chars.append(s.begin(), s.length())) { 55 abort(); 56 } 57 return *this; 58 } 59 }; 60 61 inline JSAPITestString operator+(const JSAPITestString& a, const char* b) { 62 JSAPITestString result = a; 63 result += b; 64 return result; 65 } 66 67 inline JSAPITestString operator+(const JSAPITestString& a, 68 const JSAPITestString& b) { 69 JSAPITestString result = a; 70 result += b; 71 return result; 72 } 73 74 // A singly linked list of tests which doesn't require runtime intialization. 75 // 76 // We rely on the runtime initialization of global variables containing test 77 // instances to add themselves to these lists, which depends on them already 78 // being in a valid state. 79 template <typename T> 80 class JSAPITestList { 81 T* first = nullptr; 82 T* last = nullptr; 83 84 public: 85 T* getFirst() const { return first; } 86 87 void pushBack(T* element) { 88 MOZ_ASSERT(!element->next); 89 MOZ_ASSERT(bool(first) == bool(last)); 90 91 if (!first) { 92 first = element; 93 last = element; 94 return; 95 } 96 97 last->next = element; 98 last = element; 99 } 100 }; 101 102 class JSAPIRuntimeTest; 103 104 class JSAPITest { 105 public: 106 bool knownFail; 107 JSAPITestString msgs; 108 109 JSAPITest() : knownFail(false) {} 110 111 virtual ~JSAPITest() {} 112 113 virtual const char* name() = 0; 114 115 virtual void maybeAppendException(JSAPITestString& message) {} 116 117 bool fail(const JSAPITestString& msg = JSAPITestString(), 118 const char* filename = "-", int lineno = 0) { 119 char location[256]; 120 SprintfLiteral(location, "%s:%d:", filename, lineno); 121 122 JSAPITestString message(location); 123 message += msg; 124 125 maybeAppendException(message); 126 127 fprintf(stderr, "%.*s\n", int(message.length()), message.begin()); 128 129 if (msgs.length() != 0) { 130 msgs += " | "; 131 } 132 msgs += message; 133 return false; 134 } 135 136 JSAPITestString messages() const { return msgs; } 137 }; 138 139 class JSAPIRuntimeTest : public JSAPITest { 140 public: 141 static JSAPITestList<JSAPIRuntimeTest> list; 142 JSAPIRuntimeTest* next = nullptr; 143 144 JSContext* cx; 145 JS::PersistentRootedObject global; 146 147 // Whether this test is willing to skip its init() and reuse a global (and 148 // JSContext etc.) from a previous test that also has reuseGlobal=true. It 149 // also means this test is willing to skip its uninit() if it is followed by 150 // another reuseGlobal test. 151 bool reuseGlobal; 152 153 JSAPIRuntimeTest() : JSAPITest(), cx(nullptr), reuseGlobal(false) { 154 list.pushBack(this); 155 } 156 157 virtual ~JSAPIRuntimeTest() { 158 MOZ_RELEASE_ASSERT(!cx); 159 MOZ_RELEASE_ASSERT(!global); 160 } 161 162 // Initialize this test, possibly with the cx from a previously run test. 163 bool init(JSContext* maybeReusedContext); 164 165 // If this test is ok with its cx and global being reused, release this 166 // test's cx to be reused by another test. 167 JSContext* maybeForgetContext(); 168 169 static void MaybeFreeContext(JSContext* maybeCx); 170 171 // The real initialization happens in init(JSContext*), above, but this 172 // method may be overridden to perform additional initialization after the 173 // JSContext and global have been created. 174 virtual bool init() { return true; } 175 virtual void uninit(); 176 177 virtual bool run(JS::HandleObject global) = 0; 178 179 #define EXEC(s) \ 180 do { \ 181 if (!exec(s, __FILE__, __LINE__)) return false; \ 182 } while (false) 183 184 bool exec(const char* utf8, const char* filename, int lineno); 185 186 // Like exec(), but doesn't call fail() if JS::Evaluate returns false. 187 bool execDontReport(const char* utf8, const char* filename, int lineno); 188 189 #define EVAL(s, vp) \ 190 do { \ 191 if (!evaluate(s, __FILE__, __LINE__, vp)) return false; \ 192 } while (false) 193 194 bool evaluate(const char* utf8, const char* filename, int lineno, 195 JS::MutableHandleValue vp); 196 197 JSAPITestString jsvalToSource(JS::HandleValue v) { 198 JS::Rooted<JSString*> str(cx, JS_ValueToSource(cx, v)); 199 if (str) { 200 if (JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, str)) { 201 return JSAPITestString(bytes.get()); 202 } 203 } 204 JS_ClearPendingException(cx); 205 return JSAPITestString("<<error converting value to string>>"); 206 } 207 208 JSAPITestString toSource(char c) { 209 char buf[2] = {c, '\0'}; 210 return JSAPITestString(buf); 211 } 212 213 JSAPITestString toSource(long v) { 214 char buf[40]; 215 SprintfLiteral(buf, "%ld", v); 216 return JSAPITestString(buf); 217 } 218 219 JSAPITestString toSource(unsigned long v) { 220 char buf[40]; 221 SprintfLiteral(buf, "%lu", v); 222 return JSAPITestString(buf); 223 } 224 225 JSAPITestString toSource(long long v) { 226 char buf[40]; 227 SprintfLiteral(buf, "%lld", v); 228 return JSAPITestString(buf); 229 } 230 231 JSAPITestString toSource(unsigned long long v) { 232 char buf[40]; 233 SprintfLiteral(buf, "%llu", v); 234 return JSAPITestString(buf); 235 } 236 237 JSAPITestString toSource(double d) { 238 char buf[40]; 239 SprintfLiteral(buf, "%17lg", d); 240 return JSAPITestString(buf); 241 } 242 243 JSAPITestString toSource(unsigned int v) { 244 return toSource((unsigned long)v); 245 } 246 247 JSAPITestString toSource(int v) { return toSource((long)v); } 248 249 JSAPITestString toSource(bool v) { 250 return JSAPITestString(v ? "true" : "false"); 251 } 252 253 JSAPITestString toSource(JS::RegExpFlags flags) { 254 JSAPITestString str; 255 if (flags.hasIndices()) { 256 str += "d"; 257 } 258 if (flags.global()) { 259 str += "g"; 260 } 261 if (flags.ignoreCase()) { 262 str += "i"; 263 } 264 if (flags.multiline()) { 265 str += "m"; 266 } 267 if (flags.dotAll()) { 268 str += "s"; 269 } 270 if (flags.unicode()) { 271 str += "u"; 272 } 273 if (flags.unicodeSets()) { 274 str += "v"; 275 } 276 if (flags.sticky()) { 277 str += "y"; 278 } 279 return str; 280 } 281 282 JSAPITestString toSource(JSAtom* v) { 283 JS::RootedValue val(cx, JS::StringValue((JSString*)v)); 284 return jsvalToSource(val); 285 } 286 287 // Note that in some still-supported GCC versions (we think anything before 288 // GCC 4.6), this template does not work when the second argument is 289 // nullptr. It infers type U = long int. Use CHECK_NULL instead. 290 template <typename T, typename U> 291 bool checkEqual(const T& actual, const U& expected, const char* actualExpr, 292 const char* expectedExpr, const char* filename, int lineno) { 293 static_assert(std::is_signed_v<T> == std::is_signed_v<U>, 294 "using CHECK_EQUAL with different-signed inputs triggers " 295 "compiler warnings"); 296 static_assert( 297 std::is_unsigned_v<T> == std::is_unsigned_v<U>, 298 "using CHECK_EQUAL with different-signed inputs triggers compiler " 299 "warnings"); 300 return (actual == expected) || 301 fail(JSAPITestString("CHECK_EQUAL failed: expected (") + 302 expectedExpr + ") = " + toSource(expected) + ", got (" + 303 actualExpr + ") = " + toSource(actual), 304 filename, lineno); 305 } 306 307 #define CHECK_EQUAL(actual, expected) \ 308 do { \ 309 if (!checkEqual(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ 310 return false; \ 311 } while (false) 312 313 template <typename T> 314 bool checkNull(const T* actual, const char* actualExpr, const char* filename, 315 int lineno) { 316 return (actual == nullptr) || 317 fail(JSAPITestString("CHECK_NULL failed: expected nullptr, got (") + 318 actualExpr + ") = " + toSource(actual), 319 filename, lineno); 320 } 321 322 #define CHECK_NULL(actual) \ 323 do { \ 324 if (!checkNull(actual, #actual, __FILE__, __LINE__)) return false; \ 325 } while (false) 326 327 bool checkSame(const JS::Value& actualArg, const JS::Value& expectedArg, 328 const char* actualExpr, const char* expectedExpr, 329 const char* filename, int lineno) { 330 bool same; 331 JS::RootedValue actual(cx, actualArg), expected(cx, expectedArg); 332 return (JS::SameValue(cx, actual, expected, &same) && same) || 333 fail(JSAPITestString( 334 "CHECK_SAME failed: expected JS::SameValue(cx, ") + 335 actualExpr + ", " + expectedExpr + 336 "), got !JS::SameValue(cx, " + jsvalToSource(actual) + 337 ", " + jsvalToSource(expected) + ")", 338 filename, lineno); 339 } 340 341 #define CHECK_SAME(actual, expected) \ 342 do { \ 343 if (!checkSame(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ 344 return false; \ 345 } while (false) 346 347 #define CHECK(expr) \ 348 do { \ 349 if (!(expr)) \ 350 return fail(JSAPITestString("CHECK failed: " #expr), __FILE__, \ 351 __LINE__); \ 352 } while (false) 353 354 void maybeAppendException(JSAPITestString& message) override { 355 if (JS_IsExceptionPending(cx)) { 356 message += " -- "; 357 358 js::gc::AutoSuppressGC gcoff(cx); 359 JS::RootedValue v(cx); 360 JS_GetPendingException(cx, &v); 361 JS_ClearPendingException(cx); 362 JS::Rooted<JSString*> s(cx, JS::ToString(cx, v)); 363 if (s) { 364 if (JS::UniqueChars bytes = JS_EncodeStringToLatin1(cx, s)) { 365 message += bytes.get(); 366 } 367 } 368 } 369 } 370 371 static const JSClass* basicGlobalClass() { 372 static const JSClass c = { 373 "global", 374 JSCLASS_GLOBAL_FLAGS, 375 &JS::DefaultGlobalClassOps, 376 }; 377 return &c; 378 } 379 380 protected: 381 static bool print(JSContext* cx, unsigned argc, JS::Value* vp) { 382 JS::CallArgs args = JS::CallArgsFromVp(argc, vp); 383 384 JS::Rooted<JSString*> str(cx); 385 for (unsigned i = 0; i < args.length(); i++) { 386 str = JS::ToString(cx, args[i]); 387 if (!str) { 388 return false; 389 } 390 JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, str); 391 if (!bytes) { 392 return false; 393 } 394 printf("%s%s", i ? " " : "", bytes.get()); 395 } 396 397 putchar('\n'); 398 fflush(stdout); 399 args.rval().setUndefined(); 400 return true; 401 } 402 403 bool definePrint(); 404 405 virtual JSContext* createContext() { 406 JSContext* cx = JS_NewContext(8L * 1024 * 1024); 407 if (!cx) { 408 return nullptr; 409 } 410 JS::SetWarningReporter(cx, &reportWarning); 411 return cx; 412 } 413 414 static void reportWarning(JSContext* cx, JSErrorReport* report) { 415 MOZ_RELEASE_ASSERT(report->isWarning()); 416 417 fprintf(stderr, "%s:%u:%s\n", 418 report->filename ? report->filename.c_str() : "<no filename>", 419 (unsigned int)report->lineno, report->message().c_str()); 420 } 421 422 virtual const JSClass* getGlobalClass() { return basicGlobalClass(); } 423 424 virtual JSObject* createGlobal(JSPrincipals* principals = nullptr); 425 }; 426 427 class JSAPIFrontendTest : public JSAPITest { 428 public: 429 static JSAPITestList<JSAPIFrontendTest> list; 430 JSAPIFrontendTest* next = nullptr; 431 432 JSAPIFrontendTest() : JSAPITest() { list.pushBack(this); } 433 434 virtual ~JSAPIFrontendTest() {} 435 436 virtual bool init() { return true; } 437 virtual void uninit() {} 438 439 virtual bool run() = 0; 440 }; 441 442 #define BEGIN_TEST_WITH_ATTRIBUTES_AND_EXTRA(testname, attrs, extra) \ 443 class cls_##testname : public JSAPIRuntimeTest { \ 444 public: \ 445 virtual const char* name() override { return #testname; } \ 446 extra virtual bool run(JS::HandleObject global) override attrs 447 448 #define BEGIN_TEST_WITH_ATTRIBUTES(testname, attrs) \ 449 BEGIN_TEST_WITH_ATTRIBUTES_AND_EXTRA(testname, attrs, ) 450 451 #define BEGIN_TEST(testname) BEGIN_TEST_WITH_ATTRIBUTES(testname, ) 452 453 #define BEGIN_FRONTEND_TEST_WITH_ATTRIBUTES_AND_EXTRA(testname, attrs, extra) \ 454 class cls_##testname : public JSAPIFrontendTest { \ 455 public: \ 456 virtual const char* name() override { return #testname; } \ 457 extra virtual bool run() override attrs 458 459 #define BEGIN_FRONTEND_TEST_WITH_ATTRIBUTES(testname, attrs) \ 460 BEGIN_FRONTEND_TEST_WITH_ATTRIBUTES_AND_EXTRA(testname, attrs, ) 461 462 #define BEGIN_FRONTEND_TEST(testname) \ 463 BEGIN_FRONTEND_TEST_WITH_ATTRIBUTES(testname, ) 464 465 #define BEGIN_REUSABLE_TEST(testname) \ 466 BEGIN_TEST_WITH_ATTRIBUTES_AND_EXTRA( \ 467 testname, , \ 468 cls_##testname() : JSAPIRuntimeTest() { reuseGlobal = true; }) 469 470 #define END_TEST(testname) \ 471 } \ 472 ; \ 473 MOZ_RUNINIT static cls_##testname cls_##testname##_instance; 474 475 /* 476 * A "fixture" is a subclass of JSAPIRuntimeTest that holds common definitions 477 * for a set of tests. Each test that wants to use the fixture should use 478 * BEGIN_FIXTURE_TEST and END_FIXTURE_TEST, just as one would use BEGIN_TEST and 479 * END_TEST, but include the fixture class as the first argument. The fixture 480 * class's declarations are then in scope for the test bodies. 481 */ 482 483 #define BEGIN_FIXTURE_TEST(fixture, testname) \ 484 class cls_##testname : public fixture { \ 485 public: \ 486 virtual const char* name() override { return #testname; } \ 487 virtual bool run(JS::HandleObject global) override 488 489 #define END_FIXTURE_TEST(fixture, testname) \ 490 } \ 491 ; \ 492 MOZ_RUNINIT static cls_##testname cls_##testname##_instance; 493 494 /* 495 * A class for creating and managing one temporary file. 496 * 497 * We could use the ISO C temporary file functions here, but those try to 498 * create files in the root directory on Windows, which fails for users 499 * without Administrator privileges. 500 */ 501 class TempFile { 502 const char* name; 503 FILE* stream; 504 505 public: 506 TempFile() : name(), stream() {} 507 ~TempFile() { 508 if (stream) { 509 close(); 510 } 511 if (name) { 512 remove(); 513 } 514 } 515 516 /* 517 * Return a stream for a temporary file named |fileName|. Infallible. 518 * Use only once per TempFile instance. If the file is not explicitly 519 * closed and deleted via the member functions below, this object's 520 * destructor will clean them up. 521 */ 522 FILE* open(const char* fileName) { 523 stream = fopen(fileName, "wb+"); 524 if (!stream) { 525 fprintf(stderr, "error opening temporary file '%s': %s\n", fileName, 526 strerror(errno)); 527 exit(1); 528 } 529 name = fileName; 530 return stream; 531 } 532 533 /* Close the temporary file's stream. */ 534 void close() { 535 if (fclose(stream) == EOF) { 536 fprintf(stderr, "error closing temporary file '%s': %s\n", name, 537 strerror(errno)); 538 exit(1); 539 } 540 stream = nullptr; 541 } 542 543 /* Delete the temporary file. */ 544 void remove() { 545 if (::remove(name) != 0) { 546 fprintf(stderr, "error deleting temporary file '%s': %s\n", name, 547 strerror(errno)); 548 exit(1); 549 } 550 name = nullptr; 551 } 552 }; 553 554 // Just a wrapper around JSPrincipals that allows static construction. 555 class TestJSPrincipals : public JSPrincipals { 556 public: 557 explicit TestJSPrincipals(int rc = 0) : JSPrincipals() { refcount = rc; } 558 559 bool write(JSContext* cx, JSStructuredCloneWriter* writer) override { 560 MOZ_ASSERT(false, "not implemented"); 561 return false; 562 } 563 564 bool isSystemOrAddonPrincipal() override { return true; } 565 }; 566 567 // A class that simulates externally memory-managed data, for testing with 568 // array buffers. 569 class ExternalData { 570 char* contents_; 571 size_t len_; 572 bool uniquePointerCreated_ = false; 573 574 public: 575 explicit ExternalData(const char* str) 576 : contents_(strdup(str)), len_(strlen(str) + 1) {} 577 578 size_t len() const { return len_; } 579 void* contents() const { return contents_; } 580 char* asString() const { return contents_; } 581 bool wasFreed() const { return !contents_; } 582 583 void free() { 584 MOZ_ASSERT(!wasFreed()); 585 ::free(contents_); 586 contents_ = nullptr; 587 } 588 589 mozilla::UniquePtr<void, JS::BufferContentsDeleter> pointer() { 590 MOZ_ASSERT(!uniquePointerCreated_, 591 "Not allowed to create multiple unique pointers to contents"); 592 uniquePointerCreated_ = true; 593 return {contents_, {ExternalData::freeCallback, this}}; 594 } 595 596 static void freeCallback(void* contents, void* userData) { 597 auto self = static_cast<ExternalData*>(userData); 598 MOZ_ASSERT(self->contents() == contents); 599 self->free(); 600 } 601 }; 602 603 class AutoGCParameter { 604 JSContext* cx_; 605 JSGCParamKey key_; 606 uint32_t value_; 607 608 public: 609 explicit AutoGCParameter(JSContext* cx, JSGCParamKey key, uint32_t value) 610 : cx_(cx), key_(key), value_() { 611 value_ = JS_GetGCParameter(cx, key); 612 JS_SetGCParameter(cx, key, value); 613 } 614 ~AutoGCParameter() { JS_SetGCParameter(cx_, key_, value_); } 615 }; 616 617 #ifdef JS_GC_ZEAL 618 /* 619 * Temporarily disable the GC zeal setting. This is only useful in tests that 620 * need very explicit GC behavior and should not be used elsewhere. 621 */ 622 class AutoLeaveZeal { 623 JSContext* cx_; 624 uint32_t zealBits_; 625 uint32_t frequency_; 626 627 public: 628 explicit AutoLeaveZeal(JSContext* cx) : cx_(cx), zealBits_(0), frequency_(0) { 629 uint32_t dummy; 630 JS::GetGCZealBits(cx_, &zealBits_, &frequency_, &dummy); 631 JS::SetGCZeal(cx_, 0, 0); 632 JS::PrepareForFullGC(cx_); 633 JS::NonIncrementalGC(cx_, JS::GCOptions::Normal, JS::GCReason::DEBUG_GC); 634 } 635 ~AutoLeaveZeal() { 636 JS::SetGCZeal(cx_, 0, 0); 637 for (size_t i = 0; i < sizeof(zealBits_) * 8; i++) { 638 if (zealBits_ & (1 << i)) { 639 JS::SetGCZeal(cx_, i, frequency_); 640 } 641 } 642 643 # ifdef DEBUG 644 uint32_t zealBitsAfter, frequencyAfter, dummy; 645 JS::GetGCZealBits(cx_, &zealBitsAfter, &frequencyAfter, &dummy); 646 MOZ_ASSERT(zealBitsAfter == zealBits_); 647 MOZ_ASSERT(frequencyAfter == frequency_); 648 # endif 649 } 650 }; 651 652 #else 653 class AutoLeaveZeal { 654 public: 655 explicit AutoLeaveZeal(JSContext* cx) {} 656 }; 657 #endif 658 659 #endif /* jsapi_tests_tests_h */