Merge ~robru/britney/+git/britney2-ubuntu:source_ppa into ~ubuntu-release/britney/+git/britney2-ubuntu:master

Proposed by Robert Bruce Park on 2016-10-08
Status: Merged
Merge reported by: Martin Pitt
Merged at revision: not available
Proposed branch: ~robru/britney/+git/britney2-ubuntu:source_ppa
Merge into: ~ubuntu-release/britney/+git/britney2-ubuntu:master
Diff against target: 373 lines (+303/-2)
5 files modified
britney.py (+4/-2)
policies/sourceppa.py (+115/-0)
tests/data/sourceppa.json (+7/-0)
tests/test_autopkgtest.py (+43/-0)
tests/test_sourceppa.py (+134/-0)
Reviewer Review Type Date Requested Status
Martin Pitt (community) 2016-10-08 Approve on 2016-11-17
Robert Bruce Park (community) Approve on 2016-11-15
Steve Langasek 2016-11-08 Pending
Review via email: mp+307995@code.launchpad.net

Description of the Change

Teach Britney to reject packages if other packages copied from same PPA are also rejected.

To post a comment you must log in.
Robert Bruce Park (robru) wrote :

Ok, this is conceptually complete, I just need to write the tests for it ;-)

Martin Pitt (pitti) wrote :

The general direction looks good indeed, thanks for working on this!

review: Needs Fixing
Martin Pitt (pitti) wrote :

... oh, and you of course need to actually instantiate the new policy in britney.py.

Robert Bruce Park (robru) wrote :

