Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 257 additions & 0 deletions testresults/src/org/labkey/testresults/TestResultsController.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -1037,6 +1039,261 @@ public Object execute(Object o, BindException errors)
}
}

@RequiresSiteAdmin
public static class RetrainAllAction extends MutatingApiAction<Object>
{
@Override
public Object execute(Object o, BindException errors)
{
var req = getViewContext().getRequest();
String mode = req.getParameter("mode");
if (mode == null || mode.isEmpty())
mode = "reset";
boolean incremental = mode.equalsIgnoreCase("incremental");

int maxRuns = 20;
String maxRunsParam = req.getParameter("maxRuns");
if (maxRunsParam == null || maxRunsParam.isEmpty())
maxRunsParam = req.getParameter("targetRuns"); // backwards compatibility
if (maxRunsParam != null && !maxRunsParam.isEmpty())
{
try { maxRuns = Integer.parseInt(maxRunsParam); }
catch (NumberFormatException ignored) { }
}
if (maxRuns < 1) maxRuns = 1;
if (maxRuns > 100) maxRuns = 100;

int minRuns = 5;
String minRunsParam = req.getParameter("minRuns");
if (minRunsParam != null && !minRunsParam.isEmpty())
{
try { minRuns = Integer.parseInt(minRunsParam); }
catch (NumberFormatException ignored) { }
}
if (minRuns < 1) minRuns = 1;
if (minRuns > maxRuns) minRuns = maxRuns;

Container c = getContainer();
String containerPath = c.getPath();
int expectedDuration = containerPath.toLowerCase().contains("perf") ? 720 : 540;

// Only look back 1.5x maxRuns days to avoid ancient data
int lookbackDays = (int) Math.ceil(maxRuns * 1.5);
java.sql.Timestamp cutoffDate = new java.sql.Timestamp(
System.currentTimeMillis() - (long) lookbackDays * 24 * 60 * 60 * 1000);

DbScope scope = TestResultsSchema.getSchema().getScope();
try (DbScope.Transaction transaction = scope.ensureTransaction())
{
// Get unique user IDs from recent testruns for this container
SQLFragment allUsersSql = new SQLFragment();
allUsersSql.append(
"SELECT DISTINCT userid FROM " + TestResultsSchema.getTableInfoTestRuns() +
" WHERE container = ? AND posttime >= ?");
allUsersSql.add(c.getEntityId());
allUsersSql.add(cutoffDate);
List<Integer> allUserIds = new ArrayList<>();
new SqlSelector(scope, allUsersSql).forEach(rs ->
allUserIds.add(rs.getInt("userid")));

// Get current trainrun counts per user
Map<Integer, Integer> userTrainCounts = new HashMap<>();
for (int userId : allUserIds)
userTrainCounts.put(userId, 0);

if (incremental)
{
// In incremental mode, get existing counts
SQLFragment countsSql = new SQLFragment();
countsSql.append(
"SELECT r.userid, COUNT(tr.runid) as traincount" +
" FROM " + TestResultsSchema.getTableInfoTrain() + " tr" +
" JOIN " + TestResultsSchema.getTableInfoTestRuns() + " r ON tr.runid = r.id" +
" WHERE r.container = ?" +
" GROUP BY r.userid");
countsSql.add(c.getEntityId());
new SqlSelector(scope, countsSql).forEach(rs ->
userTrainCounts.put(rs.getInt("userid"), rs.getInt("traincount")));
}
else
{
// Reset mode: delete all trainruns for this container
SQLFragment deleteTrainSql = new SQLFragment();
deleteTrainSql.append(
"DELETE FROM " + TestResultsSchema.getTableInfoTrain() +
" WHERE runid IN (SELECT id FROM " + TestResultsSchema.getTableInfoTestRuns() +
" WHERE container = ?)");
deleteTrainSql.add(c.getEntityId());
new SqlExecutor(scope).execute(deleteTrainSql);

// Delete all userdata for this container
SQLFragment deleteUserDataSql = new SQLFragment();
deleteUserDataSql.append(
"DELETE FROM " + TestResultsSchema.getTableInfoUserData() +
" WHERE container = ?");
deleteUserDataSql.add(c.getEntityId());
new SqlExecutor(scope).execute(deleteUserDataSql);
}

int usersRetrained = 0;
int totalTrainRuns = 0;
final int minRunsRequired = minRuns;

for (int userId : allUserIds)
{
// Get existing trainrun IDs for this user
SQLFragment existingRunsSql = new SQLFragment();
existingRunsSql.append(
"SELECT tr.runid FROM " + TestResultsSchema.getTableInfoTrain() + " tr" +
" JOIN " + TestResultsSchema.getTableInfoTestRuns() + " r ON tr.runid = r.id" +
" WHERE r.userid = ? AND r.container = ?");
existingRunsSql.add(userId);
existingRunsSql.add(c.getEntityId());
Set<Integer> existingRunIds = new HashSet<>();
new SqlSelector(scope, existingRunsSql).forEach(rs ->
existingRunIds.add(rs.getInt("runid")));

// Find all clean runs within lookback period (for both modes)
SQLFragment cleanRunsSql = new SQLFragment();
cleanRunsSql.append(
"SELECT tr.id FROM " + TestResultsSchema.getTableInfoTestRuns() + " tr" +
" WHERE tr.userid = ? AND tr.container = ?" +
" AND tr.posttime >= ?" +
" AND tr.failedtests = 0 AND tr.leakedtests = 0" +
" AND tr.passedtests > 0 AND tr.flagged = false" +
" AND tr.duration >= ?" +
" AND NOT EXISTS (SELECT 1 FROM " + TestResultsSchema.getTableInfoHangs() +
" h WHERE h.testrunid = tr.id)" +
" ORDER BY tr.posttime DESC");
cleanRunsSql.add(userId);
cleanRunsSql.add(c.getEntityId());
cleanRunsSql.add(cutoffDate);
cleanRunsSql.add(expectedDuration);

List<Integer> recentCleanRunIds = new ArrayList<>();
new SqlSelector(scope, cleanRunsSql).forEach(rs ->
recentCleanRunIds.add(rs.getInt("id")));

// Determine final set of trainrun IDs
final List<Integer> finalRunIds = new ArrayList<>();
final int maxRunsLimit = maxRuns;
if (incremental && !existingRunIds.isEmpty())
{
// Incremental: combine existing + new recent clean runs, keep most recent maxRuns
// Get all existing trainruns with posttimes for sorting
SQLFragment allCandidatesSql = new SQLFragment();
allCandidatesSql.append(
"SELECT id, posttime FROM " + TestResultsSchema.getTableInfoTestRuns() +
" WHERE userid = ? AND container = ?" +
" AND (id = ANY(?) OR id = ANY(?))" +
" ORDER BY posttime DESC");
allCandidatesSql.add(userId);
allCandidatesSql.add(c.getEntityId());
allCandidatesSql.add(existingRunIds.toArray(new Integer[0]));
allCandidatesSql.add(recentCleanRunIds.toArray(new Integer[0]));

new SqlSelector(scope, allCandidatesSql).forEach(rs -> {
if (finalRunIds.size() < maxRunsLimit)
finalRunIds.add(rs.getInt("id"));
});
}
else
{
// Reset mode: just use recent clean runs up to maxRuns
if (recentCleanRunIds.size() > maxRuns)
finalRunIds.addAll(recentCleanRunIds.subList(0, maxRuns));
else
finalRunIds.addAll(recentCleanRunIds);
}

// Skip if total runs is below minimum threshold
if (finalRunIds.size() < minRunsRequired)
continue;

// Determine runs to add and remove
Set<Integer> finalRunSet = new HashSet<>(finalRunIds);
List<Integer> runsToAdd = new ArrayList<>();
List<Integer> runsToRemove = new ArrayList<>();

for (int runId : finalRunIds)
{
if (!existingRunIds.contains(runId))
runsToAdd.add(runId);
}
for (int runId : existingRunIds)
{
if (!finalRunSet.contains(runId))
runsToRemove.add(runId);
}

// Remove old trainruns
for (int runId : runsToRemove)
{
SQLFragment deleteTrainSql = new SQLFragment();
deleteTrainSql.append(
"DELETE FROM " + TestResultsSchema.getTableInfoTrain() + " WHERE runid = ?");
deleteTrainSql.add(runId);
new SqlExecutor(scope).execute(deleteTrainSql);
}

// Insert new trainruns
for (int runId : runsToAdd)
{
SQLFragment insertTrainSql = new SQLFragment();
insertTrainSql.append(
"INSERT INTO " + TestResultsSchema.getTableInfoTrain() + " (runid) VALUES (?)");
insertTrainSql.add(runId);
new SqlExecutor(scope).execute(insertTrainSql);
}

// Set active=true only if user has at least maxRuns
boolean isActive = finalRunIds.size() >= maxRuns;

// Calculate stats and upsert into userdata
SQLFragment upsertStatsSql = new SQLFragment();
upsertStatsSql.append(
"INSERT INTO " + TestResultsSchema.getTableInfoUserData() +
" (userid, container, meantestsrun, meanmemory, stddevtestsrun, stddevmemory, active)" +
" SELECT ?, ?, avg(passedtests), avg(averagemem)," +
" stddev_pop(passedtests), stddev_pop(averagemem), ?" +
" FROM " + TestResultsSchema.getTableInfoTestRuns() +
" WHERE id = ANY(?)" +
" ON CONFLICT(userid, container) DO UPDATE SET" +
" meantestsrun = excluded.meantestsrun," +
" meanmemory = excluded.meanmemory," +
" stddevtestsrun = excluded.stddevtestsrun," +
" stddevmemory = excluded.stddevmemory," +
" active = excluded.active");
upsertStatsSql.add(userId);
upsertStatsSql.add(c.getEntityId());
upsertStatsSql.add(isActive);
upsertStatsSql.add(finalRunIds.toArray(new Integer[0]));
new SqlExecutor(scope).execute(upsertStatsSql);

usersRetrained++;
totalTrainRuns += runsToAdd.size();
}

transaction.commit();

ApiSimpleResponse response = new ApiSimpleResponse();
response.put("success", true);
response.put("usersRetrained", usersRetrained);
response.put("totalTrainRuns", totalTrainRuns);
response.put("mode", mode);
return response;
}
catch (Exception e)
{
_log.error("Error in RetrainAllAction", e);
ApiSimpleResponse response = new ApiSimpleResponse();
response.put("success", false);
response.put("error", e.getMessage());
return response;
}
}
}

