Merge lp:~sinzui/juju-release-tools/validate-streams into lp:juju-release-tools

Proposed by Curtis Hovey
Status: Merged
Merged at revision: 91
Proposed branch: lp:~sinzui/juju-release-tools/validate-streams
Merge into: lp:juju-release-tools
Diff against target: 459 lines (+438/-1)
3 files modified
tests/test_make_release_notes.py (+1/-1)
tests/test_validate_streams.py (+234/-0)
validate_streams.py (+203/-0)
To merge this branch: bzr merge lp:~sinzui/juju-release-tools/validate-streams
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+237975@code.launchpad.net

Description of the change

This branch adds a script that can compare the product items in two streams files and verify that the expected changes exist, that there are no missing or extra items, and that the common items are the same. The script ensures that non-numeric version cannot be in the proposed and release json

Nothing uses this script at this time.

The command line is purpose, new-version old-json, new-json. I compared the release to the proposed json like this to verify that 1.20.10 is the only change between them
    ./juju-release-tools/validate_streams.py proposed 1.20.10 old/com.ubuntu.juju\:released\:tools.json new/com.ubuntu.juju\:released\:tools.json

Comparing old with itself, but expecting 1.20.10 change is an error because the tools are missing
    ./juju-release-tools/validate_streams.py proposed 1.20.10 old/com.ubuntu.juju\:released\:tools.json old/com.ubuntu.juju\:released\:tools.json

I will use this script in assemble-streams in the future.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

I am finding it very hard to understand check_expected_tools. I don't know why it will accept version and then basically ignore it if retracted is set. It seems like retracted is really a mode, not a parameter. I would like to see a test like this:

old_tools = make_tools_data('trusty', 'amd64', ['1.20.7', '1.20.9'])
new_tools = make_tools_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
check_expected_tools(old_tools, new_tools, '1.20.8', '1.20.9')

Notice that 1.20.8 has been added and 1.20.9 has been removed. I don't know what the expected behaviour is. I think the actual behaviour is that it will complain that 1.20.8 is unexpected.

review: Needs Information
Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Aaron. Thank you for the review. your question has raised a scenario I had not considered.

Retract is like a mode. We are removing a set instead of adding one. The version being published is likely to be the previous version. The version is always passed because Jerf will always be installing that version to create the streams. I assumed that the version is the previous good version.

I don't think your example is real. I cannot think of retracting a later version, and publishing an earlier non-existent version, but...

I can image waking up and find an email from Ian that says there is a security issue with version n, his team have prepared version q. We need to retract earlier n and since something is ready, publish later q. I would certainly want to do one publication, not two to achieve this. I am certain this test would fail because there are two expected differences.

old_tools = make_tools_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
new_tools = make_tools_data('trusty', 'amd64', ['1.20.7', '1.20.9'])
check_expected_tools(old_tools, new_tools, '1.20.9', '1.20.8')

I am going to change the rules to accommodate this case. This is tricky I think the rule is that when doing a retractions, we need to ask if the version being published is new and need to be included in the expected differences.

103. By Curtis Hovey

Added can be done because we want to be clear about when we add a new version
and retract an old version.

104. By Curtis Hovey

Clearly state the content is correct when using verbose.

Revision history for this message
Aaron Bentley (abentley) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 14-10-17 01:58 PM, Curtis Hovey wrote:
> I am going to change the rules to accommodate this case. This is
> tricky I think the rule is that when doing a retractions, we need
> to ask if the version being published is new and need to be
> included in the expected differences.

I don't think it's actually that tricky. I think it's something like

added = dict(t for t in new_tools.items() if t[0] not in old_tools)
wrongly_added = dict(t for t in added.items()
                     if t[1][version] != new_version)
removed = dict(t for t in old_tools.items() if t[0] not in new_tools)
if retracted is None:
    wrongly_removed = removed
else:
    wrongly_removed = dict(t for t in added.items()
                           if t[1][version] != retracted)

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1

