From ee2e4a3cde7b80d2b761a24f95a22d756ad73e94 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 24 Jul 2025 18:13:10 -0700 Subject: [PATCH 1/6] Add test for entity dialog warnings --- src/org/labkey/test/util/TestDataGenerator.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index f9d16ae2af..e80217250b 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -298,12 +298,12 @@ public TestDataGenerator setAutoGeneratedFields(String... fieldNames) return this; } + /** + * Clear any existing generated rows and add new ones + */ public TestDataGenerator withGeneratedRows(int desiredRowCount) { - if (getRowCount() > 0) - { - throw new IllegalStateException("Rows have already been generated"); - } + _rows.clear(); generateRows(desiredRowCount); From 1063a8f0efaf1f5b39c6676bfb706215181cafbd Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 31 Jul 2025 16:59:31 -0700 Subject: [PATCH 2/6] Overload TestDataGenerator.getQueryHelper to allow a custom container path --- src/org/labkey/test/util/TestDataGenerator.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 177f8224f0..7f482becea 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -899,9 +899,14 @@ public RowsResponse deleteRows(Connection cn, List> rowsToDel return getQueryHelper(cn).deleteRows(rowsToDelete); } + public QueryApiHelper getQueryHelper(Connection connection, String containerPath) + { + return new QueryApiHelper(connection, containerPath, _schemaName, _queryName); + } + public QueryApiHelper getQueryHelper(Connection connection) { - return new QueryApiHelper(connection, _containerPath, _schemaName, _queryName); + return getQueryHelper(connection, _containerPath); } public TestDataValidator getValidator() From bac03b8667bd3164f5c90ff0b6850c057df34d11 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 1 Aug 2025 17:04:09 -0700 Subject: [PATCH 3/6] Use API for actions --- .../test/util/query/QueryApiHelper.java | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/org/labkey/test/util/query/QueryApiHelper.java b/src/org/labkey/test/util/query/QueryApiHelper.java index c53a0ae7f9..251b680740 100644 --- a/src/org/labkey/test/util/query/QueryApiHelper.java +++ b/src/org/labkey/test/util/query/QueryApiHelper.java @@ -7,12 +7,13 @@ import org.labkey.remoteapi.domain.DomainDetailsResponse; import org.labkey.remoteapi.domain.DropDomainCommand; import org.labkey.remoteapi.domain.GetDomainDetailsCommand; +import org.labkey.remoteapi.query.BaseRowsCommand; import org.labkey.remoteapi.query.DeleteRowsCommand; import org.labkey.remoteapi.query.Filter; import org.labkey.remoteapi.query.ImportDataCommand; import org.labkey.remoteapi.query.ImportDataResponse; import org.labkey.remoteapi.query.InsertRowsCommand; -import org.labkey.remoteapi.query.BaseRowsCommand; +import org.labkey.remoteapi.query.MoveRowsCommand; import org.labkey.remoteapi.query.RowsResponse; import org.labkey.remoteapi.query.SelectRowsCommand; import org.labkey.remoteapi.query.SelectRowsResponse; @@ -24,6 +25,7 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -85,24 +87,33 @@ public SelectRowsResponse selectRows(List columns, @Nullable List> rows) throws IOException, CommandException + public RowsResponse insertRows(List> rows) throws IOException, CommandException { InsertRowsCommand insertRowsCommand = new InsertRowsCommand(_schema, _query); - insertRowsCommand.setRows(rows); + insertRowsCommand.setRows(makeApiRows(rows)); insertRowsCommand.setTimeout(_insertTimout); insertRowsCommand.setAuditBehavior(BaseRowsCommand.AuditBehavior.DETAILED); return insertRowsCommand.execute(_connection, _containerPath); } - public RowsResponse updateRows(List> rows) throws IOException, CommandException + public RowsResponse updateRows(List> rows) throws IOException, CommandException { UpdateRowsCommand updateRowsCommand = new UpdateRowsCommand(_schema, _query); - updateRowsCommand.setRows(rows); + updateRowsCommand.setRows(makeApiRows(rows)); updateRowsCommand.setTimeout(_insertTimout); updateRowsCommand.setAuditBehavior(BaseRowsCommand.AuditBehavior.DETAILED); return updateRowsCommand.execute(_connection, _containerPath); } + public RowsResponse moveRows(String targetContainerPath, List> rows) throws IOException, CommandException + { + MoveRowsCommand moveRowsCommand = new MoveRowsCommand(targetContainerPath, _schema, _query); + moveRowsCommand.setRows(makeApiRows(rows)); + moveRowsCommand.setTimeout(_insertTimout); + moveRowsCommand.setAuditBehavior(BaseRowsCommand.AuditBehavior.DETAILED); + return moveRowsCommand.execute(_connection, _containerPath); + } + public ImportDataResponse importData(String text) throws IOException, CommandException { ImportDataCommand importDataCommand = new ImportDataCommand(_schema, _query); @@ -123,14 +134,25 @@ public ImportDataResponse importData(File file) throws IOException, CommandExcep * @param rowsToDelete Should include primary key(s) for the table * @return a list of the rows that were deleted */ - public RowsResponse deleteRows(List> rowsToDelete) throws IOException, CommandException + public RowsResponse deleteRows(List> rowsToDelete) throws IOException, CommandException { DeleteRowsCommand cmd = new DeleteRowsCommand(_schema, _query); - cmd.setRows(rowsToDelete); + cmd.setRows(makeApiRows(rowsToDelete)); cmd.setAuditBehavior(BaseRowsCommand.AuditBehavior.DETAILED); return cmd.execute(_connection, _containerPath); } + private List> makeApiRows(List> rows) + { + List> result = new ArrayList<>(); + for (Map row : rows) + { + Map rowMap = new HashMap<>(row); + result.add(rowMap); + } + return result; + } + /** * Delete all rows in table * @return response object From 72b0db16914af465eca9eccf547e0891654f7bef Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Mon, 4 Aug 2025 16:23:11 -0700 Subject: [PATCH 4/6] TestUser connection --- src/org/labkey/test/util/TestUser.java | 10 +++++++ .../test/util/exp/SampleTypeAPIHelper.java | 28 +++++++++++++++++++ .../test/util/query/QueryApiHelper.java | 2 +- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/util/TestUser.java b/src/org/labkey/test/util/TestUser.java index bddbbfd837..95e451bd4a 100644 --- a/src/org/labkey/test/util/TestUser.java +++ b/src/org/labkey/test/util/TestUser.java @@ -4,6 +4,7 @@ import org.labkey.remoteapi.Connection; import org.labkey.remoteapi.security.CreateUserResponse; import org.labkey.test.WebDriverWrapper; +import org.labkey.test.WebTestHelper; import java.io.IOException; @@ -102,6 +103,15 @@ public String getPassword() return _password; } + /** + * Create a non-impersonating API connection for the user. + * @return new Connection + */ + public Connection getUserConnection() + { + return new Connection(WebTestHelper.getBaseURL(), getEmail(), getPassword()); + } + public TestUser addPermission(String role, String containerPath) { new ApiPermissionsHelper(getWrapper()).addMemberToRole(getEmail(), role, PermissionsHelper.MemberType.user, containerPath); diff --git a/src/org/labkey/test/util/exp/SampleTypeAPIHelper.java b/src/org/labkey/test/util/exp/SampleTypeAPIHelper.java index 8a5bcf1a81..3857a059ea 100644 --- a/src/org/labkey/test/util/exp/SampleTypeAPIHelper.java +++ b/src/org/labkey/test/util/exp/SampleTypeAPIHelper.java @@ -20,7 +20,9 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.TreeMap; +import java.util.stream.Collectors; public class SampleTypeAPIHelper { @@ -177,4 +179,30 @@ public static Map getRowIdsForSamples(String containerPath, Str return rowIds; } + /** + * Get sample state IDs defined in the specified folder. Useful for updating sample status via API + * @param containerPath Path to the folder where the sample statuses are defined + */ + public static Map getSampleStateIds(String containerPath) throws IOException, CommandException + { + Connection cn = WebTestHelper.getRemoteApiConnection(); + SelectRowsCommand insertCmd = new SelectRowsCommand("core", "DataStates"); + return insertCmd.execute(cn, containerPath).getRows().stream().collect(Collectors.toMap(row -> + (String) row.get("label"), row -> (Integer) row.get("rowId"))); + } + + /** + * Get sample state ID defined in the specified folder. Useful for updating sample status via API + * @param name Label of the sample state + * @param containerPath Path to the folder where the sample statuses are defined + */ + public static Integer getSampleStateId(String name, String containerPath) throws IOException, CommandException + { + Map sampleStateIds = getSampleStateIds(containerPath); + if(sampleStateIds.containsKey(name)) + return sampleStateIds.get(name); + else + throw new NoSuchElementException("Sample state '%s' not defined in '%s': %s".formatted(name, containerPath, sampleStateIds.keySet())); + } + } diff --git a/src/org/labkey/test/util/query/QueryApiHelper.java b/src/org/labkey/test/util/query/QueryApiHelper.java index 251b680740..36b967051b 100644 --- a/src/org/labkey/test/util/query/QueryApiHelper.java +++ b/src/org/labkey/test/util/query/QueryApiHelper.java @@ -105,7 +105,7 @@ public RowsResponse updateRows(List> rows) throws IOException return updateRowsCommand.execute(_connection, _containerPath); } - public RowsResponse moveRows(String targetContainerPath, List> rows) throws IOException, CommandException + public RowsResponse moveRows(List> rows, String targetContainerPath) throws IOException, CommandException { MoveRowsCommand moveRowsCommand = new MoveRowsCommand(targetContainerPath, _schema, _query); moveRowsCommand.setRows(makeApiRows(rows)); From 44b85830f94732fc887fef1b6cd52684da62487a Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 6 Aug 2025 11:53:19 -0700 Subject: [PATCH 5/6] Create `ContainerInfo` class --- src/org/labkey/test/BaseWebDriverTest.java | 3 +- src/org/labkey/test/params/ContainerInfo.java | 105 ++++++++++++++++++ .../experiment/DataClassDefinition.java | 3 +- .../experiment/SampleTypeDefinition.java | 5 +- .../test/params/list/IntListDefinition.java | 4 +- .../test/params/list/VarListDefinition.java | 4 +- .../test/params/study/DatasetDefinition.java | 7 +- .../test/util/AbstractContainerHelper.java | 12 +- .../labkey/test/util/SampleTypeHelper.java | 5 +- .../labkey/test/util/TestDataGenerator.java | 7 +- src/org/labkey/test/util/TextUtils.java | 19 ++++ .../test/util/data/ColumnNameMapper.java | 2 +- .../labkey/test/util/data/TestDataUtils.java | 85 ++++++++++---- .../test/util/exp/SampleTypeAPIHelper.java | 23 ++-- 14 files changed, 228 insertions(+), 56 deletions(-) create mode 100644 src/org/labkey/test/params/ContainerInfo.java diff --git a/src/org/labkey/test/BaseWebDriverTest.java b/src/org/labkey/test/BaseWebDriverTest.java index 57b530e61b..0d5214b414 100644 --- a/src/org/labkey/test/BaseWebDriverTest.java +++ b/src/org/labkey/test/BaseWebDriverTest.java @@ -64,6 +64,7 @@ import org.labkey.test.pages.query.NewQueryPage; import org.labkey.test.pages.query.SourceQueryPage; import org.labkey.test.pages.search.SearchResultsPage; +import org.labkey.test.params.ContainerInfo; import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.FieldKey; import org.labkey.test.teamcity.TeamCityUtils; @@ -222,7 +223,7 @@ public abstract class BaseWebDriverTest extends LabKeySiteWrapper implements Cle public static final String TRICKY_CHARACTERS = "><&/%\\' \"1\u00E4\u00F6\u00FC\u00C5"; public static final String TRICKY_CHARACTERS_NO_QUOTES = "> _enableModules; + + protected ContainerInfo(String name, ContainerInfo parentContainer, String folderType, List enableModules) + { + _parentContainerPath = parentContainer == null ? null : parentContainer.getContainerPath(); + _name = Objects.requireNonNull(containerPath(name)); + _containerPath = containerPath(_parentContainerPath, name); + _folderType = folderType; + _enableModules = enableModules == null || enableModules.isEmpty() ? Collections.emptyList() : List.copyOf(enableModules); + } + + private static @NotNull String getRandomName(String folderName) + { + if (TestProperties.isTestRunningOnTeamCity()) + return TestDataGenerator.randomName(folderName, TestDataGenerator.randomInt(0, 5), 5, TRICKY_CHARACTERS, null); + else + return folderName + TRICKY_CHARACTERS; + } + + public static ContainerInfo folder(String folderName, ContainerInfo parentContainer, String folderType, List enableModules) + { + return new ContainerInfo(getRandomName(folderName), parentContainer, folderType, enableModules); + } + + public static ContainerInfo folder(String folderName, ContainerInfo parentContainer, String folderType) + { + return folder(folderName, parentContainer, folderType, null); + } + + public static ContainerInfo folder(String folderName, ContainerInfo parentContainer) + { + return folder(folderName, parentContainer, null, null); + } + + public static ContainerInfo project(String projectName, String folderType, List enableModules) + { + return folder(projectName, null, folderType, enableModules); + } + + public static ContainerInfo project(String projectName, String folderType) + { + return project(projectName, folderType, null); + } + + public static ContainerInfo project(String projectName) + { + return project(projectName, null, null); + } + + public void create(AbstractContainerHelper containerHelper) + { + create(containerHelper, _folderType); + } + + public void create(AbstractContainerHelper containerHelper, String folderType) + { + if (isProject()) + containerHelper.createProject(_name, folderType); + else + containerHelper.createSubfolder(_parentContainerPath, _name, folderType); + if (!_enableModules.isEmpty()) + { + containerHelper.enableModules(_containerPath, _enableModules); + } + } + + public @NotNull String getName() + { + return _name; + } + + public @NotNull String getContainerPath() + { + return _containerPath; + } + + public boolean isProject() + { + return _parentContainerPath == null; + } + +} diff --git a/src/org/labkey/test/params/experiment/DataClassDefinition.java b/src/org/labkey/test/params/experiment/DataClassDefinition.java index 30fffcb10d..0b54c99621 100644 --- a/src/org/labkey/test/params/experiment/DataClassDefinition.java +++ b/src/org/labkey/test/params/experiment/DataClassDefinition.java @@ -5,6 +5,7 @@ import org.labkey.remoteapi.domain.PropertyDescriptor; import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.property.DomainProps; +import org.labkey.test.util.DomainUtils.DomainKind; import org.labkey.test.util.TestDataGenerator; import java.util.ArrayList; @@ -123,7 +124,7 @@ protected Domain getDomainDesign() @Override protected String getKind() { - return "DataClass"; + return DomainKind.DataClass.name(); } @NotNull diff --git a/src/org/labkey/test/params/experiment/SampleTypeDefinition.java b/src/org/labkey/test/params/experiment/SampleTypeDefinition.java index 16e30b2568..f264846cb6 100644 --- a/src/org/labkey/test/params/experiment/SampleTypeDefinition.java +++ b/src/org/labkey/test/params/experiment/SampleTypeDefinition.java @@ -6,6 +6,7 @@ import org.labkey.test.components.ui.domainproperties.samples.SampleTypeDesigner; import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.property.DomainProps; +import org.labkey.test.util.DomainUtils.DomainKind; import java.util.ArrayList; import java.util.HashMap; @@ -14,8 +15,6 @@ import java.util.Map; import java.util.Set; -import static org.labkey.test.util.exp.SampleTypeAPIHelper.SAMPLE_TYPE_DOMAIN_KIND; - /** * Defines a Sample Type. Suitable for use with UI and API helpers. * 'exp.materials' @@ -217,7 +216,7 @@ protected Domain getDomainDesign() @Override protected String getKind() { - return SAMPLE_TYPE_DOMAIN_KIND; + return DomainKind.SampleSet.name(); } @NotNull diff --git a/src/org/labkey/test/params/list/IntListDefinition.java b/src/org/labkey/test/params/list/IntListDefinition.java index 4c755e4ffe..b80004d587 100644 --- a/src/org/labkey/test/params/list/IntListDefinition.java +++ b/src/org/labkey/test/params/list/IntListDefinition.java @@ -3,6 +3,7 @@ import org.jetbrains.annotations.NotNull; import org.labkey.remoteapi.domain.PropertyDescriptor; import org.labkey.test.params.FieldDefinition; +import org.labkey.test.util.DomainUtils.DomainKind; import org.labkey.test.util.TestDataGenerator; import java.util.List; @@ -11,7 +12,6 @@ public class IntListDefinition extends ListDefinition { - private static final String DOMAIN_KIND = "IntList"; private final boolean isAutoIncrementKey; @@ -43,7 +43,7 @@ public List getFields() @Override protected String getKind() { - return DOMAIN_KIND; + return DomainKind.IntList.name(); } @Override diff --git a/src/org/labkey/test/params/list/VarListDefinition.java b/src/org/labkey/test/params/list/VarListDefinition.java index a3ebb51cac..164e2cd732 100644 --- a/src/org/labkey/test/params/list/VarListDefinition.java +++ b/src/org/labkey/test/params/list/VarListDefinition.java @@ -1,10 +1,10 @@ package org.labkey.test.params.list; import org.jetbrains.annotations.NotNull; +import org.labkey.test.util.DomainUtils.DomainKind; public class VarListDefinition extends ListDefinition { - private static final String DOMAIN_KIND = "VarList"; public VarListDefinition(String name) { @@ -15,7 +15,7 @@ public VarListDefinition(String name) @Override protected String getKind() { - return DOMAIN_KIND; + return DomainKind.VarList.name(); } @Override diff --git a/src/org/labkey/test/params/study/DatasetDefinition.java b/src/org/labkey/test/params/study/DatasetDefinition.java index 267309ff5b..f17b25ee36 100644 --- a/src/org/labkey/test/params/study/DatasetDefinition.java +++ b/src/org/labkey/test/params/study/DatasetDefinition.java @@ -4,6 +4,7 @@ import org.labkey.remoteapi.domain.Domain; import org.labkey.remoteapi.domain.PropertyDescriptor; import org.labkey.test.params.property.DomainProps; +import org.labkey.test.util.DomainUtils.DomainKind; import java.util.ArrayList; import java.util.HashMap; @@ -12,7 +13,7 @@ public class DatasetDefinition extends DomainProps { - private String _name; + private final String _name; private String _description; private List _fields = new ArrayList<>(); private String _kindName; @@ -21,8 +22,8 @@ public class DatasetDefinition extends DomainProps private String _keyPropertyName; private Boolean _timeKeyField; - public static final String VISIT_BASED_STUDY = "StudyDatasetVisit"; - public static final String DATE_BASED_STUDY = "StudyDatasetDate"; + public static final String VISIT_BASED_STUDY = DomainKind.StudyDatasetVisit.name(); + public static final String DATE_BASED_STUDY = DomainKind.StudyDatasetDate.name(); public static DatasetDefinition create(String name) { diff --git a/src/org/labkey/test/util/AbstractContainerHelper.java b/src/org/labkey/test/util/AbstractContainerHelper.java index f190ab41e7..e04ece0fcd 100644 --- a/src/org/labkey/test/util/AbstractContainerHelper.java +++ b/src/org/labkey/test/util/AbstractContainerHelper.java @@ -199,11 +199,15 @@ private GetModulesResponse getModules(String containerPath) } } - public void enableModule(String projectName, String moduleName) + public void enableModule(String containerPath, String moduleName) { - _test.ensureAdminMode(); - _test.clickProject(projectName); - enableModule(moduleName); + enableModules(containerPath, List.of(moduleName)); + } + + public void enableModules(String containerPath, List moduleNames) + { + _test.goToProjectHome(containerPath); + enableModules(moduleNames); } public void enableModule(String moduleName) diff --git a/src/org/labkey/test/util/SampleTypeHelper.java b/src/org/labkey/test/util/SampleTypeHelper.java index 1fe96d35c1..53c2b473c6 100644 --- a/src/org/labkey/test/util/SampleTypeHelper.java +++ b/src/org/labkey/test/util/SampleTypeHelper.java @@ -83,10 +83,7 @@ public static SampleTypeHelper beginAtSampleTypesList(WebDriverWrapper dWrapper, private static String convertMapToTsv(@NotNull List> data) { List headers = new ArrayList<>(data.get(0).keySet()); - List> rows = new ArrayList<>(); - for (Map row : data) - rows.add(new HashMap<>(row)); - return TestDataUtils.tsvStringFromRowMaps(rows, headers, true); + return TestDataUtils.tsvStringFromRowMaps(data, headers, true); } @Override diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 7f482becea..b433788aa7 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -116,7 +116,7 @@ public TestDataGenerator(String schema, String queryName, String containerPath) _containerPath = containerPath; } - public static File writeCsvFile(List fields, List> entityData, String fileName) throws IOException + public static File writeCsvFile(List fields, List> entityData, String fileName) throws IOException { List> rows = TestDataUtils.replaceColumnHeaders( TestDataUtils.rowListsFromMaps(entityData), ColumnNameMapper.labelToName(fields)); // Use field names @@ -823,6 +823,11 @@ public RowsResponse insertRows(Connection cn) throws IOException, CommandExcepti return insertRows(cn, getRows()); } + public RowsResponse insertRows(Connection cn, String containerPath) throws IOException, CommandException + { + return getQueryHelper(cn, containerPath).insertRows(getRows()); + } + public RowsResponse insertRows(Connection cn, List> rows) throws IOException, CommandException { return getQueryHelper(cn).insertRows(rows); diff --git a/src/org/labkey/test/util/TextUtils.java b/src/org/labkey/test/util/TextUtils.java index 5b8a585cfd..7144a8fd76 100644 --- a/src/org/labkey/test/util/TextUtils.java +++ b/src/org/labkey/test/util/TextUtils.java @@ -1,6 +1,10 @@ package org.labkey.test.util; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.regex.Pattern; public class TextUtils @@ -38,4 +42,19 @@ public static List normalizeSpaceMultiline(List values) { return values.stream().map(TextUtils::normalizeSpaceMultiline).toList(); } + + /// Join project and folder names to create a normalized containerPath.\ + /// The resulting value will be normalized: + /// - repeated path separators will be collapsed to a single slash + /// - leading and trailing slashes will be removed + /// - whitespace around folder names will be removed + /// - the ROOT container will be represented as `null` + /// @param pathParts project name and subfolders to be joined + /// @return normalized container path. `null` for the root container + public static String containerPath(String... pathParts) + { + return StringUtils.trimToNull( + StringUtils.strip(String.join("/", Arrays.stream(pathParts).filter(Objects::nonNull).toList()), "/") + .replaceAll("\\s*/+\\s*", "/")); + } } diff --git a/src/org/labkey/test/util/data/ColumnNameMapper.java b/src/org/labkey/test/util/data/ColumnNameMapper.java index 5cd85a1b5f..3475660599 100644 --- a/src/org/labkey/test/util/data/ColumnNameMapper.java +++ b/src/org/labkey/test/util/data/ColumnNameMapper.java @@ -55,6 +55,6 @@ public String apply(String s) .map(Supplier::get) .filter(Objects::nonNull) .findFirst() - .orElse(s); + .orElseThrow(() -> new IllegalArgumentException("No column mapping found for " + s)); } } diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index e2ef164a38..9ff7ff5c6f 100644 --- a/src/org/labkey/test/util/data/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -284,12 +284,12 @@ public static List> rowMapsFromCsv(String tsvString) throws } } - public static String stringFromRowMaps(List> rowMaps, List columns, boolean includeHeaders, CSVFormat format) + public static String stringFromRowMaps(List> rowMaps, List columns, boolean includeHeaders, CSVFormat format) { return stringFromRows(rowListsFromMaps(rowMaps, columns, includeHeaders, true), format); } - public static String tsvStringFromRowMaps(List> rowMaps, List columns, + public static String tsvStringFromRowMaps(List> rowMaps, List columns, boolean includeHeaders) { return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.TDF); @@ -325,33 +325,43 @@ public static List> mapsFromRows(List> allRows) return rowMaps; } - public static List> dataRowsFromMaps(List> rowMaps, List columns) + public static List> dataRowsFromMaps(List> rowMaps, List columns) { return rowListsFromMaps(rowMaps, columns, false, true); } - public static List> rowListsFromMaps(List> rowMaps) + /** + * @see #rowListsFromMaps(List, List, boolean, boolean) + */ + public static List> rowListsFromMaps(List> rowMaps) { Set columns = new LinkedHashSet<>(); - for (Map row : rowMaps) + for (Map row : rowMaps) { columns.addAll(row.keySet()); } return rowListsFromMaps(rowMaps, new ArrayList<>(columns), true, true); } - public static List> rowListsFromMaps(List> rowMaps, List columns) + /** + * @see #rowListsFromMaps(List, List, boolean, boolean) + */ + public static List> rowListsFromMaps(List> rowMaps, List columns) { return rowListsFromMaps(rowMaps, columns, true, true); } /** - * convert a List of Map to a list of List + * convert a List of row maps to a tabular format. Column headers can be included optionally.
+ * Useful for writing to file or TSV String. * @param rowMaps Source data * @param columns keys contained in each map, will copy values associated with them to the resulting list - * @return A List> containing values + * @param includeHeaders Include headers in tabular data + * @param allowMissingValues Replace missing values with an empty string. Missing values will throw otherwise. + * @return Data rows represented as a List> + * @param Value type of row maps */ - public static List> rowListsFromMaps(List> rowMaps, List columns, boolean includeHeaders, boolean allowMissingValues) + public static List> rowListsFromMaps(List> rowMaps, List columns, boolean includeHeaders, boolean allowMissingValues) { List> lists = new ArrayList<>(); @@ -362,12 +372,12 @@ public static List> rowListsFromMaps(List> rowM lists.add(headers); } - for (Map rowMap : rowMaps) + for (Map rowMap : rowMaps) { List rowList = new ArrayList<>(); for (String column : columns) { - var value = rowMap.get(column); + Object value = rowMap.get(column); if (value == null) { if (allowMissingValues) @@ -386,31 +396,48 @@ public static List> rowListsFromMaps(List> rowM return lists; } + /** + * Replace the column headers of the provided tabular data. The first row should represent the column headers. + * Useful for converting column headers between name, label, or fieldKey + * @param rowLists Tabular data. Will not be modified. + * @param columnMapper Mapping function to calculate new headers + * @return A new list tabular data with replaced headers. Individual data rows will be contained in the new list. + * @see ColumnNameMapper + */ public static List> replaceColumnHeaders(List> rowLists, Function columnMapper) { + if (rowLists == null || rowLists.isEmpty()) + return rowLists; + List headerRow = rowLists.get(0); - List updatedHeaderRow = new ArrayList<>(); - for (String oldHeader : headerRow) - { - updatedHeaderRow.add(columnMapper.apply(oldHeader)); - } List> updatedRows = new ArrayList<>(); - updatedRows.add(updatedHeaderRow); - updatedRows.addAll(rowLists.subList(1, rowLists.size())); + updatedRows.add(headerRow.stream().map(columnMapper).collect(Collectors.toList())); + updatedRows.addAll(List.copyOf(rowLists.subList(1, rowLists.size()))); return updatedRows; } - public static List> replaceMapKeys(List> rowMaps, Function columnMapper) + /** + * Replace the keys for the provided data maps. + * Useful for converting column headers between name, label, or fieldKey + * @param rowMaps Data row maps. Will not be modified. + * @param columnMapper Mapping function to calculate new headers + * @return A new list of row maps with replaced headers + * @param Key type of input data (usually String) + * @param Key type of returned data (usually String) + * @param Value type for data (usually String or Object) + * @see ColumnNameMapper + */ + public static List> replaceMapKeys(List> rowMaps, Function columnMapper) { - List> updatedRows = new ArrayList<>(); - for (Map original : rowMaps) + List> updatedRows = new ArrayList<>(); + for (Map original : rowMaps) { - Map updatedRow = new LinkedHashMap<>(); - for (Map.Entry entry : original.entrySet()) + Map updatedRow = new LinkedHashMap<>(); + for (Map.Entry entry : original.entrySet()) { - String updatedKey = columnMapper.apply(entry.getKey()); + R updatedKey = columnMapper.apply(entry.getKey()); if (updatedRow.containsKey(updatedKey)) { throw new IllegalArgumentException("Duplicate key mapping for '" + updatedKey + "' in row: " + original); @@ -568,6 +595,16 @@ public static String stringFromRows(List> rows) return stringFromRows(rows, CSVFormat.TDF); } + public static @NotNull List getColumnValues(Collection> rows, String name) + { + return getColumnValues(rows, name, String.class); + } + + public static @NotNull List getColumnValues(Collection> rows, String name, Class type) + { + return rows.stream().map(row -> type.cast(row.get(name))).toList(); + } + /** * Used to quote values to be written to a TSV file * @see org.labkey.api.data.TSVWriter diff --git a/src/org/labkey/test/util/exp/SampleTypeAPIHelper.java b/src/org/labkey/test/util/exp/SampleTypeAPIHelper.java index 3857a059ea..8c6eaf7784 100644 --- a/src/org/labkey/test/util/exp/SampleTypeAPIHelper.java +++ b/src/org/labkey/test/util/exp/SampleTypeAPIHelper.java @@ -12,6 +12,7 @@ import org.labkey.test.params.FieldInfo; import org.labkey.test.params.experiment.SampleTypeDefinition; import org.labkey.test.util.DomainUtils; +import org.labkey.test.util.DomainUtils.DomainKind; import org.labkey.test.util.TestDataGenerator; import java.io.IOException; @@ -30,7 +31,7 @@ public class SampleTypeAPIHelper public static final String SCHEMA_NAME = "exp.materials"; // Global constants to ease migration from "Sample Set" to "Sample Type" - public static final String SAMPLE_TYPE_DOMAIN_KIND = "SampleSet"; + public static final String SAMPLE_TYPE_DOMAIN_KIND = DomainKind.SampleSet.name(); public static final String SAMPLE_TYPE_DATA_REGION_NAME = "SampleSet"; public static final String SAMPLE_TYPE_COLUMN_NAME = "Sample Set"; public static final String SAMPLE_NAME_EXPRESSION = "S-${now:date}-${dailySampleCount}"; @@ -180,8 +181,9 @@ public static Map getRowIdsForSamples(String containerPath, Str } /** - * Get sample state IDs defined in the specified folder. Useful for updating sample status via API - * @param containerPath Path to the folder where the sample statuses are defined + * Get sample state IDs defined in the specified project/folder. Useful for updating sample status via API + * @param containerPath Path to the project/folder where the sample statuses are defined (typically project) + * @return Map of */ public static Map getSampleStateIds(String containerPath) throws IOException, CommandException { @@ -192,17 +194,18 @@ public static Map getSampleStateIds(String containerPath) throw } /** - * Get sample state ID defined in the specified folder. Useful for updating sample status via API - * @param name Label of the sample state - * @param containerPath Path to the folder where the sample statuses are defined + * Get sample state ID defined in the specified project/folder. Useful for updating sample status via API + * @param label Label of the sample state (typically "Available", "Locked", or "Consumed") + * @param containerPath Path to the project/folder where the sample statuses are defined (typically project) + * @return rowId for the specified state */ - public static Integer getSampleStateId(String name, String containerPath) throws IOException, CommandException + public static Integer getSampleStateId(String label, String containerPath) throws IOException, CommandException { Map sampleStateIds = getSampleStateIds(containerPath); - if(sampleStateIds.containsKey(name)) - return sampleStateIds.get(name); + if(sampleStateIds.containsKey(label)) + return sampleStateIds.get(label); else - throw new NoSuchElementException("Sample state '%s' not defined in '%s': %s".formatted(name, containerPath, sampleStateIds.keySet())); + throw new NoSuchElementException("Sample state '%s' not defined in '%s': %s".formatted(label, containerPath, sampleStateIds.keySet())); } } From 5e997b43c11c7db6ae0a8a165cd5cdc83f67c9d6 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 6 Aug 2025 17:04:39 -0700 Subject: [PATCH 6/6] Get failure info --- src/org/labkey/test/util/TestDataGenerator.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index b433788aa7..68ce24ba9c 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -640,6 +640,8 @@ private static boolean isDomainAndFieldNameInvalid(DomainUtils.DomainKind domain try { CommandResponse response = command.execute(WebTestHelper.getRemoteApiConnection(), "/home"); + if (response.getParsedData() == null) + throw new RuntimeException("Failed to parse response for command: " + response.getText()); return response.getParsedData().containsKey("errors"); } catch (CommandException | IOException e)