[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

[PATCH]: Improve excuses



Hi,

I have written a few patches to improve excuses in the following areas:

 * HTML excuses will include a line explicitly defining whether people
   need to act on the excuse or not.  Intended for any contributor with
   less than 20 hours of experience with Britney.  See [1] for the
   concrete messages
 * The YAML excuses will have a similar information in a single enum
   value.
 * The YAML excuses will also have a per-policy verdict.  This means
   that consumers have a consistent way of determine the result of a
   given policy.

Assuming no objections, I will merge this into master early next week.

~Niels

[1]
> +VERDICT2DESC = {
> +    PolicyVerdict.PASS:
> +        'OK: Will attempt migration (Any information below is purely informational)',
> +    PolicyVerdict.PASS_HINTED:
> +        'OK: Will attempt migration due to a hint (Any information below is purely informational)',
> +    PolicyVerdict.REJECTED_TEMPORARILY:
> +        'WAITING: Waiting for test results, another package or too young (no action required now - check later)',
> +    PolicyVerdict.REJECTED_WAITING_FOR_ANOTHER_ITEM:
> +        'WAITING: Waiting for another item to be ready to migrate (no action required now - check later)',
> +    PolicyVerdict.REJECTED_BLOCKED_BY_ANOTHER_ITEM:
> +        'BLOCKED: Cannot due to another item, which is blocked (please check which dependencies are stuck)',
> +    PolicyVerdict.REJECTED_NEEDS_APPROVAL:
> +        'BLOCKED: Needs an approval (either due to a freeze or due to the source suite)',
> +    PolicyVerdict.REJECTED_CANNOT_DETERMINE_IF_PERMANENT:
> +        'BLOCKED: Maybe temporary, maybe blocked but Britney is missing information (check below or the buildds)',
> +    PolicyVerdict.REJECTED_PERMANENTLY:
> +        'BLOCKED: Rejected/introduces a regression (please see below)'
> +}
From 79f16c45239fb3188ae231ccc86fabdecb151e4a Mon Sep 17 00:00:00 2001
From: Niels Thykier <niels@thykier.net>
Date: Mon, 19 Dec 2016 17:59:54 +0000
Subject: [PATCH 1/8] Remove redundant if-else - if branch is always true

Signed-off-by: Niels Thykier <niels@thykier.net>
---
 britney2/policies/policy.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/britney2/policies/policy.py b/britney2/policies/policy.py
index 7b100ad..5bbbf39 100644
--- a/britney2/policies/policy.py
+++ b/britney2/policies/policy.py
@@ -97,10 +97,8 @@ class BasePolicy(object):
         pass
 
     def apply_policy(self, general_policy_info, suite, source_name, source_data_tdist, source_data_srcdist, excuse):
-        if self.policy_id not in general_policy_info:
-            general_policy_info[self.policy_id] = pinfo = {}
-        else:
-            pinfo = general_policy_info[self.policy_id]
+        pinfo = {}
+        general_policy_info[self.policy_id] = pinfo
         return self.apply_policy_impl(pinfo, suite, source_name, source_data_tdist, source_data_srcdist, excuse)
 
     @abstractmethod
-- 
2.11.0

From 040c71a31c09d2d3215b60c7b4fcbf4a69f38025 Mon Sep 17 00:00:00 2001
From: Niels Thykier <niels@thykier.net>
Date: Mon, 19 Dec 2016 18:09:45 +0000
Subject: [PATCH 2/8] tests/test_policy.py: refactor test cases

Signed-off-by: Niels Thykier <niels@thykier.net>
---
 tests/test_policy.py | 135 ++++++++++++++++++++++-----------------------------
 1 file changed, 58 insertions(+), 77 deletions(-)

diff --git a/tests/test_policy.py b/tests/test_policy.py
index a51b557..dd468d4 100644
--- a/tests/test_policy.py
+++ b/tests/test_policy.py
@@ -51,83 +51,76 @@ def create_policy_objects(source_name, target_version, source_version):
     )
 
 
+def apply_policy(policy, expected_verdict, src_name, *, suite='unstable', source_version='1.0', target_version='2.0'):
+    src_t, src_u, excuse, policy_info = create_policy_objects(src_name, source_version, target_version)
+    verdict = policy.apply_policy(policy_info, suite, src_name, src_t, src_u, excuse)
+    assert verdict == expected_verdict
+    return policy_info[policy.policy_id]
+
+
 class TestRCBugsPolicy(unittest.TestCase):
 
     def test_no_bugs(self):
         src_name = 'has-no-bugs'
-        src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
         policy = initialize_policy('rc-bugs/basic', RCBugPolicy)
-        verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
-        assert verdict == PolicyVerdict.PASS
-        assert set(policy_info['rc-bugs']['unique-source-bugs']) == set()
-        assert set(policy_info['rc-bugs']['unique-target-bugs']) == set()
-        assert set(policy_info['rc-bugs']['shared-bugs']) == set()
+        bug_policy_info = apply_policy(policy, PolicyVerdict.PASS, src_name)
+        assert set(bug_policy_info['unique-source-bugs']) == set()
+        assert set(bug_policy_info['unique-target-bugs']) == set()
+        assert set(bug_policy_info['shared-bugs']) == set()
 
     def test_regression(self):
         src_name = 'regression'
-        src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
         policy = initialize_policy('rc-bugs/basic', RCBugPolicy)
