From 8c3b9e58932bbfc6219fb0d355d977efccfe2349 Mon Sep 17 00:00:00 2001
From: Iain Lane <iain@orangesquash.org.uk>
Date: Thu, 21 May 2020 17:24:55 +0100
Subject: [PATCH] LinuxPolicy: Make linux* wait on corresponding -meta package

We want to treat linux-$flavor and linux-meta-$flavor as one set in
britney which goes in together or not at all. We never want to promote
linux-$flavor without the accompanying linux-meta-$flavor.

Add a new LinuxPolicy which runs after most of the other policies, which
invalidates linux-foo if linux-meta-foo is invalid.
---
 britney.py                  |  2 +
 britney2/policies/policy.py | 92 +++++++++++++++++++++++++++++++++++++
 tests/test_autopkgtest.py   | 53 +++++++++++++++++++++
 3 files changed, 147 insertions(+)

diff --git a/britney.py b/britney.py
index 1720ce3..8709e33 100755
--- a/britney.py
+++ b/britney.py
@@ -213,6 +213,7 @@ from britney2.policies.policy import (AgePolicy,
                                       BuiltUsingPolicy,
                                       BuiltOnBuilddPolicy,
                                       ImplicitDependencyPolicy,
+                                      LinuxPolicy,
                                       LPBlockBugPolicy,
                                       )
 from britney2.policies.autopkgtest import AutopkgtestPolicy
@@ -523,6 +524,7 @@ class Britney(object):
         if getattr(self.options, 'check_buildd', 'no') == 'yes':
             self._policy_engine.add_policy(BuiltOnBuilddPolicy(self.options, self.suite_info))
         self._policy_engine.add_policy(LPBlockBugPolicy(self.options, self.suite_info))
+        self._policy_engine.add_policy(LinuxPolicy(self.options, self.suite_info))
 
     @property
     def hints(self):
diff --git a/britney2/policies/policy.py b/britney2/policies/policy.py
index b6c19b6..1fbf7c7 100644
--- a/britney2/policies/policy.py
+++ b/britney2/policies/policy.py
@@ -1056,6 +1056,7 @@ class BuildDependsPolicy(BasePolicy):
             binaries_t_a = binaries_t[arch]
             provides_t_a = provides_t[arch]
             arch_results[arch] = BuildDepResult.OK
+
             # for every dependency block (formed as conjunction of disjunction)
             for block_txt in deps.split(','):
                 block = parse_src_depends(block_txt, False, arch)
@@ -1794,3 +1795,94 @@ class LPBlockBugPolicy(BasePolicy):
         excuse.addreason('block')
 
         return PolicyVerdict.REJECTED_PERMANENTLY
+
+class LinuxPolicy(BasePolicy):
+    """Make linux-* wait on linux-meta-*"""
+
+    def __init__(self, options, suite_info):
+        super().__init__('linux', options, suite_info, {SuiteClass.PRIMARY_SOURCE_SUITE})
+        self.metas = {}
+        self.invalidate = defaultdict(list)
+
+    def initialise(self, britney):
+        super().initialise(britney)
+        self.britney = britney
+
+    def meta_name(self, linux_source):
+        if linux_source.startswith('linux-signed'):
+            meta = linux_source.replace('linux-signed', 'linux-meta')
+        else:
+            meta = linux_source.replace('linux', 'linux-meta')
+
+        return meta
+
+    def find_meta_excuse(self, linux_source):
+        assert linux_source.startswith('linux')
+
+        meta = self.meta_name(linux_source)
+
+        try:
+            return self.metas[meta]
+        except KeyError:
+            return None
+
+    def apply_src_policy_impl(self, linux_info, item, source_data_tdist, source_data_srcdist, excuse):
+        source_name = item.package
+
+        if not source_name.startswith('linux'):
+            return PolicyVerdict.NOT_APPLICABLE
+
+        self.logger.info("linux policy: considering %s", source_name)
+
+        # If we're currently migrating a linux-meta package, look to see if we
+        # need to reject any of its corresponding linux packages.
+        if source_name.startswith('linux-meta'):
+            self.metas[source_name] = excuse
+
+            # We're currently in the middle of running the policies for
+            # linux-meta, so we need to look at the *tentative* verdict. If
+            # it's rejected, then we will find any linux-* that we marked to
+            # come back to (see below), and handle them now.
+            if excuse.tentative_policy_verdict.is_rejected:
+                self.logger.info("%s is rejected, checking if we now need to reject anything else", source_name)
+                linux_excuses = self.invalidate[source_name]
+                if linux_excuses:
+                    for linux_excuse in linux_excuses:
+                        self.logger.info("%s is invalid, so invalidating %s too",
+                                         excuse.name,
+                                         linux_excuse.name)
+                        linux_excuse.policy_verdict = PolicyVerdict.REJECTED_WAITING_FOR_ANOTHER_ITEM
+                        linux_excuse.addreason("linux-meta-not-ready")
+                        linux_excuse.add_verdict_info(linux_excuse.policy_verdict,
+                                                      "Cannot migrate because %s is invalid" % excuse.name)
+                else:
+                    self.logger.info("Nothing recorded to reject yet")
+            else:
+                self.logger.info("%s is a candidate, so we will not invalidate anything else", source_name)
+            return PolicyVerdict.PASS
+
+        # Or we're migrating linux-*, so look to see if we already know about
+        # linux-meta-*, which will happen if we processed that excuse first.
+        linux_meta_excuse = self.find_meta_excuse(source_name)
+
+        if not linux_meta_excuse:
+            # We don't know about linux-meta, but if it's in the source suite
+            # we will later, so remember to come back to this item then.
+            meta_name = self.meta_name(source_name)
+            self.logger.info("No excuse for %s yet, will come back to %s when visiting it later.",
+                             meta_name,
+                             excuse.name)
+            self.invalidate[meta_name].append(excuse)
+            return PolicyVerdict.PASS
+
+        # We do know about linux-meta, so invalidate linux if meta is also
+        # invalid, otherwise leave it alone.
+        if not linux_meta_excuse.is_valid:
+            self.logger.info("Invalidating %s because %s is invalid", excuse.name, linux_meta_excuse.name)
+            res = PolicyVerdict.REJECTED_WAITING_FOR_ANOTHER_ITEM
+            excuse.add_verdict_info(res, "Cannot migrate because %s is invalid" % linux_meta_excuse.name)
+            return res
+        else:
+            self.logger.info("Not invalidating %s because %s is valid", excuse.name, linux_meta_excuse.name)
+
+        return PolicyVerdict.PASS
diff --git a/tests/test_autopkgtest.py b/tests/test_autopkgtest.py
index 4b0c757..90b7b85 100644
--- a/tests/test_autopkgtest.py
+++ b/tests/test_autopkgtest.py
@@ -2072,6 +2072,59 @@ class AT(TestAutopkgtestBase):
         # everything from -meta
         self.assertEqual(exc['linux']['policy_info']['autopkgtest'], {'verdict': 'PASS'})
 
