From 8297a079c920eae2727b8c1e41ece43bff90403a Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 11 Jul 2025 11:10:26 -0700 Subject: [PATCH 1/4] Compare thumbnail files instead of stringifying them WebTestHelper.getHttpResponse subtly modifies the response LevenshteinDistance comparison is unnecessary when comparing actual bytes --- .../labkey/test/tests/SimpleModuleTest.java | 28 ++++++++----------- .../labkey/test/util/SimpleHttpRequest.java | 12 ++++++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/org/labkey/test/tests/SimpleModuleTest.java b/src/org/labkey/test/tests/SimpleModuleTest.java index 0e21664401..dd6d370034 100644 --- a/src/org/labkey/test/tests/SimpleModuleTest.java +++ b/src/org/labkey/test/tests/SimpleModuleTest.java @@ -16,7 +16,6 @@ package org.labkey.test.tests; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.text.similarity.LevenshteinDistance; import org.apache.hc.core5.http.HttpStatus; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; @@ -62,6 +61,7 @@ import org.labkey.test.util.Maps; import org.labkey.test.util.PortalHelper; import org.labkey.test.util.RReportHelper; +import org.labkey.test.util.SimpleHttpRequest; import org.labkey.test.util.TestLogger; import org.labkey.test.util.WikiHelper; import org.labkey.test.util.ext4cmp.Ext4FieldRef; @@ -1125,7 +1125,7 @@ private void doTestQueryViews() } @LogMethod - private void doTestReports() + private void doTestReports() throws IOException { RReportHelper _rReportHelper = new RReportHelper(this); WikiHelper wikiHelper = new WikiHelper(this); @@ -1177,7 +1177,7 @@ private void doTestReports() } @LogMethod - private void doTestReportThumbnails() + private void doTestReportThumbnails() throws IOException { goToProjectHome(); log("Verify custom module report thumbnail images"); @@ -1187,14 +1187,14 @@ private void doTestReportThumbnails() } @LogMethod - private void doTestReportIcon() + private void doTestReportIcon() throws IOException { log("Verify custom module report icon image"); setFormElement(Locator.xpath("//table[contains(@class, 'dataset-search')]//input"), KNITR_PEOPLE); waitForElementToDisappear(Locator.tag("tr").withClass("x4-grid-row").containing(WANT_TO_BE_COOL).notHidden()); File expectedIconFile = TestFileUtils.getSampleData(THUMBNAIL_FOLDER + KNITR_PEOPLE + ICON_FILENAME); - String expectedIcon = TestFileUtils.getFileContents(expectedIconFile); + String expectedIcon = TestFileUtils.getMD5Hash(expectedIconFile.toPath()); WebElement img = waitForElement(Locator.tag("img").withClass("dataview-icon").withoutClass("x4-tree-icon-parent").notHidden()); String backgroundImage = StringUtils.trimToEmpty(img.getCssValue("background-image")); @@ -1204,12 +1204,9 @@ private void doTestReportIcon() Assert.fail("Module report icon style is not as expected: " + img.getDomAttribute("style")); } String iconUrl = matcher.group(1); - String iconData = WebTestHelper.getHttpResponse(iconUrl).getResponseBody(); + String iconData = TestFileUtils.getMD5Hash(new SimpleHttpRequest(iconUrl).getResponseAsFile().toPath()); - int lengthToCompare = 3000; - int diff = LevenshteinDistance.getDefaultInstance().apply(expectedIcon.substring(0, lengthToCompare), iconData.substring(0, lengthToCompare)); - assertTrue("Module report icon is not as expected, diff is " + diff, expectedIcon.equals(iconData) || - diff <= lengthToCompare * 0.03); // Might be slightly different due to indentations, etc + assertEquals("Module report icon is not as expected", expectedIcon, iconData); } @LogMethod @@ -1222,20 +1219,17 @@ private void doTestReportCreatedDate() } @LogMethod - private void verifyReportThumbnail(@LoggedParam String reportTitle) + private void verifyReportThumbnail(@LoggedParam String reportTitle) throws IOException { File expectedThumbnailFile = TestFileUtils.getSampleData(THUMBNAIL_FOLDER + reportTitle + THUMBNAIL_FILENAME); - String expectedThumbnail = TestFileUtils.getFileContents(expectedThumbnailFile); + String expectedThumbnail = TestFileUtils.getMD5Hash(expectedThumbnailFile.toPath()); WebElement reportLink = waitForElement(Locator.xpath("//a[text()='" + reportTitle + "']")); mouseOver(reportLink); WebElement thumbnail = waitForElement(Locator.xpath("//div[@class='thumbnail']/img").notHidden()); - String thumbnailData = WebTestHelper.getHttpResponse(thumbnail.getDomProperty("src")).getResponseBody(); + String thumbnailData = TestFileUtils.getMD5Hash(new SimpleHttpRequest(thumbnail.getDomProperty("src")).getResponseAsFile().toPath()); - int lengthToCompare = 5000; - int diff = LevenshteinDistance.getDefaultInstance().apply(expectedThumbnail.substring(0, lengthToCompare), thumbnailData.substring(0, lengthToCompare)); - assertTrue("Module report thumbnail is not as expected, diff is " + diff, expectedThumbnail.equals(thumbnailData) || - diff <= lengthToCompare * 0.03); // Might be slightly different due to indentations, etc + assertEquals("Module report thumbnail is not as expected", expectedThumbnail, thumbnailData); } @LogMethod diff --git a/src/org/labkey/test/util/SimpleHttpRequest.java b/src/org/labkey/test/util/SimpleHttpRequest.java index ec3962a0b4..38bafa0140 100644 --- a/src/org/labkey/test/util/SimpleHttpRequest.java +++ b/src/org/labkey/test/util/SimpleHttpRequest.java @@ -19,6 +19,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.labkey.remoteapi.Connection; +import org.labkey.test.TestFileUtils; import org.openqa.selenium.Cookie; import org.openqa.selenium.WebDriver; @@ -219,16 +220,21 @@ else if (responseFilename != null && fileName.contains(".")) } } + public File getResponseAsFile() throws IOException + { + return getResponseAsFile(TestFileUtils.ensureTestTempDir()); + } + private void useCopiedSession(HttpURLConnection con) { StringBuilder cookieString = new StringBuilder(); - for (Map.Entry cookie : _cookies.entrySet()) + for (Map.Entry cookie : _cookies.entrySet()) { if (cookie.getKey().equals(Connection.X_LABKEY_CSRF)) - con.setRequestProperty((String)cookie.getKey(), (String)cookie.getValue()); + con.setRequestProperty(cookie.getKey(), cookie.getValue()); if (cookie.getKey().equals(Connection.JSESSIONID)) - con.setRequestProperty((String)cookie.getKey(), (String)cookie.getValue()); + con.setRequestProperty(cookie.getKey(), cookie.getValue()); if (!cookieString.isEmpty()) cookieString.append("; "); From 5765c64f856030f37e8f6587f48428c0cbbefbe6 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 11 Jul 2025 11:49:41 -0700 Subject: [PATCH 2/4] Use AssertJ to compare binary file contents --- src/org/labkey/test/TestFileUtils.java | 22 ++++++++++++++++++- .../test/tests/FileAttachmentColumnTest.java | 12 +++++----- .../labkey/test/tests/SimpleModuleTest.java | 14 +++++++----- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/org/labkey/test/TestFileUtils.java b/src/org/labkey/test/TestFileUtils.java index ca119a4c68..6bebb100d0 100644 --- a/src/org/labkey/test/TestFileUtils.java +++ b/src/org/labkey/test/TestFileUtils.java @@ -372,10 +372,30 @@ public static File getTestTempDir() return new File(buildDir, "testTemp"); } - public static File ensureTestTempDir() throws IOException + public static File ensureTestTempDir(String... children) throws IOException { File file = getTestTempDir(); + for (String child : children) + { + file = new File(file, child); + } + FileUtils.forceMkdir(file); + + return file; + } + + public static File ensureTestTempFile(String child0, String... children) throws IOException + { + File file = new File(getTestTempDir(), child0); + + for (String child : children) + { + file = new File(file, child); + } + + FileUtils.forceMkdirParent(file); + return file; } diff --git a/src/org/labkey/test/tests/FileAttachmentColumnTest.java b/src/org/labkey/test/tests/FileAttachmentColumnTest.java index 09f14923d6..39e714efb4 100644 --- a/src/org/labkey/test/tests/FileAttachmentColumnTest.java +++ b/src/org/labkey/test/tests/FileAttachmentColumnTest.java @@ -553,9 +553,9 @@ private void validateSampleData(String sampleType, String folderPath, List { // verify fie download behavior File downloadedFile = doAndWaitForDownload(() -> optionalFileLink.get().click()); - checker().wrapAssertion(() -> Assertions.assertThat(TestFileUtils.getMD5Hash(downloadedFile.toPath())) + checker().wrapAssertion(() -> Assertions.assertThat(downloadedFile) .as("expect the downloaded file to be the expected file") - .isEqualTo(TestFileUtils.getMD5Hash(file.toPath()))); // guard against renames like file2.xyz + .hasSameBinaryContentAs(file)); // guard against renames like file2.xyz } } } @@ -579,9 +579,9 @@ private void validateAssayRun(String assayName, String folderPath, String runNam if (optionalFileLink.isPresent()) { var file = doAndWaitForDownload(()-> optionalFileLink.get().click()); - checker().wrapAssertion(()-> Assertions.assertThat(TestFileUtils.getMD5Hash(file.toPath())) + checker().wrapAssertion(()-> Assertions.assertThat(file) .as("expect the downloaded file to have equivalent content") - .isEqualTo(TestFileUtils.getMD5Hash(runFile.toPath()))); + .hasSameBinaryContentAs(runFile)); } var resultsPage = runsPage.clickAssayIdLink(runName); @@ -647,9 +647,9 @@ private void validateDatasetData(String datasetName, String folderPath, List optionalFileLink.get().click()); - checker().wrapAssertion(() -> Assertions.assertThat(TestFileUtils.getMD5Hash(downloadedFile.toPath())) + checker().wrapAssertion(() -> Assertions.assertThat(downloadedFile) .as("expect the downloaded file to be the expected file") - .isEqualTo(TestFileUtils.getMD5Hash(file.toPath()))); // guard against renames like file2.xyz + .hasSameBinaryContentAs(file)); // guard against renames like file2.xyz } } } diff --git a/src/org/labkey/test/tests/SimpleModuleTest.java b/src/org/labkey/test/tests/SimpleModuleTest.java index dd6d370034..6b55b56fa7 100644 --- a/src/org/labkey/test/tests/SimpleModuleTest.java +++ b/src/org/labkey/test/tests/SimpleModuleTest.java @@ -17,6 +17,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hc.core5.http.HttpStatus; +import org.assertj.core.api.Assertions; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; import org.junit.Assert; @@ -1194,7 +1195,6 @@ private void doTestReportIcon() throws IOException waitForElementToDisappear(Locator.tag("tr").withClass("x4-grid-row").containing(WANT_TO_BE_COOL).notHidden()); File expectedIconFile = TestFileUtils.getSampleData(THUMBNAIL_FOLDER + KNITR_PEOPLE + ICON_FILENAME); - String expectedIcon = TestFileUtils.getMD5Hash(expectedIconFile.toPath()); WebElement img = waitForElement(Locator.tag("img").withClass("dataview-icon").withoutClass("x4-tree-icon-parent").notHidden()); String backgroundImage = StringUtils.trimToEmpty(img.getCssValue("background-image")); @@ -1204,9 +1204,10 @@ private void doTestReportIcon() throws IOException Assert.fail("Module report icon style is not as expected: " + img.getDomAttribute("style")); } String iconUrl = matcher.group(1); - String iconData = TestFileUtils.getMD5Hash(new SimpleHttpRequest(iconUrl).getResponseAsFile().toPath()); + File downloadedIcon = new SimpleHttpRequest(iconUrl).getResponseAsFile(TestFileUtils.ensureTestTempFile(KNITR_PEOPLE + ICON_FILENAME)); - assertEquals("Module report icon is not as expected", expectedIcon, iconData); + Assertions.assertThat(downloadedIcon).as("Module report icon is not as expected") + .hasSameBinaryContentAs(expectedIconFile); } @LogMethod @@ -1222,14 +1223,15 @@ private void doTestReportCreatedDate() private void verifyReportThumbnail(@LoggedParam String reportTitle) throws IOException { File expectedThumbnailFile = TestFileUtils.getSampleData(THUMBNAIL_FOLDER + reportTitle + THUMBNAIL_FILENAME); - String expectedThumbnail = TestFileUtils.getMD5Hash(expectedThumbnailFile.toPath()); WebElement reportLink = waitForElement(Locator.xpath("//a[text()='" + reportTitle + "']")); mouseOver(reportLink); WebElement thumbnail = waitForElement(Locator.xpath("//div[@class='thumbnail']/img").notHidden()); - String thumbnailData = TestFileUtils.getMD5Hash(new SimpleHttpRequest(thumbnail.getDomProperty("src")).getResponseAsFile().toPath()); + File downloadedThumbnail = new SimpleHttpRequest(thumbnail.getDomProperty("src")) + .getResponseAsFile(TestFileUtils.ensureTestTempFile(reportTitle + THUMBNAIL_FILENAME)); - assertEquals("Module report thumbnail is not as expected", expectedThumbnail, thumbnailData); + Assertions.assertThat(downloadedThumbnail).as("Module report thumbnail is not as expected") + .hasSameBinaryContentAs(expectedThumbnailFile); } @LogMethod From 0d5090b5d6ab7e4fba2bdac2de9aee049ee15c9e Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 11 Jul 2025 11:54:08 -0700 Subject: [PATCH 3/4] by getmd5hash --- src/org/labkey/test/TestFileUtils.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/org/labkey/test/TestFileUtils.java b/src/org/labkey/test/TestFileUtils.java index 6bebb100d0..f0e67e613d 100644 --- a/src/org/labkey/test/TestFileUtils.java +++ b/src/org/labkey/test/TestFileUtils.java @@ -64,8 +64,6 @@ import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.security.Security; import java.util.ArrayList; import java.util.Arrays; @@ -122,21 +120,6 @@ public static String getFileContents(Path path) } } - /** - * Compute MD5 hash for the given file. Useful checking file equivalence. - */ - public static String getMD5Hash(Path path) - { - try - { - return new String(MessageDigest.getInstance("MD5").digest(Files.readAllBytes(path)), StandardCharsets.UTF_8); - } - catch (IOException | NoSuchAlgorithmException fail) - { - throw new RuntimeException(fail); - } - } - public static String getStreamContentsAsString(InputStream is) throws IOException { return StringUtils.join(IOUtils.readLines(is, Charset.defaultCharset()).toArray(), System.lineSeparator()); From 548ddfcf30ba15511af645c9764a58a9de4a90fa Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 11 Jul 2025 12:21:08 -0700 Subject: [PATCH 4/4] Add documentation --- src/org/labkey/test/TestFileUtils.java | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/test/TestFileUtils.java b/src/org/labkey/test/TestFileUtils.java index f0e67e613d..1848c71a7b 100644 --- a/src/org/labkey/test/TestFileUtils.java +++ b/src/org/labkey/test/TestFileUtils.java @@ -355,6 +355,12 @@ public static File getTestTempDir() return new File(buildDir, "testTemp"); } + /** + * Creates a directory under the 'testTemp' directory: 'build/testTemp/[children]' + * @param children will be appended to the testTemp path + * @return A file pointer to the specified directory. The directory will exist + * @throws IOException if the directories were not created + */ public static File ensureTestTempDir(String... children) throws IOException { File file = getTestTempDir(); @@ -368,15 +374,25 @@ public static File ensureTestTempDir(String... children) throws IOException return file; } - public static File ensureTestTempFile(String child0, String... children) throws IOException + /** + * Creates a directory under the 'testTemp' directory to contain the specified file. 'build/testTemp[/children]/lastChild' + * @param children will be appended to the testTemp path to construct the desired file's path + * @return A file pointer to the specified file. The file's parents will exist but the file might not + * @throws IOException if the parent directories were not created + */ + public static File ensureTestTempFile(String... children) throws IOException { - File file = new File(getTestTempDir(), child0); + File file = getTestTempDir(); for (String child : children) { file = new File(file, child); } + if (file.toString().length() == getTestTempDir().toString().length()) + { + throw new IllegalArgumentException("No valid children were provided: " + Arrays.toString(children)); + } FileUtils.forceMkdirParent(file); return file;