-        verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
-        assert verdict == PolicyVerdict.REJECTED_PERMANENTLY
-        assert set(policy_info['rc-bugs']['unique-source-bugs']) == {'123458'}
-        assert set(policy_info['rc-bugs']['unique-target-bugs']) == set()
-        assert set(policy_info['rc-bugs']['shared-bugs']) == set()
+        bug_policy_info = apply_policy(policy, PolicyVerdict.REJECTED_PERMANENTLY, src_name)
+        assert set(bug_policy_info['unique-source-bugs']) == {'123458'}
+        assert set(bug_policy_info['unique-target-bugs']) == set()
+        assert set(bug_policy_info['shared-bugs']) == set()
 
     def test_regression_but_fixes_more_bugs(self):
         src_name = 'regression-but-fixes-more-bugs'
-        src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
         policy = initialize_policy('rc-bugs/basic', RCBugPolicy)
-        verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
-        assert verdict == PolicyVerdict.REJECTED_PERMANENTLY
-        assert set(policy_info['rc-bugs']['unique-source-bugs']) == {'100003'}
-        assert set(policy_info['rc-bugs']['unique-target-bugs']) == {'100001', '100002'}
-        assert set(policy_info['rc-bugs']['shared-bugs']) == {'100000'}
+        bug_policy_info = apply_policy(policy, PolicyVerdict.REJECTED_PERMANENTLY, src_name)
+        assert set(bug_policy_info['unique-source-bugs']) == {'100003'}
+        assert set(bug_policy_info['unique-target-bugs']) == {'100001', '100002'}
+        assert set(bug_policy_info['shared-bugs']) == {'100000'}
 
     def test_not_a_regression(self):
         src_name = 'not-a-regression'
-        src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
         policy = initialize_policy('rc-bugs/basic', RCBugPolicy)
-        verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
-        assert verdict == PolicyVerdict.PASS
-        assert set(policy_info['rc-bugs']['unique-source-bugs']) == set()
-        assert set(policy_info['rc-bugs']['unique-target-bugs']) == set()
-        assert set(policy_info['rc-bugs']['shared-bugs']) == {'123457'}
+        bug_policy_info = apply_policy(policy, PolicyVerdict.PASS, src_name)
+        assert set(bug_policy_info['unique-source-bugs']) == set()
+        assert set(bug_policy_info['unique-target-bugs']) == set()
+        assert set(bug_policy_info['shared-bugs']) == {'123457'}
 
     def test_improvement(self):
         src_name = 'fixes-bug'
-        src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
         policy = initialize_policy('rc-bugs/basic', RCBugPolicy)
-        verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
-        assert verdict == PolicyVerdict.PASS
-        assert set(policy_info['rc-bugs']['unique-source-bugs']) == set()
-        assert set(policy_info['rc-bugs']['unique-target-bugs']) == {'123456'}
-        assert set(policy_info['rc-bugs']['shared-bugs']) == set()
+        bug_policy_info = apply_policy(policy, PolicyVerdict.PASS, src_name)
+        assert set(bug_policy_info['unique-source-bugs']) == set()
+        assert set(bug_policy_info['unique-target-bugs']) == {'123456'}
+        assert set(bug_policy_info['shared-bugs']) == set()
 
     def test_regression_with_hint(self):
         src_name = 'regression'
         hints = ['ignore-rc-bugs 123458 regression/2.0']
-        src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
         policy = initialize_policy('rc-bugs/basic', RCBugPolicy, hints=hints)
-        verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
-        assert verdict == PolicyVerdict.PASS_HINTED
-        assert set(policy_info['rc-bugs']['ignored-bugs']['bugs']) == {'123458'}
-        assert policy_info['rc-bugs']['ignored-bugs']['issued-by'] == TEST_HINTER
-        assert set(policy_info['rc-bugs']['unique-source-bugs']) == set()
-        assert set(policy_info['rc-bugs']['unique-target-bugs']) == set()
-        assert set(policy_info['rc-bugs']['shared-bugs']) == set()
+        bug_policy_info = apply_policy(policy, PolicyVerdict.PASS_HINTED, src_name)
+        assert set(bug_policy_info['ignored-bugs']['bugs']) == {'123458'}
+        assert bug_policy_info['ignored-bugs']['issued-by'] == TEST_HINTER
+        assert set(bug_policy_info['unique-source-bugs']) == set()
+        assert set(bug_policy_info['unique-target-bugs']) == set()
+        assert set(bug_policy_info['shared-bugs']) == set()
 
     def test_regression_but_fixes_more_bugs_bad_hint(self):
         src_name = 'regression-but-fixes-more-bugs'
         hints = ['ignore-rc-bugs 100000 regression-but-fixes-more-bugs/2.0']
-        src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
         policy = initialize_policy('rc-bugs/basic', RCBugPolicy, hints=hints)
-        verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
-        assert verdict == PolicyVerdict.REJECTED_PERMANENTLY
-        assert set(policy_info['rc-bugs']['unique-source-bugs']) == {'100003'}
-        assert set(policy_info['rc-bugs']['unique-target-bugs']) == {'100001', '100002'}
-        assert set(policy_info['rc-bugs']['ignored-bugs']['bugs']) == {'100000'}
-        assert policy_info['rc-bugs']['ignored-bugs']['issued-by'] == TEST_HINTER
-        assert set(policy_info['rc-bugs']['shared-bugs']) == set()
+        bug_policy_info = apply_policy(policy, PolicyVerdict.REJECTED_PERMANENTLY, src_name)
+        assert set(bug_policy_info['unique-source-bugs']) == {'100003'}
+        assert set(bug_policy_info['unique-target-bugs']) == {'100001', '100002'}
+        assert set(bug_policy_info['ignored-bugs']['bugs']) == {'100000'}
+        assert bug_policy_info['ignored-bugs']['issued-by'] == TEST_HINTER
+        assert set(bug_policy_info['shared-bugs']) == set()
 
 
 class TestAgePolicy(unittest.TestCase):
