implementing request: reasons of failure should be displayed in non header cells

- reasons are retrieved from the database and  displayed in cells only when
  either columns or rows are grouped by status (because of performance reasons)
- reasons are displayed in cell below GOOD / TOTAL ratio and only if GOOD is
  less then TOTAL i.e.  when some problems occured

From: Vladimir Samarskiy <vsamarsk@google.com>
Signed-off-by: Martin Bligh <mbligh@google.com>



git-svn-id: http://test.kernel.org/svn/autotest/trunk@1441 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/tko/compose_query.cgi b/tko/compose_query.cgi
index 8b66145..d609fa2 100755
--- a/tko/compose_query.cgi
+++ b/tko/compose_query.cgi
@@ -238,7 +238,21 @@
 			return [[display.box(msg)]]
 
 	try:
-		test_data = frontend.get_matrix_data(db_obj, column, row, where)
+		## Unfortunately, we can not request reasons of failure always
+		## because it may result in an inflated size of data transfer
+		## (at the moment we fetch 500 bytes of reason descriptions into
+		## each cell )
+		## If 'status' in [row,column] then either width or height
+		## of the table <=7, hence table is not really 2D, and
+		## query_reason is relatively save.
+		## At the same time view when either rows or columns grouped
+		## by status is when users need reasons of failures the most.
+		
+		## TO DO: implement [Show/Hide reasons] button or link in
+		## all views and make thorough performance testing 
+		test_data = frontend.get_matrix_data(db_obj, column, row, where,
+				query_reasons = ('status' in [row,column])
+				)
 	except db.MySQLTooManyRows, error:
 		return [[display.box(str(error))]]			
 	
@@ -280,7 +294,7 @@
 			if x==datetime.datetime(1970,1,1): x = None
 			if y==datetime.datetime(1970,1,1): y = None
 			try:
-				box_data = test_data.data[x][y].status_count
+				box_data = test_data.data[x][y]
 			except:
 				cur_row.append(display.box(None, None))
 				continue
diff --git a/tko/display.py b/tko/display.py
index 6458e07..78688d6 100755
--- a/tko/display.py
+++ b/tko/display.py
@@ -147,19 +147,29 @@
 	return shade
 
 
-def status_html(db, status_count, shade):
+def status_html(db, box_data, shade):
 	"""
 	status_count: dict mapping from status (integer key) to count
 	eg. { 'GOOD' : 4, 'FAIL' : 1 }
 	"""
+	status_count = box_data.status_count
 	if 6 in status_count.keys():
 		html = "%d&nbsp;/&nbsp;%d " \
 			%(status_count[6],sum(status_count.values()))
 	else:
 		html = "%d&nbsp;/&nbsp;%d " % \
 			(0, sum(status_count.values()))
-	tooltip = ""
 
+	if box_data.reasons_list:
+		box_data.reasons_list.sort()
+		for reason in box_data.reasons_list:
+			reason = reason.replace('<br>','\n')
+			reason = reason.replace('<','[').replace('>',']')
+			reason = reason.replace('|','\n').replace('&',' AND ')
+			reason = reason.replace('\n','<br>')
+			html += '<br>' + reason
+
+	tooltip = ""
 	for status in sorted(status_count.keys(), reverse = True):
 		status_word = db.status_word[status]
 		tooltip += "%d %s " % (status_count[status], status_word)
@@ -182,20 +192,22 @@
 	return status_precounted_box(db, status_count, link)
 
 
-def status_precounted_box(db, status_count, link = None):
+def status_precounted_box(db, box_data, link = None):
 	"""
 	Display a ratio of total number of GOOD tests
 	to total number of all tests in the group of tests.
 	More info (e.g. 10 GOOD, 2 WARN, 3 FAIL) is in tooltips
-	"""		
+	"""
+	status_count = box_data.status_count
 	if not status_count:
 		return box(None, None)
 	
 	shade = shade_from_status_count(status_count)	
-	html,tooltip = status_html(db, status_count, shade)
+	html,tooltip = status_html(db, box_data, shade)
 	precounted_box = box(html, shade, False, link, tooltip)
 	return precounted_box
 
+
 def print_table(matrix):
 	"""
 	matrix: list of lists of boxes, giving a matrix of data
diff --git a/tko/frontend.py b/tko/frontend.py
index 0861bcb..d496fe9 100755
--- a/tko/frontend.py
+++ b/tko/frontend.py
@@ -20,11 +20,12 @@
 	def __init__(self):
 		# Count is a dictionary: status -> count of tests with status
 		self.status_count = {}
