diff --git a/CMakeLists.txt b/CMakeLists.txt index 52a7e8d2..6b732cbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,7 +45,7 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) set(CMAKE_CXX_EXTENSIONS OFF) if(UNIX) - SET(COMPILE_FLAGS "-g -O0 -coverage -fprofile-arcs -ftest-coverage") + SET(COMPILE_FLAGS "-g -ggdb -O0 -coverage -fprofile-arcs -ftest-coverage") elseif(WIN32) SET(COMPILE_FLAGS "") endif() @@ -55,7 +55,7 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) # Try to set bindings for apple # Note I don't actually think that this works currently if(APPLE) - find_package(Python 3 COMPONENTS Interpreter Development REQUIRED) + find_package(Python 3.9...3.11 COMPONENTS Interpreter Development REQUIRED) find_package(SpiderMonkey REQUIRED) set(PYTHON_MAJOR $ENV{Python_VERSION_MAJOR}) set(PYTHON_MINOR $ENV{Python_VERSION_MINOR}) @@ -64,11 +64,11 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) set(PYTHON_LIBRARIES ${Python_LIBRARIES}) message("Apple - Using Python:${Python_VERSION_MAJOR} - Libraries:${PYTHON_LIBRARIES} - IncludeDirs: ${PYTHON_INCLUDE_DIR}") elseif(UNIX) - find_package(PythonLibs 3 REQUIRED) + find_package(PythonLibs 3.9...3.11 REQUIRED) find_package(SpiderMonkey REQUIRED) elseif(WIN32) - find_package(PythonInterp 3 REQUIRED) - find_package(PythonLibs 3 REQUIRED) + find_package(PythonInterp 3.9...3.11 REQUIRED) + find_package(PythonLibs 3.9...3.11 REQUIRED) find_package(SpiderMonkey REQUIRED) set(PYTHONLIBS_VERSION_STRING $ENV{PY_VERSION}) endif() diff --git a/include/PyType.hh b/include/PyType.hh index 9a150457..a641aed8 100644 --- a/include/PyType.hh +++ b/include/PyType.hh @@ -23,6 +23,7 @@ */ struct PyType { public: + PyType(); PyType(PyObject *object); friend std::ostream &operator <<(std::ostream &str, const PyType &data) { data.print(str); diff --git a/include/StrType.hh b/include/StrType.hh index 62878cab..5748ef1f 100644 --- a/include/StrType.hh +++ b/include/StrType.hh @@ -15,6 +15,8 @@ #include "PyType.hh" #include "TypeEnum.hh" +#include + #include #include @@ -26,11 +28,43 @@ struct StrType : public PyType { public: StrType(PyObject *object); StrType(char *string); + + /** + * @brief Construct a new StrType object from a JSString. Automatically handles encoding conversion for latin1 & UCS2: + * codepoint | Python | Spidermonkey | identical representation? + * 000000-0000FF | latin1 | latin1 | Yes + * 000100-00D7FF | UCS2 | UTF16 | Yes + * 00D800-00DFFF | UCS2 (unpaired) | UTF16 (unpaired) | Yes + * 00E000-00FFFF | UCS2 | UTF16 | Yes + * 010000-10FFFF | UCS4 | UTF16 | No, conversion and new backing store required, user must explicitly call asUCS4() + * + * @param cx - javascript context pointer + * @param str - JSString pointer + */ + StrType(JSContext *cx, JSString *str); + const TYPE returnType = TYPE::STRING; const char *getValue() const; + /** + * @brief creates new UCS4-encoded pyObject string. This must be called by the user if the original JSString contains any surrogate pairs + * + * @return PyObject* - the UCS4-encoding of the pyObject string + * + */ + PyObject *asUCS4(); + protected: virtual void print(std::ostream &os) const override; + +private: + /** + * @brief check if this.pyObject contains a surrogate pair + * + * @return true - pyObject is UCS2-encoded and contains a surrogate pair + * @return false - pyObject is not UCS2-encoded or does not contain a surrogate pair + */ + bool containsSurrogatePair(); }; #endif \ No newline at end of file diff --git a/include/TypeEnum.hh b/include/TypeEnum.hh index f6c4a4d5..5b20dfa0 100644 --- a/include/TypeEnum.hh +++ b/include/TypeEnum.hh @@ -1,3 +1,14 @@ +/** + * @file TypeEnum.hh + * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) + * @brief Enum for every PyType + * @version 0.1 + * @date 2022-0-08 + * + * @copyright Copyright (c) 2022 + * + */ + #ifndef PythonMonkey_TypeEnum_ #define PythonMonkey_TypeEnum_ diff --git a/include/modules/pythonmonkey/pythonmonkey.hh b/include/modules/pythonmonkey/pythonmonkey.hh index 33d00634..ba7ae856 100644 --- a/include/modules/pythonmonkey/pythonmonkey.hh +++ b/include/modules/pythonmonkey/pythonmonkey.hh @@ -11,19 +11,59 @@ #ifndef PythonMonkey_Module_PythonMonkey #define PythonMonkey_Module_PythonMonkey +#include "include/PyType.hh" + #include +#include #include #include +/** + * @brief Callback function passed to JS_SetGCCallback to handle PythonMonkey shared memory + * + * @param cx - Pointer to the JS Context (not used) + * @param status - enum specifying whether the Callback triggered at the beginning or end of the GC Cycle + * @param reason - reason for the GC Cycle + * @param data - + */ +void handleSharedPythonMonkeyMemory(JSContext *cx, JSGCStatus status, JS::GCReason reason, void *data); + /** * @brief Destroys the JSContext and deletes associated memory. Called when python quits or faces a fatal exception. * - * @author Caleb Aikens (caleb@kingsds.network) - * @date 2022-09-09 */ static void cleanup(); +/** + * @brief This function is used to memoize PyTypes and GCThings that use the same backing store for their data, + * so that the JS garbage collector doesn't collect memory still in use by Python. It does this by storing the + * pointers in an unordered_map, with the key being the PyType pointer, and the value being a vector of GCThing + * pointers. + * + * @param pyType - Pointer to the PyType to be memoized + * @param GCThing - Pointer to the GCThing to be memoized + */ +static void memoizePyTypeAndGCThing(PyType *pyType, JS::PersistentRootedValue *GCThing); + +/** + * @brief Function exposed by the python module that calls the spidermonkey garbage collector + * + * @param self - Pointer to the module object + * @param args - Pointer to the python tuple of arguments (not used) + * @return PyObject* - returns python None + */ +static PyObject *collect(PyObject *self, PyObject *args); + +/** + * @brief Function exposed by the python module to convert UTF16 strings to UCS4 strings + * + * @param self - Pointer to the module object + * @param args - Pointer to the python tuple of arguments (expected to contain a UTF16-encoded string as the first element) + * @return PyObject* - A new python string in UCS4 encoding + */ +static PyObject *asUCS4(PyObject *self, PyObject *args); + /** * @brief Function exposed by the python module for evaluating arbitrary JS code * diff --git a/include/pyTypeFactory.hh b/include/pyTypeFactory.hh index 9e7a995a..98ac4029 100644 --- a/include/pyTypeFactory.hh +++ b/include/pyTypeFactory.hh @@ -1,3 +1,14 @@ +/** + * @file pyTypeFactory.hh + * @author Caleb Aikens (caleb@distributive.network) & Giovanni Tedesco (giovanni@distributive.network) + * @brief Function for wrapping arbitrary PyObjects into the appropriate PyType class + * @version 0.1 + * @date 2022-08-08 + * + * @copyright Copyright (c) 2022 + * + */ + #ifndef PythonMonkey_PyTypeFactory_ #define PythonMonkey_PyTypeFactory_ diff --git a/include/utilities.hh b/include/utilities.hh index 2af3d913..d2bde949 100644 --- a/include/utilities.hh +++ b/include/utilities.hh @@ -1,6 +1,15 @@ #ifndef PythonMonkey_Utilities_ #define PythonMonkey_Utilities_ +/** + * @brief + * + * @tparam Base - child type + * @tparam T - parent type + * @param ptr - pointer to parent object + * @return true - ptr is an instance of Base + * @return false - ptr is not an instance of Base + */ template inline bool instanceof(const T *ptr) { return dynamic_cast(ptr) != nullptr; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dc6c482b..f3b296f5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -50,11 +50,10 @@ elseif(UNIX) SUFFIX ".so" ) endif() +target_link_libraries(explore ${PYTHON_LIBRARIES}) +target_link_libraries(pythonmonkey ${PYTHON_LIBRARIES}) +target_link_libraries(pythonmonkey ${SPIDERMONKEY_LIBRARIES}) target_include_directories(explore PRIVATE ${PYTHON_INCLUDE_DIR}) target_include_directories(pythonmonkey PRIVATE ${PYTHON_INCLUDE_DIR}) -target_include_directories(pythonmonkey PRIVATE ${SPIDERMONKEY_INCLUDE_DIR}) - -target_link_libraries(explore ${PYTHON_LIBRARIES}) -target_link_libraries(pythonmonkey ${PYTHON_LIBRARIES}) -target_link_libraries(pythonmonkey ${SPIDERMONKEY_LIBRARIES}) \ No newline at end of file +target_include_directories(pythonmonkey PRIVATE ${SPIDERMONKEY_INCLUDE_DIR}) \ No newline at end of file diff --git a/src/PyType.cc b/src/PyType.cc index 0a4ed8b5..5725b054 100644 --- a/src/PyType.cc +++ b/src/PyType.cc @@ -4,6 +4,8 @@ #include +PyType::PyType() {} + PyType::PyType(PyObject *object) { Py_XINCREF(object); pyObject = object; diff --git a/src/StrType.cc b/src/StrType.cc index d12b8295..c865465e 100644 --- a/src/StrType.cc +++ b/src/StrType.cc @@ -4,16 +4,139 @@ #include +#include +#include + #include +#define HIGH_SURROGATE_START 0xD800 +#define HIGH_SURROGATE_END 0xDBFF +#define LOW_SURROGATE_START 0xDC00 +#define LOW_SURROGATE_END 0xDFFF + +#define PY_UNICODE_OBJECT_DATA_ANY(op) (*(PyUnicodeObject *)op).data.any +#define PY_UNICODE_OBJECT_DATA_UCS2(op) (*(PyUnicodeObject *)op).data.ucs2 +#define PY_UNICODE_OBJECT_KIND(op) (*(PyUnicodeObject *)op)._base._base.state.kind +#define PY_UNICODE_OBJECT_LENGTH(op) (*(PyUnicodeObject *)op)._base._base.length +#define PY_UNICODE_OBJECT_WSTR(op) (*(PyUnicodeObject *)op)._base._base.wstr +#define PY_UNICODE_OBJECT_WSTR_LENGTH(op) (*(PyUnicodeObject *)op)._base.wstr_length +#define PY_UNICODE_OBJECT_READY(op) (*(PyUnicodeObject *)op)._base._base.state.ready + StrType::StrType(PyObject *object) : PyType(object) {} StrType::StrType(char *string) : PyType(Py_BuildValue("s", string)) {} +StrType::StrType(JSContext *cx, JSString *str) { + JSLinearString *lstr = JS_EnsureLinearString(cx, str); + JS::AutoCheckCannotGC nogc; + PyObject *p; + + size_t length = JS::GetLinearStringLength(lstr); + pyObject = PyUnicode_FromStringAndSize(NULL, length); + + Py_XINCREF(pyObject); + + // need to free memory malloc'd by PyUnicodeObject, otherwise when we change the pointer, + // python will eventually attempt to free our new pointer that was never malloc'd + free(PY_UNICODE_OBJECT_DATA_ANY(pyObject)); + + if (JS::LinearStringHasLatin1Chars(lstr)) { // latin1 spidermonkey, latin1 python + const JS::Latin1Char *chars = JS::GetLatin1LinearStringChars(nogc, lstr); + + PY_UNICODE_OBJECT_DATA_ANY(pyObject) = (void *)chars; + PY_UNICODE_OBJECT_KIND(pyObject) = PyUnicode_1BYTE_KIND; + PY_UNICODE_OBJECT_LENGTH(pyObject) = length; + PY_UNICODE_OBJECT_WSTR(pyObject) = NULL; + PY_UNICODE_OBJECT_WSTR_LENGTH(pyObject) = 0; + PY_UNICODE_OBJECT_READY(pyObject) = 1; + } + else { // utf16 spidermonkey, ucs2 python + const char16_t *chars = JS::GetTwoByteLinearStringChars(nogc, lstr); + + PY_UNICODE_OBJECT_DATA_ANY(pyObject) = (void *)chars; + PY_UNICODE_OBJECT_KIND(pyObject) = PyUnicode_2BYTE_KIND; + PY_UNICODE_OBJECT_LENGTH(pyObject) = length; + + // python unicode objects take advantage of a possible performance gain on systems where + // sizeof(wchar_t) == 2, i.e. Windows systems if the string is using UCS2 encoding by setting the + // wstr pointer to point to the same data as the data.any pointer. + // On systems where sizeof(wchar_t) == 4, i.e. Unixy systems, a similar performance gain happens if the + // string is using UCS4 encoding [this is automatically handled by asUCS4()] + if (sizeof(wchar_t) == 2) { + PY_UNICODE_OBJECT_WSTR(pyObject) = (wchar_t *)chars; + PY_UNICODE_OBJECT_WSTR_LENGTH(pyObject) = length; + } + else { + PY_UNICODE_OBJECT_WSTR(pyObject) = NULL; + PY_UNICODE_OBJECT_WSTR_LENGTH(pyObject) = 0; + } + PY_UNICODE_OBJECT_READY(pyObject) = 1; + } +} + const char *StrType::getValue() const { return PyUnicode_AsUTF8(pyObject); } +bool StrType::containsSurrogatePair() { + if (PY_UNICODE_OBJECT_KIND(pyObject) != PyUnicode_2BYTE_KIND) { // if the string is not UCS2-encoded + return false; + } + + Py_UCS2 *chars = PY_UNICODE_OBJECT_DATA_UCS2(pyObject); + ssize_t length = PY_UNICODE_OBJECT_LENGTH(pyObject); + + for (size_t i = 0; i < length; i++) { + if (chars[i] >= HIGH_SURROGATE_START && chars[i] <= HIGH_SURROGATE_END && chars[i+1] >= LOW_SURROGATE_START && chars[i+1] <= LOW_SURROGATE_END) { + return true; + } + return false; + } +} + +PyObject *StrType::asUCS4() { + uint16_t *chars = PY_UNICODE_OBJECT_DATA_UCS2(pyObject); + ssize_t length = PY_UNICODE_OBJECT_LENGTH(pyObject); + + uint32_t ucs4String[length]; + size_t ucs4Length = 0; + + for (ssize_t i = 0; i < length; i++) { + if (chars[i] >= LOW_SURROGATE_START && chars[i] <= LOW_SURROGATE_END) // character is an unpaired low surrogate + { + char hexString[5]; + sprintf(hexString, "%x", (unsigned int)chars[i]); + std::string errorString = std::string("string contains an unpaired low surrogate at position: ") + std::to_string(i) + std::string(" with a value of 0x") + hexString; + PyErr_SetString(PyExc_UnicodeTranslateError, errorString.c_str()); + return NULL; + } + else if (chars[i] >= HIGH_SURROGATE_START && chars[i] <= HIGH_SURROGATE_END) { // character is a high surrogate + if ((i + 1 < length) && chars[i+1] >= LOW_SURROGATE_START && chars[i+1] <= LOW_SURROGATE_END) { // next character is a low surrogate + // see https://www.unicode.org/faq/utf_bom.html#utf16-3 for details + uint32_t X = (chars[i] & ((1 << 6) -1)) << 10 | chars[i+1] & ((1 << 10) -1); + uint32_t W = (chars[i] >> 6) & ((1 << 5) - 1); + uint32_t U = W+1; + ucs4String[ucs4Length] = U << 16 | X; + ucs4Length++; + i++; // skip over low surrogate + } + else { // next character is not a low surrogate + char hexString[5]; + sprintf(hexString, "%x", (unsigned int)chars[i]); + std::string errorString = std::string("string contains an unpaired high surrogate at position: ") + std::to_string(i) + std::string(" with a value of 0x") + hexString; + PyErr_SetString(PyExc_UnicodeTranslateError, errorString.c_str()); + return NULL; + } + } + else { // character is not a surrogate, and is in the BMP + ucs4String[ucs4Length] = chars[i]; + ucs4Length++; + } + } + + return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, ucs4String, ucs4Length); +} + void StrType::print(std::ostream &os) const { os << "'" << this->getValue() << "'"; } \ No newline at end of file diff --git a/src/modules/pythonmonkey/pythonmonkey.cc b/src/modules/pythonmonkey/pythonmonkey.cc index bd89e30f..0559207b 100644 --- a/src/modules/pythonmonkey/pythonmonkey.cc +++ b/src/modules/pythonmonkey/pythonmonkey.cc @@ -1,5 +1,6 @@ #include "include/modules/pythonmonkey/pythonmonkey.hh" +#include "include/PyType.hh" #include "include/StrType.hh" #include @@ -12,37 +13,118 @@ static JSContext *cx; /**< pointer to PythonMonkey's JSContext */ static JS::RootedObject *global; /**< pointer to the global object of PythonMonkey's JSContext */ +typedef std::unordered_map>::iterator PyToGCIterator; +std::unordered_map> PyTypeToGCThing; /**< data structure to hold memoized PyObject & GCThing data for handling GC*/ + +void handleSharedPythonMonkeyMemory(JSContext *cx, JSGCStatus status, JS::GCReason reason, void *data) { + if (status == JSGCStatus::JSGC_BEGIN) { + PyToGCIterator pyIt = PyTypeToGCThing.begin(); + while (pyIt != PyTypeToGCThing.end()) { + // If the PyObject reference count is exactly 1, then the only reference to the object is the one + // we are holding, which means the object is ready to be freed. + if (PyObject_GC_IsFinalized(pyIt->first->getPyObject()) || pyIt->first->getPyObject()->ob_refcnt == 1) { + for (JS::PersistentRootedValue *rval: pyIt->second) { // for each related GCThing + bool found = false; + for (PyToGCIterator innerPyIt = PyTypeToGCThing.begin(); innerPyIt != PyTypeToGCThing.end(); innerPyIt++) { // for each other PyType pointer + if (innerPyIt != pyIt && std::find(innerPyIt->second.begin(), innerPyIt->second.end(), rval) != innerPyIt->second.end()) { // if the PyType is also related to the GCThing + found = true; + break; + } + } + // if this PyObject is the last PyObject that references this GCThing, then the GCThing can also be free'd + if (!found) { + delete rval; + } + } + pyIt = PyTypeToGCThing.erase(pyIt); + } + else { + pyIt++; + } + } + } +}; + static void cleanup() { JS_DestroyContext(cx); JS_ShutDown(); delete global; } +static void memoizePyTypeAndGCThing(PyType *pyType, JS::PersistentRootedValue *GCThing) { + PyToGCIterator pyIt = PyTypeToGCThing.find(pyType); + + if (pyIt == PyTypeToGCThing.end()) { // if the PythonObject is not memoized + std::vector gcVector( + {{GCThing}}); + PyTypeToGCThing.insert({{pyType, gcVector}}); + } + else { + pyIt->second.push_back(GCThing); + } +} + +static PyObject *collect(PyObject *self, PyObject *args) { + JS_GC(cx); + Py_RETURN_NONE; +} + +static PyObject *asUCS4(PyObject *self, PyObject *args) { + StrType *str = new StrType(PyTuple_GetItem(args, 0)); + if (!PyUnicode_Check(str->getPyObject())) { + PyErr_SetString(PyExc_TypeError, "pythonmonkey.asUCS4 expects a string as its first argument"); + return NULL; + } + + return str->asUCS4(); +} + static PyObject *eval(PyObject *self, PyObject *args) { StrType *code = new StrType(PyTuple_GetItem(args, 0)); + if (!PyUnicode_Check(code->getPyObject())) { + PyErr_SetString(PyExc_TypeError, "pythonmonkey.eval expects a string as its first argument"); + return NULL; + } JSAutoRealm ar(cx, *global); JS::CompileOptions options (cx); options.setFileAndLine("noname", 1); + // initialize JS context JS::SourceText source; if (!source.init(cx, code->getValue(), strlen(code->getValue()), JS::SourceOwnership::Borrowed)) { PyErr_SetString(PyExc_RuntimeError, "Spidermonkey could not initialize with given JS code."); return NULL; } - JS::RootedValue rval(cx); - if (!JS::Evaluate(cx, options, source, &rval)) { + // evaluate source code + JS::PersistentRootedValue *rval = new JS::PersistentRootedValue(cx); + if (!JS::Evaluate(cx, options, source, rval)) { PyErr_SetString(PyExc_RuntimeError, "Spidermonkey could not evaluate the given JS code."); return NULL; } - Py_RETURN_NONE; + // translate to the proper python type + PyType *returnValue = NULL; + if (rval->isString()) { + returnValue = new StrType(cx, rval->toString()); + } + + memoizePyTypeAndGCThing(returnValue, rval); + + if (returnValue) { + return returnValue->getPyObject(); + } + else { + Py_RETURN_NONE; + } } static PyMethodDef PythonMonkeyMethods[] = { {"eval", eval, METH_VARARGS, "Javascript evaluator in Python"}, + {"collect", collect, METH_VARARGS, "Calls the spidermonkey garbage collector"}, + {"asUCS4", asUCS4, METH_VARARGS, "Expects a python string in UTF16 encoding, and returns a new equivalent string in UCS4. Undefined behaviour if the string is not in UTF16."}, {NULL, NULL, 0, NULL} }; @@ -74,5 +156,6 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void) return NULL; Py_AtExit(cleanup); + JS_SetGCCallback(cx, handleSharedPythonMonkeyMemory, NULL); return PyModule_Create(&pythonmonkey); } \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7c533d65..cf9faad7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,9 +14,9 @@ enable_testing() add_executable(tests ${TEST_FILES} ${SOURCE_FILES}) -target_include_directories(tests PRIVATE ${PYTHON_INCLUDE_DIR}) +target_include_directories(tests PRIVATE ${PYTHON_INCLUDE_DIR} ${SPIDERMONKEY_INCLUDE_DIR}) target_include_directories(tests PRIVATE ../include/) -target_link_libraries(tests PRIVATE explore gtest_main ${PYTHON_LIBRARIES}) +target_link_libraries(tests PRIVATE explore pythonmonkey gtest_main ${PYTHON_LIBRARIES} ${SPIDERMONKEY_LIBRARIES}) include(GoogleTest) gtest_discover_tests(tests) diff --git a/tests/python/test_pythonmonkey.py b/tests/python/test_pythonmonkey.py new file mode 100644 index 00000000..bbf8ed63 --- /dev/null +++ b/tests/python/test_pythonmonkey.py @@ -0,0 +1,139 @@ +import pythonmonkey as pm +import gc +import ctypes +import random + +_decref = ctypes.pythonapi.Py_DecRef +_decref.argtypes = [ctypes.py_object] +_decref.restype = None + +def test_passes(): + assert True + +def test_eval_ascii_string_matches_evaluated_string(): + py_ascii_string = "abc" + js_ascii_string = pm.eval(repr(py_ascii_string)) + assert py_ascii_string == js_ascii_string + +def test_eval_latin1_string_matches_evaluated_string(): + py_latin1_string = "a©Ð" + js_latin1_string = pm.eval(repr(py_latin1_string)) + assert py_latin1_string == js_latin1_string + +def test_eval_null_character_string_matches_evaluated_string(): + py_null_character_string = "a\x00©" + js_null_character_string = pm.eval(repr(py_null_character_string)) + assert py_null_character_string == js_null_character_string + +def test_eval_ucs2_string_matches_evaluated_string(): + py_ucs2_string = "ՄԸՋ" + js_ucs2_string = pm.eval(repr(py_ucs2_string)) + assert py_ucs2_string == js_ucs2_string + +def test_eval_unpaired_surrogate_string_matches_evaluated_string(): + py_unpaired_surrogate_string = "Ջ©\ud8fe" + js_unpaired_surrogate_string = pm.eval(repr(py_unpaired_surrogate_string)) + assert py_unpaired_surrogate_string == js_unpaired_surrogate_string + +def test_eval_ucs4_string_matches_evaluated_string(): + py_ucs4_string = "🀄🀛🜢" + js_utf16_string = pm.eval(repr(py_ucs4_string)) + js_ucs4_string = pm.asUCS4(js_utf16_string) + assert py_ucs4_string == js_ucs4_string + +def test_eval_latin1_string_fuzztest(): + n = 10 + for i in range(n): + length = random.randint(0x0000, 0xFFFF) + string1 = '' + + for i in range(length): + codepoint = random.randint(0x00, 0xFF) + string1 += chr(codepoint) # add random chr in latin1 range + + + INITIAL_STRING = string1 + m = 10 + for j in range(m): + string2 = pm.eval(repr(string1)) + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + gc.collect() + pm.collect() + + #garbage collection should not collect variables still in scope + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + string1 = string2 + assert INITIAL_STRING == string1 #strings should still match after a bunch of iterations through JS + +def test_eval_ucs2_string_fuzztest(): + n = 10 + for i in range(n): + length = random.randint(0x0000, 0xFFFF) + string1 = '' + + for i in range(length): + codepoint = random.randint(0x00, 0xFFFF) + string1 += chr(codepoint) # add random chr in ucs2 range + + + INITIAL_STRING = string1 + m = 10 + for j in range(m): + string2 = pm.eval(repr(string1)) + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + gc.collect() + pm.collect() + + #garbage collection should not collect variables still in scope + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + string1 = string2 + assert INITIAL_STRING == string1 #strings should still match after a bunch of iterations through JS + +def test_eval_ucs4_string_fuzztest(): + n = 10 + for i in range(n): + length = random.randint(0x0000, 0xFFFF) + string1 = '' + + for i in range(length): + codepoint = random.randint(0x010000, 0x10FFFF) + string1 += chr(codepoint) # add random chr outside BMP + + + INITIAL_STRING = string1 + m = 10 + for j in range(m): + utf16_string2 = pm.eval("'" + string1 + "'") + string2 = pm.asUCS4(utf16_string2) + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + gc.collect() + pm.collect() + + #garbage collection should not collect variables still in scope + assert len(string1) == length + assert len(string2) == length + assert len(string1) == len(string2) + assert string1 == string2 + + string1 = string2 + assert INITIAL_STRING == string1 #strings should still match after a bunch of iterations through JS \ No newline at end of file