@@ -146,13 +139,11 @@ class TestAgePolicy(unittest.TestCase):
 
         try:
             src_name = 'unlisted-source-package'
-            src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
             policy = initialize_policy('age/missing-age-file', AgePolicy, TestAgePolicy.DEFAULT_MIN_DAYS)
-            verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
+            age_policy_info = apply_policy(policy, PolicyVerdict.REJECTED_TEMPORARILY, src_name)
             assert os.path.exists(age_file)
-            assert verdict == PolicyVerdict.REJECTED_TEMPORARILY
-            assert policy_info['age']['age-requirement'] == TestAgePolicy.DEFAULT_MIN_DAYS[DEFAULT_URGENCY]
-            assert policy_info['age']['current-age'] == 0
+            assert age_policy_info['age-requirement'] == TestAgePolicy.DEFAULT_MIN_DAYS[DEFAULT_URGENCY]
+            assert age_policy_info['current-age'] == 0
         finally:
             if os.path.exists(age_file):
                 os.unlink(age_file)
@@ -162,50 +153,40 @@ class TestPiupartsPolicy(unittest.TestCase):
 
     def test_passes(self):
         src_name = 'pass'
-        src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
         policy = initialize_policy('piuparts/basic', PiupartsPolicy)
-        verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
-        assert verdict == PolicyVerdict.PASS
-        assert policy_info['piuparts']['test-results'] == 'pass'
-        assert policy_info['piuparts']['piuparts-test-url'] == 'https://piuparts.debian.org/sid/source/p/pass.html'
+        piu_policy_info = apply_policy(policy, PolicyVerdict.PASS, src_name)
+        assert piu_policy_info['test-results'] == 'pass'
+        assert piu_policy_info['piuparts-test-url'] == 'https://piuparts.debian.org/sid/source/p/pass.html'
 
     def test_regression(self):
         src_name = 'regression'
-        src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
         policy = initialize_policy('piuparts/basic', PiupartsPolicy)
-        verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
-        assert verdict == PolicyVerdict.REJECTED_PERMANENTLY
-        assert policy_info['piuparts']['test-results'] == 'regression'
-        assert policy_info['piuparts']['piuparts-test-url'] == 'https://piuparts.debian.org/sid/source/r/regression.html'
+        piu_policy_info = apply_policy(policy, PolicyVerdict.REJECTED_PERMANENTLY, src_name)
+        assert piu_policy_info['test-results'] == 'regression'
+        assert piu_policy_info['piuparts-test-url'] == 'https://piuparts.debian.org/sid/source/r/regression.html'
 
     def test_regression_hinted(self):
         src_name = 'regression'
         hints = ['ignore-piuparts regression/2.0']
-        src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
         policy = initialize_policy('piuparts/basic', PiupartsPolicy, hints=hints)
-        verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
-        assert verdict == PolicyVerdict.PASS_HINTED
-        assert policy_info['piuparts']['test-results'] == 'regression'
-        assert policy_info['piuparts']['piuparts-test-url'] == 'https://piuparts.debian.org/sid/source/r/regression.html'
-        assert policy_info['piuparts']['ignored-piuparts']['issued-by'] == TEST_HINTER
+        piu_policy_info = apply_policy(policy, PolicyVerdict.PASS_HINTED, src_name)
+        assert piu_policy_info['test-results'] == 'regression'
+        assert piu_policy_info['piuparts-test-url'] == 'https://piuparts.debian.org/sid/source/r/regression.html'
+        assert piu_policy_info['ignored-piuparts']['issued-by'] == TEST_HINTER
 
     def test_not_tested_yet(self):
         src_name = 'not-tested-yet'
-        src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
         policy = initialize_policy('piuparts/basic', PiupartsPolicy)
-        verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
-        assert verdict == PolicyVerdict.REJECTED_TEMPORARILY
-        assert policy_info['piuparts']['test-results'] == 'waiting-for-test-results'
-        assert policy_info['piuparts']['piuparts-test-url'] == 'https://piuparts.debian.org/sid/source/n/not-tested-yet.html'
+        piu_policy_info = apply_policy(policy, PolicyVerdict.REJECTED_TEMPORARILY, src_name)
+        assert piu_policy_info['test-results'] == 'waiting-for-test-results'
+        assert piu_policy_info['piuparts-test-url'] == 'https://piuparts.debian.org/sid/source/n/not-tested-yet.html'
 
     def test_failed_not_regression(self):
         src_name = 'failed-not-regression'
-        src_t, src_u, excuse, policy_info = create_policy_objects(src_name, '1.0', '2.0')
         policy = initialize_policy('piuparts/basic', PiupartsPolicy)
-        verdict = policy.apply_policy(policy_info, 'unstable', src_name, src_t, src_u, excuse)
-        assert verdict == PolicyVerdict.PASS
-        assert policy_info['piuparts']['test-results'] == 'failed'
-        assert policy_info['piuparts']['piuparts-test-url'] == 'https://piuparts.debian.org/sid/source/f/failed-not-regression.html'
+        piu_policy_info = apply_policy(policy, PolicyVerdict.PASS, src_name)
+        assert piu_policy_info['test-results'] == 'failed'
+        assert piu_policy_info['piuparts-test-url'] == 'https://piuparts.debian.org/sid/source/f/failed-not-regression.html'
 
 if __name__ == '__main__':
     unittest.main()
