diff --git a/src/org/labkey/test/BaseWebDriverTest.java b/src/org/labkey/test/BaseWebDriverTest.java
index d4c0eea4b3..15fe19f10d 100644
--- a/src/org/labkey/test/BaseWebDriverTest.java
+++ b/src/org/labkey/test/BaseWebDriverTest.java
@@ -1337,11 +1337,16 @@ private int getPendingRequestCount(Connection connection)
}
}
+ protected boolean skipCleanup(boolean afterTest)
+ {
+ return false;
+ }
+
private void cleanup(boolean afterTest)
{
ensureSignedInAsPrimaryTestUser();
- if (!ClassUtils.getAllInterfaces(getClass()).contains(ReadOnlyTest.class) || ((ReadOnlyTest) this).needsSetup())
+ if (!skipCleanup(afterTest) && (!(this instanceof ReadOnlyTest readOnlyTest) || readOnlyTest.needsSetup()))
{
if (afterTest)
waitForPendingRequests(WAIT_FOR_PAGE);
diff --git a/src/org/labkey/test/TestProperties.java b/src/org/labkey/test/TestProperties.java
index 85d3df7bce..651e6d14b1 100644
--- a/src/org/labkey/test/TestProperties.java
+++ b/src/org/labkey/test/TestProperties.java
@@ -383,7 +383,7 @@ public static File getDumpDir()
* @param def Default value
* @return value of the specified property
*/
- private static boolean getBooleanProperty(String key, boolean def)
+ public static boolean getBooleanProperty(String key, boolean def)
{
String prop = System.getProperty(key);
if (!StringUtils.isBlank(prop))
@@ -403,7 +403,7 @@ private static boolean getBooleanProperty(String key, boolean def)
* @param def Default value
* @return value of the specified property
*/
- private static int getIntegerProperty(String key, int def)
+ public static int getIntegerProperty(String key, int def)
{
String prop = System.getProperty(key);
if (!StringUtils.isBlank(prop))
@@ -427,7 +427,7 @@ private static int getIntegerProperty(String key, int def)
* @param def Default value
* @return value of the specified property
*/
- private static double getDoubleProperty(String key, double def)
+ public static double getDoubleProperty(String key, double def)
{
String prop = System.getProperty(key);
if (!StringUtils.isBlank(prop))
diff --git a/src/org/labkey/test/tests/component/GridPanelTest.java b/src/org/labkey/test/tests/component/GridPanelTest.java
index cd3ee0f9c1..1314586df0 100644
--- a/src/org/labkey/test/tests/component/GridPanelTest.java
+++ b/src/org/labkey/test/tests/component/GridPanelTest.java
@@ -72,6 +72,7 @@ public class GridPanelTest extends GridPanelBaseTest
private static final FieldInfo FILTER_BOOL_COL = FieldInfo.random("Bool", FieldDefinition.ColumnType.Boolean);
private static final FieldInfo FILTER_DATE_COL = FieldInfo.random("Date", FieldDefinition.ColumnType.DateAndTime);
private static final String FILTER_STORED_AMOUNT_COL = "Amount";
+ private static final String FILTER_UNITS_COL = "Units";
// Views and columns used in the views. The views are only applied to the small sample type (Small_SampleType).
private static final String VIEW_EXTRA_COLUMNS = "Extra_Columns";
@@ -1742,6 +1743,7 @@ public void testFilterDialogWithViews() throws IOException, CommandException
expectedList.add(FILTER_STRING_COL.getLabel());
expectedList.add(FILTER_DATE_COL.getLabel());
expectedList.add(FILTER_STORED_AMOUNT_COL);
+ expectedList.add(FILTER_UNITS_COL);
actualList = filterDialog.getAvailableFieldLabels();
@@ -1801,6 +1803,7 @@ public void testFilterDialogWithViews() throws IOException, CommandException
expectedList.add(FILTER_BOOL_COL.getLabel());
expectedList.add(FILTER_DATE_COL.getLabel());
expectedList.add(FILTER_STORED_AMOUNT_COL);
+ expectedList.add(FILTER_UNITS_COL);
filterDialog = grid.getGridBar().openFilterDialog();
diff --git a/src/org/labkey/test/tests/list/ListTest.java b/src/org/labkey/test/tests/list/ListTest.java
index 584c3e75d5..56d346f399 100644
--- a/src/org/labkey/test/tests/list/ListTest.java
+++ b/src/org/labkey/test/tests/list/ListTest.java
@@ -534,26 +534,27 @@ public void testNameTrimming()
public void testLongName()
{
String listName = "A_+-:''.¡™£¢∞§¶•ªº–≠œ∑´®†¥¨ˆøπ“‘«æ…¬˚∆˙©√ƒ∂ßΩ≈ç√∫µ≤≥÷‹›fifl‡°·‚—±⁄€‹›‡‰Æ«»¢∫√∑∏∂";
- String fieldWithDefault = TestDataGenerator.randomFieldName("With Default", null, DomainUtils.DomainKind.IntList);
+ var fieldWithDefault = FieldInfo.random("With Default", ColumnType.String);
EditListDefinitionPage listEditPage = _listHelper.beginCreateList(getProjectName(), listName);
listEditPage.manuallyDefineFieldsWithAutoIncrementingKey("Key");
- listEditPage.addField(new FieldDefinition(fieldWithDefault, ColumnType.String));
+ listEditPage.addField(fieldWithDefault.getFieldDefinition());
listEditPage.clickSave();
listEditPage = _listHelper.goToEditDesign(listName);
var page = listEditPage.getFieldsPanel()
.expand()
- .getField(fieldWithDefault)
+ .getField(fieldWithDefault.getName())
.clickAdvancedSettings()
.clickDefaultValuesLink();
- var input = Locator.tagContainingText("td", "With Default").followingSibling("td").descendant("input").findElement(page.getDriver());
+ var input = Locator.tagContainingText("td", fieldWithDefault.getLabel()).followingSibling("td")
+ .descendant("input").findElement(page.getDriver());
setFormElement(input, "42");
clickButton("Save Defaults");
_listHelper.beginAtList(getProjectName(), listName);
DataRegionTable list = new DataRegionTable("query", getDriver());
UpdateQueryRowPage updatePage = list.clickInsertNewRow();
- checker().verifyEquals("Default value not as expected ", "42", updatePage.getTextInputValue(fieldWithDefault));
+ checker().verifyEquals("Default value not as expected ", "42", updatePage.getTextInputValue(fieldWithDefault.getName()));
updatePage.submit();
}
@@ -1054,7 +1055,7 @@ public void listSelfJoinTest()
final String dummyCol = dummyBase + TRICKY_CHARACTERS;
final String lookupField = "lookupField" + TRICKY_CHARACTERS;
final String lookupSchema = "lists";
- final String keyCol = "Key &%<+";
+ final String keyCol = "Key &%<+\\"; // Issue 54094: Verify key field ending with "\"
log("Issue 6883: test list self join");
diff --git a/src/org/labkey/test/tests/upgrade/BaseUpgradeTest.java b/src/org/labkey/test/tests/upgrade/BaseUpgradeTest.java
new file mode 100644
index 0000000000..ffaeacacbc
--- /dev/null
+++ b/src/org/labkey/test/tests/upgrade/BaseUpgradeTest.java
@@ -0,0 +1,125 @@
+package org.labkey.test.tests.upgrade;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.labkey.test.BaseWebDriverTest;
+import org.labkey.test.TestProperties;
+import org.labkey.test.util.TestLogger;
+import org.labkey.test.util.Version;
+import org.labkey.test.util.VersionRange;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static org.apache.commons.lang3.StringUtils.trimToNull;
+
+/**
+ * Base test class for tests that setup data and configure a server then verify the persistence or modification of those
+ * data and configurations after upgrading to a newer version of LabKey.
+ * The {@code EariestVersion} and {@code LatestVersion} annotations can be used to skip particular tests when they are
+ * not relevant to the version of LabKey being upgraded from (specified in the {@code webtest.upgradePreviousVersion}
+ * system property).
+ * The setup steps will be skipped if the {@code webtest.upgradeSetup} system property is set to {@code false}.
+ */
+public abstract class BaseUpgradeTest extends BaseWebDriverTest
+{
+
+ protected static final boolean isUpgradeSetupPhase = TestProperties.getBooleanProperty("webtest.upgradeSetup", true);
+ protected static final Version previousVersion = Optional.ofNullable(trimToNull(System.getProperty("webtest.upgradePreviousVersion")))
+ .map(Version::new).orElse(null);
+
+ @Override
+ protected boolean skipCleanup(boolean afterTest)
+ {
+ return afterTest || !isUpgradeSetupPhase;
+ }
+
+ @BeforeClass
+ public static void setupProject() throws Exception
+ {
+ BaseUpgradeTest currentTest = BaseWebDriverTest.getCurrentTest();
+
+ if (isUpgradeSetupPhase)
+ {
+ currentTest.doSetup();
+ }
+ else
+ {
+ TestLogger.info("Skipping setup for %s. Verifying upgrade.". formatted(currentTest.getClass().getSimpleName()));
+ }
+ }
+
+ protected abstract void doSetup() throws Exception;
+
+ @Rule
+ public final TestRule upgradeVersionCheck = new UpgradeVersionCheck();
+
+ @Override
+ public List getAssociatedModules()
+ {
+ return Arrays.asList();
+ }
+
+ /**
+ * Annotates test methods that should only run when upgrading from particular LabKey versions, as specified in
+ * {@code webtest.upgradePreviousVersion}.
+ * Specifies the earliest version of the test class that performed the required setup for the annotated method.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD})
+ protected @interface EariestVersion {
+ String value();
+ }
+
+ /**
+ * Annotates test methods that should only run when upgrading from particular LabKey versions, as specified in
+ * {@code webtest.upgradePreviousVersion}.
+ * Specifies the latest version of the test class that performed the required setup for the annotated method.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD})
+ protected @interface LatestVersion {
+ String value();
+ }
+
+ private static class UpgradeVersionCheck implements TestRule
+ {
+
+ @Override
+ public @NotNull Statement apply(Statement base, Description description)
+ {
+ String eariestVersion = Optional.ofNullable(description.getAnnotation(EariestVersion.class))
+ .map(EariestVersion::value).orElse(null);
+ String latestVersion = Optional.ofNullable(description.getAnnotation(LatestVersion.class))
+ .map(LatestVersion::value).orElse(null);
+
+ if (isUpgradeSetupPhase || previousVersion == null || (eariestVersion == null && latestVersion == null))
+ {
+ return base; // Run the test normally
+ }
+
+ return new Statement()
+ {
+ @Override
+ public void evaluate() throws Throwable
+ {
+ Assume.assumeTrue("Test doesn't support upgrading from version: " + previousVersion,
+ VersionRange.versionRange(eariestVersion, latestVersion).contains(previousVersion)
+ );
+ base.evaluate();
+ }
+ };
+ }
+ }
+
+}
diff --git a/src/org/labkey/test/tests/upgrade/BasicUpgradeTest.java b/src/org/labkey/test/tests/upgrade/BasicUpgradeTest.java
new file mode 100644
index 0000000000..5cdb8ed7d2
--- /dev/null
+++ b/src/org/labkey/test/tests/upgrade/BasicUpgradeTest.java
@@ -0,0 +1,92 @@
+package org.labkey.test.tests.upgrade;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.labkey.remoteapi.CommandException;
+import org.labkey.remoteapi.Connection;
+import org.labkey.remoteapi.query.SelectRowsCommand;
+import org.labkey.test.params.FieldInfo;
+import org.labkey.test.params.experiment.SampleTypeDefinition;
+import org.labkey.test.util.TestUser;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertFalse;
+import static org.labkey.test.util.PermissionsHelper.READER_ROLE;
+
+@Category({})
+public class BasicUpgradeTest extends BaseUpgradeTest
+{
+ private static final TestUser USER = new TestUser("basic_upgrade_reader@basicupgradetest.test");
+ private static final String SAMPLE_TYPE = "UpgradeTestSamples";
+
+ private static final FieldInfo STR_COL = new FieldInfo("String Col1");
+
+ @Override
+ protected void doCleanup(boolean afterTest)
+ {
+ _containerHelper.deleteProject(getProjectName(), afterTest);
+ _userHelper.deleteUsers(afterTest, USER);
+ }
+
+ @Override
+ protected void doSetup() throws Exception
+ {
+ _containerHelper.createProject(getProjectName(), null);
+ USER.create(this);
+ USER.setInitialPassword();
+ USER.addPermission(READER_ROLE, getProjectName());
+
+ new SampleTypeDefinition(SAMPLE_TYPE)
+ .setFields(List.of(STR_COL.getFieldDefinition()))
+ .create(createDefaultConnection(), getProjectName())
+ .insertRows(createDefaultConnection(), List.of(Map.of(
+ "Name", "S-1",
+ STR_COL.getName(), "Test String Value"
+ )) );
+ }
+
+ @Before
+ public void preTest()
+ {
+ USER.load(this);
+ }
+
+ @Test
+ public void testSampleRowsExist() throws Exception
+ {
+ // Use primary user to verify data
+ queryData(createDefaultConnection());
+ }
+
+ @Test
+ public void testUserPassword() throws Exception
+ {
+ // Use password authentication for USER
+ queryData(USER.getUserConnection());
+ }
+
+ @Test
+ public void testUserPermissions() throws Exception
+ {
+ // Impersonate to test USER's permission level
+ queryData(createDefaultConnection().impersonate(USER.getEmail()));
+ }
+
+ private void queryData(Connection connection) throws IOException, CommandException
+ {
+ List