Bug 1869842 - Extend MockRegistry to support types and subkeys/children r=nrishel

Differential Revision: https://phabricator.services.mozilla.com/D199434
This commit is contained in:
Nipun Shukla 2024-04-16 19:24:57 +00:00
parent ddec4dc371
commit 441bf7db9f
2 changed files with 327 additions and 85 deletions

View file

@ -4,81 +4,231 @@
import { MockRegistrar } from "resource://testing-common/MockRegistrar.sys.mjs"; import { MockRegistrar } from "resource://testing-common/MockRegistrar.sys.mjs";
export class MockRegistry { class MockWindowsRegKey {
constructor() { key = null;
// Three level structure of Maps pointing to Maps pointing to Maps
// this.roots is the top of the structure and has ROOT_KEY_* values
// as keys. Maps at the second level are the values of the first
// level Map, they have registry keys (also called paths) as keys.
// Third level maps are the values in second level maps, they have
// map registry names to corresponding values (which in this implementation
// are always strings).
this.roots = new Map([
[Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, new Map()],
[Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, new Map()],
[Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT, new Map()],
]);
let registry = this;
/**
* This is a mock nsIWindowsRegistry implementation. It only implements a
* subset of the interface used in tests. In particular, only values
* of type string are supported.
*/
function MockWindowsRegKey() {}
MockWindowsRegKey.prototype = {
values: null,
// --- Overridden nsISupports interface functions --- // --- Overridden nsISupports interface functions ---
QueryInterface: ChromeUtils.generateQI(["nsIWindowsRegKey"]), QueryInterface = ChromeUtils.generateQI(["nsIWindowsRegKey"]);
#assertKey() {
if (this.key) {
return;
}
throw Components.Exception("invalid registry path", Cr.NS_ERROR_FAILURE);
}
#findOrMaybeCreateKey(root, path, mode, maybeCreateCallback) {
let rootKey = MockRegistry.getRoot(root);
let parts = path.split("\\");
if (parts.some(part => !part.length)) {
throw Components.Exception("", Cr.NS_ERROR_FAILURE);
}
let key = rootKey;
for (let part of parts) {
if (!key.subkeys.has(part)) {
maybeCreateCallback(key.subkeys, part);
}
key = key.subkeys.get(part);
}
this.key = key;
}
// --- Overridden nsIWindowsRegKey interface functions --- // --- Overridden nsIWindowsRegKey interface functions ---
open(root, path) { open(root, path, mode) {
let rootKey = registry.getRoot(root); // eslint-disable-next-line no-unused-vars
if (!rootKey.has(path)) { this.#findOrMaybeCreateKey(root, path, mode, (subkeys, part) => {
rootKey.set(path, new Map()); throw Components.Exception("", Cr.NS_ERROR_FAILURE);
});
}
create(root, path, mode) {
this.#findOrMaybeCreateKey(root, path, mode, (subkeys, part) =>
subkeys.set(part, { subkeys: new Map(), values: new Map() })
);
} }
this.values = rootKey.get(path);
},
close() { close() {
this.values = null; this.key = null;
}, }
get valueCount() { get valueCount() {
if (!this.values) { this.#assertKey();
throw Components.Exception("", Cr.NS_ERROR_FAILURE); return this.key.values.size;
} }
return this.values.size;
},
hasValue(name) { hasValue(name) {
if (!this.values) { this.#assertKey();
return false; return this.key.values.has(name);
} }
return this.values.has(name);
},
getValueType() { #getValuePair(name, expectedType = null) {
return Ci.nsIWindowsRegKey.TYPE_STRING; this.#assertKey();
}, if (!this.key.values.has(name)) {
throw Components.Exception("invalid value name", Cr.NS_ERROR_FAILURE);
}
let [value, type] = this.key.values.get(name);
if (expectedType && type !== expectedType) {
throw Components.Exception("unexpected value type", Cr.NS_ERROR_FAILURE);
}
return [value, type];
}
getValueType(name) {
let [, type] = this.#getValuePair(name);
return type;
}
getValueName(index) { getValueName(index) {
if (!this.values || index >= this.values.size) { if (!this.key || index >= this.key.values.size) {
throw Components.Exception("", Cr.NS_ERROR_FAILURE); throw Components.Exception("", Cr.NS_ERROR_FAILURE);
} }
let names = Array.from(this.values.keys()); let names = Array.from(this.key.values.keys());
return names[index]; return names[index];
}, }
readStringValue(name) { readStringValue(name) {
if (!this.values) { let [value] = this.#getValuePair(name, Ci.nsIWindowsRegKey.TYPE_STRING);
throw new Error("invalid registry path"); return value;
} }
return this.values.get(name);
}, readIntValue(name) {
}; let [value] = this.#getValuePair(name, Ci.nsIWindowsRegKey.TYPE_INT);
return value;
}
readInt64Value(name) {
let [value] = this.#getValuePair(name, Ci.nsIWindowsRegKey.TYPE_INT64);
return value;
}
readBinaryValue(name) {
let [value] = this.#getValuePair(name, Ci.nsIWindowsRegKey.TYPE_BINARY);
return value;
}
#writeValuePair(name, value, type) {
this.#assertKey();
this.key.values.set(name, [value, type]);
}
writeStringValue(name, value) {
this.#writeValuePair(name, value, Ci.nsIWindowsRegKey.TYPE_STRING);
}
writeIntValue(name, value) {
this.#writeValuePair(name, value, Ci.nsIWindowsRegKey.TYPE_INT);
}
writeInt64Value(name, value) {
this.#writeValuePair(name, value, Ci.nsIWindowsRegKey.TYPE_INT64);
}
writeBinaryValue(name, value) {
this.#writeValuePair(name, value, Ci.nsIWindowsRegKey.TYPE_BINARY);
}
removeValue(name) {
this.#assertKey();
this.key.values.delete(name);
}
get childCount() {
this.#assertKey();
return this.key.subkeys.size;
}
getChildName(index) {
if (!this.key || index >= this.key.values.size) {
throw Components.Exception("", Cr.NS_ERROR_FAILURE);
}
let names = Array.from(this.key.subkeys.keys());
return names[index];
}
hasChild(name) {
this.#assertKey();
return this.key.subkeys.has(name);
}
removeChild(name) {
this.#assertKey();
let child = this.key.subkeys.get(name);
if (!child) {
throw Components.Exception("", Cr.NS_ERROR_FAILURE);
}
if (child.subkeys.size > 0) {
throw Components.Exception("", Cr.NS_ERROR_FAILURE);
}
this.key.subkeys.delete(name);
}
#findOrMaybeCreateChild(name, mode, maybeCreateCallback) {
this.#assertKey();
if (name.split("\\").length > 1) {
throw Components.Exception("", Cr.NS_ERROR_FAILURE);
}
if (!this.key.subkeys.has(name)) {
maybeCreateCallback(this.key.subkeys, name);
}
// This won't wrap in the same way as `Cc["@mozilla.org/windows-registry-key;1"].createInstance(nsIWindowsRegKey);`.
let subKey = new MockWindowsRegKey();
subKey.key = this.key.subkeys.get(name);
return subKey;
}
openChild(name, mode) {
// eslint-disable-next-line no-unused-vars
return this.#findOrMaybeCreateChild(name, mode, (subkeys, part) => {
throw Components.Exception("", Cr.NS_ERROR_FAILURE);
});
}
createChild(name, mode) {
return this.#findOrMaybeCreateChild(name, mode, (subkeys, part) =>
subkeys.set(part, { subkeys: new Map(), values: new Map() })
);
}
}
export class MockRegistry {
// All instances of `MockRegistry` share a single data-store; this is
// conceptually parallel to the Windows registry, which is a shared global
// resource. It would be possible to have separate data-stores for separate
// instances with a little adjustment to `MockWindowsRegKey`.
//
// Top-level map is indexed by roots. A "key" is an object that has
// `subkeys` and `values`, both maps indexed by strings. Subkey items are
// again "key" objects. Value items are `[value, type]` pairs.
//
// In pseudo-code:
//
// {Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER:
// {subkeys:
// {child: {subkeys: {}, values: {key: ["string_value", Ci.nsIWindowsRegKey.TYPE_STRING]}}},
// values: {}
// },
// ...
// }
static roots;
constructor() {
MockRegistry.roots = new Map([
[
Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
{ subkeys: new Map(), values: new Map() },
],
[
Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
{ subkeys: new Map(), values: new Map() },
],
[
Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT,
{ subkeys: new Map(), values: new Map() },
],
]);
// See bug 1688838 - nsNotifyAddrListener::CheckAdaptersAddresses might // See bug 1688838 - nsNotifyAddrListener::CheckAdaptersAddresses might
// attempt to use the registry off the main thread, so we disable that // attempt to use the registry off the main thread, so we disable that
@ -100,7 +250,7 @@ export class MockRegistry {
this.cid = MockRegistrar.register( this.cid = MockRegistrar.register(
"@mozilla.org/windows-registry-key;1", "@mozilla.org/windows-registry-key;1",
MockWindowsRegKey () => new MockWindowsRegKey()
); );
} }
@ -121,7 +271,7 @@ export class MockRegistry {
this.cid = null; this.cid = null;
} }
getRoot(root) { static getRoot(root) {
if (!this.roots.has(root)) { if (!this.roots.has(root)) {
throw new Error(`No such root ${root}`); throw new Error(`No such root ${root}`);
} }
@ -129,17 +279,58 @@ export class MockRegistry {
} }
setValue(root, path, name, value) { setValue(root, path, name, value) {
let rootKey = this.getRoot(root); let key = new MockWindowsRegKey();
key.create(root, path, Ci.nsIWindowsRegKey.ACCESS_ALL);
if (!rootKey.has(path)) { if (value == null) {
rootKey.set(path, new Map()); try {
key.removeValue(name);
} catch (e) {
if (
!(e instanceof Ci.nsIException && e.result == Cr.NS_ERROR_FAILURE)
) {
throw e;
}
}
} else {
key.writeStringValue(name, value);
}
} }
let pathmap = rootKey.get(path); /**
if (value == null) { * Dump given `key` (or, if not given, all roots), and all its value and its
pathmap.delete(name); * subkeys recursively, using the given function to `printOneLine`.
*/
static dump(key = null, indent = "", printOneLine = console.log) {
let types = new Map([
[1, "REG_SZ"],
[3, "REG_BINARY"],
[4, "REG_DWORD"],
[11, "REG_QWORD"],
]);
if (!key) {
let roots = [
"ROOT_KEY_LOCAL_MACHINE",
"ROOT_KEY_CURRENT_USER",
"ROOT_KEY_CLASSES_ROOT",
];
for (let root of roots) {
printOneLine(indent + root);
this.dump(
this.roots.get(Ci.nsIWindowsRegKey[root]),
" " + indent,
printOneLine
);
}
} else { } else {
pathmap.set(name, value); for (let [k, v] of key.values.entries()) {
let [value, type] = v;
printOneLine(`${indent}${k}: ${value} (${types.get(type)})`);
}
for (let [k, child] of key.subkeys.entries()) {
printOneLine(indent + k);
this.dump(child, " " + indent, printOneLine);
}
} }
} }
} }

