getTableNames()
{
- return getSchema().getTable("globalsettings");
+ return Set.of(
+ TABLE_TEST_RUNS,
+ TABLE_USER,
+ TABLE_USER_DATA,
+ TABLE_TRAIN_RUNS,
+ TABLE_HANGS,
+ TABLE_MEMORY_LEAKS,
+ TABLE_HANDLE_LEAKS,
+ TABLE_TEST_PASSES,
+ TABLE_TEST_FAILS,
+ TABLE_GLOBAL_SETTINGS);
}
- public static SqlDialect getSqlDialect()
- {
- return getSchema().getSqlDialect();
- }
+ // ---------------------------------------------------------------------------
+ // Static table accessors — used throughout TestResultsController
+ // ---------------------------------------------------------------------------
+
+ public static TableInfo getTableInfoTestRuns() { return getSchema().getTable(TABLE_TEST_RUNS); }
+ public static TableInfo getTableInfoUser() { return getSchema().getTable(TABLE_USER); }
+ public static TableInfo getTableInfoUserData() { return getSchema().getTable(TABLE_USER_DATA); }
+ public static TableInfo getTableInfoTrain() { return getSchema().getTable(TABLE_TRAIN_RUNS); }
+ public static TableInfo getTableInfoHangs() { return getSchema().getTable(TABLE_HANGS); }
+ public static TableInfo getTableInfoMemoryLeaks() { return getSchema().getTable(TABLE_MEMORY_LEAKS); }
+ public static TableInfo getTableInfoHandleLeaks() { return getSchema().getTable(TABLE_HANDLE_LEAKS); }
+ public static TableInfo getTableInfoTestPasses() { return getSchema().getTable(TABLE_TEST_PASSES); }
+ public static TableInfo getTableInfoTestFails() { return getSchema().getTable(TABLE_TEST_FAILS); }
+ public static TableInfo getTableInfoGlobalSettings() { return getSchema().getTable(TABLE_GLOBAL_SETTINGS); }
}
diff --git a/testresults/src/org/labkey/testresults/TestResultsWebPart.java b/testresults/src/org/labkey/testresults/TestResultsWebPart.java
index c4a632d4..2a8a9a53 100644
--- a/testresults/src/org/labkey/testresults/TestResultsWebPart.java
+++ b/testresults/src/org/labkey/testresults/TestResultsWebPart.java
@@ -28,7 +28,7 @@ public WebPartView> getWebPartView(@NotNull ViewContext portalCtx, Portal.@Not
TestsDataBean bean = null;
try
{
- bean = TestResultsController.getRunDownBean(portalCtx.getUser(), c, portalCtx);
+ bean = TestResultsController.getRunDownBean(portalCtx.getUser(), c);
}
catch (ParseException | IOException e)
{
diff --git a/testresults/src/org/labkey/testresults/view/failureDetail.jsp b/testresults/src/org/labkey/testresults/view/failureDetail.jsp
index 6537303b..3d675525 100644
--- a/testresults/src/org/labkey/testresults/view/failureDetail.jsp
+++ b/testresults/src/org/labkey/testresults/view/failureDetail.jsp
@@ -392,19 +392,31 @@ $(document).ready(function() {
$("#problem-type-selection input").change(changeProblemType);
$("#problem-type-selection input[value=" + <%=q(problemType)%> + "]").prop("checked", true).trigger("change");
- // Initialize sortable table.
+ // Initialize sortable table. tablesorter 2.0.5b requires numeric column
+ // indices in `headers`, so look up the problem column's index from its ID
+ // — keeps the named selector as the source of truth if columns are reordered.
+ var problemColIdx = $("#failurestatstable thead th").index($("#col-problem"));
+ var headers = {};
+ if (problemColIdx >= 0) {
+ headers[problemColIdx] = { sorter: false };
+ } else {
+ console.warn("failureDetail.jsp: #col-problem header not found; problem column will be sortable.");
+ }
+ // Skip sortList/sortAppend on an empty tbody — tablesorter throws
+ // when asked to apply an initial sort with no rows to sort.
+ var hasRows = $("#failurestatstable tbody tr").length > 0;
$("#failurestatstable").tablesorter({
widthFixed : true,
resizable: true,
widgets: ['zebra'],
- headers : { "#col-problem": { sorter: false } },
+ headers : headers,
cssAsc: "headerSortUp",
cssDesc: "headerSortDown",
ignoreCase: true,
- sortList: [[1, 1]], // initial sort by post time descending
- sortAppend: {
+ sortList: hasRows ? [[1, 1]] : [], // initial sort by post time descending
+ sortAppend: hasRows ? {
0: [[ 1, 'a' ]] // secondary sort by date ascending
- },
+ } : {},
theme: 'default'
});
});
diff --git a/testresults/src/org/labkey/testresults/view/runDetail.jsp b/testresults/src/org/labkey/testresults/view/runDetail.jsp
index 3013117d..c4895a38 100644
--- a/testresults/src/org/labkey/testresults/view/runDetail.jsp
+++ b/testresults/src/org/labkey/testresults/view/runDetail.jsp
@@ -49,11 +49,11 @@
win.document.write('' + data + '
');
};
var showLog = function() {
- $.get('<%=h(new ActionURL(TestResultsController.ViewLogAction.class, c).addParameter("runid", runId))%>', csrf_header,
+ $.get('<%=h(new ActionURL(TestResultsController.ViewLogAction.class, c).addParameter("runId", runId))%>', csrf_header,
function(data) { popupData(data.log); }, "json");
};
var showXml = function() {
- $.get('<%=h(new ActionURL(TestResultsController.ViewXmlAction.class, c).addParameter("runid", runId))%>', csrf_header,
+ $.get('<%=h(new ActionURL(TestResultsController.ViewXmlAction.class, c).addParameter("runId", runId))%>', csrf_header,
function(data) { popupData(data.xml); }, "json");
};
@@ -85,7 +85,7 @@
Run Id: <%=run.getId()%>
- User : "><%=h(run.getUserName())%>
+ User : "><%=h(run.getUserName())%>
OS: <%=h(run.getOs())%>
Revision: <%=h(run.getRevisionFull())%>
Passed Tests : <%=run.getPasses().length%>
@@ -216,7 +216,7 @@ if (leaks.length > 0) { %>
if (data.Success) {
location.reload();
} else {
- alert(data);
+ alert("Failed to update training set." + (data.error ? " " + data.error : ""));
}
}, "json");
});
diff --git a/testresults/src/org/labkey/testresults/view/rundown.jsp b/testresults/src/org/labkey/testresults/view/rundown.jsp
index 0c42692e..63088e79 100644
--- a/testresults/src/org/labkey/testresults/view/rundown.jsp
+++ b/testresults/src/org/labkey/testresults/view/rundown.jsp
@@ -563,7 +563,7 @@ $(function() {
$(self).text(isTrain ? 'Untrain' : 'Train');
return;
}
- alert("Failure removing run. Contact Yuval");
+ alert("Failed to update training set." + (data.error ? " " + data.error : ""));
}, "json");
});
diff --git a/testresults/src/org/labkey/testresults/view/trainingdata.jsp b/testresults/src/org/labkey/testresults/view/trainingdata.jsp
index f19abf73..69d4d5cd 100644
--- a/testresults/src/org/labkey/testresults/view/trainingdata.jsp
+++ b/testresults/src/org/labkey/testresults/view/trainingdata.jsp
@@ -167,7 +167,7 @@
|
|
- "><%=h(user.getUsername())%>
+ "><%=h(user.getUsername())%>
|
<% if (user.isActive()) { %>
@@ -220,7 +220,7 @@
<% for (User user : noRunsForUser) { %>
|
|
- ">
+ ">
<%=h(user.getUsername())%>
|
@@ -254,7 +254,7 @@
}
return;
}
- alert("Failure removing run. Contact Yuval");
+ alert("Failed to update training set." + (data.error ? " " + data.error : ""));
}, "json");
});
@@ -345,7 +345,7 @@
url.searchParams.set('maxRuns', maxRuns);
url.searchParams.set('minRuns', minRuns);
$.post(url.toString(), csrf_header, function(data) {
- if (data.success) {
+ if (data.Success) {
$('#retrain-all-status').text('Retrained ' + data.usersRetrained + ' computers with ' + data.totalTrainRuns + ' runs. Reloading...');
location.reload();
} else {
diff --git a/testresults/src/org/labkey/testresults/view/user.jsp b/testresults/src/org/labkey/testresults/view/user.jsp
index 4ef8a471..0a59658c 100644
--- a/testresults/src/org/labkey/testresults/view/user.jsp
+++ b/testresults/src/org/labkey/testresults/view/user.jsp
@@ -51,7 +51,7 @@
HttpServletRequest req = getViewContext().getRequest();
String startDate = req.getParameter("start");
String endDate = req.getParameter("end");
- String user = req.getParameter("user");
+ String user = req.getParameter("username");
boolean showSingleUser = user != null && !user.isEmpty();
DateFormat df = new SimpleDateFormat("MM/dd/yyyy");
Date today = new Date();
@@ -228,7 +228,7 @@
}
let url = <%=jsURL(new ActionURL(TestResultsController.ShowUserAction.class, c))%>;
- url.searchParams.set('user', $("#users").val() || "");
+ url.searchParams.set('username', $("#users").val() || "");
url.searchParams.set('start', startDate);
url.searchParams.set('end', endDate);
url.searchParams.set('datainclude', $("#data-include").val());
@@ -268,7 +268,7 @@
trainObj.setAttribute("runTrained", !isTrainRun);
trainObj.innerHTML = !isTrainRun ? "Remove from training set" : "Add to training set";
} else {
- alert(data);
+ alert("Failed to update training set." + (data.error ? " " + data.error : ""));
}
}, "json");
});
diff --git a/testresults/test/sampledata/testresults/pc1-run-0114-disposable.xml b/testresults/test/sampledata/testresults/pc1-run-0114-disposable.xml
new file mode 100644
index 00000000..c5620843
--- /dev/null
+++ b/testresults/test/sampledata/testresults/pc1-run-0114-disposable.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/testresults/test/sampledata/testresults/pc1-run-0115-clean.xml b/testresults/test/sampledata/testresults/pc1-run-0115-clean.xml
new file mode 100644
index 00000000..1f2964a3
--- /dev/null
+++ b/testresults/test/sampledata/testresults/pc1-run-0115-clean.xml
@@ -0,0 +1,312 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# Nightly started Thursday, January 15, 2026 9:00 PM
+
+[21:00] 1.0 TestAlpha (fr)
+[21:03] 1.0 TestBeta (fr)
+[21:07] 1.0 TestGamma (fr)
+[21:10] 1.0 TestDelta (fr)
+[21:14] 1.0 TestEpsilon (fr)
+[21:18] 1.0 Test006 (fr)
+[21:21] 1.0 Test007 (fr)
+[21:25] 1.0 Test008 (fr)
+[21:28] 1.0 Test009 (fr)
+[21:32] 1.0 Test010 (fr)
+[21:36] 1.0 Test011 (fr)
+[21:39] 1.0 Test012 (fr)
+[21:43] 1.0 Test013 (fr)
+[21:46] 1.0 Test014 (fr)
+[21:50] 1.0 Test015 (fr)
+[21:54] 1.0 Test016 (fr)
+[21:57] 1.0 Test017 (fr)
+[22:01] 1.0 Test018 (fr)
+[22:04] 1.0 Test019 (fr)
+[22:08] 1.0 Test020 (fr)
+[22:12] 1.0 Test021 (fr)
+[22:15] 1.0 Test022 (fr)
+[22:19] 1.0 Test023 (fr)
+[22:22] 1.0 Test024 (fr)
+[22:26] 1.0 Test025 (fr)
+[22:30] 1.0 Test026 (fr)
+[22:33] 1.0 Test027 (fr)
+[22:37] 1.0 Test028 (fr)
+[22:40] 1.0 Test029 (fr)
+[22:44] 1.0 Test030 (fr)
+[22:48] 1.0 Test031 (fr)
+[22:51] 1.0 Test032 (fr)
+[22:55] 1.0 Test033 (fr)
+[22:58] 1.0 Test034 (fr)
+[23:02] 1.0 Test035 (fr)
+[23:06] 1.0 Test036 (fr)
+[23:09] 1.0 Test037 (fr)
+[23:13] 1.0 Test038 (fr)
+[23:16] 1.0 Test039 (fr)
+[23:20] 1.0 Test040 (fr)
+[23:24] 1.0 Test041 (fr)
+[23:27] 1.0 Test042 (fr)
+[23:31] 1.0 Test043 (fr)
+[23:34] 1.0 Test044 (fr)
+[23:38] 1.0 Test045 (fr)
+[23:42] 1.0 Test046 (fr)
+[23:45] 1.0 Test047 (fr)
+[23:49] 1.0 Test048 (fr)
+[23:52] 1.0 Test049 (fr)
+[23:56] 1.0 Test050 (fr)
+[00:00] 1.0 Test051 (fr)
+[00:03] 1.0 Test052 (fr)
+[00:07] 1.0 Test053 (fr)
+[00:10] 1.0 Test054 (fr)
+[00:14] 1.0 Test055 (fr)
+[00:18] 1.0 Test056 (fr)
+[00:21] 1.0 Test057 (fr)
+[00:25] 1.0 Test058 (fr)
+[00:28] 1.0 Test059 (fr)
+[00:32] 1.0 Test060 (fr)
+[00:36] 1.0 Test061 (fr)
+[00:39] 1.0 Test062 (fr)
+[00:43] 1.0 Test063 (fr)
+[00:46] 1.0 Test064 (fr)
+[00:50] 1.0 Test065 (fr)
+[00:54] 1.0 Test066 (fr)
+[00:57] 1.0 Test067 (fr)
+[01:01] 1.0 Test068 (fr)
+[01:04] 1.0 Test069 (fr)
+[01:08] 1.0 Test070 (fr)
+[01:12] 1.0 Test071 (fr)
+[01:15] 1.0 Test072 (fr)
+[01:19] 1.0 Test073 (fr)
+[01:22] 1.0 Test074 (fr)
+[01:26] 1.0 Test075 (fr)
+[01:30] 1.0 Test076 (fr)
+[01:33] 1.0 Test077 (fr)
+[01:37] 1.0 Test078 (fr)
+[01:40] 1.0 Test079 (fr)
+[01:44] 1.0 Test080 (fr)
+[01:48] 1.0 Test081 (fr)
+[01:51] 1.0 Test082 (fr)
+[01:55] 1.0 Test083 (fr)
+[01:58] 1.0 Test084 (fr)
+[02:02] 1.0 Test085 (fr)
+[02:06] 1.0 Test086 (fr)
+[02:09] 1.0 Test087 (fr)
+[02:13] 1.0 Test088 (fr)
+[02:16] 1.0 Test089 (fr)
+[02:20] 1.0 Test090 (fr)
+[02:24] 1.0 Test091 (fr)
+[02:27] 1.0 Test092 (fr)
+[02:31] 1.0 Test093 (fr)
+[02:34] 1.0 Test094 (fr)
+[02:38] 1.0 Test095 (fr)
+[02:42] 1.0 Test096 (fr)
+[02:45] 1.0 Test097 (fr)
+[02:49] 1.0 Test098 (fr)
+[02:52] 1.0 Test099 (fr)
+[02:56] 1.0 Test100 (fr)
+[03:00] 1.0 Test101 (fr)
+[03:03] 1.0 Test102 (fr)
+[03:07] 1.0 Test103 (fr)
+[03:10] 1.0 Test104 (fr)
+[03:14] 1.0 Test105 (fr)
+[03:18] 1.0 Test106 (fr)
+[03:21] 1.0 Test107 (fr)
+[03:25] 1.0 Test108 (fr)
+[03:28] 1.0 Test109 (fr)
+[03:32] 1.0 Test110 (fr)
+[03:36] 1.0 Test111 (fr)
+[03:39] 1.0 Test112 (fr)
+[03:43] 1.0 Test113 (fr)
+[03:46] 1.0 Test114 (fr)
+[03:50] 1.0 Test115 (fr)
+[03:54] 1.0 Test116 (fr)
+[03:57] 1.0 Test117 (fr)
+[04:01] 1.0 Test118 (fr)
+[04:04] 1.0 Test119 (fr)
+[04:08] 1.0 Test120 (fr)
+[04:12] 1.0 Test121 (fr)
+[04:15] 1.0 Test122 (fr)
+[04:19] 1.0 Test123 (fr)
+[04:22] 1.0 Test124 (fr)
+[04:26] 1.0 Test125 (fr)
+[04:30] 1.0 Test126 (fr)
+[04:33] 1.0 Test127 (fr)
+[04:37] 1.0 Test128 (fr)
+[04:40] 1.0 Test129 (fr)
+[04:44] 1.0 Test130 (fr)
+[04:48] 1.0 Test131 (fr)
+[04:51] 1.0 Test132 (fr)
+[04:55] 1.0 Test133 (fr)
+[04:58] 1.0 Test134 (fr)
+[05:02] 1.0 Test135 (fr)
+[05:06] 1.0 Test136 (fr)
+[05:09] 1.0 Test137 (fr)
+[05:13] 1.0 Test138 (fr)
+[05:16] 1.0 Test139 (fr)
+[05:20] 1.0 Test140 (fr)
+[05:24] 1.0 Test141 (fr)
+[05:27] 1.0 Test142 (fr)
+[05:31] 1.0 Test143 (fr)
+[05:34] 1.0 Test144 (fr)
+[05:38] 1.0 Test145 (fr)
+[05:42] 1.0 Test146 (fr)
+[05:45] 1.0 Test147 (fr)
+[05:49] 1.0 Test148 (fr)
+[05:52] 1.0 Test149 (fr)
+[05:56] 1.0 Test150 (fr)
+
+
diff --git a/testresults/test/sampledata/testresults/pc1-run-0116-failures.xml b/testresults/test/sampledata/testresults/pc1-run-0116-failures.xml
new file mode 100644
index 00000000..d2904402
--- /dev/null
+++ b/testresults/test/sampledata/testresults/pc1-run-0116-failures.xml
@@ -0,0 +1,167 @@
+
+
+
+
+
+ System.Exception: Assertion failed at TestFailOne line 42
+ at Skyline.Test.TestFailOne() in TestFailOne.cs:line 42
+
+
+ System.NullReferenceException: Object reference not set to an instance of an object
+ at Skyline.Test.TestFailTwo() in TestFailTwo.cs:line 17
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/testresults/test/sampledata/testresults/pc1-run-0117-leaks.xml b/testresults/test/sampledata/testresults/pc1-run-0117-leaks.xml
new file mode 100644
index 00000000..d0c2ac45
--- /dev/null
+++ b/testresults/test/sampledata/testresults/pc1-run-0117-leaks.xml
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/testresults/test/sampledata/testresults/pc2-run-0115-clean.xml b/testresults/test/sampledata/testresults/pc2-run-0115-clean.xml
new file mode 100644
index 00000000..05f8e684
--- /dev/null
+++ b/testresults/test/sampledata/testresults/pc2-run-0115-clean.xml
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/testresults/test/sampledata/testresults/pc2-run-0116-failures.xml b/testresults/test/sampledata/testresults/pc2-run-0116-failures.xml
new file mode 100644
index 00000000..95d16b64
--- /dev/null
+++ b/testresults/test/sampledata/testresults/pc2-run-0116-failures.xml
@@ -0,0 +1,163 @@
+
+
+
+
+
+ System.Exception: Assertion failed at TestFailOne line 42
+ at Skyline.Test.TestFailOne() in TestFailOne.cs:line 42
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/testresults/test/sampledata/testresults/pc2-run-0117-leaks.xml b/testresults/test/sampledata/testresults/pc2-run-0117-leaks.xml
new file mode 100644
index 00000000..c8bbe70b
--- /dev/null
+++ b/testresults/test/sampledata/testresults/pc2-run-0117-leaks.xml
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/testresults/test/src/org/labkey/test/tests/testresults/TestResultsTest.java b/testresults/test/src/org/labkey/test/tests/testresults/TestResultsTest.java
new file mode 100644
index 00000000..28b57a81
--- /dev/null
+++ b/testresults/test/src/org/labkey/test/tests/testresults/TestResultsTest.java
@@ -0,0 +1,910 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.labkey.test.tests.testresults;
+
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.labkey.remoteapi.Connection;
+import org.labkey.remoteapi.query.SelectRowsCommand;
+import org.labkey.remoteapi.query.SelectRowsResponse;
+import org.labkey.remoteapi.query.Sort;
+import org.labkey.test.BaseWebDriverTest;
+import org.labkey.test.Locator;
+import org.labkey.test.TestFileUtils;
+import org.labkey.test.WebTestHelper;
+import org.labkey.test.categories.External;
+import org.labkey.test.categories.MacCossLabModules;
+import org.labkey.test.util.APIContainerHelper;
+import org.labkey.test.util.APITestHelper;
+import org.labkey.test.util.LogMethod;
+import org.labkey.test.util.PortalHelper;
+import org.labkey.test.util.PostgresOnlyTest;
+import org.labkey.test.util.TextSearcher;
+
+import java.io.File;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@Category({External.class, MacCossLabModules.class})
+@BaseWebDriverTest.ClassTimeout(minutes = 5)
+public class TestResultsTest extends BaseWebDriverTest implements PostgresOnlyTest
+{
+ private static final String PROJECT_NAME = "TestResultsTest" + TRICKY_CHARACTERS_FOR_PROJECT_NAMES;
+ static final String COMPUTER_NAME_1 = "TEST-PC-1";
+ static final String COMPUTER_NAME_2 = "TEST-PC-2";
+
+ private static final Locator SUBMIT_BUTTON = Locator.css("input[type='submit'][value='Submit']");
+
+ // XPath for the problems matrix table (header cell contains "Fail: | Leak: | Hang:")
+ private static final String PROBLEMS_TABLE_XPATH =
+ "//table[contains(@class,'decoratedtable')]" +
+ "[.//td[contains(.,'Fail:') and contains(.,'Leak:') and contains(.,'Hang:')]]";
+
+ // Run IDs populated in @BeforeClass, used across test methods
+ private static int _disposableRunId = -1;
+ private static int _cleanRunId = -1;
+ private static int _failRunId = -1;
+ private static int _leakRunId = -1;
+
+ @BeforeClass
+ public static void setupProject()
+ {
+ TestResultsTest init = getCurrentTest();
+ init.doSetup();
+ }
+
+ @LogMethod
+ private void doSetup()
+ {
+ _containerHelper.createProject(PROJECT_NAME, null);
+ _containerHelper.enableModule("TestResults");
+ new PortalHelper(this).addWebPart("Test Results");
+
+ // TEST-PC-1 runs
+ postSampleXml("testresults/pc1-run-0114-disposable.xml");
+ postSampleXml("testresults/pc1-run-0115-clean.xml");
+ postSampleXml("testresults/pc1-run-0116-failures.xml");
+ postSampleXml("testresults/pc1-run-0117-leaks.xml");
+
+ // TEST-PC-2 runs on the same dates
+ postSampleXml("testresults/pc2-run-0115-clean.xml");
+ postSampleXml("testresults/pc2-run-0116-failures.xml");
+ postSampleXml("testresults/pc2-run-0117-leaks.xml");
+
+ // All runs in this fresh container are our sample runs, sorted by posttime ascending.
+ // PC2 runs are interleaved with PC1 runs, so identify PC1 runs by computer name.
+ List