(some discussion inline; everything I didn't reply to I agree with & will fix)

Martin Pitt (pitti) wrote :

Likewise, some remaining remarks; I didn't follow up to parts which I consider resolved/agree with.

I just rebased our britney2 to current Debian master (this also includes your speedup patch); please rebase your branch against current britney2-ubuntu.

Robert Bruce Park (robru) wrote :

Ok, I beleive I've addressed all the issues you've identified, will begin writing those tests shortly. let me know if I've overlooked anything, thanks.

Martin Pitt (pitti) wrote :

A few minor niggles left, but this looks great now! (still leaving as needs fixing for the tests).

Robert Bruce Park (robru) wrote :

Ok I've written some tests, please re-review.

One thought occurred to me: When I had to rebase the work, there was a merge conflict in the imports in policy.py (apparently they changed upstream). That was why I had them grouped and separated from the other imports, to hopefully avoid merge conflicts there, but it happened anyway. I wonder if it might make more sense to move the SourcePPAPolicy definition into a new file? This would avoid potential future merge conflicts if upstream touches the imports in policy.py

review: Approve
Martin Pitt (pitti) wrote :

I like the new test cases in general, to test the finer details of the new policy. But this mocks Britney, Options, and even Excuse, so they don't prove that the overall plumbing of this is correct. Can you please add an integration test case to test_autopkgtest.py which checks this end-to-end?

Thanks!

> move the SourcePPAPolicy definition into a new file?

If you wish, sure.

Robert Bruce Park (robru) wrote :

Ok, I'll move sourceppapolicy into a new file. That'll decrease the likelihood of merge conflicts on future upstream rebases, which'll make your life easier.

Can you point me at an example of an existing test that integrates britney as you describe? I'm not sure what that would look like really.

Martin Pitt (pitti) wrote :

All of tests/test_autopkgtest.py are of this "blackbox integration test" kind. The central helper is do_test() which sets up a few "unstable" packages in the sandbox archive, then calls britney on it, and runs the given assertions on the resulting excuses.yaml.

For that you need to be able to mock the Launchpad calls: I suggest setting $LAUNCHPAD_URL to file://<temp dir> so that you can provide defined mock replies.

618a76b... by Martin Pitt on 2016-10-20

Move build checks before running policies

For future policies such as running autopkgtests it is important to know
whether a package has built, so that expensive actions such as "trigger an
autopkgtest" are not done too early/in vain.

This requires dropping the "age != 0" check for adding the out-of-date-ness to
the Excuse, as the policies now run later. But this check only applied to an
infinitesimal age, and even with age == 0 it is still a valid excuse that there
are missing binaries.

1d68151... by Martin Pitt on 2016-10-20

Pass excuse to BasePolicy.apply_policy()

This allows tests to check whether there are any missing builds or old
binaries, so that expensive actions such as "trigger an autopkgtest" are not
done too early/in vain.

e1b2027... by Martin Pitt on 2016-10-20

Move updating of excuses into policies

It is unwieldy to have one half of output data generation in the policy but not
the other half of updating the excuse. Now that apply_policy() gets the excuse
object as argument we can move everything there.

212c91e... by Niels Thykier on 2016-10-22

britney: Remove redundant if and obsolete comment

The RC bugs part is now handled by the RCBugs policy.

Signed-off-by: Niels Thykier <email address hidden>

c341f37... by Martin Pitt on 2016-10-24

Store Testsuite-Triggers: list in sources

Compute an inverse trigger → sources map, as that's usually the direction that
britney is interested in.

fd3e644... by Martin Pitt on 2016-10-26

Drop backwards compatible autopkgtest excuse YAML structure

Consumers got ported over to "autopkgtest" being under "policy_info" now.

Robert Bruce Park (robru) wrote :

Ok, rebase was easy enough, just working on tests still.

8aea1a8... by Martin Pitt on 2016-10-27

Save policy state before upgrade tester

We might not (want to) run the upgrade tester for some use cases, but we still
need to save the policy state.

Robert Bruce Park (robru) wrote :

Ok Martin, we are really close here! Please re-review. There's just one small problem and I need your help getting past this. When apply_policy_impl gets called with the excuse object, the excuse object given to it has not actually had its is_valid property set yet, and it defaults to false. This means that, with the existing policy architecture, it is only possible to detect other invalid excuses, it's not possible to detect if my own excuse is already invalid.

So, eg, this case is working:

source ppa has 3 packages, and the second one is rejected, so when the third one is called it notices and rejects all excuses from that ppa

But this case isn't working:

source ppa has 2 packages, and the second one is rejected, so at neither invocation of apply_policy_impl am I able to detect that anything is rejected and thus reject the whole ppa.

To make matters worse, excuses class defaults to is_valid=False, which means when I inspect excuses object it's *always* false, because the logic for deciding what to set it to doesn't happen until after all the policies run. So I need a way to reach back up into britney.py and say "hey what were the results of the previous policy runs" and that just isn't in there. :-/

Robert Bruce Park (robru) wrote :

(if you run the tests currently, there's one failure, because of this issue)

Martin Pitt (pitti) wrote :

> When apply_policy_impl gets called with the excuse object, the excuse object given to it has not actually had its is_valid property set yet

Right, that only gets set at the end of should_upgrade_src() if everything is okay.

> So I need a way to reach back up into britney.py and say "hey what were the results of the previous policy runs" and that just isn't in there.

This wouldn't actually be hard to fix -- the policy verdicts are collected in should_upgrade_src() here:

        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 > policy_verdict:
                    policy_verdict = v

        if policy_verdict in [PolicyVerdict.REJECTED_PERMANENTLY, PolicyVerdict.REJECTED_TEMPORARILY]:
            update_candidate = False

so in theory we could just pass the current policy_verdict to apply_policy(). However, this isn't sufficient: there are reasons why an excuse is invalid beyond policies, like "not being built everywhere" or "uninstallable". These are not (yet?) implemented as Policies, but directly in britney.py.

So I have three ideas:

 (1) Move the "if policy_verdict ..." (last line in the code above) into the loop, and pass "update_candidate" to policy.apply_policy(). This is a bit intrusive, so if you go this way, please discuss that with Niels first (nthykier on OFTC, or email him).

 (2) Drop the local "update_candidate" variable and use excuse.is_valid for it, which would IMHO be simpler and less intrusive. But likewise, please clear with Niels.

 (3) In your apply_policy_imply(), replace the broken excuse.is_valid with "excuse.missing_builds or excuse.reason". This doesn't require changes to britney.py itself, but it is assuming that every policy actually adds a reason. We do that in AutopkgtestPolicy and LPBlockBugPolicy; the Debian policies (e. g. AgePolicy) don't do that; we also don't use them in Ubuntu, but making this consistent by adding excuse.addreason() would remove a potential future trap (in case we ever actually do use AgePolicy, e. g. to mechanize the SRU policy).

If you do (1) or (2), please do this as a separate commit.

Robert Bruce Park (robru) wrote :

Yeah i don't like 1 or 2 because they are invasive changes to the policy framework that likely won't accepted upstream because there's no benefit to them. And i don't like 3 because it's incomplete.

What about option 4, where instead of implementing this as a policy, i simply bolt it on at the very end, after all excuses are generated? That would simplify the implementation as well, because currently there's a bit of overhead where i have to simultaneously check other packages to invalidate myself, then also once invalidated i have to go back and make sure everything else is invalidated as well, in a two-step process that's a bit clunky.

If instead i could simply iterate over a fully-formed britney.excuses, i could go back to my initial implementation that worked with a single loop a lot more cleanly.

Thoughts?

Martin Pitt (pitti) wrote :

I would actually go with (2). It makes the code simpler and more useful. Niels accepted several things which are not immediately useful for Debian, but could become so in the future, including the policy reorg patches recently. I'd start with that, show it to him, and carry the patch in our tree until it lands, it shouldn't be that big.

1029a4e... by Martin Pitt on 2016-11-01

Use Excuse.is_valid when iterating through checks and Policies

This gives Policies the opportunity to see if a previous check
(build/installability) or earlier policies already invalidated the update. This
allows writing policies that work on groups of packages, or skipping expensive
checks (such as triggering autopkgtests while the package is not built or
installable yet).

Martin Pitt (pitti) wrote :

I implemented (2) in https://git.launchpad.net/~ubuntu-release/britney/+git/britney2-ubuntu/commit/?id=1029a4eec as this isn't that hard to do; I'll send it to Niels for commenting.

Robert Bruce Park (robru) wrote :

With or without that patch, i still prefer this approach, as it vastly simplifies my algorithm to have access to ALL excuse objects fully formed, just having the excuse object for current source isn't good enough and makes extra work to wait for the other excuse objects in the same ppa. Can you please take a look at this one? It's much cleaner.

https://code.launchpad.net/~robru/britney/+git/britney2-ubuntu/+merge/309621

Martin Pitt (pitti) wrote :

How can it be so massively simpler? You still need to iterate through all excuses, and a policy already implicitly gives you that. You even split discover_ppas() and reject_sources(), so I don't see any net simplification actually. It also immediately follows the Policy loop, so there's nothing in between that does additional computation.

With the is_valid patch the approach in this MP should actually work, doesn't it?

I don't like the "bolt-on" MP because it forces any subsequent addition to also not be a policy; for example, at some point we want to do email notifications. These would all need to be bolted on too. Also, the bolt-on MP is very confusing: It looks like a policy by location and class naming, but isn't. It's just a single magical constructor.

Robert Bruce Park (robru) wrote :

On Nov 3, 2016 12:44 AM, "Martin Pitt" <email address hidden> wrote:
> How can it be so massively simpler? You still need to iterate through all
excuses, and a policy already implicitly gives you that. You even split
discover_ppas() and reject_sources(), so I don't see any net simplification
actually. It also immediately follows the Policy loop, so there's nothing
in between that does additional computation.

The key difference is that apply_policy_impl doesn't give us access to the
complete picture, so the policy implementation must do an awkward two-step
check for any reason to invalidate the current package and then also reach
back and invalidate other packages as well.

>
> With the is_valid patch the approach in this MP should actually work,
doesn't it?

The main problem with this approach is that the excuses dict is not fully
generated when the policy is applied, which makes it awkward because every
application of the policy needs to check all other packages, and then
disable all other packages, after each package is loaded.

The bolted-on approach lets us just scan the completed excuses once,
whereas this approach means we have to check the previously loaded excuses
for each excuse loaded. Just look at apply_policy_impl, it's huge compared
to the bolt on approach.

Also I'm not convinced that your is_valid patch really helped that much at
all. I need to know if the excuse is invalid for any reason, policies or
otherwise. I'll need to review if that's indeed the case, for some reason i
was thinking it was only capturing some invalidations and not all.

>
> I don't like the "bolt-on" MP because it forces any subsequent addition
to also not be a policy; for example, at some point we want to do email
notifications. These would all need to be bolted on too.

Well the source ppa grouping needs to the last one anyway since any other
reason to reject something needs to come first. If there's a feature to
send emails, is that really a policy anyway?

> Also, the bolt-on MP is very confusing: It looks like a policy by
location and class naming, but isn't. It's just a single magical
constructor.

Yes yes of course, this is just bike shedding, my main concern is that the
implementation is a lot simpler if i can access the complete excuses dict
rather than apply_policy_impl which shows only an awkward slice of the
information we need to reject an entire source ppa.

Think of it this way: apply_policy_impl is called within a loop, once for
each package, but contains two loops: one to check other packages for
validity and one to then reject all other packages from same ppa.

The bolt on approach contains just two loops: one to discover the source
ppa and then one to do the rejections. It looks so much cleaner!

Martin Pitt (pitti) wrote :

Robert Bruce Park [2016-11-03 8:06 -0000]:
> The key difference is that apply_policy_impl doesn't give us access to the
> complete picture, so the policy implementation must do an awkward two-step
> check for any reason to invalidate the current package and then also reach
> back and invalidate other packages as well.

Right, you can only look backwards, not forwards (in Excuses order).
But which way around you process it doesn't make the process
significantly more complicated -- you collect the "friends" mapping as
you go, and as soon as you find any invalid source from a PPA you
invalidate all friends as well. You have to do all these for a full
excuses map as well.

> awkward because every application of the policy needs to check all
> other packages, and then disable all other packages, after each
> package is loaded.

Not all other packages, just the ones from the same PPA, which you
already cache in the "friends" dict. And you can maintain a single
bool per PPA whether it's (so far) good or already invalid. So for
every individual Excuse check (per policy invocation) there should not
be any looping at all?

> Also I'm not convinced that your is_valid patch really helped that much at
> all. I need to know if the excuse is invalid for any reason, policies or
> otherwise. I'll need to review if that's indeed the case, for some reason i
> was thinking it was only capturing some invalidations and not all.

No, it accurately reflects (and in fact *is*) the "is-candidate".

> Well the source ppa grouping needs to the last one anyway since any other
> reason to reject something needs to come first. If there's a feature to
> send emails, is that really a policy anyway?

We can do it as a policy. But e. g. the upcoming boot speed
testing/gating will also run after the PPA policy, as it's really
expensive.

> Think of it this way: apply_policy_impl is called within a loop, once for
> each package, but contains two loops: one to check other packages for
> validity and one to then reject all other packages from same ppa.

It's not necessary to loop for the former, and you have to do the
latter in either case.

Robert Bruce Park (robru) wrote :

Ok I've rebased this on latest master and fixed up the tests. This approach is fundamentally broken, however. In britney.py:

        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 > policy_verdict:
                    policy_verdict = v

        if policy_verdict in [PolicyVerdict.REJECTED_PERMANENTLY, PolicyVerdict.REJECTED_TEMPORARILY]:
            excuse.is_valid = False

This means that excuse.is_valid does not get set until *after* all policies have run, which means that at the time my policy runs, inspecting excuse.is_valid can only find the default value, which is false, which means my policy has no way of distinguishing between an excuse that has been rejected by a policy and an excuse that has not been rejected by a policy (both cases are False).

I really would strongly prefer to inspect britney.excuses after it is fully formed than try to write a policy that reaches outside the scope of a policy and inspects other excuses before the excuse-building is even complete. It's thus far proving to be quite error prone.

Martin Pitt (pitti) wrote :

Oh right, that "if policy_verdict" needs to be indented one level.

Robert Bruce Park (robru) wrote :

Hmm, i suppose that would work. I overlooked the "excuse.is_valid = True" near the beginning of the method and just saw the excuse class definition where it defaults to false for some reason. Will you submit a patch for that then or should i?

Martin Pitt (pitti) wrote :

Robert Bruce Park [2016-11-14 21:33 -0000]:
> Hmm, i suppose that would work. I overlooked the "excuse.is_valid =
> True" near the beginning of the method and just saw the excuse class
> definition where it defaults to false for some reason. Will you
> submit a patch for that then or should i?

Please apply it locally in your branch for now, so that when we
forward this upstream we know exactly what we actually need.

24622f6... by Robert Bruce Park on 2016-11-15

Invalidate excuse after each policy run.

Robert Bruce Park (robru) wrote :

Ok this is done, integration test written, tests passing, please accept.

review: Approve
Martin Pitt (pitti) wrote :

I have some stylistic/pyflakes updates and another test for applying this policy to an unbuilt package (to make sure that this works as well); this also adds a HTML explanation of the blocker: http://paste.ubuntu.com/23490188/

However, there is still some weird race condition here:

$ while tests/test_autopkgtest.py -v T.test_sourceppa; do true; done
[...]
======================================================================
FAIL: test_sourceppa (__main__.T)
Packages from same source PPA should be grouped.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests/test_autopkgtest.py", line 2059, in test_sourceppa
    self.assertEqual(exc['red']['policy_info']['source-ppa'], {'red': 'team/ubuntu/ppa', 'green': 'team/ubuntu/ppa'})
AssertionError: {} != {'red': 'team/ubuntu/ppa', 'green': 'team/ubuntu/ppa'}
- {}
+ {'green': 'team/ubuntu/ppa', 'red': 'team/ubuntu/ppa'}

This apparently depends on some python dict randomness, depending on whether red or green get examined first. The friend invalidation also happens multiple times, causing the reasons to be added multiple times (which looks confusing). This also needs fixing still.

I'm still looking into this, but wanted to provide some feedback of the current state. In general this looks really nice now, thanks!

BTW: Not sure if you are aware, but for debugging such issues, this is useful:

 SHOW_OUTPUT=1 SHOW_EXCUSES=1 SHOW_HTML=1 tests/test_autopkgtest.py T.test_sourceppa_policy

Martin Pitt (pitti) wrote :

I adjusted the preliminary britney policy support fix for upstream, sent it to Niels, and rebased our Ubuntu changes on top:

  https://git.launchpad.net/~ubuntu-release/britney/+git/britney2-ubuntu/commit/?id=4bde6d4

The "main" commit here is

  https://git.launchpad.net/~ubuntu-release/britney/+git/britney2-ubuntu/commit/?id=47f5f0

which differs from your's just in some nitpicks and two pyflakes fixes.

I did a followup commit

  https://git.launchpad.net/~ubuntu-release/britney/+git/britney2-ubuntu/commit/?id=6f16ddc

with a few fixes, particularly that unstable policy_info bit from above.

I'll land this now, let's see what happens! Thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/britney.py b/britney.py
2index 936512b..19fc048 100755
3--- a/britney.py
4+++ b/britney.py
5@@ -215,6 +215,7 @@ from britney_util import (old_libraries_format, undo_changes,
6 )
7 from policies.policy import AgePolicy, RCBugPolicy, LPBlockBugPolicy, PolicyVerdict
8 from policies.autopkgtest import AutopkgtestPolicy
9+from policies.sourceppa import SourcePPAPolicy
10
11 # Check the "check_field_name" reflection before removing an import here.
12 from consts import (SOURCE, SOURCEVER, ARCHITECTURE, CONFLICTS, DEPENDS,
13@@ -545,6 +546,7 @@ class Britney(object):
14 self.policies.append(LPBlockBugPolicy(self.options))
15 if getattr(self.options, 'adt_enable') == 'yes':
16 self.policies.append(AutopkgtestPolicy(self.options))
17+ self.policies.append(SourcePPAPolicy(self.options))
18
19 for policy in self.policies:
20 policy.register_hints(self._hint_parser)
21@@ -1758,8 +1760,8 @@ class Britney(object):
22 if v > policy_verdict:
23 policy_verdict = v
24
25- if policy_verdict in [PolicyVerdict.REJECTED_PERMANENTLY, PolicyVerdict.REJECTED_TEMPORARILY]:
26- excuse.is_valid = False
27+ if policy_verdict in [PolicyVerdict.REJECTED_PERMANENTLY, PolicyVerdict.REJECTED_TEMPORARILY]:
28+ excuse.is_valid = False
29
30 if suite in ('pu', 'tpu') and source_t:
31 # o-o-d(ish) checks for (t-)p-u
32diff --git a/policies/sourceppa.py b/policies/sourceppa.py
33new file mode 100644
34index 0000000..90a7ca8
35--- /dev/null
36+++ b/policies/sourceppa.py
37@@ -0,0 +1,115 @@
38+import os
39+import json
40+import urllib.request
41+import urllib.parse
42+
43+from collections import defaultdict
44+
45+from policies.policy import BasePolicy, PolicyVerdict
46+
47+
48+LAUNCHPAD_URL = 'https://api.launchpad.net/1.0/'
49+PRIMARY = LAUNCHPAD_URL + 'ubuntu/+archive/primary'
50+
51+
52+def query_lp_rest_api(obj, query):
53+ """Do a Launchpad REST request
54+
55+ Request <LAUNCHPAD_URL><obj>?<query>.
56+
57+ Returns dict of parsed json result from launchpad.
58+ Raises HTTPError, ValueError, or ConnectionError based on different
59+ transient failures connecting to launchpad.
60+ """
61+ url = '%s%s?%s' % (LAUNCHPAD_URL, obj, urllib.parse.urlencode(query))
62+ with urllib.request.urlopen(url, timeout=10) as req:
63+ code = req.getcode()
64+ if 200 <= code < 300:
65+ return json.loads(req.read().decode('UTF-8'))
66+ raise ConnectionError('Failed to reach launchpad, HTTP %s' % code)
67+
68+
69+class SourcePPAPolicy(BasePolicy):
70+ """Migrate packages copied from same source PPA together
71+
72+ This policy will query launchpad to determine what source PPA packages
73+ were copied from, and ensure that all packages from the same PPA migrate
74+ together.
75+ """
76+
77+ def __init__(self, options):
78+ super().__init__('source-ppa', options, {'unstable'})
79+ self.filename = os.path.join(options.unstable, 'SourcePPA')
80+ # Dict of dicts; maps pkg name -> pkg version -> source PPA URL
81+ self.source_ppas_by_pkg = defaultdict(dict)
82+ # Dict of sets; maps source PPA URL -> set of source names
83+ self.pkgs_by_source_ppa = defaultdict(set)
84+ self.britney = None
85+ # self.cache contains self.source_ppas_by_pkg from previous run
86+ self.cache = {}
87+
88+ def lp_get_source_ppa(self, pkg, version):
89+ """Ask LP what source PPA pkg was copied from"""
90+ cached = self.cache.get(pkg, {}).get(version)
91+ if cached is not None:
92+ return cached
93+
94+ data = query_lp_rest_api('%s/%s' % (self.options.distribution, self.options.series), {
95+ 'ws.op': 'getPackageUploads',
96+ 'archive': PRIMARY,
97+ 'pocket': 'Proposed',
98+ 'name': pkg,
99+ 'version': version,
100+ 'exact_match': 'true',
101+ })
102+ try:
103+ return data['entries'][0]['copy_source_archive_link'] or ''
104+ # IndexError means no packages in -proposed matched this name/version,
105+ # which is expected to happen when bileto runs britney.
106+ except IndexError:
107+ return ''
108+
109+ def initialise(self, britney):
110+ """Load and discover all source ppa data"""
111+ super().initialise(britney)
112+ self.britney = britney
113+
114+ try:
115+ with open(self.filename, encoding='utf-8') as data:
116+ self.cache = json.load(data)
117+ self.log("Loaded cached source ppa data from %s" % self.filename)
118+ except FileNotFoundError:
119+ pass
120+
121+ def apply_policy_impl(self, sourceppa_info, suite, source_name, source_data_tdist, source_data_srcdist, excuse):
122+ """Reject package if any other package copied from same PPA is invalid"""
123+ accept = excuse.is_valid
124+ britney_excuses = self.britney.excuses
125+ version = source_data_srcdist.version
126+ sourceppa = self.lp_get_source_ppa(source_name, version)
127+ self.source_ppas_by_pkg[source_name][version] = sourceppa
128+ if sourceppa:
129+ # Check for other packages that might invalidate this one
130+ for friend in self.pkgs_by_source_ppa[sourceppa]:
131+ if not britney_excuses[friend].is_valid:
132+ accept = False
133+ self.pkgs_by_source_ppa[sourceppa].add(source_name)
134+ if not accept:
135+ # Invalidate all packages in this source ppa
136+ shortppa = sourceppa.replace(LAUNCHPAD_URL, '')
137+ for friend in self.pkgs_by_source_ppa[sourceppa]:
138+ friend_exc = britney_excuses.get(friend, excuse)
139+ friend_exc.is_valid = False
140+ friend_exc.addreason('source-ppa')
141+ self.log("Blocking %s because %s from %s" % (friend, source_name, shortppa))
142+ sourceppa_info[friend] = shortppa
143+ return PolicyVerdict.REJECTED_PERMANENTLY
144+ return PolicyVerdict.PASS
145+
146+ def save_state(self, britney):
147+ """Write source ppa data to disk"""
148+ tmp = self.filename + '.tmp'
149+ with open(tmp, 'w', encoding='utf-8') as data:
150+ json.dump(self.source_ppas_by_pkg, data)
151+ os.rename(tmp, self.filename)
152+ self.log("Wrote source ppa data to %s" % self.filename)
153diff --git a/tests/data/sourceppa.json b/tests/data/sourceppa.json
154new file mode 100644
155index 0000000..70e5538
156--- /dev/null
157+++ b/tests/data/sourceppa.json
158@@ -0,0 +1,7 @@
159+{
160+ "pal": {"2.0": "https://api.launchpad.net/1.0/team/ubuntu/ppa"},
161+ "buddy": {"2.0": "https://api.launchpad.net/1.0/team/ubuntu/ppa"},
162+ "friend": {"2.0": "https://api.launchpad.net/1.0/team/ubuntu/ppa"},
163+ "noppa": {"2.0": ""},
164+ "unused": {"2.0": ""}
165+}
166diff --git a/tests/test_autopkgtest.py b/tests/test_autopkgtest.py
167index 53c91c5..b8afaae 100755
168--- a/tests/test_autopkgtest.py
169+++ b/tests/test_autopkgtest.py
170@@ -72,6 +72,19 @@ class T(TestBase):
171 'Conflicts': 'green'},
172 testsuite='specialtest')
173
174+ # Set up sourceppa cache for testing
175+ self.sourceppa_cache = {
176+ 'gcc-5': {'2': ''},
177+ 'gcc-snapshot': {'2': ''},
178+ 'green': {'2': '', '1.1': '', '3': ''},
179+ 'lightgreen': {'2': '', '1.1~beta': '', '3': ''},
180+ 'linux-meta-64only': {'1': ''},
181+ 'linux-meta-lts-grumpy': {'1': ''},
182+ 'linux-meta': {'0.2': '', '1': '', '2': ''},
183+ 'linux': {'2': ''},
184+ 'newgreen': {'2': ''},
185+ }
186+
187 # create mock Swift server (but don't start it yet, as tests first need
188 # to poke in results)
189 self.swift = mock_swift.AutoPkgTestSwiftServer(port=18085)
190@@ -96,6 +109,16 @@ class T(TestBase):
191 '''
192 for (pkg, fields, testsuite) in unstable_add:
193 self.data.add(pkg, True, fields, True, testsuite)
194+ self.sourceppa_cache.setdefault(pkg, {})
195+ if fields['Version'] not in self.sourceppa_cache[pkg]:
196+ self.sourceppa_cache[pkg][fields['Version']] = ''
197+
198+ # Set up sourceppa cache for testing
199+ sourceppa_path = os.path.join(self.data.dirs[True], 'SourcePPA')
200+ with open(sourceppa_path, 'w', encoding='utf-8') as sourceppa:
201+ sourceppa.write(json.dumps(self.sourceppa_cache))
202+ # with open(sourceppa_path, encoding='utf-8') as data:
203+ # print(data.read())
204
205 self.swift.start()
206 (excuses_yaml, excuses_html, out) = self.run_britney()
207@@ -2015,6 +2038,26 @@ class T(TestBase):
208 with open(shared_path) as f:
209 self.assertEqual(orig_contents, f.read())
210
211+ ################################################################
212+ # Tests for source ppa grouping
213+ ################################################################
214+
215+ def test_sourceppa(self):
216+ '''Packages from same source PPA should be grouped.'''
217+ self.sourceppa_cache['green'] = {'2': 'team/ubuntu/ppa'}
218+ self.sourceppa_cache['red'] = {'2': 'team/ubuntu/ppa'}
219+ with open(os.path.join(self.data.path, 'data/series-proposed/Blocks'), 'w') as f:
220+ f.write('green 12345 1471505000\ndarkgreen 98765 1471500000\n')
221+
222+ exc = self.do_test(
223+ [('green', {'Version': '2'}, 'autopkgtest'),
224+ ('red', {'Version': '2'}, 'autopkgtest')],
225+ {'green': (False, {'green': {'i386': 'RUNNING-ALWAYSFAIL', 'amd64': 'RUNNING-ALWAYSFAIL'}})},
226+ {'green': [('is-candidate', False)],
227+ 'red': [('is-candidate', False)]}
228+ )[1]
229+ self.assertEqual(exc['red']['policy_info']['source-ppa'], {'red': 'team/ubuntu/ppa', 'green': 'team/ubuntu/ppa'})
230+
231
232 if __name__ == '__main__':
233 unittest.main()
234diff --git a/tests/test_sourceppa.py b/tests/test_sourceppa.py
235new file mode 100755
236index 0000000..477a187
237--- /dev/null
238+++ b/tests/test_sourceppa.py
239@@ -0,0 +1,134 @@
240+#!/usr/bin/python3
241+# (C) 2016 Canonical Ltd.
242+#
243+# This program is free software; you can redistribute it and/or modify
244+# it under the terms of the GNU General Public License as published by
245+# the Free Software Foundation; either version 2 of the License, or
246+# (at your option) any later version.
247+
248+import os
249+import sys
250+import json
251+import tempfile
252+import unittest
253+from unittest.mock import patch
254+
255+PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
256+sys.path.insert(0, PROJECT_DIR)
257+
258+from policies.policy import PolicyVerdict
259+from policies.sourceppa import LAUNCHPAD_URL, SourcePPAPolicy
260+
261+
262+CACHE_FILE = os.path.join(PROJECT_DIR, 'tests', 'data', 'sourceppa.json')
263+
264+
265+class FakeOptions:
266+ distribution = 'testbuntu'
267+ series = 'zazzy'
268+ unstable = '/tmp'
269+ verbose = False
270+
271+
272+class FakeExcuse:
273+ ver = ('1.0', '2.0')
274+ is_valid = True
275+
276+ def addreason(self, reason):
277+ """Ignore reasons."""
278+
279+
280+class FakeBritney:
281+ def __init__(self):
282+ self.excuses = dict(
283+ pal=FakeExcuse(),
284+ buddy=FakeExcuse(),
285+ friend=FakeExcuse(),
286+ noppa=FakeExcuse())
287+
288+
289+class FakeData:
290+ version = '2.0'
291+
292+
293+class UtilTests(unittest.TestCase):
294+ maxDiff = None
295+
296+ @patch('policies.sourceppa.urllib.request.urlopen')
297+ def test_lp_rest_api_no_entries(self, urlopen):
298+ """Don't explode if LP reports no entries match pkg/version"""
299+ context = urlopen.return_value.__enter__.return_value
300+ context.getcode.return_value = 200
301+ context.read.return_value = b'{"entries": []}'
302+ pol = SourcePPAPolicy(FakeOptions)
303+ self.assertEqual(pol.lp_get_source_ppa('hello', '1.0'), '')
304+
305+ @patch('policies.sourceppa.urllib.request.urlopen')
306+ def test_lp_rest_api_no_source_ppa(self, urlopen):
307+ """Identify when package has no source PPA"""
308+ context = urlopen.return_value.__enter__.return_value
309+ context.getcode.return_value = 200
310+ context.read.return_value = b'{"entries": [{"copy_source_archive_link": null, "other_stuff": "ignored"}]}'
311+ pol = SourcePPAPolicy(FakeOptions)
312+ self.assertEqual(pol.lp_get_source_ppa('hello', '1.0'), '')
313+
314+ @patch('policies.sourceppa.urllib.request.urlopen')
315+ def test_lp_rest_api_with_source_ppa(self, urlopen):
316+ """Identify source PPA"""
317+ context = urlopen.return_value.__enter__.return_value
318+ context.getcode.return_value = 200
319+ context.read.return_value = b'{"entries": [{"copy_source_archive_link": "https://api.launchpad.net/1.0/team/ubuntu/ppa", "other_stuff": "ignored"}]}'
320+ pol = SourcePPAPolicy(FakeOptions)
321+ self.assertEqual(pol.lp_get_source_ppa('hello', '1.0'), 'https://api.launchpad.net/1.0/team/ubuntu/ppa')
322+
323+ @patch('policies.sourceppa.urllib.request.urlopen')
324+ def test_lp_rest_api_errors(self, urlopen):
325+ """Report errors instead of swallowing them"""
326+ context = urlopen.return_value.__enter__.return_value
327+ context.getcode.return_value = 500
328+ context.read.return_value = b''
329+ pol = SourcePPAPolicy(FakeOptions)
330+ with self.assertRaisesRegex(ConnectionError, 'HTTP 500'):
331+ pol.lp_get_source_ppa('hello', '1.0')
332+ # Yes, I have really seen "success with no json returned" in the wild
333+ context.getcode.return_value = 200
334+ context.read.return_value = b''
335+ with self.assertRaisesRegex(ValueError, 'Expecting value'):
336+ pol.lp_get_source_ppa('hello', '1.0')
337+
338+ def test_approve_ppa(self):
339+ """Approve packages by their PPA."""
340+ pol = SourcePPAPolicy(FakeOptions)
341+ pol.filename = CACHE_FILE
342+ pol.initialise(FakeBritney())
343+ output = {}
344+ for pkg in ('pal', 'buddy', 'friend', 'noppa'):
345+ self.assertEqual(pol.apply_policy_impl(output, None, pkg, None, FakeData, FakeExcuse), PolicyVerdict.PASS)
346+ self.assertEqual(output, {})
347+
348+ def test_reject_ppa(self):
349+ """Reject packages by their PPA."""
350+ shortppa = 'team/ubuntu/ppa'
351+ pol = SourcePPAPolicy(FakeOptions)
352+ pol.filename = CACHE_FILE
353+ brit = FakeBritney()
354+ brit.excuses['buddy'].is_valid = False # Just buddy is invalid but whole ppa fails
355+ pol.initialise(brit)
356+ output = {}
357+ # This one passes because the rejection isn't known yet
358+ self.assertEqual(pol.apply_policy_impl(output, None, 'pal', None, FakeData, brit.excuses['pal']), PolicyVerdict.PASS)
359+ # This one fails because it is itself invalid.
360+ self.assertEqual(pol.apply_policy_impl(output, None, 'buddy', None, FakeData, brit.excuses['buddy']), PolicyVerdict.REJECTED_PERMANENTLY)
361+ # This one fails because buddy failed before it.
362+ self.assertEqual(pol.apply_policy_impl(output, None, 'friend', None, FakeData, brit.excuses['friend']), PolicyVerdict.REJECTED_PERMANENTLY)
363+ # 'noppa' not from PPA so not rejected
364+ self.assertEqual(pol.apply_policy_impl(output, None, 'noppa', None, FakeData, FakeExcuse), PolicyVerdict.PASS)
365+ # All are rejected however
366+ for pkg in ('pal', 'buddy', 'friend'):
367+ self.assertFalse(brit.excuses[pkg].is_valid)
368+ self.assertDictEqual(pol.pkgs_by_source_ppa, {
369+ LAUNCHPAD_URL + shortppa: {'pal', 'buddy', 'friend'}})
370+ self.assertEqual(output, dict(pal=shortppa, buddy=shortppa, friend=shortppa))
371+
372+if __name__ == '__main__':
373+ unittest.main()

Subscribers

People subscribed via source and target branches