Programmatically detecting leaks in unit tests using the leaks tracer

I wrote code to use the leaks tracer in unit tests to check for leaks after every test and let the test fail if leaks occur. To that end, I enable the leaks tracer, and use the get-live-objects GLib action to get the list of leaked objects.

Here is my C++ code:

void init(int &argc, char **&argv)
{
   // Force the GST_TRACERS environment variable to "leaks"
   // to make GStreamer activate the leaks tracer.
   g_setenv("GST_TRACERS", "leaks", TRUE);
   gst_init(&argc, &argv);

   // Look for the leak tracer.
   GList *tracer_list = gst_tracing_get_active_tracers();
   for (GList *tracer_item = tracer_list; (tracer_item != nullptr);
        tracer_item = tracer_item->next)
   {
       GstTracer *tracer = GST_TRACER(tracer_item->data);
       gchar *tracer_name = gst_object_get_name(GST_OBJECT(tracer));
       bool is_leaks_tracer = g_str_has_prefix(tracer_name, "leaks");
       g_free(tracer_name);

       if (is_leaks_tracer)
       {
           // The leaks tracer was found. Ref it here, since
           // further below, the active tracers are unref'd
           // again, and we want to keep a reference to the
           // leaks tracer.
           gst_object_ref(GST_OBJECT(tracer));
           m_leaks_tracer = tracer;
       }
   }

   g_list_free_full(tracer_list, [](gpointer data) -> void {
       GstObject *obj = GST_OBJECT(data);
       gst_object_unref(obj);
   });
}

void shutdown()
{
   if (m_leaks_tracer != nullptr)
       gst_object_unref(GST_OBJECT(m_leaks_tracer));
   // Explicitly call this to do a final tracer processing
   // (which is done in deinit).
   gst_deinit();
}

unsigned int get_num_gstreamer_leaks()
{
   if (m_leaks_tracer == nullptr)
       return 0;

   // The leaks tracer has a "get-live-objects" GLib action that
   // can be used for querying the list of live objects. The
   // result is a GstStructure. One of that structure's fields
   // is "live-objects-list", which is a GstValueList GValue.
   // That list contains information about all leaked objects.
   //
   // The leaks tracer is active for the duration of the run
   // of the entire process. This means that if a leak happen
   // in an earlier test, the same leak will still be in this
   // list when later tests run. Therefore, any leaks that
   // have been observed in the list must be recorded so that
   // they can be filtered out in later runs. These records
   // are m_recorded_leaked_gobjects and
   // m_recorded_leaked_mini_objects.

   GstStructure *live_objects_info;
   g_signal_emit_by_name(m_leaks_tracer, "get-live-objects", &live_objects_info);
   if (live_objects_info == nullptr)
       return 0;

   guint num_leaks = 0;

   const GValue *leaks_gvalue =
       gst_structure_get_value(live_objects_info, "live-objects-list");
   if (leaks_gvalue != nullptr)
   {
       for (guint leak_index = 0; leak_index < gst_value_list_get_size(leaks_gvalue);
            ++leak_index)
       {
           // The list of live objects contains items which are GstStructures.
           // Each item contains an "object" field - that is the leaked object.

           const GValue *leak_gvalue = gst_value_list_get_value(leaks_gvalue, leak_index);
           g_assert(G_VALUE_HOLDS(leak_gvalue, GST_TYPE_STRUCTURE));

           const GstStructure *leak_structure = gst_value_get_structure(leak_gvalue);
           if (!gst_structure_has_field(leak_structure, "object"))
               continue;

           const GValue *leaked_object_gvalue =
               gst_structure_get_value(leak_structure, "object");
           g_assert(leaked_object_gvalue != nullptr);

           GType type = G_VALUE_TYPE(leaked_object_gvalue);

           // The object from the leak structure is either something
           // based on GObject or something based on GstMiniObject.
           // The former is stored as "object" in GValue, the latter
           // is stored as "boxed".
           if (g_type_is_a(type, G_TYPE_OBJECT))
           {
               gpointer object = g_value_get_object(leaked_object_gvalue);
               // IMPORTANT: When creating the list of leaked objects,
               // the leaks tracer does _not_ increment the refcount
               // of that object. When the live_objects_info structure
               // is freed by the gst_structure_free() call above though,
               // this object's refcount _will_ be decremented. This causes
               // errors later because the code does not expect the refcount
               // to overall have changed. Compensate for this by manually
               // increasing the refcount here.
               g_object_ref(G_OBJECT(object));
               // Check that the object isn't in m_recorded_leaked_gobjects
               // already to avoid counting the same leak multiple times.
               if (m_recorded_leaked_gobjects.find(object) == m_recorded_leaked_gobjects.end())
               {
                   m_recorded_leaked_gobjects.insert(object);
                   num_leaks++;
               }
           }
           else
           {
               gpointer mini_object = g_value_get_boxed(leaked_object_gvalue);
               // IMPORTANT: When creating the list of leaked mini objects,
               // the leaks tracer does _not_ increment the refcount
               // of that mini object. When the live_objects_info structure
               // is freed by the gst_structure_free() call above though,
               // this mini object's refcount _will_ be decremented. This causes
               // errors later because the code does not expect the refcount
               // to overall have changed. Compensate for this by manually
               // increasing the refcount here.
               gst_mini_object_ref(GST_MINI_OBJECT(mini_object));
               // Check that the object isn't in m_recorded_leaked_mini_objects
               // already to avoid counting the same leak multiple times.
               if (m_recorded_leaked_mini_objects.find(mini_object) ==
                   m_recorded_leaked_mini_objects.end())
               {
                   m_recorded_leaked_mini_objects.insert(mini_object);
                   num_leaks++;
               }
           }
       }
   }

   gst_structure_free(live_objects_info);

   return num_leaks;
}

GstTracer *m_leaks_tracer = nullptr;
std::unordered_set<gpointer> m_recorded_leaked_gobjects;
std::unordered_set<gpointer> m_recorded_leaked_mini_objects;

bool check_for_leaks()
{
   return get_num_gstreamer_leaks() != 0;
}

This works. Calling check_for_leaks at the end of each unit tests detects any leaks that occurred during the test. However, I am uncertain about two things:

  1. The bits marked as “IMPORTANT” ref the objects. Without this, errors occur in the gst_deinit call. Does this ref’ing look plausible?
  2. This code assumes that the objects are either GObject or GstMiniObject based, and that no other objects could be in the list. This is because the g_type_is_a(type, G_TYPE_OBJECT) call correctly detects GObject based objects, but g_type_is_a(type, GST_TYPE_MINI_OBJECT) doesn’t (it does not match GstBuffer for example). Is this okay to do?

I know I’m not answering your question, but just in case it’s useful: the leaks tracer will print a glib critical/warning on gst_deinit() in case there are leaks (but no details, for those you’d have to check the debug log then). You can make that warning fatal with G_DEBUG=fatal_warnings, then your unit test will return a failure exit code. This works at the process level of course, so ideally you’d run each individual test in a forked process so you can see which one fails then, but that’s details I suppose.