/**
* action for posting test output as an xml file
*/
Expand Down
1 change: 1 addition & 0 deletions testresults/src/org/labkey/testresults/view/errorFiles.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
Container c = getContainer();
%>

<% request.setAttribute("activeTab", "errors"); %>
<%@include file="menu.jsp" %>

<p>All the files listed below at one point or another failed to post. When a run is successfully posted through this page it gets removed from the list.</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@
}
%>

<% request.setAttribute("activeTab", ""); %>
<%@include file="menu.jsp" %>

<style>
Expand Down
1 change: 1 addition & 0 deletions testresults/src/org/labkey/testresults/view/flagged.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
TestsDataBean data = (TestsDataBean)me.getModelBean();
%>

<% request.setAttribute("activeTab", "flags"); %>
<%@include file="menu.jsp" %>

<p>Runs which are flagged will not show up in the Overview breakdown, Long Term, and Failure pages. This includes graphs, charts, and any other sort of data visualization.</p>
Expand Down
2 changes: 1 addition & 1 deletion testresults/src/org/labkey/testresults/view/longTerm.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
}
%>

<% request.setAttribute("activeTab", "longterm"); %>
<%@include file="menu.jsp" %>
<br />
<form action="<%=h(new ActionURL(TestResultsController.LongTermAction.class, c))%>">
View Type: <select id="view-type-combobox" name="viewType">
<option disabled selected> -- select an option -- </option>
Expand Down
76 changes: 65 additions & 11 deletions testresults/src/org/labkey/testresults/view/menu.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,73 @@
<%
final String menuContextPath = AppProps.getInstance().getContextPath();
Container menuContainer = getViewContext().getContainer();
// activeTab is set by parent JSP via request attribute before including menu.jsp
String activeTab = (String) request.getAttribute("activeTab");
if (activeTab == null) activeTab = "";
%>

