Bug 1632790 - Develop new tt(c) test for the FOG deletion-request ping; r=chutten
authorRaphael Pierzina <rpierzina@mozilla.com>
Mon, 22 Jun 2020 14:18:57 +0000
changeset 536591 7b022a19b8d9f7bd809a9ba9df0fb6bc3fa09a85
parent 536590 2e0776cdbf050558e4107fb0bdadffc70dc7084d
child 536592 02deda62a783db6a440a4870ddc2dc8e7e2a01c5
push id119553
push userrpierzina@mozilla.com
push dateMon, 22 Jun 2020 15:01:27 +0000
treeherderautoland@7b022a19b8d9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerschutten
bugs1632790
milestone79.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1632790 - Develop new tt(c) test for the FOG deletion-request ping; r=chutten Differential Revision: https://phabricator.services.mozilla.com/D80426
toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py
toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_server.py
toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py
toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/testcase.py
toolkit/components/telemetry/tests/marionette/tests/client/manifest.ini
toolkit/components/telemetry/tests/marionette/tests/client/test_fog_deletion_request_ping.py
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py
@@ -0,0 +1,28 @@
+# 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_DELETION_REQUEST_PING = FOGDocTypePingFilter("deletion-request")
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_server.py
@@ -0,0 +1,73 @@
+# 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
+
+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")
+        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") == "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)}
+            )
+
+            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()
new file mode 100644
--- /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 os
+import shutil
+import tempfile
+
+import mozlog
+from telemetry_harness.fog_ping_server import FOGPingServer
+from telemetry_harness.testcase import TelemetryTestCase
+
+# See https://firefox-source-docs.mozilla.org/toolkit/components/glean/preferences.html
+FOG_CHANNELS = ["default", "nightly"]
+
+
+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)
+
+        if self.marionette.get_pref("app.update.channel") not in FOG_CHANNELS:
+            # 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 only builds for channels {}".format(FOG_CHANNELS))
+
+        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)
+        )
+
+        # Make sure to escape the fog_data_path to avoid Unicode character
+        # escape sequence errors on Windows when setting Gecko preferences
+        self.fog_data_path = os.path.abspath(tempfile.mkdtemp()).encode("string-escape")
+        self._logger.info("Using FOG data_path {}".format(self.fog_data_path))
+
+        self.marionette.enforce_gecko_prefs(
+            {
+                "telemetry.fog.temporary_and_just_for_testing.data_path": self.fog_data_path,
+                "telemetry.fog.test.localhost_port": self.fog_ping_server.port,
+            }
+        )
+
+    def tearDown(self, *args, **kwargs):
+        super(FOGTestCase, self).tearDown(*args, **kwargs)
+        self.fog_ping_server.stop()
+        self._logger.info("Removing FOG data_path {}".format(self.fog_data_path))
+        shutil.rmtree(self.fog_data_path)
--- a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/testcase.py
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/testcase.py
@@ -102,29 +102,33 @@ class TelemetryTestCase(WindowManagerMix
             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):
+    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(self.ping_server.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 self.ping_server.pings
-            new_pings = self.ping_server.pings[current_num_pings:]
+            # 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(
@@ -137,21 +141,21 @@ class TelemetryTestCase(WindowManagerMix
 
         try:
             Wait(self.marionette, 60).until(wait_func)
         except Exception as e:
             self.fail("Error waiting for ping: {}".format(e.message))
 
         return filtered_pings[:count]
 
-    def wait_for_ping(self, action_func, ping_filter):
+    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] = 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."""
--- a/toolkit/components/telemetry/tests/marionette/tests/client/manifest.ini
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/manifest.ini
@@ -2,8 +2,9 @@
 tags = client
 
 [test_deletion_request_ping.py]
 [test_event_ping.py]
 [test_main_tab_scalars.py]
 [test_search_counts_across_sessions.py]
 skip-if = true #bug 1589297
 [test_subsession_management.py]
+[test_fog_deletion_request_ping.py]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_deletion_request_ping.py
@@ -0,0 +1,51 @@
+# 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_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.search_in_new_tab("mozilla firefox")
+
+        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()
+
+        self.assertEqual(self.fog_ping_server.pings[-1], ping1)
+
+        self.enable_telemetry()
+        self.restart_browser()
+
+        self.search_in_new_tab("python unittest")
+
+        ping2 = self.wait_for_ping(
+            self.disable_telemetry,
+            FOG_DELETION_REQUEST_PING,
+            ping_server=self.fog_ping_server,
+        )
+
+        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)