From 9ca0d089a669f3f3e63f58f329d354055917700b Mon Sep 17 00:00:00 2001 From: Younguk Kim Date: Tue, 10 Apr 2018 19:45:54 +0900 Subject: [PATCH 01/11] event: fix a crash when removing a task is not owned by user --- panda/src/event/asyncTaskChain.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/panda/src/event/asyncTaskChain.cxx b/panda/src/event/asyncTaskChain.cxx index 47299d31ef..c7d732eca9 100644 --- a/panda/src/event/asyncTaskChain.cxx +++ b/panda/src/event/asyncTaskChain.cxx @@ -477,6 +477,7 @@ do_remove(AsyncTask *task, bool upon_death) { { int index = find_task_on_heap(_sleeping, task); nassertr(index != -1, false); + PT(AsyncTask) hold_task = task; _sleeping.erase(_sleeping.begin() + index); make_heap(_sleeping.begin(), _sleeping.end(), AsyncTaskSortWakeTime()); cleanup_task(task, upon_death, false); @@ -486,6 +487,7 @@ do_remove(AsyncTask *task, bool upon_death) { case AsyncTask::S_active: { // Active, but not being serviced, easy. + PT(AsyncTask) hold_task = task; int index = find_task_on_heap(_active, task); if (index != -1) { _active.erase(_active.begin() + index); @@ -769,7 +771,6 @@ cleanup_task(AsyncTask *task, bool upon_death, bool clean_exit) { } nassertv(task->_chain == this); - PT(AsyncTask) hold_task = task; task->_state = AsyncTask::S_inactive; task->_chain = nullptr; From 607af9ff4963db086b1233e195db45f7aeda68c1 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 10 Apr 2018 15:29:43 +0200 Subject: [PATCH 02/11] flt: fix compile error on macOS 10.6 (which has no strnlen) --- pandatool/src/flt/fltBeadID.cxx | 4 ++-- pandatool/src/flt/fltRecord.cxx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pandatool/src/flt/fltBeadID.cxx b/pandatool/src/flt/fltBeadID.cxx index f55d3e88cc..32348d583e 100644 --- a/pandatool/src/flt/fltBeadID.cxx +++ b/pandatool/src/flt/fltBeadID.cxx @@ -78,8 +78,8 @@ extract_record(FltRecordReader &reader) { bool FltBeadID:: extract_ancillary(FltRecordReader &reader) { if (reader.get_opcode() == FO_long_id) { - vector_uchar s = reader.get_iterator().get_remaining_bytes(); - _id.assign((const char *)s.data(), strnlen((const char *)s.data(), s.size())); + DatagramIterator &di = reader.get_iterator(); + _id = di.get_fixed_string(di.get_remaining_size()); return true; } diff --git a/pandatool/src/flt/fltRecord.cxx b/pandatool/src/flt/fltRecord.cxx index a0af83e180..53844f0636 100644 --- a/pandatool/src/flt/fltRecord.cxx +++ b/pandatool/src/flt/fltRecord.cxx @@ -621,8 +621,8 @@ extract_record(FltRecordReader &) { bool FltRecord:: extract_ancillary(FltRecordReader &reader) { if (reader.get_opcode() == FO_comment) { - vector_uchar s = reader.get_iterator().get_remaining_bytes(); - _comment.assign((const char *)s.data(), strnlen((const char *)s.data(), s.size())); + DatagramIterator &di = reader.get_iterator(); + _comment = di.get_fixed_string(di.get_remaining_size()); return true; } From e1675f1a539c22fdb3c5ebdacaa6a5c0fb383152 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 10 Apr 2018 15:31:48 +0200 Subject: [PATCH 03/11] makepanda: fix compile error on Windows with some versions of flex --- makepanda/makepanda.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index b772a4a9e7..3f597f90b7 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -552,6 +552,7 @@ if (COMPILER == "MSVC"): PkgDisable("EGL") PkgDisable("CARBON") PkgDisable("COCOA") + DefSymbol("FLEX", "YY_NO_UNISTD_H") if (PkgSkip("PYTHON")==0): IncDirectory("ALWAYS", SDK["PYTHON"] + "/include") LibDirectory("ALWAYS", SDK["PYTHON"] + "/libs") @@ -1396,7 +1397,7 @@ def CompileBison(wobj, wsrc, opts): CopyFile(wdsth, GetOutputDir()+"/tmp/"+ifile+".h") # Finally, compile the generated source file. - CompileCxx(wobj,wdstc,opts) + CompileCxx(wobj, wdstc, opts + ["FLEX"]) ######################################################################## ## From 0cef19fd900977bd67eb7ed17200a62aa317f28c Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 11 Apr 2018 17:29:49 +0200 Subject: [PATCH 04/11] putil: more validation in DatagramInputFile::get_datagram Intended to fix test_file_corrupt for 32-bit platforms. See discussion in 89be2c19af74c62b57961469c779b324c69979f1 --- panda/src/putil/datagramInputFile.cxx | 28 ++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/panda/src/putil/datagramInputFile.cxx b/panda/src/putil/datagramInputFile.cxx index f3bd8a081e..f81381d02e 100644 --- a/panda/src/putil/datagramInputFile.cxx +++ b/panda/src/putil/datagramInputFile.cxx @@ -138,35 +138,45 @@ get_datagram(Datagram &data) { return true; } - streamsize num_bytes = (streamsize)num_bytes_32; + size_t num_bytes = (size_t)num_bytes_32; if (num_bytes_32 == (uint32_t)-1) { // Another special case for a value larger than 32 bits. - num_bytes = reader.get_uint64(); - } + uint64_t num_bytes_64 = reader.get_uint64(); - // Make sure we have a reasonable datagram size for putting into memory. - nassertr(num_bytes == (size_t)num_bytes, false); + if (_in->fail() || _in->eof()) { + _error = true; + return false; + } + + num_bytes = (size_t)num_bytes_64; + + // Make sure we have a reasonable datagram size for putting into memory. + if (num_bytes_64 != (uint64_t)num_bytes) { + _error = true; + return false; + } + } // Now, read the datagram itself. We construct an empty datagram, use // pad_bytes to make it big enough, and read *directly* into the datagram's // internal buffer. Doing this saves us a copy operation. data = Datagram(); - streamsize bytes_read = 0; + size_t bytes_read = 0; while (bytes_read < num_bytes) { - streamsize bytes_left = num_bytes - bytes_read; + size_t bytes_left = num_bytes - bytes_read; // Hold up a second - datagrams >4MB are pretty large by bam/network // standards. Let's take it 4MB at a time just in case the length is // corrupt, so we don't allocate potentially a few GBs of RAM only to // find a truncated file. - bytes_left = min(bytes_left, (streamsize)4*1024*1024); + bytes_left = min(bytes_left, (size_t)4*1024*1024); PTA_uchar buffer = data.modify_array(); buffer.resize(buffer.size() + bytes_left); unsigned char *ptr = &buffer.p()[bytes_read]; - _in->read((char *)ptr, bytes_left); + _in->read((char *)ptr, (streamsize)bytes_left); if (_in->fail() || _in->eof()) { _error = true; return false; From f979c4d3121ded6bc0ea3f2cfea68c5d686ab137 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 11 Apr 2018 17:41:16 +0200 Subject: [PATCH 05/11] makepanda: don't use pkg-config for assimp It seems that they removed the -I option, which we need to build with. This commit is intended to fix the Ubuntu Bionic build. --- makepanda/makepanda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 3f597f90b7..5ae064b649 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -828,7 +828,7 @@ if (COMPILER=="GCC"): SmartPkgEnable("EIGEN", "eigen3", (), ("Eigen/Dense",), target_pkg = 'ALWAYS') SmartPkgEnable("ARTOOLKIT", "", ("AR"), "AR/ar.h") SmartPkgEnable("FCOLLADA", "", ChooseLib(fcollada_libs, "FCOLLADA"), ("FCollada", "FCollada/FCollada.h")) - SmartPkgEnable("ASSIMP", "assimp", ("assimp"), "assimp") + SmartPkgEnable("ASSIMP", "", ("assimp"), "assimp") SmartPkgEnable("FFMPEG", ffmpeg_libs, ffmpeg_libs, ("libavformat/avformat.h", "libavcodec/avcodec.h", "libavutil/avutil.h")) SmartPkgEnable("SWSCALE", "libswscale", "libswscale", ("libswscale/swscale.h"), target_pkg = "FFMPEG", thirdparty_dir = "ffmpeg") SmartPkgEnable("SWRESAMPLE","libswresample", "libswresample", ("libswresample/swresample.h"), target_pkg = "FFMPEG", thirdparty_dir = "ffmpeg") From 4877e8350d58cafa42f12fe026c795656a4984a3 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 18 Apr 2018 18:25:29 +0200 Subject: [PATCH 06/11] tests: fix unit test on 64-bit Ubuntu due to float imprecision [skip ci] --- tests/bullet/test_bullet_bam.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bullet/test_bullet_bam.py b/tests/bullet/test_bullet_bam.py index c6b9d2c282..11523a4f51 100644 --- a/tests/bullet/test_bullet_bam.py +++ b/tests/bullet/test_bullet_bam.py @@ -88,8 +88,8 @@ def test_minkowski_sum_shape(): assert type(shape) is type(shape2) assert shape.margin == shape2.margin assert shape.name == shape2.name - assert shape.transform_a.compare_to(shape2.transform_a, True) == 0 - assert shape.transform_b.compare_to(shape2.transform_b, True) == 0 + assert shape.transform_a.mat.compare_to(shape2.transform_a.mat) == 0 + assert shape.transform_b.mat.compare_to(shape2.transform_b.mat) == 0 assert type(shape.shape_a) == type(shape2.shape_a) assert type(shape.shape_b) == type(shape2.shape_b) From fc172f89183ca23b64ae4bbc0f35978f82ab5082 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 20 Apr 2018 12:48:26 +0200 Subject: [PATCH 07/11] tests: fix unit test on 32-bit Ubuntu due to float imprecision --- tests/bullet/test_bullet_bam.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bullet/test_bullet_bam.py b/tests/bullet/test_bullet_bam.py index 11523a4f51..e22aa57ba2 100644 --- a/tests/bullet/test_bullet_bam.py +++ b/tests/bullet/test_bullet_bam.py @@ -88,8 +88,8 @@ def test_minkowski_sum_shape(): assert type(shape) is type(shape2) assert shape.margin == shape2.margin assert shape.name == shape2.name - assert shape.transform_a.mat.compare_to(shape2.transform_a.mat) == 0 - assert shape.transform_b.mat.compare_to(shape2.transform_b.mat) == 0 + assert shape.transform_a.mat.compare_to(shape2.transform_a.mat, 0.001) == 0 + assert shape.transform_b.mat.compare_to(shape2.transform_b.mat, 0.001) == 0 assert type(shape.shape_a) == type(shape2.shape_a) assert type(shape.shape_b) == type(shape2.shape_b) From e6e39cce0e9140de96febc3aba3d709cdffe8de1 Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 20 Apr 2018 12:48:57 +0200 Subject: [PATCH 08/11] bullet: fix deadlock in get_manifold --- panda/src/bullet/bulletWorld.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda/src/bullet/bulletWorld.cxx b/panda/src/bullet/bulletWorld.cxx index 9bfa707e9d..9732760460 100644 --- a/panda/src/bullet/bulletWorld.cxx +++ b/panda/src/bullet/bulletWorld.cxx @@ -1052,7 +1052,7 @@ BulletPersistentManifold *BulletWorld:: get_manifold(int idx) const { LightMutexHolder holder(get_global_lock()); - nassertr(idx < get_num_manifolds(), NULL); + nassertr(idx < _dispatcher->getNumManifolds(), NULL); btPersistentManifold *ptr = _dispatcher->getManifoldByIndexInternal(idx); return (ptr) ? new BulletPersistentManifold(ptr) : NULL; From 1ac351d594e559b5fa20bcdacb6b939b1b1ac8c0 Mon Sep 17 00:00:00 2001 From: deflected Date: Fri, 20 Apr 2018 21:25:41 +0300 Subject: [PATCH 09/11] bullet: Allow intractions with bullet world during tick callbacks - Release the global lock during the user tick callback and allow interactions with bullet world - Acquire the lock again after the callback Signed-off-by: deflected --- panda/src/bullet/bulletWorld.cxx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/panda/src/bullet/bulletWorld.cxx b/panda/src/bullet/bulletWorld.cxx index 9732760460..6098759581 100644 --- a/panda/src/bullet/bulletWorld.cxx +++ b/panda/src/bullet/bulletWorld.cxx @@ -1186,7 +1186,12 @@ tick_callback(btDynamicsWorld *world, btScalar timestep) { CallbackObject *obj = w->_tick_callback_obj; if (obj) { BulletTickCallbackData cbdata(timestep); + // Release the global lock that we are holding during the tick callback + // and allow interactions with bullet world in the user callback + get_global_lock().release(); obj->do_callback(&cbdata); + // Acquire the global lock again and protect the execution + get_global_lock().acquire(); } } From 574000aedd9c4ef48f4a22b5cab52e373ba12c86 Mon Sep 17 00:00:00 2001 From: Donny Lawrence Date: Fri, 20 Apr 2018 22:25:01 -0500 Subject: [PATCH 10/11] Add getter and setter for Content-Type header Fixes #296 Closes #305 --- panda/src/downloader/httpChannel.I | 17 +++++++++++++++++ panda/src/downloader/httpChannel.cxx | 3 ++- panda/src/downloader/httpChannel.h | 4 ++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/panda/src/downloader/httpChannel.I b/panda/src/downloader/httpChannel.I index ccc30d504d..1a254e478a 100644 --- a/panda/src/downloader/httpChannel.I +++ b/panda/src/downloader/httpChannel.I @@ -426,6 +426,23 @@ get_max_updates_per_second() const { return _max_updates_per_second; } +/** + * Specifies the Content-Type header, useful for applications that require + * different types of content, such as JSON. + */ +INLINE void HTTPChannel:: +set_content_type(string content_type) { + _content_type = content_type; +} + +/** + * Returns the value of the Content-Type header. + */ +INLINE string HTTPChannel:: +get_content_type() const { + return _content_type; +} + /** * This may be called immediately after a call to get_document() or some * related function to specify the expected size of the document we are diff --git a/panda/src/downloader/httpChannel.cxx b/panda/src/downloader/httpChannel.cxx index a7eb5f58d3..93e04f1432 100644 --- a/panda/src/downloader/httpChannel.cxx +++ b/panda/src/downloader/httpChannel.cxx @@ -100,6 +100,7 @@ HTTPChannel(HTTPClient *client) : _response_type = RT_none; _http_version = _client->get_http_version(); _http_version_string = _client->get_http_version_string(); + _content_type = "application/x-www-form-urlencoded"; _state = S_new; _done_state = S_new; _started_download = false; @@ -3624,7 +3625,7 @@ make_header() { if (!_body.empty()) { stream - << "Content-Type: application/x-www-form-urlencoded\r\n" + << "Content-Type: " << _content_type << "\r\n" << "Content-Length: " << _body.length() << "\r\n"; } diff --git a/panda/src/downloader/httpChannel.h b/panda/src/downloader/httpChannel.h index b9131eba9a..7de0e17e83 100644 --- a/panda/src/downloader/httpChannel.h +++ b/panda/src/downloader/httpChannel.h @@ -143,6 +143,9 @@ PUBLISHED: INLINE void set_max_updates_per_second(double max_updates_per_second); INLINE double get_max_updates_per_second() const; + INLINE void set_content_type(string content_type); + INLINE string get_content_type() const; + INLINE void set_expected_file_size(size_t file_size); streamsize get_file_size() const; INLINE bool is_file_size_known() const; @@ -336,6 +339,7 @@ private: string request_path; string _header; string _body; + string _content_type; bool _want_ssl; bool _proxy_serves_document; bool _proxy_tunnel_now; From f986f8de1b7734205da4d60cd43318fe2d3e24ec Mon Sep 17 00:00:00 2001 From: rdb Date: Sat, 21 Apr 2018 11:20:49 +0200 Subject: [PATCH 11/11] showbase: add back clampScalar and PriorityCallbacks to PythonUtil These were removed by 88dbb31daa02779405ef5326677eefb5cf93f3c8 under the assumption that they were not used, but it has recently come to my attention that there is still code out there that uses these. PriorityCallbacks has been updated to be compatible with Python 3 by only comparing the priority, rather than the (priority, callback) tuple. This also has the side-effect of ditching the bisect dependency. Also moves testing code from the source to the unit tests. --- direct/src/showbase/PythonUtil.py | 105 ++++++++++++++---------------- tests/showbase/test_PythonUtil.py | 105 ++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 57 deletions(-) create mode 100644 tests/showbase/test_PythonUtil.py diff --git a/direct/src/showbase/PythonUtil.py b/direct/src/showbase/PythonUtil.py index 0c850f51a0..2cd4e62d15 100644 --- a/direct/src/showbase/PythonUtil.py +++ b/direct/src/showbase/PythonUtil.py @@ -10,7 +10,7 @@ __all__ = ['indent', 'bound', 'clamp', 'lerp', 'average', 'addListsByValue', 'boolEqual', 'lineupPos', 'formatElapsedSeconds', 'solveQuadratic', 'findPythonModule', 'mostDerivedLast', -'weightedChoice', 'randFloat', 'normalDistrib', +'clampScalar', 'weightedChoice', 'randFloat', 'normalDistrib', 'weightedRand', 'randUint31', 'randInt32', 'SerialNumGen', 'serialNum', 'uniqueName', 'Enum', 'Singleton', 'SingletonError', 'printListEnum', 'safeRepr', @@ -178,27 +178,6 @@ class Queue: def __len__(self): return len(self.__list) -if __debug__ and __name__ == '__main__': - q = Queue() - assert q.isEmpty() - q.clear() - assert q.isEmpty() - q.push(10) - assert not q.isEmpty() - q.push(20) - assert not q.isEmpty() - assert len(q) == 2 - assert q.front() == 10 - assert q.back() == 20 - assert q.top() == 10 - assert q.top() == 10 - assert q.pop() == 10 - assert len(q) == 1 - assert not q.isEmpty() - assert q.pop() == 20 - assert len(q) == 0 - assert q.isEmpty() - def indent(stream, numIndents, str): """ @@ -1130,6 +1109,23 @@ def findPythonModule(module): return None +def clampScalar(value, a, b): + # calling this ought to be faster than calling both min and max + if a < b: + if value < a: + return a + elif value > b: + return b + else: + return value + else: + if value < b: + return b + elif value > a: + return a + else: + return value + def weightedChoice(choiceList, rng=random.random, sum=None): """given a list of (weight, item) pairs, chooses an item based on the weights. rng must return 0..1. if you happen to have the sum of the @@ -2313,36 +2309,6 @@ def flywheel(*args, **kArgs): pass return flywheel -if __debug__ and __name__ == '__main__': - f = flywheel(['a','b','c','d'], countList=[11,20,3,4]) - obj2count = {} - for obj in f: - obj2count.setdefault(obj, 0) - obj2count[obj] += 1 - assert obj2count['a'] == 11 - assert obj2count['b'] == 20 - assert obj2count['c'] == 3 - assert obj2count['d'] == 4 - - f = flywheel([1,2,3,4], countFunc=lambda x: x*2) - obj2count = {} - for obj in f: - obj2count.setdefault(obj, 0) - obj2count[obj] += 1 - assert obj2count[1] == 2 - assert obj2count[2] == 4 - assert obj2count[3] == 6 - assert obj2count[4] == 8 - - f = flywheel([1,2,3,4], countFunc=lambda x: x, scale = 3) - obj2count = {} - for obj in f: - obj2count.setdefault(obj, 0) - obj2count[obj] += 1 - assert obj2count[1] == 1 * 3 - assert obj2count[2] == 2 * 3 - assert obj2count[3] == 3 * 3 - assert obj2count[4] == 4 * 3 if __debug__: def quickProfile(name="unnamed"): @@ -2687,11 +2653,36 @@ def unescapeHtmlString(s): result += char return result -if __debug__ and __name__ == '__main__': - assert unescapeHtmlString('asdf') == 'asdf' - assert unescapeHtmlString('as+df') == 'as df' - assert unescapeHtmlString('as%32df') == 'as2df' - assert unescapeHtmlString('asdf%32') == 'asdf2' +class PriorityCallbacks: + """ manage a set of prioritized callbacks, and allow them to be invoked in order of priority """ + def __init__(self): + self._callbacks = [] + + def clear(self): + del self._callbacks[:] + + def add(self, callback, priority=None): + if priority is None: + priority = 0 + callbacks = self._callbacks + lo = 0 + hi = len(callbacks) + while lo < hi: + mid = (lo + hi) // 2 + if priority < callbacks[mid][0]: + hi = mid + else: + lo = mid + 1 + item = (priority, callback) + callbacks.insert(lo, item) + return item + + def remove(self, item): + self._callbacks.remove(item) + + def __call__(self): + for priority, callback in self._callbacks: + callback() builtins.Functor = Functor builtins.Stack = Stack diff --git a/tests/showbase/test_PythonUtil.py b/tests/showbase/test_PythonUtil.py new file mode 100644 index 0000000000..faf5da269f --- /dev/null +++ b/tests/showbase/test_PythonUtil.py @@ -0,0 +1,105 @@ +from direct.showbase import PythonUtil + + +def test_queue(): + q = PythonUtil.Queue() + assert q.isEmpty() + q.clear() + assert q.isEmpty() + q.push(10) + assert not q.isEmpty() + q.push(20) + assert not q.isEmpty() + assert len(q) == 2 + assert q.front() == 10 + assert q.back() == 20 + assert q.top() == 10 + assert q.top() == 10 + assert q.pop() == 10 + assert len(q) == 1 + assert not q.isEmpty() + assert q.pop() == 20 + assert len(q) == 0 + assert q.isEmpty() + + +def test_flywheel(): + f = PythonUtil.flywheel(['a','b','c','d'], countList=[11,20,3,4]) + obj2count = {} + for obj in f: + obj2count.setdefault(obj, 0) + obj2count[obj] += 1 + assert obj2count['a'] == 11 + assert obj2count['b'] == 20 + assert obj2count['c'] == 3 + assert obj2count['d'] == 4 + + f = PythonUtil.flywheel([1,2,3,4], countFunc=lambda x: x*2) + obj2count = {} + for obj in f: + obj2count.setdefault(obj, 0) + obj2count[obj] += 1 + assert obj2count[1] == 2 + assert obj2count[2] == 4 + assert obj2count[3] == 6 + assert obj2count[4] == 8 + + f = PythonUtil.flywheel([1,2,3,4], countFunc=lambda x: x, scale = 3) + obj2count = {} + for obj in f: + obj2count.setdefault(obj, 0) + obj2count[obj] += 1 + assert obj2count[1] == 1 * 3 + assert obj2count[2] == 2 * 3 + assert obj2count[3] == 3 * 3 + assert obj2count[4] == 4 * 3 + + +def test_unescape_html_string(): + assert PythonUtil.unescapeHtmlString('asdf') == 'asdf' + assert PythonUtil.unescapeHtmlString('as+df') == 'as df' + assert PythonUtil.unescapeHtmlString('as%32df') == 'as2df' + assert PythonUtil.unescapeHtmlString('asdf%32') == 'asdf2' + + +def test_priority_callbacks(): + l = [] + def a(l=l): + l.append('a') + def b(l=l): + l.append('b') + def c(l=l): + l.append('c') + + pc = PythonUtil.PriorityCallbacks() + pc.add(a) + pc() + assert l == ['a'] + + del l[:] + bItem = pc.add(b) + pc() + assert 'a' in l + assert 'b' in l + assert len(l) == 2 + + del l[:] + pc.remove(bItem) + pc() + assert l == ['a'] + + del l[:] + pc.add(c, 2) + bItem = pc.add(b, 10) + pc() + assert l == ['a', 'c', 'b'] + + del l[:] + pc.remove(bItem) + pc() + assert l == ['a', 'c'] + + del l[:] + pc.clear() + pc() + assert len(l) == 0