iQEcBAEBAgAGBQJUQWVfAAoJEK84cMOcf+9hk5QIAIk9rRS3YnrPjwSrhsRHB8n9
FXgZPXTi/Z61AbG3nrbESbpHE8R8FVtM5dQL7chVbhUSi52oFl2CVVS4wBTfX+/r
TYViz/bJQOTuAAgOwtRIMZ/ylHSdgjgVcbI0AaOguP5KevuERWxR3mXH6pusdX5+
xeaYtCTkkgK2jfcBNdPW54o97T/MnCh57iRKOFmgWCm5QMktizGkyPAvgpCBwhmx
vaPwjqnFjZUIuN3zWLFL7Gn6YS9QHWWbDAvjKp2XqyoDAuKVk3x+QH5RhFkuQlfB
luf56rgHhj/YoCbkBrj4dB6gTcv2pmFW1mHdontpKJvn2bgcOXK5tKIpYVqQBuw=
=Sd2G
-----END PGP SIGNATURE-----

105. By Curtis Hovey

added is optional. use --added to clearly state a version was added.

Revision history for this message
Curtis Hovey (sinzui) wrote :

I have a subtle change to the script. Instead of passing the args that might be passed to assemble-streams, the script now expects the caller to clearly state the expected differences.

I changed the script command line to clearly indicate when a version is added:
   --added 1.20.9 --retracted 1.20.8

Which also renamed version => added

I have changed added to be optional so that the function does not need to guess about expected differences
    check_expected_tools(old_tools, new_tools, added=None, retracted=None)

The assemble-streams script will need intelligence to know when something is added and retracted. This might not be tricky since it is configured with the reacted version and there is a check if any new tools were created.

Note I made a change to verbose. I used this script for the 1.20.10 release, and I had to check the return code because --verbose didn't clearly tell me that everything was fine. I really wanted a clear message that the content was correct.

106. By Curtis Hovey

Extracted temp_dir() to tests.utils

Revision history for this message
Curtis Hovey (sinzui) wrote :

Oh, and I am sure you noted that temp_dir() could be extracted to a utils.py. I have done this to keep the code DRY.

Revision history for this message
Aaron Bentley (abentley) wrote :

Okay, I think I'm getting the hang of this code, and I'm pretty sure it is not correct. There are two ways check_expected_tools can be tricked out of warning that "added" is missing.

1. Do a successful retract. This means that expected_differences will be non-empty from the retract clause, and so the added clause cannot set missing_errors.

2. Have other missing versions. This means that the "added" clause will set missing_errors, but the "missing" clause will overwrite its value. There will be an error, but it won't mention "added".

Other comments below.

review: Needs Fixing
107. By Curtis Hovey

Merged tip and resolved conflicts.

108. By Curtis Hovey

Read the file directly into the stream.

109. By Curtis Hovey

Do not overwrite missing_errors.

Revision history for this message
Curtis Hovey (sinzui) wrote :

I have replied to the comments and pushed up some changes. I have another change I need to make, at least a test, and maybe a code change to account for streams.canonical.com's behaviour.

