[gjs/wip/3v1n0/toggle-queue-tests] tests: Add unit tests for ToggleQueue and ObjectInstance usage of it




commit 471d8c36700688f10213bc7cf979bf05f1b81fff
Author: Marco Trevisan (TreviƱo) <mail 3v1n0 net>
Date:   Sat May 1 22:31:19 2021 +0200

    tests: Add unit tests for ToggleQueue and ObjectInstance usage of it
    
    Instead of relying only on (at times unpredictible) JS tests for toggle
    queue handling and Objects memory management, add unit tests in which we
    manually handle an ObjectInstance and we simulate toggle events on it.

 gi/object.h                                    |   1 +
 gi/toggle.h                                    |   1 +
 installed-tests/js/libgjstesttools/meson.build |   3 +
 meson.build                                    |  39 +-
 test/gjs-test-toggle-queue.cpp                 | 687 +++++++++++++++++++++++++
 test/gjs-test-utils.h                          |   2 +
 test/gjs-tests-internal.cpp                    |  23 +
 test/meson.build                               |  30 +-
 8 files changed, 768 insertions(+), 18 deletions(-)
---
diff --git a/gi/object.h b/gi/object.h
index abe2655b..fdd55b49 100644
--- a/gi/object.h
+++ b/gi/object.h
@@ -294,6 +294,7 @@ class ObjectInstance : public GIWrapperInstance<ObjectBase, ObjectPrototype,
                                    GObject>;
     friend class GIWrapperBase<ObjectBase, ObjectPrototype, ObjectInstance>;
     friend class ObjectBase;  // for add_property, prop_getter, etc.
+    friend struct ObjectInstanceTest;
 
     // GIWrapperInstance::m_ptr may be null in ObjectInstance.
 
diff --git a/gi/toggle.h b/gi/toggle.h
index 17f22c06..89a1564e 100644
--- a/gi/toggle.h
+++ b/gi/toggle.h
@@ -31,6 +31,7 @@ public:
     using Handler = void (*)(ObjectInstance*, Direction);
 
  private:
+    friend struct ToggleQueueTest;
     struct Item {
         Item() {}
         Item(ObjectInstance* o, Direction d) : object(o), direction(d) {}
diff --git a/installed-tests/js/libgjstesttools/meson.build b/installed-tests/js/libgjstesttools/meson.build
index 036d7076..2e57483a 100644
--- a/installed-tests/js/libgjstesttools/meson.build
+++ b/installed-tests/js/libgjstesttools/meson.build
@@ -17,3 +17,6 @@ gjstest_tools_gir = gnome.generate_gir(libgjstesttools,
     install: get_option('installed_tests'), install_dir_gir: false,
     install_dir_typelib: installed_tests_execdir)
 gjstest_tools_typelib = gjstest_tools_gir[1]
+libgjstesttools_dep = declare_dependency(
+    link_with: libgjstesttools,
+    include_directories: include_directories('.'))
diff --git a/meson.build b/meson.build
index d71c57a6..3d3df512 100644
--- a/meson.build
+++ b/meson.build
@@ -495,12 +495,22 @@ if host_machine.system() == 'windows'
     libgjs_cpp_args += ['-DWIN32', '-DXP_WIN']
 endif
 
+gjs_dep = declare_dependency(
+    compile_args: libgjs_cpp_args,
+    dependencies: libgjs_dependencies,
+)
+
 libgjs_jsapi = static_library(meson.project_name() + '-jsapi',
     libgjs_jsapi_sources, probes_header, probes_objfile,
-    cpp_args: libgjs_cpp_args,
-    dependencies: libgjs_dependencies,
+    dependencies: gjs_dep,
     install: false)
 
+libgjs_private = static_library(meson.project_name() + '-private',
+    libgjs_sources, probes_header, probes_objfile,
+    dependencies: gjs_dep,
+    link_with: libgjs_jsapi,
+)
+
 link_args = []
 symbol_map = files('libgjs.map')
 symbol_list = files('libgjs.symbols')
@@ -512,12 +522,10 @@ link_args += cxx.get_supported_link_arguments([
 ])
 
 libgjs = shared_library(meson.project_name(),
-    libgjs_sources, libgjs_private_sources, module_resource_srcs,
-    probes_header, probes_objfile,
-    cpp_args: libgjs_cpp_args,
+    sources: [ libgjs_private_sources, module_resource_srcs ],
     link_args: link_args, link_depends: [symbol_map, symbol_list],
-    link_with: libgjs_jsapi,
-    dependencies: libgjs_dependencies,
+    link_whole: libgjs_private,
+    dependencies: gjs_dep,
     version: '0.0.0', soversion: '0',
     gnu_symbol_visibility: 'hidden',
     install: true)
@@ -526,7 +534,8 @@ install_headers(gjs_public_headers, subdir: api_name / 'gjs')
 
 # Allow using libgjs as a subproject
 libgjs_dep = declare_dependency(link_with: [libgjs, libgjs_jsapi],
-    dependencies: libgjs_dependencies, include_directories: top_include)
+    dependencies: gjs_dep, include_directories: top_include,
+)
 
 ### Build GjsPrivate introspection library #####################################
 
