Two issues here: - There was some inconsistency in how we handled domains with multiple dots at the end. Instead of making this consistent, I just did the easier thing and return an error if a domain ends with more than one dot, because I don't think there's a realistic use case for it. - Also, places that we called `Preferences::UnregisterCallback()` assumed that it succeeded and would always release the last reference to the `PrefCallback`, so if it didn't succeed a `PrefCallback` would get freed but would still be in `mObservers`. So now we check if `UnregisterCallback()` fails and if so, add the `PrefCallback` back to `mObservers`. (I decided to do it this way since `UnregisterCallback()` failing should be very rare so I didn't want to incur a lookup followed by a remove in the normal case) (technically we only need to fix one of these to close the hole, but fixing both made sense) Differential Revision: https://phabricator.services.mozilla.com/D323047
557 lines
21 KiB
C++
557 lines
21 KiB
C++
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "gtest/gtest.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "mozilla/SpinEventLoopUntil.h"
|
|
#include "nsITimer.h"
|
|
#include "nsTArray.h"
|
|
#include "nsIObserver.h"
|
|
#include "nsThreadUtils.h"
|
|
#include "nsServiceManagerUtils.h"
|
|
#include "nsWeakReference.h"
|
|
|
|
using namespace mozilla;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers shared by the CallbackTrie tests below.
|
|
// ---------------------------------------------------------------------------
|
|
namespace {
|
|
|
|
struct CallbackOrder {
|
|
nsTArray<int>* order;
|
|
int id;
|
|
};
|
|
|
|
void TrackOrder(const char*, void* aData) {
|
|
auto* d = static_cast<CallbackOrder*>(aData);
|
|
d->order->AppendElement(d->id);
|
|
}
|
|
|
|
void IncrementCount(const char*, void* aData) { (*static_cast<int*>(aData))++; }
|
|
|
|
} // namespace
|
|
|
|
class TestWeakPrefObserver final : public nsIObserver,
|
|
public nsSupportsWeakReference {
|
|
public:
|
|
NS_DECL_ISUPPORTS
|
|
NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
|
|
const char16_t* aData) override {
|
|
mNotifyCount++;
|
|
return NS_OK;
|
|
}
|
|
int mNotifyCount = 0;
|
|
|
|
private:
|
|
~TestWeakPrefObserver() = default;
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS(TestWeakPrefObserver, nsIObserver, nsISupportsWeakReference)
|
|
|
|
TEST(PrefsBasics, Errors)
|
|
{
|
|
Preferences::SetBool("foo.bool", true, PrefValueKind::Default);
|
|
Preferences::SetBool("foo.bool", false, PrefValueKind::User);
|
|
ASSERT_EQ(Preferences::GetBool("foo.bool", false, PrefValueKind::Default),
|
|
true);
|
|
ASSERT_EQ(Preferences::GetBool("foo.bool", true, PrefValueKind::User), false);
|
|
|
|
Preferences::SetInt("foo.int", -66, PrefValueKind::Default);
|
|
Preferences::SetInt("foo.int", -77, PrefValueKind::User);
|
|
ASSERT_EQ(Preferences::GetInt("foo.int", 1, PrefValueKind::Default), -66);
|
|
ASSERT_EQ(Preferences::GetInt("foo.int", 1, PrefValueKind::User), -77);
|
|
|
|
Preferences::SetUint("foo.uint", 88, PrefValueKind::Default);
|
|
Preferences::SetUint("foo.uint", 99, PrefValueKind::User);
|
|
ASSERT_EQ(Preferences::GetUint("foo.uint", 1, PrefValueKind::Default), 88U);
|
|
ASSERT_EQ(Preferences::GetUint("foo.uint", 1, PrefValueKind::User), 99U);
|
|
|
|
Preferences::SetFloat("foo.float", 3.33f, PrefValueKind::Default);
|
|
Preferences::SetFloat("foo.float", 4.44f, PrefValueKind::User);
|
|
ASSERT_FLOAT_EQ(
|
|
Preferences::GetFloat("foo.float", 1.0f, PrefValueKind::Default), 3.33f);
|
|
ASSERT_FLOAT_EQ(Preferences::GetFloat("foo.float", 1.0f, PrefValueKind::User),
|
|
4.44f);
|
|
}
|
|
|
|
TEST(PrefsBasics, Serialize)
|
|
{
|
|
// Ensure that at least this one preference exists
|
|
Preferences::SetBool("foo.bool", true, PrefValueKind::Default);
|
|
ASSERT_EQ(Preferences::GetBool("foo.bool", false, PrefValueKind::Default),
|
|
true);
|
|
|
|
nsCString str;
|
|
Preferences::SerializePreferences(str, true);
|
|
fprintf(stderr, "%s\n", str.get());
|
|
// Assert that some prefs were not sanitized
|
|
ASSERT_NE(nullptr, strstr(str.get(), "B--:"));
|
|
ASSERT_NE(nullptr, strstr(str.get(), "I--:"));
|
|
ASSERT_NE(nullptr, strstr(str.get(), "S--:"));
|
|
// Assert that something was sanitized
|
|
ASSERT_NE(
|
|
nullptr,
|
|
strstr(
|
|
str.get(),
|
|
"I-S:56/datareporting.policy.dataSubmissionPolicyAcceptedVersion"));
|
|
}
|
|
|
|
TEST(PrefsBasics, WeakObserverIdleSweep)
|
|
{
|
|
// In gtest there is no RefreshDriver to drive idle scheduling and we don't
|
|
// want to wait for the maximum idle delay. A repeating timer provides enough
|
|
// main-thread activity for the idle task machinery to find idle time.
|
|
nsCOMPtr<nsITimer> keepAlive = NS_NewTimer();
|
|
keepAlive->InitWithNamedFuncCallback(
|
|
[](nsITimer*, void*) {}, nullptr, 16, nsITimer::TYPE_REPEATING_SLACK,
|
|
"PrefsBasics.WeakObserverIdleSweep.keepAlive"_ns);
|
|
|
|
// Drain any startup-triggered sweep runner before we begin.
|
|
TimeStamp drainDeadline =
|
|
TimeStamp::Now() + TimeDuration::FromMilliseconds(100);
|
|
MOZ_ALWAYS_TRUE(
|
|
SpinEventLoopUntil("PrefsBasics.WeakObserverIdleSweep.drain"_ns,
|
|
[&] { return TimeStamp::Now() >= drainDeadline; }));
|
|
NS_ProcessPendingEvents(nullptr);
|
|
|
|
static const char kPref[] = "test.weak.observer.sweep";
|
|
Preferences::SetBool(kPref, false);
|
|
|
|
uint32_t countWithObserver;
|
|
{
|
|
RefPtr<TestWeakPrefObserver> observer = new TestWeakPrefObserver();
|
|
nsresult rv = Preferences::AddWeakObserver(observer, kPref);
|
|
ASSERT_TRUE(NS_SUCCEEDED(rv));
|
|
countWithObserver = Preferences::GetCallbackCount();
|
|
}
|
|
|
|
// Observer expired, but no pref change — callback is still in the list.
|
|
EXPECT_EQ(Preferences::GetCallbackCount(), countWithObserver);
|
|
|
|
// Changing the pref notifies the expired observer, scheduling an idle sweep.
|
|
Preferences::SetBool(kPref, true);
|
|
|
|
// Spin the event loop until the idle sweep runs and removes the callback.
|
|
MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
|
|
"PrefsBasics.WeakObserverIdleSweep"_ns,
|
|
[&] { return Preferences::GetCallbackCount() < countWithObserver; }));
|
|
|
|
keepAlive->Cancel();
|
|
}
|
|
|
|
TEST(PrefsBasics, WeakObserverRegistrationSweep)
|
|
{
|
|
// In gtest there is no RefreshDriver to drive idle scheduling and we don't
|
|
// want to wait for the maximum idle delay. A repeating timer provides enough
|
|
// main-thread activity for the idle task machinery to find idle time.
|
|
nsCOMPtr<nsITimer> keepAlive = NS_NewTimer();
|
|
keepAlive->InitWithNamedFuncCallback(
|
|
[](nsITimer*, void*) {}, nullptr, 16, nsITimer::TYPE_REPEATING_SLACK,
|
|
"PrefsBasics.WeakObserverRegistrationSweep.keepAlive"_ns);
|
|
|
|
// Drain any pending sweep runner before we begin.
|
|
TimeStamp drainDeadline =
|
|
TimeStamp::Now() + TimeDuration::FromMilliseconds(100);
|
|
MOZ_ALWAYS_TRUE(
|
|
SpinEventLoopUntil("PrefsBasics.WeakObserverRegistrationSweep.drain"_ns,
|
|
[&] { return TimeStamp::Now() >= drainDeadline; }));
|
|
NS_ProcessPendingEvents(nullptr);
|
|
|
|
static const char kPref[] = "test.weak.observer.regsweep";
|
|
Preferences::SetBool(kPref, false);
|
|
|
|
uint32_t countWithObserver;
|
|
{
|
|
RefPtr<TestWeakPrefObserver> observer = new TestWeakPrefObserver();
|
|
nsresult rv = Preferences::AddWeakObserver(observer, kPref);
|
|
ASSERT_TRUE(NS_SUCCEEDED(rv));
|
|
countWithObserver = Preferences::GetCallbackCount();
|
|
}
|
|
|
|
// Observer expired, but callback is still in the list.
|
|
EXPECT_EQ(Preferences::GetCallbackCount(), countWithObserver);
|
|
|
|
// Register 512 weak observers to hit the periodic sweep threshold.
|
|
// Keep them alive during registration to avoid address reuse causing
|
|
// duplicate keys in the observer hashtable.
|
|
static constexpr uint32_t kSweepInterval = 512;
|
|
nsTArray<RefPtr<TestWeakPrefObserver>> observers(kSweepInterval);
|
|
for (uint32_t i = 0; i < kSweepInterval; i++) {
|
|
observers.AppendElement(new TestWeakPrefObserver());
|
|
Preferences::AddWeakObserver(observers.LastElement(), kPref);
|
|
}
|
|
EXPECT_EQ(Preferences::GetCallbackCount(),
|
|
countWithObserver + kSweepInterval);
|
|
|
|
// Let all observers expire at once.
|
|
observers.Clear();
|
|
|
|
// Spin the event loop until the sweep removes all expired callbacks.
|
|
MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
|
|
"PrefsBasics.WeakObserverRegistrationSweep"_ns,
|
|
[&] { return Preferences::GetCallbackCount() < countWithObserver; }));
|
|
|
|
keepAlive->Cancel();
|
|
}
|
|
|
|
TEST(PrefsBasics, FreeObserverListRemovesAllCallbacks)
|
|
{
|
|
Preferences::SetBool("test.free.a.pref", false);
|
|
Preferences::SetBool("test.free.b.pref", false);
|
|
|
|
uint32_t baselineCount = Preferences::GetCallbackCount();
|
|
|
|
nsCOMPtr<nsIPrefService> prefService =
|
|
do_GetService(NS_PREFSERVICE_CONTRACTID);
|
|
ASSERT_TRUE(prefService);
|
|
|
|
nsCOMPtr<nsIPrefBranch> branchA;
|
|
nsresult rv = prefService->GetBranch("test.free.a.", getter_AddRefs(branchA));
|
|
ASSERT_TRUE(NS_SUCCEEDED(rv));
|
|
|
|
nsCOMPtr<nsIPrefBranch> branchB;
|
|
rv = prefService->GetBranch("test.free.b.", getter_AddRefs(branchB));
|
|
ASSERT_TRUE(NS_SUCCEEDED(rv));
|
|
|
|
RefPtr<TestWeakPrefObserver> obs1 = new TestWeakPrefObserver();
|
|
RefPtr<TestWeakPrefObserver> obs2 = new TestWeakPrefObserver();
|
|
RefPtr<TestWeakPrefObserver> obs3 = new TestWeakPrefObserver();
|
|
|
|
// Interleave observer registration across the two branches.
|
|
rv = branchA->AddObserver("pref", obs1, false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(rv));
|
|
rv = branchB->AddObserver("pref", obs2, false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(rv));
|
|
rv = branchA->AddObserver("pref", obs3, false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(rv));
|
|
EXPECT_EQ(Preferences::GetCallbackCount(), baselineCount + 3);
|
|
|
|
// Releasing branchA should only remove its two callbacks.
|
|
branchA = nullptr;
|
|
EXPECT_EQ(Preferences::GetCallbackCount(), baselineCount + 1);
|
|
|
|
// Releasing branchB removes the remaining one.
|
|
branchB = nullptr;
|
|
EXPECT_EQ(Preferences::GetCallbackCount(), baselineCount);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CallbackTrie tests — verify HashedSegmentsTrie notification semantics.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Callback at the exact pref name fires when that pref changes.
|
|
TEST(PrefsCallbackTrie, ExactMatch)
|
|
{
|
|
int count = 0;
|
|
Preferences::SetBool("test.trie.exact", false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterCallback(
|
|
IncrementCount, "test.trie.exact", &count)));
|
|
Preferences::SetBool("test.trie.exact", true);
|
|
EXPECT_EQ(count, 1);
|
|
Preferences::UnregisterCallback(IncrementCount, "test.trie.exact", &count);
|
|
}
|
|
|
|
// Prefix callback at an ancestor fires when a deeper descendant pref changes.
|
|
TEST(PrefsCallbackTrie, PrefixAncestorFiresForDescendant)
|
|
{
|
|
int count = 0;
|
|
Preferences::SetBool("test.trie.anc.deep.pref", false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterPrefixCallback(
|
|
IncrementCount, "test.trie.anc"_ns, &count)));
|
|
Preferences::SetBool("test.trie.anc.deep.pref", true);
|
|
EXPECT_EQ(count, 1);
|
|
Preferences::UnregisterPrefixCallback(IncrementCount, "test.trie.anc"_ns,
|
|
&count);
|
|
}
|
|
|
|
// Exact callback at an ancestor does NOT fire when a descendant pref changes.
|
|
TEST(PrefsCallbackTrie, ExactAncestorDoesNotFireForDescendant)
|
|
{
|
|
int count = 0;
|
|
Preferences::SetBool("test.trie.anc.exact.pref", false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterCallback(
|
|
IncrementCount, "test.trie.anc.exact"_ns, &count)));
|
|
Preferences::SetBool("test.trie.anc.exact.pref", true);
|
|
EXPECT_EQ(count, 0);
|
|
Preferences::UnregisterCallback(IncrementCount, "test.trie.anc.exact"_ns,
|
|
&count);
|
|
}
|
|
|
|
// Callback does NOT fire for a sibling pref (different child at the same
|
|
// depth).
|
|
TEST(PrefsCallbackTrie, NoFireForSibling)
|
|
{
|
|
int count = 0;
|
|
Preferences::SetBool("test.trie.sib.b", false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterCallback(
|
|
IncrementCount, "test.trie.sib.a", &count)));
|
|
Preferences::SetBool("test.trie.sib.b", true);
|
|
EXPECT_EQ(count, 0);
|
|
Preferences::UnregisterCallback(IncrementCount, "test.trie.sib.a", &count);
|
|
}
|
|
|
|
// Callback does NOT fire for a pref whose name shares a prefix at the byte
|
|
// level but is a different dot-separated segment ("ab" vs "abc").
|
|
TEST(PrefsCallbackTrie, NoFireForPrefixSubstring)
|
|
{
|
|
int count = 0;
|
|
Preferences::SetBool("test.trie.seg.abc", false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterCallback(
|
|
IncrementCount, "test.trie.seg.ab", &count)));
|
|
Preferences::SetBool("test.trie.seg.abc", true);
|
|
EXPECT_EQ(count, 0);
|
|
Preferences::UnregisterCallback(IncrementCount, "test.trie.seg.ab", &count);
|
|
}
|
|
|
|
// Prefix callbacks at ancestor nodes fire before callbacks at deeper nodes.
|
|
TEST(PrefsCallbackTrie, AncestorBeforeDescendantOrder)
|
|
{
|
|
nsTArray<int> order;
|
|
CallbackOrder dataA{&order, 1};
|
|
CallbackOrder dataAB{&order, 2};
|
|
CallbackOrder dataABC{&order, 3};
|
|
|
|
Preferences::SetBool("test.trie.order.a.b.c", false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterPrefixCallback(
|
|
TrackOrder, "test.trie.order.a"_ns, &dataA)));
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterPrefixCallback(
|
|
TrackOrder, "test.trie.order.a.b"_ns, &dataAB)));
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterCallback(
|
|
TrackOrder, "test.trie.order.a.b.c"_ns, &dataABC)));
|
|
|
|
Preferences::SetBool("test.trie.order.a.b.c", true);
|
|
ASSERT_EQ(order.Length(), 3u);
|
|
EXPECT_EQ(order[0], 1);
|
|
EXPECT_EQ(order[1], 2);
|
|
EXPECT_EQ(order[2], 3);
|
|
|
|
Preferences::UnregisterPrefixCallback(TrackOrder, "test.trie.order.a"_ns,
|
|
&dataA);
|
|
Preferences::UnregisterPrefixCallback(TrackOrder, "test.trie.order.a.b"_ns,
|
|
&dataAB);
|
|
Preferences::UnregisterCallback(TrackOrder, "test.trie.order.a.b.c"_ns,
|
|
&dataABC);
|
|
}
|
|
|
|
// Within a single trie node, callbacks fire in LIFO order (most recently
|
|
// registered fires first).
|
|
TEST(PrefsCallbackTrie, LIFOWithinNode)
|
|
{
|
|
nsTArray<int> order;
|
|
CallbackOrder data1{&order, 1};
|
|
CallbackOrder data2{&order, 2};
|
|
|
|
Preferences::SetBool("test.trie.lifo", false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(
|
|
Preferences::RegisterCallback(TrackOrder, "test.trie.lifo", &data1)));
|
|
ASSERT_TRUE(NS_SUCCEEDED(
|
|
Preferences::RegisterCallback(TrackOrder, "test.trie.lifo", &data2)));
|
|
|
|
Preferences::SetBool("test.trie.lifo", true);
|
|
ASSERT_EQ(order.Length(), 2u);
|
|
EXPECT_EQ(order[0], 2);
|
|
EXPECT_EQ(order[1], 1);
|
|
|
|
Preferences::UnregisterCallback(TrackOrder, "test.trie.lifo", &data1);
|
|
Preferences::UnregisterCallback(TrackOrder, "test.trie.lifo", &data2);
|
|
}
|
|
|
|
// Even when a leaf callback is registered before its ancestor prefix callback,
|
|
// the ancestor still fires first (trie depth determines order, not creation
|
|
// order).
|
|
TEST(PrefsCallbackTrie, LeafRegisteredBeforeAncestor)
|
|
{
|
|
nsTArray<int> order;
|
|
CallbackOrder dataLeaf{&order, 2};
|
|
CallbackOrder dataAncestor{&order, 1};
|
|
|
|
Preferences::SetBool("test.trie.corder.a.b.c", false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterCallback(
|
|
TrackOrder, "test.trie.corder.a.b.c"_ns, &dataLeaf)));
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterPrefixCallback(
|
|
TrackOrder, "test.trie.corder.a"_ns, &dataAncestor)));
|
|
|
|
Preferences::SetBool("test.trie.corder.a.b.c", true);
|
|
ASSERT_EQ(order.Length(), 2u);
|
|
EXPECT_EQ(order[0], 1);
|
|
EXPECT_EQ(order[1], 2);
|
|
|
|
Preferences::UnregisterCallback(TrackOrder, "test.trie.corder.a.b.c"_ns,
|
|
&dataLeaf);
|
|
Preferences::UnregisterPrefixCallback(TrackOrder, "test.trie.corder.a"_ns,
|
|
&dataAncestor);
|
|
}
|
|
|
|
// A domain registered with a trailing dot is equivalent to one without.
|
|
TEST(PrefsCallbackTrie, TrailingDotEquivalence)
|
|
{
|
|
int count = 0;
|
|
Preferences::SetBool("test.trie.dot.a.b", false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterPrefixCallback(
|
|
IncrementCount, "test.trie.dot.a."_ns, &count)));
|
|
Preferences::SetBool("test.trie.dot.a.b", true);
|
|
EXPECT_EQ(count, 1);
|
|
// Unregistering without the trailing dot should remove the same node.
|
|
Preferences::UnregisterPrefixCallback(IncrementCount, "test.trie.dot.a"_ns,
|
|
&count);
|
|
Preferences::SetBool("test.trie.dot.a.b", false);
|
|
EXPECT_EQ(count, 1);
|
|
}
|
|
|
|
// A domain ending in more than one dot is rejected rather than partially
|
|
// normalized.
|
|
TEST(PrefsCallbackTrie, MultipleTrailingDotsRejected)
|
|
{
|
|
int count = 0;
|
|
EXPECT_EQ(Preferences::RegisterCallback(IncrementCount,
|
|
"test.trie.dots.a.."_ns, &count),
|
|
NS_ERROR_INVALID_ARG);
|
|
EXPECT_EQ(Preferences::RegisterPrefixCallback(
|
|
IncrementCount, "test.trie.dots.a.."_ns, &count),
|
|
NS_ERROR_INVALID_ARG);
|
|
EXPECT_EQ(Preferences::UnregisterCallback(IncrementCount,
|
|
"test.trie.dots.a.."_ns, &count),
|
|
NS_ERROR_INVALID_ARG);
|
|
|
|
nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
|
|
ASSERT_TRUE(prefs);
|
|
RefPtr<TestWeakPrefObserver> observer = new TestWeakPrefObserver();
|
|
EXPECT_EQ(prefs->AddObserver("test.trie.dots.d.."_ns, observer, false),
|
|
NS_ERROR_INVALID_ARG);
|
|
Preferences::SetBool("test.trie.dots.d.e", true);
|
|
EXPECT_EQ(observer->mNotifyCount, 0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Compact / MarkDead tests — verify lazy-unregister behavior.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Unregistering a callback marks it dead immediately; a pref change before
|
|
// the idle sweep does not fire the dead callback but does fire live siblings
|
|
// registered at the same path.
|
|
TEST(PrefsCallbackTrie, DeadCallbackSkippedBeforeSweep)
|
|
{
|
|
int deadCount = 0;
|
|
int liveCount = 0;
|
|
|
|
Preferences::SetBool("test.trie.dead1", false);
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterCallback(
|
|
IncrementCount, "test.trie.dead1", &deadCount)));
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterCallback(
|
|
IncrementCount, "test.trie.dead1", &liveCount)));
|
|
|
|
Preferences::UnregisterCallback(IncrementCount, "test.trie.dead1",
|
|
&deadCount);
|
|
|
|
// Fire before the idle sweep — the dead callback must not fire.
|
|
Preferences::SetBool("test.trie.dead1", true);
|
|
EXPECT_EQ(deadCount, 0);
|
|
EXPECT_EQ(liveCount, 1);
|
|
|
|
Preferences::UnregisterCallback(IncrementCount, "test.trie.dead1",
|
|
&liveCount);
|
|
}
|
|
|
|
// A "one-shot" callback that unregisters itself during its own execution.
|
|
// NotifyCallbacks does not Compact; the self-removed callback is only marked
|
|
// dead (Func == null) and the node lingers until the idle sweep. A deeper
|
|
// callback at the same change must still fire on the first notification, and
|
|
// the dead callback must not fire on subsequent notifications because
|
|
// NotifyMatching skips null-Func nodes when building each snapshot.
|
|
namespace {
|
|
|
|
struct OneShotData {
|
|
int count = 0;
|
|
nsCString path;
|
|
static void Callback(const char*, void* aData) {
|
|
auto* d = static_cast<OneShotData*>(aData);
|
|
++d->count;
|
|
Preferences::UnregisterPrefixCallback(Callback, d->path, d);
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST(PrefsCallbackTrie, DeadNodeSkippedAfterSelfUnregister)
|
|
{
|
|
int deeperCount = 0;
|
|
OneShotData oneShot;
|
|
oneShot.path = "test.trie.oneshot.a.b"_ns;
|
|
|
|
Preferences::SetBool("test.trie.oneshot.a.b.c", false);
|
|
// Register one-shot at ancestor (prefix) and a persistent callback at deeper
|
|
// path.
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterPrefixCallback(
|
|
OneShotData::Callback, "test.trie.oneshot.a.b"_ns, &oneShot)));
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterCallback(
|
|
IncrementCount, "test.trie.oneshot.a.b.c"_ns, &deeperCount)));
|
|
|
|
// First change: one-shot fires (and marks itself dead), deeper fires. The
|
|
// dead node is not compacted here; it is just skipped on later snapshots.
|
|
Preferences::SetBool("test.trie.oneshot.a.b.c", true);
|
|
EXPECT_EQ(oneShot.count, 1);
|
|
EXPECT_EQ(deeperCount, 1);
|
|
|
|
// Second change: one-shot is gone; deeper still fires.
|
|
Preferences::SetBool("test.trie.oneshot.a.b.c", false);
|
|
EXPECT_EQ(oneShot.count, 1);
|
|
EXPECT_EQ(deeperCount, 2);
|
|
|
|
Preferences::UnregisterCallback(IncrementCount, "test.trie.oneshot.a.b.c"_ns,
|
|
&deeperCount);
|
|
}
|
|
|
|
// A callback at an ancestor unregisters a callback at a descendant path while
|
|
// both are in the same notification round. The descendant callback is in the
|
|
// snapshot taken by NotifyMatching but must be skipped (Func == null) when the
|
|
// notification loop reaches it. Subsequent notifications only fire the
|
|
// ancestor.
|
|
namespace {
|
|
|
|
struct CrossUnregData {
|
|
int count = 0;
|
|
int* targetCount = nullptr; // points to the count of the node to unregister
|
|
|
|
static void Callback(const char*, void* aData) {
|
|
auto* d = static_cast<CrossUnregData*>(aData);
|
|
++d->count;
|
|
if (d->targetCount) {
|
|
Preferences::UnregisterCallback(IncrementCount, "test.trie.cross.a.b",
|
|
d->targetCount);
|
|
d->targetCount = nullptr;
|
|
}
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST(PrefsCallbackTrie, DeadNodeSkippedAfterCrossNodeUnregister)
|
|
{
|
|
int descendantCount = 0;
|
|
CrossUnregData ancestor;
|
|
ancestor.targetCount = &descendantCount;
|
|
|
|
Preferences::SetBool("test.trie.cross.a.b", false);
|
|
// Ancestor fires first (shallower depth, prefix registration).
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterPrefixCallback(
|
|
CrossUnregData::Callback, "test.trie.cross.a"_ns, &ancestor)));
|
|
ASSERT_TRUE(NS_SUCCEEDED(Preferences::RegisterCallback(
|
|
IncrementCount, "test.trie.cross.a.b"_ns, &descendantCount)));
|
|
|
|
// First change: ancestor fires and unregisters the descendant.
|
|
// The descendant is in the toNotify snapshot but is skipped (Func == null).
|
|
Preferences::SetBool("test.trie.cross.a.b", true);
|
|
EXPECT_EQ(ancestor.count, 1);
|
|
EXPECT_EQ(descendantCount, 0);
|
|
|
|
// Second change: only the ancestor fires; descendant was removed by Compact.
|
|
Preferences::SetBool("test.trie.cross.a.b", false);
|
|
EXPECT_EQ(ancestor.count, 2);
|
|
EXPECT_EQ(descendantCount, 0);
|
|
|
|
Preferences::UnregisterPrefixCallback(CrossUnregData::Callback,
|
|
"test.trie.cross.a"_ns, &ancestor);
|
|
}
|