Revision history for this message
Aaron Bentley (abentley) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 14-10-20 05:55 PM, Curtis Hovey wrote:
> The "versions" keys are arbitrary names :(. juju metadata will
> name it something like "20141020". We only get 1 key in versions,
> but the specification allows many. We don't care if there is 1 or
> many of these versions we only want to get the the next level,
> "items". The goal is to say all items on old are the same as the
> items in new, except we know that we sometimes add items and we
> have one removed items, once published without items, and on a few
> occasions change items.
>
> In Xpath, this is products/versions/*/items. I could change the
> test for items to be if isinstance(version, dict) and "items" in
> versions:

Okay, the code is fine as it stands, then.

>> + # needs a version to install to make streams, even when
>> it + # intends to remove something. + for n, t in
>> old_expected.items(): + if t['version'] == retracted:
>> + expected_differences.update([(n, t)])
>
> The goal was to state the expected differences to create a common
> set of old and new tools that must match in name a content.

Deleting entries from old_expected is perfectly sane. But why are
updating expected_differences? You don't use it anywhere, except the
"added" clause. And in the "added" clause, it can only cause a bug,
by falsely preventing the "missing.append(added)".

> So expected_differences was just a way to remove what we know to
> be different to prove nothing else is changed.

No, it doesn't do anything like that. All it does is prevent
"missing.append(added)", so that this version is not considered missing.
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1

iQEcBAEBAgAGBQJURnGpAAoJEK84cMOcf+9hwDkIAJmJOGytVsStF5De6MijLpV5
4G6YNYWhzp+ShM38KIhT39CZ83pJV4DY7yO3ajIi03/359j1rufRug4CxukxPGwq
ByQO+/SuyvKZYpHFRVqmpsCe1kaKX4WC8gInLm2JLgFoMr/zXDWAL9jnZ8TMmEa7
V0a9T/jNit/3q9t4hMEoaNohaAaWj3vxbNWdm/gasmtApMiqtLltHD1SNEm8czau
py1qmtidjNd4R4q+TTjROmo5uJqOR0jPBOe1MoYdzzUY4F/1QgqWlCEo6TEVTseU
rSuaU1aVG0ks63Et+S8/GcBuqGOjT5DIjHDIt1LYVPa4xZYuBkHuP6loJmEyOZ4=
=GrUS
-----END PGP SIGNATURE-----

110. By Curtis Hovey

Merged tip, Removed IGNORE.

111. By Curtis Hovey

Renamed retracted => removed.

112. By Curtis Hovey

expected_differences is not needed.

113. By Curtis Hovey

Make code easier to maintain, though a little redudant, by creating smaller
functions with clear purpose.

114. By Curtis Hovey

Revise docstrings.

Revision history for this message
Curtis Hovey (sinzui) wrote :

My latest changes follow your suggestion to simplify the purpose of the functions to make the code easier to maintain. I split the rules to check for expected versions from rules to find unexpected version.

I also changed all the checks to always return a list to make the contact simple and it is easy for the callee to extend the list of errors.

Revision history for this message
Aaron Bentley (abentley) wrote :

This looks clear and correct. Thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'tests/test_make_release_notes.py'
--- tests/test_make_release_notes.py 2014-10-20 18:43:54 +0000
+++ tests/test_make_release_notes.py 2014-10-22 21:03:40 +0000
@@ -3,7 +3,7 @@
3from StringIO import StringIO3from StringIO import StringIO
4from unittest import TestCase4from unittest import TestCase
55
66from utils import temp_dir
7from make_release_notes import (7from make_release_notes import (
8 DEVEL,8 DEVEL,
9 get_bugs,9 get_bugs,
1010
=== added file 'tests/test_validate_streams.py'
--- tests/test_validate_streams.py 1970-01-01 00:00:00 +0000
+++ tests/test_validate_streams.py 2014-10-22 21:03:40 +0000
@@ -0,0 +1,234 @@
1from mock import patch
2import json
3from unittest import TestCase
4
5from utils import temp_dir
6from validate_streams import (
7 check_devel_not_stable,
8 check_expected_changes,
9 check_expected_unchanged,
10 check_agents_content,
11 compare_agents,
12 find_agents,
13 parse_args,
14)
15
16
17def make_tool_data(version='1.20.7', release='trusty', arch='amd64'):
18 name = '{}-{}-{}'.format(version, release, arch)
19 tool = {
20 "release": "{}".format(release),
21 "version": "{}".format(version),
22 "arch": "{}".format(arch),
23 "size": 8234578,
24 "path": "releases/juju-{}.tgz".format(name),
25 "ftype": "tar.gz",
26 "sha256": "valid_sum"
27 }
28 return name, tool
29
30
31def make_agents_data(release='trusty', arch='amd64', versions=['1.20.7']):
32 return dict(make_tool_data(v, release, arch) for v in versions)
33
34
35def make_product_data(release='trusty', arch='amd64', versions=['1.20.7']):
36 name = 'com.ubuntu.juju:{}:{}'.format(release, arch)
37 items = make_agents_data(release, arch, versions)
38 product = {
39 "version": "{}".format(versions[0]),
40 "arch": "{}".format(arch),
41 "versions": {
42 "20140919": {
43 "items": items
44 }
45 }
46 }
47 return name, product
48
49
50def make_products_data(versions):
51 products = {}
52 for release, arch in (('trusty', 'amd64'), ('trusty', 'i386')):
53 name, product = make_product_data(release, arch, versions)
54 products[name] = product
55 stream = {
56 "products": products,
57 "updated": "Fri, 19 Sep 2014 13:25:28 -0400",
58 "format": "products:1.0",
59 "content_id": "com.ubuntu.juju:released:agents"
60 }
61 return stream
62
63
64class ValidateStreams(TestCase):
65
66 def test_parge_args(self):
67 # The purpose, release, old json and new json are required.
68 required = ['--added', '1.20.9', 'proposed', 'old/json', 'new/json']
69 args = parse_args(required)
70 self.assertEqual('proposed', args.purpose)
71 self.assertEqual('old/json', args.old_json)
72 self.assertEqual('new/json', args.new_json)
73 self.assertEqual('1.20.9', args.added)
74 # A bad release version can be removed.
75 args = parse_args(['--removed', 'bad'] + required)
76 self.assertEqual('bad', args.removed)
77
78 def test_find_agents(self):
79 products = make_products_data(['1.20.7', '1.20.8'])
80 with temp_dir() as wd:
81 file_path = '{}/json'.format(wd)
82 with open(file_path, 'w') as f:
83 f.write(json.dumps(products))
84 agents = find_agents(file_path)
85 expected = [
86 '1.20.7-trusty-i386', '1.20.7-trusty-amd64',
87 '1.20.8-trusty-amd64', '1.20.8-trusty-i386']
88 self.assertEqual(expected, agents.keys())
89
90 def test_check_devel_not_stable(self):
91 # devel agents cannot ever got to proposed and release.
92 old_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
93 new_agents = make_agents_data(
94 'trusty', 'amd64', ['1.20.7', '1.20.8', '1.21-alpha1'])
95 # Devel versions can go to testing
96 message = check_devel_not_stable(old_agents, new_agents, 'testing')
97 self.assertEqual([], message)
98 # Devel versions can go to devel
99 message = check_devel_not_stable(old_agents, new_agents, 'devel')
100 self.assertEqual([], message)
101 # Devel versions cannot be proposed.
102 message = check_devel_not_stable(old_agents, new_agents, 'proposed')
103 self.assertEqual(
104 ["Devel versions in proposed stream: "
105 "['1.21-alpha1-trusty-amd64']"],
106 message)
107 # Devel versions cannot be release.
108 message = check_devel_not_stable(old_agents, new_agents, 'release')
109 self.assertEqual(
110 ["Devel versions in release stream: ['1.21-alpha1-trusty-amd64']"],
111 message)
112
113 def test_check_agents_content(self):
114 old_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
115 new_agents = make_agents_data(
116 'trusty', 'amd64', ['1.20.7', '1.20.8'])
117 new_agents['1.20.7-trusty-amd64']['sha256'] = 'bad_sum'
118 message = check_agents_content(old_agents, new_agents)
119 self.assertEqual(
120 (['Tool 1.20.7-trusty-amd64 sha256 changed from '
121 'valid_sum to bad_sum']),
122 message)
123
124 def test_compare_agents_identical(self):
125 old_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
126 new_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
127 message = compare_agents(
128 old_agents, new_agents, 'proposed', added=None, removed=None)
129 self.assertIs(None, message)
130
131 def test_check_expected_changes_with_no_changes(self):
132 new_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
133 errors = check_expected_changes(new_agents, added=None, removed=None)
134 self.assertEqual([], errors)
135
136 def test_check_expected_changes_with_changes(self):
137 new_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.9'])
138 errors = check_expected_changes(
139 new_agents, added='1.20.9', removed='1.20.8')
140 self.assertEqual([], errors)
141
142 def test_check_expected_changes_with_found_errors(self):
143 new_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
144 errors = check_expected_changes(
145 new_agents, added=None, removed='1.20.8')
146 self.assertEqual(
147 ["1.20.8 agents were not removed: ['1.20.8-trusty-amd64']"],
148 errors)
149
150 def test_check_expected_changes_with_missing_errors(self):
151 new_agents = make_agents_data('trusty', 'amd64', ['1.20.7'])
152 errors = check_expected_changes(
153 new_agents, added='1.20.8', removed=None)
154 self.assertEqual(['1.20.8 agents were not added'], errors)
155
156 def test_check_expected_unchanged_without_changes(self):
157 old_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
158 new_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
159 errors = check_expected_unchanged(
160 old_agents, new_agents, added=None, removed=None)
161 self.assertEqual([], errors)
162
163 def test_check_expected_unchanged_without_changes_and_added_removed(self):
164 old_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
165 new_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.9'])
166 errors = check_expected_unchanged(
167 old_agents, new_agents, added='1.20.9', removed='1.20.8')
168 self.assertEqual([], errors)
169
170 def test_check_expected_unchanged_with_missing_errors(self):
171 old_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
172 new_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.9'])
173 errors = check_expected_unchanged(
174 old_agents, new_agents, added='1.20.9', removed=None)
175 self.assertEqual(
176 ["These agents are missing: ['1.20.8-trusty-amd64']"],
177 errors)
178
179 def test_check_expected_unchanged_with_found_errors(self):
180 old_agents = make_agents_data('trusty', 'amd64', ['1.20.7'])
181 new_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.9'])
182 errors = check_expected_unchanged(
183 old_agents, new_agents, added=None, removed=None)
184 self.assertEqual(
185 ["These unknown agents were found: ['1.20.9-trusty-amd64']"],
186 errors)
187
188 def test_compare_agents_changed_tool(self):
189 old_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
190 new_agents = make_agents_data(
191 'trusty', 'amd64', ['1.20.7', '1.20.8', '1.20.9'])
192 new_agents['1.20.7-trusty-amd64']['sha256'] = 'bad_sum'
193 errors = compare_agents(
194 old_agents, new_agents, 'proposed', '1.20.9', removed=None)
195 self.assertEqual(
196 ['Tool 1.20.7-trusty-amd64 sha256 changed from '
197 'valid_sum to bad_sum'],
198 errors)
199
200 def test_compare_agents_called_check_expected_agents_called(self):
201 old_agents = make_agents_data('trusty', 'amd64', ['1.20.7'])
202 new_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
203 with patch("validate_streams.check_expected_changes",
204 return_value=(['foo'])) as cec_mock:
205 with patch("validate_streams.check_expected_unchanged",
206 return_value=(['bar'])) as ceu_mock:
207 errors = compare_agents(
208 old_agents, new_agents, 'proposed', '1.20.9', removed=None)
209 cec_mock.assert_called_with(new_agents, '1.20.9', None)
210 ceu_mock.assert_called_with(
211 old_agents, new_agents, '1.20.9', None)
212 self.assertEqual(['foo', 'bar'], errors)
213
214 def test_compare_agents_added_devel_version(self):
215 # devel agents cannot ever got to proposed and release.
216 old_agents = make_agents_data('trusty', 'amd64', ['1.20.7', '1.20.8'])
217 new_agents = make_agents_data(
218 'trusty', 'amd64', ['1.20.7', '1.20.8', '1.21-alpha1'])
219 # Devel versions can go to devel
220 message = compare_agents(
221 old_agents, new_agents, 'devel', '1.21-alpha1', removed=None)
222 self.assertIs(None, message)
223 # Devel versions cannot be proposed.
224 errors = compare_agents(
225 old_agents, new_agents, 'proposed', '1.21-alpha1', removed=None)
226 expected = (
227 "Devel versions in proposed stream: ['1.21-alpha1-trusty-amd64']")
228 self.assertEqual([expected], errors)
229 # Devel versions cannot be release.
230 errors = compare_agents(
231 old_agents, new_agents, 'release', '1.21-alpha1', removed=None)
232 expected = (
233 "Devel versions in release stream: ['1.21-alpha1-trusty-amd64']")
234 self.assertEqual([expected], errors)
0235
=== added file 'validate_streams.py'
--- validate_streams.py 1970-01-01 00:00:00 +0000
+++ validate_streams.py 2014-10-22 21:03:40 +0000
@@ -0,0 +1,203 @@
1#!/usr/bin/python
2
3from __future__ import print_function
4
5from argparse import ArgumentParser
6import json
7import re
8import sys
9import traceback
10
11
12RELEASE = 'release'
13PROPOSED = 'proposed'
14DEVEL = 'devel'
15TESTING = 'testing'
16PURPOSES = (RELEASE, PROPOSED, DEVEL, TESTING)
17
18
19def find_agents(file_path):
20 with open(file_path) as f:
21 stream = json.load(f)
22 agents = {}
23 for name, product in stream['products'].items():
24 versions = product['versions']
25 for version in versions.values():
26 if isinstance(version, dict):
27 items = version['items']
28 agents.update(items)
29 return agents
30
31
32def check_devel_not_stable(old_agents, new_agents, purpose):
33 """Return a list of errors if the version can be included in the stream.
34
35 Devel versions cannot be proposed or release because the letters in
36 the version break older jujus.
37
38 :param old_agents: the dict of all the products/versions/*/items
39 in the old json.
40 :param new_agents: the dict of all the products/versions/*/items
41 in the new json.
42 :param purpose: either release, proposed, devel, or testing.
43 :return: a list of errors, which will be empty when there are none.
44 """
45 if purpose in (TESTING, DEVEL):
46 return []
47 stable_pattern = re.compile(r'\d+\.\d+\.\d+-*')
48 devel_versions = [
49 v for v in new_agents.keys() if not stable_pattern.match(v)]
50 errors = []
51 if devel_versions:
52 errors.append(
53 'Devel versions in {} stream: {}'.format(purpose, devel_versions))
54 return errors
55
56
57def check_expected_changes(new_agents, added=None, removed=None):
58 """Return an list of errors if the expected changes are not present.
59
60 :param new_agents: the dict of all the products/versions/*/items
61 in the new json.
62 :param added: the version added to the new json, eg '1.20.9'.
63 :param removed: the version removed from the new json, eg '1.20.8'.
64 :return: a list of errors, which will be empty when there are none.
65 """
66 found = []
67 seen = False
68 for n, t in new_agents.items():
69 if removed and t['version'] == removed:
70 found.append(n)
71 elif added and t['version'] == added:
72 seen = True
73 errors = []
74 if added and not seen:
75 errors.append('{} agents were not added'.format(added))
76 if found:
77 errors.append('{} agents were not removed: {}'.format(removed, found))
78 return errors
79
80
81def check_expected_unchanged(old_agents, new_agents, added=None, removed=None):
82 """Return a list of errors if the expected unchanged versions do not match.
83
84 :param old_agents: the dict of all the products/versions/*/items
85 in the old json.
86 :param new_agents: the dict of all the products/versions/*/items
87 in the new json.
88 :param added: the version added to the new json, eg '1.20.9'.
89 :param removed: the version removed from the new json, eg '1.20.8'.
90 :return: a list of errors, which will be empty when there are none.
91 """
92 old_versions = set(k for (k, v) in old_agents.items()
93 if v['version'] != removed)
94 new_versions = set(k for (k, v) in new_agents.items()
95 if v['version'] != added)
96 missing_errors = old_versions - new_versions
97 errors = []
98 if missing_errors:
99 missing_errors = list(missing_errors)
100 errors.append('These agents are missing: {}'.format(missing_errors))
101 found_errors = new_versions - old_versions
102 if found_errors:
103 found_errors = list(found_errors)
104 errors.append('These unknown agents were found: {}'.format(
105 found_errors))
106 return errors
107
108
109def check_agents_content(old_agents, new_agents):
110 """Return a list of error messages if agents content changes.
111
112 Are the old versions identical to the new versions?
113 We care about change values, not new keys in the new tool.
114
115 :param old_agents: the dict of all the products/versions/*/items
116 in the old json.
117 :param new_agents: the dict of all the products/versions/*/items
118 in the new json.
119 :return: a list of errors, which will be empty when there are none.
120 """
121 if not new_agents:
122 return None
123 errors = []
124 for name, old_tool in old_agents.items():
125 try:
126 new_tool = new_agents[name]
127 except KeyError:
128 # This is a missing version case reported by check_expected_agents.
129 continue
130 for old_key, old_val in old_tool.items():
131 new_val = new_tool[old_key]
132 if old_val != new_val:
133 errors.append(
134 'Tool {} {} changed from {} to {}'.format(
135 name, old_key, old_val, new_val))
136 return errors
137
138
139def compare_agents(old_agents, new_agents, purpose, added=None, removed=None):
140 """Return a list of error messages from all the validation checks.
141
142 :param old_agents: the dict of all the products/versions/*/items
143 in the old json.
144 :param new_agents: the dict of all the products/versions/*/items
145 in the new json.
146 :return: a list of errors, which will be empty when there are none.
147 """
148 errors = []
149 errors.extend(
150 check_devel_not_stable(old_agents, new_agents, purpose))
151 errors.extend(
152 check_expected_changes(new_agents, added, removed))
153 errors.extend(
154 check_expected_unchanged(old_agents, new_agents, added, removed))
155 errors.extend(
156 check_agents_content(old_agents, new_agents))
157 return errors or None
158
159
160def parse_args(args=None):
161 """Return the argument parser for this program."""
162 parser = ArgumentParser("Compare old and new stream data.")
163 parser.add_argument(
164 '-v', '--verbose', action="store_true", default=False,
165 help='Increse verbosity.')
166 parser.add_argument(
167 '-r', '--removed', default=None, help='The release version removed')
168 parser.add_argument(
169 '-a', '--added', default=None, help="The release version added")
170 parser.add_argument('purpose', help="<{}>".format(' | '.join(PURPOSES)))
171 parser.add_argument('old_json', help="The old simple streams data file")
172 parser.add_argument('new_json', help="The new simple streams data file")
173 return parser.parse_args(args)
174
175
176def main(argv):
177 """Verify that the new json has all the expected changes.
178
179 An exit code of 1 will have a list of strings explaining the problems.
180 An exit code of 0 is a pass and the explanation is None.
181 """
182 args = parse_args(argv[1:])
183 try:
184 old_agents = find_agents(args.old_json)
185 new_agents = find_agents(args.new_json)
186 errors = compare_agents(
187 old_agents, new_agents, args.purpose, args.version,
188 retracted=args.retracted)
189 if errors:
190 print('\n'.join(errors))
191 return 1
192 except Exception as e:
193 print(e)
194 if args.verbose:
195 traceback.print_tb(sys.exc_info()[2])
196 return 2
197 if args.verbose:
198 print("All changes are correct.")
199 return 0
200
201
202if __name__ == '__main__':
203 sys.exit(main(sys.argv))

Subscribers

People subscribed via source and target branches