@@ -542,7 +551,6 @@ gjs_private_typelib = gjs_private_gir[1]
 gjs_console_srcs = ['gjs/console.cpp']
 
 gjs_console = executable('gjs-console', gjs_console_srcs,
-    cpp_args: libgjs_cpp_args,
     dependencies: libgjs_dep, install: true)
 
 meson.add_install_script('build/symlink-gjs.py', get_option('bindir'))
@@ -584,6 +592,7 @@ libgjs_test_tools_builddir = js_tests_builddir / 'libgjstesttools'
 tests_environment.set('TOP_BUILDDIR', meson.build_root())
 tests_environment.set('GJS_USE_UNINSTALLED_FILES', '1')
 tests_environment.set('GJS_PATH', '')
+tests_environment.set('GJS_DEBUG_OUTPUT', 'stderr')
 tests_environment.prepend('GI_TYPELIB_PATH', meson.current_build_dir(),
     js_tests_builddir, libgjs_test_tools_builddir)
 tests_environment.prepend('LD_LIBRARY_PATH', meson.current_build_dir(),
@@ -623,18 +632,18 @@ endif
 
 ### Tests and test setups ######################################################
 
-# Note: The test program in test/ needs to be ported
-#       to Windows before we can build it on Windows.
-if host_machine.system() != 'windows'
-    subdir('test')
-endif
-
 if not get_option('skip_gtk_tests')
     have_gtk4 = dependency('gtk4', required: false).found()
 endif
 
 subdir('installed-tests')
 
+# Note: The test program in test/ needs to be ported
+#       to Windows before we can build it on Windows.
+if host_machine.system() != 'windows'
+    subdir('test')
+endif
+
 valgrind_environment = environment()
 valgrind_environment.set('G_SLICE', 'always-malloc,debug-blocks')
 valgrind_environment.set('G_DEBUG',
diff --git a/test/gjs-test-toggle-queue.cpp b/test/gjs-test-toggle-queue.cpp
new file mode 100644
index 00000000..299621c0
--- /dev/null
+++ b/test/gjs-test-toggle-queue.cpp
@@ -0,0 +1,687 @@
+/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Canonical, Ltd.
+// SPDX-FileContributor: Marco Trevisan <marco trevisan canonical com>
+
+#include <config.h>
+
+#include <algorithm>  // for copy
+#include <atomic>
+#include <chrono>
+#include <deque>
+#include <memory>
+#include <thread>
+#include <tuple>    // for tie
+#include <utility>  // for pair
+
+#include <girepository.h>
+#include <glib-object.h>
+#include <glib.h>
+
+#include <js/GCAPI.h>  // for JS_GC
+#include <js/TypeDecls.h>
+
+#include "gi/object.h"
+#include "gi/toggle.h"
+#include "gjs/context.h"
+#include "gjs/jsapi-util.h"
+#include "installed-tests/js/libgjstesttools/gjs-test-tools.h"
+#include "test/gjs-test-utils.h"
+
+static GMutex s_gc_lock;
+static GCond s_gc_finished;
+static std::atomic_int s_gc_counter;
+static std::deque<std::pair<ObjectInstance*, ToggleQueue::Direction>>
+    s_toggle_history;
+
+struct ObjectInstanceTest : ObjectInstance {
+    using ObjectInstance::ensure_uses_toggle_ref;
+    using ObjectInstance::new_for_gobject;
+    using ObjectInstance::wrapper_is_rooted;
+};
+
+struct ToggleQueueTest {
+    static void reset_queue() {
+        auto tq = ToggleQueue::get_default();
+        tq->m_shutdown = false;
+        g_clear_handle_id(&tq->m_idle_id, g_source_remove);
+        tq->q.clear();
+    }
+    static decltype(ToggleQueue::q) queue() {
+        return ToggleQueue::get_default()->q;
+    }
+    static ToggleQueue::Handler handler() {
+        return ToggleQueue::get_default()->m_toggle_handler;
+    }
+};
+
+static void on_gc(JSContext*, JSGCStatus status, JS::GCReason, void*) {
+    if (status != JSGC_END)
+        return;
+
+    g_mutex_lock(&s_gc_lock);
+    s_gc_counter.fetch_add(1);
+    g_cond_broadcast(&s_gc_finished);
+    g_mutex_unlock(&s_gc_lock);
+}
+
+static void setup(GjsUnitTestFixture* fx, const void*) {
+    g_irepository_prepend_search_path(g_getenv("TOP_BUILDDIR"));
+    gjs_test_tools_init();
+    gjs_unit_test_fixture_setup(fx, nullptr);
+    JS_SetGCCallback(fx->cx, on_gc, fx);
+
+    GjsAutoError error;
+    int code;
+
+    const char* gi_initializer = "imports.gi;";
+    g_assert_true(gjs_context_eval(fx->gjs_context, gi_initializer, -1,
+                                   "<gjs-test-toggle>", &code, error.out()));
+    g_assert_no_error(error);
+}
+
+static void wait_for_gc(GjsUnitTestFixture* fx) {
+    int count = s_gc_counter.load();
+
+    JS_GC(fx->cx);
+
+    g_mutex_lock(&s_gc_lock);
+    while (count == s_gc_counter.load()) {
+        g_cond_wait(&s_gc_finished, &s_gc_lock);
+    }
+    g_mutex_unlock(&s_gc_lock);
+}
+
+static void teardown(GjsUnitTestFixture* fx, const void*) {
+    for (auto pair : s_toggle_history)
+        ToggleQueue::get_default()->cancel(pair.first);
+
+    s_toggle_history.clear();
+    gjs_unit_test_fixture_teardown(fx, nullptr);
+
+    g_assert_true(ToggleQueueTest::queue().empty());
+    ToggleQueueTest::reset_queue();
+    gjs_test_tools_reset();
+}
+
+static ObjectInstance* new_test_gobject(GjsUnitTestFixture* fx) {
+    GjsAutoUnref<GObject> gobject(
+        G_OBJECT(g_object_new(G_TYPE_OBJECT, nullptr)));
+    return ObjectInstanceTest::new_for_gobject(fx->cx, gobject);
+}
+
+static void wait_for(int interval) {
+    GjsAutoPointer<GMainLoop, GMainLoop, g_main_loop_unref> loop(
+        g_main_loop_new(nullptr, false));
+    g_timeout_add_full(
+        G_PRIORITY_LOW, interval,
+        [](void* data) {
+            g_main_loop_quit(static_cast<GMainLoop*>(data));
+            return G_SOURCE_REMOVE;
+        },
+        loop, nullptr);
+    g_main_loop_run(loop);
+}
+
+static void toggles_handler(ObjectInstance* object,
+                            ToggleQueue::Direction direction) {
+    s_toggle_history.emplace_back(object, direction);
+}
+
+static void test_toggle_queue_unlock_empty(GjsUnitTestFixture*, const void*) {
+    g_assert_true(ToggleQueue::get_default()->cancel(nullptr) ==
+                  std::make_pair(false, false));
+}
+
+static void test_toggle_queue_unlock_same_thread(GjsUnitTestFixture*,
+                                                 const void*) {
+    auto tq = ToggleQueue::get_default();
+    g_assert_true(tq->cancel(nullptr) == std::make_pair(false, false));
+    g_assert_true(ToggleQueue::get_default()->cancel(nullptr) ==
+                  std::make_pair(false, false));
+}
+
+static void test_toggle_blocks_other_thread(GjsUnitTestFixture*, const void*) {
+    struct LockedQueue {
+        decltype(ToggleQueue::get_default()) tq = ToggleQueue::get_default();
+    };
+
+    auto locked_queue = std::make_unique<LockedQueue>();
+    g_assert_true(locked_queue->tq->cancel(nullptr) ==
+                  std::make_pair(false, false));
+
+    std::atomic_bool other_thread_running(false);
+    std::atomic_bool accessed_from_other_thread(false);
+    auto th = std::thread([&accessed_from_other_thread, &other_thread_running] {
+        other_thread_running.store(true);
+        auto locked_queue = std::make_unique<LockedQueue>();
+        accessed_from_other_thread.store(true);
+        g_assert_true(ToggleQueue::get_default()->cancel(nullptr) ==
+                      std::make_pair(false, false));
+        other_thread_running = false;
+    });
+
+    while (!other_thread_running.load())
+        g_assert_false(accessed_from_other_thread.load());
+
+    std::this_thread::sleep_for(std::chrono::milliseconds(100));
+    g_assert_true(other_thread_running);
+    g_assert_false(accessed_from_other_thread);
+
+    auto other_queue = std::make_unique<LockedQueue>();
+    g_assert_true(other_queue->tq->cancel(nullptr) ==
+                  std::make_pair(false, false));
+
+    other_queue.reset();
+    std::this_thread::sleep_for(std::chrono::milliseconds(100));
+    g_assert_true(other_thread_running);
+    g_assert_false(accessed_from_other_thread);
+
+    // Ok, now other thread may get the lock...
+    locked_queue.reset();
+    while (!accessed_from_other_thread.load()) {
+    }
+    g_assert_true(accessed_from_other_thread);
+
+    // Can enter again from main thread!
+    th.join();
+    g_assert_false(other_thread_running);
+    g_assert_true(ToggleQueue::get_default()->cancel(nullptr) ==
+                  std::make_pair(false, false));
+}
+
+static void test_toggle_queue_empty(GjsUnitTestFixture*, const void*) {
+    auto tq = ToggleQueue::get_default();
+    tq->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+}
+
+static void test_toggle_queue_empty_cancel(GjsUnitTestFixture*, const void*) {
+    auto tq = ToggleQueue::get_default();
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(nullptr);
+    g_assert_false(toggle_down_queued);
+    g_assert_false(toggle_up_queued);
+}
+
+static void test_toggle_queue_enqueue_one(GjsUnitTestFixture* fx, const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto tq = ToggleQueue::get_default();
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+
+    tq->handle_all_toggles(toggles_handler);
+    g_assert_cmpuint(s_toggle_history.size(), ==, 1);
+    g_assert_true(s_toggle_history.front().first == instance);
+    g_assert_cmpuint(s_toggle_history.front().second, ==,
+                     ToggleQueue::Direction::UP);
+}
+
+static void test_toggle_queue_enqueue_one_cancel(GjsUnitTestFixture* fx,
+                                                 const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto tq = ToggleQueue::get_default();
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(instance);
+    g_assert_false(toggle_down_queued);
+    g_assert_true(toggle_up_queued);
+
+    tq->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+}
+
+static void test_toggle_queue_enqueue_many_equal(GjsUnitTestFixture* fx,
+                                                 const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto tq = ToggleQueue::get_default();
+    tq->enqueue(instance, ToggleQueue::Direction::DOWN, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::DOWN, toggles_handler);
+
+    tq->handle_all_toggles(toggles_handler);
+    g_assert_cmpuint(s_toggle_history.size(), ==, 0);
+
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(instance);
+    g_assert_false(toggle_down_queued);
+    g_assert_false(toggle_up_queued);
+}
+
+static void test_toggle_queue_enqueue_many_equal_cancel(GjsUnitTestFixture* fx,
+                                                        const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto tq = ToggleQueue::get_default();
+    tq->enqueue(instance, ToggleQueue::Direction::DOWN, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::DOWN, toggles_handler);
+
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(instance);
+    g_assert_false(toggle_down_queued);
+    g_assert_false(toggle_up_queued);
+}
+
+static void test_toggle_queue_enqueue_more_up(GjsUnitTestFixture* fx,
+                                              const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto tq = ToggleQueue::get_default();
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::DOWN, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::DOWN, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+
+    tq->handle_all_toggles(toggles_handler);
+    g_assert_cmpuint(s_toggle_history.size(), ==, 2);
+    g_assert_true(s_toggle_history.at(0).first == instance);
+    g_assert_cmpuint(s_toggle_history.at(0).second, ==,
+                     ToggleQueue::Direction::UP);
+    g_assert_true(s_toggle_history.at(1).first == instance);
+    g_assert_cmpuint(s_toggle_history.at(1).second, ==,
+                     ToggleQueue::Direction::UP);
+
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(instance);
+    g_assert_false(toggle_down_queued);
+    g_assert_false(toggle_up_queued);
+}
+
+static void test_toggle_queue_enqueue_only_up(GjsUnitTestFixture* fx,
+                                              const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto tq = ToggleQueue::get_default();
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+
+    tq->handle_all_toggles(toggles_handler);
+    g_assert_cmpuint(s_toggle_history.size(), ==, 4);
+    g_assert_true(s_toggle_history.at(0).first == instance);
+    g_assert_cmpuint(s_toggle_history.at(0).second, ==,
+                     ToggleQueue::Direction::UP);
+    g_assert_true(s_toggle_history.at(1).first == instance);
+    g_assert_cmpuint(s_toggle_history.at(1).second, ==,
+                     ToggleQueue::Direction::UP);
+    g_assert_true(s_toggle_history.at(2).first == instance);
+    g_assert_cmpuint(s_toggle_history.at(2).second, ==,
+                     ToggleQueue::Direction::UP);
+    g_assert_true(s_toggle_history.at(3).first == instance);
+    g_assert_cmpuint(s_toggle_history.at(3).second, ==,
+                     ToggleQueue::Direction::UP);
+
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(instance);
+    g_assert_false(toggle_down_queued);
+    g_assert_false(toggle_up_queued);
+}
+
+static void test_toggle_queue_handle_more_up(GjsUnitTestFixture* fx,
+                                             const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto tq = ToggleQueue::get_default();
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::DOWN, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::DOWN, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+
+    wait_for(50);
+
+    g_assert_cmpuint(s_toggle_history.size(), ==, 2);
+    g_assert_true(s_toggle_history.at(0).first == instance);
+    g_assert_cmpuint(s_toggle_history.at(0).second, ==,
+                     ToggleQueue::Direction::UP);
+    g_assert_true(s_toggle_history.at(1).first == instance);
+    g_assert_cmpuint(s_toggle_history.at(1).second, ==,
+                     ToggleQueue::Direction::UP);
+
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(instance);
+    g_assert_false(toggle_down_queued);
+    g_assert_false(toggle_up_queued);
+}
+
+static void test_toggle_queue_handle_only_up(GjsUnitTestFixture* fx,
+                                             const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto tq = ToggleQueue::get_default();
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+
+    wait_for(50);
+
+    g_assert_cmpuint(s_toggle_history.size(), ==, 4);
+    g_assert_true(s_toggle_history.at(0).first == instance);
+    g_assert_cmpuint(s_toggle_history.at(0).second, ==,
+                     ToggleQueue::Direction::UP);
+    g_assert_true(s_toggle_history.at(1).first == instance);
+    g_assert_cmpuint(s_toggle_history.at(1).second, ==,
+                     ToggleQueue::Direction::UP);
+    g_assert_true(s_toggle_history.at(2).first == instance);
+    g_assert_cmpuint(s_toggle_history.at(2).second, ==,
+                     ToggleQueue::Direction::UP);
+    g_assert_true(s_toggle_history.at(3).first == instance);
+    g_assert_cmpuint(s_toggle_history.at(3).second, ==,
+                     ToggleQueue::Direction::UP);
+
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(instance);
+    g_assert_false(toggle_down_queued);
+    g_assert_false(toggle_up_queued);
+}
+
+static void test_toggle_queue_enqueue_only_up_cancel(GjsUnitTestFixture* fx,
+                                                     const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto tq = ToggleQueue::get_default();
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+    tq->enqueue(instance, ToggleQueue::Direction::UP, toggles_handler);
+
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(instance);
+    g_assert_false(toggle_down_queued);
+    g_assert_true(toggle_up_queued);
+
+    tq->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+}
+
+void test_toggle_queue_object_from_main_thread(GjsUnitTestFixture* fx,
+                                               const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto tq = ToggleQueue::get_default();
+
+    reinterpret_cast<ObjectInstanceTest*>(instance)->ensure_uses_toggle_ref(
+        fx->cx);
+    GjsAutoUnref<GObject> reffed(instance->ptr(), GjsAutoTakeOwnership());
+
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(instance);
+    g_assert_false(toggle_down_queued);
+    g_assert_false(toggle_up_queued);
+
+    tq->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+}
+
+void test_toggle_queue_object_from_main_thread_already_enqueued(
+    GjsUnitTestFixture* fx, const void*) {
+    auto* instance = new_test_gobject(fx);
+    GjsAutoUnref<GObject> reffed;
+
+    reinterpret_cast<ObjectInstanceTest*>(instance)->ensure_uses_toggle_ref(
+        fx->cx);
+    reffed = instance->ptr();
+    gjs_test_tools_ref_other_thread(reffed);
+
+    g_assert_cmpuint(ToggleQueueTest::queue().size(), ==, 1);
+    g_assert_true(ToggleQueueTest::queue().at(0).object == instance);
+    g_assert_cmpuint(ToggleQueueTest::queue().at(0).direction, ==,
+                     ToggleQueue::Direction::UP);
+
+    auto tq = ToggleQueue::get_default();
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(instance);
+    g_assert_false(toggle_down_queued);
+    g_assert_true(toggle_up_queued);
+
+    tq->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+}
+
+void test_toggle_queue_object_from_main_thread_unref_already_enqueued(
+    GjsUnitTestFixture* fx, const void*) {
+    auto* instance = new_test_gobject(fx);
+    GjsAutoUnref<GObject> reffed;
+
+    reinterpret_cast<ObjectInstanceTest*>(instance)->ensure_uses_toggle_ref(
+        fx->cx);
+    reffed = instance->ptr();
+    gjs_test_tools_ref_other_thread(reffed);
+    g_assert_cmpuint(ToggleQueueTest::queue().size(), ==, 1);
+    g_assert_cmpuint(ToggleQueueTest::queue().at(0).direction, ==,
+                     ToggleQueue::Direction::UP);
+
+    reffed.reset();
+    g_assert_true(ToggleQueueTest::queue().empty());
+
+    auto tq = ToggleQueue::get_default();
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(instance);
+    g_assert_false(toggle_down_queued);
+    g_assert_false(toggle_up_queued);
+
+    tq->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+}
+
+void test_toggle_queue_object_from_other_thread_ref_unref(
+    GjsUnitTestFixture* fx, const void*) {
+    auto* instance = new_test_gobject(fx);
+    reinterpret_cast<ObjectInstanceTest*>(instance)->ensure_uses_toggle_ref(
+        fx->cx);
+
+    gjs_test_tools_ref_other_thread(instance->ptr());
+    g_assert_cmpuint(ToggleQueueTest::queue().size(), ==, 1);
+    g_assert_cmpuint(ToggleQueueTest::queue().at(0).direction, ==,
+                     ToggleQueue::Direction::UP);
+
+    gjs_test_tools_unref_other_thread(instance->ptr());
+    g_assert_true(ToggleQueueTest::queue().empty());
+
+    auto tq = ToggleQueue::get_default();
+    bool toggle_down_queued, toggle_up_queued;
+    std::tie(toggle_down_queued, toggle_up_queued) = tq->cancel(instance);
+    g_assert_false(toggle_down_queued);
+    g_assert_false(toggle_up_queued);
+
+    tq->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+}
+
+void test_toggle_queue_object_handle_up(GjsUnitTestFixture* fx, const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto* instance_test = reinterpret_cast<ObjectInstanceTest*>(instance);
+    instance_test->ensure_uses_toggle_ref(fx->cx);
+
+    gjs_test_tools_ref_other_thread(instance->ptr());
+    GjsAutoUnref<GObject> reffed(instance->ptr());
+    g_assert_cmpuint(ToggleQueueTest::queue().size(), ==, 1);
+    g_assert_cmpuint(ToggleQueueTest::queue().at(0).direction, ==,
+                     ToggleQueue::Direction::UP);
+
+    wait_for(50);
+    g_assert_true(instance_test->wrapper_is_rooted());
+    ToggleQueue::get_default()->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+}
+
+void test_toggle_queue_object_handle_up_down(GjsUnitTestFixture* fx,
+                                             const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto* instance_test = reinterpret_cast<ObjectInstanceTest*>(instance);
+    instance_test->ensure_uses_toggle_ref(fx->cx);
+
+    gjs_test_tools_ref_other_thread(instance->ptr());
+    g_assert_cmpuint(ToggleQueueTest::queue().size(), ==, 1);
+    g_assert_cmpuint(ToggleQueueTest::queue().at(0).direction, ==,
+                     ToggleQueue::Direction::UP);
+
+    gjs_test_tools_unref_other_thread(instance->ptr());
+    g_assert_true(ToggleQueueTest::queue().empty());
+
+    wait_for(50);
+    g_assert_false(instance_test->wrapper_is_rooted());
+    ToggleQueue::get_default()->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+}
+
+void test_toggle_queue_object_handle_up_down_delayed(GjsUnitTestFixture* fx,
+                                                     const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto* instance_test = reinterpret_cast<ObjectInstanceTest*>(instance);
+    instance_test->ensure_uses_toggle_ref(fx->cx);
+
+    gjs_test_tools_ref_other_thread(instance->ptr());
+    g_assert_cmpuint(ToggleQueueTest::queue().size(), ==, 1);
+    g_assert_cmpuint(ToggleQueueTest::queue().at(0).direction, ==,
+                     ToggleQueue::Direction::UP);
+
+    wait_for(50);
+    g_assert_true(instance_test->wrapper_is_rooted());
+    ToggleQueue::get_default()->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+
+    gjs_test_tools_unref_other_thread(instance->ptr());
+    g_assert_cmpuint(ToggleQueueTest::queue().size(), ==, 1);
+    g_assert_cmpuint(ToggleQueueTest::queue().at(0).direction, ==,
+                     ToggleQueue::Direction::DOWN);
+
+    wait_for(50);
+    g_assert_false(instance_test->wrapper_is_rooted());
+    ToggleQueue::get_default()->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+}
+
+void test_toggle_queue_object_handle_up_down_on_gc(GjsUnitTestFixture* fx,
+                                                   const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto* instance_test = reinterpret_cast<ObjectInstanceTest*>(instance);
+    instance_test->ensure_uses_toggle_ref(fx->cx);
+
+    gjs_test_tools_ref_other_thread(instance->ptr());
+    g_assert_cmpuint(ToggleQueueTest::queue().size(), ==, 1);
+    g_assert_cmpuint(ToggleQueueTest::queue().at(0).direction, ==,
+                     ToggleQueue::Direction::UP);
+
+    gjs_test_tools_unref_other_thread(instance->ptr());
+    g_assert_true(ToggleQueueTest::queue().empty());
+
+    GWeakRef weak_ref;
+    g_weak_ref_init(&weak_ref, instance->ptr());
+
+    wait_for_gc(fx);
+    g_assert_null(g_weak_ref_get(&weak_ref));
+
+    ToggleQueue::get_default()->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+}
+
+void test_toggle_queue_object_handle_many_up(GjsUnitTestFixture* fx,
+                                             const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto* instance_test = reinterpret_cast<ObjectInstanceTest*>(instance);
+    instance_test->ensure_uses_toggle_ref(fx->cx);
+
+    gjs_test_tools_ref_other_thread(instance->ptr());
+    GjsAutoUnref<GObject> reffed(instance->ptr());
+    // Simulating the case where late threads are causing this...
+    ToggleQueue::get_default()->enqueue(instance, ToggleQueue::Direction::UP,
+                                        ToggleQueueTest().handler());
+
+    g_assert_cmpuint(ToggleQueueTest::queue().size(), ==, 2);
+    g_assert_cmpuint(ToggleQueueTest::queue().at(0).direction, ==,
+                     ToggleQueue::Direction::UP);
+    g_assert_cmpuint(ToggleQueueTest::queue().at(1).direction, ==,
+                     ToggleQueue::Direction::UP);
+
+    wait_for(50);
+    g_assert_true(instance_test->wrapper_is_rooted());
+    ToggleQueue::get_default()->handle_all_toggles(toggles_handler);
+    g_assert_true(s_toggle_history.empty());
+}
+
+void test_toggle_queue_object_handle_many_up_and_down(GjsUnitTestFixture* fx,
+                                                      const void*) {
+    auto* instance = new_test_gobject(fx);
+    auto* instance_test = reinterpret_cast<ObjectInstanceTest*>(instance);
+    instance_test->ensure_uses_toggle_ref(fx->cx);
+
+    // This is something similar to what is happening on #297
+    gjs_test_tools_ref_other_thread(instance->ptr());
+    ToggleQueue::get_default()->enqueue(instance, ToggleQueue::Direction::UP,
+                                        ToggleQueueTest().handler());
+    gjs_test_tools_unref_other_thread(instance->ptr());
+    ToggleQueue::get_default()->enqueue(instance, ToggleQueue::Direction::DOWN,
+                                        ToggleQueueTest().handler());
+
+    g_assert_true(ToggleQueueTest::queue().empty());
+
+    wait_for(50);
+    g_assert_false(instance_test->wrapper_is_rooted());
+    g_assert_true(ToggleQueueTest::queue().empty());
+
+    GWeakRef weak_ref;
+    g_assert_true(G_IS_OBJECT(instance->ptr()));
+    g_weak_ref_init(&weak_ref, instance->ptr());
+
+    wait_for_gc(fx);
+    g_assert_null(g_weak_ref_get(&weak_ref));
+    g_assert_true(ToggleQueueTest::queue().empty());
+}
+
+void gjs_test_add_tests_for_toggle_queue(void) {
+#define ADD_TOGGLE_QUEUE_TEST(path, f)                                       \
+    g_test_add("/toggle-queue/" path, GjsUnitTestFixture, nullptr, setup, f, \
+               teardown);
+
+    ADD_TOGGLE_QUEUE_TEST("spin-lock/unlock-empty",
+                          test_toggle_queue_unlock_empty);
+    ADD_TOGGLE_QUEUE_TEST("spin-lock/unlock-same-thread",
+                          test_toggle_queue_unlock_same_thread);
+    ADD_TOGGLE_QUEUE_TEST("spin-lock/blocks-other-thread",
+                          test_toggle_blocks_other_thread);
+
+    ADD_TOGGLE_QUEUE_TEST("empty", test_toggle_queue_empty);
+    ADD_TOGGLE_QUEUE_TEST("empty_cancel", test_toggle_queue_empty_cancel);
+    ADD_TOGGLE_QUEUE_TEST("enqueue_one", test_toggle_queue_enqueue_one);
+    ADD_TOGGLE_QUEUE_TEST("enqueue_one_cancel",
+                          test_toggle_queue_enqueue_one_cancel);
+    ADD_TOGGLE_QUEUE_TEST("enqueue_many_equal",
+                          test_toggle_queue_enqueue_many_equal);
+    ADD_TOGGLE_QUEUE_TEST("enqueue_many_equal_cancel",
+                          test_toggle_queue_enqueue_many_equal_cancel);
+    ADD_TOGGLE_QUEUE_TEST("enqueue_more_up", test_toggle_queue_enqueue_more_up);
+    ADD_TOGGLE_QUEUE_TEST("enqueue_only_up", test_toggle_queue_enqueue_only_up);
+    ADD_TOGGLE_QUEUE_TEST("enqueue_only_up_cancel",
+                          test_toggle_queue_enqueue_only_up_cancel);
+    ADD_TOGGLE_QUEUE_TEST("handle_more_up", test_toggle_queue_handle_more_up);
+    ADD_TOGGLE_QUEUE_TEST("handle_only_up", test_toggle_queue_handle_only_up);
+
+    ADD_TOGGLE_QUEUE_TEST("object/not-enqueued_main_thread",
+                          test_toggle_queue_object_from_main_thread);
+    ADD_TOGGLE_QUEUE_TEST(
+        "object/already_enqueued_main_thread",
+        test_toggle_queue_object_from_main_thread_already_enqueued);
+    ADD_TOGGLE_QUEUE_TEST(
+        "object/already_enqueued_unref_main_thread",
+        test_toggle_queue_object_from_main_thread_unref_already_enqueued);
+    ADD_TOGGLE_QUEUE_TEST("object/ref_unref_other_thread",
+                          test_toggle_queue_object_from_other_thread_ref_unref);
+    ADD_TOGGLE_QUEUE_TEST("object/handle_up",
+                          test_toggle_queue_object_handle_up);
+    ADD_TOGGLE_QUEUE_TEST("object/handle_up_down",
+                          test_toggle_queue_object_handle_up_down);
+    ADD_TOGGLE_QUEUE_TEST("object/handle_up_down_delayed",
+                          test_toggle_queue_object_handle_up_down_delayed);
+    ADD_TOGGLE_QUEUE_TEST("object/handle_up_down_on_gc",
+                          test_toggle_queue_object_handle_up_down_on_gc);
+    ADD_TOGGLE_QUEUE_TEST("object/handle_many_up",
+                          test_toggle_queue_object_handle_many_up);
+    ADD_TOGGLE_QUEUE_TEST("object/handle_many_up_and_down",
+                          test_toggle_queue_object_handle_many_up_and_down);
+
+#undef ADD_TOGGLE_QUEUE_TEST
+
+    // #undef ADD_CONTEXT_DESTROY_TEST
+}
diff --git a/test/gjs-test-utils.h b/test/gjs-test-utils.h
index f44298a6..dd7a0aa8 100644
--- a/test/gjs-test-utils.h
+++ b/test/gjs-test-utils.h
@@ -32,4 +32,6 @@ void gjs_test_add_tests_for_rooting(void);
 
 void gjs_test_add_tests_for_jsapi_utils();
 
+void gjs_test_add_tests_for_toggle_queue();
+
 #endif  // TEST_GJS_TEST_UTILS_H_
diff --git a/test/gjs-tests-internal.cpp b/test/gjs-tests-internal.cpp
new file mode 100644
index 00000000..f9e08357
--- /dev/null
+++ b/test/gjs-tests-internal.cpp
@@ -0,0 +1,23 @@
+/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Canonical, Ltd.
+// SPDX-FileContributor: Marco Trevisan <marco trevisan canonical com>
+
+#include <config.h>
+#include <glib.h>
+
+#include "test/gjs-test-utils.h"
+
+int main(int argc, char** argv) {
+    /* Avoid interference in the tests from stray environment variable */
+    g_unsetenv("GJS_ENABLE_PROFILER");
+    g_unsetenv("GJS_TRACE_FD");
+
+    g_test_init(&argc, &argv, nullptr);
+
+    gjs_test_add_tests_for_toggle_queue();
+
+    g_test_run();
+
+    return 0;
+}
diff --git a/test/meson.build b/test/meson.build
index 83c63067..0dbcdef6 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -7,9 +7,17 @@ mock_js_resources_files = gnome.compile_resources('mock-js-resources',
     'mock-js-resources.gresource.xml', c_name: 'mock_js_resources',
     source_dir: '..')
 
+libgjs_tests = static_library(meson.project_name() + '-tests',
+    sources: [
+        'gjs-test-utils.cpp', 'gjs-test-utils.h',
+        'gjs-test-common.cpp', 'gjs-test-common.h',
+    ],
+    cpp_args: libgjs_cpp_args,
+    include_directories: top_include, dependencies: libgjs_dependencies,
+)
+
 gjs_tests_sources = [
     'gjs-tests.cpp',
-    'gjs-test-common.cpp', 'gjs-test-common.h',
     'gjs-test-utils.cpp', 'gjs-test-utils.h',
     'gjs-test-call-args.cpp',
     'gjs-test-coverage.cpp',
@@ -19,9 +27,25 @@ gjs_tests_sources = [
 ]
 
 gjs_tests = executable('gjs-tests', gjs_tests_sources, mock_js_resources_files,
-    cpp_args: ['-DGJS_COMPILATION'] + directory_defines,
-    include_directories: top_include, dependencies: libgjs_dep)
+    include_directories: top_include, dependencies: libgjs_dep,
+    link_with: libgjs_tests)
 
 test('API tests', gjs_tests, args: ['--tap', '--keep-going', '--verbose'],
     depends: gjs_private_typelib, env: tests_environment, protocol: 'tap',
     suite: 'C', timeout: 60)
+
+gjs_tests_internal = executable('gjs-tests-internal',
+    sources: [
+        'gjs-tests-internal.cpp',
+        'gjs-test-toggle-queue.cpp',
+        module_resource_srcs,
+    ],
+    include_directories: top_include,
+    cpp_args: libgjs_cpp_args,
+    dependencies: [libgjs_dependencies, libgjstesttools_dep],
+    link_with: [libgjs_tests, libgjs_private])
+
+test('Internal API tests', gjs_tests_internal,
+    args: ['--tap', '--keep-going', '--verbose'],
+    env: tests_environment, protocol: 'tap',
+    suite: ['C', 'thread-safe'])


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]