Le jeudi 30 novembre 2017 à 22:39:52+0100, Pierre-Elliott Bécue a écrit :
> Control: tags -1 patch
> 
> Here are two patches to solve the bug. They might need some rework, as I'm
> not quite sure I did my best here.
> 
> Essentially, the issue was the Python3 transition: the file wnpp_rm is
> treated as bytes, but the function receiving it and analyzing was expecting
> content of type str.
> 
> I used a function I wrote for a previous feature request to ensure that the
> content is decoded.

Based on comments I received on IRC, here are new patches that are a little
better and more atomic.

-- 
PEB
From 4e8798ae4d3d79227a09607e58dff1339210493a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pierre-Elliott=20B=C3=A9cue?= <be...@crans.org>
Date: Fri, 1 Dec 2017 14:43:42 +0100
Subject: [PATCH 1/4] Adds an only_if_updated argument to get_resource
 functions

---
 distro_tracker/core/tests/tests_utils.py | 97 ++++++++++++++++++++++++++++++--
 distro_tracker/core/utils/http.py        | 28 +++++++--
 2 files changed, 114 insertions(+), 11 deletions(-)

diff --git a/distro_tracker/core/tests/tests_utils.py b/distro_tracker/core/tests/tests_utils.py
index 572d31f..3f5d75d 100644
--- a/distro_tracker/core/tests/tests_utils.py
+++ b/distro_tracker/core/tests/tests_utils.py
@@ -764,13 +764,11 @@ Section: libs
 
 
 class HttpCacheTest(SimpleTestCase):
-    def set_mock_response(self, mock_requests, headers=None, status_code=200):
-        set_mock_response(
-            mock_requests,
-            text=self.response_content.decode('utf-8'),
-            headers=headers,
-            status_code=status_code)
+    """Tests for the HttpCache utility"""
 
+    #
+    # Tests constructors
+    #
     def setUp(self):
         # Set up a cache directory to use in the tests
         self.cache_directory = tempfile.mkdtemp(suffix='test-cache')
@@ -783,6 +781,30 @@ class HttpCacheTest(SimpleTestCase):
         import shutil
         shutil.rmtree(self.cache_directory)
 
+    #
+    # Tests helper functions, to avoid code redundancy
+    #
+    def set_mock_response(self, mock_requests, headers=None, status_code=200):
+        """Defines a mock response for the cases where http.requests is
+        patched via mock."""
+        set_mock_response(
+            mock_requests,
+            text=self.response_content.decode('utf-8'),
+            headers=headers,
+            status_code=status_code)
+
+    def get_resource_function_common_setup(self):
+        """
+        Common setup function for the get_resource function tests
+        to avoid code redundancy.
+        """
+        mock_cache = mock.create_autospec(HttpCache)
+        self.response_content = b"Some content"
+        mock_cache.get_content.return_value = self.response_content
+        url = 'http://some.url.com'
+
+        return url, mock_cache
+
     def test_parse_cache_control_header(self):
         """
         Tests the utility function for parsing a Cache-Control header into a
@@ -1128,6 +1150,69 @@ class HttpCacheTest(SimpleTestCase):
         # The function updated the cache
         mock_cache.update.assert_called_once_with(url)
 
+    def test_get_resource_content_with_only_arg_and_cache_expired(self):
+        """
+        Tests the :func:`distro_tracker.core.utils.http.get_resource_content`
+        utility function with the only_if_updated argument and an expired
+        cache.
+        """
+
+        url, mock_cache = self.get_resource_function_common_setup()
+
+        # Cache expired.
+        mock_cache.is_expired.return_value = True
+        mock_cache.update.return_value = (None, True)
+
+        # First call.
+        content = get_resource_content(url, mock_cache, only_if_updated=True)
+
+        # Has to work
+        self.assertEqual(content, self.response_content)
+
+        # The function updated the cache
+        mock_cache.update.assert_called_once_with(url)
+
+    def test_get_resource_content_with_only_arg_and_cache_not_expired(self):
+        """
+        Tests the :func:`distro_tracker.core.utils.http.get_resource_content`
+        utility function with the only_if_updated argument and a not expired
+        cache.
+        """
+
+        url, mock_cache = self.get_resource_function_common_setup()
+
+        # This time, the cache is not expired.
+        mock_cache.is_expired.return_value = False
+
+        content = get_resource_content(url, mock_cache, only_if_updated=True)
+
+        # No content, because not updated.
+        self.assertIsNone(content)
+
+        # The function did not update the cache
+        self.assertFalse(mock_cache.update.called)
+
+    def test_get_resource_content_with_only_arg_cache_expired_no_update(self):
+        """
+        Tests the :func:`distro_tracker.core.utils.http.get_resource_content`
+        utility function with the only_if_updated argument, an expired cache
+        but no real update done.
+        """
+
+        url, mock_cache = self.get_resource_function_common_setup()
+
+        # Cache expired.
+        mock_cache.is_expired.return_value = True
+        mock_cache.update.return_value = (None, False)
+
+        content = get_resource_content(url, mock_cache, only_if_updated=True)
+
+        # Nothing returned
+        self.assertIsNone(content)
+
+        # The function updated the cache
+        mock_cache.update.assert_called_once_with(url)
+
     @mock.patch('distro_tracker.core.utils.http.get_resource_content')
     def test_get_resource_text(self, mock_get_resource_content):
         """