View file

@ -5,10 +5,47 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file, * 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/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
const { MockRegistry } = ChromeUtils.importESModule(
"resource://testing-common/MockRegistry.sys.mjs"
);
const nsIWindowsRegKey = Ci.nsIWindowsRegKey; const nsIWindowsRegKey = Ci.nsIWindowsRegKey;
let regKeyComponent = Cc["@mozilla.org/windows-registry-key;1"]; let regKeyComponent = Cc["@mozilla.org/windows-registry-key;1"];
function run_test() { // We run these tests twice: once against the native Windows registry, and a
// second time against `MockRegistry`. This gives some confidence that
// `MockRegistry` implements the same APIs as the native Windows registry.
// This order is important: the native registry test must run first, because
// otherwise we would end up running the `MockRegistry` test twice without
// checking it against the native Windows registry.
add_task(function test_native_registry() {
info("Running test for native Windows registry");
run_one_test();
});
add_task(function test_MockRegistry() {
let registry = new MockRegistry();
registerCleanupFunction(() => {
registry.shutdown();
});
// Before, there's nothing -- just the 3 roots (HKLM, HKCU, HKCR).
let linesBefore = [];
MockRegistry.dump(null, "", linesBefore.push.bind(linesBefore));
strictEqual(linesBefore.length, 3);
info("Running test for MockRegistry");
run_one_test({ cleanup: false });
// After, there's something -- more than just the roots.
let linesAfter = [];
MockRegistry.dump(null, "", linesAfter.push.bind(linesAfter));
strictEqual(linesAfter.length, 8);
});
function run_one_test({ cleanup = true } = {}) {
//* create a key structure in a spot that's normally writable (somewhere under HKCU). //* create a key structure in a spot that's normally writable (somewhere under HKCU).
let testKey = regKeyComponent.createInstance(nsIWindowsRegKey); let testKey = regKeyComponent.createInstance(nsIWindowsRegKey);
@ -28,11 +65,16 @@ function run_test() {
//* check that the get* functions fail with the right exception codes if we ask for the wrong type or if the value name doesn't exist at all //* check that the get* functions fail with the right exception codes if we ask for the wrong type or if the value name doesn't exist at all
test_invalidread_functions(testKey); test_invalidread_functions(testKey);
//* check that removing/deleting values works
test_remove_functions(testKey);
//* check that creating/enumerating/deleting child keys works //* check that creating/enumerating/deleting child keys works
test_childkey_functions(testKey); test_childkey_functions(testKey);
//* clean up //* clean up
if (cleanup) {
cleanup_test_run(testKey, keyName); cleanup_test_run(testKey, keyName);
}
} }
function setup_test_run(testKey, keyName) { function setup_test_run(testKey, keyName) {
@ -152,6 +194,15 @@ function test_invalidread_functions(testKey) {
} }
} }
function test_remove_functions(testKey) {
strictEqual(testKey.valueCount, 4);
testKey.removeValue(TESTDATA_INT64NAME);
strictEqual(testKey.valueCount, 3);
testKey.removeValue(TESTDATA_INT64NAME);
strictEqual(testKey.valueCount, 3);
}
function test_childkey_functions(testKey) { function test_childkey_functions(testKey) {
strictEqual(testKey.childCount, 0); strictEqual(testKey.childCount, 0);
strictEqual(testKey.hasChild(TESTDATA_CHILD_KEY), false); strictEqual(testKey.hasChild(TESTDATA_CHILD_KEY), false);