Add full webactivity support (bug 936842)

This commit is contained in:
mattbasta 2013-11-21 22:02:57 +00:00
Родитель d76e5c3c39
Коммит e80a91ba22
4 изменённых файлов: 241 добавлений и 114 удалений

Просмотреть файл

@ -129,7 +129,13 @@ class Spec(object):
# Check that the node is of the proper type. If it isn't, then we need
# to stop iterating at this point.
if not isinstance(branch, spec_branch["expected_type"]):
exp_type = spec_branch.get("expected_type")
if (exp_type and
not isinstance(branch, exp_type) or
# Handle `isinstance(True, int) == True` :(
(isinstance(branch, bool) and
(exp_type == int if isinstance(exp_type, type) else
bool not in exp_type))):
self.error(
err_id=("spec", "iterate", "bad_type"),
error="%s's `%s` was of an unexpected type." %
@ -163,21 +169,21 @@ class Spec(object):
error="`%s` contains an invalid value in %s" %
(branch_name, self.SPEC_NAME),
description=["A `%s` was encountered while validating a "
"%s containing the value '%s'. This value is "
"not appropriate for this type of element." %
"`%s` containing the value '%s'. This value "
"is not appropriate for this type of "
"element." %
(branch_name, self.SPEC_NAME, branch),
self.MORE_INFO])
elif ("value_matches" in spec_branch and
isinstance(branch, types.StringTypes)):
raw_pattern = spec_branch.get("value_matches")
pattern = re.compile(raw_pattern)
if not pattern.match(branch):
raw_pattern = spec_branch["value_matches"]
if not re.match(raw_pattern, branch):
self.error(
err_id=("spec", "iterate", "value_pattern_fail"),
error="`%s` contains an invalid value in %s" %
(branch_name, self.SPEC_NAME),
description=["A `%s` was encountered while validating "
"a %s. Its value does not match the "
"a `%s`. Its value does not match the "
"pattern required for `%s`s." %
(branch_name, self.SPEC_NAME,
branch_name),
@ -197,7 +203,7 @@ class Spec(object):
self.MORE_INFO])
# The rest of the tests are for child items.
if not isinstance(branch, list):
if not isinstance(branch, (list, tuple)):
return
if "child_nodes" in spec_branch:

Просмотреть файл