+		self.reasons_list = []
 		self.job_tag = None
 		self.job_tag_count = 0
 
 
-	def add(self, status, count, job_tags):
+	def add(self, status, count, job_tags, reasons = None):
 		assert count > 0
 
 		self.job_tag = job_tags
@@ -33,21 +34,31 @@
 			self.job_tag = None
 		
 		self.status_count[status] = count
+		### status == 6 means 'GOOD'
+		if status != 6:
+			## None implies sorting problems and extra CRs in a cell
+			if reasons:
+				self.reasons_list.append(reasons)
 
 
 class status_data:
-	def __init__(self, sql_rows, x_field, y_field):
+	def __init__(self, sql_rows, x_field, y_field, query_reasons = False):
 		data = {}
 		y_values = set()
 
 		# Walk through the query, filing all results by x, y info
-		for (x, y, status, count, job_tags) in sql_rows:
+		for row in sql_rows:
+			if query_reasons:
+				(x,y, status, count, job_tags, reasons) = row
+			else:
+				(x,y, status, count, job_tags) = row
+				reasons = None
 			if not data.has_key(x):
 				data[x] = {}
 			if not data[x].has_key(y):
 				y_values.add(y)
 				data[x][y] = status_cell()
-			data[x][y].add(status, count, job_tags)
+			data[x][y].add(status, count, job_tags, reasons)
 
 		# 2-d hash of data - [x-value][y-value]
 		self.data = data
@@ -61,18 +72,24 @@
 			raise db.MySQLTooManyRows(msg)
 			
 
-def get_matrix_data(db_obj, x_axis, y_axis, where = None):
+def get_matrix_data(db_obj, x_axis, y_axis, where = None,
+		    query_reasons = False):
 	# Searches on the test_view table - x_axis and y_axis must both be
 	# column names in that table.
 	x_field = test_view_field_dict[x_axis]
 	y_field = test_view_field_dict[y_axis]
-	fields = ( '%s, %s, status, COUNT(status), ' +
-		  'LEFT(GROUP_CONCAT(job_tag), 100)' # limit what's returned
-		 ) % (x_field, y_field)
+	query_fields_list = [x_field, y_field, 'status','COUNT(status)']
+	query_fields_list.append("LEFT(GROUP_CONCAT(job_tag),100)")
+	if query_reasons:
+		query_fields_list.append(
+			"LEFT(GROUP_CONCAT(DISTINCT reason SEPARATOR '|'),500)"
+			)
+	fields = ','.join(query_fields_list)
+
 	group_by = '%s, %s, status' % (x_field, y_field)
 	rows = db_obj.select(fields, 'test_view',
 			where=where, group_by=group_by, max_rows = MAX_RECORDS)
-	return status_data(rows, x_field, y_field)
+	return status_data(rows, x_field, y_field, query_reasons)
 
 
 # Dictionary used simply for fast lookups from short reference names for users
@@ -91,6 +108,7 @@
 	'time_daily'    : 'DATE(test_finished_time)'
 }
 
+
 def smart_sort(list, field):
 	if field == 'kernel_printable':
 		def kernel_encode(kernel):
@@ -130,7 +148,8 @@
 
 	def tests(self, where = {}):
 		values = [self.name]
-		sql = 't inner join machines m on m.machine_idx=t.machine_idx where m.machine_group=%s'
+		sql = 't inner join machines m on m.machine_idx=t.machine_idx'
+		sql += ' where m.machine_group=%s'
 		for key in where.keys():
 			sql += ' and %s=%%s' % key
 			values.append(where[key])
@@ -178,7 +197,8 @@
 		fields = ['test_idx', 'job_idx', 'test', 'subdir', 
 			  'kernel_idx', 'status', 'reason', 'machine_idx']
 		tests = []
-		for row in db.select(','.join(fields), 'tests', where, wherein,distinct):
+		for row in db.select(','.join(fields), 'tests', where,
+				     wherein,distinct):
 			tests.append(klass(db, *row))
 		return tests
 
@@ -192,7 +212,8 @@
 		return [klass(db, *row) for row in rows]
 
 		
-	def __init__(self, db, test_idx, job_idx, testname, subdir, kernel_idx, status_num, reason, machine_idx):
+	def __init__(self, db, test_idx, job_idx, testname, subdir, kernel_idx,
+		     status_num, reason, machine_idx):
 		self.idx = test_idx
 		self.job = job(db, job_idx)
 		self.testname = testname