diff --git a/distro_tracker/core/utils/http.py b/distro_tracker/core/utils/http.py
index 6756fee..37dd437 100644
--- a/distro_tracker/core/utils/http.py
+++ b/distro_tracker/core/utils/http.py
@@ -184,7 +184,10 @@ class HttpCache(object):
         return md5(url.encode('utf-8')).hexdigest()
 
 
-def get_resource_content(url, cache=None, compression="auto"):
+def get_resource_content(url,
+                         cache=None,
+                         compression="auto",
+                         only_if_updated=False):
     """
     A helper function which returns the content of the resource found at the
     given URL.
@@ -206,6 +209,9 @@ def get_resource_content(url, cache=None, compression="auto"):
         resource, and thus the compression method one should use to decompress
         it. If auto, then guess it from the url file extension.
     :type compression: str
+    :param only_if_updated: if set to `True` returns None when no update is
+        done. Otherwise, returns the content in any case.
+    :type only_if_updated: bool
 
     :returns: The bytes representation of the resource found at the given url
     :rtype: bytes
@@ -215,18 +221,27 @@ def get_resource_content(url, cache=None, compression="auto"):
         cache = HttpCache(cache_directory_path)
 
     try:
+        updated = False
         if cache.is_expired(url):
-            cache.update(url)
+            _, updated = cache.update(url)
+
+        if not updated and only_if_updated:
+            return
+
         return cache.get_content(url, compression=compression)
     except:
         pass
 
 
-def get_resource_text(url, cache=None, compression="auto", encoding="utf-8"):
+def get_resource_text(url,
+                      cache=None,
+                      compression="auto",
+                      only_if_updated=False,
+                      encoding="utf-8"):
     """
     Clone of :py:func:`get_resource_content` which transparently decodes
     the downloaded content into text. It supports the same parameters
-    and add the encoding parameter.
+    and adds the encoding parameter.
 
     :param encoding: Specifies an encoding to decode the resource content.
     :type encoding: str
@@ -235,7 +250,10 @@ def get_resource_text(url, cache=None, compression="auto", encoding="utf-8"):
     :rtype: str
     """
 
-    content = get_resource_content(url, cache=cache, compression=compression)
+    content = get_resource_content(url,
+                                   cache=cache,
+                                   only_if_updated=only_if_updated,
+                                   compression=compression)
 
     if content is not None:
         return content.decode(encoding)
-- 
2.11.0

From 100d8bb8d3030e8f7a0ef8c330a3942a96fc31e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pierre-Elliott=20B=C3=A9cue?= <be...@crans.org>
Date: Fri, 1 Dec 2017 14:44:22 +0100
Subject: [PATCH 2/4] Refactors the HttpCache tests using
 get_resource_function_common_setup

---
 distro_tracker/core/tests/tests_utils.py | 36 +++++++++++++++++++++++---------
 1 file changed, 26 insertions(+), 10 deletions(-)

diff --git a/distro_tracker/core/tests/tests_utils.py b/distro_tracker/core/tests/tests_utils.py
index 3f5d75d..480bf73 100644
--- a/distro_tracker/core/tests/tests_utils.py
+++ b/distro_tracker/core/tests/tests_utils.py
@@ -805,6 +805,9 @@ class HttpCacheTest(SimpleTestCase):
 
         return url, mock_cache
 
+    #
+    # Proper tests - Headers functionalities
+    #
     def test_parse_cache_control_header(self):
         """
         Tests the utility function for parsing a Cache-Control header into a