-- 
2.11.0

From 289fbcdbe445e5c39d6262d3d1f8c4fc9e49ab5f Mon Sep 17 00:00:00 2001
From: Niels Thykier <niels@thykier.net>
Date: Mon, 19 Dec 2016 18:27:38 +0000
Subject: [PATCH 3/8] Add a verdict field to all policies with the actual
 verdict

Signed-off-by: Niels Thykier <niels@thykier.net>
---
 britney2/policies/policy.py | 6 +++++-
 tests/test_policy.py        | 4 +++-
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/britney2/policies/policy.py b/britney2/policies/policy.py
index 5bbbf39..d5ad271 100644
--- a/britney2/policies/policy.py
+++ b/britney2/policies/policy.py
@@ -99,7 +99,11 @@ class BasePolicy(object):
     def apply_policy(self, general_policy_info, suite, source_name, source_data_tdist, source_data_srcdist, excuse):
         pinfo = {}
         general_policy_info[self.policy_id] = pinfo
-        return self.apply_policy_impl(pinfo, suite, source_name, source_data_tdist, source_data_srcdist, excuse)
+        verdict = self.apply_policy_impl(pinfo, suite, source_name, source_data_tdist, source_data_srcdist, excuse)
+        # The base policy provides this field, so the subclass should leave it blank
+        assert 'verdict' not in pinfo
+        pinfo['verdict'] = verdict.name
+        return verdict
 
     @abstractmethod
     def apply_policy_impl(self, policy_info, suite, source_name, source_data_tdist, source_data_srcdist, excuse):  # pragma: no cover
diff --git a/tests/test_policy.py b/tests/test_policy.py
index dd468d4..1e6bfa4 100644
--- a/tests/test_policy.py
+++ b/tests/test_policy.py
@@ -54,8 +54,10 @@ def create_policy_objects(source_name, target_version, source_version):
 def apply_policy(policy, expected_verdict, src_name, *, suite='unstable', source_version='1.0', target_version='2.0'):
     src_t, src_u, excuse, policy_info = create_policy_objects(src_name, source_version, target_version)
     verdict = policy.apply_policy(policy_info, suite, src_name, src_t, src_u, excuse)
+    pinfo = policy_info[policy.policy_id]
     assert verdict == expected_verdict
-    return policy_info[policy.policy_id]
+    assert pinfo['verdict'] == expected_verdict.name
+    return pinfo
 
 
 class TestRCBugsPolicy(unittest.TestCase):
-- 
2.11.0

From 8d8f1053b938482c9fe474f27a0f14be21147f49 Mon Sep 17 00:00:00 2001
From: Niels Thykier <niels@thykier.net>
Date: Wed, 8 Feb 2017 17:38:49 +0000
Subject: [PATCH 4/8] britney: Don't show "fake" packages in excuses

They are implementation details and should not appear in the excuses.

Signed-off-by: Niels Thykier <niels@thykier.net>
---
 britney.py | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/britney.py b/britney.py