+    def test_kernel_waits_on_meta(self):
+        '''linux waits on linux-meta'''
+
+        self.data.add('dkms', False, {})
+        self.data.add('dkms', True, {})
+        self.data.add('fancy-dkms', False, {'Source': 'fancy', 'Depends': 'dkms (>= 1)'}, testsuite='autopkgtest-pkg-dkms')
+        self.data.add('fancy-dkms', True, {'Source': 'fancy', 'Depends': 'dkms (>= 1)'}, testsuite='autopkgtest-pkg-dkms')
+        self.data.add('linux-image-generic', False, {'Version': '0.1', 'Source': 'linux-meta', 'Depends': 'linux-image-1'})
+        self.data.add('linux-image-1', False, {'Source': 'linux'}, testsuite='autopkgtest')
+        self.data.add('linux-firmware', False, {'Source': 'linux-firmware'}, testsuite='autopkgtest')
+
+        self.swift.set_results({'autopkgtest-testing': {
+            'testing/i386/f/fancy/20150101_090000@': (0, 'fancy 0.5', tr('passedbefore/1')),
+            'testing/i386/l/linux/20150101_100000@': (0, 'linux 2', tr('linux-meta/0.2')),
+            'testing/amd64/l/linux/20150101_100000@': (0, 'linux 2', tr('linux-meta/0.2')),
+            'testing/i386/l/linux-firmware/20150101_100000@': (0, 'linux-firmware 2', tr('linux-firmware/2')),
+            'testing/amd64/l/linux-firmware/20150101_100000@': (0, 'linux-firmware 2', tr('linux-firmware/2')),
+        }})
+
+        self.run_it(
+            [('linux-image-generic', {'Version': '0.2', 'Source': 'linux-meta', 'Depends': 'linux-image-2'}, None),
+             ('linux-image-2', {'Version': '2', 'Source': 'linux'}, 'autopkgtest'),
+             ('linux-firmware', {'Version': '2', 'Source': 'linux-firmware'}, 'autopkgtest'),
+            ],
+            {'linux-meta': (False, {'fancy': {'amd64': 'RUNNING-ALWAYSFAIL', 'i386': 'RUNNING'},
+                                    'linux/2': {'amd64': 'PASS', 'i386': 'PASS'}
+                                   }),
+             # no tests, but should wait on linux-meta
+             'linux': (False, {}),
+             # this one does not have a -meta, so don't wait
+             'linux-firmware': (True, {'linux-firmware/2': {'amd64': 'PASS', 'i386': 'PASS'}}),
+            },
+            {'linux': [('excuses', 'Cannot migrate because linux-meta/0.2 is invalid'),
+                       ('dependencies', {'migrate-after': ['linux-meta']})]
+            }
+        )
+
+        # now linux-meta is ready to go
+        self.swift.set_results({'autopkgtest-testing': {
+            'testing/i386/f/fancy/20150101_100000@': (0, 'fancy 1', tr('linux-meta/0.2')),
+            'testing/amd64/f/fancy/20150101_100000@': (0, 'fancy 1', tr('linux-meta/0.2')),
+        }})
+        self.run_it(
+            [],
+            {'linux-meta': (True, {'fancy/1': {'amd64': 'PASS', 'i386': 'PASS'},
+                                   'linux/2': {'amd64': 'PASS', 'i386': 'PASS'}}),
+             'linux': (True, {}),
+             'linux-firmware': (True, {'linux-firmware/2': {'amd64': 'PASS', 'i386': 'PASS'}}),
+            },
+            {'linux': [('dependencies', {'migrate-after': ['linux-meta']})]
+            }
+        )
+
     ################################################################
     # Tests for special-cased packages
     ################################################################