@@ -982,6 +985,9 @@ class HttpCacheTest(SimpleTestCase):
 
         self.assertTrue(cache.is_expired(url))
 
+    #
+    # Proper tests - Caching behaviour
+    #
     @mock.patch('distro_tracker.core.utils.http.requests')
     def test_cache_remove_url(self, mock_requests):
         """
@@ -998,6 +1004,9 @@ class HttpCacheTest(SimpleTestCase):
 
         self.assertFalse(url in cache)
 
+    #
+    # Proper tests - ETags
+    #
     @mock.patch('distro_tracker.core.utils.http.requests')
     def test_conditional_get_etag(self, mock_requests):
         """
@@ -1081,6 +1090,9 @@ class HttpCacheTest(SimpleTestCase):
             headers={'Cache-Control': 'no-cache'})
         self.assertTrue(updated)
 
+    #
+    # Proper tests - Compression utilities
+    #
     @mock.patch('distro_tracker.core.utils.http.requests')
     def test_get_content_detects_compression(self, mock_requests):
         """
@@ -1119,16 +1131,17 @@ class HttpCacheTest(SimpleTestCase):
         utility function when the resource is cached in the given cache
         instance.
         """
-        mock_cache = mock.create_autospec(HttpCache)
+
+        url, mock_cache = self.get_resource_function_common_setup()
+
+        # In this test, the is_expired call has to return False
         mock_cache.is_expired.return_value = False
-        expected_content = b"Some content"
-        mock_cache.get_content.return_value = expected_content
-        url = 'http://some.url.com'
 
         content = get_resource_content(url, cache=mock_cache)
 
         # The expected content is retrieved
-        self.assertEqual(content, expected_content)
+        self.assertEqual(content, self.response_content)
+
         # The function did not update the cache
         self.assertFalse(mock_cache.update.called)
 
@@ -1138,15 +1151,18 @@ class HttpCacheTest(SimpleTestCase):
         utility function when the resource is not cached in the given cache
         instance.
         """
-        mock_cache = mock.create_autospec(HttpCache)
+
+        url, mock_cache = self.get_resource_function_common_setup()
+
+        # In this test, the cache is expired, and hence update has
+        # to be called.
         mock_cache.is_expired.return_value = True
-        expected_content = b"Some content"
-        mock_cache.get_content.return_value = expected_content
-        url = 'http://some.url.com'
+        mock_cache.update.return_value = (None, True)
 
         content = get_resource_content(url, mock_cache)
 
-        self.assertEqual(content, expected_content)
+        self.assertEqual(content, self.response_content)
+
         # The function updated the cache
         mock_cache.update.assert_called_once_with(url)
 
-- 
2.11.0

From adb0a19dbedc95c16a743699090224e13565e4b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pierre-Elliott=20B=C3=A9cue?= <be...@crans.org>
Date: Fri, 1 Dec 2017 15:45:30 +0100
Subject: [PATCH 3/4] Fixes bug #883101 by using get_resource_text in
 UpdateWnppStatsTask

---
 distro_tracker/vendor/debian/tracker_tasks.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/distro_tracker/vendor/debian/tracker_tasks.py b/distro_tracker/vendor/debian/tracker_tasks.py
index 541db22..18a67fd 100644
--- a/distro_tracker/vendor/debian/tracker_tasks.py
+++ b/distro_tracker/vendor/debian/tracker_tasks.py
@@ -34,6 +34,7 @@ from distro_tracker.vendor.debian.models import PackageExcuses
 from distro_tracker.vendor.debian.models import UbuntuPackage
 from distro_tracker.core.utils.http import HttpCache
 from distro_tracker.core.utils.http import get_resource_content
+from distro_tracker.core.utils.http import get_resource_text
 from distro_tracker.core.utils.misc import get_data_checksum
 from distro_tracker.core.utils.packages import package_hashdir
 from .models import DebianContributor
@@ -1775,7 +1776,8 @@ class UpdateWnppStatsTask(BaseTask):
 
         :returns: A dict mapping package names to wnpp stats.
         """