index fa1740e..ceab846 100755
--- a/britney.py
+++ b/britney.py
@@ -1259,8 +1259,12 @@ class Britney(object):
         the object attribute excuses.
         """
 
-        # retrieve the source packages for testing (if available) and suite
         source_u = self.sources[suite][src]
+        if source_u.is_fakesrc:
+            # it is a fake package created to satisfy Britney implementation details; silently ignore it
+            return False
+
+        # retrieve the source packages for testing (if available) and suite
         if src in self.sources['testing']:
             source_t = self.sources['testing'][src]
             # if testing and unstable have the same version, then this is a candidate for binary-NMUs only
@@ -1291,11 +1295,6 @@ class Britney(object):
         # the starting point is that we will update the candidate
         excuse.is_valid = True
 
-        # check if the source package really exists or if it is a fake one
-        if source_u.is_fakesrc:
-            excuse.addhtml("%s source package doesn't exist" % (src))
-            excuse.is_valid = False
-
         # if there is a `remove' hint and the requested version is the same as the
         # version in testing, then stop here and return False
         for hint in self.hints.search('remove', package=src):
-- 
2.11.0

From fd80bdc571517693f69078ce6c9f9b4fa9543b1e Mon Sep 17 00:00:00 2001
From: Niels Thykier <niels@thykier.net>
Date: Wed, 8 Feb 2017 17:49:30 +0000
Subject: [PATCH 5/8] Only show the first (relevant) remove hint in excuses

In the unlike case that there are multiple removal hints, showing
the first valid hint should be sufficient.

Signed-off-by: Niels Thykier <niels@thykier.net>
---
 britney.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/britney.py b/britney.py
index ceab846..6a7262f 100755
--- a/britney.py
+++ b/britney.py
@@ -1304,6 +1304,7 @@ class Britney(object):
                 excuse.addhtml("Removal request by %s" % (hint.user))
                 excuse.addhtml("Trying to remove package, not update it")
                 excuse.is_valid = False
+                break
 
         # check if there is a `block' or `block-udeb' hint for this package, or a `block-all source' hint
         blocked = {}
-- 
2.11.0

From 03c78d8e085cade9ac7a9a35d839d4e5e3e3e34d Mon Sep 17 00:00:00 2001
From: Niels Thykier <niels@thykier.net>
Date: Wed, 8 Feb 2017 18:14:29 +0000
Subject: [PATCH 6/8] Replace dontinvalidate with forced

Signed-off-by: Niels Thykier <niels@thykier.net>
---
 britney.py         |  7 +++----
 britney2/excuse.py | 11 ++++-------
 britney2/utils.py  | 14 +++++++-------
 3 files changed, 14 insertions(+), 18 deletions(-)

diff --git a/britney.py b/britney.py
index 6a7262f..7d754b1 100755
--- a/britney.py
+++ b/britney.py
@@ -1498,11 +1498,10 @@ class Britney(object):
         # check if there is a `force' hint for this package, which allows it to go in even if it is not updateable
         forces = self.hints.search('force', package=src, version=source_u.version)
         if forces:
-            excuse.dontinvalidate = True
-            if not excuse.is_valid:
+            # force() updates the final verdict for us
+            changed_state = excuse.force()
+            if changed_state:
                 excuse.addhtml("Should ignore, but forced by %s" % (forces[0].user))
-                excuse.force()
-                excuse.is_valid = True
 
         self.excuses[excuse.name] = excuse
         return excuse.is_valid
diff --git a/britney2/excuse.py b/britney2/excuse.py
index d7afd18..706e95a 100644
--- a/britney2/excuse.py
+++ b/britney2/excuse.py
@@ -48,7 +48,6 @@ class Excuse(object):
         self.mindays = None
         self.section = None
         self._is_valid = False
-        self._dontinvalidate = False
         self.needs_approval = False
         self.hints = []
         self.forced = False
@@ -80,13 +79,7 @@ class Excuse(object):
     def is_valid(self, value):
         self._is_valid = value
 
-    @property
-    def dontinvalidate(self):
-        return self._dontinvalidate
 
-    @dontinvalidate.setter
-    def dontinvalidate(self, value):
-        self._dontinvalidate = value
 
     def set_vers(self, tver, uver):
         """Set the testing and unstable versions"""
@@ -127,6 +120,10 @@ class Excuse(object):
     def force(self):
         """Add force hint"""
         self.forced = True
+        if not self._is_valid:
+            self._is_valid = True
+            return True
+        return False
 
     def addhtml(self, note):
         """Add a note in HTML"""
diff --git a/britney2/utils.py b/britney2/utils.py
index c819362..3827aa0 100644
--- a/britney2/utils.py
+++ b/britney2/utils.py
@@ -802,14 +802,14 @@ def invalidate_excuses(excuses, valid, invalid):
             continue
         # loop on the reverse dependencies
         for x in revdeps[ename]:
-            # if the item is valid and it is marked as `dontinvalidate', skip the item
-            if x in valid and excuses[x].dontinvalidate:
-                continue
-
-            # otherwise, invalidate the dependency and mark as invalidated and
-            # remove the depending excuses
-            excuses[x].invalidate_dep(ename)
             if x in valid:
+                # if the item is valid and it is marked as `forced', skip the item
+                if excuses[x].forced:
+                    continue
+
+                # otherwise, invalidate the dependency and mark as invalidated and
+                # remove the depending excuses
+                excuses[x].invalidate_dep(ename)
                 p = valid.index(x)
                 invalid.append(valid.pop(p))
                 excuses[x].addhtml("Invalidated by dependency")
-- 
2.11.0

From a80d1c2d83b7cc41b5ad21f5f702ec0f6fe5fe44 Mon Sep 17 00:00:00 2001
From: Niels Thykier <niels@thykier.net>
Date: Wed, 8 Feb 2017 19:01:41 +0000
Subject: [PATCH 7/8] Aggregate all migration decisions and present it in
 excuses

With this change, Britney can now provide a very brief summary of the
migration via one single value (YAML) or line (HTML).  This solves two
issues:

 * It provides an aggregated version of the policy decision without
   having to loop over all policies (and even those would not give
   a full verdict on their own as not all rejections come from
   policies)

 * It enables a simple way to inform readers of the HTML excuses of
   whether a rejection is permanent or not.  This should hopefully
   make it easier for contributors to understand Britney and react
   more pro-actively.

Signed-off-by: Niels Thykier <niels@thykier.net>
---
 britney.py         | 30 ++++++++++++++++--------------
 britney2/excuse.py | 37 ++++++++++++++++++++++++++++++-------
 britney2/utils.py  |  4 +++-
 3 files changed, 49 insertions(+), 22 deletions(-)

diff --git a/britney.py b/britney.py
index 7d754b1..a8b4e31 100755
--- a/britney.py
+++ b/britney.py
@@ -1085,7 +1085,7 @@ class Britney(object):
             self.excuses[excuse.name] = excuse
             return False
 
-        excuse.is_valid = True
+        excuse.policy_verdict = PolicyVerdict.PASS
         self.excuses[excuse.name] = excuse
         return True
 
@@ -1237,7 +1237,7 @@ class Britney(object):
 
         # if there is nothing wrong and there is something worth doing, this is a valid candidate
         if not anywrongver and anyworthdoing:
-            excuse.is_valid = True
+            excuse.policy_verdict = PolicyVerdict.PASS
             self.excuses[excuse.name] = excuse
             return True
         # else if there is something worth doing (but something wrong, too) this package won't be considered
@@ -1293,7 +1293,7 @@ class Britney(object):
             return False
 
         # the starting point is that we will update the candidate
-        excuse.is_valid = True
+        excuse.policy_verdict = PolicyVerdict.PASS
 
         # if there is a `remove' hint and the requested version is the same as the
         # version in testing, then stop here and return False
@@ -1303,7 +1303,7 @@ class Britney(object):
                 excuse.add_hint(hint)
                 excuse.addhtml("Removal request by %s" % (hint.user))
                 excuse.addhtml("Trying to remove package, not update it")
-                excuse.is_valid = False
+                excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY
                 break
 
         # check if there is a `block' or `block-udeb' hint for this package, or a `block-all source' hint
@@ -1358,7 +1358,7 @@ class Britney(object):
                 else:
                     excuse.addhtml("NEEDS APPROVAL BY RM")
                     excuse.addreason("block")
-                excuse.is_valid = False
+                excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY
 
         # at this point, we check the status of the builds on all the supported architectures
         # to catch the out-of-date ones
@@ -1404,7 +1404,7 @@ class Britney(object):
                         # we would not need to be forgiving at
                         # all. However, due to how arch:all packages
                         # are handled, we do run into occasionally.
-                        excuse.is_valid = False
+                        excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY
 
             # if there are out-of-date packages, warn about them in the excuse and set excuse.is_valid
             # to False to block the update; if the architecture where the package is out-of-date is
@@ -1434,9 +1434,9 @@ class Britney(object):
                         if self.options.ignore_cruft:
                             text = text + " (but ignoring cruft, so nevermind)"
                         else:
-                            excuse.is_valid = False
+                            excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY
                     else:
-                        excuse.is_valid = False
+                        excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY
                         excuse.missing_build_on_arch(arch)
 
                 excuse.addhtml(text)
@@ -1445,21 +1445,20 @@ class Britney(object):
         if not source_u.binaries:
             excuse.addhtml("%s has no binaries on any arch" % src)
             excuse.addreason("no-binaries")
-            excuse.is_valid = False
+            excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY
 
         # if the suite is unstable, then we have to check the urgency and the minimum days of
         # permanence in unstable before updating testing; if the source package is too young,
         # the check fails and we set is_valid to False to block the update; consider
         # the age-days hint, if specified for the package
+        policy_verdict = excuse.policy_verdict
         policy_info = excuse.policy_info
-        policy_verdict = PolicyVerdict.PASS
         for policy in self.policies:
             if suite in policy.applicable_suites:
                 v = policy.apply_policy(policy_info, suite, src, source_t, source_u, excuse)
                 if v.value > policy_verdict.value:
                     policy_verdict = v
-                    if policy_verdict.is_rejected:
-                        excuse.is_valid = False
+        excuse.policy_verdict = policy_verdict
 
         if suite in ('pu', 'tpu') and source_t:
             # o-o-d(ish) checks for (t-)p-u
@@ -1490,7 +1489,7 @@ class Britney(object):
                     text = text + " (but %s isn't keeping up, so never mind)" % (arch)
                     excuse.missing_build_on_ood_arch(arch)
                 else:
-                    excuse.is_valid = False
+                    excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY
                     excuse.missing_build_on_arch(arch)
 
                 excuse.addhtml(text)
@@ -1585,7 +1584,10 @@ class Britney(object):
             excuse.addhtml("Removal request by %s" % (hint.user))
             excuse.addhtml("Package is broken, will try to remove")
             excuse.add_hint(hint)
-            excuse.is_valid = True
+            # Using "PASS" here as "Created by a hint" != "accepted due to hint".  In a future
+            # where there might be policy checks on removals, it would make sense to distinguish
+            # those two states.  Not sure that future will ever be.
+            excuse.policy_verdict = PolicyVerdict.PASS
             excuses[excuse.name] = excuse
 
         # extract the not considered packages, which are in the excuses but not in upgrade_me
diff --git a/britney2/excuse.py b/britney2/excuse.py
index 706e95a..e854277 100644
--- a/britney2/excuse.py
+++ b/britney2/excuse.py
@@ -17,6 +17,7 @@
 from collections import defaultdict
 import re
 
+from britney2.policies.policy import PolicyVerdict
 
 class Excuse(object):
     """Excuse class
