diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/telemetry/tests/marionette | |
parent | Initial commit. (diff) | |
download | firefox-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')
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 Binary files differnew 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 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) |