# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
import unittest
from cuddlefish.docs.apiparser import parse_hunks, ParseError
tests_path = os.path.abspath(os.path.dirname(__file__))
static_files_path = os.path.join(tests_path, "static-files")
class ParserTests(unittest.TestCase):
def pathname(self, filename):
return os.path.join(static_files_path, "docs", filename)
def parse_text(self, text):
return list(parse_hunks(text))
def parse(self, pathname):
return self.parse_text(open(pathname).read())
def test_parser(self):
parsed = self.parse(self.pathname("APIsample.md"))
#for i,h in enumerate(parsed):
# print i, h
self.assertEqual(parsed[0],
("version", 4))
self.assertEqual(parsed[1],
("markdown", """\
# Title #
Some text here
"""))
self.assertEqual(parsed[2][0], "api-json")
p_test = parsed[2][1]
self.assertEqual(p_test["name"], "test")
self.assertEqual(p_test["type"], "function")
self.assertEqual(p_test["signature"], "test(argOne, argTwo, \
argThree, options)")
self.assertEqual(p_test["description"],
"This is a function which does nothing in \
particular.")
r = p_test["returns"]
self.assertEqual(r["datatype"], "object")
self.assertEqual(r["description"], "")
self.assertEqual(len(r["props"]), 2)
self.assertEqual(r["props"][0]["datatype"], "string")
self.assertEqual(r["props"][0]["description"], "First string")
self.assertEqual(r["props"][1]["datatype"], "url")
self.assertEqual(r["props"][1]["description"], "First URL")
self.assertEqual(p_test["params"][0],
{"name": "argOne",
"required": True,
"datatype": "string",
"description": "This is the first argument.",
"line_number": 15,
})
self.assertEqual(p_test["params"][1],
{"name": "argTwo",
"required": False,
"datatype": "bool",
"description": "This is the second argument.",
"line_number": 16,
})
self.assertEqual(p_test["params"][2],
{"name": "argThree",
"required": False,
"default": "default",
"datatype": "uri",
"line_number": 17,
"description": """\
This is the third and final argument. And this is
a test of the ability to do multiple lines of
text.""",
})
p3 = p_test["params"][3]
self.assertEqual(p3["name"], "options")
self.assertEqual(p3["required"], False)
self.failIf("type" in p3)
self.assertEqual(p3["description"], "Options Bag")
self.assertEqual(p3["props"][0],
{"name": "style",
"required": False,
"datatype": "string",
"description": "Some style information.",
"line_number": 22,
})
self.assertEqual(p3["props"][1],
{"name": "secondToLastOption",
"required": False,
"default": "True",
"datatype": "bool",
"description": "The last property.",
"line_number": 23,
})
self.assertEqual(p3["props"][2]["name"], "lastOption")
self.assertEqual(p3["props"][2]["required"], False)
self.assertEqual(p3["props"][2]["datatype"], "uri")
self.assertEqual(p3["props"][2]["description"], """\
And this time we have
A multiline description
Written as haiku""")
self.assertEqual(parsed[3][0], "markdown")
self.assertEqual(parsed[3][1], "\n\nThis text appears between the \
API blocks.\n\n")
self.assertEqual(parsed[4][0], "api-json")
p_test = parsed[4][1]
expected = {'line_number': 32,
'name': 'append',
'params': [{'props':[{'line_number': 37,
'required': False,
'datatype': 'uri',
'name': 'icon',
'description': 'The HREF of an icon to show as the \
method of accessing your features slideBar'},
{'line_number': 38,
'required': False,
'datatype': 'string/xml',
'name': 'html',
'description': 'The content of the feature, either \
as an HTML string,\nor an E4X document fragment.'},
{'line_number': 41,
'required': False,
'datatype': 'uri',
'name': 'url',
'description': 'The url to load into the content area \
of the feature'},
{'line_number': 42,
'required': False,
'datatype': 'int',
'name': 'width',
'description': 'Width of the content area and the \
selected slide size'},
{'line_number': 43,
'required': False,
'datatype': 'bool',
'name': 'persist',
'description': 'Default slide behavior when being \
selected as follows:\nIf true: blah; If false: double blah.'},
{'line_number': 46,
'required': False,
'datatype': 'bool',
'name': 'autoReload',
'description': 'Automatically reload content on \
select'},
{'line_number': 47,
'required': False,
'datatype': 'function',
'name': 'onClick',
'description': 'Callback when the icon is \
clicked'},
{'line_number': 48,
'required': False,
'datatype': 'function',
'name': 'onSelect',
'description': 'Callback when the feature is selected'},
{'line_number': 49,
'required': False,
'datatype': 'function',
'name': 'onReady',
'description':
'Callback when featured is loaded'}],
'line_number': 35,
'required': True,
'name': 'options',
'description': 'Pass in all of your options here.'}],
'signature': 'append(options)',
'type': 'function',
'description': 'This is a list of options to specify modifications to your \
slideBar instance.'}
self.assertEqual(p_test, expected)
self.assertEqual(parsed[6][0], "api-json")
p_test = parsed[6][1]
self.assertEqual(p_test["name"], "cool-func.dot")
self.assertEqual(p_test["signature"], "cool-func.dot(howMuch, double, \
options, onemore, options2)")
self.assertEqual(p_test["returns"]["description"],
"""\
A value telling you just how cool you are.
A boa-constructor!
This description can go on for a while, and can even contain
some **realy** fancy things. Like `code`, or even
~~~~{.javascript}
// Some code!
~~~~""")
self.assertEqual(p_test["params"][2]["props"][0],
{"name": "callback",
"required": True,
"datatype": "function",
"line_number": 67,
"description": "The callback",
})
self.assertEqual(p_test["params"][2]["props"][1],
{"name": "random",
"required": False,
"datatype": "bool",
"line_number": 68,
"description": "Do something random?",
})
p_test = parsed[8][1]
self.assertEqual(p_test["signature"],"random()")
# tests for classes
#1) empty class
p_test = parsed[10][1]
self.assertEqual(len(p_test), 4)
self.assertEqual(p_test["name"], "empty-class")
self.assertEqual(p_test["description"], "This class contains nothing.")
self.assertEqual(p_test["type"], "class")
# 2) class with just one ctor
p_test = parsed[12][1]
self.assertEqual(len(p_test), 5)
self.assertEqual(p_test["name"], "only-one-ctor")
self.assertEqual(p_test["description"], "This class contains only \
one constructor.")
self.assertEqual(p_test["type"], "class")
constructors = p_test["constructors"]
self.assertEqual(len(constructors), 1)
self._test_class_constructor(constructors[0], "one-constructor")
# 3) class with 2 ctors
p_test = parsed[14][1]
self.assertEqual(len(p_test), 5)
self.assertEqual(p_test["name"], "two-ctors")
self.assertEqual(p_test["description"], "This class contains two \
constructors.")
self.assertEqual(p_test["type"], "class")
constructors = p_test["constructors"]
self.assertEqual(len(constructors), 2)
self._test_class_constructor(constructors[0], "one-constructor")
self._test_class_constructor(constructors[1], "another-constructor")
# 4) class with ctor + method
p_test = parsed[16][1]
self.assertEqual(len(p_test), 6)
self.assertEqual(p_test["name"], "ctor-and-method")
self.assertEqual(p_test["description"], "This class contains one \
constructor and one method.")
self.assertEqual(p_test["type"], "class")
constructors = p_test["constructors"]
self.assertEqual(len(constructors), 1)
self._test_class_constructor(constructors[0], "one-constructor")
methods = p_test["methods"]
self.assertEqual(len(methods), 1)
self._test_class_method(methods[0])
# 5) class with ctor + method + property
p_test = parsed[18][1]
self.assertEqual(len(p_test), 8)
self.assertEqual(p_test["name"], "ctor-method-prop-event")
self.assertEqual(p_test["description"], "This class contains one \
constructor, one method, one property and an event.")
self.assertEqual(p_test["type"], "class")
constructors = p_test["constructors"]
self.assertEqual(len(constructors), 1)
self._test_class_constructor(constructors[0], "one-constructor")
methods = p_test["methods"]
self.assertEqual(len(methods), 1)
self._test_class_method(methods[0])
properties = p_test["properties"]
self.assertEqual(len(properties), 1)
self._test_class_property(properties[0])
events = p_test["events"]
self.assertEqual(len(events), 1)
self._test_class_event(events[0])
self.assertEqual(parsed[-1][0], "markdown")
self.assertEqual(parsed[-1][1], "\n\nSome more text here, \
at the end of the file.\n\n")
def _test_class_constructor(self, constructor, name):
self.assertEqual(constructor["type"], "constructor")
self.assertEqual(constructor["name"], name)
params = constructor["params"]
self.assertEqual(len(params), 1)
self.assertEqual(params[0]["name"], "options")
self.assertEqual(params[0]["description"], "An object-bag of goodies.")
def _test_class_method(self, method):
self.assertEqual(method["type"], "method")
self.assertEqual(method["name"], "a-method")
self.assertEqual(method["description"], "Does things.")
params = method["params"]
self.assertEqual(len(params), 1)
self.assertEqual(params[0]["name"], "options")
self.assertEqual(params[0]["description"], "An argument.")
def _test_class_property(self, prop):
self.assertEqual(prop["type"], "property")
self.assertEqual(prop["name"], "a-property")
self.assertEqual(prop["description"], "Represents stuff.")
self.assertEqual(prop["datatype"], "bool")
def _test_class_event(self, event):
self.assertEqual(event["type"], "event")
self.assertEqual(event["name"], "message")
self.assertEqual(event["description"], "Event emitted when the \
content script sends a message to the add-on.")
arguments = event["arguments"]
self.assertEqual(len(arguments), 1)
argument = arguments[0]
self.assertEqual(argument["datatype"], "JSON")
self.assertEqual(argument["description"], "The message itself as a \
JSON-serialized object.")
def test_missing_return_propname(self):
md = '''\
@method
This is a function which does nothing in particular.
@returns {object}
@prop {string} First string, but the property name is missing
@prop {url} First URL, same problem
@param argOne {string} This is the first argument.
'''
self.assertRaises(ParseError, self.parse_text, md)
def test_missing_return_proptype(self):
md = '''\
@method
This is a function which does nothing in particular.
@returns {object}
@prop untyped It is an error to omit the type of a return property.
@param argOne {string} This is the first argument.
@param [argTwo=True] {bool} This is the second argument.
'''
self.assertRaises(ParseError, self.parse_text, md)
def test_return_propnames(self):
md = '''\
@method
This is a function which does nothing in particular.
@returns {object}
@prop firststring {string} First string.
@prop [firsturl] {url} First URL, not always provided.
@param argOne {string} This is the first argument.
@param [argTwo=True] {bool} This is the second argument.
'''
parsed = self.parse_text(md)
r = parsed[1][1]["returns"]
self.assertEqual(r["props"][0]["name"], "firststring")
self.assertEqual(r["props"][0],
{"name": "firststring",
"datatype": "string",
"description": "First string.",
"required": True,
"line_number": 5, # 1-indexed
})
self.assertEqual(r["props"][1],
{"name": "firsturl",
"datatype": "url",
"description": "First URL, not always provided.",
"required": False,
"line_number": 6,
})
def test_return_description_1(self):
md = '''\
@method
This is a function which does nothing in particular.
@returns {object} A one-line description.
@prop firststring {string} First string.
@prop [firsturl] {url} First URL, not always provided.
@param argOne {string} This is the first argument.
@param [argTwo=True] {bool} This is the second argument.
'''
parsed = self.parse_text(md)
r = parsed[1][1]["returns"]
self.assertEqual(r["description"], "A one-line description.")
def test_return_description_2(self):
md = '''\
@method
This is a function which does nothing in particular.
@returns {object} A six-line description
which is consistently indented by two spaces
except for this line
and preserves the following empty line
from which a two-space indentation will be removed.
@prop firststring {string} First string.
@prop [firsturl] {url} First URL, not always provided.
@param argOne {string} This is the first argument.
@param [argTwo=True] {bool} This is the second argument.
'''
parsed = self.parse_text(md)
r = parsed[1][1]["returns"]
self.assertEqual(r["description"],
"A six-line description\n"
"which is consistently indented by two spaces\n"
" except for this line\n"
"and preserves the following empty line\n"
"\n"
"from which a two-space indentation will be removed.")
def test_return_description_3(self):
md = '''\
@method
This is a function which does nothing in particular.
@returns A one-line untyped description.
@param argOne {string} This is the first argument.
@param [argTwo=True] {bool} This is the second argument.
'''
parsed = self.parse_text(md)
r = parsed[1][1]["returns"]
self.assertEqual(r["description"], "A one-line untyped description.")
# if the return value was supposed to be an array, the correct syntax
# would not have any @prop tags:
# @returns {array}
# Array consists of two elements, a string and a url...
def test_return_array(self):
md = '''\
@method
This is a function which returns an array.
@returns {array}
Array consists of two elements, a string and a url.
@param argOne {string} This is the first argument.
@param [argTwo=True] {bool} This is the second argument.
'''
parsed = self.parse_text(md)
r = parsed[1][1]["returns"]
self.assertEqual(r["description"],
"Array consists of two elements, a string and a url.")
def test_bad_default_on_required_parameter(self):
md = '''\
@method
This is a function which does nothing in particular.
@returns something
@param argOne=ILLEGAL {string} Mandatory parameters do not take defaults.
@param [argTwo=Chicago] {string} This is the second argument.
'''
self.assertRaises(ParseError, self.parse_text, md)
def test_missing_apitype(self):
md = '''\
Sorry, you must have a @method or something before the description.
Putting it after the description is not good enough
@method
@returns something
'''
self.assertRaises(ParseError, self.parse_text, md)
def test_missing_param_propname(self):
md = '''\
@method
This is a function which does nothing in particular.
@param p1 {object} This is a parameter.
@prop {string} Oops, props must have a name.
'''
self.assertRaises(ParseError, self.parse_text, md)
def test_missing_param_proptype(self):
md = '''\
@method
This is a function which does nothing in particular.
@param p1 {object} This is a parameter.
@prop name Oops, props must have a type.
'''
self.assertRaises(ParseError, self.parse_text, md)
def test_property(self):
md = '''\
@property {foo}
An object property named test of type foo.
'''
parsed = self.parse_text(md)
self.assertEqual(parsed[1][0], 'api-json')
actual_api_json_obj = parsed[1][1]
expected_api_json_obj = {
'line_number': 1,
'datatype': 'foo',
'type': 'property',
'name': 'test',
'description': "An object property named test of type foo."
}
self.assertEqual(actual_api_json_obj, expected_api_json_obj)
def test_property_no_type(self):
md = '''\
@property
This property needs to specify a type!
'''
self.assertRaises(ParseError, self.parse_text, md)
def test_missing_api_closing_tag(self):
md = '''\
@class
This is a class with a missing closing tag.
'''
self.assertRaises(ParseError, self.parse_text, md)
if __name__ == "__main__":
unittest.main()