summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/tests/marionette/harness
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/telemetry/tests/marionette/harness
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/telemetry/tests/marionette/harness')
-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
15 files changed, 729 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..d30fd67986
--- /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["payload"]["info"]["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)