@@ -51,6 +52,7 @@ class Excuse(object):
         self.needs_approval = False
         self.hints = []
         self.forced = False
+        self._policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY
 
         self.invalid_deps = []
         self.deps = {}
@@ -73,13 +75,19 @@ class Excuse(object):
 
     @property
     def is_valid(self):
-        return self._is_valid
-
-    @is_valid.setter
-    def is_valid(self, value):
-        self._is_valid = value
+        return False if self._policy_verdict.is_rejected else True
 
+    @property
+    def policy_verdict(self):
+        return self._policy_verdict
 
+    @policy_verdict.setter
+    def policy_verdict(self, value):
+        if value.is_rejected and self.forced:
+            # By virtue of being forced, the item was hinted to
+            # undo the rejection
+            value = PolicyVerdict.PASS_HINTED
+        self._policy_verdict = value
 
     def set_vers(self, tver, uver):
         """Set the testing and unstable versions"""
@@ -120,8 +128,8 @@ class Excuse(object):
     def force(self):
         """Add force hint"""
         self.forced = True
-        if not self._is_valid:
-            self._is_valid = True
+        if self._policy_verdict.is_rejected:
+            self._policy_verdict = PolicyVerdict.PASS_HINTED
             return True
         return False
 
@@ -144,10 +152,24 @@ class Excuse(object):
     def add_hint(self, hint):
         self.hints.append(hint)
 