@ -8,6 +8,9 @@ from ..constants import DESCRIPTION_TYPES
from ..specprocessor import Spec, LITERAL_TYPE
# This notably excludes booleans.
JSON_LITERALS = types.StringTypes + (int, float)
BANNED_ORIGINS = [
"gaiamobile.org",
"mozilla.com",
@ -20,6 +23,50 @@ _FULL_PERMISSIONS = ("readonly", "readwrite", "readcreate", "createonly")
FXOS_ICON_SIZES = (60, 90, 120)
FILTER_DEF_OBJ = {
"expected_type": dict,
"not_empty": True,
"child_nodes": {
"required": {"expected_type": bool},
"value": {
"expected_type": JSON_LITERALS + (list, tuple),
"not_empty": True,
"child_nodes": {"expected_type": JSON_LITERALS},
},
"min": {"expected_type": (int, float)},
"max": {"expected_type": (int, float)},
"pattern": {"expected_type": types.StringTypes},
"regexp": {"expected_type": types.StringTypes}, # FXOS 1.0/1.1
"patternFlags": {
"expected_type": types.StringTypes,
"max_length": 4,
"value_matches": "[igmy]+",
},
},
}
FILTER_DEF_OBJ["allowed_once_nodes"] = FILTER_DEF_OBJ["child_nodes"].keys()
WEB_ACTIVITY_HANDLER = {
"href": {"expected_type": types.StringTypes,
"process": lambda s: s.process_act_href,
"not_empty": True},
"disposition": {"expected_type": types.StringTypes,
"values": ["window", "inline"]},
"filters": {
"expected_type": dict,
"allowed_nodes": ["*"],
"not_empty": True,
"child_nodes": {
"*": {
"expected_type": JSON_LITERALS + (list, dict),
"not_empty": True,
"process": lambda s: s.process_act_filter,
}
},
},
"returnValue": {"expected_type": bool},
}
class WebappSpec(Spec):
"""This object parses and subsequently validates webapp manifest files."""
@ -129,26 +176,10 @@ class WebappSpec(Spec):
"*": {
"expected_type": dict,
"required_nodes": ["href"],
"allowed_once_nodes": ["disposition", "filters"],
"child_nodes": {
"href": {"expected_type": types.StringTypes,
"process": lambda s: s.process_act_href,
"not_empty": True},
"disposition": {"expected_type": types.StringTypes,
"values": ["window", "inline"]},
"filters": {
"expected_type": dict,
"allowed_nodes": ["*"],
"child_nodes":
{"*": {"expected_type": DESCRIPTION_TYPES,
"process":
lambda s: s.process_act_type,
"not_empty": True},
"number": {"expected_type": LITERAL_TYPE}}
},
"returnValue": {
"expected_type": bool}
}
"allowed_once_nodes": [
"disposition", "filters", "returnValue"
],
"child_nodes": WEB_ACTIVITY_HANDLER,
}
}
},
@ -504,16 +535,38 @@ class WebappSpec(Spec):
"Found: %s" % node,
self.MORE_INFO])
def process_act_type(self, node):
if (isinstance(node, list) and
not all(isinstance(s, types.StringTypes) for s in node)):
def process_act_filter(self, node):
if isinstance(node, JSON_LITERALS):
# Standard JSON literals can be safely ignored.
return
if isinstance(node, list):
# Arrays must contain only JSON literals.
if not all(isinstance(s, JSON_LITERALS) and
not isinstance(s, bool) for s in node):
self.error(
err_id=("spec", "webapp", "act_type"),
error="Activity filter is not valid.",
description=[
"The value for an activity's filter must either "
"be a basic value or array of basic values.",
"Found: [%s]" % ", ".join(map(repr, node)),
self.MORE_INFO])
elif isinstance(node, dict):
# Objects are filter definition objects, which have rules.
return self._iterate(self.path[-1], node, FILTER_DEF_OBJ)
else:
# Everything else is invalid.
self.error(
err_id=("spec", "webapp", "act_type"),
error="Activity `type` is not valid.",
description=["The `type` value for an activity must either be "
"a string or array of strings.",
"Found: [%s]" % ", ".join(map(str, node)),
self.MORE_INFO])
error="Activity filter is not valid.",
description=[
"The value for an activity's filter must either "
"be a basic value or array of basic values.",
"Found: %s" % repr(node),
self.MORE_INFO])
def process_orientation(self, node):
values = [u"portrait", u"landscape", u"portrait-secondary",

Просмотреть файл

@ -42,10 +42,10 @@ class TestWebappAccessories(TestCase):
eq_(s._path_valid("data:asdf", can_be_data=True), True)
class TestWebapps(TestCase):
class WebappBaseTestCase(TestCase):
def setUp(self):
super(TestWebapps, self).setUp()
super(WebappBaseTestCase, self).setUp()
self.listed = False
descr = "Exciting Open Web development action!"
@ -126,6 +126,8 @@ class TestWebapps(TestCase):
appvalidator.webapp.detect_webapp(self.err, name)
os.unlink(name)
class TestWebapps(WebappBaseTestCase):
def test_pass(self):
"""Test that a bland webapp file throws no errors."""
self.analyze()
@ -610,82 +612,9 @@ class TestWebapps(TestCase):
self.analyze()
self.assert_silent()
def test_act_base(self):
"""Test that the most basic web activity passes."""
self.data["activities"] = {
"foo": {"href": "/foo/bar"}
}
self.analyze()
self.assert_silent()
def test_act_full(self):
"""Test that the fullest web activity passes."""
self.data["activities"] = {
"foo": {"href": "/foo/bar",
"disposition": "window",
"filters": {"type": "foo", "number": 1}},
"bar": {"href": "foo/bar",
"disposition": "inline",
"filters": {"whatever": ["foo", "bar"]}}
}
self.analyze()
self.assert_silent()
def test_act_bad_href(self):
"""Test that bad activity hrefs are disallowed."""
self.data["activities"] = {
"foo": {"href": "http://foo.bar/asdf",
"disposition": "window",
"filters": {"type": "foo"}}
}
self.analyze()
self.assert_failed(with_errors=True)
def test_act_bad_disp(self):
"""Test that the disposition of an activity is correct."""
self.data["activities"] = {
"foo": {"href": "/foo/bar",
"disposition": "lol not a disposition",
"filters": {"type": "foo"}}
}
self.analyze()
self.assert_failed(with_errors=True)
def test_act_bad_filter_type(self):
"""Test that the filter values are correct."""
self.data["activities"] = {
"foo": {"href": "/foo/bar",
"disposition": "window",
"filters": {"type": 2}}
}
self.analyze()
self.assert_failed(with_errors=True)
def test_act_bad_filter_base_type(self):
"""Test that the filter values are correct."""
self.data["activities"] = {
"foo": {"href": "/foo/bar",
"disposition": "window",
"filters": "this is not a dict"}
}
self.analyze()
self.assert_failed(with_errors=True)
def test_act_missing_href(self):
"""Test that activities require an href."""
self.data["activities"] = {
"foo": {"disposition": "window",
"filters": {"type": "foo"}}
}
self.analyze()
self.assert_failed(with_errors=True)
###########
# Web activities are tested in tests/test_webapp_activity.py
###########
def test_act_root_type(self):
"""Test that the most basic web activity passes."""

Просмотреть файл

@ -0,0 +1,139 @@
from test_webapp import WebappBaseTestCase
class TestWebappActivity(WebappBaseTestCase):
"""
This suite tests that web activities are properly handled for all
reasonable combinations of valid nodes.
"""
def setUp(self):
super(TestWebappActivity, self).setUp()
self.ad = self.data["activities"] = {
"simple": {
"href": "url.html",
"disposition": "window",
"returnValue": True,
"filters": {
"literal": 123,
"array": ["literal", 123],
"filter_obj": {
"required": True,
"value": "literal",
"min": 1,
"max": 2,
"pattern": "asdf",
"patternFlags": "ig",
"regexp": "asdf",
},
},
},
}
self.simple = self.ad["simple"]
def broken(self):
self.analyze()
return self.assert_failed(with_errors=True)
def suspicious(self):
self.analyze()
return self.assert_failed(with_warnings=True)
def test_pass(self):
self.analyze()
self.assert_silent()
def test_missing_href(self):
del self.simple["href"]
self.broken()
def test_min_features(self):
self.data["activities"] = {
"simple": {"href": "foo.html"},
}
self.analyze()
self.assert_silent()
def test_bad_href(self):
self.simple["href"] = "http://foo.bar/asdf"
self.broken()
def test_bad_disposition(self):
self.simple["disposition"] = "not a disposition"
self.broken()
def test_bad_returnValue(self):
self.simple["returnValue"] = "foo"
self.broken()
def test_bad_extra(self):
self.simple["extra"] = "this isn't part of the spec!"
self.suspicious()
def test_bad_filter_base(self):
self.simple["filters"] = "foo"
self.broken()
def test_empty_filters(self):
self.simple["filters"] = {}
self.broken()
def test_bad_basic_values(self):
# Basic values can't be boolean, according to the spec.
self.simple["filters"]["literal"] = True
self.broken()
def test_bad_basic_values_in_array(self):
self.simple["filters"]["array"].append(True)
self.broken()
def test_empty_filterobj(self):
self.simple["filters"]["filter_obj"] = {}
self.broken()
def test_extra_filterobj(self):
self.simple["filters"]["filter_obj"]["extra"] = "foo"
self.suspicious()
def test_bad_filterobj_required(self):
self.simple["filters"]["filter_obj"]["required"] = "foo"
self.broken()
def test_bad_filterobj_value(self):
self.simple["filters"]["filter_obj"]["value"] = True
self.broken()
def test_bad_filterobj_value_array(self):
self.simple["filters"]["filter_obj"]["value"] = [True]
self.broken()
def test_filterobj_value_array(self):
self.simple["filters"]["filter_obj"]["value"] = [123, "foo"]
self.analyze()
self.assert_silent()
def test_bad_filterobj_pattern(self):
self.simple["filters"]["filter_obj"]["pattern"] = 123
self.broken()
def test_bad_filterobj_patternFlags(self):
self.simple["filters"]["filter_obj"]["patternFlags"] = "asdf"
self.broken()
def test_bad_filterobj_patternFlags_length(self):
self.simple["filters"]["filter_obj"]["patternFlags"] = "iiiii"
self.broken()
def test_bad_filterobj_regexp(self):
self.simple["filters"]["filter_obj"]["regexp"] = 123
self.broken()
def test_filterobj_optional_elements(self):
self.simple["filters"]["filter_obj"] = {"min": 1}
self.analyze()
self.assert_silent()
self.simple["filters"]["filter_obj"] = {"required": True}
self.analyze()
self.assert_silent()
# Thus, no one field is required.