diff options
Diffstat (limited to 'toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js')
-rw-r--r-- | toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js | 2711 |
1 files changed, 2711 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js new file mode 100644 index 0000000000..504692e6c4 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js @@ -0,0 +1,2711 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { AddonManager, AddonManagerPrivate } = ChromeUtils.import( + "resource://gre/modules/AddonManager.jsm" +); +ChromeUtils.import("resource://gre/modules/TelemetryEnvironment.jsm", this); +ChromeUtils.import("resource://gre/modules/Preferences.jsm", this); +ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm", this); +ChromeUtils.import("resource://gre/modules/Timer.jsm", this); +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this); +ChromeUtils.import("resource://testing-common/ContentTaskUtils.jsm", this); +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +ChromeUtils.import("resource://testing-common/MockRegistrar.jsm", this); +const { FileUtils } = ChromeUtils.import( + "resource://gre/modules/FileUtils.jsm" +); +const { CommonUtils } = ChromeUtils.import( + "resource://services-common/utils.js" +); +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); +const { SearchTestUtils } = ChromeUtils.import( + "resource://testing-common/SearchTestUtils.jsm" +); +if (AppConstants.MOZ_GLEAN) { + Cu.importGlobalProperties(["Glean"]); +} + +// AttributionCode is only needed for Firefox +ChromeUtils.defineModuleGetter( + this, + "AttributionCode", + "resource:///modules/AttributionCode.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "ExtensionTestUtils", + "resource://testing-common/ExtensionXPCShellUtils.jsm" +); + +SearchTestUtils.init(this); + +async function installXPIFromURL(url) { + let install = await AddonManager.getInstallForURL(url); + return install.install(); +} + +function promiseNextTick() { + return new Promise(resolve => executeSoon(resolve)); +} + +// The webserver hosting the addons. +var gHttpServer = null; +// The URL of the webserver root. +var gHttpRoot = null; +// The URL of the data directory, on the webserver. +var gDataRoot = null; + +const PLATFORM_VERSION = "1.9.2"; +const APP_VERSION = "1"; +const APP_ID = "xpcshell@tests.mozilla.org"; +const APP_NAME = "XPCShell"; + +const DISTRIBUTION_ID = "distributor-id"; +const DISTRIBUTION_VERSION = "4.5.6b"; +const DISTRIBUTOR_NAME = "Some Distributor"; +const DISTRIBUTOR_CHANNEL = "A Channel"; +const PARTNER_NAME = "test"; +const PARTNER_ID = "NicePartner-ID-3785"; +const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC = + "distribution-customization-complete"; + +const GFX_VENDOR_ID = "0xabcd"; +const GFX_DEVICE_ID = "0x1234"; + +// The profile reset date, in milliseconds (Today) +const PROFILE_RESET_DATE_MS = Date.now(); +// The profile creation date, in milliseconds (Yesterday). +const PROFILE_FIRST_USE_MS = PROFILE_RESET_DATE_MS - MILLISECONDS_PER_DAY; +const PROFILE_CREATION_DATE_MS = PROFILE_FIRST_USE_MS - MILLISECONDS_PER_DAY; + +const FLASH_PLUGIN_NAME = "Shockwave Flash"; +const FLASH_PLUGIN_DESC = "A mock flash plugin"; +const FLASH_PLUGIN_VERSION = "\u201c1.1.1.1\u201d"; +const PLUGIN_MIME_TYPE1 = "application/x-shockwave-flash"; +const PLUGIN_MIME_TYPE2 = "text/plain"; + +const PLUGIN2_NAME = "Quicktime"; +const PLUGIN2_DESC = "A mock Quicktime plugin"; +const PLUGIN2_VERSION = "2.3"; + +const PLUGIN_UPDATED_TOPIC = "plugins-list-updated"; + +// system add-ons are enabled at startup, so record date when the test starts +const SYSTEM_ADDON_INSTALL_DATE = Date.now(); + +const EXPECTED_HDD_FIELDS = ["profile", "binary", "system"]; + +// Valid attribution code to write so that settings.attribution can be tested. +const ATTRIBUTION_CODE = "source%3Dgoogle.com"; + +const pluginHost = Cc["@mozilla.org/plugin/host;1"].getService( + Ci.nsIPluginHost +); + +/** + * Used to mock plugin tags in our fake plugin host. + */ +function PluginTag(aName, aDescription, aVersion, aEnabled) { + this.pluginTag = pluginHost.createFakePlugin({ + handlerURI: "resource://fake-plugin/${Math.random()}.xhtml", + mimeEntries: this.mimeTypes.map(type => ({ type })), + name: aName, + description: aDescription, + fileName: `${aName}.so`, + version: aVersion, + }); + this.name = aName; + this.description = aDescription; + this.version = aVersion; + this.disabled = !aEnabled; +} + +PluginTag.prototype = { + name: null, + description: null, + version: null, + filename: null, + fullpath: null, + blocklisted: false, + clicktoplay: true, + + get disabled() { + return this.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED; + }, + set disabled(val) { + this.pluginTag.enabledState = + Ci.nsIPluginTag[val ? "STATE_DISABLED" : "STATE_CLICKTOPLAY"]; + }, + + mimeTypes: [PLUGIN_MIME_TYPE1, PLUGIN_MIME_TYPE2], + + getMimeTypes() { + return this.mimeTypes; + }, +}; + +// A container for the plugins handled by the fake plugin host. +var gInstalledPlugins = [ + new PluginTag("Java", "A mock Java plugin", "1.0", false /* Disabled */), + new PluginTag( + FLASH_PLUGIN_NAME, + FLASH_PLUGIN_DESC, + FLASH_PLUGIN_VERSION, + true + ), +]; + +// A fake plugin host for testing plugin telemetry environment. +var PluginHost = { + getPluginTags() { + return gInstalledPlugins.map(plugin => plugin.pluginTag); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIPluginHost"]), +}; + +function registerFakePluginHost() { + MockRegistrar.register("@mozilla.org/plugin/host;1", PluginHost); +} + +var SysInfo = { + overrides: {}, + + getProperty(name) { + // Assert.ok(false, "Mock SysInfo: " + name + ", " + JSON.stringify(this.overrides)); + if (name in this.overrides) { + return this.overrides[name]; + } + + return this._genuine.QueryInterface(Ci.nsIPropertyBag).getProperty(name); + }, + + getPropertyAsACString(name) { + return this.get(name); + }, + + getPropertyAsUint32(name) { + return this.get(name); + }, + + get(name) { + return this._genuine.QueryInterface(Ci.nsIPropertyBag2).get(name); + }, + + get diskInfo() { + return this._genuine.QueryInterface(Ci.nsISystemInfo).diskInfo; + }, + + get osInfo() { + return this._genuine.QueryInterface(Ci.nsISystemInfo).osInfo; + }, + + get processInfo() { + return this._genuine.QueryInterface(Ci.nsISystemInfo).processInfo; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIPropertyBag2", "nsISystemInfo"]), +}; + +function registerFakeSysInfo() { + MockRegistrar.register("@mozilla.org/system-info;1", SysInfo); +} + +function MockAddonWrapper(aAddon) { + this.addon = aAddon; +} +MockAddonWrapper.prototype = { + get id() { + return this.addon.id; + }, + + get type() { + return "service"; + }, + + get appDisabled() { + return false; + }, + + get isCompatible() { + return true; + }, + + get isPlatformCompatible() { + return true; + }, + + get scope() { + return AddonManager.SCOPE_PROFILE; + }, + + get foreignInstall() { + return false; + }, + + get providesUpdatesSecurely() { + return true; + }, + + get blocklistState() { + return 0; // Not blocked. + }, + + get pendingOperations() { + return AddonManager.PENDING_NONE; + }, + + get permissions() { + return AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_DISABLE; + }, + + get isActive() { + return true; + }, + + get name() { + return this.addon.name; + }, + + get version() { + return this.addon.version; + }, + + get creator() { + return new AddonManagerPrivate.AddonAuthor(this.addon.author); + }, + + get userDisabled() { + return this.appDisabled; + }, +}; + +function createMockAddonProvider(aName) { + let mockProvider = { + _addons: [], + + get name() { + return aName; + }, + + addAddon(aAddon) { + this._addons.push(aAddon); + AddonManagerPrivate.callAddonListeners( + "onInstalled", + new MockAddonWrapper(aAddon) + ); + }, + + async getAddonsByTypes(aTypes) { + return this._addons + .filter(a => !aTypes || aTypes.includes(a.type)) + .map(a => new MockAddonWrapper(a)); + }, + + shutdown() { + return Promise.resolve(); + }, + }; + + return mockProvider; +} + +function spoofGfxAdapter() { + try { + let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug); + gfxInfo.fireTestProcess(); + gfxInfo.spoofVendorID(GFX_VENDOR_ID); + gfxInfo.spoofDeviceID(GFX_DEVICE_ID); + } catch (x) { + // If we can't test gfxInfo, that's fine, we'll note it later. + } +} + +function spoofProfileReset() { + return CommonUtils.writeJSON( + { + created: PROFILE_CREATION_DATE_MS, + reset: PROFILE_RESET_DATE_MS, + firstUse: PROFILE_FIRST_USE_MS, + }, + OS.Path.join(OS.Constants.Path.profileDir, "times.json") + ); +} + +function spoofPartnerInfo() { + let prefsToSpoof = {}; + prefsToSpoof["distribution.id"] = DISTRIBUTION_ID; + prefsToSpoof["distribution.version"] = DISTRIBUTION_VERSION; + prefsToSpoof["app.distributor"] = DISTRIBUTOR_NAME; + prefsToSpoof["app.distributor.channel"] = DISTRIBUTOR_CHANNEL; + prefsToSpoof["app.partner.test"] = PARTNER_NAME; + prefsToSpoof["mozilla.partner.id"] = PARTNER_ID; + + // Spoof the preferences. + for (let pref in prefsToSpoof) { + Preferences.set(pref, prefsToSpoof[pref]); + } +} + +async function spoofAttributionData() { + if (gIsWindows || gIsMac) { + AttributionCode._clearCache(); + await AttributionCode.writeAttributionFile(ATTRIBUTION_CODE); + } +} + +function cleanupAttributionData() { + if (gIsWindows || gIsMac) { + AttributionCode.attributionFile.remove(false); + AttributionCode._clearCache(); + } +} + +/** + * Check that a value is a string and not empty. + * + * @param aValue The variable to check. + * @return True if |aValue| has type "string" and is not empty, False otherwise. + */ +function checkString(aValue) { + return typeof aValue == "string" && aValue != ""; +} + +/** + * If value is non-null, check if it's a valid string. + * + * @param aValue The variable to check. + * @return True if it's null or a valid string, false if it's non-null and an invalid + * string. + */ +function checkNullOrString(aValue) { + if (aValue) { + return checkString(aValue); + } else if (aValue === null) { + return true; + } + + return false; +} + +/** + * If value is non-null, check if it's a boolean. + * + * @param aValue The variable to check. + * @return True if it's null or a valid boolean, false if it's non-null and an invalid + * boolean. + */ +function checkNullOrBool(aValue) { + return aValue === null || typeof aValue == "boolean"; +} + +function checkBuildSection(data) { + const expectedInfo = { + applicationId: APP_ID, + applicationName: APP_NAME, + buildId: gAppInfo.appBuildID, + version: APP_VERSION, + vendor: "Mozilla", + platformVersion: PLATFORM_VERSION, + xpcomAbi: "noarch-spidermonkey", + }; + + Assert.ok("build" in data, "There must be a build section in Environment."); + + for (let f in expectedInfo) { + Assert.ok(checkString(data.build[f]), f + " must be a valid string."); + Assert.equal( + data.build[f], + expectedInfo[f], + f + " must have the correct value." + ); + } + + // Make sure architecture is in the environment. + Assert.ok(checkString(data.build.architecture)); + + Assert.equal( + data.build.updaterAvailable, + AppConstants.MOZ_UPDATER, + "build.updaterAvailable must equal AppConstants.MOZ_UPDATER" + ); +} + +function checkSettingsSection(data) { + const EXPECTED_FIELDS_TYPES = { + blocklistEnabled: "boolean", + e10sEnabled: "boolean", + e10sMultiProcesses: "number", + fissionEnabled: "boolean", + intl: "object", + locale: "string", + telemetryEnabled: "boolean", + update: "object", + userPrefs: "object", + }; + + Assert.ok( + "settings" in data, + "There must be a settings section in Environment." + ); + + for (let f in EXPECTED_FIELDS_TYPES) { + Assert.equal( + typeof data.settings[f], + EXPECTED_FIELDS_TYPES[f], + f + " must have the correct type." + ); + } + + // This property is not always present, but when it is, it must be a number. + if ("launcherProcessState" in data.settings) { + Assert.equal(typeof data.settings.launcherProcessState, "number"); + } + + // Check "addonCompatibilityCheckEnabled" separately. + Assert.equal( + data.settings.addonCompatibilityCheckEnabled, + AddonManager.checkCompatibility + ); + + // Check "isDefaultBrowser" separately, as it is not available on Android an can either be + // null or boolean on other platforms. + if (gIsAndroid) { + Assert.ok( + !("isDefaultBrowser" in data.settings), + "Must not be available on Android." + ); + } else if ("isDefaultBrowser" in data.settings) { + // isDefaultBrowser might not be available in the payload, since it's + // gathered after the session was restored. + Assert.ok(checkNullOrBool(data.settings.isDefaultBrowser)); + } + + // Check "channel" separately, as it can either be null or string. + let update = data.settings.update; + Assert.ok(checkNullOrString(update.channel)); + Assert.equal(typeof update.enabled, "boolean"); + Assert.equal(typeof update.autoDownload, "boolean"); + + // Check "defaultSearchEngine" separately, as it can either be undefined or string. + if ("defaultSearchEngine" in data.settings) { + checkString(data.settings.defaultSearchEngine); + Assert.equal(typeof data.settings.defaultSearchEngineData, "object"); + } + + if ("defaultPrivateSearchEngineData" in data.settings) { + Assert.equal(typeof data.settings.defaultPrivateSearchEngineData, "object"); + } + + if ((gIsWindows || gIsMac) && AppConstants.MOZ_BUILD_APP == "browser") { + Assert.equal(typeof data.settings.attribution, "object"); + Assert.equal(data.settings.attribution.source, "google.com"); + } + + checkIntlSettings(data.settings); +} + +function checkIntlSettings({ intl }) { + let fields = [ + "requestedLocales", + "availableLocales", + "appLocales", + "acceptLanguages", + ]; + + for (let field of fields) { + Assert.ok(Array.isArray(intl[field]), `${field} is an array`); + } + + // These fields may be null if they aren't ready yet. This is mostly to deal + // with test failures on Android, but they aren't guaranteed to exist. + let optionalFields = ["systemLocales", "regionalPrefsLocales"]; + + for (let field of optionalFields) { + let isArray = Array.isArray(intl[field]); + let isNull = intl[field] === null; + Assert.ok(isArray || isNull, `${field} is an array or null`); + } +} + +function checkProfileSection(data) { + Assert.ok( + "profile" in data, + "There must be a profile section in Environment." + ); + Assert.equal( + data.profile.creationDate, + truncateToDays(PROFILE_CREATION_DATE_MS) + ); + Assert.equal(data.profile.resetDate, truncateToDays(PROFILE_RESET_DATE_MS)); + Assert.equal(data.profile.firstUseDate, truncateToDays(PROFILE_FIRST_USE_MS)); +} + +function checkPartnerSection(data, isInitial) { + const EXPECTED_FIELDS = { + distributionId: DISTRIBUTION_ID, + distributionVersion: DISTRIBUTION_VERSION, + partnerId: PARTNER_ID, + distributor: DISTRIBUTOR_NAME, + distributorChannel: DISTRIBUTOR_CHANNEL, + }; + + Assert.ok( + "partner" in data, + "There must be a partner section in Environment." + ); + + for (let f in EXPECTED_FIELDS) { + let expected = isInitial ? null : EXPECTED_FIELDS[f]; + Assert.strictEqual( + data.partner[f], + expected, + f + " must have the correct value." + ); + } + + // Check that "partnerNames" exists and contains the correct element. + Assert.ok(Array.isArray(data.partner.partnerNames)); + if (isInitial) { + Assert.equal(data.partner.partnerNames.length, 0); + } else { + Assert.ok(data.partner.partnerNames.includes(PARTNER_NAME)); + } +} + +function checkGfxAdapter(data) { + const EXPECTED_ADAPTER_FIELDS_TYPES = { + description: "string", + vendorID: "string", + deviceID: "string", + subsysID: "string", + RAM: "number", + driver: "string", + driverVendor: "string", + driverVersion: "string", + driverDate: "string", + GPUActive: "boolean", + }; + + for (let f in EXPECTED_ADAPTER_FIELDS_TYPES) { + Assert.ok(f in data, f + " must be available."); + + if (data[f]) { + // Since we have a non-null value, check if it has the correct type. + Assert.equal( + typeof data[f], + EXPECTED_ADAPTER_FIELDS_TYPES[f], + f + " must have the correct type." + ); + } + } +} + +function checkSystemSection(data, assertProcessData) { + const EXPECTED_FIELDS = [ + "memoryMB", + "cpu", + "os", + "hdd", + "gfx", + "appleModelId", + ]; + + Assert.ok("system" in data, "There must be a system section in Environment."); + + // Make sure we have all the top level sections and fields. + for (let f of EXPECTED_FIELDS) { + Assert.ok(f in data.system, f + " must be available."); + } + + Assert.ok( + Number.isFinite(data.system.memoryMB), + "MemoryMB must be a number." + ); + + if (assertProcessData) { + if (gIsWindows || gIsMac || gIsLinux) { + let EXTRA_CPU_FIELDS = [ + "cores", + "model", + "family", + "stepping", + "l2cacheKB", + "l3cacheKB", + "speedMHz", + "vendor", + ]; + + for (let f of EXTRA_CPU_FIELDS) { + // Note this is testing TelemetryEnvironment.js only, not that the + // values are valid - null is the fallback. + Assert.ok(f in data.system.cpu, f + " must be available under cpu."); + } + + if (gIsWindows) { + Assert.equal( + typeof data.system.isWow64, + "boolean", + "isWow64 must be available on Windows and have the correct type." + ); + Assert.equal( + typeof data.system.isWowARM64, + "boolean", + "isWowARM64 must be available on Windows and have the correct type." + ); + Assert.ok( + "virtualMaxMB" in data.system, + "virtualMaxMB must be available." + ); + Assert.ok( + Number.isFinite(data.system.virtualMaxMB), + "virtualMaxMB must be a number." + ); + + for (let f of [ + "count", + "model", + "family", + "stepping", + "l2cacheKB", + "l3cacheKB", + "speedMHz", + ]) { + Assert.ok( + Number.isFinite(data.system.cpu[f]), + f + " must be a number if non null." + ); + } + } + + // These should be numbers if they are not null + for (let f of [ + "count", + "model", + "family", + "stepping", + "l2cacheKB", + "l3cacheKB", + "speedMHz", + ]) { + Assert.ok( + !(f in data.system.cpu) || + data.system.cpu[f] === null || + Number.isFinite(data.system.cpu[f]), + f + " must be a number if non null." + ); + } + + // We insist these are available + for (let f of ["cores"]) { + Assert.ok( + !(f in data.system.cpu) || Number.isFinite(data.system.cpu[f]), + f + " must be a number if non null." + ); + } + } + } + + let cpuData = data.system.cpu; + + Assert.ok( + Array.isArray(cpuData.extensions), + "CPU extensions must be available." + ); + + let osData = data.system.os; + Assert.ok(checkNullOrString(osData.name)); + Assert.ok(checkNullOrString(osData.version)); + Assert.ok(checkNullOrString(osData.locale)); + + // Service pack is only available on Windows. + if (gIsWindows) { + Assert.ok( + Number.isFinite(osData.servicePackMajor), + "ServicePackMajor must be a number." + ); + Assert.ok( + Number.isFinite(osData.servicePackMinor), + "ServicePackMinor must be a number." + ); + if ("windowsBuildNumber" in osData) { + // This might not be available on all Windows platforms. + Assert.ok( + Number.isFinite(osData.windowsBuildNumber), + "windowsBuildNumber must be a number." + ); + } + if ("windowsUBR" in osData) { + // This might not be available on all Windows platforms. + Assert.ok( + osData.windowsUBR === null || Number.isFinite(osData.windowsUBR), + "windowsUBR must be null or a number." + ); + } + } else if (gIsAndroid) { + Assert.ok(checkNullOrString(osData.kernelVersion)); + } + + for (let disk of EXPECTED_HDD_FIELDS) { + Assert.ok(checkNullOrString(data.system.hdd[disk].model)); + Assert.ok(checkNullOrString(data.system.hdd[disk].revision)); + Assert.ok(checkNullOrString(data.system.hdd[disk].type)); + } + + let gfxData = data.system.gfx; + Assert.ok("D2DEnabled" in gfxData); + Assert.ok("DWriteEnabled" in gfxData); + Assert.ok("Headless" in gfxData); + Assert.ok("EmbeddedInFirefoxReality" in gfxData); + // DWriteVersion is disabled due to main thread jank and will be enabled + // again as part of bug 1154500. + // Assert.ok("DWriteVersion" in gfxData); + if (gIsWindows) { + Assert.equal(typeof gfxData.D2DEnabled, "boolean"); + Assert.equal(typeof gfxData.DWriteEnabled, "boolean"); + Assert.equal(typeof gfxData.EmbeddedInFirefoxReality, "boolean"); + // As above, will be enabled again as part of bug 1154500. + // Assert.ok(checkString(gfxData.DWriteVersion)); + } + + Assert.ok("adapters" in gfxData); + Assert.ok( + !!gfxData.adapters.length, + "There must be at least one GFX adapter." + ); + for (let adapter of gfxData.adapters) { + checkGfxAdapter(adapter); + } + Assert.equal(typeof gfxData.adapters[0].GPUActive, "boolean"); + Assert.ok( + gfxData.adapters[0].GPUActive, + "The first GFX adapter must be active." + ); + + Assert.ok(Array.isArray(gfxData.monitors)); + if (gIsWindows || gIsMac || gIsLinux) { + Assert.ok(gfxData.monitors.length >= 1, "There is at least one monitor."); + Assert.equal(typeof gfxData.monitors[0].screenWidth, "number"); + Assert.equal(typeof gfxData.monitors[0].screenHeight, "number"); + if (gIsWindows) { + Assert.equal(typeof gfxData.monitors[0].refreshRate, "number"); + Assert.equal(typeof gfxData.monitors[0].pseudoDisplay, "boolean"); + } + if (gIsMac) { + Assert.equal(typeof gfxData.monitors[0].scale, "number"); + } + } + + Assert.equal(typeof gfxData.features, "object"); + Assert.equal(typeof gfxData.features.compositor, "string"); + + Assert.equal(typeof gfxData.features.gpuProcess, "object"); + Assert.equal(typeof gfxData.features.gpuProcess.status, "string"); + + try { + // If we've not got nsIGfxInfoDebug, then this will throw and stop us doing + // this test. + let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug); + + if (gIsWindows || gIsMac) { + Assert.equal(GFX_VENDOR_ID, gfxData.adapters[0].vendorID); + Assert.equal(GFX_DEVICE_ID, gfxData.adapters[0].deviceID); + } + + let features = gfxInfo.getFeatures(); + Assert.equal(features.compositor, gfxData.features.compositor); + Assert.equal( + features.gpuProcess.status, + gfxData.features.gpuProcess.status + ); + Assert.equal(features.opengl, gfxData.features.opengl); + Assert.equal(features.webgl, gfxData.features.webgl); + } catch (e) {} + + if (gIsMac) { + Assert.ok(checkString(data.system.appleModelId)); + } else { + Assert.ok(checkNullOrString(data.system.appleModelId)); + } + + // This feature is only available on Windows 8+ + if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) { + Assert.ok("sec" in data.system, "sec must be available under data.system"); + + let SEC_FIELDS = ["antivirus", "antispyware", "firewall"]; + for (let f of SEC_FIELDS) { + Assert.ok( + f in data.system.sec, + f + " must be available under data.system.sec" + ); + + let value = data.system.sec[f]; + // value is null on Windows Server + Assert.ok( + value === null || Array.isArray(value), + f + " must be either null or an array" + ); + if (Array.isArray(value)) { + for (let product of value) { + Assert.equal( + typeof product, + "string", + "Each element of " + f + " must be a string" + ); + } + } + } + } +} + +function checkActiveAddon(data, partialRecord) { + let signedState = "number"; + // system add-ons have an undefined signState + if (data.isSystem) { + signedState = "undefined"; + } + + const EXPECTED_ADDON_FIELDS_TYPES = { + version: "string", + scope: "number", + type: "string", + updateDay: "number", + isSystem: "boolean", + isWebExtension: "boolean", + multiprocessCompatible: "boolean", + }; + + const FULL_ADDON_FIELD_TYPES = { + blocklisted: "boolean", + name: "string", + userDisabled: "boolean", + appDisabled: "boolean", + foreignInstall: "boolean", + hasBinaryComponents: "boolean", + installDay: "number", + signedState, + }; + + let fields = EXPECTED_ADDON_FIELDS_TYPES; + if (!partialRecord) { + fields = Object.assign({}, fields, FULL_ADDON_FIELD_TYPES); + } + + for (let [name, type] of Object.entries(fields)) { + Assert.ok(name in data, name + " must be available."); + Assert.equal( + typeof data[name], + type, + name + " must have the correct type." + ); + } + + if (!partialRecord) { + // We check "description" separately, as it can be null. + Assert.ok(checkNullOrString(data.description)); + } +} + +function checkPlugin(data) { + const EXPECTED_PLUGIN_FIELDS_TYPES = { + name: "string", + version: "string", + description: "string", + blocklisted: "boolean", + disabled: "boolean", + clicktoplay: "boolean", + updateDay: "number", + }; + + for (let f in EXPECTED_PLUGIN_FIELDS_TYPES) { + Assert.ok(f in data, f + " must be available."); + Assert.equal( + typeof data[f], + EXPECTED_PLUGIN_FIELDS_TYPES[f], + f + " must have the correct type." + ); + } + + Assert.ok(Array.isArray(data.mimeTypes)); + for (let type of data.mimeTypes) { + Assert.ok(checkString(type)); + } +} + +function checkTheme(data) { + const EXPECTED_THEME_FIELDS_TYPES = { + id: "string", + blocklisted: "boolean", + name: "string", + userDisabled: "boolean", + appDisabled: "boolean", + version: "string", + scope: "number", + foreignInstall: "boolean", + installDay: "number", + updateDay: "number", + }; + + for (let f in EXPECTED_THEME_FIELDS_TYPES) { + Assert.ok(f in data, f + " must be available."); + Assert.equal( + typeof data[f], + EXPECTED_THEME_FIELDS_TYPES[f], + f + " must have the correct type." + ); + } + + // We check "description" separately, as it can be null. + Assert.ok(checkNullOrString(data.description)); +} + +function checkActiveGMPlugin(data) { + // GMP plugin version defaults to null until GMPDownloader runs to update it. + if (data.version) { + Assert.equal(typeof data.version, "string"); + } + Assert.equal(typeof data.userDisabled, "boolean"); + Assert.equal(typeof data.applyBackgroundUpdates, "number"); +} + +function checkAddonsSection(data, expectBrokenAddons, partialAddonsRecords) { + const EXPECTED_FIELDS = [ + "activeAddons", + "theme", + "activePlugins", + "activeGMPlugins", + ]; + + Assert.ok( + "addons" in data, + "There must be an addons section in Environment." + ); + for (let f of EXPECTED_FIELDS) { + Assert.ok(f in data.addons, f + " must be available."); + } + + // Check the active addons, if available. + if (!expectBrokenAddons) { + let activeAddons = data.addons.activeAddons; + for (let addon in activeAddons) { + checkActiveAddon(activeAddons[addon], partialAddonsRecords); + } + } + + // Check "theme" structure. + if (Object.keys(data.addons.theme).length !== 0) { + checkTheme(data.addons.theme); + } + + // Check the active plugins. + Assert.ok(Array.isArray(data.addons.activePlugins)); + for (let plugin of data.addons.activePlugins) { + checkPlugin(plugin); + } + + // Check active GMPlugins + let activeGMPlugins = data.addons.activeGMPlugins; + for (let gmPlugin in activeGMPlugins) { + checkActiveGMPlugin(activeGMPlugins[gmPlugin]); + } +} + +function checkExperimentsSection(data) { + // We don't expect the experiments section to be always available. + let experiments = data.experiments || {}; + if (!Object.keys(experiments).length) { + return; + } + + for (let id in experiments) { + Assert.ok(checkString(id), id + " must be a valid string."); + + // Check that we have valid experiment info. + let experimentData = experiments[id]; + Assert.ok( + "branch" in experimentData, + "The experiment must have branch data." + ); + Assert.ok( + checkString(experimentData.branch), + "The experiment data must be valid." + ); + if ("type" in experimentData) { + Assert.ok(checkString(experimentData.type)); + } + } +} + +function checkEnvironmentData(data, options = {}) { + const { + isInitial = false, + expectBrokenAddons = false, + assertProcessData = false, + } = options; + + checkBuildSection(data); + checkSettingsSection(data); + checkProfileSection(data); + checkPartnerSection(data, isInitial); + checkSystemSection(data, assertProcessData); + checkAddonsSection(data, expectBrokenAddons); +} + +add_task(async function setup() { + registerFakeSysInfo(); + spoofGfxAdapter(); + do_get_profile(); + + if (AppConstants.MOZ_GLEAN) { + // We need to ensure FOG is initialized, otherwise we will panic trying to get test values. + let FOG = Cc["@mozilla.org/toolkit/glean;1"].createInstance(Ci.nsIFOG); + FOG.initializeFOG(); + } + + // The system add-on must be installed before AddonManager is started. + const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true); + do_get_file("system.xpi").copyTo( + distroDir, + "tel-system-xpi@tests.mozilla.org.xpi" + ); + let system_addon = FileUtils.File(distroDir.path); + system_addon.append("tel-system-xpi@tests.mozilla.org.xpi"); + system_addon.lastModifiedTime = SYSTEM_ADDON_INSTALL_DATE; + loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION); + + // The test runs in a fresh profile so starting the AddonManager causes + // the addons database to be created (as does setting new theme). + // For test_addonsStartup below, we want to test a "warm" startup where + // there is already a database on disk. Simulate that here by just + // restarting the AddonManager. + await AddonTestUtils.promiseShutdownManager(); + await AddonTestUtils.overrideBuiltIns({ system: [] }); + AddonTestUtils.addonStartup.remove(true); + await AddonTestUtils.promiseStartupManager(); + // Override ExtensionXPCShellUtils.jsm's overriding of the pref as the + // search service needs it. + Services.prefs.clearUserPref("services.settings.default_bucket"); + + // Register a fake plugin host for consistent flash version data. + registerFakePluginHost(); + + // Setup a webserver to serve Addons, Plugins, etc. + gHttpServer = new HttpServer(); + gHttpServer.start(-1); + let port = gHttpServer.identity.primaryPort; + gHttpRoot = "http://localhost:" + port + "/"; + gDataRoot = gHttpRoot + "data/"; + gHttpServer.registerDirectory("/data/", do_get_cwd()); + registerCleanupFunction(() => gHttpServer.stop(() => {})); + + // Create the attribution data file, so that settings.attribution will exist. + // The attribution functionality only exists in Firefox. + if (AppConstants.MOZ_BUILD_APP == "browser") { + spoofAttributionData(); + registerCleanupFunction(cleanupAttributionData); + } + + await spoofProfileReset(); + await TelemetryEnvironment.delayedInit(); + await SearchTestUtils.useTestEngines("data", "search-extensions"); +}); + +add_task(async function test_checkEnvironment() { + // During startup we have partial addon records. + // First make sure we haven't yet read the addons DB, then test that + // we have some partial addons data. + Assert.equal( + AddonManagerPrivate.isDBLoaded(), + false, + "addons database is not loaded" + ); + + let data = TelemetryEnvironment.currentEnvironment; + checkAddonsSection(data, false, true); + + // Check that settings.intl is lazily loaded. + Assert.equal( + typeof data.settings.intl, + "object", + "intl is initially an object" + ); + Assert.equal( + Object.keys(data.settings.intl).length, + 0, + "intl is initially empty" + ); + + // Now continue with startup. + let initPromise = TelemetryEnvironment.onInitialized(); + finishAddonManagerStartup(); + + // Fake the delayed startup event for intl data to load. + fakeIntlReady(); + + let environmentData = await initPromise; + checkEnvironmentData(environmentData, { isInitial: true }); + + spoofPartnerInfo(); + Services.obs.notifyObservers(null, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC); + + environmentData = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(environmentData, { assertProcessData: true }); +}); + +add_task(async function test_prefWatchPolicies() { + const PREF_TEST_1 = "toolkit.telemetry.test.pref_new"; + const PREF_TEST_2 = "toolkit.telemetry.test.pref1"; + const PREF_TEST_3 = "toolkit.telemetry.test.pref2"; + const PREF_TEST_4 = "toolkit.telemetry.test.pref_old"; + const PREF_TEST_5 = "toolkit.telemetry.test.requiresRestart"; + + const expectedValue = "some-test-value"; + const unexpectedValue = "unexpected-test-value"; + + const PREFS_TO_WATCH = new Map([ + [PREF_TEST_1, { what: TelemetryEnvironment.RECORD_PREF_VALUE }], + [PREF_TEST_2, { what: TelemetryEnvironment.RECORD_PREF_STATE }], + [PREF_TEST_3, { what: TelemetryEnvironment.RECORD_PREF_STATE }], + [PREF_TEST_4, { what: TelemetryEnvironment.RECORD_PREF_VALUE }], + [ + PREF_TEST_5, + { what: TelemetryEnvironment.RECORD_PREF_VALUE, requiresRestart: true }, + ], + ]); + + Preferences.set(PREF_TEST_4, expectedValue); + Preferences.set(PREF_TEST_5, expectedValue); + + // Set the Environment preferences to watch. + await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + let deferred = PromiseUtils.defer(); + + // Check that the pref values are missing or present as expected + Assert.strictEqual( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1], + undefined + ); + Assert.strictEqual( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_4], + expectedValue + ); + Assert.strictEqual( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_5], + expectedValue + ); + + TelemetryEnvironment.registerChangeListener( + "testWatchPrefs", + (reason, data) => deferred.resolve(data) + ); + let oldEnvironmentData = TelemetryEnvironment.currentEnvironment; + + // Trigger a change in the watched preferences. + Preferences.set(PREF_TEST_1, expectedValue); + Preferences.set(PREF_TEST_2, false); + Preferences.set(PREF_TEST_5, unexpectedValue); + let eventEnvironmentData = await deferred.promise; + + // Unregister the listener. + TelemetryEnvironment.unregisterChangeListener("testWatchPrefs"); + + // Check environment contains the correct data. + Assert.deepEqual(oldEnvironmentData, eventEnvironmentData); + let userPrefs = TelemetryEnvironment.currentEnvironment.settings.userPrefs; + + Assert.equal( + userPrefs[PREF_TEST_1], + expectedValue, + "Environment contains the correct preference value." + ); + Assert.equal( + userPrefs[PREF_TEST_2], + "<user-set>", + "Report that the pref was user set but the value is not shown." + ); + Assert.ok( + !(PREF_TEST_3 in userPrefs), + "Do not report if preference not user set." + ); + Assert.equal( + userPrefs[PREF_TEST_5], + expectedValue, + "The pref value in the environment data should still be the same" + ); +}); + +add_task(async function test_prefWatch_prefReset() { + const PREF_TEST = "toolkit.telemetry.test.pref1"; + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }], + ]); + + // Set the preference to a non-default value. + Preferences.set(PREF_TEST, false); + + // Set the Environment preferences to watch. + await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener( + "testWatchPrefs_reset", + deferred.resolve + ); + + Assert.strictEqual( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], + "<user-set>" + ); + + // Trigger a change in the watched preferences. + Preferences.reset(PREF_TEST); + await deferred.promise; + + Assert.strictEqual( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], + undefined + ); + + // Unregister the listener. + TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_reset"); +}); + +add_task(async function test_prefDefault() { + const PREF_TEST = "toolkit.telemetry.test.defaultpref1"; + const expectedValue = "some-test-value"; + + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE }], + ]); + + // Set the preference to a default value. + Services.prefs.getDefaultBranch(null).setCharPref(PREF_TEST, expectedValue); + + // Set the Environment preferences to watch. + // We're not watching, but this function does the setup we need. + await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + + Assert.strictEqual( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], + expectedValue + ); +}); + +add_task(async function test_prefDefaultState() { + const PREF_TEST = "toolkit.telemetry.test.defaultpref2"; + const expectedValue = "some-test-value"; + + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_STATE }], + ]); + + await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + + Assert.equal( + PREF_TEST in TelemetryEnvironment.currentEnvironment.settings.userPrefs, + false + ); + + // Set the preference to a default value. + Services.prefs.getDefaultBranch(null).setCharPref(PREF_TEST, expectedValue); + + Assert.strictEqual( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], + "<set>" + ); +}); + +add_task(async function test_prefInvalid() { + const PREF_TEST_1 = "toolkit.telemetry.test.invalid1"; + const PREF_TEST_2 = "toolkit.telemetry.test.invalid2"; + + const PREFS_TO_WATCH = new Map([ + [PREF_TEST_1, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE }], + [PREF_TEST_2, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_STATE }], + ]); + + await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + + Assert.strictEqual( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1], + undefined + ); + Assert.strictEqual( + TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_2], + undefined + ); +}); + +add_task(async function test_addonsWatch_InterestingChange() { + const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi"; + const ADDON_ID = "tel-restartless-webext@tests.mozilla.org"; + // We only expect a single notification for each install, uninstall, enable, disable. + const EXPECTED_NOTIFICATIONS = 4; + + let receivedNotifications = 0; + + let registerCheckpointPromise = aExpected => { + return new Promise(resolve => + TelemetryEnvironment.registerChangeListener( + "testWatchAddons_Changes" + aExpected, + (reason, data) => { + Assert.equal(reason, "addons-changed"); + receivedNotifications++; + resolve(); + } + ) + ); + }; + + let assertCheckpoint = aExpected => { + Assert.equal(receivedNotifications, aExpected); + TelemetryEnvironment.unregisterChangeListener( + "testWatchAddons_Changes" + aExpected + ); + }; + + // Test for receiving one notification after each change. + let checkpointPromise = registerCheckpointPromise(1); + await installXPIFromURL(ADDON_INSTALL_URL); + await checkpointPromise; + assertCheckpoint(1); + Assert.ok( + ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons + ); + + checkpointPromise = registerCheckpointPromise(2); + let addon = await AddonManager.getAddonByID(ADDON_ID); + await addon.disable(); + await checkpointPromise; + assertCheckpoint(2); + Assert.ok( + !(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons) + ); + + checkpointPromise = registerCheckpointPromise(3); + let startupPromise = AddonTestUtils.promiseWebExtensionStartup(ADDON_ID); + await addon.enable(); + await checkpointPromise; + assertCheckpoint(3); + Assert.ok( + ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons + ); + await startupPromise; + + checkpointPromise = registerCheckpointPromise(4); + (await AddonManager.getAddonByID(ADDON_ID)).uninstall(); + await checkpointPromise; + assertCheckpoint(4); + Assert.ok( + !(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons) + ); + + Assert.equal( + receivedNotifications, + EXPECTED_NOTIFICATIONS, + "We must only receive the notifications we expect." + ); +}); + +add_task(async function test_pluginsWatch_Add() { + if (!gIsFirefox) { + Assert.ok(true, "Skipping: there is no Plugin Manager on Android."); + return; + } + + Assert.equal( + TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, + 1 + ); + + let newPlugin = new PluginTag( + PLUGIN2_NAME, + PLUGIN2_DESC, + PLUGIN2_VERSION, + true + ); + gInstalledPlugins.push(newPlugin); + + let receivedNotifications = 0; + let callback = (reason, data) => { + receivedNotifications++; + }; + TelemetryEnvironment.registerChangeListener("testWatchPlugins_Add", callback); + + Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC); + + await ContentTaskUtils.waitForCondition(() => { + return ( + TelemetryEnvironment.currentEnvironment.addons.activePlugins.length == 2 + ); + }); + + TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Add"); + + Assert.equal( + receivedNotifications, + 0, + "We must not receive any notifications." + ); +}); + +add_task(async function test_pluginsWatch_Remove() { + if (!gIsFirefox) { + Assert.ok(true, "Skipping: there is no Plugin Manager on Android."); + return; + } + + Assert.equal( + TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, + 2 + ); + + // Find the test plugin. + let plugin = gInstalledPlugins.find(p => p.name == PLUGIN2_NAME); + Assert.ok(plugin, "The test plugin must exist."); + + // Remove it from the PluginHost. + gInstalledPlugins = gInstalledPlugins.filter(p => p != plugin); + + let receivedNotifications = 0; + let callback = () => { + receivedNotifications++; + }; + TelemetryEnvironment.registerChangeListener( + "testWatchPlugins_Remove", + callback + ); + + Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC); + + await ContentTaskUtils.waitForCondition(() => { + return ( + TelemetryEnvironment.currentEnvironment.addons.activePlugins.length == 1 + ); + }); + + TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Remove"); + + Assert.equal( + receivedNotifications, + 0, + "We must not receive any notifications." + ); +}); + +add_task(async function test_addonsWatch_NotInterestingChange() { + // We are not interested to dictionary addons changes. + const DICTIONARY_ADDON_INSTALL_URL = gDataRoot + "dictionary.xpi"; + const INTERESTING_ADDON_INSTALL_URL = gDataRoot + "restartless.xpi"; + + let receivedNotification = false; + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener("testNotInteresting", () => { + Assert.ok( + !receivedNotification, + "Should not receive multiple notifications" + ); + receivedNotification = true; + deferred.resolve(); + }); + + let dictionaryAddon = await installXPIFromURL(DICTIONARY_ADDON_INSTALL_URL); + let interestingAddon = await installXPIFromURL(INTERESTING_ADDON_INSTALL_URL); + + await deferred.promise; + Assert.ok( + !( + "telemetry-dictionary@tests.mozilla.org" in + TelemetryEnvironment.currentEnvironment.addons.activeAddons + ), + "Dictionaries should not appear in active addons." + ); + + TelemetryEnvironment.unregisterChangeListener("testNotInteresting"); + + dictionaryAddon.uninstall(); + await interestingAddon.startupPromise; + interestingAddon.uninstall(); +}); + +add_task(async function test_addonsAndPlugins() { + const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi"; + const ADDON_ID = "tel-restartless-webext@tests.mozilla.org"; + const ADDON_INSTALL_DATE = truncateToDays(Date.now()); + const EXPECTED_ADDON_DATA = { + blocklisted: false, + description: "A restartless addon which gets enabled without a reboot.", + name: "XPI Telemetry Restartless Test", + userDisabled: false, + appDisabled: false, + version: "1.0", + scope: 1, + type: "extension", + foreignInstall: false, + hasBinaryComponents: false, + installDay: ADDON_INSTALL_DATE, + updateDay: ADDON_INSTALL_DATE, + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + isSystem: false, + isWebExtension: true, + multiprocessCompatible: true, + }; + const SYSTEM_ADDON_ID = "tel-system-xpi@tests.mozilla.org"; + const EXPECTED_SYSTEM_ADDON_DATA = { + blocklisted: false, + description: "A system addon which is shipped with Firefox.", + name: "XPI Telemetry System Add-on Test", + userDisabled: false, + appDisabled: false, + version: "1.0", + scope: 1, + type: "extension", + foreignInstall: false, + hasBinaryComponents: false, + installDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE), + updateDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE), + signedState: undefined, + isSystem: true, + isWebExtension: true, + multiprocessCompatible: true, + }; + + const WEBEXTENSION_ADDON_ID = "tel-webextension-xpi@tests.mozilla.org"; + const WEBEXTENSION_ADDON_INSTALL_DATE = truncateToDays(Date.now()); + const EXPECTED_WEBEXTENSION_ADDON_DATA = { + blocklisted: false, + description: "A webextension addon.", + name: "XPI Telemetry WebExtension Add-on Test", + userDisabled: false, + appDisabled: false, + version: "1.0", + scope: 1, + type: "extension", + foreignInstall: false, + hasBinaryComponents: false, + installDay: WEBEXTENSION_ADDON_INSTALL_DATE, + updateDay: WEBEXTENSION_ADDON_INSTALL_DATE, + signedState: AddonManager.SIGNEDSTATE_PRIVILEGED, + isSystem: false, + isWebExtension: true, + multiprocessCompatible: true, + }; + + const EXPECTED_PLUGIN_DATA = { + name: FLASH_PLUGIN_NAME, + version: FLASH_PLUGIN_VERSION, + description: FLASH_PLUGIN_DESC, + blocklisted: false, + disabled: false, + clicktoplay: true, + }; + + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener( + "test_WebExtension", + (reason, data) => { + Assert.equal(reason, "addons-changed"); + deferred.resolve(); + } + ); + + // Install an add-on so we have some data. + let addon = await installXPIFromURL(ADDON_INSTALL_URL); + + // Install a webextension as well. + ExtensionTestUtils.init(this); + + let webextension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + name: "XPI Telemetry WebExtension Add-on Test", + description: "A webextension addon.", + version: "1.0", + applications: { + gecko: { + id: WEBEXTENSION_ADDON_ID, + }, + }, + }, + }); + + await webextension.startup(); + await deferred.promise; + TelemetryEnvironment.unregisterChangeListener("test_WebExtension"); + + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + // Check addon data. + Assert.ok( + ADDON_ID in data.addons.activeAddons, + "We must have one active addon." + ); + let targetAddon = data.addons.activeAddons[ADDON_ID]; + for (let f in EXPECTED_ADDON_DATA) { + Assert.equal( + targetAddon[f], + EXPECTED_ADDON_DATA[f], + f + " must have the correct value." + ); + } + + // Check system add-on data. + Assert.ok( + SYSTEM_ADDON_ID in data.addons.activeAddons, + "We must have one active system addon." + ); + let targetSystemAddon = data.addons.activeAddons[SYSTEM_ADDON_ID]; + for (let f in EXPECTED_SYSTEM_ADDON_DATA) { + Assert.equal( + targetSystemAddon[f], + EXPECTED_SYSTEM_ADDON_DATA[f], + f + " must have the correct value." + ); + } + + // Check webextension add-on data. + Assert.ok( + WEBEXTENSION_ADDON_ID in data.addons.activeAddons, + "We must have one active webextension addon." + ); + let targetWebExtensionAddon = data.addons.activeAddons[WEBEXTENSION_ADDON_ID]; + for (let f in EXPECTED_WEBEXTENSION_ADDON_DATA) { + Assert.equal( + targetWebExtensionAddon[f], + EXPECTED_WEBEXTENSION_ADDON_DATA[f], + f + " must have the correct value." + ); + } + + await webextension.unload(); + + // Check plugin data. + Assert.equal( + data.addons.activePlugins.length, + 1, + "We must have only one active plugin." + ); + let targetPlugin = data.addons.activePlugins[0]; + for (let f in EXPECTED_PLUGIN_DATA) { + Assert.equal( + targetPlugin[f], + EXPECTED_PLUGIN_DATA[f], + f + " must have the correct value." + ); + } + + // Check plugin mime types. + Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE1)); + Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE2)); + Assert.ok(!targetPlugin.mimeTypes.find(m => m == "Not There.")); + + // Uninstall the addon. + await addon.startupPromise; + await addon.uninstall(); +}); + +add_task(async function test_signedAddon() { + AddonTestUtils.useRealCertChecks = true; + + const ADDON_INSTALL_URL = gDataRoot + "signed-webext.xpi"; + const ADDON_ID = "tel-signed-webext@tests.mozilla.org"; + const ADDON_INSTALL_DATE = truncateToDays(Date.now()); + const EXPECTED_ADDON_DATA = { + blocklisted: false, + description: "A signed webextension", + name: "XPI Telemetry Signed Test", + userDisabled: false, + appDisabled: false, + version: "1.0", + scope: 1, + type: "extension", + foreignInstall: false, + hasBinaryComponents: false, + installDay: ADDON_INSTALL_DATE, + updateDay: ADDON_INSTALL_DATE, + signedState: AddonManager.SIGNEDSTATE_SIGNED, + }; + + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener( + "test_signedAddon", + deferred.resolve + ); + + // Install the addon. + let addon = await installXPIFromURL(ADDON_INSTALL_URL); + + await deferred.promise; + // Unregister the listener. + TelemetryEnvironment.unregisterChangeListener("test_signedAddon"); + + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + // Check addon data. + Assert.ok( + ADDON_ID in data.addons.activeAddons, + "Add-on should be in the environment." + ); + let targetAddon = data.addons.activeAddons[ADDON_ID]; + for (let f in EXPECTED_ADDON_DATA) { + Assert.equal( + targetAddon[f], + EXPECTED_ADDON_DATA[f], + f + " must have the correct value." + ); + } + + AddonTestUtils.useRealCertChecks = false; + await addon.startupPromise; + await addon.uninstall(); +}); + +add_task(async function test_addonsFieldsLimit() { + const ADDON_INSTALL_URL = gDataRoot + "long-fields.xpi"; + const ADDON_ID = "tel-longfields-webext@tests.mozilla.org"; + + // Install the addon and wait for the TelemetryEnvironment to pick it up. + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener( + "test_longFieldsAddon", + deferred.resolve + ); + let addon = await installXPIFromURL(ADDON_INSTALL_URL); + await deferred.promise; + TelemetryEnvironment.unregisterChangeListener("test_longFieldsAddon"); + + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + // Check that the addon is available and that the string fields are limited. + Assert.ok( + ADDON_ID in data.addons.activeAddons, + "Add-on should be in the environment." + ); + let targetAddon = data.addons.activeAddons[ADDON_ID]; + + // TelemetryEnvironment limits the length of string fields for activeAddons to 100 chars, + // to mitigate misbehaving addons. + Assert.lessOrEqual( + targetAddon.version.length, + 100, + "The version string must have been limited" + ); + Assert.lessOrEqual( + targetAddon.name.length, + 100, + "The name string must have been limited" + ); + Assert.lessOrEqual( + targetAddon.description.length, + 100, + "The description string must have been limited" + ); + + await addon.startupPromise; + await addon.uninstall(); +}); + +add_task(async function test_collectionWithbrokenAddonData() { + const BROKEN_ADDON_ID = "telemetry-test2.example.com@services.mozilla.org"; + const BROKEN_MANIFEST = { + id: "telemetry-test2.example.com@services.mozilla.org", + name: "telemetry broken addon", + origin: "https://telemetry-test2.example.com", + version: 1, // This is intentionally not a string. + signedState: AddonManager.SIGNEDSTATE_SIGNED, + type: "extension", + }; + + const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi"; + const ADDON_ID = "tel-restartless-webext@tests.mozilla.org"; + const ADDON_INSTALL_DATE = truncateToDays(Date.now()); + const EXPECTED_ADDON_DATA = { + blocklisted: false, + description: "A restartless addon which gets enabled without a reboot.", + name: "XPI Telemetry Restartless Test", + userDisabled: false, + appDisabled: false, + version: "1.0", + scope: 1, + type: "extension", + foreignInstall: false, + hasBinaryComponents: false, + installDay: ADDON_INSTALL_DATE, + updateDay: ADDON_INSTALL_DATE, + signedState: AddonManager.SIGNEDSTATE_MISSING, + }; + + let receivedNotifications = 0; + + let registerCheckpointPromise = aExpected => { + return new Promise(resolve => + TelemetryEnvironment.registerChangeListener( + "testBrokenAddon_collection" + aExpected, + (reason, data) => { + Assert.equal(reason, "addons-changed"); + receivedNotifications++; + resolve(); + } + ) + ); + }; + + let assertCheckpoint = aExpected => { + Assert.equal(receivedNotifications, aExpected); + TelemetryEnvironment.unregisterChangeListener( + "testBrokenAddon_collection" + aExpected + ); + }; + + // Register the broken provider and install the broken addon. + let checkpointPromise = registerCheckpointPromise(1); + let brokenAddonProvider = createMockAddonProvider( + "Broken Extensions Provider" + ); + AddonManagerPrivate.registerProvider(brokenAddonProvider); + brokenAddonProvider.addAddon(BROKEN_MANIFEST); + await checkpointPromise; + assertCheckpoint(1); + + // Now install an addon which returns the correct information. + checkpointPromise = registerCheckpointPromise(2); + let addon = await installXPIFromURL(ADDON_INSTALL_URL); + await checkpointPromise; + assertCheckpoint(2); + + // Check that the new environment contains the Social addon installed with the broken + // manifest and the rest of the data. + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data, { expectBrokenAddons: true }); + + let activeAddons = data.addons.activeAddons; + Assert.ok( + BROKEN_ADDON_ID in activeAddons, + "The addon with the broken manifest must be reported." + ); + Assert.equal( + activeAddons[BROKEN_ADDON_ID].version, + null, + "null should be reported for invalid data." + ); + Assert.ok(ADDON_ID in activeAddons, "The valid addon must be reported."); + Assert.equal( + activeAddons[ADDON_ID].description, + EXPECTED_ADDON_DATA.description, + "The description for the valid addon should be correct." + ); + + // Unregister the broken provider so we don't mess with other tests. + AddonManagerPrivate.unregisterProvider(brokenAddonProvider); + + // Uninstall the valid addon. + await addon.startupPromise; + await addon.uninstall(); +}); + +async function checkDefaultSearch(privateOn, reInitSearchService) { + // Start off with separate default engine for private browsing turned off. + Preferences.set( + "browser.search.separatePrivateDefault.ui.enabled", + privateOn + ); + Preferences.set("browser.search.separatePrivateDefault", privateOn); + + let data = await TelemetryEnvironment.testCleanRestart().onInitialized(); + checkEnvironmentData(data); + Assert.ok(!("defaultSearchEngine" in data.settings)); + Assert.ok(!("defaultSearchEngineData" in data.settings)); + Assert.ok(!("defaultPrivateSearchEngine" in data.settings)); + Assert.ok(!("defaultPrivateSearchEngineData" in data.settings)); + + // Load the engines definitions from a xpcshell data: that's needed so that + // the search provider reports an engine identifier. + + // Initialize the search service. + if (reInitSearchService) { + Services.search.wrappedJSObject.reset(); + } + await Services.search.init(); + await promiseNextTick(); + + // Our default engine from the JAR file has an identifier. Check if it is correctly + // reported. + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.equal(data.settings.defaultSearchEngine, "telemetrySearchIdentifier"); + let expectedSearchEngineData = { + name: "telemetrySearchIdentifier", + loadPath: + "[other]addEngineWithDetails:telemetrySearchIdentifier@search.mozilla.org", + origin: "default", + submissionURL: + "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB?search=&sourceId=Mozilla-search", + }; + Assert.deepEqual( + data.settings.defaultSearchEngineData, + expectedSearchEngineData + ); + if (privateOn) { + Assert.equal( + data.settings.defaultPrivateSearchEngine, + "telemetrySearchIdentifier" + ); + Assert.deepEqual( + data.settings.defaultPrivateSearchEngineData, + expectedSearchEngineData, + "Should have the correct data for the private search engine" + ); + } else { + Assert.ok( + !("defaultPrivateSearchEngine" in data.settings), + "Should not have private name recorded as the pref for separate is off" + ); + Assert.ok( + !("defaultPrivateSearchEngineData" in data.settings), + "Should not have private data recorded as the pref for separate is off" + ); + } + + // Add a new search engine (this will have no engine identifier). + const SEARCH_ENGINE_ID = "telemetry_default"; + const SEARCH_ENGINE_URL = `http://www.example.org/${ + privateOn ? "private" : "" + }?search={searchTerms}`; + await Services.search.addEngineWithDetails(SEARCH_ENGINE_ID, { + method: "get", + template: SEARCH_ENGINE_URL, + }); + + // Register a new change listener and then wait for the search engine change to be notified. + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener( + "testWatch_SearchDefault", + deferred.resolve + ); + if (privateOn) { + // As we had no default and no search engines, the normal mode engine will + // assume the same as the added engine. To ensure the telemetry is different + // we enforce a different default here. + const engine = await Services.search.getEngineByName( + "telemetrySearchIdentifier" + ); + engine.hidden = false; + await Services.search.setDefault(engine); + await Services.search.setDefaultPrivate( + Services.search.getEngineByName(SEARCH_ENGINE_ID) + ); + } else { + await Services.search.setDefault( + Services.search.getEngineByName(SEARCH_ENGINE_ID) + ); + } + await deferred.promise; + + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID; + const EXPECTED_SEARCH_ENGINE_DATA = { + name: "telemetry_default", + loadPath: "[other]addEngineWithDetails:telemetry_default@test.engine", + origin: "verified", + }; + if (privateOn) { + Assert.equal( + data.settings.defaultSearchEngine, + "telemetrySearchIdentifier" + ); + Assert.deepEqual( + data.settings.defaultSearchEngineData, + expectedSearchEngineData + ); + Assert.equal( + data.settings.defaultPrivateSearchEngine, + EXPECTED_SEARCH_ENGINE + ); + Assert.deepEqual( + data.settings.defaultPrivateSearchEngineData, + EXPECTED_SEARCH_ENGINE_DATA + ); + } else { + Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE); + Assert.deepEqual( + data.settings.defaultSearchEngineData, + EXPECTED_SEARCH_ENGINE_DATA + ); + } + TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault"); +} + +add_task(async function test_defaultSearchEngine() { + await checkDefaultSearch(false); + + // Cleanly install an engine from an xml file, and check if origin is + // recorded as "verified". + let promise = new Promise(resolve => { + TelemetryEnvironment.registerChangeListener( + "testWatch_SearchDefault", + resolve + ); + }); + let engine = await new Promise((resolve, reject) => { + Services.obs.addObserver(function obs(obsSubject, obsTopic, obsData) { + try { + let searchEngine = obsSubject.QueryInterface(Ci.nsISearchEngine); + info("Observed " + obsData + " for " + searchEngine.name); + if ( + obsData != "engine-added" || + searchEngine.name != "engine-telemetry" + ) { + return; + } + + Services.obs.removeObserver(obs, "browser-search-engine-modified"); + resolve(searchEngine); + } catch (ex) { + reject(ex); + } + }, "browser-search-engine-modified"); + Services.search.addOpenSearchEngine( + "file://" + do_get_cwd().path + "/engine.xml", + null + ); + }); + await Services.search.setDefault(engine); + await promise; + TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault"); + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.deepEqual(data.settings.defaultSearchEngineData, { + name: "engine-telemetry", + loadPath: "[other]/engine.xml", + origin: "verified", + }); + + // Now break this engine's load path hash. + promise = new Promise(resolve => { + TelemetryEnvironment.registerChangeListener( + "testWatch_SearchDefault", + resolve + ); + }); + engine.wrappedJSObject.setAttr("loadPathHash", "broken"); + Services.obs.notifyObservers( + null, + "browser-search-engine-modified", + "engine-default" + ); + await promise; + TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault"); + data = TelemetryEnvironment.currentEnvironment; + Assert.equal(data.settings.defaultSearchEngineData.origin, "invalid"); + await Services.search.removeEngine(engine); + + const SEARCH_ENGINE_ID = "telemetry_default"; + const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID; + // Work around bug 1165341: Intentionally set the default engine. + await Services.search.setDefault( + Services.search.getEngineByName(SEARCH_ENGINE_ID) + ); + + // Double-check the default for the next part of the test. + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE); + + // Define and reset the test preference. + const PREF_TEST = "toolkit.telemetry.test.pref1"; + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }], + ]); + Preferences.reset(PREF_TEST); + + // Watch the test preference. + await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener( + "testSearchEngine_pref", + deferred.resolve + ); + // Trigger an environment change. + Preferences.set(PREF_TEST, 1); + await deferred.promise; + TelemetryEnvironment.unregisterChangeListener("testSearchEngine_pref"); + + // Check that the search engine information is correctly retained when prefs change. + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE); +}); + +add_task(async function test_defaultPrivateSearchEngine() { + await checkDefaultSearch(true, true); +}); + +add_task(async function test_defaultSearchEngine_paramsChanged() { + let extension = await SearchTestUtils.installSearchExtension({ + name: "TestEngine", + search_url: "https://www.google.com/fake1", + }); + + let promise = new Promise(resolve => { + TelemetryEnvironment.registerChangeListener( + "testWatch_SearchDefault", + resolve + ); + }); + let engine = Services.search.getEngineByName("TestEngine"); + await Services.search.setDefault(engine); + await promise; + + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.deepEqual(data.settings.defaultSearchEngineData, { + name: "TestEngine", + loadPath: "[other]addEngineWithDetails:example@tests.mozilla.org", + origin: "verified", + submissionURL: "https://www.google.com/fake1?q=", + }); + + promise = new Promise(resolve => { + TelemetryEnvironment.registerChangeListener( + "testWatch_SearchDefault", + resolve + ); + }); + + engine.wrappedJSObject._updateFromManifest( + extension.id, + extension.baseURI, + SearchTestUtils.createEngineManifest({ + name: "TestEngine", + version: "1.2", + search_url: "https://www.google.com/fake2", + }) + ); + + await promise; + + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.deepEqual(data.settings.defaultSearchEngineData, { + name: "TestEngine", + loadPath: "[other]addEngineWithDetails:example@tests.mozilla.org", + origin: "verified", + submissionURL: "https://www.google.com/fake2?q=", + }); + + await extension.unload(); +}); + +add_task( + { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" }, + async function test_delayed_defaultBrowser() { + // Skip this test on Thunderbird since it is not a browser, so it cannot + // be the default browser. + + // Make sure we don't have anything already cached for this test. + await TelemetryEnvironment.testCleanRestart().onInitialized(); + + let environmentData = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(environmentData); + Assert.equal( + environmentData.settings.isDefaultBrowser, + null, + "isDefaultBrowser must be null before the session is restored." + ); + + Services.obs.notifyObservers(null, "sessionstore-windows-restored"); + + environmentData = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(environmentData); + Assert.ok( + "isDefaultBrowser" in environmentData.settings, + "isDefaultBrowser must be available after the session is restored." + ); + Assert.equal( + typeof environmentData.settings.isDefaultBrowser, + "boolean", + "isDefaultBrowser must be of the right type." + ); + + // Make sure pref-flipping doesn't overwrite the browser default state. + const PREF_TEST = "toolkit.telemetry.test.pref1"; + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }], + ]); + Preferences.reset(PREF_TEST); + + // Watch the test preference. + await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener( + "testDefaultBrowser_pref", + deferred.resolve + ); + // Trigger an environment change. + Preferences.set(PREF_TEST, 1); + await deferred.promise; + TelemetryEnvironment.unregisterChangeListener("testDefaultBrowser_pref"); + + // Check that the data is still available. + environmentData = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(environmentData); + Assert.ok( + "isDefaultBrowser" in environmentData.settings, + "isDefaultBrowser must still be available after a pref is flipped." + ); + } +); + +add_task(async function test_osstrings() { + // First test that numbers in sysinfo properties are converted to string fields + // in system.os. + SysInfo.overrides = { + version: 1, + name: 2, + kernel_version: 3, + }; + + await TelemetryEnvironment.testCleanRestart().onInitialized(); + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + Assert.equal(data.system.os.version, "1"); + Assert.equal(data.system.os.name, "2"); + if (AppConstants.platform == "android") { + Assert.equal(data.system.os.kernelVersion, "3"); + } + + // Check that null values are also handled. + SysInfo.overrides = { + version: null, + name: null, + kernel_version: null, + }; + + await TelemetryEnvironment.testCleanRestart().onInitialized(); + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + Assert.equal(data.system.os.version, null); + Assert.equal(data.system.os.name, null); + if (AppConstants.platform == "android") { + Assert.equal(data.system.os.kernelVersion, null); + } + + // Clean up. + SysInfo.overrides = {}; + await TelemetryEnvironment.testCleanRestart().onInitialized(); +}); + +add_task(async function test_experimentsAPI() { + const EXPERIMENT1 = "experiment-1"; + const EXPERIMENT1_BRANCH = "nice-branch"; + const EXPERIMENT2 = "experiment-2"; + const EXPERIMENT2_BRANCH = "other-branch"; + + let checkExperiment = (environmentData, id, branch, type = null) => { + Assert.ok( + "experiments" in environmentData, + "The current environment must report the experiment annotations." + ); + Assert.ok( + id in environmentData.experiments, + "The experiments section must contain the expected experiment id." + ); + Assert.equal( + environmentData.experiments[id].branch, + branch, + "The experiment branch must be correct." + ); + }; + + // Clean the environment and check that it's reporting the correct info. + await TelemetryEnvironment.testCleanRestart().onInitialized(); + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + // We don't expect the experiments section to be there if no annotation + // happened. + Assert.ok( + !("experiments" in data), + "No experiments section must be reported if nothing was annotated." + ); + + // Add a change listener and add an experiment annotation. + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener( + "test_experimentsAPI", + (reason, env) => { + deferred.resolve(env); + } + ); + TelemetryEnvironment.setExperimentActive(EXPERIMENT1, EXPERIMENT1_BRANCH); + let eventEnvironmentData = await deferred.promise; + + // Check that the old environment does not contain the experiments. + checkEnvironmentData(eventEnvironmentData); + Assert.ok( + !("experiments" in eventEnvironmentData), + "No experiments section must be reported in the old environment." + ); + + // Check that the current environment contains the right experiment. + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + checkExperiment(data, EXPERIMENT1, EXPERIMENT1_BRANCH); + + TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI"); + + // Add a second annotation and check that both experiments are there. + deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener( + "test_experimentsAPI2", + (reason, env) => { + deferred.resolve(env); + } + ); + TelemetryEnvironment.setExperimentActive(EXPERIMENT2, EXPERIMENT2_BRANCH); + eventEnvironmentData = await deferred.promise; + + // Check that the current environment contains both the experiment. + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + checkExperiment(data, EXPERIMENT1, EXPERIMENT1_BRANCH); + checkExperiment(data, EXPERIMENT2, EXPERIMENT2_BRANCH); + + // The previous environment should only contain the first experiment. + checkExperiment(eventEnvironmentData, EXPERIMENT1, EXPERIMENT1_BRANCH); + Assert.ok( + !(EXPERIMENT2 in eventEnvironmentData), + "The old environment must not contain the new experiment annotation." + ); + + TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI2"); + + // Check that removing an unknown experiment annotation does not trigger + // a notification. + TelemetryEnvironment.registerChangeListener("test_experimentsAPI3", () => { + Assert.ok( + false, + "Removing an unknown experiment annotation must not trigger a change." + ); + }); + TelemetryEnvironment.setExperimentInactive("unknown-experiment-id"); + // Also make sure that passing non-string parameters arguments doesn't throw nor + // trigger a notification. + TelemetryEnvironment.setExperimentActive({}, "some-branch"); + TelemetryEnvironment.setExperimentActive("some-id", {}); + TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI3"); + + // Check that removing a known experiment leaves the other in place and triggers + // a change. + deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener( + "test_experimentsAPI4", + (reason, env) => { + deferred.resolve(env); + } + ); + TelemetryEnvironment.setExperimentInactive(EXPERIMENT1); + eventEnvironmentData = await deferred.promise; + + // Check that the current environment contains just the second experiment. + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.ok( + !(EXPERIMENT1 in data), + "The current environment must not contain the removed experiment annotation." + ); + checkExperiment(data, EXPERIMENT2, EXPERIMENT2_BRANCH); + + // The previous environment should contain both annotations. + checkExperiment(eventEnvironmentData, EXPERIMENT1, EXPERIMENT1_BRANCH); + checkExperiment(eventEnvironmentData, EXPERIMENT2, EXPERIMENT2_BRANCH); + + // Set an experiment with a type and check that it correctly shows up. + TelemetryEnvironment.setExperimentActive( + "typed-experiment", + "random-branch", + { type: "ab-test" } + ); + data = TelemetryEnvironment.currentEnvironment; + checkExperiment(data, "typed-experiment", "random-branch", "ab-test"); +}); + +add_task(async function test_experimentsAPI_limits() { + const EXPERIMENT = + "experiment-2-experiment-2-experiment-2-experiment-2-experiment-2" + + "-experiment-2-experiment-2-experiment-2-experiment-2"; + const EXPERIMENT_BRANCH = + "other-branch-other-branch-other-branch-other-branch-other" + + "-branch-other-branch-other-branch-other-branch-other-branch"; + const EXPERIMENT_TRUNCATED = EXPERIMENT.substring(0, 100); + const EXPERIMENT_BRANCH_TRUNCATED = EXPERIMENT_BRANCH.substring(0, 100); + + // Clean the environment and check that it's reporting the correct info. + await TelemetryEnvironment.testCleanRestart().onInitialized(); + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + // We don't expect the experiments section to be there if no annotation + // happened. + Assert.ok( + !("experiments" in data), + "No experiments section must be reported if nothing was annotated." + ); + + // Add a change listener and wait for the annotation to happen. + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener("test_experimentsAPI", () => + deferred.resolve() + ); + TelemetryEnvironment.setExperimentActive(EXPERIMENT, EXPERIMENT_BRANCH); + await deferred.promise; + + // Check that the current environment contains the truncated values + // for the experiment data. + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.ok( + "experiments" in data, + "The environment must contain an experiments section." + ); + Assert.ok( + EXPERIMENT_TRUNCATED in data.experiments, + "The experiments must be reporting the truncated id." + ); + Assert.ok( + !(EXPERIMENT in data.experiments), + "The experiments must not be reporting the full id." + ); + Assert.equal( + EXPERIMENT_BRANCH_TRUNCATED, + data.experiments[EXPERIMENT_TRUNCATED].branch, + "The experiments must be reporting the truncated branch." + ); + + TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI"); + + // Check that an overly long type is truncated. + const longType = "a0123456678901234567890123456789"; + TelemetryEnvironment.setExperimentActive("exp", "some-branch", { + type: longType, + }); + data = TelemetryEnvironment.currentEnvironment; + Assert.equal(data.experiments.exp.type, longType.substring(0, 20)); +}); + +if (gIsWindows) { + add_task(async function test_environmentHDDInfo() { + await TelemetryEnvironment.testCleanRestart().onInitialized(); + let data = TelemetryEnvironment.currentEnvironment; + let empty = { model: null, revision: null, type: null }; + Assert.deepEqual( + data.system.hdd, + { binary: empty, profile: empty, system: empty }, + "Should have no data yet." + ); + await TelemetryEnvironment.delayedInit(); + data = TelemetryEnvironment.currentEnvironment; + for (let k of EXPECTED_HDD_FIELDS) { + checkString(data.system.hdd[k].model); + checkString(data.system.hdd[k].revision); + checkString(data.system.hdd[k].type); + } + if (AppConstants.MOZ_GLEAN) { + if (data.system.hdd.profile.type == "SSD") { + Assert.equal( + true, + Glean.fogValidation.profileDiskIsSsd.testGetValue(), + "SSDness should be recorded in Glean" + ); + } else { + Assert.equal( + false, + Glean.fogValidation.profileDiskIsSsd.testGetValue(), + "nonSSDness should be recorded in Glean" + ); + } + } + }); + + add_task(async function test_environmentProcessInfo() { + await TelemetryEnvironment.testCleanRestart().onInitialized(); + let data = TelemetryEnvironment.currentEnvironment; + Assert.deepEqual(data.system.isWow64, null, "Should have no data yet."); + await TelemetryEnvironment.delayedInit(); + data = TelemetryEnvironment.currentEnvironment; + Assert.equal( + typeof data.system.isWow64, + "boolean", + "isWow64 must be a boolean." + ); + Assert.equal( + typeof data.system.isWowARM64, + "boolean", + "isWowARM64 must be a boolean." + ); + // These should be numbers if they are not null + for (let f of [ + "count", + "model", + "family", + "stepping", + "l2cacheKB", + "l3cacheKB", + "speedMHz", + "cores", + ]) { + Assert.ok( + !(f in data.system.cpu) || + data.system.cpu[f] === null || + Number.isFinite(data.system.cpu[f]), + f + " must be a number if non null." + ); + } + Assert.ok( + checkString(data.system.cpu.vendor), + "vendor must be a valid string." + ); + }); + + add_task(async function test_environmentOSInfo() { + await TelemetryEnvironment.testCleanRestart().onInitialized(); + let data = TelemetryEnvironment.currentEnvironment; + Assert.deepEqual( + data.system.os.installYear, + null, + "Should have no data yet." + ); + await TelemetryEnvironment.delayedInit(); + data = TelemetryEnvironment.currentEnvironment; + Assert.ok( + Number.isFinite(data.system.os.installYear), + "Install year must be a number." + ); + }); +} + +add_task( + { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" }, + async function test_environmentServicesInfo() { + let cache = TelemetryEnvironment.testCleanRestart(); + await cache.onInitialized(); + let oldGetFxaSignedInUser = cache._getFxaSignedInUser; + try { + // Test the 'yes to both' case. + + // This makes the weave service return that the usere is definitely a sync user + Preferences.set("services.sync.username", "c00lperson123@example.com"); + let calledFxa = false; + cache._getFxaSignedInUser = () => { + calledFxa = true; + return null; + }; + + await cache._updateServicesInfo(); + ok( + !calledFxa, + "Shouldn't need to ask FxA if they're definitely signed in" + ); + deepEqual(cache.currentEnvironment.services, { + accountEnabled: true, + syncEnabled: true, + }); + + // Test the fxa-but-not-sync case. + Preferences.reset("services.sync.username"); + // We don't actually inspect the returned object, just t + cache._getFxaSignedInUser = async () => { + return {}; + }; + await cache._updateServicesInfo(); + deepEqual(cache.currentEnvironment.services, { + accountEnabled: true, + syncEnabled: false, + }); + // Test the "no to both" case. + cache._getFxaSignedInUser = async () => { + return null; + }; + await cache._updateServicesInfo(); + deepEqual(cache.currentEnvironment.services, { + accountEnabled: false, + syncEnabled: false, + }); + // And finally, the 'fxa is in an error state' case. + cache._getFxaSignedInUser = () => { + throw new Error("You'll never know"); + }; + await cache._updateServicesInfo(); + equal(cache.currentEnvironment.services, null); + } finally { + cache._getFxaSignedInUser = oldGetFxaSignedInUser; + Preferences.reset("services.sync.username"); + } + } +); + +add_task(async function test_normandyTestPrefsGoneAfter91() { + const testPrefBool = "app.normandy.test-prefs.bool"; + const testPrefInteger = "app.normandy.test-prefs.integer"; + const testPrefString = "app.normandy.test-prefs.string"; + + Services.prefs.setBoolPref(testPrefBool, true); + Services.prefs.setIntPref(testPrefInteger, 10); + Services.prefs.setCharPref(testPrefString, "test-string"); + + const data = TelemetryEnvironment.currentEnvironment; + + if (Services.vc.compare(data.build.version, "91") > 0) { + Assert.equal( + data.settings.userPrefs["app.normandy.test-prefs.bool"], + null, + "This probe should expire in FX91. bug 1686105 " + ); + Assert.equal( + data.settings.userPrefs["app.normandy.test-prefs.integer"], + null, + "This probe should expire in FX91. bug 1686105 " + ); + Assert.equal( + data.settings.userPrefs["app.normandy.test-prefs.string"], + null, + "This probe should expire in FX91. bug 1686105 " + ); + } +}); + +add_task(async function test_environmentShutdown() { + // Define and reset the test preference. + const PREF_TEST = "toolkit.telemetry.test.pref1"; + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }], + ]); + Preferences.reset(PREF_TEST); + + // Set up the preferences and listener, then the trigger shutdown + await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + TelemetryEnvironment.registerChangeListener( + "test_environmentShutdownChange", + () => { + // Register a new change listener that asserts if change is propogated + Assert.ok(false, "No change should be propagated after shutdown."); + } + ); + TelemetryEnvironment.shutdown(); + + // Flipping the test preference after shutdown should not trigger the listener + Preferences.set(PREF_TEST, 1); + + // Unregister the listener. + TelemetryEnvironment.unregisterChangeListener( + "test_environmentShutdownChange" + ); +}); |