+    def _format_verdict_summary(self):
+        verdict = self._policy_verdict
+        if not verdict.is_rejected:
+            msg = 'OK: Will attempt migration'
+            if verdict == PolicyVerdict.PASS_HINTED:
+                msg = 'OK: Will attempt migration due to a hint'
+            msg += " (Any information below is purely informational)"
+            return msg
+        if verdict == PolicyVerdict.REJECTED_PERMANENTLY:
+            msg = "BLOCKED: Will not migrate (Please review if it introduces a regression or needs approval/unblock)"
+            return msg
+        return "TEMP-BLOCKED: Waiting for test results, another package or too young (no action required at this time)"
+
     def html(self):
         """Render the excuse in HTML"""
         res = "<a id=\"%s\" name=\"%s\">%s</a> (%s to %s)\n<ul>\n" % \
             (self.name, self.name, self.name, self.ver[0], self.ver[1])
+        res += "<li>Migration status: %s" % self._format_verdict_summary()
         if self.maint:
             res = res + "<li>Maintainer: %s\n" % (self.maint)
         if self.section and self.section.find("/") > -1:
@@ -210,6 +232,7 @@ class Excuse(object):
         excusedata["excuses"] = self._text()
         excusedata["item-name"] = self.name
         excusedata["source"] = source
+        excusedata["migration-policy-verdict"] = self._policy_verdict
         excusedata["old-version"] = self.ver[0]
         excusedata["new-version"] = self.ver[1]
         if self.maint:
diff --git a/britney2/utils.py b/britney2/utils.py
index 3827aa0..b518dd9 100644
--- a/britney2/utils.py
+++ b/britney2/utils.py
@@ -39,6 +39,7 @@ from britney2.consts import (VERSION, PROVIDES, DEPENDS, CONFLICTS,
                              SOURCE, MAINTAINER, MULTIARCH,
                              ESSENTIAL)
 from britney2.migrationitem import MigrationItem, UnversionnedMigrationItem
+from britney2.policies.policy import PolicyVerdict
 
 
 def ifilter_except(container, iterable=None):
@@ -814,7 +815,8 @@ def invalidate_excuses(excuses, valid, invalid):
                 invalid.append(valid.pop(p))
                 excuses[x].addhtml("Invalidated by dependency")
                 excuses[x].addreason("depends")
-                excuses[x].is_valid = False
+                if excuses[x].policy_verdict.value < PolicyVerdict.REJECTED_TEMPORARILY.value:
+                    excuses[x].policy_verdict = PolicyVerdict.REJECTED_TEMPORARILY
 
 
 def compile_nuninst(binaries_t, inst_tester, architectures, nobreakall_arches):
-- 
2.11.0

From 10333ee45da6664ace7c6a5a9a5d68024d1ec07f Mon Sep 17 00:00:00 2001
From: Niels Thykier <niels@thykier.net>
Date: Wed, 8 Feb 2017 20:02:16 +0000
Subject: [PATCH 8/8] Add more verdicts to provide more detailed excuses

Signed-off-by: Niels Thykier <niels@thykier.net>
---
 britney.py                  |  6 +++---
 britney2/excuse.py          | 33 +++++++++++++++++++++++----------
 britney2/policies/policy.py | 28 +++++++++++++++++++++++++++-
 britney2/utils.py           |  9 +++++++--
 4 files changed, 60 insertions(+), 16 deletions(-)

diff --git a/britney.py b/britney.py
index a8b4e31..bc0120f 100755
--- a/britney.py
+++ b/britney.py
@@ -1358,7 +1358,7 @@ class Britney(object):
                 else:
                     excuse.addhtml("NEEDS APPROVAL BY RM")
                     excuse.addreason("block")
-                excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY
+                excuse.policy_verdict = PolicyVerdict.REJECTED_NEEDS_APPROVAL
 
         # at this point, we check the status of the builds on all the supported architectures
         # to catch the out-of-date ones
@@ -1436,7 +1436,7 @@ class Britney(object):
                         else:
                             excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY
                     else:
-                        excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY
+                        excuse.policy_verdict = PolicyVerdict.REJECTED_CANNOT_DETERMINE_IF_PERMANENT
                         excuse.missing_build_on_arch(arch)
 
                 excuse.addhtml(text)
@@ -1489,7 +1489,7 @@ class Britney(object):
                     text = text + " (but %s isn't keeping up, so never mind)" % (arch)
                     excuse.missing_build_on_ood_arch(arch)
                 else:
-                    excuse.policy_verdict = PolicyVerdict.REJECTED_PERMANENTLY
+                    excuse.policy_verdict = PolicyVerdict.REJECTED_CANNOT_DETERMINE_IF_PERMANENT
                     excuse.missing_build_on_arch(arch)
 
                 excuse.addhtml(text)
diff --git a/britney2/excuse.py b/britney2/excuse.py
index e854277..a1ce27a 100644
--- a/britney2/excuse.py
+++ b/britney2/excuse.py
@@ -19,6 +19,26 @@ import re
 
 from britney2.policies.policy import PolicyVerdict
 
