summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/tests/marionette
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/telemetry/tests/marionette
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/telemetry/tests/marionette')
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/MANIFEST.in3
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/requirements.txt2
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/setup.py48
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/__init__.py3
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py31
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_server.py86
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py63
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_filters.py75
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_server.py77
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/dynamic_addon/dynamic-probe-telemetry-extension-signed.xpibin0 -> 7965 bytes
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/helloworld.html18
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/manifest.json12
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py63
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runtests.py15
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/testcase.py233
-rw-r--r--toolkit/components/telemetry/tests/marionette/mach_commands.py95
-rw-r--r--toolkit/components/telemetry/tests/marionette/moz.build11
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/manifest.ini12
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_deletion_request_ping.py83
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_dynamic_probes.py26
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_fog_custom_ping.py24
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_fog_deletion_request_ping.py67
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_fog_user_activity.py47
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_main_tab_scalars.py54
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_shutdown_pings_succeed.py55
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_subsession_management.py147
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_unicode_encoding.py39
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/manifest.ini2
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/unit/manifest.ini4
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/unit/test_ping_server_received_ping.py45
30 files changed, 1440 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/marionette/harness/MANIFEST.in b/toolkit/components/telemetry/tests/marionette/harness/MANIFEST.in
new file mode 100644
index 0000000000..e24a6b1ba6
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/MANIFEST.in
@@ -0,0 +1,3 @@
+exclude MANIFEST.in
+include requirements.txt
+recursive-include telemetry_harness/resources * \ No newline at end of file
diff --git a/toolkit/components/telemetry/tests/marionette/harness/requirements.txt b/toolkit/components/telemetry/tests/marionette/harness/requirements.txt
new file mode 100644
index 0000000000..c15861abd7
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/requirements.txt
@@ -0,0 +1,2 @@
+marionette-harness >= 4.0.0
+requests>=2.11.1 \ No newline at end of file
diff --git a/toolkit/components/telemetry/tests/marionette/harness/setup.py b/toolkit/components/telemetry/tests/marionette/harness/setup.py
new file mode 100644
index 0000000000..38d4d35662
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/setup.py
@@ -0,0 +1,48 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+
+from setuptools import find_packages, setup
+
+PACKAGE_VERSION = "0.1"
+
+THIS_DIR = os.path.dirname(os.path.realpath(__name__))
+
+
+def read(*parts):
+ with open(os.path.join(THIS_DIR, *parts)) as f:
+ return f.read()
+
+
+setup(
+ name="telemetry-harness",
+ version=PACKAGE_VERSION,
+ description=(
+ "Custom Marionette runner classes and entry scripts for "
+ "Telemetry specific Marionette tests."
+ ),
+ classifiers=[
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
+ "Natural Language :: English",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ],
+ keywords="mozilla",
+ author="Firefox Test Engineering Team",
+ author_email="firefox-test-engineering@mozilla.org",
+ url="https://developer.mozilla.org/en-US/docs/Mozilla/QA/telemetry_harness",
+ license="MPL 2.0",
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=read("requirements.txt").splitlines(),
+ entry_points="""
+ [console_scripts]
+ telemetry-harness = telemetry_harness.runtests:cli
+ """,
+)
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/__init__.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/__init__.py
new file mode 100644
index 0000000000..6fbe8159b2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/__init__.py
@@ -0,0 +1,3 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py
new file mode 100644
index 0000000000..d0d006fc0a
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py
@@ -0,0 +1,31 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+class FOGPingFilter(object):
+ """Ping filter that accepts any FOG pings."""
+
+ def __call__(self, ping):
+ return True
+
+
+class FOGDocTypePingFilter(FOGPingFilter):
+ """Ping filter that accepts FOG pings that match the doc-type."""
+
+ def __init__(self, doc_type):
+ super(FOGDocTypePingFilter, self).__init__()
+ self.doc_type = doc_type
+
+ def __call__(self, ping):
+ if not super(FOGDocTypePingFilter, self).__call__(ping):
+ return False
+
+ # Verify that the given ping was submitted to the URL for the doc_type
+ return ping["request_url"]["doc_type"] == self.doc_type
+
+
+FOG_BACKGROUND_UPDATE_PING = FOGDocTypePingFilter("background-update")
+FOG_BASELINE_PING = FOGDocTypePingFilter("baseline")
+FOG_DELETION_REQUEST_PING = FOGDocTypePingFilter("deletion-request")
+FOG_ONE_PING_ONLY_PING = FOGDocTypePingFilter("one-ping-only")
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_server.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_server.py
new file mode 100644
index 0000000000..9ad2f60a59
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_server.py
@@ -0,0 +1,86 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import zlib
+
+import wptserve.logger
+from marionette_harness.runner import httpd
+from mozlog import get_default_logger
+from six.moves.urllib import parse as urlparse
+
+
+class FOGPingServer(object):
+ """HTTP server for receiving Firefox on Glean pings."""
+
+ def __init__(self, server_root, url):
+ self._logger = get_default_logger(component="fog_ping_server")
+
+ # Ensure we see logs from wptserve
+ try:
+ wptserve.logger.set_logger(self._logger)
+ except Exception:
+ # Raises if already been set
+ pass
+
+ self.pings = []
+
+ @httpd.handlers.handler
+ def pings_handler(request, response):
+ """Handler for HTTP requests to the ping server."""
+ request_data = request.body
+
+ if request.headers.get("Content-Encoding") == b"gzip":
+ request_data = zlib.decompress(request_data, zlib.MAX_WBITS | 16)
+
+ request_url = request.route_match.copy()
+
+ self.pings.append(
+ {
+ "request_url": request_url,
+ "payload": json.loads(request_data),
+ "debug_tag": request.headers.get("X-Debug-ID"),
+ }
+ )
+
+ self._logger.info(
+ "pings_handler received '{}' ping".format(request_url["doc_type"])
+ )
+
+ status_code = 200
+ content = "OK"
+ headers = [
+ ("Content-Type", "text/plain"),
+ ("Content-Length", len(content)),
+ ]
+
+ return (status_code, headers, content)
+
+ self._httpd = httpd.FixtureServer(server_root, url=url)
+
+ # See https://mozilla.github.io/glean/book/user/pings/index.html#ping-submission
+ self._httpd.router.register(
+ "POST",
+ "/submit/{application_id}/{doc_type}/{glean_schema_version}/{document_id}",
+ pings_handler,
+ )
+
+ @property
+ def url(self):
+ """Return the URL for the running HTTP FixtureServer."""
+ return self._httpd.get_url("/")
+
+ @property
+ def port(self):
+ """Return the port for the running HTTP FixtureServer."""
+ parse_result = urlparse.urlparse(self.url)
+ return parse_result.port
+
+ def start(self):
+ """Start the HTTP FixtureServer."""
+ return self._httpd.start()
+
+ def stop(self):
+ """Stop the HTTP FixtureServer."""
+ return self._httpd.stop()
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py
new file mode 100644
index 0000000000..c5bc54e9d2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py
@@ -0,0 +1,63 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import mozlog
+
+from telemetry_harness.fog_ping_server import FOGPingServer
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class FOGTestCase(TelemetryTestCase):
+ """Base testcase class for project FOG."""
+
+ def __init__(self, *args, **kwargs):
+ """Initialize the test case and create a ping server."""
+ super(FOGTestCase, self).__init__(*args, **kwargs)
+ self._logger = mozlog.get_default_logger(component="FOGTestCase")
+
+ def setUp(self, *args, **kwargs):
+ """Set up the test case and create a FOG ping server.
+
+ This test is skipped if the build doesn't support FOG.
+ """
+ super(FOGTestCase, self).setUp(*args, **kwargs)
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ fog_android = self.marionette.execute_script(
+ "return AppConstants.MOZ_GLEAN_ANDROID;"
+ )
+
+ if fog_android:
+ # Before we skip this test, we need to quit marionette and the ping
+ # server created in TelemetryTestCase by running tearDown
+ super(FOGTestCase, self).tearDown(*args, **kwargs)
+ self.skipTest("FOG is only initialized when not in an Android build.")
+
+ self.fog_ping_server = FOGPingServer(
+ self.testvars["server_root"], "http://localhost:0"
+ )
+ self.fog_ping_server.start()
+
+ self._logger.info(
+ "Submitting to FOG ping server at {}".format(self.fog_ping_server.url)
+ )
+
+ self.marionette.enforce_gecko_prefs(
+ {
+ "telemetry.fog.test.localhost_port": self.fog_ping_server.port,
+ # Enable FOG logging. 5 means "Verbose". See
+ # https://firefox-source-docs.mozilla.org/xpcom/logging.html
+ # for details.
+ "logging.config.clear_on_startup": False,
+ "logging.config.sync": True,
+ "logging.fog::*": 5,
+ "logging.fog_control::*": 5,
+ "logging.glean::*": 5,
+ "logging.glean_core::*": 5,
+ }
+ )
+
+ def tearDown(self, *args, **kwargs):
+ super(FOGTestCase, self).tearDown(*args, **kwargs)
+ self.fog_ping_server.stop()
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_filters.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_filters.py
new file mode 100644
index 0000000000..6e003b25d5
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_filters.py
@@ -0,0 +1,75 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+class PingFilter(object):
+ """Ping filter that accepts any pings."""
+
+ def __call__(self, ping):
+ return True
+
+
+class DeletionRequestPingFilter(PingFilter):
+ """Ping filter that accepts deletion-request pings."""
+
+ def __call__(self, ping):
+ if not super(DeletionRequestPingFilter, self).__call__(ping):
+ return False
+
+ return ping["type"] == "deletion-request"
+
+
+class EventPingFilter(PingFilter):
+ """Ping filter that accepts event pings."""
+
+ def __call__(self, ping):
+ if not super(EventPingFilter, self).__call__(ping):
+ return False
+
+ return ping["type"] == "event"
+
+
+class FirstShutdownPingFilter(PingFilter):
+ """Ping filter that accepts first-shutdown pings."""
+
+ def __call__(self, ping):
+ if not super(FirstShutdownPingFilter, self).__call__(ping):
+ return False
+
+ return ping["type"] == "first-shutdown"
+
+
+class MainPingFilter(PingFilter):
+ """Ping filter that accepts main pings."""
+
+ def __call__(self, ping):
+ if not super(MainPingFilter, self).__call__(ping):
+ return False
+
+ return ping["type"] == "main"
+
+
+class MainPingReasonFilter(MainPingFilter):
+ """Ping filter that accepts main pings that match the
+ specified reason.
+ """
+
+ def __init__(self, reason):
+ super(MainPingReasonFilter, self).__init__()
+ self.reason = reason
+
+ def __call__(self, ping):
+ if not super(MainPingReasonFilter, self).__call__(ping):
+ return False
+
+ return ping["payload"]["info"]["reason"] == self.reason
+
+
+ANY_PING = PingFilter()
+DELETION_REQUEST_PING = DeletionRequestPingFilter()
+EVENT_PING = EventPingFilter()
+FIRST_SHUTDOWN_PING = FirstShutdownPingFilter()
+MAIN_PING = MainPingFilter()
+MAIN_SHUTDOWN_PING = MainPingReasonFilter("shutdown")
+MAIN_ENVIRONMENT_CHANGE_PING = MainPingReasonFilter("environment-change")
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_server.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_server.py
new file mode 100644
index 0000000000..86487672c7
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_server.py
@@ -0,0 +1,77 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import zlib
+
+import mozlog
+import wptserve.logger
+from marionette_harness.runner import httpd
+
+
+class PingServer(object):
+ """HTTP server for receiving Firefox Client Telemetry pings."""
+
+ def __init__(self, server_root, url):
+ self._logger = mozlog.get_default_logger(component="pingserver")
+
+ # Ensure we see logs from wptserve
+ try:
+ wptserve.logger.set_logger(self._logger)
+ except Exception:
+ # Raises if already been set
+ pass
+ self.pings = []
+
+ @httpd.handlers.handler
+ def pings_handler(request, response):
+ """Handler for HTTP requests to the ping server."""
+ request_data = request.body
+
+ if request.headers.get("Content-Encoding") == b"gzip":
+ request_data = zlib.decompress(request_data, zlib.MAX_WBITS | 16)
+
+ ping_data = json.loads(request_data)
+
+ # We don't have another channel to hand, so stuff this in the ping payload.
+ ping_data["X-PingSender-Version"] = request.headers.get(
+ "X-PingSender-Version", b""
+ )
+
+ # Store JSON data to self.pings to be used by wait_for_pings()
+ self.pings.append(ping_data)
+
+ ping_type = ping_data["type"]
+
+ log_message = "pings_handler received '{}' ping".format(ping_type)
+
+ if ping_type == "main":
+ ping_reason = ping_data["payload"]["info"]["reason"]
+ log_message = "{} with reason '{}'".format(log_message, ping_reason)
+
+ self._logger.info(log_message)
+
+ status_code = 200
+ content = "OK"
+ headers = [
+ ("Content-Type", "text/plain"),
+ ("Content-Length", len(content)),
+ ]
+
+ return (status_code, headers, content)
+
+ self._httpd = httpd.FixtureServer(server_root, url=url)
+ self._httpd.router.register("POST", "/pings*", pings_handler)
+
+ def get_url(self, *args, **kwargs):
+ """Return a URL from the HTTP server."""
+ return self._httpd.get_url(*args, **kwargs)
+
+ def start(self):
+ """Start the HTTP server."""
+ return self._httpd.start()
+
+ def stop(self):
+ """Stop the HTTP server."""
+ return self._httpd.stop()
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/dynamic_addon/dynamic-probe-telemetry-extension-signed.xpi b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/dynamic_addon/dynamic-probe-telemetry-extension-signed.xpi
new file mode 100644
index 0000000000..f399815c10
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/dynamic_addon/dynamic-probe-telemetry-extension-signed.xpi
Binary files differ
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/helloworld.html b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/helloworld.html
new file mode 100644
index 0000000000..146ad025d9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/helloworld.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ body {
+ background-color: lightgrey;
+ }
+ p {
+ font-size: 25px;
+ padding: 25px 50px;
+ }
+ </style>
+ </head>
+ <body>
+ <p>Hello World!</p>
+ </body>
+</html>
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/manifest.json b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/manifest.json
new file mode 100644
index 0000000000..0e35d8a2e3
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/manifest.json
@@ -0,0 +1,12 @@
+{
+ "description": "Extension to be installed in telemetry-tests-client tests.",
+ "manifest_version": 2,
+ "name": "helloworld",
+ "version": "1.0",
+ "homepage_url": "https://hg.mozilla.org/mozilla-central/",
+ "browser_action": {
+ "browser_style": true,
+ "default_title": "Hello World",
+ "default_popup": "helloworld.html"
+ }
+}
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py
new file mode 100644
index 0000000000..37a91023ce
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py
@@ -0,0 +1,63 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from marionette_harness import BaseMarionetteTestRunner
+
+from telemetry_harness.testcase import TelemetryTestCase
+
+SERVER_URL = "http://localhost:8000"
+
+
+class TelemetryTestRunner(BaseMarionetteTestRunner):
+ """TestRunner for the telemetry-tests-client suite."""
+
+ def __init__(self, **kwargs):
+ """Set test variables and preferences specific to Firefox client
+ telemetry.
+ """
+
+ # Select the appropriate GeckoInstance
+ kwargs["app"] = "fxdesktop"
+
+ prefs = kwargs.pop("prefs", {})
+
+ prefs["fission.autostart"] = True
+ if kwargs["disable_fission"]:
+ prefs["fission.autostart"] = False
+
+ # Set Firefox Client Telemetry specific preferences
+ prefs.update(
+ {
+ # Clear the region detection url to
+ # * avoid net access in tests
+ # * stabilize browser.search.region to avoid extra subsessions (bug 1579840#c40)
+ "browser.region.network.url": "",
+ # Disable smart sizing because it changes prefs at startup. (bug 1547750)
+ "browser.cache.disk.smart_size.enabled": False,
+ "toolkit.telemetry.server": "{}/pings".format(SERVER_URL),
+ "telemetry.fog.test.localhost_port": -1,
+ "toolkit.telemetry.initDelay": 1,
+ "toolkit.telemetry.minSubsessionLength": 0,
+ "datareporting.healthreport.uploadEnabled": True,
+ "datareporting.policy.dataSubmissionEnabled": True,
+ "datareporting.policy.dataSubmissionPolicyBypassNotification": True,
+ "toolkit.telemetry.log.level": "Trace",
+ "toolkit.telemetry.log.dump": True,
+ "toolkit.telemetry.send.overrideOfficialCheck": True,
+ "toolkit.telemetry.testing.disableFuzzingDelay": True,
+ # Disable Normandy to avoid extra subsessions due to Experiment
+ # activation in tests (bug 1641571)
+ "app.normandy.enabled": False,
+ # Disable Normandy a little harder (bug 1608807).
+ # This should also disable Nimbus.
+ "app.shield.optoutstudies.enabled": False,
+ }
+ )
+
+ super(TelemetryTestRunner, self).__init__(prefs=prefs, **kwargs)
+
+ self.testvars["server_root"] = kwargs["server_root"]
+ self.testvars["server_url"] = SERVER_URL
+
+ self.test_handlers = [TelemetryTestCase]
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runtests.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runtests.py
new file mode 100644
index 0000000000..4ecee669f1
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runtests.py
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from marionette_harness.runtests import cli as mn_cli
+
+from telemetry_harness.runner import TelemetryTestRunner
+
+
+def cli(args=None):
+ mn_cli(runner_class=TelemetryTestRunner, args=args)
+
+
+if __name__ == "__main__":
+ cli()
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/testcase.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/testcase.py
new file mode 100644
index 0000000000..b6f51e47b2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/testcase.py
@@ -0,0 +1,233 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import contextlib
+import os
+import re
+import textwrap
+
+from marionette_driver.addons import Addons
+from marionette_driver.errors import MarionetteException
+from marionette_driver.wait import Wait
+from marionette_harness import MarionetteTestCase
+from marionette_harness.runner.mixins.window_manager import WindowManagerMixin
+
+from telemetry_harness.ping_server import PingServer
+
+CANARY_CLIENT_ID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0"
+UUID_PATTERN = re.compile(
+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
+)
+
+
+class TelemetryTestCase(WindowManagerMixin, MarionetteTestCase):
+ def __init__(self, *args, **kwargs):
+ """Initialize the test case and create a ping server."""
+ super(TelemetryTestCase, self).__init__(*args, **kwargs)
+
+ def setUp(self, *args, **kwargs):
+ """Set up the test case and start the ping server."""
+
+ self.ping_server = PingServer(
+ self.testvars["server_root"], self.testvars["server_url"]
+ )
+ self.ping_server.start()
+
+ super(TelemetryTestCase, self).setUp(*args, **kwargs)
+
+ # Store IDs of addons installed via self.install_addon()
+ self.addon_ids = []
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CONTENT):
+ self.marionette.navigate("about:about")
+
+ def disable_telemetry(self):
+ """Disable the Firefox Data Collection and Use in the current browser."""
+ self.marionette.instance.profile.set_persistent_preferences(
+ {"datareporting.healthreport.uploadEnabled": False}
+ )
+ self.marionette.set_pref("datareporting.healthreport.uploadEnabled", False)
+
+ def enable_telemetry(self):
+ """Enable the Firefox Data Collection and Use in the current browser."""
+ self.marionette.instance.profile.set_persistent_preferences(
+ {"datareporting.healthreport.uploadEnabled": True}
+ )
+ self.marionette.set_pref("datareporting.healthreport.uploadEnabled", True)
+
+ @contextlib.contextmanager
+ def new_tab(self):
+ """Perform operations in a new tab and then close the new tab."""
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ start_tab = self.marionette.current_window_handle
+ new_tab = self.open_tab(focus=True)
+ self.marionette.switch_to_window(new_tab)
+
+ yield
+
+ self.marionette.close()
+ self.marionette.switch_to_window(start_tab)
+
+ def navigate_in_new_tab(self, url):
+ """Open a new tab and navigate to the provided URL."""
+
+ with self.new_tab():
+ with self.marionette.using_context(self.marionette.CONTEXT_CONTENT):
+ self.marionette.navigate(url)
+
+ def assertIsValidUUID(self, value):
+ """Check if the given UUID is valid."""
+
+ self.assertIsNotNone(value)
+ self.assertNotEqual(value, "")
+
+ # Check for client ID that is used when Telemetry upload is disabled
+ self.assertNotEqual(value, CANARY_CLIENT_ID, msg="UUID is CANARY CLIENT ID")
+
+ self.assertIsNotNone(
+ re.match(UUID_PATTERN, value),
+ msg="UUID does not match regular expression",
+ )
+
+ def wait_for_pings(self, action_func, ping_filter, count, ping_server=None):
+ """Call the given action and wait for pings to come in and return
+ the `count` number of pings, that match the given filter.
+ """
+
+ if ping_server is None:
+ ping_server = self.ping_server
+
+ # Keep track of the current number of pings
+ current_num_pings = len(ping_server.pings)
+
+ # New list to store new pings that satisfy the filter
+ filtered_pings = []
+
+ def wait_func(*args, **kwargs):
+ # Ignore existing pings in ping_server.pings
+ new_pings = ping_server.pings[current_num_pings:]
+
+ # Filter pings to make sure we wait for the correct ping type
+ filtered_pings[:] = [p for p in new_pings if ping_filter(p)]
+
+ return len(filtered_pings) >= count
+
+ self.logger.info(
+ "wait_for_pings running action '{action}'.".format(
+ action=action_func.__name__
+ )
+ )
+
+ # Call given action and wait for a ping
+ action_func()
+
+ try:
+ Wait(self.marionette, 60).until(wait_func)
+ except Exception as e:
+ self.fail("Error waiting for ping: {}".format(e))
+
+ return filtered_pings[:count]
+
+ def wait_for_ping(self, action_func, ping_filter, ping_server=None):
+ """Call wait_for_pings() with the given action_func and ping_filter and
+ return the first result.
+ """
+ [ping] = self.wait_for_pings(
+ action_func, ping_filter, 1, ping_server=ping_server
+ )
+ return ping
+
+ def restart_browser(self):
+ """Restarts browser while maintaining the same profile."""
+ return self.marionette.restart(clean=False, in_app=True)
+
+ def start_browser(self):
+ """Start the browser."""
+ return self.marionette.start_session()
+
+ def quit_browser(self):
+ """Quit the browser."""
+ return self.marionette.quit()
+
+ def install_addon(self):
+ """Install a minimal addon."""
+ addon_name = "helloworld"
+ self._install_addon(addon_name)
+
+ def install_dynamic_addon(self):
+ """Install a dynamic probe addon.
+
+ Source Code:
+ https://github.com/mozilla-extensions/dynamic-probe-telemetry-extension
+ """
+ addon_name = "dynamic_addon/dynamic-probe-telemetry-extension-signed.xpi"
+ self._install_addon(addon_name, temp=False)
+
+ def _install_addon(self, addon_name, temp=True):
+ """Logic to install addon and add its ID to self.addons.ids"""
+ resources_dir = os.path.join(os.path.dirname(__file__), "resources")
+ addon_path = os.path.abspath(os.path.join(resources_dir, addon_name))
+
+ try:
+ # Ensure the Environment has init'd so the installed addon
+ # triggers an "environment-change" ping.
+ script = """\
+ let [resolve] = arguments;
+ const { TelemetryEnvironment } = ChromeUtils.import(
+ "resource://gre/modules/TelemetryEnvironment.jsm"
+ );
+ TelemetryEnvironment.onInitialized().then(resolve);
+ """
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ self.marionette.execute_async_script(textwrap.dedent(script))
+
+ addons = Addons(self.marionette)
+ addon_id = addons.install(addon_path, temp=temp)
+ except MarionetteException as e:
+ self.fail("{} - Error installing addon: {} - ".format(e.cause, e))
+ else:
+ self.addon_ids.append(addon_id)
+
+ def set_persistent_profile_preferences(self, preferences):
+ """Wrapper for setting persistent preferences on a user profile"""
+ return self.marionette.instance.profile.set_persistent_preferences(preferences)
+
+ def set_preferences(self, preferences):
+ """Wrapper for setting persistent preferences on a user profile"""
+ return self.marionette.set_prefs(preferences)
+
+ @property
+ def client_id(self):
+ """Return the ID of the current client."""
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ return self.marionette.execute_script(
+ """\
+ const { ClientID } = ChromeUtils.import(
+ "resource://gre/modules/ClientID.jsm"
+ );
+ return ClientID.getCachedClientID();
+ """
+ )
+
+ @property
+ def subsession_id(self):
+ """Return the ID of the current subsession."""
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ ping_data = self.marionette.execute_script(
+ """\
+ const { TelemetryController } = ChromeUtils.import(
+ "resource://gre/modules/TelemetryController.jsm"
+ );
+ return TelemetryController.getCurrentPingData(true);
+ """
+ )
+ return ping_data[u"payload"][u"info"][u"subsessionId"]
+
+ def tearDown(self, *args, **kwargs):
+ """Stop the ping server and tear down the testcase."""
+ super(TelemetryTestCase, self).tearDown()
+ self.ping_server.stop()
+ self.marionette.quit(in_app=False, clean=True)
diff --git a/toolkit/components/telemetry/tests/marionette/mach_commands.py b/toolkit/components/telemetry/tests/marionette/mach_commands.py
new file mode 100644
index 0000000000..c518a894e6
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/mach_commands.py
@@ -0,0 +1,95 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import argparse
+import logging
+import os
+import sys
+
+from mach.decorators import Command
+from mozbuild.base import BinaryNotFoundException
+from mozbuild.base import MachCommandConditions as conditions
+
+
+def create_parser_tests():
+ from marionette_harness.runtests import MarionetteArguments
+ from mozlog.structured import commandline
+
+ parser = MarionetteArguments()
+ commandline.add_logging_group(parser)
+ return parser
+
+
+def run_telemetry(tests, binary=None, topsrcdir=None, **kwargs):
+ from marionette_harness.runtests import MarionetteHarness
+ from mozlog.structured import commandline
+ from telemetry_harness.runtests import TelemetryTestRunner
+
+ parser = create_parser_tests()
+
+ if not tests:
+ tests = [
+ os.path.join(
+ topsrcdir,
+ "toolkit/components/telemetry/tests/marionette/tests/manifest.ini",
+ )
+ ]
+
+ args = argparse.Namespace(tests=tests)
+
+ args.binary = binary
+ args.logger = kwargs.pop("log", None)
+
+ for k, v in kwargs.items():
+ setattr(args, k, v)
+
+ parser.verify_usage(args)
+
+ os.environ["MOZ_IGNORE_NSS_SHUTDOWN_LEAKS"] = "1"
+
+ # Causes Firefox to crash when using non-local connections.
+ os.environ["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
+
+ if not args.logger:
+ args.logger = commandline.setup_logging(
+ "Telemetry Client Tests", args, {"mach": sys.stdout}
+ )
+ failed = MarionetteHarness(TelemetryTestRunner, args=vars(args)).run()
+ if failed > 0:
+ return 1
+ return 0
+
+
+@Command(
+ "telemetry-tests-client",
+ category="testing",
+ description="Run tests specifically for the Telemetry client",
+ conditions=[conditions.is_firefox_or_android],
+ parser=create_parser_tests,
+)
+def telemetry_test(command_context, tests, **kwargs):
+ if "test_objects" in kwargs:
+ tests = []
+ for obj in kwargs["test_objects"]:
+ tests.append(obj["file_relpath"])
+ del kwargs["test_objects"]
+ if not kwargs.get("binary") and conditions.is_firefox(command_context):
+ try:
+ kwargs["binary"] = command_context.get_binary_path("app")
+ except BinaryNotFoundException as e:
+ command_context.log(
+ logging.ERROR,
+ "telemetry-tests-client",
+ {"error": str(e)},
+ "ERROR: {error}",
+ )
+ command_context.log(
+ logging.INFO, "telemetry-tests-client", {"help": e.help()}, "{help}"
+ )
+ return 1
+ if not kwargs.get("server_root"):
+ kwargs[
+ "server_root"
+ ] = "toolkit/components/telemetry/tests/marionette/harness/www"
+ return run_telemetry(tests, topsrcdir=command_context.topsrcdir, **kwargs)
diff --git a/toolkit/components/telemetry/tests/marionette/moz.build b/toolkit/components/telemetry/tests/marionette/moz.build
new file mode 100644
index 0000000000..7a6aa0bc15
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TELEMETRY_TESTS_CLIENT_MANIFESTS += ["tests/manifest.ini"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Telemetry")
+ SCHEDULES.exclusive = ["telemetry-tests-client"]
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/manifest.ini b/toolkit/components/telemetry/tests/marionette/tests/client/manifest.ini
new file mode 100644
index 0000000000..b5a251906e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/manifest.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = client
+
+[test_deletion_request_ping.py]
+[test_main_tab_scalars.py]
+[test_subsession_management.py]
+[test_fog_deletion_request_ping.py]
+[test_fog_custom_ping.py]
+[test_fog_user_activity.py]
+[test_dynamic_probes.py]
+[test_shutdown_pings_succeed.py]
+[test_unicode_encoding.py]
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_deletion_request_ping.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_deletion_request_ping.py
new file mode 100644
index 0000000000..92219c7dc7
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_deletion_request_ping.py
@@ -0,0 +1,83 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.ping_filters import (
+ ANY_PING,
+ DELETION_REQUEST_PING,
+ MAIN_SHUTDOWN_PING,
+)
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestDeletionRequestPing(TelemetryTestCase):
+ """Tests for "deletion-request" ping."""
+
+ def test_deletion_request_ping_across_sessions(self):
+ """Test the "deletion-request" ping behaviour across sessions."""
+
+ # Get the client_id.
+ client_id = self.wait_for_ping(self.install_addon, ANY_PING)["clientId"]
+ self.assertIsValidUUID(client_id)
+
+ # Trigger an "deletion-request" ping.
+ ping = self.wait_for_ping(self.disable_telemetry, DELETION_REQUEST_PING)
+
+ self.assertIn("clientId", ping)
+ self.assertIn("payload", ping)
+ self.assertNotIn("environment", ping["payload"])
+
+ # Close Firefox cleanly.
+ self.quit_browser()
+
+ # TODO: Check pending pings aren't persisted
+
+ # Start Firefox.
+ self.start_browser()
+
+ # Trigger an environment change, which isn't allowed to send a ping.
+ self.install_addon()
+
+ # Ensure we've sent no pings since "disabling telemetry".
+ self.assertEqual(self.ping_server.pings[-1], ping)
+
+ # Turn Telemetry back on.
+ self.enable_telemetry()
+
+ # Enabling telemetry resets the client ID,
+ # so we can wait for it to be set.
+ #
+ # **WARNING**
+ #
+ # This MUST NOT be used outside of telemetry tests to wait for that kind of signal.
+ # Reach out to the Telemetry Team if you have a need for that.
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ return self.marionette.execute_async_script(
+ """
+ let [resolve] = arguments;
+ const { ClientID } = ChromeUtils.import(
+ "resource://gre/modules/ClientID.jsm"
+ );
+ ClientID.getClientID().then(resolve);
+ """,
+ script_timeout=1000,
+ )
+
+ # Close Firefox cleanly, collecting its "main"/"shutdown" ping.
+ main_ping = self.wait_for_ping(self.restart_browser, MAIN_SHUTDOWN_PING)
+
+ # Ensure the "main" ping has changed its client id.
+ self.assertIn("clientId", main_ping)
+ self.assertIsValidUUID(main_ping["clientId"])
+ self.assertNotEqual(main_ping["clientId"], client_id)
+
+ # Ensure we note in the ping that the user opted in.
+ parent_scalars = main_ping["payload"]["processes"]["parent"]["scalars"]
+
+ self.assertIn("telemetry.data_upload_optin", parent_scalars)
+ self.assertIs(parent_scalars["telemetry.data_upload_optin"], True)
+
+ # Ensure all pings sent during this test don't have the c0ffee client id.
+ for ping in self.ping_server.pings:
+ if "clientId" in ping:
+ self.assertIsValidUUID(ping["clientId"])
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_dynamic_probes.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_dynamic_probes.py
new file mode 100644
index 0000000000..27cd0eedcb
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_dynamic_probes.py
@@ -0,0 +1,26 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.ping_filters import (
+ MAIN_ENVIRONMENT_CHANGE_PING,
+ MAIN_SHUTDOWN_PING,
+)
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestDynamicProbes(TelemetryTestCase):
+ """Tests for Dynamic Probes."""
+
+ def test_dynamic_probes(self):
+ """Test for dynamic probes."""
+ self.wait_for_ping(self.install_dynamic_addon, MAIN_ENVIRONMENT_CHANGE_PING)
+
+ ping = self.wait_for_ping(self.restart_browser, MAIN_SHUTDOWN_PING)
+
+ [addon_id] = self.addon_ids # check addon id exists
+ active_addons = ping["environment"]["addons"]["activeAddons"]
+ self.assertIn(addon_id, active_addons)
+
+ scalars = ping["payload"]["processes"]["dynamic"]["scalars"]
+ self.assertEqual(scalars["dynamic.probe.counter_scalar"], 1337)
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_custom_ping.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_custom_ping.py
new file mode 100644
index 0000000000..11bf07472c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_custom_ping.py
@@ -0,0 +1,24 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.fog_ping_filters import FOG_ONE_PING_ONLY_PING
+from telemetry_harness.fog_testcase import FOGTestCase
+
+
+class TestDeletionRequestPing(FOGTestCase):
+ """Tests for the "one-ping-only" FOG custom ping."""
+
+ def test_one_ping_only_ping(self):
+ def send_opo_ping(marionette):
+ ping_sending_script = "GleanPings.onePingOnly.submit();"
+ with marionette.using_context(marionette.CONTEXT_CHROME):
+ marionette.execute_script(ping_sending_script)
+
+ ping1 = self.wait_for_ping(
+ lambda: send_opo_ping(self.marionette),
+ FOG_ONE_PING_ONLY_PING,
+ ping_server=self.fog_ping_server,
+ )
+
+ self.assertNotIn("client_id", ping1["payload"]["client_info"])
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_deletion_request_ping.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_deletion_request_ping.py
new file mode 100644
index 0000000000..5b4fc89e60
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_deletion_request_ping.py
@@ -0,0 +1,67 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import textwrap
+
+from telemetry_harness.fog_ping_filters import FOG_DELETION_REQUEST_PING
+from telemetry_harness.fog_testcase import FOGTestCase
+
+
+class TestDeletionRequestPing(FOGTestCase):
+ """Tests for FOG deletion-request ping."""
+
+ def test_deletion_request_ping_across_sessions(self):
+ """Test the "deletion-request" ping behaviour across sessions."""
+
+ self.navigate_in_new_tab("about:glean")
+
+ ping1 = self.wait_for_ping(
+ self.disable_telemetry,
+ FOG_DELETION_REQUEST_PING,
+ ping_server=self.fog_ping_server,
+ )
+
+ self.assertIn("ping_info", ping1["payload"])
+ self.assertIn("client_info", ping1["payload"])
+
+ self.assertIn("client_id", ping1["payload"]["client_info"])
+ client_id1 = ping1["payload"]["client_info"]["client_id"]
+ self.assertIsValidUUID(client_id1)
+
+ self.restart_browser()
+
+ # We'd like to assert that a "deletion-request" is the last ping we
+ # ever receive, but it's possible there's another ping on another
+ # thread that gets sent after the sync-sent "deletion-request".
+ # (This is fine, it'll be deleted within 28 days on the server.)
+ # self.assertEqual(self.fog_ping_server.pings[-1], ping1)
+
+ self.enable_telemetry()
+ self.restart_browser()
+
+ debug_tag = "my-test-tag"
+ tagging_script = """\
+ Services.fog.setTagPings("{}");
+ """.format(
+ debug_tag
+ )
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ self.marionette.execute_script(textwrap.dedent(tagging_script))
+ self.navigate_in_new_tab("about:glean")
+
+ ping2 = self.wait_for_ping(
+ self.disable_telemetry,
+ FOG_DELETION_REQUEST_PING,
+ ping_server=self.fog_ping_server,
+ )
+
+ self.assertEqual(ping2["debug_tag"].decode("utf-8"), debug_tag)
+
+ self.assertIn("client_id", ping2["payload"]["client_info"])
+ client_id2 = ping2["payload"]["client_info"]["client_id"]
+ self.assertIsValidUUID(client_id2)
+
+ # Verify that FOG creates a new client ID when a user
+ # opts out of sending technical and interaction data.
+ self.assertNotEqual(client_id2, client_id1)
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_user_activity.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_user_activity.py
new file mode 100644
index 0000000000..fec7147632
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_user_activity.py
@@ -0,0 +1,47 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.fog_ping_filters import FOG_BASELINE_PING
+from telemetry_harness.fog_testcase import FOGTestCase
+
+
+class TestClientActivity(FOGTestCase):
+ """Tests for client activity and FOG's scheduling of the "baseline" ping."""
+
+ def test_user_activity(self):
+
+ # First test that restarting the browser sends a "active" ping
+ ping0 = self.wait_for_ping(
+ self.restart_browser, FOG_BASELINE_PING, ping_server=self.fog_ping_server
+ )
+ self.assertEqual("active", ping0["payload"]["ping_info"]["reason"])
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ zero_prefs_script = """\
+ Services.prefs.setIntPref("telemetry.fog.test.inactivity_limit", 0);
+ Services.prefs.setIntPref("telemetry.fog.test.activity_limit", 0);
+ """
+ self.marionette.execute_script(zero_prefs_script)
+
+ def user_active(active, marionette):
+ script = "Services.obs.notifyObservers(null, 'user-interaction-{}active')".format(
+ "" if active else "in"
+ )
+ with marionette.using_context(marionette.CONTEXT_CHROME):
+ marionette.execute_script(script)
+
+ ping1 = self.wait_for_ping(
+ lambda: user_active(True, self.marionette),
+ FOG_BASELINE_PING,
+ ping_server=self.fog_ping_server,
+ )
+
+ ping2 = self.wait_for_ping(
+ lambda: user_active(False, self.marionette),
+ FOG_BASELINE_PING,
+ ping_server=self.fog_ping_server,
+ )
+
+ self.assertEqual("active", ping1["payload"]["ping_info"]["reason"])
+ self.assertEqual("inactive", ping2["payload"]["ping_info"]["reason"])
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_main_tab_scalars.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_main_tab_scalars.py
new file mode 100644
index 0000000000..d548d7ccc9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_main_tab_scalars.py
@@ -0,0 +1,54 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.ping_filters import MAIN_SHUTDOWN_PING
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestMainTabScalars(TelemetryTestCase):
+ """Tests for Telemetry Scalars."""
+
+ def test_main_tab_scalars(self):
+ """Test for Telemetry Scalars."""
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ # Bug 1829464: BrowserUsageTelemetry's telemetry collection about
+ # open tabs is async. We manually set the task timeout here to 0 ms
+ # so that it instead happens immediately after a tab opens. This
+ # prevents race conditions between telemetry submission and our
+ # test.
+ self.marionette.execute_script(
+ """
+ const { BrowserUsageTelemetry } = ChromeUtils.import(
+ "resource:///modules/BrowserUsageTelemetry.jsm"
+ );
+
+ BrowserUsageTelemetry._onTabsOpenedTask._timeoutMs = 0;
+ """
+ )
+
+ start_tab = self.marionette.current_window_handle
+
+ tab2 = self.open_tab(focus=True)
+ self.marionette.switch_to_window(tab2)
+
+ tab3 = self.open_tab(focus=True)
+ self.marionette.switch_to_window(tab3)
+
+ self.marionette.close()
+ self.marionette.switch_to_window(tab2)
+
+ self.marionette.close()
+ self.marionette.switch_to_window(start_tab)
+
+ ping = self.wait_for_ping(self.restart_browser, MAIN_SHUTDOWN_PING)
+
+ self.assertEqual(ping["type"], "main")
+ self.assertEqual(ping["clientId"], self.client_id)
+
+ scalars = ping["payload"]["processes"]["parent"]["scalars"]
+
+ self.assertEqual(scalars["browser.engagement.max_concurrent_tab_count"], 3)
+ self.assertEqual(scalars["browser.engagement.tab_open_event_count"], 2)
+ self.assertEqual(scalars["browser.engagement.max_concurrent_window_count"], 1)
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_shutdown_pings_succeed.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_shutdown_pings_succeed.py
new file mode 100644
index 0000000000..c8c1743146
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_shutdown_pings_succeed.py
@@ -0,0 +1,55 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from marionette_harness import parameterized
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestShutdownPingsSucced(TelemetryTestCase):
+ """Test Firefox shutdown pings."""
+
+ def tearDown(self):
+ super(TestShutdownPingsSucced, self).tearDown()
+
+ # We need a fresh profile next run in order that the "new-profile" and
+ # "first-shutdown" pings are sent.
+ self.marionette.profile = None
+
+ @parameterized("pingsender1", pingsender_version=b"1.0")
+ @parameterized("pingsender2", pingsender_version=b"2.0")
+ def test_shutdown_pings_succeed(self, pingsender_version=b""):
+ """Test that known Firefox shutdown pings are received, with the correct
+ X-PingSender-Version headers."""
+
+ pingsender2_enabled = {b"1.0": False, b"2.0": True}[pingsender_version]
+ self.marionette.set_pref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ pingsender2_enabled,
+ )
+
+ # Map ping type to expected X-PingSender-Version header. Not all pings
+ # will be sent via pingsender, so they might have an empty (binary)
+ # string version.
+ ping_types = {
+ "event": pingsender_version,
+ "first-shutdown": pingsender_version,
+ "main": b"",
+ "new-profile": pingsender_version,
+ }
+
+ # We don't need the browser after this, but the harness expects a
+ # running browser to clean up, so we `restart_browser` rather than
+ # `quit_browser`.
+ pings = self.wait_for_pings(
+ self.restart_browser,
+ lambda p: p["type"] in ping_types.keys(),
+ len(ping_types),
+ )
+
+ self.assertEqual(len(pings), len(ping_types))
+ self.assertEqual(set(ping_types.keys()), set(p["type"] for p in pings))
+
+ self.assertEqual(
+ ping_types, dict((p["type"], p["X-PingSender-Version"]) for p in pings)
+ )
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_subsession_management.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_subsession_management.py
new file mode 100644
index 0000000000..c663f02eae
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_subsession_management.py
@@ -0,0 +1,147 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.ping_filters import (
+ MAIN_ENVIRONMENT_CHANGE_PING,
+ MAIN_SHUTDOWN_PING,
+)
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestSubsessionManagement(TelemetryTestCase):
+ """Tests for Firefox Telemetry subsession management."""
+
+ def test_subsession_management(self):
+ """Test for Firefox Telemetry subsession management."""
+
+ # Session S1, subsession 1
+ # Actions:
+ # 1. Open browser
+ # 2. Open a new tab
+ # 3. Restart browser in new session
+
+ with self.new_tab():
+ # If Firefox Telemetry is working correctly, this will
+ # be sufficient to record a tab open event.
+ pass
+
+ ping1 = self.wait_for_ping(self.restart_browser, MAIN_SHUTDOWN_PING)
+
+ # Session S2, subsession 1
+ # Outcome 1:
+ # Received a main ping P1 for previous session
+ # - Ping base contents:
+ # - clientId should be a valid UUID
+ # - reason should be "shutdown"
+ # - sessionId should be set
+ # - subsessionId should be set
+ # - previousSessionId should not be set
+ # - previousSubsessionId should not be set
+ # - subSessionCounter should be 1
+ # - profileSubSessionCounter should be 1
+ # - Other ping contents:
+ # - tab_open_event_count in scalars
+
+ client_id = ping1["clientId"]
+ self.assertIsValidUUID(client_id)
+
+ ping1_info = ping1["payload"]["info"]
+ self.assertEqual(ping1_info["reason"], "shutdown")
+
+ s1_session_id = ping1_info["sessionId"]
+ self.assertNotEqual(s1_session_id, "")
+
+ s1_s1_subsession_id = ping1_info["subsessionId"]
+ self.assertNotEqual(s1_s1_subsession_id, "")
+ self.assertIsNone(ping1_info["previousSessionId"])
+ self.assertIsNone(ping1_info["previousSubsessionId"])
+ self.assertEqual(ping1_info["subsessionCounter"], 1)
+ self.assertEqual(ping1_info["profileSubsessionCounter"], 1)
+
+ scalars1 = ping1["payload"]["processes"]["parent"]["scalars"]
+ self.assertNotIn("browser.engagement.window_open_event_count", scalars1)
+ self.assertEqual(scalars1["browser.engagement.tab_open_event_count"], 1)
+
+ # Actions:
+ # 1. Install addon
+
+ ping2 = self.wait_for_ping(self.install_addon, MAIN_ENVIRONMENT_CHANGE_PING)
+
+ [addon_id] = self.addon_ids # Store the addon ID for verifying ping3 later
+
+ # Session S2, subsession 2
+ # Outcome 2:
+ # Received a main ping P2 for previous subsession
+ # - Ping base contents:
+ # - clientId should be set to the same value
+ # - sessionId should be set to a new value
+ # - subsessionId should be set to a new value
+ # - previousSessionId should be set to P1s sessionId value
+ # - previousSubsessionId should be set to P1s subsessionId value
+ # - subSessionCounter should be 1
+ # - profileSubSessionCounter should be 2
+ # - reason should be "environment-change"
+ # - Other ping contents:
+ # - tab_open_event_count not in scalars
+
+ self.assertEqual(ping2["clientId"], client_id)
+
+ ping2_info = ping2["payload"]["info"]
+ self.assertEqual(ping2_info["reason"], "environment-change")
+
+ s2_session_id = ping2_info["sessionId"]
+ self.assertNotEqual(s2_session_id, s1_session_id)
+
+ s2_s1_subsession_id = ping2_info["subsessionId"]
+ self.assertNotEqual(s2_s1_subsession_id, s1_s1_subsession_id)
+ self.assertEqual(ping2_info["previousSessionId"], s1_session_id)
+ self.assertEqual(ping2_info["previousSubsessionId"], s1_s1_subsession_id)
+ self.assertEqual(ping2_info["subsessionCounter"], 1)
+ self.assertEqual(ping2_info["profileSubsessionCounter"], 2)
+
+ scalars2 = ping2["payload"]["processes"]["parent"]["scalars"]
+ self.assertNotIn("browser.engagement.window_open_event_count", scalars2)
+ self.assertNotIn("browser.engagement.tab_open_event_count", scalars2)
+
+ # Actions
+ # 1. Restart browser in new session
+
+ ping3 = self.wait_for_ping(self.restart_browser, MAIN_SHUTDOWN_PING)
+
+ # Session S3, subsession 1
+ # Outcome 3:
+ # Received a main ping P3 for session 2, subsession 2
+ # - Ping base contents:
+ # - clientId should be set to the same value
+ # - sessionId should be set to P2s sessionId value
+ # - subsessionId should be set to a new value
+ # - previousSessionId should be set to P1s sessionId value
+ # - previousSubsessionId should be set to P2s subsessionId value
+ # - subSessionCounter should be 2
+ # - profileSubSessionCounter should be 3
+ # - reason should be "shutdown"
+ # - Other ping contents:
+ # - addon ID in activeAddons in environment
+
+ self.assertEqual(ping3["clientId"], client_id)
+
+ ping3_info = ping3["payload"]["info"]
+ self.assertEqual(ping3_info["reason"], "shutdown")
+
+ self.assertEqual(ping3_info["sessionId"], s2_session_id)
+
+ s2_s2_subsession_id = ping3_info["subsessionId"]
+ self.assertNotEqual(s2_s2_subsession_id, s1_s1_subsession_id)
+ self.assertNotEqual(s2_s2_subsession_id, s2_s1_subsession_id)
+ self.assertEqual(ping3_info["previousSessionId"], s1_session_id)
+ self.assertEqual(ping3_info["previousSubsessionId"], s2_s1_subsession_id)
+ self.assertEqual(ping3_info["subsessionCounter"], 2)
+ self.assertEqual(ping3_info["profileSubsessionCounter"], 3)
+
+ scalars3 = ping3["payload"]["processes"]["parent"]["scalars"]
+ self.assertNotIn("browser.engagement.window_open_event_count", scalars3)
+ self.assertNotIn("browser.engagement.tab_open_event_count", scalars3)
+
+ active_addons = ping3["environment"]["addons"]["activeAddons"]
+ self.assertIn(addon_id, active_addons)
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_unicode_encoding.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_unicode_encoding.py
new file mode 100644
index 0000000000..748565c0ff
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_unicode_encoding.py
@@ -0,0 +1,39 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.ping_filters import MAIN_SHUTDOWN_PING
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestUnicodeEncoding(TelemetryTestCase):
+ """Tests for Firefox Telemetry Unicode encoding."""
+
+ def test_unicode_encoding(self):
+ """Test for Firefox Telemetry Unicode encoding."""
+
+ # We can use any string (not char!) pref to test the round-trip.
+ pref = "app.support.baseURL"
+ orig = "€ —"
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ value = self.marionette.execute_script(
+ r"""
+ Services.prefs.setStringPref("{pref}", "{orig}");
+ return Services.prefs.getStringPref("{pref}");
+ """.format(
+ orig=orig,
+ pref=pref,
+ )
+ )
+
+ self.assertEqual(value, orig)
+
+ # We don't need the browser after this, but the harness expects a
+ # running browser to clean up, so we `restart_browser` rather than
+ # `quit_browser`.
+ ping1 = self.wait_for_ping(self.restart_browser, MAIN_SHUTDOWN_PING)
+
+ self.assertEqual(
+ ping1["environment"]["settings"]["userPrefs"][pref],
+ orig,
+ )
diff --git a/toolkit/components/telemetry/tests/marionette/tests/manifest.ini b/toolkit/components/telemetry/tests/marionette/tests/manifest.ini
new file mode 100644
index 0000000000..b5e6f442e9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/manifest.ini
@@ -0,0 +1,2 @@
+[include:unit/manifest.ini]
+[include:client/manifest.ini]
diff --git a/toolkit/components/telemetry/tests/marionette/tests/unit/manifest.ini b/toolkit/components/telemetry/tests/marionette/tests/unit/manifest.ini
new file mode 100644
index 0000000000..9bb2de707a
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/unit/manifest.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+tags = unit
+
+[test_ping_server_received_ping.py] \ No newline at end of file
diff --git a/toolkit/components/telemetry/tests/marionette/tests/unit/test_ping_server_received_ping.py b/toolkit/components/telemetry/tests/marionette/tests/unit/test_ping_server_received_ping.py
new file mode 100644
index 0000000000..ffe606098f
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/unit/test_ping_server_received_ping.py
@@ -0,0 +1,45 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/
+
+import requests
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestPingServer(TelemetryTestCase):
+ def setUp(self, *args, **kwargs):
+ """Set up the test case retrieve the pings URL."""
+ super(TestPingServer, self).setUp(*args, **kwargs)
+ self.pings_url = self.ping_server.get_url("/pings")
+
+ def test_ping_server_received_ping(self):
+ ping_type = "server-test-ping"
+ ping_reason = "unit-test"
+
+ def send_ping_request():
+ """Perform a POST request to the ping server."""
+ data = {"type": ping_type, "reason": ping_reason}
+ headers = {
+ "Content-type": "application/json",
+ "Accept": "text/plain",
+ }
+
+ response = requests.post(self.pings_url, json=data, headers=headers)
+
+ self.assertEqual(
+ response.status_code,
+ 200,
+ msg="Error sending POST request to ping server: {response.text}".format(
+ response=response
+ ),
+ )
+ return response
+
+ def ping_filter_func(ping):
+ return ping["type"] == ping_type
+
+ [ping] = self.wait_for_pings(send_ping_request, ping_filter_func, 1)
+
+ self.assertIsNotNone(ping)
+ self.assertEqual(ping["type"], ping_type)
+ self.assertEqual(ping["reason"], ping_reason)