metadata_validate.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. #!/usr/bin/python
  2. #
  3. # Copyright (C) 2012 The Android Open Source Project
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. """
  18. Usage:
  19. metadata_validate.py <filename.xml>
  20. - validates that the metadata properties defined in filename.xml are
  21. semantically correct.
  22. - does not do any XSD validation, use xmllint for that (in metadata-validate)
  23. Module:
  24. A set of helpful functions for dealing with BeautifulSoup element trees.
  25. Especially the find_* and fully_qualified_name functions.
  26. Dependencies:
  27. BeautifulSoup - an HTML/XML parser available to download from
  28. http://www.crummy.com/software/BeautifulSoup/
  29. """
  30. from bs4 import BeautifulSoup
  31. from bs4 import Tag
  32. import sys
  33. #####################
  34. #####################
  35. def fully_qualified_name(entry):
  36. """
  37. Calculates the fully qualified name for an entry by walking the path
  38. to the root node.
  39. Args:
  40. entry: a BeautifulSoup Tag corresponding to an <entry ...> XML node,
  41. or a <clone ...> XML node.
  42. Raises:
  43. ValueError: if entry does not correspond to one of the above XML nodes
  44. Returns:
  45. A string with the full name, e.g. "android.lens.info.availableApertureSizes"
  46. """
  47. filter_tags = ['namespace', 'section']
  48. parents = [i['name'] for i in entry.parents if i.name in filter_tags]
  49. if entry.name == 'entry':
  50. name = entry['name']
  51. elif entry.name == 'clone':
  52. name = entry['entry'].split(".")[-1] # "a.b.c" => "c"
  53. else:
  54. raise ValueError("Unsupported tag type '%s' for element '%s'" \
  55. %(entry.name, entry))
  56. parents.reverse()
  57. parents.append(name)
  58. fqn = ".".join(parents)
  59. return fqn
  60. def find_parent_by_name(element, names):
  61. """
  62. Find the ancestor for an element whose name matches one of those
  63. in names.
  64. Args:
  65. element: A BeautifulSoup Tag corresponding to an XML node
  66. Returns:
  67. A BeautifulSoup element corresponding to the matched parent, or None.
  68. For example, assuming the following XML structure:
  69. <static>
  70. <anything>
  71. <entry name="Hello" /> # this is in variable 'Hello'
  72. </anything>
  73. </static>
  74. el = find_parent_by_name(Hello, ['static'])
  75. # el is now a value pointing to the '<static>' element
  76. """
  77. matching_parents = [i.name for i in element.parents if i.name in names]
  78. if matching_parents:
  79. return matching_parents[0]
  80. else:
  81. return None
  82. def find_all_child_tags(element, tag):
  83. """
  84. Finds all the children that are a Tag (as opposed to a NavigableString),
  85. with a name of tag. This is useful to filter out the NavigableString out
  86. of the children.
  87. Args:
  88. element: A BeautifulSoup Tag corresponding to an XML node
  89. tag: A string representing the name of the tag
  90. Returns:
  91. A list of Tag instances
  92. For example, given the following XML structure:
  93. <enum> # This is the variable el
  94. Hello world # NavigableString
  95. <value>Apple</value> # this is the variale apple (Tag)
  96. <value>Orange</value> # this is the variable orange (Tag)
  97. Hello world again # NavigableString
  98. </enum>
  99. lst = find_all_child_tags(el, 'value')
  100. # lst is [apple, orange]
  101. """
  102. matching_tags = [i for i in element.children if isinstance(i, Tag) and i.name == tag]
  103. return matching_tags
  104. def find_child_tag(element, tag):
  105. """
  106. Finds the first child that is a Tag with the matching name.
  107. Args:
  108. element: a BeautifulSoup Tag
  109. tag: A String representing the name of the tag
  110. Returns:
  111. An instance of a Tag, or None if there was no matches.
  112. For example, given the following XML structure:
  113. <enum> # This is the variable el
  114. Hello world # NavigableString
  115. <value>Apple</value> # this is the variale apple (Tag)
  116. <value>Orange</value> # this is the variable orange (Tag)
  117. Hello world again # NavigableString
  118. </enum>
  119. res = find_child_tag(el, 'value')
  120. # res is apple
  121. """
  122. matching_tags = find_all_child_tags(element, tag)
  123. if matching_tags:
  124. return matching_tags[0]
  125. else:
  126. return None
  127. def find_kind(element):
  128. """
  129. Finds the kind Tag ancestor for an element.
  130. Args:
  131. element: a BeautifulSoup Tag
  132. Returns:
  133. a BeautifulSoup tag, or None if there was no matches
  134. Remarks:
  135. This function only makes sense to be called for an Entry, Clone, or
  136. InnerNamespace XML types. It will always return 'None' for other nodes.
  137. """
  138. kinds = ['dynamic', 'static', 'controls']
  139. parent_kind = find_parent_by_name(element, kinds)
  140. return parent_kind
  141. def validate_error(msg):
  142. """
  143. Print a validation error to stderr.
  144. Args:
  145. msg: a string you want to be printed
  146. """
  147. print >> sys.stderr, "ERROR: " + msg
  148. def validate_clones(soup):
  149. """
  150. Validate that all <clone> elements point to an existing <entry> element.
  151. Args:
  152. soup - an instance of BeautifulSoup
  153. Returns:
  154. True if the validation succeeds, False otherwise
  155. """
  156. success = True
  157. for clone in soup.find_all("clone"):
  158. clone_entry = clone['entry']
  159. clone_kind = clone['kind']
  160. parent_kind = find_kind(clone)
  161. find_entry = lambda x: x.name == 'entry' \
  162. and find_kind(x) == clone_kind \
  163. and fully_qualified_name(x) == clone_entry
  164. matching_entry = soup.find(find_entry)
  165. if matching_entry is None:
  166. error_msg = ("Did not find corresponding clone entry '%s' " + \
  167. "with kind '%s'") %(clone_entry, clone_kind)
  168. validate_error(error_msg)
  169. success = False
  170. clone_name = fully_qualified_name(clone)
  171. if clone_name != clone_entry:
  172. error_msg = ("Clone entry target '%s' did not match fully qualified " + \
  173. "name '%s'.") %(clone_entry, clone_name)
  174. validate_error(error_msg)
  175. success = False
  176. if matching_entry is not None:
  177. entry_hal_major_version = 3
  178. entry_hal_minor_version = 2
  179. entry_hal_version = matching_entry.get('hal_version')
  180. if entry_hal_version is not None:
  181. entry_hal_major_version = int(entry_hal_version.partition('.')[0])
  182. entry_hal_minor_version = int(entry_hal_version.partition('.')[2])
  183. clone_hal_major_version = entry_hal_major_version
  184. clone_hal_minor_version = entry_hal_minor_version
  185. clone_hal_version = clone.get('hal_version')
  186. if clone_hal_version is not None:
  187. clone_hal_major_version = int(clone_hal_version.partition('.')[0])
  188. clone_hal_minor_version = int(clone_hal_version.partition('.')[2])
  189. if clone_hal_major_version < entry_hal_major_version or \
  190. (clone_hal_major_version == entry_hal_major_version and \
  191. clone_hal_minor_version < entry_hal_minor_version):
  192. error_msg = ("Clone '%s' HAL version '%d.%d' is older than entry target HAL version '%d.%d'" \
  193. % (clone_name, clone_hal_major_version, clone_hal_minor_version, entry_hal_major_version, entry_hal_minor_version))
  194. validate_error(error_msg)
  195. success = False
  196. return success
  197. # All <entry> elements with container=$foo have a <$foo> child
  198. # If type="enum", <enum> tag is present
  199. # In <enum> for all <value id="$x">, $x is numeric
  200. def validate_entries(soup):
  201. """
  202. Validate all <entry> elements with the following rules:
  203. * If there is a container="$foo" attribute, there is a <$foo> child
  204. * If there is a type="enum" attribute, there is an <enum> child
  205. * In the <enum> child, all <value id="$x"> have a numeric $x
  206. Args:
  207. soup - an instance of BeautifulSoup
  208. Returns:
  209. True if the validation succeeds, False otherwise
  210. """
  211. success = True
  212. for entry in soup.find_all("entry"):
  213. entry_container = entry.attrs.get('container')
  214. if entry_container is not None:
  215. container_tag = entry.find(entry_container)
  216. if container_tag is None:
  217. success = False
  218. validate_error(("Entry '%s' in kind '%s' has type '%s' but " + \
  219. "missing child element <%s>") \
  220. %(fully_qualified_name(entry), find_kind(entry), \
  221. entry_container, entry_container))
  222. enum = entry.attrs.get('enum')
  223. if enum and enum == 'true':
  224. if entry.enum is None:
  225. validate_error(("Entry '%s' in kind '%s' is missing enum") \
  226. % (fully_qualified_name(entry), find_kind(entry),
  227. ))
  228. success = False
  229. else:
  230. for value in entry.enum.find_all('value'):
  231. value_id = value.attrs.get('id')
  232. if value_id is not None:
  233. try:
  234. id_int = int(value_id, 0) #autoguess base
  235. except ValueError:
  236. validate_error(("Entry '%s' has id '%s', which is not" + \
  237. " numeric.") \
  238. %(fully_qualified_name(entry), value_id))
  239. success = False
  240. else:
  241. if entry.enum:
  242. validate_error(("Entry '%s' kind '%s' has enum el, but no enum attr") \
  243. % (fully_qualified_name(entry), find_kind(entry),
  244. ))
  245. success = False
  246. deprecated = entry.attrs.get('deprecated')
  247. if deprecated and deprecated == 'true':
  248. if entry.deprecation_description is None:
  249. validate_error(("Entry '%s' in kind '%s' is deprecated, but missing deprecation description") \
  250. % (fully_qualified_name(entry), find_kind(entry),
  251. ))
  252. success = False
  253. else:
  254. if entry.deprecation_description is not None:
  255. validate_error(("Entry '%s' in kind '%s' has deprecation description, but is not deprecated") \
  256. % (fully_qualified_name(entry), find_kind(entry),
  257. ))
  258. success = False
  259. return success
  260. def validate_xml(xml):
  261. """
  262. Validate all XML nodes according to the rules in validate_clones and
  263. validate_entries.
  264. Args:
  265. xml - A string containing a block of XML to validate
  266. Returns:
  267. a BeautifulSoup instance if validation succeeds, None otherwise
  268. """
  269. soup = BeautifulSoup(xml, features='xml')
  270. succ = validate_clones(soup)
  271. succ = validate_entries(soup) and succ
  272. if succ:
  273. return soup
  274. else:
  275. return None
  276. #####################
  277. #####################
  278. if __name__ == "__main__":
  279. if len(sys.argv) <= 1:
  280. print >> sys.stderr, "Usage: %s <filename.xml>" % (sys.argv[0])
  281. sys.exit(0)
  282. file_name = sys.argv[1]
  283. succ = validate_xml(file(file_name).read()) is not None
  284. if succ:
  285. print "%s: SUCCESS! Document validated" %(file_name)
  286. sys.exit(0)
  287. else:
  288. print >> sys.stderr, "%s: ERRORS: Document failed to validate" %(file_name)
  289. sys.exit(1)