+VERDICT2DESC = {
+    PolicyVerdict.PASS:
+        'OK: Will attempt migration (Any information below is purely informational)',
+    PolicyVerdict.PASS_HINTED:
+        'OK: Will attempt migration due to a hint (Any information below is purely informational)',
+    PolicyVerdict.REJECTED_TEMPORARILY:
+        'WAITING: Waiting for test results, another package or too young (no action required now - check later)',
+    PolicyVerdict.REJECTED_WAITING_FOR_ANOTHER_ITEM:
+        'WAITING: Waiting for another item to be ready to migrate (no action required now - check later)',
+    PolicyVerdict.REJECTED_BLOCKED_BY_ANOTHER_ITEM:
+        'BLOCKED: Cannot due to another item, which is blocked (please check which dependencies are stuck)',
+    PolicyVerdict.REJECTED_NEEDS_APPROVAL:
+        'BLOCKED: Needs an approval (either due to a freeze or due to the source suite)',
+    PolicyVerdict.REJECTED_CANNOT_DETERMINE_IF_PERMANENT:
+        'BLOCKED: Maybe temporary, maybe blocked but Britney is missing information (check below or the buildds)',
+    PolicyVerdict.REJECTED_PERMANENTLY:
+        'BLOCKED: Rejected/introduces a regression (please see below)'
+}
+
+
 class Excuse(object):
     """Excuse class
     
@@ -154,16 +174,9 @@ class Excuse(object):
 
     def _format_verdict_summary(self):
         verdict = self._policy_verdict
-        if not verdict.is_rejected:
-            msg = 'OK: Will attempt migration'
-            if verdict == PolicyVerdict.PASS_HINTED:
-                msg = 'OK: Will attempt migration due to a hint'
-            msg += " (Any information below is purely informational)"
-            return msg
-        if verdict == PolicyVerdict.REJECTED_PERMANENTLY:
-            msg = "BLOCKED: Will not migrate (Please review if it introduces a regression or needs approval/unblock)"
-            return msg
-        return "TEMP-BLOCKED: Waiting for test results, another package or too young (no action required at this time)"
+        if verdict in VERDICT2DESC:
+            return VERDICT2DESC[verdict]
+        return "UNKNOWN: Missing description for {0} - Please file a bug against Britney".format(verdict.name)
 
     def html(self):
         """Render the excuse in HTML"""
diff --git a/britney2/policies/policy.py b/britney2/policies/policy.py
index d5ad271..6e2c6e1 100644
--- a/britney2/policies/policy.py
+++ b/britney2/policies/policy.py
@@ -27,15 +27,41 @@ class PolicyVerdict(Enum):
     """
     REJECTED_TEMPORARILY = 3
     """
+    The migration item is temporarily unable to migrate due to another item.  The other item is temporarily blocked.
+    """
+    REJECTED_WAITING_FOR_ANOTHER_ITEM = 4
+    """
+    The migration item is permanently unable to migrate due to another item.  The other item is permanently blocked.
+    """
+    REJECTED_BLOCKED_BY_ANOTHER_ITEM = 5
+    """
+    The migration item needs approval to migrate
+    """
+    REJECTED_NEEDS_APPROVAL = 6
+    """
+    The migration item is blocked, but there is not enough information to determine
+    if this issue is permanent or temporary
+    """
+    REJECTED_CANNOT_DETERMINE_IF_PERMANENT = 7
+    """
     The migration item did not pass the policy and the failure is believed
     to be uncorrectable (i.e. a hint or a new version is needed)
     """
-    REJECTED_PERMANENTLY = 4
+    REJECTED_PERMANENTLY = 8
 
     @property
     def is_rejected(self):
         return True if self.name.startswith('REJECTED') else False
 
+    def is_blocked(self):
+        """Whether the item (probably) needs a fix or manual assistance to migrate"""
+        return self in {
+            PolicyVerdict.REJECTED_BLOCKED_BY_ANOTHER_ITEM,
+            PolicyVerdict.REJECTED_NEEDS_APPROVAL,
+            PolicyVerdict.REJECTED_CANNOT_DETERMINE_IF_PERMANENT, # Assuming the worst
+            PolicyVerdict.REJECTED_PERMANENTLY,
+        }
+
 
 class BasePolicy(object):
 
diff --git a/britney2/utils.py b/britney2/utils.py
index b518dd9..99f83e9 100644
--- a/britney2/utils.py
+++ b/britney2/utils.py
@@ -801,6 +801,11 @@ def invalidate_excuses(excuses, valid, invalid):
         # if the dependency can be satisfied by a testing-proposed-updates excuse, skip the item
         if (ename + "_tpu") in valid:
             continue
+
+        rdep_verdict = PolicyVerdict.REJECTED_WAITING_FOR_ANOTHER_ITEM
+        if excuses[ename].policy_verdict.is_blocked:
+            rdep_verdict = PolicyVerdict.REJECTED_BLOCKED_BY_ANOTHER_ITEM
+
         # loop on the reverse dependencies
         for x in revdeps[ename]:
             if x in valid:
@@ -815,8 +820,8 @@ def invalidate_excuses(excuses, valid, invalid):
                 invalid.append(valid.pop(p))
                 excuses[x].addhtml("Invalidated by dependency")
                 excuses[x].addreason("depends")
-                if excuses[x].policy_verdict.value < PolicyVerdict.REJECTED_TEMPORARILY.value:
-                    excuses[x].policy_verdict = PolicyVerdict.REJECTED_TEMPORARILY
+                if excuses[x].policy_verdict.value < rdep_verdict.value:
+                    excuses[x].policy_verdict = rdep_verdict
 
 
 def compile_nuninst(binaries_t, inst_tester, architectures, nobreakall_arches):
-- 
2.11.0

Attachment: signature.asc
Description: OpenPGP digital signature


Reply to: