build_log.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. #!/usr/bin/env python
  2. # Copyright 2008 Rene Rivera
  3. # Distributed under the Boost Software License, Version 1.0.
  4. # (See accompanying file LICENSE_1_0.txt or http://www.boost.org/LICENSE_1_0.txt)
  5. import re
  6. import optparse
  7. import time
  8. import xml.dom.minidom
  9. import xml.dom.pulldom
  10. from xml.sax.saxutils import unescape, escape
  11. import os.path
  12. from pprint import pprint
  13. from __builtin__ import exit
  14. class BuildOutputXMLParsing(object):
  15. '''
  16. XML parsing utilities for dealing with the Boost Build output
  17. XML format.
  18. '''
  19. def get_child_data( self, root, tag = None, id = None, name = None, strip = False, default = None ):
  20. return self.get_data(self.get_child(root,tag=tag,id=id,name=name),strip=strip,default=default)
  21. def get_data( self, node, strip = False, default = None ):
  22. data = None
  23. if node:
  24. data_node = None
  25. if not data_node:
  26. data_node = self.get_child(node,tag='#text')
  27. if not data_node:
  28. data_node = self.get_child(node,tag='#cdata-section')
  29. data = ""
  30. while data_node:
  31. data += data_node.data
  32. data_node = data_node.nextSibling
  33. if data_node:
  34. if data_node.nodeName != '#text' \
  35. and data_node.nodeName != '#cdata-section':
  36. data_node = None
  37. if not data:
  38. data = default
  39. else:
  40. if strip:
  41. data = data.strip()
  42. return data
  43. def get_child( self, root, tag = None, id = None, name = None, type = None ):
  44. return self.get_sibling(root.firstChild,tag=tag,id=id,name=name,type=type)
  45. def get_sibling( self, sibling, tag = None, id = None, name = None, type = None ):
  46. n = sibling
  47. while n:
  48. found = True
  49. if type and found:
  50. found = found and type == n.nodeType
  51. if tag and found:
  52. found = found and tag == n.nodeName
  53. if (id or name) and found:
  54. found = found and n.nodeType == xml.dom.Node.ELEMENT_NODE
  55. if id and found:
  56. if n.hasAttribute('id'):
  57. found = found and n.getAttribute('id') == id
  58. else:
  59. found = found and n.hasAttribute('id') and n.getAttribute('id') == id
  60. if name and found:
  61. found = found and n.hasAttribute('name') and n.getAttribute('name') == name
  62. if found:
  63. return n
  64. n = n.nextSibling
  65. return None
  66. class BuildOutputProcessor(BuildOutputXMLParsing):
  67. def __init__(self, inputs):
  68. self.test = {}
  69. self.target_to_test = {}
  70. self.target = {}
  71. self.parent = {}
  72. self.timestamps = []
  73. for input in inputs:
  74. self.add_input(input)
  75. def add_input(self, input):
  76. '''
  77. Add a single build XML output file to our data.
  78. '''
  79. events = xml.dom.pulldom.parse(input)
  80. context = []
  81. for (event,node) in events:
  82. if event == xml.dom.pulldom.START_ELEMENT:
  83. context.append(node)
  84. if node.nodeType == xml.dom.Node.ELEMENT_NODE:
  85. x_f = self.x_name_(*context)
  86. if x_f:
  87. events.expandNode(node)
  88. # expanding eats the end element, hence walking us out one level
  89. context.pop()
  90. # call handler
  91. (x_f[1])(node)
  92. elif event == xml.dom.pulldom.END_ELEMENT:
  93. context.pop()
  94. def x_name_(self, *context, **kwargs):
  95. node = None
  96. names = [ ]
  97. for c in context:
  98. if c:
  99. if not isinstance(c,xml.dom.Node):
  100. suffix = '_'+c.replace('-','_').replace('#','_')
  101. else:
  102. suffix = '_'+c.nodeName.replace('-','_').replace('#','_')
  103. node = c
  104. names.append('x')
  105. names = map(lambda x: x+suffix,names)
  106. if node:
  107. for name in names:
  108. if hasattr(self,name):
  109. return (name,getattr(self,name))
  110. return None
  111. def x_build_test(self, node):
  112. '''
  113. Records the initial test information that will eventually
  114. get expanded as we process the rest of the results.
  115. '''
  116. test_node = node
  117. test_name = test_node.getAttribute('name')
  118. test_target = self.get_child_data(test_node,tag='target',strip=True)
  119. ## print ">>> %s %s" %(test_name,test_target)
  120. self.test[test_name] = {
  121. 'library' : "/".join(test_name.split('/')[0:-1]),
  122. 'test-name' : test_name.split('/')[-1],
  123. 'test-type' : test_node.getAttribute('type').lower(),
  124. 'test-program' : self.get_child_data(test_node,tag='source',strip=True),
  125. 'target' : test_target,
  126. 'info' : self.get_child_data(test_node,tag='info',strip=True),
  127. 'dependencies' : [],
  128. 'actions' : [],
  129. }
  130. # Add a lookup for the test given the test target.
  131. self.target_to_test[self.test[test_name]['target']] = test_name
  132. return None
  133. def x_build_targets_target( self, node ):
  134. '''
  135. Process the target dependency DAG into an ancestry tree so we can look up
  136. which top-level library and test targets specific build actions correspond to.
  137. '''
  138. target_node = node
  139. name = self.get_child_data(target_node,tag='name',strip=True)
  140. path = self.get_child_data(target_node,tag='path',strip=True)
  141. jam_target = self.get_child_data(target_node,tag='jam-target',strip=True)
  142. #~ Map for jam targets to virtual targets.
  143. self.target[jam_target] = {
  144. 'name' : name,
  145. 'path' : path
  146. }
  147. #~ Create the ancestry.
  148. dep_node = self.get_child(self.get_child(target_node,tag='dependencies'),tag='dependency')
  149. while dep_node:
  150. child = self.get_data(dep_node,strip=True)
  151. child_jam_target = '<p%s>%s' % (path,child.split('//',1)[1])
  152. self.parent[child_jam_target] = jam_target
  153. dep_node = self.get_sibling(dep_node.nextSibling,tag='dependency')
  154. return None
  155. def x_build_action( self, node ):
  156. '''
  157. Given a build action log, process into the corresponding test log and
  158. specific test log sub-part.
  159. '''
  160. action_node = node
  161. name = self.get_child(action_node,tag='name')
  162. if name:
  163. name = self.get_data(name)
  164. #~ Based on the action, we decide what sub-section the log
  165. #~ should go into.
  166. action_type = None
  167. if re.match('[^%]+%[^.]+[.](compile)',name):
  168. action_type = 'compile'
  169. elif re.match('[^%]+%[^.]+[.](link|archive)',name):
  170. action_type = 'link'
  171. elif re.match('[^%]+%testing[.](capture-output)',name):
  172. action_type = 'run'
  173. elif re.match('[^%]+%testing[.](expect-failure|expect-success)',name):
  174. action_type = 'result'
  175. else:
  176. # TODO: Enable to see what other actions can be included in the test results.
  177. # action_type = None
  178. action_type = 'other'
  179. #~ print "+ [%s] %s %s :: %s" %(action_type,name,'','')
  180. if action_type:
  181. #~ Get the corresponding test.
  182. (target,test) = self.get_test(action_node,type=action_type)
  183. #~ Skip action that have no corresponding test as they are
  184. #~ regular build actions and don't need to show up in the
  185. #~ regression results.
  186. if not test:
  187. ##print "??? [%s] %s %s :: %s" %(action_type,name,target,test)
  188. return None
  189. ##print "+++ [%s] %s %s :: %s" %(action_type,name,target,test)
  190. #~ Collect some basic info about the action.
  191. action = {
  192. 'command' : self.get_action_command(action_node,action_type),
  193. 'output' : self.get_action_output(action_node,action_type),
  194. 'info' : self.get_action_info(action_node,action_type)
  195. }
  196. #~ For the test result status we find the appropriate node
  197. #~ based on the type of test. Then adjust the result status
  198. #~ accordingly. This makes the result status reflect the
  199. #~ expectation as the result pages post processing does not
  200. #~ account for this inversion.
  201. action['type'] = action_type
  202. if action_type == 'result':
  203. if re.match(r'^compile',test['test-type']):
  204. action['type'] = 'compile'
  205. elif re.match(r'^link',test['test-type']):
  206. action['type'] = 'link'
  207. elif re.match(r'^run',test['test-type']):
  208. action['type'] = 'run'
  209. #~ The result sub-part we will add this result to.
  210. if action_node.getAttribute('status') == '0':
  211. action['result'] = 'succeed'
  212. else:
  213. action['result'] = 'fail'
  214. # Add the action to the test.
  215. test['actions'].append(action)
  216. # Set the test result if this is the result action for the test.
  217. if action_type == 'result':
  218. test['result'] = action['result']
  219. return None
  220. def x_build_timestamp( self, node ):
  221. '''
  222. The time-stamp goes to the corresponding attribute in the result.
  223. '''
  224. self.timestamps.append(self.get_data(node).strip())
  225. return None
  226. def get_test( self, node, type = None ):
  227. '''
  228. Find the test corresponding to an action. For testing targets these
  229. are the ones pre-declared in the --dump-test option. For libraries
  230. we create a dummy test as needed.
  231. '''
  232. jam_target = self.get_child_data(node,tag='jam-target')
  233. base = self.target[jam_target]['name']
  234. target = jam_target
  235. while target in self.parent:
  236. target = self.parent[target]
  237. #~ print "--- TEST: %s ==> %s" %(jam_target,target)
  238. #~ main-target-type is a precise indicator of what the build target is
  239. #~ originally meant to be.
  240. #main_type = self.get_child_data(self.get_child(node,tag='properties'),
  241. # name='main-target-type',strip=True)
  242. main_type = None
  243. if main_type == 'LIB' and type:
  244. lib = self.target[target]['name']
  245. if not lib in self.test:
  246. self.test[lib] = {
  247. 'library' : re.search(r'libs/([^/]+)',lib).group(1),
  248. 'test-name' : os.path.basename(lib),
  249. 'test-type' : 'lib',
  250. 'test-program' : os.path.basename(lib),
  251. 'target' : lib
  252. }
  253. test = self.test[lib]
  254. else:
  255. target_name_ = self.target[target]['name']
  256. if self.target_to_test.has_key(target_name_):
  257. test = self.test[self.target_to_test[target_name_]]
  258. else:
  259. test = None
  260. return (base,test)
  261. #~ The command executed for the action. For run actions we omit the command
  262. #~ as it's just noise.
  263. def get_action_command( self, action_node, action_type ):
  264. if action_type != 'run':
  265. return self.get_child_data(action_node,tag='command')
  266. else:
  267. return ''
  268. #~ The command output.
  269. def get_action_output( self, action_node, action_type ):
  270. return self.get_child_data(action_node,tag='output',default='')
  271. #~ Some basic info about the action.
  272. def get_action_info( self, action_node, action_type ):
  273. info = {}
  274. #~ The jam action and target.
  275. info['name'] = self.get_child_data(action_node,tag='name')
  276. info['path'] = self.get_child_data(action_node,tag='path')
  277. #~ The timing of the action.
  278. info['time-start'] = action_node.getAttribute('start')
  279. info['time-end'] = action_node.getAttribute('end')
  280. info['time-user'] = action_node.getAttribute('user')
  281. info['time-system'] = action_node.getAttribute('system')
  282. #~ Testing properties.
  283. test_info_prop = self.get_child_data(self.get_child(action_node,tag='properties'),name='test-info')
  284. info['always_show_run_output'] = test_info_prop == 'always_show_run_output'
  285. #~ And for compiles some context that may be hidden if using response files.
  286. if action_type == 'compile':
  287. info['define'] = []
  288. define = self.get_child(self.get_child(action_node,tag='properties'),name='define')
  289. while define:
  290. info['define'].append(self.get_data(define,strip=True))
  291. define = self.get_sibling(define.nextSibling,name='define')
  292. return info
  293. class BuildConsoleSummaryReport(object):
  294. HEADER = '\033[35m\033[1m'
  295. INFO = '\033[34m'
  296. OK = '\033[32m'
  297. WARNING = '\033[33m'
  298. FAIL = '\033[31m'
  299. ENDC = '\033[0m'
  300. def __init__(self, bop, opt):
  301. self.bop = bop
  302. def generate(self):
  303. self.summary_info = {
  304. 'total' : 0,
  305. 'success' : 0,
  306. 'failed' : [],
  307. }
  308. self.header_print("======================================================================")
  309. self.print_test_log()
  310. self.print_summary()
  311. self.header_print("======================================================================")
  312. @property
  313. def failed(self):
  314. return len(self.summary_info['failed']) > 0
  315. def print_test_log(self):
  316. self.header_print("Tests run..")
  317. self.header_print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
  318. for k in sorted(self.bop.test.keys()):
  319. test = self.bop.test[k]
  320. if len(test['actions']) > 0:
  321. self.summary_info['total'] += 1
  322. ##print ">>>> {0}".format(test['test-name'])
  323. if 'result' in test:
  324. succeed = test['result'] == 'succeed'
  325. else:
  326. succeed = test['actions'][-1]['result'] == 'succeed'
  327. if succeed:
  328. self.summary_info['success'] += 1
  329. else:
  330. self.summary_info['failed'].append(test)
  331. if succeed:
  332. self.ok_print("[PASS] {0}",k)
  333. else:
  334. self.fail_print("[FAIL] {0}",k)
  335. for action in test['actions']:
  336. self.print_action(succeed, action)
  337. def print_action(self, test_succeed, action):
  338. '''
  339. Print the detailed info of failed or always print tests.
  340. '''
  341. #self.info_print(">>> {0}",action.keys())
  342. if not test_succeed or action['info']['always_show_run_output']:
  343. output = action['output'].strip()
  344. if output != "":
  345. p = self.fail_print if action['result'] == 'fail' else self.p_print
  346. self.info_print("")
  347. self.info_print("({0}) {1}",action['info']['name'],action['info']['path'])
  348. p("")
  349. p("{0}",action['command'].strip())
  350. p("")
  351. for line in output.splitlines():
  352. p("{0}",line.encode('utf-8'))
  353. def print_summary(self):
  354. self.header_print("")
  355. self.header_print("Testing summary..")
  356. self.header_print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
  357. self.p_print("Total: {0}",self.summary_info['total'])
  358. self.p_print("Success: {0}",self.summary_info['success'])
  359. if self.failed:
  360. self.fail_print("Failed: {0}",len(self.summary_info['failed']))
  361. for test in self.summary_info['failed']:
  362. self.fail_print(" {0}/{1}",test['library'],test['test-name'])
  363. def p_print(self, format, *args, **kargs):
  364. print format.format(*args,**kargs)
  365. def info_print(self, format, *args, **kargs):
  366. print self.INFO+format.format(*args,**kargs)+self.ENDC
  367. def header_print(self, format, *args, **kargs):
  368. print self.HEADER+format.format(*args,**kargs)+self.ENDC
  369. def ok_print(self, format, *args, **kargs):
  370. print self.OK+format.format(*args,**kargs)+self.ENDC
  371. def warn_print(self, format, *args, **kargs):
  372. print self.WARNING+format.format(*args,**kargs)+self.ENDC
  373. def fail_print(self, format, *args, **kargs):
  374. print self.FAIL+format.format(*args,**kargs)+self.ENDC
  375. class Main(object):
  376. def __init__(self,args=None):
  377. op = optparse.OptionParser(
  378. usage="%prog [options] input+")
  379. op.add_option( '--output',
  380. help="type of output to generate" )
  381. ( opt, inputs ) = op.parse_args(args)
  382. bop = BuildOutputProcessor(inputs)
  383. output = None
  384. if opt.output == 'console':
  385. output = BuildConsoleSummaryReport(bop, opt)
  386. if output:
  387. output.generate()
  388. self.failed = output.failed
  389. if __name__ == '__main__':
  390. m = Main()
  391. if m.failed:
  392. exit(-1)