@@ -84,6 +84,11 @@
<div id="test_detail_view" title="Test details">
<span id="td_fetch" class="box-full">Fetch test by ID:
<span id="td_fetch_controls"></span>
+ </span><br>
+ <span id="td_comp_attr" class="box-full">
+ Compare this execution using attributes <span id="td_comp_attr_control"></span>
+ with previous executions of <span id="td_comp_name_control"></span>
+ <span id="td_comp_save_control"></span>
</span><br><br>
<div id="td_title" class="title"></div><br>
<div id="td_data">
@@ -110,6 +115,7 @@
<span class="field-name">Test labels:</span>
<span id="td_test_labels"></span><br>
<span id="td_attributes"></span><br>
+ <span id="td_analysis"></span><br>
<br>
<span class="field-name">Key log files:</span>
@@ -62,3 +62,12 @@ div.spreadsheet-cell-nonclickable {
font-weight: bold;
}
+.test-analysis .content {
+ white-space: pre;
+ font-family: monospace;
+}
+
+.test-analysis .header table {
+ font-weight: bold;
+}
+
@@ -7,11 +7,13 @@ import autotest.common.Utils;
import autotest.common.ui.DetailView;
import autotest.common.ui.NotifyManager;
import autotest.common.ui.RealHyperlink;
+import autotest.common.StaticDataRepository;
import com.google.gwt.json.client.JSONNumber;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.json.client.JSONArray;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.WindowResizeListener;
import com.google.gwt.user.client.ui.Composite;
@@ -23,6 +25,10 @@ import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.SimplePanel;
import java.util.ArrayList;
@@ -33,18 +39,23 @@ import java.util.List;
class TestDetailView extends DetailView {
private static final int NO_TEST_ID = -1;
- private static final JsonRpcProxy logLoadingProxy =
+ private static final JsonRpcProxy logLoadingProxy =
new PaddedJsonRpcProxy(Utils.RETRIEVE_LOGS_URL);
+ private static JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
private int testId = NO_TEST_ID;
+ private String testName = null;
private String jobTag;
private List<LogFileViewer> logFileViewers = new ArrayList<LogFileViewer>();
private RealHyperlink logLink = new RealHyperlink("(view all logs)");
private RealHyperlink testLogLink = new RealHyperlink("(view test logs)");
+ protected static TextBox attrInput = new TextBox();
+ protected static TextBox testSetInput = new TextBox();
+ protected Button attrUpdateButton = new Button("Save");
private Panel logPanel;
private Panel attributePanel = new SimplePanel();
- private class LogFileViewer extends Composite
+ private class LogFileViewer extends Composite
implements DisclosureHandler, WindowResizeListener {
private DisclosurePanel panel;
private ScrollPanel scroller; // ScrollPanel wrapping log contents
@@ -66,17 +77,17 @@ class TestDetailView extends DetailView {
handle(result);
}
};
-
+
public LogFileViewer(String logFilePath, String logFileName) {
this.logFilePath = logFilePath;
panel = new DisclosurePanel(logFileName);
panel.addEventHandler(this);
panel.addStyleName("log-file-panel");
initWidget(panel);
-
+
Window.addWindowResizeListener(this);
}
-
+
public void onOpen(DisclosureEvent event) {
JSONObject params = new JSONObject();
params.put("path", new JSONString(getLogUrl()));
@@ -88,7 +99,7 @@ class TestDetailView extends DetailView {
private String getLogUrl() {
return Utils.getLogsUrl(jobTag + "/" + logFilePath);
}
-
+
public void handle(JSONValue value) {
String logContents = value.isString().stringValue();
if (logContents.equals("")) {
@@ -107,8 +118,8 @@ class TestDetailView extends DetailView {
}
/**
- * Firefox fails to set relative widths correctly for elements with overflow: scroll (or
- * auto, or hidden). Instead, it just expands the element to fit the contents. So we use
+ * Firefox fails to set relative widths correctly for elements with overflow: scroll (or
+ * auto, or hidden). Instead, it just expands the element to fit the contents. So we use
* this trick to dynamically implement width: 100%.
*/
private void setScrollerWidth() {
@@ -132,11 +143,119 @@ class TestDetailView extends DetailView {
public void onClose(DisclosureEvent event) {}
}
-
+
+
+ /* This class handles the test comparison analysis.*/
+ private static class AnalysisTable extends Composite implements DisclosureHandler {
+ private ScrollPanel scroller;
+ private int testID;
+ private DisclosurePanel panel = new DisclosurePanel("Test case comparison");
+ private JsonRpcCallback rpcCallback = new JsonRpcCallback() {
+ @Override
+ public void onError(JSONObject errorObject) {
+ super.onError(errorObject);
+ String errorString = getErrorString(errorObject);
+ if (errorString.equals("")) {
+ errorString = "No comparison analysis data available for this configuration.";
+ }
+ setStatusText(errorString);
+ }
+
+ @Override
+ public void onSuccess(JSONValue result) {
+ handle(result);
+ }
+
+ };
+
+ public AnalysisTable(int tid) {
+ this.testID = tid;
+ panel.addEventHandler(this);
+ panel.addStyleName("test-analysis");
+ initWidget(panel);
+ }
+
+ public void onOpen(DisclosureEvent event) {
+ JSONObject params = new JSONObject();
+ params.put("test", new JSONString(Integer.toString(testID)));
+ params.put("attr", new JSONString(attrInput.getText()));
+ params.put("testset", new JSONString(testSetInput.getText()));
+ rpcProxy.rpcCall("get_testcase_comparison_data", params, rpcCallback);
+ setStatusText("Loading (may take few seconds)...");
+ }
+
+ public void handle(JSONValue value) {
+ String contents = value.isString().stringValue();
+ if (contents.equals("")) {
+ setText("No analysis data for this test case.");
+ } else {
+ setText(contents);
+ }
+ }
+
+ private void setText(String text) {
+ panel.clear();
+ scroller = new ScrollPanel();
+ scroller.getElement().setInnerText(text);
+ panel.add(scroller);
+ }
+
+ private void setStatusText(String status) {
+ panel.clear();
+ panel.add(new HTML("<i>" + status + "</i>"));
+ }
+
+ public void onClose(DisclosureEvent event) {}
+
+ }
+
+
+ private void retrieveComparisonAttributes(String testName) {
+ attrInput.setText("");
+ testSetInput.setText("");
+ StaticDataRepository staticData = StaticDataRepository.getRepository();
+ JSONObject args = new JSONObject();
+ args.put("owner", new JSONString(staticData.getCurrentUserLogin()));
+ args.put("testname", new JSONString(testName));
+
+ rpcProxy.rpcCall("get_test_comparison_attr", args, new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ JSONArray queries = result.isArray();
+ if (queries.size() == 0) {
+ return;
+ }
+ assert queries.size() == 1;
+ JSONObject query = queries.get(0).isObject();
+ attrInput.setText(query.get("attributes").isString().stringValue());
+ testSetInput.setText(query.get("testset").isString().stringValue());
+ return;
+ }
+ });
+
+ }
+
+ public void saveComparisonAttribute() {
+ StaticDataRepository staticData = StaticDataRepository.getRepository();
+ JSONObject args = new JSONObject();
+ args.put("name", new JSONString(testName));
+ args.put("owner", new JSONString(staticData.getCurrentUserLogin()));
+ args.put("attr_token", new JSONString(attrInput.getText()));
+ args.put("testset_token", new JSONString(testSetInput.getText()));
+ rpcProxy.rpcCall("add_test_comparison_attr", args, new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ NotifyManager.getInstance().showMessage("Test comparison attributes saved");
+ }
+ });
+ }
+
+
+
private static class AttributeTable extends Composite {
private DisclosurePanel container = new DisclosurePanel("Test attributes");
private FlexTable table = new FlexTable();
-
+
public AttributeTable(JSONObject attributes) {
processAttributes(attributes);
setupTableStyle();
@@ -149,7 +268,7 @@ class TestDetailView extends DetailView {
table.setText(0, 0, "No test attributes");
return;
}
-
+
List<String> sortedKeys = new ArrayList<String>(attributes.keySet());
Collections.sort(sortedKeys);
for (String key : sortedKeys) {
@@ -159,7 +278,7 @@ class TestDetailView extends DetailView {
table.setText(row, 1, value);
}
}
-
+
private void setupTableStyle() {
container.addStyleName("test-attributes");
}
@@ -170,12 +289,24 @@ class TestDetailView extends DetailView {
super.initialize();
addWidget(attributePanel, "td_attributes");
+
+ addWidget(attrInput, getTdCompAttrControlId());
+ addWidget(testSetInput, getTdCompSetNameControlId());
+ addWidget(attrUpdateButton, getTdCompSetNameControlId());
+
logPanel = new FlowPanel();
addWidget(logPanel, "td_log_files");
testLogLink.setOpensNewWindow(true);
addWidget(testLogLink, "td_view_logs_link");
logLink.setOpensNewWindow(true);
addWidget(logLink, "td_view_logs_link");
+
+ attrUpdateButton.addClickListener(new ClickListener() {
+ public void onClick(Widget sender) {
+ saveComparisonAttribute();
+ }
+ });
+
}
private void addLogViewers(String testName) {
@@ -209,10 +340,10 @@ class TestDetailView extends DetailView {
resetPage();
return;
}
-
+ testName = new String(test.get("test_name").isString().stringValue());
showTest(test);
}
-
+
@Override
public void onError(JSONObject errorObject) {
super.onError(errorObject);
@@ -220,7 +351,7 @@ class TestDetailView extends DetailView {
}
});
}
-
+
@Override
protected void setObjectId(String id) {
try {
@@ -230,7 +361,7 @@ class TestDetailView extends DetailView {
throw new IllegalArgumentException();
}
}
-
+
@Override
protected String getObjectId() {
if (testId == NO_TEST_ID) {
@@ -263,7 +394,19 @@ class TestDetailView extends DetailView {
public String getElementId() {
return "test_detail_view";
}
-
+
+ protected String getTdCompAttrControlId() {
+ return "td_comp_attr_control";
+ }
+
+ protected String getTdCompSetNameControlId() {
+ return "td_comp_name_control";
+ }
+
+ protected String getTdCompSaveControlId() {
+ return "td_comp_save_control";
+ }
+
@Override
public void display() {
super.display();
@@ -273,7 +416,8 @@ class TestDetailView extends DetailView {
protected void showTest(JSONObject test) {
String testName = test.get("test_name").isString().stringValue();
jobTag = test.get("job_tag").isString().stringValue();
-
+
+ retrieveComparisonAttributes(testName);
showText(testName, "td_test");
showText(jobTag, "td_job_tag");
showField(test, "job_name", "td_job_name");
@@ -295,7 +439,9 @@ class TestDetailView extends DetailView {
JSONObject attributes = test.get("attributes").isObject();
attributePanel.clear();
attributePanel.add(new AttributeTable(attributes));
-
+ RootPanel analysisPanel = RootPanel.get("td_analysis");
+ analysisPanel.clear();
+ analysisPanel.add(new AnalysisTable(testId));
logLink.setHref(Utils.getRetrieveLogsUrl(jobTag));
testLogLink.setHref(Utils.getRetrieveLogsUrl(jobTag) + "/" + testName);
addLogViewers(testName);
@@ -11,6 +11,7 @@ query_timeout: 3600
min_retry_delay: 20
max_retry_delay: 60
graph_cache_creation_timeout_minutes: 10
+test_comparison_maximum_attribute_mismatches: 4
[AUTOTEST_WEB]
host: localhost
@@ -228,6 +228,17 @@ class IterationAttribute(dbmodels.Model, model_logic.ModelExtensions):
db_table = 'iteration_attributes'
+class TestComparisonAttribute(dbmodels.Model, model_logic.ModelExtensions):
+ testname = dbmodels.CharField(max_length=90)
+ # TODO: change this to foreign key once DBs are merged
+ owner = dbmodels.CharField(max_length=80)
+ attributes = dbmodels.CharField(max_length=1024)
+ testset = dbmodels.CharField(max_length=1024)
+
+ class Meta:
+ db_table = 'test_comparison_attributes'
+
+
class IterationResult(dbmodels.Model, model_logic.ModelExtensions):
# see comment on IterationAttribute regarding primary_key=True
test = dbmodels.ForeignKey(Test, db_column='test_idx', primary_key=True)
@@ -1,10 +1,11 @@
-import os, pickle, datetime, itertools, operator
+import os, pickle, datetime, itertools, operator, urllib2, re
from django.db import models as dbmodels
from autotest_lib.frontend import thread_local
from autotest_lib.frontend.afe import rpc_utils, model_logic
from autotest_lib.frontend.afe import readonly_connection
from autotest_lib.new_tko.tko import models, tko_rpc_utils, graphing_utils
from autotest_lib.new_tko.tko import preconfigs
+from autotest_lib.client.common_lib import global_config
# table/spreadsheet view support
@@ -397,6 +398,52 @@ def delete_saved_queries(id_list):
query.delete()
+def get_test_comparison_attr(**filter_data):
+ """
+ Attributes list for test comparison is store at test comparison attributes
+ db table per user per test name. This function returns the database record
+ (if exists) that matches the user name and test name provided in
+ filter_data.
+
+ @param **filter_data: Dictionary of parameters that will be delegated to
+ TestComparisonAttribute.list_objects.
+ """
+ return rpc_utils.prepare_for_serialization(
+ models.TestComparisonAttribute.list_objects(filter_data))
+
+
+def add_test_comparison_attr(name, owner, attr_token=None, testset_token=""):
+ """
+ This function adds a new record to the test comparison attributes db table.
+
+ @param name: Test name.
+ @param owner: Test owner.
+ """
+ testname = name.strip()
+ if attr_token:
+ # if testset is not defined, the default is to save the test name (e.g.
+ # make the comparison on previous executions of the given testname)
+ if not testset_token or testset_token == "":
+ testset = testname
+ else:
+ testset = testset_token.strip()
+ existing_list = models.TestComparisonAttribute.objects.filter(
+ owner=owner,testname=name)
+ if existing_list:
+ query_object = existing_list[0]
+ query_object.attributes= attr_token.strip()
+ query_object.testset = testset
+ query_object.save()
+ return query_object.id
+ return models.TestComparisonAttribute.add_object(owner=owner,
+ testname=name,
+ attributes=attr_token,
+ testset=testset).id
+ else:
+ raise model_logic.ValidationError('No attributes defined for '
+ 'comparison operation')
+
+
# other
def get_motd():
return rpc_utils.get_motd()
@@ -465,3 +512,322 @@ def get_static_data():
result['motd'] = rpc_utils.get_motd()
return result
+
+
+def _get_test_lists(test_id, attr_keys, testset):
+ """
+ A test execution is a list containing header (ID, Status, Reason and
+ Attributes) values. The function searches the results database (tko)
+ and returns three test lists:
+
+ @param test_id: Test ID.
+ @param attr_keys: List with test attributes.
+ @param testset: Regular expression whose match describes a list of tests.
+ @return: Tuple with the following elements:
+ * test headers list
+ * current test execution to compare with past executions.
+ * List of previous test executions of the same testcase
+ (test name is identical)
+ * All attributes that matched the reg exp of the attribute keys
+ provided
+ """
+ current_set = models.TestView.objects.filter(test_idx=long(test_id))[0]
+ (test_name, status, reason) = (current_set.test_name, current_set.status,
+ current_set.reason)
+
+ # From all previous executions, filter those whose name matches the regular
+ # expression provided by the user
+ previous_valid_executions = []
+ previous_valid_attributes = []
+ previous_set = (
+ models.TestView.objects.exclude(test_idx=long(test_id)).distinct())
+ p = re.compile('^%s$' % testset.strip())
+ for set in previous_set:
+ execution = [set.test_idx, set.test_name, set.status, set.reason]
+ attributes = [set.attributes, set.test_attributes]
+ if p.match(set.test_name):
+ previous_valid_executions.append(execution)
+ previous_valid_attributes.append(attributes)
+
+ if len(previous_valid_executions) == 0:
+ raise model_logic.ValidationError('No comparison data available for '
+ 'this configuration.')
+
+ current_execution = [test_id, str(test_name), str(status), str(reason)]
+ current_execution_attr = [str(current_set.attribute),
+ str(current_set.test_attributes)]
+
+ previous_executions = []
+ test_headers = ['ID', 'NAME', 'STATUS', 'REASON']
+
+ # Find all attributes for the comparison (including all matches if
+ # regexp specified)
+ attributes = []
+ for key in attr_keys:
+ valid_key = False
+ p = re.compile('^%s$' % key.strip())
+ for attr in current_execution_attr:
+ if p.match(attr):
+ test_headers.append(attr)
+ attributes.append(attr)
+ valid_key=True
+ if not valid_key:
+ raise model_logic.ValidationError("Attribute '%s' does "
+ "not exist in this test "
+ "execution." % key)
+
+ # Check that previous test contains all required attributes for comparison
+ gap = [a for a in attributes if a not in previous_valid_attributes]
+
+ if len(gap) == 0:
+ for row in previous_valid_executions:
+ # Just to keep the record, each row for previous_valid_executions
+ # has the format [set.test_idx, set.test_name, set.status,
+ # set.reason]
+ if row[0] != long(test_id):
+ test = [int(row[0]), str(row[1]), str(row[2]), str(row[3])]
+ for key in attributes:
+ new_set = previous_set.filter(test_idx=row[0])
+ attr_key = new_set.attribute_value
+ attr_val = new_set.test_attributes
+ if str(attr_key) == key:
+ if row[0] == long(test_id):
+ current_execution.append(str(attr_val))
+ else:
+ test.append(str(attr_val))
+ break
+ if row[0] != long(test_id):
+ previous_executions.append(test)
+
+ if len(previous_executions) == 0:
+ raise model_logic.ValidationError('No comparison data available '
+ 'for this configuration.')
+
+ return (attributes, test_headers, current_execution, previous_executions)
+
+
+def _find_mismatches(headers, orig, new, valid_attr):
+ """
+ Finds the attributes mismatch between two tests provided: orig and new.
+
+ @param headers: Test headers.
+ @param orig: First test to be compared.
+ @param new: Second test to be compared.
+ @param valid_attr: Test attributes to be considered valid.
+ @returns: Tuple with the number of mismaching attributes found, a list
+ of the mismaching attribute names and a list of the mismaching
+ attribute values.
+ """
+ i = 0
+ mismatched_headers = []
+ mismatched_values = []
+
+ for index, item in enumerate(orig):
+ if item != new[index] and headers[index] in valid_attr:
+ i += 1
+ mismatched_headers.append(headers[index])
+ mismatched_values.append(new[index])
+
+ return (i,','.join(mismatched_headers),','.join(mismatched_values))
+
+
+def _prepare_test_analysis_dict(test_headers, previous_test_executions,
+ current_test_execution, passed, attributes,
+ max_mismatches_allowed=2):
+ """
+ Prepares and returns a dictionary of comparison analysis data.
+
+ @param test_headers: Dictionary with test headers.
+ @param previous_test_executions: List of test executions.
+ @param current_test_execution: Test execution being visualized on TKO.
+ @param passed: List of passed tests.
+ @param attributes: List of test attributes.
+ @param max_mismatches_allowed: Maximum amount of mismatches to be
+ considered for analysis.
+ """
+ dict = {}
+ counters = ['total', 'passed', 'pass_rate']
+ if len(previous_test_executions) > 0: # At least one test defined
+ for item in previous_test_executions:
+ (mismatches, attr_headers,
+ attr_values) = _find_mismatches(test_headers,
+ current_test_execution, item,
+ attributes)
+ if mismatches <= max_mismatches_allowed:
+ # Create dictionary keys if does not exist and
+ # initialize all counters
+ if mismatches not in dict.keys():
+ dict[mismatches] = {'total': 0, 'passed': 0, 'pass_rate': 0}
+ if attr_headers not in dict[mismatches].keys():
+ dict[mismatches][attr_headers] = {'total': 0, 'passed': 0,
+ 'pass_rate': 0}
+ if attr_values not in dict[mismatches][attr_headers].keys():
+ dict[mismatches][attr_headers][attr_values] = {'total': 0,
+ 'passed': 0,
+ 'pass_rate': 0}
+ if (item[test_headers.index('STATUS')] not in
+ dict[mismatches][attr_headers][attr_values].keys()):
+ s = item[test_headers.index('STATUS')]
+ dict[mismatches][attr_headers][attr_values][s] = []
+
+ # Update all counters
+ testcase = [item[test_headers.index('ID')],
+ item[test_headers.index('NAME')],
+ item[test_headers.index('REASON')]]
+
+ s = item[test_headers.index('STATUS')]
+ dict[mismatches][attr_headers][attr_values][s].append(testcase)
+ dict[mismatches]['total'] += 1
+ dict[mismatches][attr_headers]['total'] += 1
+ dict[mismatches][attr_headers][attr_values]['total'] += 1
+
+ if item[test_headers.index('STATUS')] in passed:
+ dict[mismatches]['passed'] += 1
+ dict[mismatches][attr_headers]['passed'] += 1
+ dict[mismatches][attr_headers][attr_values]['passed'] += 1
+
+ p = float(dict[mismatches]['passed'])
+ t = float(dict[mismatches]['total'])
+ dict[mismatches]['pass_rate'] = int(p/t * 100)
+
+ p = float(dict[mismatches][attr_headers]['passed'])
+ t = float(dict[mismatches][attr_headers]['total'])
+ dict[mismatches][attr_headers]['pass_rate'] = int(p/t * 100)
+
+ p = float(dict[mismatches][attr_headers][attr_values]['passed'])
+ t = float(dict[mismatches][attr_headers][attr_values]['total'])
+ dict[mismatches][attr_headers][attr_values]['pass_rate'] = (
+ int(p/t * 100))
+
+ return (dict, counters)
+
+
+def _make_content(test_id, current_test_execution, headers, counters, t_dict,
+ attributes, max_mismatches_allowed):
+ """
+ Prepares the comparison analysis text to be presented in the frontend
+ GUI.
+
+ @param test_id: Test ID.
+ @param current_test_execution: Current text execution.
+ @param headers: Test headers.
+ @param counters: Auxiliary counters.
+ @param t_dict: Dictionary with the result that will be returned.
+ @param attributes: Test attributes.
+ @param max_mismatches_allowed: Number of maximum mismatches allowed.
+ """
+ mismatches = t_dict.keys()
+ content = ('Test case comparison with the following attributes: %s\n' %
+ str(attributes))
+ content += ('(maximum allowed mismatching attributes = %d)\n' %
+ int(max_mismatches_allowed))
+
+ for mismatch in mismatches:
+ if mismatch not in counters:
+ content += ('\n%d mismatching attributes (%d/%d)\n' %
+ (mismatch, int(t_dict[mismatch]['passed']),
+ int(t_dict[mismatch]['total'])))
+ attribute_headers = t_dict[mismatch].keys()
+ for attr_header in attribute_headers:
+ if attr_header not in counters:
+ if int(mismatch) > 0:
+ p = int(t_dict[mismatch][attr_header]['passed'])
+ t = int(t_dict[mismatch][attr_header]['total'])
+ content += ' %s (%d/%d)\n' % (attr_header, p, t)
+ attribute_values = t_dict[mismatch][attr_header].keys()
+ for attr_value in attribute_values:
+ if attr_value not in counters:
+ if int(mismatch) > 0:
+ p = int(
+ t_dict[mismatch][attr_header][attr_value]['passed'])
+ t = int(
+ t_dict[mismatch][attr_header][attr_value]['total'])
+ content += (' %s (%d/%d)\n' % (attr_value, p, t))
+ test_sets = (
+ t_dict[mismatch][attr_header][attr_value].keys())
+ for test_set in test_sets:
+ if test_set not in counters:
+ tc = (
+ t_dict[mismatch][attr_header][attr_value][test_set])
+ if len(tc) > 0:
+ content += ' %s\n' % (test_set)
+ links =[]
+ for t in tc:
+ link = '%d : %s : %s' % (t[0], t[1], t[2])
+ content += ' %s\n' % link
+
+ return content
+
+
+def get_testcase_comparison_data(test, attr ,testset):
+ """
+ Test case comparison compares a given test execution with other executions
+ of the same test according to a predefined list of attributes. The result
+ is a dictionary of test cases classified by status (GOOD, FAIL, etc.) and
+ attributes mismatching distance (same idea as hamming distance, just
+ implemented on test attributes).
+
+ Distance of 0 refers to tests that have identical attributes values,
+ distance of 1 to tests that have only one different attribute value, so on
+ and so forth.
+
+ The general layout of test case comparison result is:
+
+ number of mismatching attributes (nSuccess/nTotal)
+ differing attributes (nSuccess/nTotal)
+ attribute value (nSuccess/nTotal)
+ Status
+ test_id reason
+ ...
+ Status (different)
+ test_id reason
+ ...
+ ...
+
+ @param test: Job ID that we'll get comparison data for.
+ @param attr: String of attributes to use for the comparison delimited by
+ comma.
+ @return: Dictionary with test comparison data that will be serialized.
+ """
+ result = {}
+ tid = None
+ attr_keys = None
+ attributes = None
+ if test:
+ tid = int(test)
+ if attr:
+ attr_keys = attr.replace(' ', '').split(',')
+
+ # process test comparison analysis
+ try:
+ c = global_config.global_config
+ max_mismatches_allowed = int(c.get_config_value('TKO',
+ 'test_comparison_maximum_attribute_mismatches'))
+ passed= ['GOOD']
+ if not tid:
+ raise model_logic.ValidationError('Test was not specified.')
+ if not attr_keys:
+ raise model_logic.ValidationError('At least one attribute must be '
+ 'specified.')
+ if not testset:
+ raise model_logic.ValidationError('Previous test executions scope '
+ 'must be specified.')
+
+ (attributes, test_headers, current_test_execution,
+ previous_test_executions) = _get_test_lists(tid, attr_keys, testset)
+
+ (test_comparison_dict,
+ counters) = _prepare_test_analysis_dict(test_headers,
+ previous_test_executions,
+ current_test_execution,
+ passed, attributes,
+ max_mismatches_allowed)
+
+ result = _make_content(tid, current_test_execution, test_headers,
+ counters, test_comparison_dict, attributes,
+ max_mismatches_allowed)
+
+ except urllib2.HTTPError:
+ result = 'Test comparison error!'
+
+ return rpc_utils.prepare_for_serialization(result)
new file mode 100644
@@ -0,0 +1,20 @@
+UP_SQL = """
+CREATE TABLE `test_comparison_attributes` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `testname` varchar(255) NOT NULL,
+ `owner` varchar(80) NOT NULL,
+ `attributes` varchar(1024) NOT NULL,
+ `testset` varchar(1024) NOT NULL
+);
+"""
+
+DOWN_SQL = """
+DROP TABLE IF EXISTS `test_comparison_attributes`;
+"""
+
+def migrate_up(manager):
+ manager.execute(UP_SQL)
+
+
+def migrate_down(manager):
+ manager.execute(DOWN_SQL)