-        content = self._get_wnpp_content()
+        url = 'https://qa.debian.org/data/bts/wnpp_rm'
+        content = get_resource_text(url, only_if_updated=True)
         if content is None:
             return
 
-- 
2.11.0

From fccd9c27bfbd2783fd98bf90b3d40d040fd61e31 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pierre-Elliott=20B=C3=A9cue?= <be...@crans.org>
Date: Fri, 1 Dec 2017 15:46:43 +0100
Subject: [PATCH 4/4] Gets rid of _get_wnpp_content function and update tests

---
 distro_tracker/vendor/debian/tests.py         | 36 ++++++++++++++++-----------
 distro_tracker/vendor/debian/tracker_tasks.py | 10 --------
 2 files changed, 22 insertions(+), 24 deletions(-)

diff --git a/distro_tracker/vendor/debian/tests.py b/distro_tracker/vendor/debian/tests.py
index a060234..8c9858d 100644
--- a/distro_tracker/vendor/debian/tests.py
+++ b/distro_tracker/vendor/debian/tests.py
@@ -3477,6 +3477,7 @@ class UbuntuPanelTests(TestCase):
         self.assertIn(ubuntu_version, response_content)
 
 
+@mock.patch('distro_tracker.core.utils.http.requests')
 class UpdateWnppStatsTaskTests(TestCase):
 
     """
@@ -3502,7 +3503,7 @@ class UpdateWnppStatsTaskTests(TestCase):
         :param content: A list of (package_name, issues) pairs. ``issues`` is
             a list of dicts describing the WNPP bugs the package has.
         """
