Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stress test framework improvements #2239

Merged
merged 9 commits into from
Feb 4, 2025
18 changes: 16 additions & 2 deletions src/org/labkey/test/WebTestHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.json.JSONObject;
import org.junit.Assert;
import org.labkey.remoteapi.CommandException;
import org.labkey.remoteapi.CommandResponse;
import org.labkey.remoteapi.Connection;
import org.labkey.remoteapi.SimplePostCommand;
import org.labkey.remoteapi.query.DeleteRowsCommand;
import org.labkey.remoteapi.query.Filter;
import org.labkey.remoteapi.query.SaveRowsResponse;
import org.labkey.remoteapi.query.SelectRowsCommand;
import org.labkey.serverapi.reader.Readers;
import org.labkey.test.util.InstallCert;
Expand Down Expand Up @@ -82,10 +84,12 @@
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -113,6 +117,7 @@ public class WebTestHelper
private static final Map<String, Map<String, Cookie>> savedCookies = new HashMap<>();
private static final Map<String, String> savedSessionKeys = new HashMap<>();
private static final Map<String, String> savedApiKeys = new HashMap<>();
private static final Set<String> deletedApiKeys = new HashSet<>();

static { TestProperties.load(); }

Expand Down Expand Up @@ -195,8 +200,10 @@ public static void deleteApiKey(Connection connection, String apiKey)
{
DeleteRowsCommand deleteRowsCommand = new DeleteRowsCommand("core", "apiKeys");
deleteRowsCommand.setRows(rows);
deleteRowsCommand.execute(connection, null);
SaveRowsResponse response = deleteRowsCommand.execute(connection, null);
savedApiKeys.remove(apiKey);
deletedApiKeys.add(apiKey);
Assert.assertEquals("Wrong number of rows affected by apiKey deletion", 1, response.getRowsAffected());
}
else
{
Expand All @@ -210,7 +217,14 @@ public static void deleteApiKey(Connection connection, String apiKey)
}
else
{
TestLogger.warn("Refusing to delete an API key not created by this test");
if (deletedApiKeys.contains(apiKey))
{
TestLogger.warn("API key already deleted");
}
else
{
TestLogger.warn("Refusing to delete an API key not created by 'WebTestHelper.createApiKey'");
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/org/labkey/test/components/ui/grids/GridFilterModal.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ public FilterExpressionPanel selectExpressionTab()
return elementCache().filterExpressionPanel();
}

public GridFilterModal setFilter(FilterExpressionPanel.Expression expression)
{
selectExpressionTab().setFilter(expression);
return this;
}

/**
* Select the facet tab for the current field. Will throw <code>NoSuchElementException</code> if tab isn't present.
* @return panel for configuring faceted filter
Expand Down
2 changes: 2 additions & 0 deletions src/org/labkey/test/components/ui/search/SampleFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
*/
public class SampleFinder extends WebDriverComponent<SampleFinder.ElementCache>
{
public static final String ALL_SAMPLE_TYPES = "All Sample Types";

private final WebElement _el;
private final WebDriver _driver;

Expand Down
49 changes: 49 additions & 0 deletions src/org/labkey/test/stress/ActivityRecorder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.labkey.test.stress;

import org.labkey.remoteapi.CommandException;
import org.labkey.remoteapi.Connection;
import org.labkey.remoteapi.miniprofiler.RequestInfo;
import org.labkey.test.util.LogMethod;
import org.labkey.test.util.LoggedParam;
import org.labkey.test.util.TestLogger;

import java.io.IOException;
import java.util.List;
import java.util.function.Supplier;

public class ActivityRecorder
{
private final RecentRequestsCollector recentRequestsCollector;

public ActivityRecorder(Connection connection) throws IOException, CommandException
{
recentRequestsCollector = new RecentRequestsCollector(connection);
}

@LogMethod
public <R> R recordActivity(@LoggedParam String description, Supplier<R> activity) throws IOException, CommandException
{
R result = activity.get();

List<RequestInfo> recentRequests = recentRequestsCollector.getRecentRequests();
TestLogger.log("%s triggered %s requests".formatted(description, recentRequests.size()));

return result;
}

public void recordActivity(String description, Runnable activity) throws IOException, CommandException
{
recordActivity(description, () -> {
activity.run();
return null;
});
}

public List<RequestInfo> skipRecentRequests() throws IOException, CommandException
{
List<RequestInfo> recentRequests = recentRequestsCollector.getRecentRequests();
TestLogger.log("Skipping %s requests".formatted(recentRequests.size()));

return recentRequests;
}
}
4 changes: 2 additions & 2 deletions src/org/labkey/test/stress/HarConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
public class HarConverter
{
private static final Logger LOG = LogManager.getLogger(HarConverter.class);
private static final Set<ControllerActionId> excludedActions = Set.of(new ControllerActionId("login", "whoami"));
public static final Set<ControllerActionId> EXCLUDED_ACTIONS = Set.of(new ControllerActionId("login", "whoami"));

private final String inputParam;

Expand Down Expand Up @@ -213,7 +213,7 @@ private boolean shouldIncludeHarEntry(JSONObject entry)
try
{
ControllerActionId actionId = new ControllerActionId(url);
if (excludedActions.contains(actionId) || StringUtils.isBlank(actionId.getAction()) || "app".equals(actionId.getAction()))
if (EXCLUDED_ACTIONS.contains(actionId) || StringUtils.isBlank(actionId.getAction()) || "app".equals(actionId.getAction()))
{
LOG.info("Skipping request: " + url);
return false;
Expand Down
29 changes: 29 additions & 0 deletions src/org/labkey/test/stress/RecentRequestsCollector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.labkey.test.stress;

import org.labkey.remoteapi.CommandException;
import org.labkey.remoteapi.Connection;
import org.labkey.remoteapi.miniprofiler.RecentRequestsCommand;
import org.labkey.remoteapi.miniprofiler.RequestInfo;
import org.labkey.test.util.Crawler;

import java.io.IOException;
import java.util.List;

public class RecentRequestsCollector
{
private final Connection _connection;
private long lastRequestId = 0L;

public RecentRequestsCollector(Connection connection) throws IOException, CommandException
{
_connection = connection;
getRecentRequests(); // Prime 'lastRequestId'
}

public List<RequestInfo> getRecentRequests() throws IOException, CommandException
{
List<RequestInfo> requestInfos = new RecentRequestsCommand(lastRequestId).execute(_connection, null).getRequestInfos();
lastRequestId = requestInfos.get(requestInfos.size() - 1).getId();
return requestInfos.stream().filter(requestInfo -> HarConverter.EXCLUDED_ACTIONS.contains(new Crawler.ControllerActionId(requestInfo.getUrl()))).toList();
}
}
33 changes: 23 additions & 10 deletions src/org/labkey/test/stress/Simulation.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
Expand All @@ -35,7 +36,6 @@
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
* Simulation: A series of activities that represents some user workflow (e.g. loading the dashboard, navigating to the sample finder, and performing a search). For simplicity, API simulations in the initial proof of concept will consist of a single activity.
Expand Down Expand Up @@ -230,7 +230,10 @@ private void makeRequest(Activity.RequestParams requestParams) throws Interrupte
* {@link #_connectionFactory} - used to generate an API connection for the simulation to use
* </li>
* <li>
* {@link #activityDefinitions} - {@link Activity} list that defines the simulation. These are deserialized from {@link ApiTestsDocument} XML files.
* {@link #activityFiles} - {@link ApiTestsDocument} XML files that define the simulation. Used to generate {@link Activity} definitions.
* </li>
* <li>
* {@link #replacements} - String replacements to inject into activity definitions. e.g. 'CONTAINER' or 'USERID'
* </li>
* <li>
* {@link #maxActivityThreads} - the size of thread pool to use for requests
Expand All @@ -248,7 +251,8 @@ public static class Definition
{
private final Supplier<Connection> _connectionFactory;

private List<Activity> activityDefinitions = Collections.emptyList();
private List<File> activityFiles = new ArrayList<>();
private Map<String, String> replacements = Collections.emptyMap();
private int maxActivityThreads = 6; // This seems to be the number of parallel requests browsers handle
private int delayBetweenActivities = 5_000;
private boolean runOnce = false;
Expand Down Expand Up @@ -287,15 +291,13 @@ public Definition setDelayBetweenActivities(int delayBetweenActivities)

public Definition setActivityFiles(File... activityFiles)
{
return setActivityFilesWithReplacements(
Arrays.stream(activityFiles).collect(Collectors.toMap(f -> f, f -> Collections.emptyMap())));
this.activityFiles = Arrays.asList(activityFiles);
return this;
}

public Definition setActivityFilesWithReplacements(Map<File, Map<String, String>> activityFilesWithReplacements)
public Definition setReplacements(Map<String, String> replacements)
{
activityDefinitions = activityFilesWithReplacements.entrySet().stream()
.map(entry -> new Activity(entry.getKey().getName(), Definition.parseTests(entry.getKey(), entry.getValue())))
.toList();
this.replacements = new HashMap<>(replacements);
return this;
}

Expand All @@ -305,6 +307,17 @@ public Definition setRunOnce(boolean runOnce)
return this;
}

private List<Activity> buildActivityDefinitions()
{
if (activityFiles.isEmpty())
{
throw new IllegalArgumentException("No activity files specified");
}
return activityFiles.stream()
.map(file -> new Activity(file.getName(), Definition.parseTests(file, replacements)))
.toList();
}

/**
* Start the simulation according to this definition
* @param resultCollectorFactory The simulation will submit results to the supplied {@link ResultCollector}
Expand All @@ -316,7 +329,7 @@ public <T> Simulation<T> startSimulation(Function<Connection, ResultCollector<T>
Connection connection = _connectionFactory.get();
// Prime connection before starting simulation to ensure credentials are good
new WhoAmICommand().execute(connection, null);
return new Simulation<>(connection, activityDefinitions, delayBetweenActivities, maxActivityThreads, resultCollectorFactory.apply(connection), runOnce);
return new Simulation<>(connection, buildActivityDefinitions(), delayBetweenActivities, maxActivityThreads, resultCollectorFactory.apply(connection), runOnce);
}

public Simulation<RequestResult> startSimulation() throws IOException, CommandException
Expand Down
28 changes: 28 additions & 0 deletions src/org/labkey/test/util/APIAssayHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,34 @@ public static List<String> getListOfAssayNames(String containerPath) throws IOEx
return resultData;
}

/**
* For a given container get rowIds for all assay protocols.
*
* @param containerPath Container path.
* @param connection remoteApi connection
* @return Map of assay rowIds by protocol name and assay type
* @throws IOException Can be thrown by the SelectRowsCommand.
* @throws CommandException Can be thrown by the SelectRowsCommand.
*/
public static Map<String, Integer> getProtocolIds(String containerPath, Connection connection) throws IOException, CommandException
{
SelectRowsCommand cmd = new SelectRowsCommand("assay", "AssayList");
cmd.setColumns(Arrays.asList("Name", "Type", "RowId"));

Map<String, Integer> resultData = new HashMap<>();

SelectRowsResponse response = cmd.execute(connection, containerPath);
for(Row row : response.getRowset())
{
String type = (String) row.getValue("Type");
String name = (String) row.getValue("Name");
Integer rowId = (Integer) row.getValue("RowId");
resultData.put(type + "." + name, rowId);
}

return resultData;
}

public void saveBatch(String assayName, String runName, Map<String, Object> runProperties, List<Map<String, Object>> resultRows, String projectName) throws IOException, CommandException
{
int assayId = getIdFromAssayName(assayName, projectName);
Expand Down
Loading