Merge ~jslarraz/review-tools:initial-schema-support into review-tools:master

Proposed by Jorge Sancho Larraz
Status: Rejected
Rejected by: Jorge Sancho Larraz
Proposed branch: ~jslarraz/review-tools:initial-schema-support
Merge into: review-tools:master
Diff against target: 534 lines (+223/-244)
6 files modified
reviewtools/schemas/snap.json (+14/-0)
reviewtools/sr_lint.py (+22/-35)
reviewtools/tests/schemas/test_schema_against_store.py (+76/-0)
reviewtools/tests/schemas/test_schema_base.py (+44/-0)
reviewtools/tests/schemas/test_schema_snap.py (+67/-0)
reviewtools/tests/test_sr_lint.py (+0/-209)
Reviewer Review Type Date Requested Status
Jorge Sancho Larraz Needs Fixing
Review via email: mp+465701@code.launchpad.net

Commit message

Initial schema support

Description of the change

Set up the framework to verify snap.yaml via schema. Attributes will be included in the schema iteratively to make PR reasonable to review

To post a comment you must log in.
Revision history for this message
Jorge Sancho Larraz (jslarraz) :
review: Needs Fixing

Unmerged commits

e23df21... by Jorge Sancho Larraz

Initial schema support

Failed
[FAILED] test:0 (build)
[WAITING] coverage:0 (build)
12 of 2 results

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/reviewtools/schemas/snap.json b/reviewtools/schemas/snap.json
2new file mode 100644
3index 0000000..3c239c4
4--- /dev/null
5+++ b/reviewtools/schemas/snap.json
6@@ -0,0 +1,14 @@
7+{
8+ "description": "snap.yaml",
9+ "type": "object",
10+ "properties": {
11+ "name": {
12+ "description": "The identifying name of the snap.",
13+ "type": "string",
14+ "pattern": "^(?:[a-z0-9]|(?<=[a-z0-9])-)*[a-z](?:[a-z0-9]|-(?=[a-z0-9]))*$",
15+ "minLength": 2,
16+ "maxLength": 40
17+ }
18+ },
19+ "required": ["name"]
20+}
21diff --git a/reviewtools/sr_lint.py b/reviewtools/sr_lint.py
22index c94340a..e1175e8 100644
23--- a/reviewtools/sr_lint.py
24+++ b/reviewtools/sr_lint.py
25@@ -18,8 +18,6 @@ from __future__ import print_function
26 from reviewtools.sr_common import SnapReview
27 from reviewtools.common import (
28 find_external_symlinks,
29- STORE_PKGNAME_SNAPV2_MAXLEN,
30- STORE_PKGNAME_SNAPV2_MINLEN,
31 )
32 from reviewtools.overrides import (
33 redflagged_snap_types_overrides,
34@@ -33,6 +31,8 @@ import os
35 import re
36 import shlex
37 import unicodedata
38+import jsonschema
39+import json
40
41
42 class SnapReviewLint(SnapReview):
43@@ -77,6 +77,26 @@ class SnapReviewLint(SnapReview):
44 "test-snapd-refresh-control-base",
45 ]
46
47+ def check_schema(self):
48+ """Check snap.yaml against the schema"""
49+ t = "info"
50+ n = self._get_check_name("snap_schema")
51+ s = "OK"
52+ manual_review = False
53+
54+ with open("reviewtools/schemas/snap.json") as fd:
55+ schema = json.loads(fd.read())
56+
57+ try:
58+ jsonschema.validate(self.snap_yaml, schema)
59+ except jsonschema.ValidationError as e:
60+ t = "error"
61+ s = "Error found in snap.yaml while validating %s: %s" % (
62+ e.json_path,
63+ e.message,
64+ )
65+ self._add_result(t, n, s, manual_review=manual_review)
66+
67 def check_architectures(self):
68 """Check architectures in snap.yaml is valid"""
69 t = "info"
70@@ -175,39 +195,6 @@ class SnapReviewLint(SnapReview):
71 # snap/validate.go
72 self._check_description_summary_title("title", 40)
73
74- def check_name(self):
75- """Check package name"""
76- t = "info"
77- n = self._get_check_name("name_valid")
78- s = "OK"
79- if "name" not in self.snap_yaml:
80- t = "error"
81- s = "could not find 'name' in yaml"
82- elif not isinstance(self.snap_yaml["name"], str):
83- t = "error"
84- s = "malformed 'name': %s (not a str)" % (self.snap_yaml["name"])
85- elif len(self.snap_yaml["name"]) > STORE_PKGNAME_SNAPV2_MAXLEN:
86- t = "error"
87- s = (
88- "malformed 'name': '%s' " % self.snap_yaml["name"]
89- + "(length '%d' " % len(self.snap_yaml["name"])
90- + "exceeds store limit '%d')" % STORE_PKGNAME_SNAPV2_MAXLEN
91- )
92- elif len(self.snap_yaml["name"]) < STORE_PKGNAME_SNAPV2_MINLEN:
93- t = "error"
94- s = (
95- "malformed 'name': '%s' " % self.snap_yaml["name"]
96- + "(length '%d' " % len(self.snap_yaml["name"])
97- + "below store limit '%d')" % STORE_PKGNAME_SNAPV2_MINLEN
98- )
99- elif not self._verify_pkgname(self.snap_yaml["name"]):
100- t = "error"
101- s = (
102- "malformed 'name': '%s' " % self.snap_yaml["name"]
103- + "(may be only lower case, digits and hyphens)"
104- )
105- self._add_result(t, n, s)
106-
107 def check_type(self):
108 """Check type"""
109 t = "info"
110diff --git a/reviewtools/tests/schemas/test_schema_against_store.py b/reviewtools/tests/schemas/test_schema_against_store.py
111new file mode 100644
112index 0000000..16259e2
113--- /dev/null
114+++ b/reviewtools/tests/schemas/test_schema_against_store.py
115@@ -0,0 +1,76 @@
116+from subprocess import run
117+import json
118+import yaml
119+import os
120+import sys
121+import jsonschema
122+import unittest
123+
124+SNAP_SCHEMA = "reviewtools/schemas/snap.json"
125+SNAP_YAML_CACHE = os.path.expanduser("~/.cache/review-tools")
126+
127+
128+@unittest.skipUnless(
129+ "TEST_SCHEMA_AGAINST_STORE" in os.environ, "not intended to be run by lpci"
130+)
131+def test_store():
132+ # Create cache directory if it does not exist
133+ try:
134+ os.mkdir(
135+ SNAP_YAML_CACHE,
136+ )
137+ except OSError:
138+ pass
139+
140+ # Load schema
141+ with open(SNAP_SCHEMA) as fd:
142+ snap_schema = json.loads(fd.read())
143+
144+ # Get list of snaps in the store
145+ with open("/var/cache/snapd/names", "r") as fd:
146+ snaps = fd.readlines()
147+
148+ rc = 0
149+ for snap in snaps:
150+ snap = snap.strip("\n ")
151+
152+ # Load the snap.yaml if cached, otherwise download it from the store
153+ snap_file = os.path.join(SNAP_YAML_CACHE, snap + "_yaml.json")
154+ if os.path.exists(snap_file):
155+ with open(snap_file) as fd:
156+ snap_yaml = json.loads(fd.read())
157+ else:
158+ try:
159+ p = run(
160+ ["review-tools.store-query", "--snap-yaml", snap],
161+ capture_output=True,
162+ )
163+ snap_yaml = yaml.safe_load(p.stdout.decode())
164+ except Exception as e:
165+ print(
166+ "Error occurred while getting snap.yaml for %s: %s" % (snap, str(e))
167+ )
168+ continue
169+
170+ with open(snap_file, "w") as fd:
171+ fd.write(json.dumps(snap_yaml))
172+
173+ # There are some snaps that has no snap.yaml. Skip them for now
174+ if snap_yaml is None:
175+ continue
176+
177+ # Validate the snap.yaml against the schema
178+ try:
179+ jsonschema.validate(snap_yaml, snap_schema)
180+ except jsonschema.ValidationError as e:
181+ print(
182+ "Error to validate %s for snap %s: %s" % (e.json_path, snap, e.message)
183+ )
184+ rc = 1
185+
186+ sys.exit(rc)
187+
188+
189+if __name__ == "__main__":
190+ if os.environ.get("TEST_SCHEMA_AGAINST_STORE"):
191+ test_store()
192diff --git a/reviewtools/tests/schemas/test_schema_base.py b/reviewtools/tests/schemas/test_schema_base.py
193new file mode 100644
194index 0000000..b41f5dd
195--- /dev/null
196+++ b/reviewtools/tests/schemas/test_schema_base.py
197@@ -0,0 +1,44 @@
198+import json
199+import jsonschema
200+import copy
201+import unittest
202+
203+
204+class TestSchemaBase(unittest.TestCase):
205+ yaml = {}
206+ schema_file = "reviewtools/schemas/****.json"
207+
208+ def setUp(self):
209+ # Load the relevant schema (defined in subclass)
210+ with open(self.schema_file) as fd:
211+ self.schema = json.loads(fd.read())
212+
213+ # Validate base yaml against the schema
214+ error = self._validate(self.yaml, self.schema)
215+ self.assertEqual(None, error)
216+
217+ def _validate(self, yaml, schema):
218+ # Validate yaml against the schema
219+ try:
220+ jsonschema.validate(yaml, schema)
221+ return None
222+ except jsonschema.ValidationError as e:
223+ return e.message
224+
225+ def _test_value(self, key, value, expected_error):
226+ # Prepare yaml, make a copy so we use the pristine yaml on each subtest
227+ yaml = copy.deepcopy(self.yaml)
228+ if value is None:
229+ if key in yaml:
230+ del yaml[key]
231+ else:
232+ yaml[key] = value
233+
234+ # Validate the schema
235+ error = self._validate(yaml, self.schema)
236+
237+ # Compare the errors against the expected ones
238+ if expected_error is None:
239+ self.assertIsNone(error)
240+ else:
241+ self.assertIn(expected_error, error)
242diff --git a/reviewtools/tests/schemas/test_schema_snap.py b/reviewtools/tests/schemas/test_schema_snap.py
243new file mode 100644
244index 0000000..315b278
245--- /dev/null
246+++ b/reviewtools/tests/schemas/test_schema_snap.py
247@@ -0,0 +1,67 @@
248+from reviewtools.tests.schemas.test_schema_base import TestSchemaBase
249+
250+
251+class TestSchemaSnap(TestSchemaBase):
252+
253+ schema_file = "reviewtools/schemas/snap.json"
254+
255+ def setUp(self):
256+ self.yaml = {
257+ "name": "foo",
258+ "version": "0.1",
259+ "type": "app",
260+ "description": "Test description",
261+ "summary": "Test summary",
262+ }
263+ super(TestSchemaSnap, self).setUp()
264+
265+ def test_name(self):
266+ for value, error in [
267+ # test_check_name_toplevel
268+ ("foo", None),
269+ # test_check_name_toplevel_startswith_number
270+ ("01game", None),
271+ # test_check_name_toplevel_maxlen_for_store
272+ ("a" * 40, None),
273+ # test_check_name_toplevel_minlen_for_store
274+ ("a" * 2, None),
275+ # test_check_name_toplevel_too_long_for_store
276+ ("a" * 41, "'{value}' is too long"),
277+ # test_check_name_toplevel_too_short_for_store
278+ ("a", "'{value}' is too short"),
279+ # test_check_name_toplevel_efficient
280+ ("u-94903713687486543234157734673284536758", None),
281+ # test_check_name_flat
282+ ("foo.bar", "'{value}' does not match "),
283+ # test_check_name_reverse_domain
284+ ("com.ubuntu.develeper.baz.foo", "'{value}' does not match "),
285+ # test_check_name_bad - ?
286+ ("foo?bar", "'{value}' does not match "),
287+ # test_check_name_bad1 - /
288+ ("foo/bar", "'{value}' does not match "),
289+ # test_check_name_bad2 - empty
290+ ("", "'{value}' does not match "),
291+ # test_check_name_bad3 - list
292+ ([], "{value} is not of type 'string'"),
293+ # test_check_name_bad4 - dict
294+ ({}, "{value} is not of type 'string'"),
295+ # test_check_name_bad5 - --
296+ ("foo--bar", "'{value}' does not match "),
297+ # test_check_name_bad6 - endswith -
298+ ("foo-bar-", "'{value}' does not match "),
299+ # test_check_name_bad6b - startswith -
300+ ("-foo-bar", "'{value}' does not match "),
301+ # test_check_name_bad7
302+ ("foo-Bar", "'{value}' does not match "),
303+ # test_check_name_bad8 - cap
304+ ("01", "'{value}' does not match "),
305+ # test_check_name_bad9 - all numbers and dashes
306+ ("0-1", "'{value}' does not match "),
307+ # test_check_name_missing
308+ (None, "'name' is a required property"),
309+ # ### integer
310+ (2, "{value} is not of type 'string'"),
311+ ]:
312+ with self.subTest(value=value):
313+ error = error.replace("{value}", str(value)) if error else error
314+ self._test_value("name", value, error)
315diff --git a/reviewtools/tests/test_sr_lint.py b/reviewtools/tests/test_sr_lint.py
316index 46f141c..d720017 100644
317--- a/reviewtools/tests/test_sr_lint.py
318+++ b/reviewtools/tests/test_sr_lint.py
319@@ -144,215 +144,6 @@ class TestSnapReviewLint(sr_tests.TestSnapReview):
320 sum += len(c.review_report[i])
321 self.assertTrue(sum != 0)
322
323- def test_check_name_toplevel(self):
324- """Test check_name - toplevel"""
325- self.set_test_snap_yaml("name", "foo")
326- c = SnapReviewLint(self.test_name)
327- c.check_name()
328- r = c.review_report
329- expected_counts = {"info": 1, "warn": 0, "error": 0}
330- self.check_results(r, expected_counts)
331-
332- def test_check_name_toplevel_startswith_number(self):
333- """Test check_name - toplevel starts with number"""
334- self.set_test_snap_yaml("name", "01game")
335- c = SnapReviewLint(self.test_name)
336- c.check_name()
337- r = c.review_report
338- expected_counts = {"info": 1, "warn": 0, "error": 0}
339- self.check_results(r, expected_counts)
340-
341- def test_check_name_toplevel_maxlen_for_store(self):
342- """Test check_name - toplevel maxlen for store"""
343- self.set_test_snap_yaml("name", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa40chars")
344- c = SnapReviewLint(self.test_name)
345- c.check_name()
346- r = c.review_report
347- expected_counts = {"info": 1, "warn": 0, "error": 0}
348- self.check_results(r, expected_counts)
349-
350- def test_check_name_toplevel_minlen_for_store(self):
351- """Test check_name - toplevel minlen for store"""
352- self.set_test_snap_yaml("name", "aa")
353- c = SnapReviewLint(self.test_name)
354- c.check_name()
355- r = c.review_report
356- expected_counts = {"info": 1, "warn": 0, "error": 0}
357- self.check_results(r, expected_counts)
358-
359- def test_check_name_toplevel_too_long_for_store(self):
360- """Test check_name - toplevel too long for store"""
361- self.set_test_snap_yaml("name", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa41chars")
362- c = SnapReviewLint(self.test_name)
363- c.check_name()
364- r = c.review_report
365- expected_counts = {"info": None, "warn": 0, "error": 1}
366- self.check_results(r, expected_counts)
367-
368- expected = dict()
369- expected["error"] = dict()
370- expected["warn"] = dict()
371- expected["info"] = dict()
372- name = "lint-snap-v2:name_valid"
373- expected["error"][name] = {
374- "text": "malformed 'name': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa41chars' (length '41' exceeds store limit '40')"
375- }
376- self.check_results(r, expected=expected)
377-
378- def test_check_name_toplevel_too_short_for_store(self):
379- """Test check_name - toplevel too short for store"""
380- self.set_test_snap_yaml("name", "a")
381- c = SnapReviewLint(self.test_name)
382- c.check_name()
383- r = c.review_report
384- expected_counts = {"info": None, "warn": 0, "error": 1}
385- self.check_results(r, expected_counts)
386-
387- expected = dict()
388- expected["error"] = dict()
389- expected["warn"] = dict()
390- expected["info"] = dict()
391- name = "lint-snap-v2:name_valid"
392- expected["error"][name] = {
393- "text": "malformed 'name': 'a' (length '1' below store limit '2')"
394- }
395- self.check_results(r, expected=expected)
396-
397- def test_check_name_toplevel_efficient(self):
398- """Test check_name - toplevel is efficient"""
399- self.set_test_snap_yaml("name", "u-94903713687486543234157734673284536758")
400- c = SnapReviewLint(self.test_name)
401- c.check_name()
402- r = c.review_report
403- expected_counts = {"info": 1, "warn": 0, "error": 0}
404- self.check_results(r, expected_counts)
405-
406- def test_check_name_flat(self):
407- """Test check_name - obsoleted flat"""
408- self.set_test_snap_yaml("name", "foo.bar")
409- c = SnapReviewLint(self.test_name)
410- c.check_name()
411- r = c.review_report
412- expected_counts = {"info": None, "warn": 0, "error": 1}
413- self.check_results(r, expected_counts)
414-
415- def test_check_name_reverse_domain(self):
416- """Test check_name - obsoleted reverse domain"""
417- self.set_test_snap_yaml("name", "com.ubuntu.develeper.baz.foo")
418- c = SnapReviewLint(self.test_name)
419- c.check_name()
420- r = c.review_report
421- expected_counts = {"info": None, "warn": 0, "error": 1}
422- self.check_results(r, expected_counts)
423-
424- def test_check_name_bad(self):
425- """Test check_name - bad - ?"""
426- self.set_test_snap_yaml("name", "foo?bar")
427- c = SnapReviewLint(self.test_name)
428- c.check_name()
429- r = c.review_report
430- expected_counts = {"info": None, "warn": 0, "error": 1}
431- self.check_results(r, expected_counts)
432-
433- def test_check_name_bad1(self):
434- """Test check_name - bad - /"""
435- self.set_test_snap_yaml("name", "foo/bar")
436- c = SnapReviewLint(self.test_name)
437- c.check_name()
438- r = c.review_report
439- expected_counts = {"info": None, "warn": 0, "error": 1}
440- self.check_results(r, expected_counts)
441-
442- def test_check_name_bad2(self):
443- """Test check_name - empty"""
444- self.set_test_snap_yaml("name", "")
445- c = SnapReviewLint(self.test_name)
446- c.check_name()
447- r = c.review_report
448- expected_counts = {"info": None, "warn": 0, "error": 1}
449- self.check_results(r, expected_counts)
450-
451- def test_check_name_bad3(self):
452- """Test check_name - list"""
453- self.set_test_snap_yaml("name", [])
454- c = SnapReviewLint(self.test_name)
455- c.check_name()
456- r = c.review_report
457- expected_counts = {"info": None, "warn": 0, "error": 1}
458- self.check_results(r, expected_counts)
459-
460- def test_check_name_bad4(self):
461- """Test check_name - dict"""
462- self.set_test_snap_yaml("name", {})
463- c = SnapReviewLint(self.test_name)
464- c.check_name()
465- r = c.review_report
466- expected_counts = {"info": None, "warn": 0, "error": 1}
467- self.check_results(r, expected_counts)
468-
469- def test_check_name_bad5(self):
470- """Test check_name - bad - --"""
471- self.set_test_snap_yaml("name", "foo--bar")
472- c = SnapReviewLint(self.test_name)
473- c.check_name()
474- r = c.review_report
475- expected_counts = {"info": None, "warn": 0, "error": 1}
476- self.check_results(r, expected_counts)
477-
478- def test_check_name_bad6(self):
479- """Test check_name - bad - endswith -"""
480- self.set_test_snap_yaml("name", "foo-bar-")
481- c = SnapReviewLint(self.test_name)
482- c.check_name()
483- r = c.review_report
484- expected_counts = {"info": None, "warn": 0, "error": 1}
485- self.check_results(r, expected_counts)
486-
487- def test_check_name_bad6b(self):
488- """Test check_name - bad - startswith -"""
489- self.set_test_snap_yaml("name", "-foo-bar")
490- c = SnapReviewLint(self.test_name)
491- c.check_name()
492- r = c.review_report
493- expected_counts = {"info": None, "warn": 0, "error": 1}
494- self.check_results(r, expected_counts)
495-
496- def test_check_name_bad7(self):
497- """Test check_name - bad - cap"""
498- self.set_test_snap_yaml("name", "foo-Bar")
499- c = SnapReviewLint(self.test_name)
500- c.check_name()
501- r = c.review_report
502- expected_counts = {"info": None, "warn": 0, "error": 1}
503- self.check_results(r, expected_counts)
504-
505- def test_check_name_bad8(self):
506- """Test check_name - bad - all numbers"""
507- self.set_test_snap_yaml("name", "01")
508- c = SnapReviewLint(self.test_name)
509- c.check_name()
510- r = c.review_report
511- expected_counts = {"info": None, "warn": 0, "error": 1}
512- self.check_results(r, expected_counts)
513-
514- def test_check_name_bad9(self):
515- """Test check_name - bad - all numbers and dashes"""
516- self.set_test_snap_yaml("name", "0-1")
517- c = SnapReviewLint(self.test_name)
518- c.check_name()
519- r = c.review_report
520- expected_counts = {"info": None, "warn": 0, "error": 1}
521- self.check_results(r, expected_counts)
522-
523- def test_check_name_missing(self):
524- """Test check_name - missing"""
525- self.set_test_snap_yaml("name", None)
526- c = SnapReviewLint(self.test_name)
527- c.check_name()
528- r = c.review_report
529- expected_counts = {"info": None, "warn": 0, "error": 1}
530- self.check_results(r, expected_counts)
531-
532 def test_check_version(self):
533 """Test check_version"""
534 self.set_test_snap_yaml("version", 1)

Subscribers

People subscribed via source and target branches