-        self.task._get_wnpp_content.return_value = '\n'.join(
+        return '\n'.join(
             '{pkg}: {issues}'.format(
                 pkg=pkg,
                 issues='|'.join(
@@ -3515,19 +3516,20 @@ class UpdateWnppStatsTaskTests(TestCase):
     def run_task(self):
         self.task.execute()
 
-    def test_action_item_created(self):
+    def test_action_item_created(self, mock_requests):
         """
         Tests that an :class:`ActionItem
         <distro_tracker.core.models.ActionItem>` instance is created when the
         package has a WNPP bug.
         """
         wnpp_type, bug_id = 'O', 12345
-        self.set_wnpp_content([(
+        content = self.set_wnpp_content([(
             self.package.name, [{
                 'wnpp_type': wnpp_type,
                 'bug_id': bug_id,
             }]
         )])
+        set_mock_response(mock_requests, text=content)
 
         self.run_task()
 
@@ -3552,19 +3554,20 @@ class UpdateWnppStatsTaskTests(TestCase):
                ' been orphaned and needs a maintainer.</a>')
         self.assertEqual(dsc, item.short_description)
 
-    def test_action_item_created_unknown_type(self):
+    def test_action_item_created_unknown_type(self, mock_requests):
         """
         Tests that an :class:`ActionItem
         <distro_tracker.core.models.ActionItem>` instance is created when the
         package has a WNPP bug of an unknown type.
         """
         wnpp_type, bug_id = 'RFC', 12345
-        self.set_wnpp_content([(
+        content = self.set_wnpp_content([(
             self.package.name, [{
                 'wnpp_type': wnpp_type,
                 'bug_id': bug_id,
             }]
         )])
+        set_mock_response(mock_requests, text=content)
 
         self.run_task()
 
@@ -3589,7 +3592,7 @@ class UpdateWnppStatsTaskTests(TestCase):
                ' contains an entry for this package.</a>')
         self.assertEqual(dsc, item.short_description)
 
-    def test_action_item_updated(self):
+    def test_action_item_updated(self, mock_requests):
         """
         Tests that an existing :class:`ActionItem
         <distro_tracker.core.models.ActionItem>` instance is updated when there
@@ -3609,12 +3612,13 @@ class UpdateWnppStatsTaskTests(TestCase):
         old_timestamp = old_item.last_updated_timestamp
         # Set new WNPP info
         wnpp_type, bug_id = 'O', 12345
-        self.set_wnpp_content([(
+        content = self.set_wnpp_content([(
             self.package.name, [{
                 'wnpp_type': wnpp_type,
                 'bug_id': bug_id,
             }]
         )])
+        set_mock_response(mock_requests, text=content)
 
         self.run_task()
 
@@ -3630,7 +3634,7 @@ class UpdateWnppStatsTaskTests(TestCase):
         }
         self.assertEqual(expected_data, item.extra_data['wnpp_info'])
 
-    def test_action_item_not_updated(self):
+    def test_action_item_not_updated(self, mock_requests):
         """
         Tests that an existing :class:`ActionItem
         <distro_tracker.core.models.ActionItem>` instance is not updated when
@@ -3649,12 +3653,13 @@ class UpdateWnppStatsTaskTests(TestCase):
             })
         old_timestamp = old_item.last_updated_timestamp
         # Set "new" WNPP info
-        self.set_wnpp_content([(
+        content = self.set_wnpp_content([(
             self.package.name, [{
                 'wnpp_type': wnpp_type,
                 'bug_id': bug_id,
             }]
         )])
+        set_mock_response(mock_requests, text=content)
 
         self.run_task()
 
@@ -3664,7 +3669,7 @@ class UpdateWnppStatsTaskTests(TestCase):
         item = ActionItem.objects.all()[0]
         self.assertEqual(old_timestamp, item.last_updated_timestamp)
 
-    def test_action_item_removed(self):
+    def test_action_item_removed(self, mock_requests):
         """
         Tests that an existing :class:`ActionItem
         <distro_tracker.core.models.ActionItem>` instance is removed when there
@@ -3682,6 +3687,7 @@ class UpdateWnppStatsTaskTests(TestCase):
                 },
             })
         # Set "new" WNPP info
+        set_mock_response(mock_requests, text="")
 
         self.run_task()
 
@@ -3689,26 +3695,27 @@ class UpdateWnppStatsTaskTests(TestCase):
         # No more actino items
         self.assertEqual(0, ActionItem.objects.count())
 
-    def test_action_item_not_created(self):
+    def test_action_item_not_created(self, mock_requests):
         """
         Tests that an :class:`ActionItem
         <distro_tracker.core.models.ActionItem>` instance is not created for non
         existing packages.
         """
         wnpp_type, bug_id = 'O', 12345
-        self.set_wnpp_content([(
+        content = self.set_wnpp_content([(
             'no-exist', [{
                 'wnpp_type': wnpp_type,
                 'bug_id': bug_id,
             }]
         )])
+        set_mock_response(mock_requests, text=content)
 
         self.run_task()
 
         # No action items
         self.assertEqual(0, ActionItem.objects.count())
 
-    def test_action_item_multiple_packages(self):
+    def test_action_item_multiple_packages(self, mock_requests):
         """
         Tests that an :class:`ActionItem
         <distro_tracker.core.models.ActionItem>` instance is created for
@@ -3728,10 +3735,11 @@ class UpdateWnppStatsTaskTests(TestCase):
             name='other-package',
             source=True)
         packages = [other_package, self.package]
-        self.set_wnpp_content([
+        content = self.set_wnpp_content([
             (package.name, [wnpp_item])
             for package, wnpp_item in zip(packages, wnpp)
         ])
+        set_mock_response(mock_requests, text=content)
 
         self.run_task()
 
diff --git a/distro_tracker/vendor/debian/tracker_tasks.py b/distro_tracker/vendor/debian/tracker_tasks.py
index 18a67fd..d5976cd 100644
--- a/distro_tracker/vendor/debian/tracker_tasks.py
+++ b/distro_tracker/vendor/debian/tracker_tasks.py
@@ -1759,16 +1759,6 @@ class UpdateWnppStatsTask(BaseTask):
         if 'force_update' in parameters:
             self.force_update = parameters['force_update']
 
-    def _get_wnpp_content(self):
-        url = 'https://qa.debian.org/data/bts/wnpp_rm'
-        cache = HttpCache(settings.DISTRO_TRACKER_CACHE_DIRECTORY)
-        if not cache.is_expired(url):
-            return
-        response, updated = cache.update(url, force=self.force_update)
-        if not updated:
-            return
-        return response.content
-
     def get_wnpp_stats(self):
         """
         Retrieves and parses the wnpp stats for all packages. WNPP stats
-- 
2.11.0

Attachment: signature.asc
Description: PGP signature

Reply via email to