<div id="menu">
<style>
#menu.testresults-nav {
background: #4b2e83;
padding: 0 !important;
padding-top: 6px !important;
margin: 0 0 12px 0 !important;
height: 40px !important;
overflow: visible;
}
#menu.testresults-nav ul {
list-style: none !important;
margin: 0 !important;
padding: 0 0 0 10px !important;
display: inline;
}
#menu.testresults-nav li {
display: inline;
margin: 0;
padding: 0;
}
#menu.testresults-nav li:hover {
background-color: transparent !important;
}
.testresults-nav .nav-tab {
display: inline-block;
padding: 4px 10px;
color: #fff;
text-decoration: none;
border-radius: 4px;
transition: background 0.2s;
}
.testresults-nav .nav-tab:hover {
background: #B8A506;
}
.testresults-nav .nav-tab.active {
background: rgba(255, 255, 255, 0.25);
font-weight: bold;
}
#menu.testresults-nav #stats {
float: right;
margin-right: 40px;
margin-top: 6px;
color: #fff;
font-weight: 600;
}
#menu.testresults-nav #uw {
float: right;
margin-top: 2px;
}
</style>

<div id="menu" class="testresults-nav">
<img src="<%=getWebappURL("TestResults/img/uw.png")%>" id="uw" alt="UW">
<span id="stats"></span>
<ul>
<li><a href="<%=h(new ActionURL(TestResultsController.BeginAction.class, menuContainer))%>" style="color:#fff;">-Overview</a></li>
<li><a href="<%=h(new ActionURL(TestResultsController.ShowUserAction.class, menuContainer))%>" style="color:#fff;">-User</a></li>
<li><a href="<%=h(new ActionURL(TestResultsController.ShowRunAction.class, menuContainer))%>" style="color:#fff;">-Run</a></li>
<li><a href="<%=h(new ActionURL(TestResultsController.LongTermAction.class, menuContainer))%>" style="color:#fff;">-Long Term</a></li>
<li><a href="<%=h(new ActionURL(TestResultsController.ShowFlaggedAction.class, menuContainer))%>" style="color:#fff;">-Flags</a></li>
<li><a href="<%=h(new ActionURL(TestResultsController.TrainingDataViewAction.class, menuContainer))%>" style="color:#fff;">-Training Data</a></li>
<li><a href="<%=h(new ActionURL(TestResultsController.ErrorFilesAction.class, menuContainer))%>" style="color:#fff;">-Posting Errors</a></li>
<li><a href="/home/issues/project-begin.view" target="_blank" title="Report bugs/Request features. Use 'TestResults' as area when creating new issue" style="color:#fff;">-Issues</a></li>
<img src="<%=getWebappURL("TestResults/img/uw.png")%>" id="uw">
<span id="stats"></span>
<li><a href="<%=h(new ActionURL(TestResultsController.BeginAction.class, menuContainer))%>" class="<%=h("nav-tab" + (activeTab.equals("overview") ? " active" : ""))%>">Overview</a></li>
<li><a href="<%=h(new ActionURL(TestResultsController.ShowUserAction.class, menuContainer))%>" class="<%=h("nav-tab" + (activeTab.equals("user") ? " active" : ""))%>">User</a></li>
<li><a href="<%=h(new ActionURL(TestResultsController.ShowRunAction.class, menuContainer))%>" class="<%=h("nav-tab" + (activeTab.equals("run") ? " active" : ""))%>">Run</a></li>
<li><a href="<%=h(new ActionURL(TestResultsController.LongTermAction.class, menuContainer))%>" class="<%=h("nav-tab" + (activeTab.equals("longterm") ? " active" : ""))%>">Long Term</a></li>
<li><a href="<%=h(new ActionURL(TestResultsController.ShowFlaggedAction.class, menuContainer))%>" class="<%=h("nav-tab" + (activeTab.equals("flags") ? " active" : ""))%>">Flags</a></li>
<li><a href="<%=h(new ActionURL(TestResultsController.TrainingDataViewAction.class, menuContainer))%>" class="<%=h("nav-tab" + (activeTab.equals("trainingdata") ? " active" : ""))%>">Training Data</a></li>
<li><a href="<%=h(new ActionURL(TestResultsController.ErrorFilesAction.class, menuContainer))%>" class="<%=h("nav-tab" + (activeTab.equals("errors") ? " active" : ""))%>">Posting Errors</a></li>
<li><a href="/home/issues/project-begin.view" target="_blank" title="Report bugs/Request features. Use 'TestResults' as area when creating new issue" class="nav-tab">Issues</a></li>
</ul>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@
}
%>

<% request.setAttribute("activeTab", ""); %>
<%@include file="menu.jsp" %>
<br />
<form action="<%=h(new ActionURL(TestResultsController.ShowFailures.class, c))%>">
View Type: <select name="viewType">
<option disabled selected> -- select an option -- </option>
Expand Down
Loading