Skip to content

Commit

Permalink
Stress test framework improvements (#2239)
Browse files Browse the repository at this point in the history
  • Loading branch information
labkey-tchad authored Feb 4, 2025
1 parent c8db5b2 commit 79720b9
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 22 deletions.
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

0 comments on commit 79720b9

Please sign in to comment.