[autotest] AFE child jobs table on parent job.
Add a table to list all the child jobs of a parent job on Job View tab.
One can click on and navigate between child and parent.
BUG=chromium:379959
DEPLOY=afe,apache
TEST=ran afe, viewed a job, navigated between parent and child jobs
Change-Id: Id70c41c8f7cee40bd71a206e1f3e08a68efe054f
Reviewed-on: https://chromium-review.googlesource.com/202579
Reviewed-by: Simran Basi <[email protected]>
Commit-Queue: Jiaxi Luo <[email protected]>
Tested-by: Jiaxi Luo <[email protected]>
diff --git a/frontend/afe/doctests/001_rpc_test.txt b/frontend/afe/doctests/001_rpc_test.txt
index 8442802..57f4497 100644
--- a/frontend/afe/doctests/001_rpc_test.txt
+++ b/frontend/afe/doctests/001_rpc_test.txt
@@ -34,6 +34,12 @@
>>> if drone_set:
... _ = models.DroneSet.objects.create(name=drone_set)
+# mock up tko rpc_interface
+>>> from autotest_lib.client.common_lib.test_utils import mock
+>>> mock.mock_god().stub_function_to_return(rpc_interface.tko_rpc_interface,
+... 'get_status_counts',
+... None)
+
# basic interface test
######################
diff --git a/frontend/afe/rpc_interface.py b/frontend/afe/rpc_interface.py
index 7a376aa..bd68425 100644
--- a/frontend/afe/rpc_interface.py
+++ b/frontend/afe/rpc_interface.py
@@ -37,6 +37,7 @@
from autotest_lib.frontend.afe import models, model_logic, model_attributes
from autotest_lib.frontend.afe import control_file, rpc_utils
from autotest_lib.frontend.afe import site_rpc_interface
+from autotest_lib.frontend.tko import rpc_interface as tko_rpc_interface
from autotest_lib.server.cros.dynamic_suite import tools
def get_parameterized_autoupdate_image_url(job):
@@ -711,15 +712,23 @@
def get_jobs_summary(**filter_data):
"""\
- Like get_jobs(), but adds a 'status_counts' field, which is a dictionary
- mapping status strings to the number of hosts currently with that
- status, i.e. {'Queued' : 4, 'Running' : 2}.
+ Like get_jobs(), but adds 'status_counts' and 'result_counts' field.
+
+ 'status_counts' filed is a dictionary mapping status strings to the number
+ of hosts currently with that status, i.e. {'Queued' : 4, 'Running' : 2}.
+
+ 'result_counts' field is piped to tko's rpc_interface and has the return
+ format specified under get_group_counts.
"""
jobs = get_jobs(**filter_data)
ids = [job['id'] for job in jobs]
all_status_counts = models.Job.objects.get_status_counts(ids)
for job in jobs:
job['status_counts'] = all_status_counts[job['id']]
+ job['result_counts'] = tko_rpc_interface.get_status_counts(
+ ['afe_job_id', 'afe_job_id'],
+ header_groups=[['afe_job_id'], ['afe_job_id']],
+ **{'afe_job_id': job['id']})
return rpc_utils.prepare_for_serialization(jobs)
diff --git a/frontend/afe/rpc_interface_unittest.py b/frontend/afe/rpc_interface_unittest.py
index 6ce76eb..89af961 100755
--- a/frontend/afe/rpc_interface_unittest.py
+++ b/frontend/afe/rpc_interface_unittest.py
@@ -1,7 +1,7 @@
#pylint: disable-msg=C0111
#!/usr/bin/python
-import datetime, unittest
+import datetime
import common
from autotest_lib.frontend import setup_django_environment
@@ -12,6 +12,8 @@
from autotest_lib.client.common_lib import control_data
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib import priorities
+from autotest_lib.client.common_lib.test_utils import mock
+from autotest_lib.client.common_lib.test_utils import unittest
CLIENT = control_data.CONTROL_TYPE_NAMES.CLIENT
SERVER = control_data.CONTROL_TYPE_NAMES.SERVER
@@ -23,9 +25,11 @@
frontend_test_utils.FrontendTestMixin):
def setUp(self):
self._frontend_common_setup()
+ self.god = mock.mock_god()
def tearDown(self):
+ self.god.unstub_all()
self._frontend_common_teardown()
@@ -138,6 +142,11 @@
entries[2].aborted = True
entries[2].save()
+ # Mock up tko_rpc_interface.get_status_counts.
+ self.god.stub_function_to_return(rpc_interface.tko_rpc_interface,
+ 'get_status_counts',
+ None)
+
job_summaries = rpc_interface.get_jobs_summary(id=job.id)
self.assertEquals(len(job_summaries), 1)
summary = job_summaries[0]
diff --git a/frontend/client/src/autotest/afe/JobDetailView.java b/frontend/client/src/autotest/afe/JobDetailView.java
index 7100822..71d6aef 100644
--- a/frontend/client/src/autotest/afe/JobDetailView.java
+++ b/frontend/client/src/autotest/afe/JobDetailView.java
@@ -7,6 +7,7 @@
import autotest.common.table.DataTable;
import autotest.common.table.DynamicTable;
import autotest.common.table.ListFilter;
+import autotest.common.table.RpcDataSource;
import autotest.common.table.SearchFilter;
import autotest.common.table.SelectionManager;
import autotest.common.table.SimpleFilter;
@@ -37,7 +38,7 @@
import java.util.Set;
-public class JobDetailView extends DetailView implements TableWidgetFactory, TableActionsListener {
+public class JobDetailView extends DetailView implements TableWidgetFactory {
private static final String[][] JOB_HOSTS_COLUMNS = {
{DataTable.CLICKABLE_WIDGET_COLUMN, ""}, // selection checkbox
{"hostname", "Host"}, {"full_status", "Status"},
@@ -45,9 +46,15 @@
// columns for status log and debug log links
{DataTable.CLICKABLE_WIDGET_COLUMN, ""}, {DataTable.CLICKABLE_WIDGET_COLUMN, ""}
};
+ private static final String[][] CHILD_JOBS_COLUMNS = {
+ { "id", "ID" }, { "name", "Name" }, { "priority", "Priority" },
+ { "control_type", "Client/Server" }, { JobTable.HOSTS_SUMMARY, "Status" },
+ { JobTable.RESULTS_SUMMARY, "Passed Tests" }
+ };
public static final String NO_URL = "about:blank";
public static final int NO_JOB_ID = -1;
public static final int HOSTS_PER_PAGE = 30;
+ public static final int CHILD_JOBS_PER_PAGE = 30;
public static final String RESULTS_MAX_WIDTH = "700px";
public static final String RESULTS_MAX_HEIGHT = "500px";
@@ -57,11 +64,20 @@
public void onCreateRecurringJob(int id);
}
+ protected class ChildJobsListener {
+ public void onJobSelected(int id) {
+ fetchById(Integer.toString(id));
+ }
+ }
+
protected int jobId = NO_JOB_ID;
private JobStatusDataSource jobStatusDataSource = new JobStatusDataSource();
+ protected JobTable childJobsTable = new JobTable(CHILD_JOBS_COLUMNS);
+ protected TableDecorator childJobsTableDecorator = new TableDecorator(childJobsTable);
+ protected SimpleFilter parentJobIdFliter = new SimpleFilter();
protected DynamicTable hostsTable = new DynamicTable(JOB_HOSTS_COLUMNS, jobStatusDataSource);
- protected TableDecorator tableDecorator = new TableDecorator(hostsTable);
+ protected TableDecorator hostsTableDecorator = new TableDecorator(hostsTable);
protected SimpleFilter jobFilter = new SimpleFilter();
protected Button abortButton = new Button("Abort job");
protected Button cloneButton = new Button("Clone job");
@@ -69,7 +85,9 @@
protected Frame tkoResultsFrame = new Frame();
protected JobDetailListener listener;
- private SelectionManager selectionManager;
+ protected ChildJobsListener childJobsListener = new ChildJobsListener();
+ private SelectionManager hostsSelectionManager;
+ private SelectionManager childJobsSelectionManager;
private Label controlFile = new Label();
private DisclosurePanel controlFilePanel = new DisclosurePanel("");
@@ -172,6 +190,9 @@
jobFilter.setParameter("job", new JSONNumber(jobId));
hostsTable.refresh();
+
+ parentJobIdFliter.setParameter("parent_job", new JSONNumber(jobId));
+ childJobsTable.refresh();
}
@@ -202,6 +223,22 @@
idInput.setVisibleLength(5);
+ childJobsTable.setRowsPerPage(CHILD_JOBS_PER_PAGE);
+ childJobsTable.setClickable(true);
+ childJobsTable.addListener(new DynamicTableListener() {
+ public void onRowClicked(int rowIndex, JSONObject row, boolean isRightClick) {
+ int jobId = (int) row.get("id").isNumber().doubleValue();
+ childJobsListener.onJobSelected(jobId);
+ }
+
+ public void onTableRefreshed() {}
+ });
+
+ childJobsTableDecorator.addPaginators();
+ childJobsSelectionManager = childJobsTableDecorator.addSelectionManager(false);
+ childJobsTable.setWidgetFactory(childJobsSelectionManager);
+ addWidget(childJobsTableDecorator, "child_jobs_table");
+
hostsTable.setRowsPerPage(HOSTS_PER_PAGE);
hostsTable.setClickable(true);
hostsTable.addListener(new DynamicTableListener() {
@@ -215,11 +252,29 @@
});
hostsTable.setWidgetFactory(this);
- tableDecorator.addPaginators();
+ hostsTableDecorator.addPaginators();
addTableFilters();
- selectionManager = tableDecorator.addSelectionManager(false);
- tableDecorator.addTableActionsPanel(this, true);
- addWidget(tableDecorator, "job_hosts_table");
+ hostsSelectionManager = hostsTableDecorator.addSelectionManager(false);
+ hostsTableDecorator.addTableActionsPanel(new TableActionsListener() {
+ public ContextMenu getActionMenu() {
+ ContextMenu menu = new ContextMenu();
+
+ menu.addItem("Abort hosts", new Command() {
+ public void execute() {
+ abortSelectedHosts();
+ }
+ });
+
+ menu.addItem("Clone job on selected hosts", new Command() {
+ public void execute() {
+ cloneJobOnSelectedHosts();
+ }
+ });
+
+ return menu;
+ }
+ }, true);
+ addWidget(hostsTableDecorator, "job_hosts_table");
abortButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
@@ -256,6 +311,7 @@
protected void addTableFilters() {
hostsTable.addFilter(jobFilter);
+ childJobsTable.addFilter(parentJobIdFliter);
SearchFilter hostnameFilter = new SearchFilter("host__hostname", true);
ListFilter statusFilter = new ListFilter("status");
@@ -263,8 +319,8 @@
JSONArray statuses = staticData.getData("job_statuses").isArray();
statusFilter.setChoices(Utils.JSONtoStrings(statuses));
- tableDecorator.addFilter("Hostname", hostnameFilter);
- tableDecorator.addFilter("Status", statusFilter);
+ hostsTableDecorator.addFilter("Hostname", hostnameFilter);
+ hostsTableDecorator.addFilter("Status", statusFilter);
}
private void abortJob() {
@@ -278,7 +334,8 @@
}
private void abortSelectedHosts() {
- AfeUtils.abortHostQueueEntries(selectionManager.getSelectedObjects(), new SimpleCallback() {
+ AfeUtils.abortHostQueueEntries(hostsSelectionManager.getSelectedObjects(),
+ new SimpleCallback() {
public void doCallback(Object source) {
refresh();
}
@@ -313,7 +370,7 @@
}
private void cloneJobOnSelectedHosts() {
- Set<JSONObject> hostsQueueEntries = selectionManager.getSelectedObjects();
+ Set<JSONObject> hostsQueueEntries = hostsSelectionManager.getSelectedObjects();
JSONArray queueEntryIds = new JSONArray();
for (JSONObject queueEntry : hostsQueueEntries) {
queueEntryIds.set(queueEntryIds.size(), queueEntry.get("id"));
@@ -453,7 +510,7 @@
public Widget createWidget(int row, int cell, JSONObject hostQueueEntry) {
if (cell == 0) {
- return selectionManager.createWidget(row, cell, hostQueueEntry);
+ return hostsSelectionManager.createWidget(row, cell, hostQueueEntry);
}
String executionSubdir = Utils.jsonToString(hostQueueEntry.get("execution_subdir"));
@@ -477,22 +534,4 @@
url = Utils.getRetrieveLogsUrl(url);
return "<a target=\"_blank\" href=\"" + url + "\">" + text + "</a>";
}
-
- public ContextMenu getActionMenu() {
- ContextMenu menu = new ContextMenu();
-
- menu.addItem("Abort hosts", new Command() {
- public void execute() {
- abortSelectedHosts();
- }
- });
-
- menu.addItem("Clone job on selected hosts", new Command() {
- public void execute() {
- cloneJobOnSelectedHosts();
- }
- });
-
- return menu;
- }
}
diff --git a/frontend/client/src/autotest/afe/JobTable.java b/frontend/client/src/autotest/afe/JobTable.java
index c311557..ce9137c 100644
--- a/frontend/client/src/autotest/afe/JobTable.java
+++ b/frontend/client/src/autotest/afe/JobTable.java
@@ -1,6 +1,7 @@
package autotest.afe;
import autotest.common.StaticDataRepository;
+import autotest.common.StatusSummary;
import autotest.common.table.DynamicTable;
import autotest.common.table.RpcDataSource;
import autotest.common.table.DataSource.SortDirection;
@@ -15,25 +16,50 @@
*/
public class JobTable extends DynamicTable {
public static final String HOSTS_SUMMARY = "hosts_summary";
+ public static final String RESULTS_SUMMARY = "results_summary";
public static final String CREATED_TEXT = "created_text";
protected StaticDataRepository staticData = StaticDataRepository.getRepository();
- private static final String[][] JOB_COLUMNS = { {CLICKABLE_WIDGET_COLUMN, "Select"},
- { "id", "ID" }, { "owner", "Owner" }, { "name", "Name" },
- { "priority", "Priority" }, { "control_type", "Client/Server" },
- { CREATED_TEXT, "Created" }, { HOSTS_SUMMARY, "Status" } };
+ private static final String GROUP_COUNT_FIELD = "group_count";
+ private static final String PASS_COUNT_FIELD = "pass_count";
+ private static final String COMPLETE_COUNT_FIELD = "complete_count";
+ private static final String INCOMPLETE_COUNT_FIELD = "incomplete_count";
+ private static final String[][] DEFAULT_JOB_COLUMNS = {
+ {CLICKABLE_WIDGET_COLUMN, "Select"},
+ { "id", "ID" }, { "owner", "Owner" }, { "name", "Name" },
+ { "priority", "Priority" }, { "control_type", "Client/Server" },
+ { CREATED_TEXT, "Created" }, { HOSTS_SUMMARY, "Status" }
+ };
+
+ private static String[][] jobColumns;
public JobTable() {
- super(JOB_COLUMNS, new RpcDataSource("get_jobs_summary", "get_num_jobs"));
+ this(DEFAULT_JOB_COLUMNS);
+ }
+
+ public JobTable(String[][] jobColumns) {
+ super(jobColumns, new RpcDataSource("get_jobs_summary", "get_num_jobs"));
+ this.jobColumns = jobColumns;
sortOnColumn("id", SortDirection.DESCENDING);
}
@Override
protected void preprocessRow(JSONObject row) {
- JSONObject counts = row.get("status_counts").isObject();
- String countString = AfeUtils.formatStatusCounts(counts, "\n");
- row.put(HOSTS_SUMMARY, new JSONString(countString));
+ JSONObject status_counts = row.get("status_counts").isObject();
+ String statusCountString = AfeUtils.formatStatusCounts(status_counts, "\n");
+ row.put(HOSTS_SUMMARY, new JSONString(statusCountString));
+
+ JSONArray result_counts = row.get("result_counts").isObject().get("groups").isArray();
+ if (result_counts.size() > 0) {
+ StatusSummary statusSummary = StatusSummary.getStatusSummary(
+ result_counts.get(0).isObject(), PASS_COUNT_FIELD,
+ COMPLETE_COUNT_FIELD, INCOMPLETE_COUNT_FIELD,
+ GROUP_COUNT_FIELD);
+ String resultCountString = statusSummary.formatContents();
+ row.put(RESULTS_SUMMARY, new JSONString(resultCountString));
+ }
+
Double priorityValue = row.get("priority").isNumber().getValue();
String priorityName = staticData.getPriorityName(priorityValue);
row.put("priority", new JSONString(priorityName));
diff --git a/frontend/client/src/autotest/tko/StatusSummary.java b/frontend/client/src/autotest/common/StatusSummary.java
similarity index 72%
rename from frontend/client/src/autotest/tko/StatusSummary.java
rename to frontend/client/src/autotest/common/StatusSummary.java
index 5d93956..cedc6ad 100644
--- a/frontend/client/src/autotest/tko/StatusSummary.java
+++ b/frontend/client/src/autotest/common/StatusSummary.java
@@ -1,15 +1,12 @@
// Copyright 2008 Google Inc. All Rights Reserved.
-package autotest.tko;
-
-import autotest.common.AbstractStatusSummary;
-import autotest.common.Utils;
+package autotest.common;
import com.google.gwt.json.client.JSONObject;
import java.util.Arrays;
-class StatusSummary extends AbstractStatusSummary {
+public class StatusSummary extends AbstractStatusSummary {
public int passed = 0;
public int complete = 0;
public int incomplete = 0;
@@ -17,12 +14,14 @@
private String[] contents = null;
- public static StatusSummary getStatusSummary(JSONObject group) {
+ public static StatusSummary getStatusSummary(JSONObject group, String passCountField,
+ String completeCountField, String incompleteCountField,
+ String groupCountField) {
StatusSummary summary = new StatusSummary();
- summary.passed = getField(group, TestGroupDataSource.PASS_COUNT_FIELD);
- summary.complete = getField(group, TestGroupDataSource.COMPLETE_COUNT_FIELD);
- summary.incomplete = getField(group, TestGroupDataSource.INCOMPLETE_COUNT_FIELD);
- summary.total = getField(group, TestGroupDataSource.GROUP_COUNT_FIELD);
+ summary.passed = getField(group, passCountField);
+ summary.complete = getField(group, completeCountField);
+ summary.incomplete = getField(group, incompleteCountField);
+ summary.total = getField(group, groupCountField);
if (group.containsKey("extra_info")) {
summary.contents = Utils.JSONtoStrings(group.get("extra_info").isArray());
diff --git a/frontend/client/src/autotest/public/AfeClient.html b/frontend/client/src/autotest/public/AfeClient.html
index f74125c..a13cc5d 100644
--- a/frontend/client/src/autotest/public/AfeClient.html
+++ b/frontend/client/src/autotest/public/AfeClient.html
@@ -89,7 +89,6 @@
<span id="view_synch_count"></span><br>
<span class="field-name">Status:</span>
<span id="view_status"></span><br>
-
<div id="view_control_file"></div><br>
<span class="field-name">
Full results
@@ -103,6 +102,9 @@
<span class="field-name">Hosts</span>
<div id="job_hosts_table"></div>
+
+ <span class="field-name">Child jobs</span>
+ <div id="child_jobs_table"></div><br>
</div>
</div>
diff --git a/frontend/client/src/autotest/tko/SpreadsheetDataProcessor.java b/frontend/client/src/autotest/tko/SpreadsheetDataProcessor.java
index 66445f8..fc0b9fd 100644
--- a/frontend/client/src/autotest/tko/SpreadsheetDataProcessor.java
+++ b/frontend/client/src/autotest/tko/SpreadsheetDataProcessor.java
@@ -1,5 +1,6 @@
package autotest.tko;
+import autotest.common.StatusSummary;
import autotest.common.spreadsheet.Spreadsheet;
import autotest.common.spreadsheet.Spreadsheet.CellInfo;
import autotest.common.spreadsheet.Spreadsheet.Header;
@@ -131,7 +132,12 @@
int row = (int) headerIndices.get(0).isNumber().doubleValue();
int column = (int) headerIndices.get(1).isNumber().doubleValue();
CellInfo cellInfo = spreadsheet.getCellInfo(row, column);
- StatusSummary statusSummary = StatusSummary.getStatusSummary(group);
+ StatusSummary statusSummary = StatusSummary.getStatusSummary(
+ group,
+ TestGroupDataSource.PASS_COUNT_FIELD,
+ TestGroupDataSource.COMPLETE_COUNT_FIELD,
+ TestGroupDataSource.INCOMPLETE_COUNT_FIELD,
+ TestGroupDataSource.GROUP_COUNT_FIELD);
numTotalTests += statusSummary.getTotal();
cellInfo.contents = statusSummary.formatContents();
cellInfo.cssClass = statusSummary.getCssClass();
diff --git a/frontend/client/src/autotest/tko/TableView.java b/frontend/client/src/autotest/tko/TableView.java
index c3d04b0..27d65bf 100644
--- a/frontend/client/src/autotest/tko/TableView.java
+++ b/frontend/client/src/autotest/tko/TableView.java
@@ -1,5 +1,6 @@
package autotest.tko;
+import autotest.common.StatusSummary;
import autotest.common.Utils;
import autotest.common.CustomHistory.HistoryToken;
import autotest.common.table.DataTable;
@@ -537,7 +538,12 @@
public Widget createWidget(int row, int cell, JSONObject rowObject) {
assert getActiveGrouping() == GroupingType.STATUS_COUNTS;
- StatusSummary statusSummary = StatusSummary.getStatusSummary(rowObject);
+ StatusSummary statusSummary = StatusSummary.getStatusSummary(
+ rowObject,
+ TestGroupDataSource.PASS_COUNT_FIELD,
+ TestGroupDataSource.COMPLETE_COUNT_FIELD,
+ TestGroupDataSource.INCOMPLETE_COUNT_FIELD,
+ TestGroupDataSource.GROUP_COUNT_FIELD);
SimplePanel panel = new SimplePanel();
panel.add(new HTML(statusSummary.formatContents()));
panel.getElement().addClassName(statusSummary.getCssClass());
diff --git a/frontend/tko/rpc_interface_unittest_fixme.py b/frontend/tko/rpc_interface_unittest_fixme.py
index 6649a63..cb65d88 100755
--- a/frontend/tko/rpc_interface_unittest_fixme.py
+++ b/frontend/tko/rpc_interface_unittest_fixme.py
@@ -20,6 +20,8 @@
tko_tests.status AS status_idx,
tko_tests.reason AS reason,
tko_tests.machine_idx AS machine_idx,
+ tko_tests.invalid AS invalid,
+ tko_tests.invalidates_test_idx AS invalidates_test_idx,
tko_tests.started_time AS test_started_time,
tko_tests.finished_time AS test_finished_time,
tko_jobs.tag AS job_tag,