From b048f9cb9af75f5f63483e0094f44872356a99a0 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Fri, 14 Apr 2023 16:44:28 -0500 Subject: [PATCH 01/31] Hacked together tests --- gradle/web-gwt-test.gradle | 7 +- web/client-api/client-api.gradle | 38 ++ .../io/deephaven/web/DeephavenApi.gwt.xml | 4 +- .../web/ClientIntegrationTestSuite.java | 19 + ...estSuite.java => ClientUnitTestSuite.java} | 10 +- .../client/api/AbstractAsyncGwtTestCase.java | 351 ++++++++++++ .../deephaven/web/client/api/JsPredicate.java | 11 + .../api/filter/FilterConditionTestGwt.java | 2 +- .../api/i18n/JsDateTimeFormatTestGwt.java | 2 +- .../api/i18n/JsNumberFormatTestGwt.java | 2 +- .../api/subscription/ViewportTestGwt.java | 500 ++++++++++++++++++ 11 files changed, 935 insertions(+), 11 deletions(-) create mode 100644 web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java rename web/client-api/src/test/java/io/deephaven/web/{ApiTestSuite.java => ClientUnitTestSuite.java} (67%) create mode 100644 web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java create mode 100644 web/client-api/src/test/java/io/deephaven/web/client/api/JsPredicate.java create mode 100644 web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java diff --git a/gradle/web-gwt-test.gradle b/gradle/web-gwt-test.gradle index cb941530adc..2a29d659315 100644 --- a/gradle/web-gwt-test.gradle +++ b/gradle/web-gwt-test.gradle @@ -15,8 +15,13 @@ String testDir = "$buildDir/testGwt" task 'gwtTest', type: Test, { Test t -> t.inputs.files(sourceSets.test.output.files) + + gradle.projectsEvaluated { + t.classpath += project(":web-client-api").tasks.getByName('gwtCompile').src + } + // t.classpath = configurations.testRuntime - t.systemProperties = [ 'gwt.args': "${manualGwt ? '-runStyle Manual:1' : ''} ${testPort ? /-port $testPort/ : ''} -war $testDir/war -setProperty dhTestServer=$testServer -ea -style PRETTY -generateJsInteropExports", + t.systemProperties = [ 'gwt.args': "${manualGwt ? '-runStyle Manual:1' : ''} ${testPort ? /-port $testPort/ : ''} -war $testDir/war -ea -style PRETTY", 'gwt.persistentunitcachedir': "$testDir/unitCache", 'dhTestServer': testServer ] diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index d90b087281c..5804c640315 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -1,5 +1,6 @@ plugins { id 'io.deephaven.project.register' + id 'io.deephaven.deephaven-in-docker' } apply from: "$rootDir/gradle/web-client.gradle" @@ -42,6 +43,43 @@ artifacts { } } +tasks.register('gwtUnitTest', Test) { t -> + + t.systemProperties = [ + 'gwt.args':'-runStyle HtmlUnit -ea -style PRETTY', + 'gwt.persistentunitcachedir':layout.buildDirectory.dir('unitTest-unitCache') + ] +} + +// start a grpc-api server +String randomSuffix = UUID.randomUUID().toString(); +deephavenDocker { + envVars.set([ + 'START_OPTS':'-Xmx512m' + ]) + containerName.set "dh-server-for-cpp-${randomSuffix}" + networkName.set "cpp-test-network-${randomSuffix}" +} + +tasks.register('gwtIntegrationTest', Test) { t -> + t.dependsOn(deephavenDocker.portTask) + doFirst { + t.systemProperty('dh.server', 'http://localhost:' + deephavenDocker.port.get()) + } + t.finalizedBy(deephavenDocker.endTask) + t.systemProperties = [ + 'gwt.args':'-runStyle HtmlUnit -ea -style PRETTY', + 'gwt.persistentunitcachedir':layout.buildDirectory.dir('integrationTest-unitCache').toString(), +// 'dh.server':'http://localhost:' + deephavenDocker.port.get() + ] + t.include '**/*TestSuite.class' + t.useJUnit() + t.scanForTestClasses = false + +} + project.tasks.getByName('quick').dependsOn project.tasks.withType(de.esoco.gwt.gradle.task.GwtCompileTask) +tasks.getByName('check').dependsOn(tasks.withType(Test)) + apply from: "$rootDir/gradle/web-gwt-test.gradle" diff --git a/web/client-api/src/main/java/io/deephaven/web/DeephavenApi.gwt.xml b/web/client-api/src/main/java/io/deephaven/web/DeephavenApi.gwt.xml index 4f6177621b8..ba15260401a 100644 --- a/web/client-api/src/main/java/io/deephaven/web/DeephavenApi.gwt.xml +++ b/web/client-api/src/main/java/io/deephaven/web/DeephavenApi.gwt.xml @@ -13,8 +13,8 @@ - - + + diff --git a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java new file mode 100644 index 00000000000..84b8080a53a --- /dev/null +++ b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java @@ -0,0 +1,19 @@ +package io.deephaven.web; + +import com.google.gwt.junit.tools.GWTTestSuite; +import io.deephaven.web.client.api.filter.FilterConditionTestGwt; +import junit.framework.Test; +import junit.framework.TestSuite; + +public class ClientIntegrationTestSuite extends GWTTestSuite { + public static Test suite() { + TestSuite suite = new TestSuite("Deephaven JS API Unit Test Suite"); + + // This test doesn't actually talk to the server, but it requires the dh-internal library be available + suite.addTestSuite(FilterConditionTestGwt.class); + + + + return suite; + } +} diff --git a/web/client-api/src/test/java/io/deephaven/web/ApiTestSuite.java b/web/client-api/src/test/java/io/deephaven/web/ClientUnitTestSuite.java similarity index 67% rename from web/client-api/src/test/java/io/deephaven/web/ApiTestSuite.java rename to web/client-api/src/test/java/io/deephaven/web/ClientUnitTestSuite.java index 84f65261967..b8824afe125 100644 --- a/web/client-api/src/test/java/io/deephaven/web/ApiTestSuite.java +++ b/web/client-api/src/test/java/io/deephaven/web/ClientUnitTestSuite.java @@ -4,17 +4,17 @@ package io.deephaven.web; import com.google.gwt.junit.tools.GWTTestSuite; -import io.deephaven.web.client.api.filter.FilterConditionTestGwt; import io.deephaven.web.client.api.i18n.JsDateTimeFormatTestGwt; import io.deephaven.web.client.api.i18n.JsNumberFormatTestGwt; import junit.framework.Test; import junit.framework.TestSuite; -public class ApiTestSuite extends GWTTestSuite { +/** + * Tests that require a browser environment to run, but do not require the server. + */ +public class ClientUnitTestSuite extends GWTTestSuite { public static Test suite() { - TestSuite suite = new TestSuite("Deephaven Web API Test Suite"); - suite.addTestSuite(FilterConditionTestGwt.class); - + TestSuite suite = new TestSuite("Deephaven JS API Unit Test Suite"); suite.addTestSuite(JsDateTimeFormatTestGwt.class); suite.addTestSuite(JsNumberFormatTestGwt.class); return suite; diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java new file mode 100644 index 00000000000..71582eb9219 --- /dev/null +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java @@ -0,0 +1,351 @@ +package io.deephaven.web.client.api; + +import com.google.gwt.core.client.JavaScriptException; +import com.google.gwt.junit.client.GWTTestCase; +import elemental2.core.JsArray; +import elemental2.core.JsError; +import elemental2.core.JsString; +import elemental2.dom.CustomEvent; +import elemental2.dom.CustomEventInit; +import elemental2.dom.DomGlobal; +import elemental2.promise.IThenable; +import elemental2.promise.Promise; +import io.deephaven.web.client.api.subscription.ViewportData; +import io.deephaven.web.shared.data.Viewport; +import io.deephaven.web.shared.fu.JsRunnable; +import io.deephaven.web.shared.fu.RemoverFn; +import jsinterop.annotations.JsProperty; +import jsinterop.base.Js; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +public abstract class AbstractAsyncGwtTestCase extends GWTTestCase { + + public static class TableSourceBuilder { + public TableSourceBuilder script(String tableName, String python, String groovy) { + + + return this; + } + + public TableSource build() { + return null; + } + } + public interface TableSource { + Promise table(String name); + } + + + /** + * Set this to a value higher than 1 to get more time to run debugger without timeouts failing. + */ + protected static final int TIMEOUT_SCALE = 1; + public static final double DELTA = 0.0001; + + public JsString toJsString(String k) { + return Js.cast(k); + } + + public JsArray toJsString(String ... k) { + return Js.cast(k); + } + + @JsProperty(name = "log", namespace = "console") + private static native elemental2.core.Function getLog(); + + static IThenable.ThenOnFulfilledCallbackFn logOnSuccess(Object ... rest) { + return value-> { + // GWT will puke if we have varargs being sent to varargs; + // so we go JS on it and just grab the function to apply + getLog().apply(null, rest); + return Promise.resolve(value); + }; + } + + static Promise log(Object ... rest) { + getLog().apply(null, rest); + return Promise.resolve(rest); + } + + JsRunnable assertEventNotCalled( + HasEventHandling handling, + String... events + ) { + RemoverFn[] undos = new RemoverFn[events.length]; + for (int i = 0; i < events.length; i++) { + final String ev = events[i]; + undos[i] = handling.addEventListener(ev, e -> { + log("Did not expect", ev, "but fired event", e); + report("Expected " + ev + " to not be called; detail: " + (e.detail)); + }); + } + return ()->{ + for (RemoverFn undo : undos) { + undo.remove(); + } + + }; + } + + static IThenable.ThenOnFulfilledCallbackFn run(JsRunnable allow) { + return t->{ + allow.run(); + return Promise.resolve(t); + }; + } + + static Promise expectFailure(Promise state, T value) { + return state.then(val-> Promise.reject("Failed"), + error->Promise.resolve(value) + ); + } + + /** + * Connects and authenticates to the specified server, and resolves once the specified query config has + * become available. + */ + protected Promise connect(String queryConfigName) { + //start by delaying test finish by .5s so we fail fast in cases where we aren't set up right + delayTestFinish(2000); + //for example, if the server doesn't have the query config that we're expecting to get, we'll fail nearly right away + return new Promise<>((resolve, reject) -> { + IrisClient irisClient = new IrisClient(localServer); + irisClient.addEventListener(IrisClient.EVENT_CONNECT, e -> { + irisClient.login(LoginCredentials.of(username, password, "password", username)).then(success -> { + //success, but we don't need to do anything, events are wired up to get specifics, grant another timeout + + delayTestFinish(2001 * TIMEOUT_SCALE);//if this (uniquely value'd) timeout fails, likely the config doesn't exist. + + return null; + }, failure -> { + reject.onInvoke("login failed " + failure); + return null; + }); + }); + EventFn[] eventFn = {}; + eventFn[0] = e -> { + QueryInfo query = (QueryInfo) ((CustomEvent) e).detail; + console.log("config event: " + query.getName() + " [" + query.getStatus() + "]"); + if (!queryConfigName.equals(query.getName())) { + //unimportant, wait a bit longer + return; + } + if ("Running".equalsIgnoreCase(query.getStatus())) { + // provide more detail in the console + query.addEventListener(QueryInfo.EVENT_TABLE_METRICS, metricsEvent -> { + JsPropertyMap detail = (JsPropertyMap) ((CustomEvent) metricsEvent).detail; + console.log(detail.get("type") + ": " + detail.get("formatted")); + }); + + resolve.onInvoke(query); + irisClient.removeEventListener(IrisClient.EVENT_CONFIG_ADDED, eventFn[0]); + irisClient.removeEventListener(IrisClient.EVENT_CONFIG_UPDATED, eventFn[0]); + } else if (errorStates.contains(query.getStatus())) { + //failed to connect, + reject.onInvoke("Query config in unusable state '" + query.getStatus() + "'"); + irisClient.removeEventListener(IrisClient.EVENT_CONFIG_ADDED, eventFn[0]); + irisClient.removeEventListener(IrisClient.EVENT_CONFIG_UPDATED, eventFn[0]); + } else { + console.log("Not ready, will wait another 10s before giving up"); + //give it 10s to move to one of the expected terminal states, might take a while to start up + delayTestFinish(1000 * 10); + } + }; + irisClient.addEventListener(IrisClient.EVENT_CONFIG_ADDED, eventFn[0]); + irisClient.addEventListener(IrisClient.EVENT_CONFIG_UPDATED, eventFn[0]); + }); + } + + public IThenable.ThenOnFulfilledCallbackFn table(String tableName) { + delayFinish(2000); + TODO continue here + } + + /** + * Utility method to report Promise errors to the unit test framework + */ + protected Promise report(Object error) { + if (error instanceof String) { + reportUncaughtException(new RuntimeException((String)error)); + } else if (error instanceof Throwable) { + reportUncaughtException((Throwable) error); + } if (error instanceof JsError) { + reportUncaughtException(new JavaScriptException(error)); + } else { + reportUncaughtException(new RuntimeException(error.toString())); + } + //keep failing down the chain in case someone else cares + return Promise.reject(error); + } + + protected Promise finish(Object input) { + finishTest(); + return Promise.resolve(input); + } + + /** + * Helper method to add a listener to the promise of a table, and ensure that an update is recieved with the + * expected number of items, within the specified timeout. + * + * Prereq: have already requested a viewport on that table + */ + protected Promise assertUpdateReceived(Promise tablePromise, int count, int timeoutInMillis) { + return tablePromise.then(table -> assertUpdateReceived(table, count, timeoutInMillis)); + } + + /** + * Helper method to add a listener to a table, and ensure that an update is recieved with the expected number of + * items, within the specified timeout. + * + * Prereq: have already requested a viewport on that table. Remember to request that within the same event loop, + * so that there isn't a data race and the update gets missed. + */ + protected Promise assertUpdateReceived(JsTable table, int count, int timeoutInMillis) { + return assertUpdateReceived(table, viewportData -> assertEquals(count, viewportData.getRows().length), timeoutInMillis); + } + protected Promise assertUpdateReceived(JsTable table, Consumer check, int timeoutInMillis) { + return this.waitForEvent(table, JsTable.EVENT_UPDATED, e -> { + ViewportData viewportData = e.detail; + check.accept(viewportData); + }, timeoutInMillis); + } + + protected IThenable.ThenOnFulfilledCallbackFn delayFinish(int timeout) { + return table -> { + delayTestFinish(timeout); + return Promise.resolve(table); + }; + } + protected IThenable.ThenOnFulfilledCallbackFn waitForTick(int timeout) { + return table -> waitForEvent(table, JsTable.EVENT_SIZECHANGED, ignored->{}, timeout); + } + protected IThenable.ThenOnFulfilledCallbackFn waitForTickTwice(int timeout) { + return table -> { + // wait for two ticks... one from setting the viewport, and then another for whenever the table actually ticks. + // (if these happen out of order, they will still be very close) + return waitForEvent(table, JsTable.EVENT_SIZECHANGED, ignored->{}, timeout) + .then(t-> waitForEvent(table, JsTable.EVENT_SIZECHANGED, ignored->{}, timeout)); + }; + } + protected Promise waitForEventWhere(V evented, String eventName, Predicate> check, int timeout) { + //note that this roughly reimplements the 'kill timer' so this can be run in parallel with itself or other similar steps + return new Promise<>((resolve, reject) -> { + boolean[] complete = {false}; + console.log("adding " + eventName + " listener ", evented); + //apparent compiler bug, review in gwt 2.9 + RemoverFn unsub = Js.uncheckedCast(evented) + .addEventListener(eventName, e -> { + if (complete[0]) { + return;//already done, but timeout hasn't cleared us yet + } + console.log("event ", e, " observed ", eventName, " for ", evented); + try { + if (check.test(e)) { + complete[0] = true; + resolve.onInvoke(evented); + } + } catch (Throwable ex) { + reject.onInvoke(ex); + } + }); + DomGlobal.setTimeout(p0 -> { + unsub.remove(); + if (!complete[0]) { + reject.onInvoke("Failed to complete in " + timeout + "ms " + evented); + } + complete[0] = true; + //complete already handled + }, timeout * TIMEOUT_SCALE); + + }); + } + + protected Promise waitForEvent(V evented, String eventName, Consumer> check, int timeout) { + return this.waitForEventWhere(evented, eventName, e -> { + check.accept(e); + return true; + }, timeout); + } + + + protected static Promise promiseAllThen(T then, IThenable ... promises) { + return Promise.all(promises).then(items->Promise.resolve(then)); + } + + protected IThenable waitFor(BooleanSupplier predicate, int checkInterval, int timeout, T result) { + return new Promise<>((resolve, reject) -> { + schedule(predicate, checkInterval, () -> resolve.onInvoke(result), () -> reject.onInvoke("timeout of " + timeout + " exceeded"), timeout); + }); + } + protected IThenable.ThenOnFulfilledCallbackFn waitFor(int millis) { + return result -> new Promise<>((resolve, reject) -> { + DomGlobal.setTimeout(p -> resolve.onInvoke(result), millis); + }); + } + protected IThenable.ThenOnFulfilledCallbackFn waitForEvent(T table, String eventName, int millis) { + return result -> new Promise<>((resolve, reject) -> { + boolean[] success = {false}; + table.addEventListenerOneShot(eventName, e-> { + success[0] = true; + resolve.onInvoke(table); + }); + DomGlobal.setTimeout(p -> { + if (!success[0]) { + reject.onInvoke("Waited " + millis + "ms"); + } + }, millis); + }); + } + + private void schedule(BooleanSupplier predicate, int checkInterval, Runnable complete, Runnable fail, int timeout) { + if (timeout <= 0) { + fail.run(); + } + if (predicate.getAsBoolean()) { + complete.run(); + } + DomGlobal.setTimeout(ignore -> { + if (predicate.getAsBoolean()) { + complete.run(); + } else { + schedule(predicate, checkInterval, complete, fail, (timeout * TIMEOUT_SCALE) - checkInterval); + } + }, checkInterval); + } + + protected Promise assertNextViewportIs(JsTable table, Function column, String[] expected) { + return assertUpdateReceived(table, viewportData -> { + String[] actual = Js.uncheckedCast(getColumnData(viewportData, column.apply(table))); + assertTrue("Expected " + Arrays.toString(expected) + ", found " + Arrays.toString(actual) + " in table " + table + " at state " + table.state(), Arrays.equals(expected, actual)); + }, 2000); + } + + protected Object getColumnData(ViewportData viewportData, Column a) { + return viewportData.getRows().map((r, index, all) -> r.get(a)); + } + + protected Promise assertNextViewportIs(JsTable table, double... expected) { + return assertUpdateReceived(table, viewportData -> { + double[] actual = Js.uncheckedCast(getColumnData(viewportData, table.findColumn("I"))); + assertTrue("Expected " + Arrays.toString(expected) + ", found " + Arrays.toString(actual) + " in table " + table, Arrays.equals(expected, actual)); + }, 2000); + } + + public static List filterColumns(JsTable table, JsPredicate filter) { + List matches = new ArrayList<>(); + table.getColumns().forEach((c, i, arr)->{ + if (filter.test(c)) { + matches.add(c); + } + return null; + }); + return matches; + } +} diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/JsPredicate.java b/web/client-api/src/test/java/io/deephaven/web/client/api/JsPredicate.java new file mode 100644 index 00000000000..9412244301f --- /dev/null +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/JsPredicate.java @@ -0,0 +1,11 @@ +package io.deephaven.web.client.api; + +import jsinterop.annotations.JsFunction; + +@JsFunction +@FunctionalInterface +public interface JsPredicate { + + @SuppressWarnings("unusable-by-js") + boolean test(I input); +} \ No newline at end of file diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java index 14296bf4223..a745f67fc52 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java @@ -14,7 +14,7 @@ public class FilterConditionTestGwt extends GWTTestCase { @Override public String getModuleName() { - return "io.deephaven.web.DhApiDev"; + return "io.deephaven.web.DeephavenApiDev"; } private Column getColumn() { diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsDateTimeFormatTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsDateTimeFormatTestGwt.java index abe3f477692..4defa31fd14 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsDateTimeFormatTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsDateTimeFormatTestGwt.java @@ -90,6 +90,6 @@ private long assertRoundTrip(String formatString, String input) { @Override public String getModuleName() { - return "io.deephaven.web.DhApiDev"; + return "io.deephaven.web.DeephavenApiDev"; } } diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsNumberFormatTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsNumberFormatTestGwt.java index fe95c725609..4c2a604e3e8 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsNumberFormatTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsNumberFormatTestGwt.java @@ -70,6 +70,6 @@ public void testLongFormat() { @Override public String getModuleName() { - return "io.deephaven.web.DhApiDev"; + return "io.deephaven.web.DeephavenApiDev"; } } diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java new file mode 100644 index 00000000000..1d832bd01c7 --- /dev/null +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java @@ -0,0 +1,500 @@ +package io.deephaven.web.client.api.subscription; + +import com.google.gwt.junit.client.GWTTestCase; +import elemental2.dom.DomGlobal; +import elemental2.promise.IThenable; +import elemental2.promise.Promise; +import io.deephaven.web.client.api.AbstractAsyncGwtTestCase; +import io.deephaven.web.client.api.Column; +import io.deephaven.web.client.api.HasEventHandling; +import io.deephaven.web.client.api.JsTable; +import io.deephaven.web.shared.fu.RemoverFn; +import jsinterop.base.Js; + +import java.util.Objects; + +/** + * Assumes two tables, ticking every 2 seconds: + * + * growingForward = db.timeTable("00:00:01").update("I=i", "J=i*i", "K=0") + * growingBackward = growingForward.sortDescending("Timestamp") + * blinkOne = db.timeTable("00:00:01").update("I=i", "J=1").lastBy("J").where("I%2 != 0") + * + * And another static one: + * + * staticTable = emptyTable(100).update("I=i") + */ +public class ViewportTestGwt extends AbstractAsyncGwtTestCase { + + private final TableSource tables = new TableSourceBuilder() + .script("staticTable", "deephaven.empty_table(100).update(\"I=i\")", "emptyTable(100).update(\"I=i\")") + .script("growingForward", "timeTable(\"00:00:01\").update(\"I=i\", \"J=i*i\", \"K=0\")", "timeTable(\"00:00:01\").update(\"I=i\", \"J=i*i\", \"K=0\")") + .script("growingBackward", "growingForward.sortDescending(\"Timestamp\")", "growingForward.sortDescending(\"Timestamp\")") + .script("blinkOne", "deephaven.empty_table(100).update(\"I=i\")", "emptyTable(100).update(\"I=i\")") + .build(); + + public void testViewportOnStaticTable() { + connect(tables) + .then(table("staticTable")) + .then(table -> { + delayTestFinish(5000); + + int size = (int)table.getSize(); + int lastRow = size - 1; + table.setViewport(0, lastRow, null); + return assertUpdateReceived(table, size, 500); + }) + .then(table -> { + //table has 100 rows, go through each page of 25, make sure the offset and length is sane + table.setViewport(0, 24, null); + return assertUpdateReceived(table, viewport -> { + assertEquals(0, (long) viewport.getOffset()); + assertEquals(25, viewport.getRows().length); + }, 1000); + }) + .then(table -> { + table.setViewport(25, 49, null); + return assertUpdateReceived(table, viewport -> { + assertEquals(25, (long) viewport.getOffset()); + assertEquals(25, viewport.getRows().length); + }, 1000); + }) + .then(table -> { + table.setViewport(50, 74, null); + return assertUpdateReceived(table, viewport -> { + assertEquals(50, (long) viewport.getOffset()); + assertEquals(25, viewport.getRows().length); + }, 1000); + }) + .then(table -> { + table.setViewport(75, 99, null); + return assertUpdateReceived(table, viewport -> { + assertEquals(75, (long) viewport.getOffset()); + assertEquals(25, viewport.getRows().length); + }, 1000); + }) + .then(this::finish).catch_(this::report); + } + + // TODO: https://deephaven.atlassian.net/browse/DH-11196 + public void ignore_testViewportOnGrowingTable() { + connect("sample") + .then(table("growingForward")) + .then(waitForTick(2200)) + .then(delayFinish(25_000)) + .then(table ->{ + // set viewport to actual table size, check that all items are present + int size = (int) table.getSize(); + int lastRow = size - 1; + table.setViewport(0, lastRow, null); + return assertUpdateReceived(table, size, 1500); + }) + .then(waitForTick(2201)) + .then(table -> { + // set viewport size to be larger than range of items, check only size items are present + int size = (int) table.getSize(); + table.setViewport(0, size, null); + return assertUpdateReceived(table, size, 1501); + }) + .then(waitForTick(2202)) + .then(table -> { + table.setViewport(1, 2, null); + // start with the last visible item, showing more than one item, should only see one item at first, + // but we'll tick forward to see more + int size = (int) table.getSize(); + int lastRow = size - 1; + table.setViewport(lastRow, lastRow + 99, null); + return assertUpdateReceived(table, 1, 1502); + }) + .then(waitForTick(2203)) + .then(table -> { + // wait for the size to tick once, verify that the current viewport size reflects that + double size = table.getSize(); + double lastRow = size - 1; + table.setViewport(size, lastRow + 9, null); + return waitFor(() -> table.getSize() == size + 1, 100, 3000, table) + .then(waitForEvent(table, JsTable.EVENT_SIZECHANGED, 2503)) + .then(JsTable::getViewportData) + .then(viewportData -> { + assertEquals(2, viewportData.getRows().length); + //switch back to table for next promise + return Promise.resolve(table); + }); + }) + .then(this::finish).catch_(this::report); + } + + public void testViewportOnUpdatingTable() { + connect("sample") + .then(table("growingBackward")) + .then(table -> { + delayTestFinish(4000); + // set up a viewport, and watch it show up, and tick once + table.setViewport(0, 9, null); + return assertUpdateReceived(table, viewportData -> {}, 1000); + }) + .then(table -> { + return assertUpdateReceived(table, viewportData -> {}, 2000); + }) + .then(this::finish).catch_(this::report); + } + + private static int indexOf(JsArray array, T object) { + return indexOf(array.asList().toArray(), object); + } + private static int indexOf(Object[] array, T object) { + for (int i = 0; i < array.length; i++) { + Object t = array[i]; + if (Objects.equals(t, object)) { + return i; + } + } + + return -1; + } + + public void testViewportSubsetOfColumns() { + connect("sample") + .then(table("growingBackward")) + .then(table -> { + delayTestFinish(8000); + table.setViewport(0, 0, Js.uncheckedCast(new Column[] {table.findColumn("I")})); + + return assertUpdateReceived(table, viewport -> { + assertEquals(1, viewport.getColumns().length); + assertEquals(0, indexOf(viewport.getColumns(), table.findColumn("I"))); + + assertEquals(1, viewport.getRows().length); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("I"))); + assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("J"))); + assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("K"))); + + }, 500); + }) + .then(table -> { + //don't change viewport, test the same thing again, make sure deltas behave too + return assertUpdateReceived(table, viewport -> { + assertEquals(1, viewport.getColumns().length); + assertEquals(0, indexOf(viewport.getColumns(), table.findColumn("I"))); + + assertEquals(1, viewport.getRows().length); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("I"))); + assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("J"))); + assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("K"))); + + }, 2000); + }) + .then(table -> { + table.setViewport(0, 0, Js.uncheckedCast(new Column[] {table.findColumn("J")})); + + return assertUpdateReceived(table, viewport -> { + assertEquals(1, viewport.getColumns().length); + assertEquals(0, indexOf(viewport.getColumns(), table.findColumn("J"))); + + assertEquals(1, viewport.getRows().length); + assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("I"))); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("J"))); + assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("K"))); + + }, 500); + }) + .then(table -> { + table.setViewport(0, 0, Js.uncheckedCast(new Column[] {table.findColumn("K")})); + + return assertUpdateReceived(table, viewport -> { + assertEquals(1, viewport.getColumns().length); + assertEquals(0, indexOf(viewport.getColumns(), table.findColumn("K"))); + + assertEquals(1, viewport.getRows().length); + assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("I"))); + assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("J"))); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("K"))); + + }, 500); + }) + .then(table -> { + table.setViewport(0, 0, Js.uncheckedCast(new Column[] { + table.findColumn("J"), + table.findColumn("K") + })); + + return assertUpdateReceived(table, viewport -> { + assertEquals(2, viewport.getColumns().length); + assertEquals(0, indexOf(viewport.getColumns(), table.findColumn("J"))); + assertEquals(1, indexOf(viewport.getColumns(), table.findColumn("K"))); + + assertEquals(1, viewport.getRows().length); + assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("I"))); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("J"))); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("K"))); + + }, 500); + }) + .then(table -> { + table.setViewport(0, 0, Js.uncheckedCast(new Column[] { + table.findColumn("J"), + table.findColumn("Timestamp"), + table.findColumn("I"), + table.findColumn("K") + })); + + return assertUpdateReceived(table, viewport -> { + assertEquals(4, viewport.getColumns().length); + assertEquals(0, indexOf(viewport.getColumns(), table.findColumn("Timestamp"))); + assertEquals(1, indexOf(viewport.getColumns(), table.findColumn("I"))); + assertEquals(2, indexOf(viewport.getColumns(), table.findColumn("J"))); + assertEquals(3, indexOf(viewport.getColumns(), table.findColumn("K"))); + + assertEquals(1, viewport.getRows().length); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("Timestamp"))); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("I"))); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("J"))); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("K"))); + + }, 500); + }) + .then(table -> { + table.setViewport(0, 0, null); + + return assertUpdateReceived(table, viewport -> { + assertEquals(4, viewport.getColumns().length); + assertEquals(0, indexOf(viewport.getColumns(), table.findColumn("Timestamp"))); + assertEquals(1, indexOf(viewport.getColumns(), table.findColumn("I"))); + assertEquals(2, indexOf(viewport.getColumns(), table.findColumn("J"))); + assertEquals(3, indexOf(viewport.getColumns(), table.findColumn("K"))); + + assertEquals(1, viewport.getRows().length); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("Timestamp"))); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("I"))); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("J"))); + assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("K"))); + + }, 500); + }) + .then(this::finish).catch_(this::report); + } + + // TODO: https://deephaven.atlassian.net/browse/DH-11196 + public void ignore_testEmptyTableWithViewport() { + //confirm that when the viewport is set on an empty table that we get exactly one update event + connect("sample") + .then(table("staticTable")) + .then(table -> { + delayTestFinish(10000); + DomGlobal.console.log("size", table.getSize()); + // change the filter, set a viewport, assert that sizechanged and update both happen once + table.applyFilter(new FilterCondition[] { + FilterValue.ofBoolean(false).isTrue() + }); + table.setViewport(0, 100, null); + return JsPromise.all(new IThenable[] { + // when IDS-2113 is fixed, restore this stronger assertion +// assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000) + waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> {}, 1000), + assertEventFiresOnce(table, JsTable.EVENT_SIZECHANGED, 1000) + }).then(ignore -> Promise.resolve(table)); + }) + .then(table -> { + // reset the filter, wait for back to normal + table.applyFilter(new FilterCondition[0]); + table.setViewport(0, 100, null); + return assertUpdateReceived(table, ignore -> {}, 1000); + }) + .then(table -> { + //change the filter, don't set a viewport, assert only size changes + table.applyFilter(new FilterCondition[] { + FilterValue.ofBoolean(false).isTrue() + }); + return assertEventFiresOnce(table, JsTable.EVENT_SIZECHANGED, 1000); + }) + .then(table -> { + //set a viewport, assert that update fires and no size change + table.setViewport(0, 100, null); + // when IDS-2113 is fixed, restore this stronger assertion +// return assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000); + return waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> {}, 1000); + }) + .then(this::finish).catch_(this::report); + } + + public void testViewportOutOfRangeOfTable() { + //confirm that when the viewport is set beyond the range of the table that we get exactly one update event + connect("sample") + .then(table("staticTable")) + .then(table -> { + table.setViewport(100, 104, null); + + return JsPromise.all(new IThenable[]{ + // when IDS-2113 is fixed, restore this stronger assertion +// assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000) + waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> {}, 1000) + }).then(ignore -> Promise.resolve(table)); + }) + .then(this::finish).catch_(this::report); + + } + + public void testRapidChangingViewport() { + connect("sample") + .then(table("staticTable")) + .then(table -> { + delayTestFinish(5000); + //test running both synchronously + table.setViewport(0, 10, null); + table.setViewport(5, 14, null); + return assertUpdateReceived(table, viewport -> { + assertEquals(5, (int) viewport.getOffset()); + assertEquals(10, (int) viewport.getRows().length); + }, 1000); + }) + .then(table -> { + //test changing the viewport over a microtask (anyone in the web api getting clever with batching?) + table.setViewport(0, 10, null); + return Promise.resolve((Object)null).then(ignore -> Promise.resolve(table)); + }) + .then(table -> { + table.setViewport(6, 14, null); + return assertUpdateReceived(table, viewport -> { + assertEquals(6, (int) viewport.getOffset()); + assertEquals(9, (int) viewport.getRows().length); + }, 1000); + }) + .then(table -> { + table.setViewport(0, 10, null); + return Promise.resolve(table); + }) + //test again over a 4ms delay, minimum task delay + .then(waitFor(4)) + .then(table -> { + table.setViewport(7, 17, null); + return assertUpdateReceived(table, ignored->{}, 1000) + .then(waitFor(JsTable.DEBOUNCE_TIME * 2)) + .then(t -> { + // force the debounce to be processed + t.processSnapshot(); + t.getViewportData().then(vp->{ +// assertEquals(7, (int) vp.getOffset()); + assertEquals(11, (int) vp.getRows().length); + return Promise.resolve(vp); + }); + return Promise.resolve(t); + }); + }) + .then(this::finish).catch_(this::report); + } + + public void testViewportWithNoInitialItems() { + // The bug exposed by this case is that a snapshot might start initially empty, but then get + // a delta to make it non-empty. This test goes further, and waits until it is empty again, + // and then cycles back to non-empty once more to make sure all the transitions are tested + connect("sample") + .then(table("blinkOne")) + .then(table -> { + delayTestFinish(20_000); + + //first run, assume all columns + return helperForViewportWithNoInitialItems(table, null, table.getColumns()); + }).then(table -> { + // second, specify only one column to ensure that it is respected + Column i = table.findColumn("I"); + return helperForViewportWithNoInitialItems(table, new Column[] {i}, new JsArray<>(i)); + }) + .then(this::finish).catch_(this::report); + } + + private IThenable helperForViewportWithNoInitialItems(JsTable t, Column[] requestColumns, JsArray expectedColumns) { + // wait until zero rows are present, so we can set the viewport and get a zero-row "snapshot" + return waitFor(() -> t.getSize() == 0, 100, 2000, t) + .then(table -> { + // set up the viewport to only watch for the first row, then wait until zero rows + table.setViewport(0, 0, Js.uncheckedCast(requestColumns)); + + // viewport should come back quickly showing no data, and all columns + return assertUpdateReceived(table, emptyViewport -> { + assertEquals(0, emptyViewport.getRows().length); + assertEquals(expectedColumns.length, emptyViewport.getColumns().length); + }, 1501); + }) + .then(table -> { + // wait for the next tick, where we get the "first" row added, confirm that the viewport + // data is sane + return waitForEventWhere(table, "updated", (CustomEvent e) -> { + ViewportData viewport = (ViewportData) e.detail; + if (viewport.getRows().length != 1) { + return false; //wrong data, wait for another event + } + assertEquals(expectedColumns.length, viewport.getColumns().length); + for (int i = 0; i < viewport.getColumns().length; i++) { + final Column c = viewport.getColumns().getAt(i); + assertNotNull(viewport.getRows().getAt(0).get(c)); + } + return true; + }, 1502); + }) + .then(table -> { + // again wait for the table to go back to zero items, make sure it makes sense + return waitForEventWhere(table, "updated", (CustomEvent e) -> { + ViewportData emptyViewport = (ViewportData) e.detail; + if (emptyViewport.getRows().length != 0) { + return false; //wrong data, wait for another event + } + assertEquals(expectedColumns.length, emptyViewport.getColumns().length); + return true; + }, 1503); + }) + .then(table -> { + // one more tick later, we'll see the item back again + return waitForEventWhere(table, "updated", (CustomEvent e) -> { + ViewportData viewport = (ViewportData) e.detail; + if (viewport.getRows().length != 1) { + return false; //wrong data, wait for another event + } + assertEquals(expectedColumns.length, viewport.getColumns().length); + for (int i = 0; i < viewport.getColumns().length; i++) { + final Column c = viewport.getColumns().getAt(i); + assertNotNull(viewport.getRows().getAt(0).get(c)); + } + return true; + }, 1504); + }); + } + + private Promise assertEventFiresOnce(T eventSource, String eventName, int intervalInMilliseconds) { + return new Promise<>((resolve, reject) -> { + int[] runCount = {0}; + console.log("adding " + eventName + " listener " + eventSource); + //apparent compiler bug, review in gwt 2.9 + RemoverFn unsub = Js.uncheckedCast(eventSource) + .addEventListener(eventName, e -> { + runCount[0]++; + console.log(eventName + " event observed " + eventSource + ", #" + runCount[0]); + if (runCount[0] > 1) { + reject.onInvoke("Event " + eventName + " fired " + runCount[0] + " times"); + } + }); + DomGlobal.setTimeout(p0 -> { + unsub.remove(); + if (runCount[0] == 1) { + resolve.onInvoke(eventSource); + } else { + reject.onInvoke("Event " + eventName + " fired " + runCount[0] + " times"); + } + }, intervalInMilliseconds); + }); + } + + private void assertThrowsException(Runnable r) { + try { + r.run(); + fail("Expected exception"); + } catch (Exception ignore) { + //expected + } + } + + @Override + public String getModuleName() { + return "io.deephaven.web.DeephavenIntegrationTest"; + } +} + From c05823ac5e90ca59eaad0ded17cd0b138991eb1a Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Wed, 19 Apr 2023 10:44:31 -0500 Subject: [PATCH 02/31] Tests build, but do not pass --- buildSrc/src/main/groovy/GwtTools.groovy | 2 +- web/client-api/client-api.gradle | 18 ++- .../io/deephaven/web/client/api/JsTable.java | 5 +- .../web/ClientIntegrationTestSuite.java | 3 + .../web/DeephavenIntegrationTest.gwt.xml | 3 + .../deephaven/web/DeephavenUnitTest.gwt.xml | 3 + .../client/api/AbstractAsyncGwtTestCase.java | 115 ++++++++++-------- .../api/filter/FilterConditionTestGwt.java | 2 +- .../api/subscription/ViewportTestGwt.java | 33 ++--- 9 files changed, 110 insertions(+), 74 deletions(-) create mode 100644 web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml create mode 100644 web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml diff --git a/buildSrc/src/main/groovy/GwtTools.groovy b/buildSrc/src/main/groovy/GwtTools.groovy index 51cd7d4fee5..3a9063bfffa 100644 --- a/buildSrc/src/main/groovy/GwtTools.groovy +++ b/buildSrc/src/main/groovy/GwtTools.groovy @@ -134,7 +134,7 @@ class GwtTools { static void addGeneratedSources(Project project, GwtCompileTask gwtc) { if (project.configurations.getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME).dependencies) { (gwtc.src as ConfigurableFileCollection).from( - (project.tasks.getByName(JavaPlugin.COMPILE_JAVA_TASK_NAME) as JavaCompile).options.annotationProcessorGeneratedSourcesDirectory + (project.tasks.getByName(JavaPlugin.COMPILE_JAVA_TASK_NAME) as JavaCompile).options.generatedSourceOutputDirectory ) } project.configurations.getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME).allDependencies.withType(ProjectDependency)*.dependencyProject*.each { diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index 5804c640315..700b715ef31 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -44,11 +44,16 @@ artifacts { } tasks.register('gwtUnitTest', Test) { t -> - +// gradle.projectsEvaluated { +// t.classpath += project(":web-client-api").tasks.getByName('gwtCompile').src +// } t.systemProperties = [ 'gwt.args':'-runStyle HtmlUnit -ea -style PRETTY', - 'gwt.persistentunitcachedir':layout.buildDirectory.dir('unitTest-unitCache') + 'gwt.persistentunitcachedir':layout.buildDirectory.dir('integrationTest-unitCache').get().asFile.absolutePath, ] + t.include '**/ClientUnitTestSuite.class' + t.useJUnit() + t.scanForTestClasses = false } // start a grpc-api server @@ -65,17 +70,20 @@ tasks.register('gwtIntegrationTest', Test) { t -> t.dependsOn(deephavenDocker.portTask) doFirst { t.systemProperty('dh.server', 'http://localhost:' + deephavenDocker.port.get()) + t.classpath += tasks.getByName('gwtCompile').src + t.classpath.files.each { + println it + } } t.finalizedBy(deephavenDocker.endTask) t.systemProperties = [ 'gwt.args':'-runStyle HtmlUnit -ea -style PRETTY', - 'gwt.persistentunitcachedir':layout.buildDirectory.dir('integrationTest-unitCache').toString(), + 'gwt.persistentunitcachedir':layout.buildDirectory.dir('integrationTest-unitCache').get().asFile.absolutePath, // 'dh.server':'http://localhost:' + deephavenDocker.port.get() ] - t.include '**/*TestSuite.class' + t.include '**/ClientIntegrationTestSuite.class' t.useJUnit() t.scanForTestClasses = false - } project.tasks.getByName('quick').dependsOn project.tasks.withType(de.esoco.gwt.gradle.task.GwtCompileTask) diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java index 398c2915a3e..dbde60b282a 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java @@ -71,6 +71,7 @@ import io.deephaven.web.shared.fu.JsProvider; import io.deephaven.web.shared.fu.JsRunnable; import io.deephaven.web.shared.fu.RemoverFn; +import jsinterop.annotations.JsIgnore; import jsinterop.annotations.JsMethod; import jsinterop.annotations.JsNullable; import jsinterop.annotations.JsOptional; @@ -1722,8 +1723,8 @@ public void handleSnapshot(TableTicket handle, TableSnapshot snapshot) { viewportRows.size()); } - - protected void processSnapshot() { + @JsIgnore + public void processSnapshot() { try { if (debounce == null) { JsLog.debug("Skipping snapshot b/c debounce is null"); diff --git a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java index 84b8080a53a..03b94f7f9ad 100644 --- a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java +++ b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java @@ -2,6 +2,7 @@ import com.google.gwt.junit.tools.GWTTestSuite; import io.deephaven.web.client.api.filter.FilterConditionTestGwt; +import io.deephaven.web.client.api.subscription.ViewportTestGwt; import junit.framework.Test; import junit.framework.TestSuite; @@ -12,6 +13,8 @@ public static Test suite() { // This test doesn't actually talk to the server, but it requires the dh-internal library be available suite.addTestSuite(FilterConditionTestGwt.class); + // Actual integration tests + suite.addTestSuite(ViewportTestGwt.class); return suite; diff --git a/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml b/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml new file mode 100644 index 00000000000..63f659f1685 --- /dev/null +++ b/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml b/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml new file mode 100644 index 00000000000..63f659f1685 --- /dev/null +++ b/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java index 71582eb9219..3d66911a0ef 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java @@ -11,11 +11,17 @@ import elemental2.promise.IThenable; import elemental2.promise.Promise; import io.deephaven.web.client.api.subscription.ViewportData; +import io.deephaven.web.client.fu.CancellablePromise; +import io.deephaven.web.client.ide.IdeSession; import io.deephaven.web.shared.data.Viewport; import io.deephaven.web.shared.fu.JsRunnable; import io.deephaven.web.shared.fu.RemoverFn; +import jsinterop.annotations.JsMethod; +import jsinterop.annotations.JsPackage; import jsinterop.annotations.JsProperty; import jsinterop.base.Js; +import jsinterop.base.JsPropertyMap; +import org.apache.tapestry.INamespace; import java.util.ArrayList; import java.util.Arrays; @@ -25,7 +31,19 @@ import java.util.function.Function; import java.util.function.Predicate; +import static elemental2.dom.DomGlobal.console; + public abstract class AbstractAsyncGwtTestCase extends GWTTestCase { + @JsMethod(namespace = JsPackage.GLOBAL, name = "import") + private static native Promise> importScript(String moduleName); + private static Promise importDhInternal() { + return importScript("dhinternal.js") + .then(module -> { + Js.asPropertyMap(DomGlobal.window).set("dhinternal", module.get("dhinternal")); + return Promise.resolve((Void) null); + }); + } + public static final String localServer = System.getProperty("dhTestServer", "http://localhost:10000"); public static class TableSourceBuilder { public TableSourceBuilder script(String tableName, String python, String groovy) { @@ -35,12 +53,21 @@ public TableSourceBuilder script(String tableName, String python, String groovy) } public TableSource build() { - return null; + return new TableSourceImpl(); } } public interface TableSource { Promise table(String name); } + private static class TableSourceImpl implements TableSource { + private JsArray groovyScripts; + private JsArray pythonScripts; + + @Override + public Promise table(String name) { + return null; + } + } /** @@ -111,61 +138,47 @@ static Promise expectFailure(Promise state, T value) { * Connects and authenticates to the specified server, and resolves once the specified query config has * become available. */ - protected Promise connect(String queryConfigName) { + protected Promise connect(TableSource tables) { + TableSourceImpl impl = (TableSourceImpl) tables; //start by delaying test finish by .5s so we fail fast in cases where we aren't set up right - delayTestFinish(2000); - //for example, if the server doesn't have the query config that we're expecting to get, we'll fail nearly right away - return new Promise<>((resolve, reject) -> { - IrisClient irisClient = new IrisClient(localServer); - irisClient.addEventListener(IrisClient.EVENT_CONNECT, e -> { - irisClient.login(LoginCredentials.of(username, password, "password", username)).then(success -> { - //success, but we don't need to do anything, events are wired up to get specifics, grant another timeout - - delayTestFinish(2001 * TIMEOUT_SCALE);//if this (uniquely value'd) timeout fails, likely the config doesn't exist. - - return null; - }, failure -> { - reject.onInvoke("login failed " + failure); - return null; - }); - }); - EventFn[] eventFn = {}; - eventFn[0] = e -> { - QueryInfo query = (QueryInfo) ((CustomEvent) e).detail; - console.log("config event: " + query.getName() + " [" + query.getStatus() + "]"); - if (!queryConfigName.equals(query.getName())) { - //unimportant, wait a bit longer - return; - } - if ("Running".equalsIgnoreCase(query.getStatus())) { - // provide more detail in the console - query.addEventListener(QueryInfo.EVENT_TABLE_METRICS, metricsEvent -> { - JsPropertyMap detail = (JsPropertyMap) ((CustomEvent) metricsEvent).detail; - console.log(detail.get("type") + ": " + detail.get("formatted")); + delayTestFinish(500); + return importDhInternal().then(module -> { + CoreClient coreClient = new CoreClient(localServer, null); + return coreClient.login(JsPropertyMap.of("type", CoreClient.LOGIN_TYPE_ANONYMOUS)) + .then(ignore -> coreClient.getAsIdeConnection()) + .then(ide -> { + delayTestFinish(500); + return ide.getConsoleTypes().then(consoleTypes -> { + CancellablePromise ideSession = ide.startSession(consoleTypes.getAt(0)); + return ideSession.then(session -> { + if (consoleTypes.includes("groovy")) { + // scripts must run in order, to be sure let block on each one + return runAllScriptsInOrder(ideSession, session, impl.groovyScripts); + } else if (consoleTypes.includes("python")) { + return runAllScriptsInOrder(ideSession, session, impl.pythonScripts); + } + throw new IllegalStateException("Unknown script type " + consoleTypes); + }); + }); }); - - resolve.onInvoke(query); - irisClient.removeEventListener(IrisClient.EVENT_CONFIG_ADDED, eventFn[0]); - irisClient.removeEventListener(IrisClient.EVENT_CONFIG_UPDATED, eventFn[0]); - } else if (errorStates.contains(query.getStatus())) { - //failed to connect, - reject.onInvoke("Query config in unusable state '" + query.getStatus() + "'"); - irisClient.removeEventListener(IrisClient.EVENT_CONFIG_ADDED, eventFn[0]); - irisClient.removeEventListener(IrisClient.EVENT_CONFIG_UPDATED, eventFn[0]); - } else { - console.log("Not ready, will wait another 10s before giving up"); - //give it 10s to move to one of the expected terminal states, might take a while to start up - delayTestFinish(1000 * 10); - } - }; - irisClient.addEventListener(IrisClient.EVENT_CONFIG_ADDED, eventFn[0]); - irisClient.addEventListener(IrisClient.EVENT_CONFIG_UPDATED, eventFn[0]); }); } - public IThenable.ThenOnFulfilledCallbackFn table(String tableName) { - delayFinish(2000); - TODO continue here + private Promise runAllScriptsInOrder(CancellablePromise ideSession, IdeSession session, JsArray code) { + Promise result = ideSession; + for (int i = 0; i < code.length; i++) { + final int index = i; + result = result.then(ignore -> { + delayFinish(2000); + session.runCode(code.getAt(index)); + return ideSession; + }); + } + return result; + } + + public IThenable.ThenOnFulfilledCallbackFn table(String tableName) { + return session -> session.getTable(tableName, null); } /** diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java index a745f67fc52..f259668208d 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java @@ -14,7 +14,7 @@ public class FilterConditionTestGwt extends GWTTestCase { @Override public String getModuleName() { - return "io.deephaven.web.DeephavenApiDev"; + return "io.deephaven.web.DeephavenIntegrationTest"; } private Column getColumn() { diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java index 1d832bd01c7..18cc0b23892 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java @@ -1,6 +1,7 @@ package io.deephaven.web.client.api.subscription; -import com.google.gwt.junit.client.GWTTestCase; +import elemental2.core.JsArray; +import elemental2.dom.CustomEvent; import elemental2.dom.DomGlobal; import elemental2.promise.IThenable; import elemental2.promise.Promise; @@ -8,11 +9,15 @@ import io.deephaven.web.client.api.Column; import io.deephaven.web.client.api.HasEventHandling; import io.deephaven.web.client.api.JsTable; +import io.deephaven.web.client.api.filter.FilterCondition; +import io.deephaven.web.client.api.filter.FilterValue; import io.deephaven.web.shared.fu.RemoverFn; import jsinterop.base.Js; import java.util.Objects; +import static elemental2.dom.DomGlobal.console; + /** * Assumes two tables, ticking every 2 seconds: * @@ -78,7 +83,7 @@ public void testViewportOnStaticTable() { // TODO: https://deephaven.atlassian.net/browse/DH-11196 public void ignore_testViewportOnGrowingTable() { - connect("sample") + connect(tables) .then(table("growingForward")) .then(waitForTick(2200)) .then(delayFinish(25_000)) @@ -125,7 +130,7 @@ public void ignore_testViewportOnGrowingTable() { } public void testViewportOnUpdatingTable() { - connect("sample") + connect(tables) .then(table("growingBackward")) .then(table -> { delayTestFinish(4000); @@ -154,7 +159,7 @@ private static int indexOf(Object[] array, T object) { } public void testViewportSubsetOfColumns() { - connect("sample") + connect(tables) .then(table("growingBackward")) .then(table -> { delayTestFinish(8000); @@ -277,17 +282,17 @@ public void testViewportSubsetOfColumns() { // TODO: https://deephaven.atlassian.net/browse/DH-11196 public void ignore_testEmptyTableWithViewport() { //confirm that when the viewport is set on an empty table that we get exactly one update event - connect("sample") + connect(tables) .then(table("staticTable")) .then(table -> { delayTestFinish(10000); - DomGlobal.console.log("size", table.getSize()); + console.log("size", table.getSize()); // change the filter, set a viewport, assert that sizechanged and update both happen once table.applyFilter(new FilterCondition[] { FilterValue.ofBoolean(false).isTrue() }); table.setViewport(0, 100, null); - return JsPromise.all(new IThenable[] { + return Promise.all(new IThenable[] { // when IDS-2113 is fixed, restore this stronger assertion // assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000) waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> {}, 1000), @@ -319,12 +324,12 @@ public void ignore_testEmptyTableWithViewport() { public void testViewportOutOfRangeOfTable() { //confirm that when the viewport is set beyond the range of the table that we get exactly one update event - connect("sample") + connect(tables) .then(table("staticTable")) .then(table -> { table.setViewport(100, 104, null); - return JsPromise.all(new IThenable[]{ + return Promise.all(new IThenable[]{ // when IDS-2113 is fixed, restore this stronger assertion // assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000) waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> {}, 1000) @@ -335,7 +340,7 @@ public void testViewportOutOfRangeOfTable() { } public void testRapidChangingViewport() { - connect("sample") + connect(tables) .then(table("staticTable")) .then(table -> { delayTestFinish(5000); @@ -387,7 +392,7 @@ public void testViewportWithNoInitialItems() { // The bug exposed by this case is that a snapshot might start initially empty, but then get // a delta to make it non-empty. This test goes further, and waits until it is empty again, // and then cycles back to non-empty once more to make sure all the transitions are tested - connect("sample") + connect(tables) .then(table("blinkOne")) .then(table -> { delayTestFinish(20_000); @@ -418,7 +423,7 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column .then(table -> { // wait for the next tick, where we get the "first" row added, confirm that the viewport // data is sane - return waitForEventWhere(table, "updated", (CustomEvent e) -> { + return waitForEventWhere(table, "updated", (CustomEvent e) -> { ViewportData viewport = (ViewportData) e.detail; if (viewport.getRows().length != 1) { return false; //wrong data, wait for another event @@ -433,7 +438,7 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column }) .then(table -> { // again wait for the table to go back to zero items, make sure it makes sense - return waitForEventWhere(table, "updated", (CustomEvent e) -> { + return waitForEventWhere(table, "updated", (CustomEvent e) -> { ViewportData emptyViewport = (ViewportData) e.detail; if (emptyViewport.getRows().length != 0) { return false; //wrong data, wait for another event @@ -444,7 +449,7 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column }) .then(table -> { // one more tick later, we'll see the item back again - return waitForEventWhere(table, "updated", (CustomEvent e) -> { + return waitForEventWhere(table, "updated", (CustomEvent e) -> { ViewportData viewport = (ViewportData) e.detail; if (viewport.getRows().length != 1) { return false; //wrong data, wait for another event From 30d1df458b6e88dbbdcb32238adbf9e693154c0f Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Thu, 28 Sep 2023 08:24:58 -0500 Subject: [PATCH 03/31] bad idea: opt in to a huge payload from the server - should instead replace this with handling many smaller snapshots --- .../main/java/io/deephaven/web/client/api/WorkerConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/WorkerConnection.java b/web/client-api/src/main/java/io/deephaven/web/client/api/WorkerConnection.java index 7237e5b5a66..a770cd75b83 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/WorkerConnection.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/WorkerConnection.java @@ -1380,7 +1380,7 @@ private void flush() { // TODO #188 support minUpdateIntervalMs double serializationOptionsOffset = BarrageSubscriptionOptions .createBarrageSubscriptionOptions(subscriptionReq, ColumnConversionMode.Stringify, true, 1000, - 0, 0); + 0, 1_000_000_000); double tableTicketOffset = BarrageSubscriptionRequest.createTicketVector(subscriptionReq, state.getHandle().getTicket()); BarrageSubscriptionRequest.startBarrageSubscriptionRequest(subscriptionReq); From 47a68cd52c93055c382488ef4d31d88327f42ba5 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Thu, 28 Sep 2023 08:27:39 -0500 Subject: [PATCH 04/31] quick hack to decrease treemap costs - fix with non-random loading --- .../subscription/SubscriptionTableData.java | 77 ++++++++++++++----- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/subscription/SubscriptionTableData.java b/web/client-api/src/main/java/io/deephaven/web/client/api/subscription/SubscriptionTableData.java index 263d26f66e8..a679abef980 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/subscription/SubscriptionTableData.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/subscription/SubscriptionTableData.java @@ -12,10 +12,7 @@ import io.deephaven.web.shared.data.*; import io.deephaven.web.shared.data.columns.ColumnData; import jsinterop.annotations.JsFunction; -import jsinterop.annotations.JsIgnore; -import jsinterop.annotations.JsMethod; import jsinterop.annotations.JsProperty; -import jsinterop.annotations.JsType; import jsinterop.base.Any; import jsinterop.base.Js; import jsinterop.base.JsArrayLike; @@ -25,7 +22,11 @@ import java.math.BigInteger; import java.util.NavigableSet; import java.util.PrimitiveIterator; +import java.util.Spliterators; import java.util.TreeMap; +import java.util.stream.IntStream; +import java.util.stream.StreamSupport; + import static io.deephaven.web.client.api.subscription.ViewportData.NO_ROW_FORMAT_COLUMN; public class SubscriptionTableData { @@ -43,7 +44,7 @@ private interface ArrayCopy { private RangeSet index; // mappings from the index to the position of a row in the data array - private TreeMap redirectedIndexes; + private TreeMap redirectedIndexes; // rows in the data columns that no longer contain data and can be reused private RangeSet reusableDestinations; @@ -68,6 +69,7 @@ public TableData handleSnapshot(TableSnapshot snapshot) { long includedRowCount = snapshot.getIncludedRows().size(); RangeSet destination = freeRows(includedRowCount); + long[] destArray = array(destination); for (int index = 0; index < dataColumns.length; index++) { ColumnData dataColumn = dataColumns[index]; @@ -85,19 +87,51 @@ public TableData handleSnapshot(TableSnapshot snapshot) { data[index] = localCopy; PrimitiveIterator.OfLong destIter = destination.indexIterator(); PrimitiveIterator.OfLong indexIter = snapshot.getIncludedRows().indexIterator(); - int j = 0; - while (indexIter.hasNext()) { - assert destIter.hasNext(); - long dest = destIter.nextLong(); - redirectedIndexes.put(indexIter.nextLong(), dest); - arrayCopy.copyTo(localCopy, dest, dataColumn.getData(), j++); + long[] indexArray = array(snapshot.getIncludedRows()); + int[] positions = IntStream.range(0, indexArray.length).toArray(); + shuffle(destArray, indexArray, positions); + + for (int j = 0; j < indexArray.length; j++) { + long dest = destArray[j]; + long index2 = indexArray[j]; + redirectedIndexes.put(index2, (int) dest); + arrayCopy.copyTo(localCopy, dest, dataColumn.getData(), positions[j]); } - assert !destIter.hasNext(); + +// int j = 0; +// while (indexIter.hasNext()) { +// assert destIter.hasNext(); +// long dest = destIter.nextLong(); +// redirectedIndexes.put(indexIter.nextLong(), dest); +// arrayCopy.copyTo(localCopy, dest, dataColumn.getData(), j++); +// } +// assert !destIter.hasNext(); } return notifyUpdates(index, RangeSet.empty(), RangeSet.empty()); } + private long[] array(RangeSet rangeSet) { + return StreamSupport.longStream(Spliterators.spliterator(rangeSet.indexIterator(), Long.MAX_VALUE, 0), false).toArray(); + } + + public void shuffle(long[] destArray, long[] indexArray, int[] positions) { + for (int i = destArray.length - 1; i > 0; i--) { + int j = (int) (Math.random() * (i + 1)); + long x = destArray[i]; + destArray[i] = destArray[j]; + destArray[j] = x; + + x = indexArray[i]; + indexArray[i] = indexArray[j]; + indexArray[j] = x; + + int pos = positions[i]; + positions[i] = positions[j]; + positions[j] = pos; + } + } + /** * Helper to avoid appending many times when modifying indexes. The append() method should be called for each key * _in order_ to ensure that RangeSet.addRange isn't called excessively. When no more items will be added, flush() @@ -175,7 +209,7 @@ public TableData handleDelta(DeltaUpdates delta) { // iterate backward and move them forward for (Long key : toMove.descendingSet()) { long shiftedKey = key + offset; - Long oldValue = redirectedIndexes.put(shiftedKey, redirectedIndexes.remove(key)); + Integer oldValue = redirectedIndexes.put(shiftedKey, redirectedIndexes.remove(key)); assert oldValue == null : shiftedKey + " already has a value, " + oldValue; shifter.append(shiftedKey); } @@ -193,7 +227,7 @@ public TableData handleDelta(DeltaUpdates delta) { // iterate forward and move them backward for (Long key : toMove) { long shiftedKey = key + offset; - Long oldValue = redirectedIndexes.put(shiftedKey, redirectedIndexes.remove(key)); + Integer oldValue = redirectedIndexes.put(shiftedKey, redirectedIndexes.remove(key)); assert oldValue == null : shiftedKey + " already has a value, " + oldValue; shifter.append(shiftedKey); } @@ -219,8 +253,8 @@ public TableData handleDelta(DeltaUpdates delta) { long origIndex = addedIndexes.nextLong(); assert delta.getIncludedAdditions().contains(origIndex); assert destIter.hasNext(); - long dest = destIter.nextLong(); - Long old = redirectedIndexes.put(origIndex, dest); + int dest = (int) destIter.nextLong(); + Integer old = redirectedIndexes.put(origIndex, dest); assert old == null || old == dest; arrayCopy.copyTo(data[addedColumn.getColumnIndex()], dest, addedColumn.getValues().getData(), j++); } @@ -457,10 +491,12 @@ private RangeSet freeRows(long required) { @TsName(namespace = "dh") public class SubscriptionRow implements TableData.Row { private final long index; + private final long storageIndex; public LongWrapper indexCached; - public SubscriptionRow(long index) { + public SubscriptionRow(long index, int storageIndex) { this.index = index; + this.storageIndex = storageIndex; } @Override @@ -473,9 +509,8 @@ public LongWrapper getIndex() { @Override public Any get(Column column) { - int redirectedIndex = (int) (long) redirectedIndexes.get(this.index); JsArrayLike columnData = Js.asArrayLike(data[column.getIndex()]); - return columnData.getAtAsAny(redirectedIndex); + return columnData.getAtAsAny((int) storageIndex); } @Override @@ -545,8 +580,8 @@ public UpdateEventData(RangeSet added, RangeSet removed, RangeSet modified) { public JsArray getRows() { if (allRows == null) { allRows = new JsArray<>(); - index.indexIterator().forEachRemaining((long index) -> { - allRows.push(new SubscriptionRow(index)); + redirectedIndexes.forEach((index, storage) -> { + allRows.push(new SubscriptionRow(index, storage)); }); if (JsSettings.isDevMode()) { assert allRows.length == index.size(); @@ -568,7 +603,7 @@ public Row get(int index) { */ @Override public SubscriptionRow get(long index) { - return new SubscriptionRow(index); + return new SubscriptionRow(index, redirectedIndexes.get(index)); } @Override From ea5323630fdce0b3cfa101beee0497d8abc5da68 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Mon, 23 Oct 2023 15:39:54 -0500 Subject: [PATCH 05/31] spotless for ci --- .../subscription/SubscriptionTableData.java | 19 ++-- .../client/api/AbstractAsyncGwtTestCase.java | 101 +++++++++++------- .../deephaven/web/client/api/JsPredicate.java | 2 +- .../api/subscription/ViewportTestGwt.java | 92 +++++++++------- 4 files changed, 124 insertions(+), 90 deletions(-) diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/subscription/SubscriptionTableData.java b/web/client-api/src/main/java/io/deephaven/web/client/api/subscription/SubscriptionTableData.java index a679abef980..8a6ae7faaeb 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/subscription/SubscriptionTableData.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/subscription/SubscriptionTableData.java @@ -98,21 +98,22 @@ public TableData handleSnapshot(TableSnapshot snapshot) { arrayCopy.copyTo(localCopy, dest, dataColumn.getData(), positions[j]); } -// int j = 0; -// while (indexIter.hasNext()) { -// assert destIter.hasNext(); -// long dest = destIter.nextLong(); -// redirectedIndexes.put(indexIter.nextLong(), dest); -// arrayCopy.copyTo(localCopy, dest, dataColumn.getData(), j++); -// } -// assert !destIter.hasNext(); + // int j = 0; + // while (indexIter.hasNext()) { + // assert destIter.hasNext(); + // long dest = destIter.nextLong(); + // redirectedIndexes.put(indexIter.nextLong(), dest); + // arrayCopy.copyTo(localCopy, dest, dataColumn.getData(), j++); + // } + // assert !destIter.hasNext(); } return notifyUpdates(index, RangeSet.empty(), RangeSet.empty()); } private long[] array(RangeSet rangeSet) { - return StreamSupport.longStream(Spliterators.spliterator(rangeSet.indexIterator(), Long.MAX_VALUE, 0), false).toArray(); + return StreamSupport.longStream(Spliterators.spliterator(rangeSet.indexIterator(), Long.MAX_VALUE, 0), false) + .toArray(); } public void shuffle(long[] destArray, long[] indexArray, int[] positions) { diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java index 3d66911a0ef..650f2abbe67 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java @@ -36,6 +36,7 @@ public abstract class AbstractAsyncGwtTestCase extends GWTTestCase { @JsMethod(namespace = JsPackage.GLOBAL, name = "import") private static native Promise> importScript(String moduleName); + private static Promise importDhInternal() { return importScript("dhinternal.js") .then(module -> { @@ -43,6 +44,7 @@ private static Promise importDhInternal() { return Promise.resolve((Void) null); }); } + public static final String localServer = System.getProperty("dhTestServer", "http://localhost:10000"); public static class TableSourceBuilder { @@ -80,15 +82,15 @@ public JsString toJsString(String k) { return Js.cast(k); } - public JsArray toJsString(String ... k) { + public JsArray toJsString(String... k) { return Js.cast(k); } @JsProperty(name = "log", namespace = "console") private static native elemental2.core.Function getLog(); - static IThenable.ThenOnFulfilledCallbackFn logOnSuccess(Object ... rest) { - return value-> { + static IThenable.ThenOnFulfilledCallbackFn logOnSuccess(Object... rest) { + return value -> { // GWT will puke if we have varargs being sent to varargs; // so we go JS on it and just grab the function to apply getLog().apply(null, rest); @@ -96,15 +98,14 @@ static IThenable.ThenOnFulfilledCallbackFn logOnSuccess(Object ... res }; } - static Promise log(Object ... rest) { + static Promise log(Object... rest) { getLog().apply(null, rest); return Promise.resolve(rest); } JsRunnable assertEventNotCalled( HasEventHandling handling, - String... events - ) { + String... events) { RemoverFn[] undos = new RemoverFn[events.length]; for (int i = 0; i < events.length; i++) { final String ev = events[i]; @@ -113,7 +114,7 @@ JsRunnable assertEventNotCalled( report("Expected " + ev + " to not be called; detail: " + (e.detail)); }); } - return ()->{ + return () -> { for (RemoverFn undo : undos) { undo.remove(); } @@ -122,25 +123,24 @@ JsRunnable assertEventNotCalled( } static IThenable.ThenOnFulfilledCallbackFn run(JsRunnable allow) { - return t->{ + return t -> { allow.run(); return Promise.resolve(t); }; } static Promise expectFailure(Promise state, T value) { - return state.then(val-> Promise.reject("Failed"), - error->Promise.resolve(value) - ); + return state.then(val -> Promise.reject("Failed"), + error -> Promise.resolve(value)); } /** - * Connects and authenticates to the specified server, and resolves once the specified query config has - * become available. + * Connects and authenticates to the specified server, and resolves once the specified query config has become + * available. */ protected Promise connect(TableSource tables) { TableSourceImpl impl = (TableSourceImpl) tables; - //start by delaying test finish by .5s so we fail fast in cases where we aren't set up right + // start by delaying test finish by .5s so we fail fast in cases where we aren't set up right delayTestFinish(500); return importDhInternal().then(module -> { CoreClient coreClient = new CoreClient(localServer, null); @@ -164,7 +164,8 @@ protected Promise connect(TableSource tables) { }); } - private Promise runAllScriptsInOrder(CancellablePromise ideSession, IdeSession session, JsArray code) { + private Promise runAllScriptsInOrder(CancellablePromise ideSession, IdeSession session, + JsArray code) { Promise result = ideSession; for (int i = 0; i < code.length; i++) { final int index = i; @@ -186,15 +187,16 @@ public IThenable.ThenOnFulfilledCallbackFn table(String tab */ protected Promise report(Object error) { if (error instanceof String) { - reportUncaughtException(new RuntimeException((String)error)); + reportUncaughtException(new RuntimeException((String) error)); } else if (error instanceof Throwable) { reportUncaughtException((Throwable) error); - } if (error instanceof JsError) { + } + if (error instanceof JsError) { reportUncaughtException(new JavaScriptException(error)); } else { reportUncaughtException(new RuntimeException(error.toString())); } - //keep failing down the chain in case someone else cares + // keep failing down the chain in case someone else cares return Promise.reject(error); } @@ -217,12 +219,14 @@ protected Promise assertUpdateReceived(Promise tablePromise, i * Helper method to add a listener to a table, and ensure that an update is recieved with the expected number of * items, within the specified timeout. * - * Prereq: have already requested a viewport on that table. Remember to request that within the same event loop, - * so that there isn't a data race and the update gets missed. + * Prereq: have already requested a viewport on that table. Remember to request that within the same event loop, so + * that there isn't a data race and the update gets missed. */ protected Promise assertUpdateReceived(JsTable table, int count, int timeoutInMillis) { - return assertUpdateReceived(table, viewportData -> assertEquals(count, viewportData.getRows().length), timeoutInMillis); + return assertUpdateReceived(table, viewportData -> assertEquals(count, viewportData.getRows().length), + timeoutInMillis); } + protected Promise assertUpdateReceived(JsTable table, Consumer check, int timeoutInMillis) { return this.waitForEvent(table, JsTable.EVENT_UPDATED, e -> { ViewportData viewportData = e.detail; @@ -236,27 +240,36 @@ protected IThenable.ThenOnFulfilledCallbackFn delayFinish(int timeout) return Promise.resolve(table); }; } + protected IThenable.ThenOnFulfilledCallbackFn waitForTick(int timeout) { - return table -> waitForEvent(table, JsTable.EVENT_SIZECHANGED, ignored->{}, timeout); + return table -> waitForEvent(table, JsTable.EVENT_SIZECHANGED, ignored -> { + }, timeout); } + protected IThenable.ThenOnFulfilledCallbackFn waitForTickTwice(int timeout) { return table -> { - // wait for two ticks... one from setting the viewport, and then another for whenever the table actually ticks. + // wait for two ticks... one from setting the viewport, and then another for whenever the table actually + // ticks. // (if these happen out of order, they will still be very close) - return waitForEvent(table, JsTable.EVENT_SIZECHANGED, ignored->{}, timeout) - .then(t-> waitForEvent(table, JsTable.EVENT_SIZECHANGED, ignored->{}, timeout)); + return waitForEvent(table, JsTable.EVENT_SIZECHANGED, ignored -> { + }, timeout) + .then(t -> waitForEvent(table, JsTable.EVENT_SIZECHANGED, ignored -> { + }, timeout)); }; } - protected Promise waitForEventWhere(V evented, String eventName, Predicate> check, int timeout) { - //note that this roughly reimplements the 'kill timer' so this can be run in parallel with itself or other similar steps + + protected Promise waitForEventWhere(V evented, String eventName, + Predicate> check, int timeout) { + // note that this roughly reimplements the 'kill timer' so this can be run in parallel with itself or other + // similar steps return new Promise<>((resolve, reject) -> { boolean[] complete = {false}; console.log("adding " + eventName + " listener ", evented); - //apparent compiler bug, review in gwt 2.9 + // apparent compiler bug, review in gwt 2.9 RemoverFn unsub = Js.uncheckedCast(evented) .addEventListener(eventName, e -> { if (complete[0]) { - return;//already done, but timeout hasn't cleared us yet + return;// already done, but timeout hasn't cleared us yet } console.log("event ", e, " observed ", eventName, " for ", evented); try { @@ -274,13 +287,14 @@ protected Promise waitForEventWhere(V evented reject.onInvoke("Failed to complete in " + timeout + "ms " + evented); } complete[0] = true; - //complete already handled + // complete already handled }, timeout * TIMEOUT_SCALE); }); } - protected Promise waitForEvent(V evented, String eventName, Consumer> check, int timeout) { + protected Promise waitForEvent(V evented, String eventName, + Consumer> check, int timeout) { return this.waitForEventWhere(evented, eventName, e -> { check.accept(e); return true; @@ -288,24 +302,28 @@ protected Promise waitForEvent(V evented, Str } - protected static Promise promiseAllThen(T then, IThenable ... promises) { - return Promise.all(promises).then(items->Promise.resolve(then)); + protected static Promise promiseAllThen(T then, IThenable... promises) { + return Promise.all(promises).then(items -> Promise.resolve(then)); } protected IThenable waitFor(BooleanSupplier predicate, int checkInterval, int timeout, T result) { return new Promise<>((resolve, reject) -> { - schedule(predicate, checkInterval, () -> resolve.onInvoke(result), () -> reject.onInvoke("timeout of " + timeout + " exceeded"), timeout); + schedule(predicate, checkInterval, () -> resolve.onInvoke(result), + () -> reject.onInvoke("timeout of " + timeout + " exceeded"), timeout); }); } + protected IThenable.ThenOnFulfilledCallbackFn waitFor(int millis) { return result -> new Promise<>((resolve, reject) -> { DomGlobal.setTimeout(p -> resolve.onInvoke(result), millis); }); } - protected IThenable.ThenOnFulfilledCallbackFn waitForEvent(T table, String eventName, int millis) { + + protected IThenable.ThenOnFulfilledCallbackFn waitForEvent(T table, + String eventName, int millis) { return result -> new Promise<>((resolve, reject) -> { boolean[] success = {false}; - table.addEventListenerOneShot(eventName, e-> { + table.addEventListenerOneShot(eventName, e -> { success[0] = true; resolve.onInvoke(table); }); @@ -333,10 +351,12 @@ private void schedule(BooleanSupplier predicate, int checkInterval, Runnable com }, checkInterval); } - protected Promise assertNextViewportIs(JsTable table, Function column, String[] expected) { + protected Promise assertNextViewportIs(JsTable table, Function column, + String[] expected) { return assertUpdateReceived(table, viewportData -> { String[] actual = Js.uncheckedCast(getColumnData(viewportData, column.apply(table))); - assertTrue("Expected " + Arrays.toString(expected) + ", found " + Arrays.toString(actual) + " in table " + table + " at state " + table.state(), Arrays.equals(expected, actual)); + assertTrue("Expected " + Arrays.toString(expected) + ", found " + Arrays.toString(actual) + " in table " + + table + " at state " + table.state(), Arrays.equals(expected, actual)); }, 2000); } @@ -347,13 +367,14 @@ protected Object getColumnData(ViewportData viewportData, Column a) { protected Promise assertNextViewportIs(JsTable table, double... expected) { return assertUpdateReceived(table, viewportData -> { double[] actual = Js.uncheckedCast(getColumnData(viewportData, table.findColumn("I"))); - assertTrue("Expected " + Arrays.toString(expected) + ", found " + Arrays.toString(actual) + " in table " + table, Arrays.equals(expected, actual)); + assertTrue("Expected " + Arrays.toString(expected) + ", found " + Arrays.toString(actual) + " in table " + + table, Arrays.equals(expected, actual)); }, 2000); } public static List filterColumns(JsTable table, JsPredicate filter) { List matches = new ArrayList<>(); - table.getColumns().forEach((c, i, arr)->{ + table.getColumns().forEach((c, i, arr) -> { if (filter.test(c)) { matches.add(c); } diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/JsPredicate.java b/web/client-api/src/test/java/io/deephaven/web/client/api/JsPredicate.java index 9412244301f..927bec52f09 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/JsPredicate.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/JsPredicate.java @@ -8,4 +8,4 @@ public interface JsPredicate { @SuppressWarnings("unusable-by-js") boolean test(I input); -} \ No newline at end of file +} diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java index 18cc0b23892..51b3943bdb6 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java @@ -21,9 +21,9 @@ /** * Assumes two tables, ticking every 2 seconds: * - * growingForward = db.timeTable("00:00:01").update("I=i", "J=i*i", "K=0") - * growingBackward = growingForward.sortDescending("Timestamp") - * blinkOne = db.timeTable("00:00:01").update("I=i", "J=1").lastBy("J").where("I%2 != 0") + * growingForward = db.timeTable("00:00:01").update("I=i", "J=i*i", "K=0") growingBackward = + * growingForward.sortDescending("Timestamp") blinkOne = db.timeTable("00:00:01").update("I=i", + * "J=1").lastBy("J").where("I%2 != 0") * * And another static one: * @@ -33,8 +33,10 @@ public class ViewportTestGwt extends AbstractAsyncGwtTestCase { private final TableSource tables = new TableSourceBuilder() .script("staticTable", "deephaven.empty_table(100).update(\"I=i\")", "emptyTable(100).update(\"I=i\")") - .script("growingForward", "timeTable(\"00:00:01\").update(\"I=i\", \"J=i*i\", \"K=0\")", "timeTable(\"00:00:01\").update(\"I=i\", \"J=i*i\", \"K=0\")") - .script("growingBackward", "growingForward.sortDescending(\"Timestamp\")", "growingForward.sortDescending(\"Timestamp\")") + .script("growingForward", "timeTable(\"00:00:01\").update(\"I=i\", \"J=i*i\", \"K=0\")", + "timeTable(\"00:00:01\").update(\"I=i\", \"J=i*i\", \"K=0\")") + .script("growingBackward", "growingForward.sortDescending(\"Timestamp\")", + "growingForward.sortDescending(\"Timestamp\")") .script("blinkOne", "deephaven.empty_table(100).update(\"I=i\")", "emptyTable(100).update(\"I=i\")") .build(); @@ -44,13 +46,13 @@ public void testViewportOnStaticTable() { .then(table -> { delayTestFinish(5000); - int size = (int)table.getSize(); + int size = (int) table.getSize(); int lastRow = size - 1; table.setViewport(0, lastRow, null); return assertUpdateReceived(table, size, 500); }) .then(table -> { - //table has 100 rows, go through each page of 25, make sure the offset and length is sane + // table has 100 rows, go through each page of 25, make sure the offset and length is sane table.setViewport(0, 24, null); return assertUpdateReceived(table, viewport -> { assertEquals(0, (long) viewport.getOffset()); @@ -87,7 +89,7 @@ public void ignore_testViewportOnGrowingTable() { .then(table("growingForward")) .then(waitForTick(2200)) .then(delayFinish(25_000)) - .then(table ->{ + .then(table -> { // set viewport to actual table size, check that all items are present int size = (int) table.getSize(); int lastRow = size - 1; @@ -122,7 +124,7 @@ public void ignore_testViewportOnGrowingTable() { .then(JsTable::getViewportData) .then(viewportData -> { assertEquals(2, viewportData.getRows().length); - //switch back to table for next promise + // switch back to table for next promise return Promise.resolve(table); }); }) @@ -136,10 +138,12 @@ public void testViewportOnUpdatingTable() { delayTestFinish(4000); // set up a viewport, and watch it show up, and tick once table.setViewport(0, 9, null); - return assertUpdateReceived(table, viewportData -> {}, 1000); + return assertUpdateReceived(table, viewportData -> { + }, 1000); }) .then(table -> { - return assertUpdateReceived(table, viewportData -> {}, 2000); + return assertUpdateReceived(table, viewportData -> { + }, 2000); }) .then(this::finish).catch_(this::report); } @@ -147,6 +151,7 @@ public void testViewportOnUpdatingTable() { private static int indexOf(JsArray array, T object) { return indexOf(array.asList().toArray(), object); } + private static int indexOf(Object[] array, T object) { for (int i = 0; i < array.length; i++) { Object t = array[i]; @@ -177,7 +182,7 @@ public void testViewportSubsetOfColumns() { }, 500); }) .then(table -> { - //don't change viewport, test the same thing again, make sure deltas behave too + // don't change viewport, test the same thing again, make sure deltas behave too return assertUpdateReceived(table, viewport -> { assertEquals(1, viewport.getColumns().length); assertEquals(0, indexOf(viewport.getColumns(), table.findColumn("I"))); @@ -281,7 +286,7 @@ public void testViewportSubsetOfColumns() { // TODO: https://deephaven.atlassian.net/browse/DH-11196 public void ignore_testEmptyTableWithViewport() { - //confirm that when the viewport is set on an empty table that we get exactly one update event + // confirm that when the viewport is set on an empty table that we get exactly one update event connect(tables) .then(table("staticTable")) .then(table -> { @@ -294,8 +299,9 @@ public void ignore_testEmptyTableWithViewport() { table.setViewport(0, 100, null); return Promise.all(new IThenable[] { // when IDS-2113 is fixed, restore this stronger assertion -// assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000) - waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> {}, 1000), + // assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000) + waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> { + }, 1000), assertEventFiresOnce(table, JsTable.EVENT_SIZECHANGED, 1000) }).then(ignore -> Promise.resolve(table)); }) @@ -303,36 +309,39 @@ public void ignore_testEmptyTableWithViewport() { // reset the filter, wait for back to normal table.applyFilter(new FilterCondition[0]); table.setViewport(0, 100, null); - return assertUpdateReceived(table, ignore -> {}, 1000); + return assertUpdateReceived(table, ignore -> { + }, 1000); }) .then(table -> { - //change the filter, don't set a viewport, assert only size changes + // change the filter, don't set a viewport, assert only size changes table.applyFilter(new FilterCondition[] { FilterValue.ofBoolean(false).isTrue() }); return assertEventFiresOnce(table, JsTable.EVENT_SIZECHANGED, 1000); }) .then(table -> { - //set a viewport, assert that update fires and no size change + // set a viewport, assert that update fires and no size change table.setViewport(0, 100, null); // when IDS-2113 is fixed, restore this stronger assertion -// return assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000); - return waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> {}, 1000); + // return assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000); + return waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> { + }, 1000); }) .then(this::finish).catch_(this::report); } public void testViewportOutOfRangeOfTable() { - //confirm that when the viewport is set beyond the range of the table that we get exactly one update event + // confirm that when the viewport is set beyond the range of the table that we get exactly one update event connect(tables) .then(table("staticTable")) .then(table -> { table.setViewport(100, 104, null); - return Promise.all(new IThenable[]{ + return Promise.all(new IThenable[] { // when IDS-2113 is fixed, restore this stronger assertion -// assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000) - waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> {}, 1000) + // assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000) + waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> { + }, 1000) }).then(ignore -> Promise.resolve(table)); }) .then(this::finish).catch_(this::report); @@ -344,7 +353,7 @@ public void testRapidChangingViewport() { .then(table("staticTable")) .then(table -> { delayTestFinish(5000); - //test running both synchronously + // test running both synchronously table.setViewport(0, 10, null); table.setViewport(5, 14, null); return assertUpdateReceived(table, viewport -> { @@ -353,9 +362,9 @@ public void testRapidChangingViewport() { }, 1000); }) .then(table -> { - //test changing the viewport over a microtask (anyone in the web api getting clever with batching?) + // test changing the viewport over a microtask (anyone in the web api getting clever with batching?) table.setViewport(0, 10, null); - return Promise.resolve((Object)null).then(ignore -> Promise.resolve(table)); + return Promise.resolve((Object) null).then(ignore -> Promise.resolve(table)); }) .then(table -> { table.setViewport(6, 14, null); @@ -368,17 +377,18 @@ public void testRapidChangingViewport() { table.setViewport(0, 10, null); return Promise.resolve(table); }) - //test again over a 4ms delay, minimum task delay + // test again over a 4ms delay, minimum task delay .then(waitFor(4)) .then(table -> { table.setViewport(7, 17, null); - return assertUpdateReceived(table, ignored->{}, 1000) + return assertUpdateReceived(table, ignored -> { + }, 1000) .then(waitFor(JsTable.DEBOUNCE_TIME * 2)) .then(t -> { // force the debounce to be processed t.processSnapshot(); - t.getViewportData().then(vp->{ -// assertEquals(7, (int) vp.getOffset()); + t.getViewportData().then(vp -> { + // assertEquals(7, (int) vp.getOffset()); assertEquals(11, (int) vp.getRows().length); return Promise.resolve(vp); }); @@ -397,7 +407,7 @@ public void testViewportWithNoInitialItems() { .then(table -> { delayTestFinish(20_000); - //first run, assume all columns + // first run, assume all columns return helperForViewportWithNoInitialItems(table, null, table.getColumns()); }).then(table -> { // second, specify only one column to ensure that it is respected @@ -407,7 +417,8 @@ public void testViewportWithNoInitialItems() { .then(this::finish).catch_(this::report); } - private IThenable helperForViewportWithNoInitialItems(JsTable t, Column[] requestColumns, JsArray expectedColumns) { + private IThenable helperForViewportWithNoInitialItems(JsTable t, Column[] requestColumns, + JsArray expectedColumns) { // wait until zero rows are present, so we can set the viewport and get a zero-row "snapshot" return waitFor(() -> t.getSize() == 0, 100, 2000, t) .then(table -> { @@ -426,7 +437,7 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column return waitForEventWhere(table, "updated", (CustomEvent e) -> { ViewportData viewport = (ViewportData) e.detail; if (viewport.getRows().length != 1) { - return false; //wrong data, wait for another event + return false; // wrong data, wait for another event } assertEquals(expectedColumns.length, viewport.getColumns().length); for (int i = 0; i < viewport.getColumns().length; i++) { @@ -441,7 +452,7 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column return waitForEventWhere(table, "updated", (CustomEvent e) -> { ViewportData emptyViewport = (ViewportData) e.detail; if (emptyViewport.getRows().length != 0) { - return false; //wrong data, wait for another event + return false; // wrong data, wait for another event } assertEquals(expectedColumns.length, emptyViewport.getColumns().length); return true; @@ -452,7 +463,7 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column return waitForEventWhere(table, "updated", (CustomEvent e) -> { ViewportData viewport = (ViewportData) e.detail; if (viewport.getRows().length != 1) { - return false; //wrong data, wait for another event + return false; // wrong data, wait for another event } assertEquals(expectedColumns.length, viewport.getColumns().length); for (int i = 0; i < viewport.getColumns().length; i++) { @@ -464,11 +475,12 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column }); } - private Promise assertEventFiresOnce(T eventSource, String eventName, int intervalInMilliseconds) { + private Promise assertEventFiresOnce(T eventSource, String eventName, + int intervalInMilliseconds) { return new Promise<>((resolve, reject) -> { int[] runCount = {0}; console.log("adding " + eventName + " listener " + eventSource); - //apparent compiler bug, review in gwt 2.9 + // apparent compiler bug, review in gwt 2.9 RemoverFn unsub = Js.uncheckedCast(eventSource) .addEventListener(eventName, e -> { runCount[0]++; @@ -488,12 +500,12 @@ private Promise assertEventFiresOnce(T eventSour }); } - private void assertThrowsException(Runnable r) { + private void assertThrowsException(Runnable r) { try { r.run(); fail("Expected exception"); } catch (Exception ignore) { - //expected + // expected } } From e8ddd50612aba66d486af9fd906b3b2b9f4293ea Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Thu, 2 Nov 2023 12:18:16 -0500 Subject: [PATCH 06/31] fix regression in formatting longs --- .../java/io/deephaven/web/client/api/i18n/JsNumberFormat.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/i18n/JsNumberFormat.java b/web/client-api/src/main/java/io/deephaven/web/client/api/i18n/JsNumberFormat.java index 6bc69bc746d..1343053263d 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/i18n/JsNumberFormat.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/i18n/JsNumberFormat.java @@ -162,7 +162,7 @@ public String format(NumberUnion number) { } else if (number.isBigInteger()) { return wrapped.format(number.asBigInteger().getWrapped()); } else if (number.isLongWrapper()) { - return wrapped.format(number.asLongWrapper().getWrapped()); + return wrapped.format((Long) number.asLongWrapper().getWrapped()); } throw new IllegalStateException("Can't format non-number object of type " + Js.typeof(number)); } From 5f8cb54e794b97cfbd1f09a5bb124832bf9a6fd8 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Thu, 2 Nov 2023 15:04:25 -0500 Subject: [PATCH 07/31] integration tests run, fail --- web/client-api/client-api.gradle | 3 +-- .../java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml | 1 + .../deephaven/web/client/api/AbstractAsyncGwtTestCase.java | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index 700b715ef31..200826ddb6e 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -69,7 +69,7 @@ deephavenDocker { tasks.register('gwtIntegrationTest', Test) { t -> t.dependsOn(deephavenDocker.portTask) doFirst { - t.systemProperty('dh.server', 'http://localhost:' + deephavenDocker.port.get()) + t.systemProperty('gwt.args', '-runStyle Manual:1 -ea -style PRETTY -setProperty dh.server=http://localhost:' + deephavenDocker.port.get()) t.classpath += tasks.getByName('gwtCompile').src t.classpath.files.each { println it @@ -77,7 +77,6 @@ tasks.register('gwtIntegrationTest', Test) { t -> } t.finalizedBy(deephavenDocker.endTask) t.systemProperties = [ - 'gwt.args':'-runStyle HtmlUnit -ea -style PRETTY', 'gwt.persistentunitcachedir':layout.buildDirectory.dir('integrationTest-unitCache').get().asFile.absolutePath, // 'dh.server':'http://localhost:' + deephavenDocker.port.get() ] diff --git a/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml b/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml index 63f659f1685..d9368823b4f 100644 --- a/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml +++ b/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml @@ -1,3 +1,4 @@ + \ No newline at end of file diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java index 650f2abbe67..a99ec824bcd 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java @@ -34,18 +34,18 @@ import static elemental2.dom.DomGlobal.console; public abstract class AbstractAsyncGwtTestCase extends GWTTestCase { - @JsMethod(namespace = JsPackage.GLOBAL, name = "import") + @JsMethod(namespace = "", name = "import") private static native Promise> importScript(String moduleName); private static Promise importDhInternal() { - return importScript("dhinternal.js") + return importScript( localServer + "/jsapi/dh-internal.js") .then(module -> { Js.asPropertyMap(DomGlobal.window).set("dhinternal", module.get("dhinternal")); return Promise.resolve((Void) null); }); } - public static final String localServer = System.getProperty("dhTestServer", "http://localhost:10000"); + public static final String localServer = System.getProperty("dh.server", "http://localhost:10000"); public static class TableSourceBuilder { public TableSourceBuilder script(String tableName, String python, String groovy) { From a85598535107d3b6971ec88782f131f2461aed32 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Fri, 3 Nov 2023 09:44:20 -0500 Subject: [PATCH 08/31] Tests are passing in manual mode, except filter setup --- web/client-api/client-api.gradle | 10 +-- .../io/deephaven/web/DeephavenApi.gwt.xml | 4 +- .../web/DeephavenIntegrationTest.gwt.xml | 5 ++ .../deephaven/web/DeephavenUnitTest.gwt.xml | 3 + .../client/api/AbstractAsyncGwtTestCase.java | 62 +++++++++---------- .../api/subscription/ViewportTestGwt.java | 17 +++-- 6 files changed, 49 insertions(+), 52 deletions(-) diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index 200826ddb6e..d902c655467 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -44,9 +44,6 @@ artifacts { } tasks.register('gwtUnitTest', Test) { t -> -// gradle.projectsEvaluated { -// t.classpath += project(":web-client-api").tasks.getByName('gwtCompile').src -// } t.systemProperties = [ 'gwt.args':'-runStyle HtmlUnit -ea -style PRETTY', 'gwt.persistentunitcachedir':layout.buildDirectory.dir('integrationTest-unitCache').get().asFile.absolutePath, @@ -60,10 +57,10 @@ tasks.register('gwtUnitTest', Test) { t -> String randomSuffix = UUID.randomUUID().toString(); deephavenDocker { envVars.set([ - 'START_OPTS':'-Xmx512m' + 'START_OPTS':'-Xmx512m -DAuthHandlers=io.deephaven.auth.AnonymousAuthenticationHandler' ]) - containerName.set "dh-server-for-cpp-${randomSuffix}" - networkName.set "cpp-test-network-${randomSuffix}" + containerName.set "dh-server-for-js-${randomSuffix}" + networkName.set "js-test-network-${randomSuffix}" } tasks.register('gwtIntegrationTest', Test) { t -> @@ -78,7 +75,6 @@ tasks.register('gwtIntegrationTest', Test) { t -> t.finalizedBy(deephavenDocker.endTask) t.systemProperties = [ 'gwt.persistentunitcachedir':layout.buildDirectory.dir('integrationTest-unitCache').get().asFile.absolutePath, -// 'dh.server':'http://localhost:' + deephavenDocker.port.get() ] t.include '**/ClientIntegrationTestSuite.class' t.useJUnit() diff --git a/web/client-api/src/main/java/io/deephaven/web/DeephavenApi.gwt.xml b/web/client-api/src/main/java/io/deephaven/web/DeephavenApi.gwt.xml index ba15260401a..4f6177621b8 100644 --- a/web/client-api/src/main/java/io/deephaven/web/DeephavenApi.gwt.xml +++ b/web/client-api/src/main/java/io/deephaven/web/DeephavenApi.gwt.xml @@ -13,8 +13,8 @@ - - + + diff --git a/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml b/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml index d9368823b4f..3d6f63cee15 100644 --- a/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml +++ b/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml @@ -1,4 +1,9 @@ + + + + + \ No newline at end of file diff --git a/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml b/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml index 63f659f1685..617e28a1095 100644 --- a/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml +++ b/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml @@ -1,3 +1,6 @@ + + + \ No newline at end of file diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java index a99ec824bcd..315950a9617 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java @@ -34,8 +34,11 @@ import static elemental2.dom.DomGlobal.console; public abstract class AbstractAsyncGwtTestCase extends GWTTestCase { - @JsMethod(namespace = "", name = "import") - private static native Promise> importScript(String moduleName); + @JsMethod(namespace = JsPackage.GLOBAL) + private static native Object eval(String code); + private static Promise> importScript(String moduleName) { + return (Promise>) eval("import('" + moduleName + "')"); + } private static Promise importDhInternal() { return importScript( localServer + "/jsapi/dh-internal.js") @@ -48,30 +51,19 @@ private static Promise importDhInternal() { public static final String localServer = System.getProperty("dh.server", "http://localhost:10000"); public static class TableSourceBuilder { - public TableSourceBuilder script(String tableName, String python, String groovy) { - - + private final List pythonScripts = new ArrayList<>(); + public TableSourceBuilder script(String script) { + pythonScripts.add(script); return this; } - public TableSource build() { - return new TableSourceImpl(); - } - } - public interface TableSource { - Promise table(String name); - } - private static class TableSourceImpl implements TableSource { - private JsArray groovyScripts; - private JsArray pythonScripts; + public TableSourceBuilder script(String tableName, String python) { + pythonScripts.add(tableName + "=" + python); - @Override - public Promise table(String name) { - return null; + return this; } } - /** * Set this to a value higher than 1 to get more time to run debugger without timeouts failing. */ @@ -135,11 +127,9 @@ static Promise expectFailure(Promise state, T value) { } /** - * Connects and authenticates to the specified server, and resolves once the specified query config has become - * available. + * Connects and authenticates to the configured server and runs the specified scripts. */ - protected Promise connect(TableSource tables) { - TableSourceImpl impl = (TableSourceImpl) tables; + protected Promise connect(TableSourceBuilder tables) { // start by delaying test finish by .5s so we fail fast in cases where we aren't set up right delayTestFinish(500); return importDhInternal().then(module -> { @@ -147,17 +137,16 @@ protected Promise connect(TableSource tables) { return coreClient.login(JsPropertyMap.of("type", CoreClient.LOGIN_TYPE_ANONYMOUS)) .then(ignore -> coreClient.getAsIdeConnection()) .then(ide -> { - delayTestFinish(500); + delayTestFinish(501); return ide.getConsoleTypes().then(consoleTypes -> { + delayTestFinish(502); CancellablePromise ideSession = ide.startSession(consoleTypes.getAt(0)); return ideSession.then(session -> { - if (consoleTypes.includes("groovy")) { - // scripts must run in order, to be sure let block on each one - return runAllScriptsInOrder(ideSession, session, impl.groovyScripts); - } else if (consoleTypes.includes("python")) { - return runAllScriptsInOrder(ideSession, session, impl.pythonScripts); + + if (consoleTypes.includes("python")) { + return runAllScriptsInOrder(ideSession, session, tables.pythonScripts); } - throw new IllegalStateException("Unknown script type " + consoleTypes); + throw new IllegalStateException("Unsupported script type " + consoleTypes); }); }); }); @@ -165,13 +154,18 @@ protected Promise connect(TableSource tables) { } private Promise runAllScriptsInOrder(CancellablePromise ideSession, IdeSession session, - JsArray code) { + List code) { Promise result = ideSession; - for (int i = 0; i < code.length; i++) { + for (int i = 0; i < code.size(); i++) { final int index = i; result = result.then(ignore -> { - delayFinish(2000); - session.runCode(code.getAt(index)); + delayTestFinish(2000 + index); + + return session.runCode(code.get(index)); + }).then(r -> { + if (r.getError() != null) { + return Promise.reject(r.getError()); + } return ideSession; }); } diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java index 51b3943bdb6..dc387be99c4 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java @@ -31,14 +31,13 @@ */ public class ViewportTestGwt extends AbstractAsyncGwtTestCase { - private final TableSource tables = new TableSourceBuilder() - .script("staticTable", "deephaven.empty_table(100).update(\"I=i\")", "emptyTable(100).update(\"I=i\")") - .script("growingForward", "timeTable(\"00:00:01\").update(\"I=i\", \"J=i*i\", \"K=0\")", - "timeTable(\"00:00:01\").update(\"I=i\", \"J=i*i\", \"K=0\")") - .script("growingBackward", "growingForward.sortDescending(\"Timestamp\")", - "growingForward.sortDescending(\"Timestamp\")") - .script("blinkOne", "deephaven.empty_table(100).update(\"I=i\")", "emptyTable(100).update(\"I=i\")") - .build(); + private final TableSourceBuilder tables = new TableSourceBuilder() + .script("from deephaven import empty_table, time_table") + .script("staticTable", "empty_table(100).update(\"I=i\")") + .script("from datetime import datetime, timedelta") + .script("growingForward", "time_table(period=\"PT00:00:01\", start_time=datetime.now() - timedelta(minutes=1)).update([\"I=i\", \"J=i*i\", \"K=0\"])") + .script("growingBackward", "growingForward.sort_descending(\"Timestamp\")") + .script("blinkOne", "time_table(\"PT00:00:01\").update([\"I=i\", \"J=1\"]).last_by(by=\"J\").where(\"I%2 != 0\")"); public void testViewportOnStaticTable() { connect(tables) @@ -435,7 +434,7 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column // wait for the next tick, where we get the "first" row added, confirm that the viewport // data is sane return waitForEventWhere(table, "updated", (CustomEvent e) -> { - ViewportData viewport = (ViewportData) e.detail; + ViewportData viewport = e.detail; if (viewport.getRows().length != 1) { return false; // wrong data, wait for another event } From 06a17a540af211b637ae172e5fe26ce69ca25d4d Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Fri, 3 Nov 2023 11:19:00 -0500 Subject: [PATCH 09/31] Tidy up tests, disable filter tests for now --- .../web/ClientIntegrationTestSuite.java | 5 +- .../client/api/AbstractAsyncGwtTestCase.java | 14 +- .../api/filter/FilterConditionTestGwt.java | 180 ++++++++++-------- .../api/subscription/ViewportTestGwt.java | 6 +- 4 files changed, 119 insertions(+), 86 deletions(-) diff --git a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java index 03b94f7f9ad..ff1523c0763 100644 --- a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java +++ b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java @@ -10,8 +10,9 @@ public class ClientIntegrationTestSuite extends GWTTestSuite { public static Test suite() { TestSuite suite = new TestSuite("Deephaven JS API Unit Test Suite"); - // This test doesn't actually talk to the server, but it requires the dh-internal library be available - suite.addTestSuite(FilterConditionTestGwt.class); + // This test doesn't actually talk to the server, but it requires the dh-internal library be available. + // Disabled for now, we don't have good toString on the FilterCondition/FilterValue types. +// suite.addTestSuite(FilterConditionTestGwt.class); // Actual integration tests suite.addTestSuite(ViewportTestGwt.class); diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java index 315950a9617..34b3cc552e1 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java @@ -36,12 +36,13 @@ public abstract class AbstractAsyncGwtTestCase extends GWTTestCase { @JsMethod(namespace = JsPackage.GLOBAL) private static native Object eval(String code); + private static Promise> importScript(String moduleName) { return (Promise>) eval("import('" + moduleName + "')"); } private static Promise importDhInternal() { - return importScript( localServer + "/jsapi/dh-internal.js") + return importScript(localServer + "/jsapi/dh-internal.js") .then(module -> { Js.asPropertyMap(DomGlobal.window).set("dhinternal", module.get("dhinternal")); return Promise.resolve((Void) null); @@ -52,6 +53,7 @@ private static Promise importDhInternal() { public static class TableSourceBuilder { private final List pythonScripts = new ArrayList<>(); + public TableSourceBuilder script(String script) { pythonScripts.add(script); return this; @@ -67,7 +69,7 @@ public TableSourceBuilder script(String tableName, String python) { /** * Set this to a value higher than 1 to get more time to run debugger without timeouts failing. */ - protected static final int TIMEOUT_SCALE = 1; + protected static final int TIMEOUT_SCALE = 2; public static final double DELTA = 0.0001; public JsString toJsString(String k) { @@ -126,6 +128,14 @@ static Promise expectFailure(Promise state, T value) { error -> Promise.resolve(value)); } + /** + * Imports the webpack content, including protobuf types. Does not connect to the server. + */ + protected Promise setupDhInternal() { + delayTestFinish(500); + return importDhInternal(); + } + /** * Connects and authenticates to the configured server and runs the specified scripts. */ diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java index f259668208d..d518ae08680 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java @@ -4,13 +4,14 @@ package io.deephaven.web.client.api.filter; import com.google.gwt.junit.client.GWTTestCase; +import io.deephaven.web.client.api.AbstractAsyncGwtTestCase; import io.deephaven.web.client.api.Column; /** * Tests basic construction of filter condition instances from simple tables. This does not fully end-to-end test the * filter, just the API around the simple AST we use, especially validation. */ -public class FilterConditionTestGwt extends GWTTestCase { +public class FilterConditionTestGwt extends AbstractAsyncGwtTestCase { @Override public String getModuleName() { @@ -26,88 +27,107 @@ private FilterValue[] arr(FilterValue filterValue) { } public void testCreateSimpleFilters() { - Column c = getColumn(); - - assertEquals("ColumnName == (ignore case) 1", - c.filter().eqIgnoreCase(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); - assertEquals("ColumnName != (ignore case) 1", - c.filter().notEqIgnoreCase(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); - - assertEquals("ColumnName == 1", - c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); - assertEquals("ColumnName != 1", - c.filter().notEq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); - assertEquals("ColumnName > 1", - c.filter().greaterThan(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); - assertEquals("ColumnName < 1", - c.filter().lessThan(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); - assertEquals("ColumnName >= 1", - c.filter().greaterThanOrEqualTo(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); - assertEquals("ColumnName <= 1", - c.filter().lessThanOrEqualTo(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); - - assertEquals("ColumnName in 1", - c.filter().in(arr(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1)))).toString()); - assertEquals("ColumnName not in 1", - c.filter().notIn(arr(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1)))).toString()); - assertEquals("ColumnName icase in 1", - c.filter().inIgnoreCase(arr(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1)))).toString()); - assertEquals("ColumnName icase not in 1", - c.filter().notInIgnoreCase(arr(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1)))).toString()); - - assertEquals("ColumnName == true", c.filter().isTrue().toString()); - assertEquals("ColumnName == false", c.filter().isFalse().toString()); - assertEquals("isNull(ColumnName)", c.filter().isNull().toString()); - - assertEquals("ColumnName.foo1()", c.filter().invoke("foo1").toString()); - assertEquals("ColumnName.foo2(1)", - c.filter().invoke("foo2", FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); - assertEquals("ColumnName.foo3(1, 2, \"three\")", - c.filter() - .invoke("foo3", FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1)), - FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(2)), - FilterValue.ofString("three")) - .toString()); - - assertEquals("foo4()", FilterCondition.invoke("foo4").toString()); - assertEquals("foo5(1)", - FilterCondition.invoke("foo5", FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); - assertEquals("foo6(1, 2, \"three\")", - FilterCondition - .invoke("foo6", FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1)), - FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(2)), - FilterValue.ofString("three")) - .toString()); + setupDhInternal().then(ignored -> { + + Column c = getColumn(); + + assertEquals("ColumnName == (ignore case) 1", + c.filter().eqIgnoreCase(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); + assertEquals("ColumnName != (ignore case) 1", + c.filter().notEqIgnoreCase(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); + + assertEquals("ColumnName == 1", + c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); + assertEquals("ColumnName != 1", + c.filter().notEq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); + assertEquals("ColumnName > 1", + c.filter().greaterThan(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); + assertEquals("ColumnName < 1", + c.filter().lessThan(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); + assertEquals("ColumnName >= 1", + c.filter().greaterThanOrEqualTo(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))) + .toString()); + assertEquals("ColumnName <= 1", + c.filter().lessThanOrEqualTo(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))) + .toString()); + + assertEquals("ColumnName in 1", + c.filter().in(arr(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1)))).toString()); + assertEquals("ColumnName not in 1", + c.filter().notIn(arr(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1)))).toString()); + assertEquals("ColumnName icase in 1", + c.filter().inIgnoreCase(arr(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1)))) + .toString()); + assertEquals("ColumnName icase not in 1", + c.filter().notInIgnoreCase(arr(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1)))) + .toString()); + + assertEquals("ColumnName == true", c.filter().isTrue().toString()); + assertEquals("ColumnName == false", c.filter().isFalse().toString()); + assertEquals("isNull(ColumnName)", c.filter().isNull().toString()); + + assertEquals("ColumnName.foo1()", c.filter().invoke("foo1").toString()); + assertEquals("ColumnName.foo2(1)", + c.filter().invoke("foo2", FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).toString()); + assertEquals("ColumnName.foo3(1, 2, \"three\")", + c.filter() + .invoke("foo3", FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1)), + FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(2)), + FilterValue.ofString("three")) + .toString()); + + assertEquals("foo4()", FilterCondition.invoke("foo4").toString()); + assertEquals("foo5(1)", + FilterCondition.invoke("foo5", FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))) + .toString()); + assertEquals("foo6(1, 2, \"three\")", + FilterCondition + .invoke("foo6", FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1)), + FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(2)), + FilterValue.ofString("three")) + .toString()); + finishTest(); + return null; + }) + .then(this::finish).catch_(this::report); } public void testCreateCombinedFilters() { - Column c = getColumn(); - - // individual AND - assertEquals("(ColumnName == 1 && ColumnName != 2)", - c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))) - .and(c.filter().notEq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(2)))).toString()); - - // individual OR - assertEquals("(ColumnName == 1 || ColumnName != 2)", - c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))) - .or(c.filter().notEq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(2)))).toString()); - - // individual NOT - assertEquals("!(ColumnName == 1)", - c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).not().toString()); - - // nested/combined - assertEquals("(ColumnName == 1 && !((ColumnName == 2 || ColumnName == 3 || ColumnName == 4)))", - c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).and( - c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(2))) - .or( - c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(3))), - c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(4)))) - .not()) - .toString() - - ); + setupDhInternal().then(ignored -> { + + Column c = getColumn(); + + // individual AND + assertEquals("(ColumnName == 1 && ColumnName != 2)", + c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))) + .and(c.filter().notEq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(2)))) + .toString()); + + // individual OR + assertEquals("(ColumnName == 1 || ColumnName != 2)", + c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))) + .or(c.filter().notEq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(2)))) + .toString()); + + // individual NOT + assertEquals("!(ColumnName == 1)", + c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).not().toString()); + + // nested/combined + assertEquals("(ColumnName == 1 && !((ColumnName == 2 || ColumnName == 3 || ColumnName == 4)))", + c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(1))).and( + c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(2))) + .or( + c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(3))), + c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(4)))) + .not()) + .toString() + ); + finishTest(); + return null; + }) + .then(this::finish).catch_(this::report); + } } diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java index dc387be99c4..9148fdd21ff 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java @@ -35,9 +35,11 @@ public class ViewportTestGwt extends AbstractAsyncGwtTestCase { .script("from deephaven import empty_table, time_table") .script("staticTable", "empty_table(100).update(\"I=i\")") .script("from datetime import datetime, timedelta") - .script("growingForward", "time_table(period=\"PT00:00:01\", start_time=datetime.now() - timedelta(minutes=1)).update([\"I=i\", \"J=i*i\", \"K=0\"])") + .script("growingForward", + "time_table(period=\"PT00:00:01\", start_time=datetime.now() - timedelta(minutes=1)).update([\"I=i\", \"J=i*i\", \"K=0\"])") .script("growingBackward", "growingForward.sort_descending(\"Timestamp\")") - .script("blinkOne", "time_table(\"PT00:00:01\").update([\"I=i\", \"J=1\"]).last_by(by=\"J\").where(\"I%2 != 0\")"); + .script("blinkOne", + "time_table(\"PT00:00:01\").update([\"I=i\", \"J=1\"]).last_by(by=\"J\").where(\"I%2 != 0\")"); public void testViewportOnStaticTable() { connect(tables) From 354fd484b4260af3d0dbf66f7bc1013ab46fd1c2 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Mon, 6 Nov 2023 12:12:24 -0600 Subject: [PATCH 10/31] Add additional integration tests --- .../web/client/api/JsColumnStatistics.java | 1 + .../io/deephaven/web/client/api/JsTable.java | 7 + .../web/ClientIntegrationTestSuite.java | 7 +- .../client/api/AbstractAsyncGwtTestCase.java | 8 +- .../client/api/ConcurrentTableTestGwt.java | 92 +++ .../client/api/TableManipulationTestGwt.java | 605 ++++++++++++++++++ .../api/filter/FilterConditionTestGwt.java | 3 +- 7 files changed, 716 insertions(+), 7 deletions(-) create mode 100644 web/client-api/src/test/java/io/deephaven/web/client/api/ConcurrentTableTestGwt.java create mode 100644 web/client-api/src/test/java/io/deephaven/web/client/api/TableManipulationTestGwt.java diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/JsColumnStatistics.java b/web/client-api/src/main/java/io/deephaven/web/client/api/JsColumnStatistics.java index 7fc67848a58..d14ee1f9e18 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/JsColumnStatistics.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/JsColumnStatistics.java @@ -11,6 +11,7 @@ import jsinterop.annotations.JsIgnore; import jsinterop.annotations.JsMethod; import jsinterop.annotations.JsProperty; +import jsinterop.annotations.JsType; import java.util.Arrays; import java.util.HashMap; diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java index dbde60b282a..a02352d925e 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java @@ -45,6 +45,7 @@ import io.deephaven.web.client.api.batch.RequestBatcher; import io.deephaven.web.client.api.console.JsVariableType; import io.deephaven.web.client.api.filter.FilterCondition; +import io.deephaven.web.client.api.filter.FilterValue; import io.deephaven.web.client.api.input.JsInputTable; import io.deephaven.web.client.api.lifecycle.HasLifecycle; import io.deephaven.web.client.api.state.StateCache; @@ -71,6 +72,7 @@ import io.deephaven.web.shared.fu.JsProvider; import io.deephaven.web.shared.fu.JsRunnable; import io.deephaven.web.shared.fu.RemoverFn; +import javaemul.internal.annotations.DoNotAutobox; import jsinterop.annotations.JsIgnore; import jsinterop.annotations.JsMethod; import jsinterop.annotations.JsNullable; @@ -596,6 +598,11 @@ public JsArray applyFilter(FilterCondition[] filter) { @TsUnion @JsType(name = "?", namespace = JsPackage.GLOBAL, isNative = true) public interface CustomColumnArgUnionType { + @JsOverlay + static CustomColumnArgUnionType of(@DoNotAutobox Object value) { + return Js.cast(value); + } + @JsOverlay default boolean isString() { return (Object) this instanceof String; diff --git a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java index ff1523c0763..77e5d2a393c 100644 --- a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java +++ b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java @@ -1,6 +1,8 @@ package io.deephaven.web; import com.google.gwt.junit.tools.GWTTestSuite; +import io.deephaven.web.client.api.ConcurrentTableTestGwt; +import io.deephaven.web.client.api.TableManipulationTestGwt; import io.deephaven.web.client.api.filter.FilterConditionTestGwt; import io.deephaven.web.client.api.subscription.ViewportTestGwt; import junit.framework.Test; @@ -12,11 +14,12 @@ public static Test suite() { // This test doesn't actually talk to the server, but it requires the dh-internal library be available. // Disabled for now, we don't have good toString on the FilterCondition/FilterValue types. -// suite.addTestSuite(FilterConditionTestGwt.class); + // suite.addTestSuite(FilterConditionTestGwt.class); // Actual integration tests suite.addTestSuite(ViewportTestGwt.class); - + suite.addTestSuite(TableManipulationTestGwt.class); + suite.addTestSuite(ConcurrentTableTestGwt.class); return suite; } diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java index 34b3cc552e1..6d3fcfb806a 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java @@ -169,7 +169,7 @@ private Promise runAllScriptsInOrder(CancellablePromise for (int i = 0; i < code.size(); i++) { final int index = i; result = result.then(ignore -> { - delayTestFinish(2000 + index); + delayTestFinish(4000 + index); return session.runCode(code.get(index)); }).then(r -> { @@ -232,10 +232,12 @@ protected Promise assertUpdateReceived(JsTable table, int count, int ti } protected Promise assertUpdateReceived(JsTable table, Consumer check, int timeoutInMillis) { - return this.waitForEvent(table, JsTable.EVENT_UPDATED, e -> { + return Promise.race(this.waitForEvent(table, JsTable.EVENT_UPDATED, e -> { ViewportData viewportData = e.detail; check.accept(viewportData); - }, timeoutInMillis); + }, timeoutInMillis), + table.nextEvent(JsTable.EVENT_REQUEST_FAILED, (double) timeoutInMillis).then(Promise::reject)) + .then(ignore -> Promise.resolve(table)); } protected IThenable.ThenOnFulfilledCallbackFn delayFinish(int timeout) { diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/ConcurrentTableTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/ConcurrentTableTestGwt.java new file mode 100644 index 00000000000..42fbd06078e --- /dev/null +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/ConcurrentTableTestGwt.java @@ -0,0 +1,92 @@ +package io.deephaven.web.client.api; + +import com.google.gwt.junit.DoNotRunWith; +import com.google.gwt.junit.Platform; +import elemental2.core.JsArray; +import elemental2.promise.IThenable; +import elemental2.promise.Promise; +import io.deephaven.web.client.api.filter.FilterCondition; +import jsinterop.base.Js; + +import static elemental2.dom.DomGlobal.console; + +@DoNotRunWith(Platform.HtmlUnitBug) +public class ConcurrentTableTestGwt extends AbstractAsyncGwtTestCase { + + private final TableSourceBuilder tables = new TableSourceBuilder() + .script("from deephaven import time_table") + .script("from datetime import datetime, timedelta") + .script("updates", + "time_table(period=\"PT00:00:01\", start_time=datetime.now() - timedelta(minutes=1)).update_view(\"condition= (i%2 == 0)\").sort_descending(\"Timestamp\")"); + + /** + * Take a table, get a viewport on it, copy it, filter the copy, get a viewport on the copy, ensure updates arrive + * on both copies. + * + * Assumes a table exists called `updates` with more than 20 items, where each page gets ticked at least once per + * second, and there is a column `condition` which is `true` for half and `false` for half. + */ + public void testOldCopyKeepsGettingUpdates() { + connect(tables) + .then(table("updates")) + .then(table -> { + // assign a viewport + table.setViewport(0, 9, null); + console.log("viewport set"); + + // within the usual update interval, expect to see an update (added 50% to ensure we get it) + delayTestFinish(2002); + return assertUpdateReceived(table, 10, 1500); + }).then(table -> { + // copy the table, apply a filter to it + console.log("applying filter"); + Promise filteredCopy = table.copy(true).then(toFilter -> { + Column conditionColumn = + toFilter.getColumns().find((c, p1, p2) -> c.getName().equals("condition")); + toFilter.applyFilter(new FilterCondition[] {conditionColumn.filter().isTrue()}); + toFilter.setViewport(0, 9, null); + return new Promise<>((resolve, reject) -> { + toFilter.addEventListener(JsTable.EVENT_FILTERCHANGED, e -> { + resolve.onInvoke(toFilter); + }); + }); + }); + + console.log("testing results"); + // confirm all are getting updates - higher timeout for filtered table, since half the items are + // skipped, 2s tick now + delayTestFinish(3501); + // noinspection unchecked + return Promise.all(new IThenable[] { + assertUpdateReceived(filteredCopy, 10, 3001), + assertUpdateReceived(table, 10, 1501) + }); + }).then(this::finish).catch_(this::report); + } + + public void testTwoIdenticalTablesWithDifferentViewports() { + connect(tables) + .then(delayFinish(2007)) + .then(table("updates")) + .then(table -> { + return Promise.all(new Promise[] {Promise.resolve(table), table.copy(true)}); + }) + .then(result -> { + JsArray array = Js.uncheckedCast(result); + array.getAt(0).setViewport(0, 9, null); + array.getAt(1).setViewport(10, 24, null); + delayTestFinish(3007); + // noinspection unchecked + return Promise.all(new Promise[] { + assertUpdateReceived(array.getAt(0), 10, 2007), + assertUpdateReceived(array.getAt(1), 15, 2008) + }); + }) + .then(this::finish).catch_(this::report); + } + + @Override + public String getModuleName() { + return "io.deephaven.web.DeephavenIntegrationTest"; + } +} diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/TableManipulationTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/TableManipulationTestGwt.java new file mode 100644 index 00000000000..d4b9c29854b --- /dev/null +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/TableManipulationTestGwt.java @@ -0,0 +1,605 @@ +package io.deephaven.web.client.api; + +import com.google.gwt.junit.DoNotRunWith; +import com.google.gwt.junit.Platform; +import elemental2.core.JsArray; +import elemental2.promise.IThenable; +import elemental2.promise.Promise; +import io.deephaven.web.client.api.filter.FilterCondition; +import io.deephaven.web.client.api.filter.FilterValue; +import jsinterop.base.Js; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@DoNotRunWith(Platform.HtmlUnitBug) +public class TableManipulationTestGwt extends AbstractAsyncGwtTestCase { + private final TableSourceBuilder tables = new TableSourceBuilder() + .script("from deephaven import empty_table") + .script("truthtable", + "empty_table(8).update([\"I=i\", \"four=(int)(i/4)%2\", \"two=(int)(i/2)%2\", \"one=i%2\"])") + .script("strings", "empty_table(1).update([\"str=`abcdefg`\", \"nostr=(String)null\"])") + .script("threedays", + "empty_table(3).update([\"I=i\", \"Timestamp=now() - (i * 24 * 60 * 60 * 1000 * 1000000l)\"])"); + + public void testChangingFilters() { + connect(tables) + .then(table("truthtable")) + .then(table -> { + delayTestFinish(5000); + // filter one column + table.applyFilter( + new FilterCondition[] {table.findColumn("one").filter().eq(FilterValue.ofNumber(0.0))}); + + // before setting viewport, test the size of the table + return waitForEvent(table, JsTable.EVENT_SIZECHANGED, e -> { + assertEquals(4., table.getSize(), 0); + assertEquals(8., table.getTotalSize(), 0); + }, 1000); + }) + .then(table -> { + // then set the viewport, confirm we get those items back + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 0, 2, 4, 6); + }) + .then(table -> { + // filter another too + table.applyFilter(new FilterCondition[] { + table.findColumn("one").filter().eq(FilterValue.ofNumber(0.0)), + table.findColumn("two").filter().eq(FilterValue.ofNumber(1.0)), + }); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 2, 6); + }) + .then(table -> { + // check full table size from the last filter + assertEquals(2., table.getSize(), 0); + + + // remove the first filter + table.applyFilter(new FilterCondition[] { + table.findColumn("two").filter().eq(FilterValue.ofNumber(1.0)), + }); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 2, 3, 6, 7); + }) + .then(table -> { + // check full table size from the last filter + assertEquals(4., table.getSize(), 0); + + // clear all filters + table.applyFilter(new FilterCondition[] {}); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 0, 1, 2, 3, 4, 5, 6, 7); + }) + .then(table -> { + // check full table size from the last filter + assertEquals(8., table.getSize(), 0); + assertEquals(8., table.getTotalSize(), 0); + + return Promise.resolve(table); + }) + + .then(this::finish).catch_(this::report); + } + + public void testStackingSorts() { + connect(tables) + .then(table("truthtable")) + .then(table -> { + delayTestFinish(5000); + // simple sort + table.applySort(new Sort[] {table.findColumn("one").sort().asc()}); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 0, 2, 4, 6, 1, 3, 5, 7); + }) + .then(table -> { + // check full table size from the last operation + assertEquals(8., table.getSize(), 0); + assertEquals(8., table.getTotalSize(), 0); + + // toggle it + table.applySort(new Sort[] {table.findColumn("one").sort().desc()}); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 1, 3, 5, 7, 0, 2, 4, 6); + }) + .then(table -> { + // check full table size from the last operation + assertEquals(8., table.getSize(), 0); + + // add another sort + table.applySort(new Sort[] { + table.findColumn("one").sort().desc(), + table.findColumn("two").sort().asc(), + }); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 1, 5, 3, 7, 0, 4, 2, 6); + }) + .then(table -> { + // toggle second sort + table.applySort(new Sort[] { + table.findColumn("one").sort().desc(), + table.findColumn("two").sort().desc(), + }); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 3, 7, 1, 5, 2, 6, 0, 4); + }) + .then(table -> { + // remove first sort + table.applySort(new Sort[] { + table.findColumn("two").sort().desc(), + }); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 2, 3, 6, 7, 0, 1, 4, 5); + }) + .then(table -> { + // clear all sorts + table.applySort(new Sort[] {}); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 0, 1, 2, 3, 4, 5, 6, 7); + }) + .then(this::finish).catch_(this::report); + } + + public void testSerialFilterAndSort() { + connect(tables) + .then(table("truthtable")) + .then(table -> { + delayTestFinish(5000); + // first, filter, as soon as that is resolved on the server, sort + table.applyFilter(new FilterCondition[] { + table.findColumn("two").filter().eq(FilterValue.ofNumber(0.0)) + }); + // no viewport, since we're going to make another change + return waitForEvent(table, JsTable.EVENT_FILTERCHANGED, e -> { + }, 1000); + }) + .then(table -> { + table.applySort(new Sort[] { + table.findColumn("one").sort().desc() + }); + // set a viewport once this is complete + table.setViewport(0, 6, null); + return waitForEvent(table, JsTable.EVENT_SORTCHANGED, e -> { + }, 1000); + }) + .then(table -> { + return assertNextViewportIs(table, 1, 5, 0, 4); + })// this looks confusing, remember it sorts "one" descending, and is a stable sort + .then(table -> { + // check full table size from the last filter + assertEquals(4., table.getSize(), 0); + + return Promise.resolve(table); + }) + .then(this::finish).catch_(this::report); + } + + + public void testRapidFilterAndSort() { + connect(tables) + .then(table("truthtable")) + .then(table -> { + delayTestFinish(5000); + // filter and sort, and set a viewport, wait for results + table.applyFilter(new FilterCondition[] { + table.findColumn("two").filter().eq(FilterValue.ofNumber(0.0)) + }); + table.applySort(new Sort[] { + table.findColumn("one").sort().desc() + }); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 1, 5, 0, 4); + }) + .then(table -> { + // replace sort then replace filter then set viewport, wait for results + table.applyFilter(new FilterCondition[] { + table.findColumn("one").filter().eq(FilterValue.ofNumber(0.0)) + }); + table.applySort(new Sort[] { + table.findColumn("two").sort().asc() + }); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 0, 4, 2, 6); + }) + .then(table -> { + // remove both then re-add both in reverse order, wait for results + table.applySort(new Sort[] { + table.findColumn("two").sort().asc() + }); + table.applyFilter(new FilterCondition[] { + table.findColumn("one").filter().eq(FilterValue.ofNumber(0.0)) + }); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 0, 4, 2, 6); + }) + .then(this::finish).catch_(this::report); + } + + public void testSwappingFilterAndSort() { + connect(tables) + .then(table("truthtable")) + .then(table -> { + delayTestFinish(5000); + // filter one way, set a sort, then change the filter back again before setting a viewport + table.applyFilter(new FilterCondition[] { + table.findColumn("one").filter().eq(FilterValue.ofNumber(1.0)) + }); + table.applySort(new Sort[] { + table.findColumn("two").sort().asc() + }); + table.applyFilter(new FilterCondition[] { + table.findColumn("one").filter().eq(FilterValue.ofNumber(0.0)) + }); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 0, 4, 2, 6); + }) + .then(this::finish).catch_(this::report); + } + + // TODO: https://deephaven.atlassian.net/browse/DH-11196 + public void ignore_testClearingFilter() { + connect(tables) + .then(table("truthtable")) + .then(table -> { + delayTestFinish(5000); + table.applyFilter(new FilterCondition[] { + table.findColumn("I").filter().lessThan(FilterValue.ofNumber(3.5)) + }); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 0, 1, 2, 3); + }) + .then(table -> { + table.applyFilter(new FilterCondition[0]); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 0, 1, 2, 3, 4, 5, 6, 7); + }) + .then(this::finish).catch_(this::report); + } + + public void testFilterOutAllItems() { + connect(tables) + .then(table("truthtable")) + .then(table -> { + delayTestFinish(2012); + table.applyFilter(new FilterCondition[] { + table.findColumn("one").filter().eq(FilterValue.ofNumber(100.0)) + }); + table.setViewport(0, 7, null); + return assertNextViewportIs(table); + }) + .then(table -> { + // check full table size from the last filter + assertEquals(0., table.getSize(), 0); + + return Promise.resolve(table); + }) + .then(this::finish).catch_(this::report); + } + + public void testReplaceSort() { + connect(tables) + .then(table("truthtable")) + .then(table -> { + delayTestFinish(5000); + // sort, get results, remove sort, get results + table.applySort(new Sort[] { + table.findColumn("I").sort().desc() + }); + table.setViewport(0, 7, null); + + return assertNextViewportIs(table, 7, 6, 5, 4, 3, 2, 1, 0); + }) + .then(table -> { + table.applySort(new Sort[] {}); + table.setViewport(0, 7, null); + + return assertNextViewportIs(table, 0, 1, 2, 3, 4, 5, 6, 7); + }) + // .then(table -> { + // //sort, set viewport, then right away switch sort + // table.applySort(new Sort[]{ + // table.findColumn("one").sort().desc() + // }); + // table.setViewport(0, 7, null); + // table.applySort(new Sort[]{ + // table.findColumn("one").sort().asc() + // }); + // table.setViewport(0, 7, null); + // + // return assertNextViewportIs(table, 0, 2, 4, 6, 1, 3, 5, 7); + // }) + .then(table -> { + // sort, set viewport, wait a moment, then do the same + table.applySort(new Sort[] { + table.findColumn("one").sort().desc() + }); + table.setViewport(0, 7, null); + return Promise.resolve(table); + }) + .then(table -> { + table.applySort(new Sort[] { + table.findColumn("one").sort().asc() + }); + table.setViewport(0, 7, null); + return assertNextViewportIs(table, 0, 2, 4, 6, 1, 3, 5, 7); + }) + .then(this::finish).catch_(this::report); + } + + public void testChangingColumns() { + connect(tables) + .then(table("truthtable")) + .then(table -> { + delayTestFinish(5000); + + table.applyCustomColumns(JsArray.of(JsTable.CustomColumnArgUnionType.of("a=\"\" + I"))); + table.setViewport(0, 7, null); + + return assertNextViewportIs(table, t -> t.findColumn("a"), + new String[] {"0", "1", "2", "3", "4", "5", "6", "7"}); + }) + .then(table -> { + + table.applyCustomColumns(JsArray.of( + JsTable.CustomColumnArgUnionType.of("a=\"\" + I"), + JsTable.CustomColumnArgUnionType.of("b=\"x\" + I"))); + table.setViewport(0, 7, null); + + @SuppressWarnings("unchecked") + Promise assertion = Promise.all(new IThenable[] { + assertNextViewportIs(table, t -> t.findColumn("a"), + new String[] {"0", "1", "2", "3", "4", "5", "6", "7"}), + assertNextViewportIs(table, t -> t.findColumn("b"), + new String[] {"x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"}) + }); + // we don't want the array of results, just the success/failure and the same table we were working + // with before + return assertion.then(ignore -> Promise.resolve(table)); + }) + .then(table -> { + + table.applyCustomColumns(JsArray.of(JsTable.CustomColumnArgUnionType.of("b=\"x\" + I"))); + table.setViewport(0, 7, null); + + return assertUpdateReceived(table, viewportData -> { + // make sure we see the one column, but not the other + String[] expected = new String[] {"x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"}; + String[] actual = Js.uncheckedCast(getColumnData(viewportData, table.findColumn("b"))); + assertTrue("Expected " + Arrays.toString(expected) + ", found " + Arrays.toString(actual) + + " in table " + table, Arrays.equals(expected, actual)); + for (int i = 0; i < table.getColumns().length; i++) { + assertFalse("a".equals(table.getColumns().getAt(i).getName())); + } + assertEquals(5, viewportData.getColumns().length); + }, 2001); + }) + .then(table -> { + + table.applyCustomColumns(new JsArray<>()); + table.setViewport(0, 7, null); + + return assertUpdateReceived(table, viewportData -> { + // verify the absence of "a" and "b" in data and table + for (int i = 0; i < table.getColumns().length; i++) { + Column col = table.getColumns().getAt(i); + assertFalse("a".equals(col.getName())); + assertFalse("b".equals(col.getName())); + } + assertEquals(4, viewportData.getColumns().length); + }, 2002); + }) + .then(this::finish).catch_(this::report); + } + + // TODO: https://deephaven.atlassian.net/browse/DH-11196 + public void ignore_testColumnsFiltersAndSorts() { + connect(tables) + .then(table("truthtable")) + .then(table -> { + delayTestFinish(10000); + // add a column + table.applyCustomColumns(JsArray.of(JsTable.CustomColumnArgUnionType.of("a=\"\" + I"))); + table.setViewport(0, 7, null); + + return assertNextViewportIs(table, t -> t.findColumn("a"), + new String[] {"0", "1", "2", "3", "4", "5", "6", "7"}); + }) + .then(table -> { + delayTestFinish(10000); + // apply a filter + table.applyFilter(new FilterCondition[] { + table.findColumn("two").filter().eq(FilterValue.ofNumber(0.0)) + }); + table.setViewport(0, 7, null); + + return assertNextViewportIs(table, t -> t.findColumn("a"), new String[] {"0", "1", "4", "5"}); + }) + .then(table -> { + delayTestFinish(10000); + // apply a sort + table.applySort(new Sort[] { + table.findColumn("one").sort().desc() + }); + table.setViewport(0, 7, null); + + return assertNextViewportIs(table, t -> t.findColumn("a"), new String[] {"1", "5", "0", "4"}); + }) + .then(table -> { + delayTestFinish(10000); + // remove the column + table.applyCustomColumns(new JsArray<>()); + table.setViewport(0, 7, null); + + @SuppressWarnings("unchecked") + Promise assertion = Promise.all(new IThenable[] { + assertUpdateReceived(table, viewportData -> { + // make sure we don't see the column + for (int i = 0; i < table.getColumns().length; i++) { + assertFalse("a".equals(table.getColumns().getAt(i).getName())); + } + assertEquals(4, viewportData.getColumns().length); + }, 2001), + assertNextViewportIs(table, 1, 5, 0, 4) + }); + return assertion.then(ignore -> Promise.resolve(table)); + }) + .then(table -> { + delayTestFinish(10000); + // put the column back + table.applyCustomColumns(JsArray.of(JsTable.CustomColumnArgUnionType.of("a=\"\" + I"))); + table.setViewport(0, 7, null); + + return assertNextViewportIs(table, t -> t.findColumn("a"), new String[] {"1", "5", "0", "4"}); + }) + .then(table -> { + delayTestFinish(10000); + // remove the filter (column and sort, no filter) + table.applyFilter(new FilterCondition[] {}); + table.setViewport(0, 7, null); + + return assertNextViewportIs(table, t -> t.findColumn("a"), + new String[] {"1", "3", "5", "7", "0", "2", "4", "6"}); + }) + .then(table -> { + delayTestFinish(10000); + // remove the column (sorted, no column) + table.applyCustomColumns(new JsArray<>()); + table.setViewport(0, 7, null); + + return assertNextViewportIs(table, 1, 3, 5, 7, 0, 2, 4, 6); + }) + .then(this::finish).catch_(this::report); + + } + + public void testCustomColumnsReferencingOtherCustomColumns() { + connect(tables) + .then(table("truthtable")) + .then(table -> { + delayTestFinish(2001); + table.applyCustomColumns(JsArray.of(JsTable.CustomColumnArgUnionType.of("y=`z`"), + JsTable.CustomColumnArgUnionType.of("z=y"))); + table.setViewport(0, 7, null); + + String[] expected = {"z", "z", "z", "z", "z", "z", "z", "z"}; + // noinspection unchecked + return Promise.all(new IThenable[] { + assertNextViewportIs(table, t -> t.findColumn("y"), expected), + assertNextViewportIs(table, t -> t.findColumn("z"), expected) + }); + }).then(this::finish).catch_(this::report); + } + + public void testDateTimeInFilters() { + List dates = new ArrayList<>(); + connect(tables) + .then(table("threedays")) + .then(table -> { + delayTestFinish(2002); + // grab the three days in the db + table.setViewport(0, 2, null); + + return assertUpdateReceived(table, viewportData -> { + viewportData.getRows().forEach((row, index, all) -> { + dates.add(row.get(table.findColumn("Timestamp")).cast()); + return null; + }); + }, 2003); + }) + .then(table -> { + // take the first date, filter it out, confirm we see the other two + table.applyFilter(new FilterCondition[] { + table.findColumn("Timestamp").filter() + .notIn(new FilterValue[] { + FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(dates.get(0)))}) + }); + table.setViewport(0, 2, null); + return assertUpdateReceived(table, viewportData -> { + assertEquals(2, viewportData.getRows().length); + // this looks shady with the toString(), but they are both LongWrapper values + assertEquals(dates.get(1).toString(), + viewportData.getRows().getAt(0).get(table.findColumn("Timestamp")).toString()); + assertEquals(dates.get(2).toString(), + viewportData.getRows().getAt(1).get(table.findColumn("Timestamp")).toString()); + }, 2004); + }) + .then(table -> { + // take the first date, filter for it, confirm we see only it + table.applyFilter(new FilterCondition[] { + table.findColumn("Timestamp").filter() + .in(new FilterValue[] { + FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(dates.get(0)))}) + }); + table.setViewport(0, 2, null); + return assertUpdateReceived(table, viewportData -> { + assertEquals(1, viewportData.getRows().length); + assertEquals(dates.get(0).toString(), + viewportData.getRows().getAt(0).get(table.findColumn("Timestamp")).toString()); + + }, 2005); + }) + .then(this::finish).catch_(this::report); + } + + public void testIcaseEqualFilters() { + connect(tables) + .then(table("strings")) + .then(table -> { + delayTestFinish(5000); + // ==icase with match + table.applyFilter(new FilterCondition[] { + table.findColumn("str").filter().eqIgnoreCase(FilterValue.ofString("ABCdefg")) + }); + table.setViewport(0, 0, null); + return assertUpdateReceived(table, 1, 2006); + }) + .then(table -> { + // ==icase with no match + table.applyFilter(new FilterCondition[] { + table.findColumn("str").filter().eqIgnoreCase(FilterValue.ofString("xyz")) + }); + table.setViewport(0, 0, null); + return assertUpdateReceived(table, 0, 2007); + }) + .then(table -> { + // !=icase with match (i.e. don't show anything) + table.applyFilter(new FilterCondition[] { + table.findColumn("str").filter().notEqIgnoreCase(FilterValue.ofString("ABCdefg")) + }); + table.setViewport(0, 0, null); + return assertUpdateReceived(table, 0, 2008); + }) + .then(table -> { + // !=icase with no match (i.e. show row) + table.applyFilter(new FilterCondition[] { + table.findColumn("nostr").filter().notEqIgnoreCase(FilterValue.ofString("ABC")) + }); + table.setViewport(0, 0, null); + return assertUpdateReceived(table, 1, 2009); + }) + .then(table -> { + // 0=icase with value check against null (i.e. find nothing) + table.applyFilter(new FilterCondition[] { + table.findColumn("nostr").filter().eqIgnoreCase(FilterValue.ofString("ABC")) + }); + table.setViewport(0, 0, null); + return assertUpdateReceived(table, 0, 2010); + }) + .then(table -> { + // !=icase with value check against null (i.e. find a row) + table.applyFilter(new FilterCondition[] { + table.findColumn("nostr").filter().notEqIgnoreCase(FilterValue.ofString("ABC")) + }); + table.setViewport(0, 0, null); + return assertUpdateReceived(table, 1, 2011); + }) + .then(this::finish).catch_(this::report); + } + + @Override + public String getModuleName() { + return "io.deephaven.web.DeephavenIntegrationTest"; + } +} diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java index d518ae08680..27aa370f501 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/filter/FilterConditionTestGwt.java @@ -121,8 +121,7 @@ public void testCreateCombinedFilters() { c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(3))), c.filter().eq(FilterValue.ofNumber(FilterValue.OfNumberUnionParam.of(4)))) .not()) - .toString() - ); + .toString()); finishTest(); return null; }) From d8ad542d4f679865a478f18956332cb4d45ea7af Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Mon, 6 Nov 2023 13:39:51 -0600 Subject: [PATCH 11/31] null values test --- .../web/ClientIntegrationTestSuite.java | 5 +- .../web/client/api/NullValueTestGwt.java | 78 +++++++++++++++++++ .../ConcurrentTableTestGwt.java | 5 +- 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 web/client-api/src/test/java/io/deephaven/web/client/api/NullValueTestGwt.java rename web/client-api/src/test/java/io/deephaven/web/client/api/{ => subscription}/ConcurrentTableTestGwt.java (95%) diff --git a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java index 77e5d2a393c..03b4dd86deb 100644 --- a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java +++ b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java @@ -1,9 +1,9 @@ package io.deephaven.web; import com.google.gwt.junit.tools.GWTTestSuite; -import io.deephaven.web.client.api.ConcurrentTableTestGwt; +import io.deephaven.web.client.api.NullValueTestGwt; +import io.deephaven.web.client.api.subscription.ConcurrentTableTestGwt; import io.deephaven.web.client.api.TableManipulationTestGwt; -import io.deephaven.web.client.api.filter.FilterConditionTestGwt; import io.deephaven.web.client.api.subscription.ViewportTestGwt; import junit.framework.Test; import junit.framework.TestSuite; @@ -20,6 +20,7 @@ public static Test suite() { suite.addTestSuite(ViewportTestGwt.class); suite.addTestSuite(TableManipulationTestGwt.class); suite.addTestSuite(ConcurrentTableTestGwt.class); + suite.addTestSuite(NullValueTestGwt.class); return suite; } diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/NullValueTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/NullValueTestGwt.java new file mode 100644 index 00000000000..8cf67db724d --- /dev/null +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/NullValueTestGwt.java @@ -0,0 +1,78 @@ +package io.deephaven.web.client.api; + +import com.google.gwt.junit.DoNotRunWith; +import com.google.gwt.junit.Platform; +import elemental2.core.JsArray; +import elemental2.promise.Promise; +import io.deephaven.web.client.api.subscription.ViewportRow; + +@DoNotRunWith(Platform.HtmlUnitBug) +public class NullValueTestGwt extends AbstractAsyncGwtTestCase { + private final TableSourceBuilder tables = new TableSourceBuilder() + .script("from deephaven import empty_table") + .script("nulltable", "empty_table(2).update([\n" + + " \"MyInt=i==0?null:i\",\n" + + " \"MyLong=i==0?null:(long)i\",\n" + + " \"MyDouble=i==0?null:(double)i\",\n" + + " \"MyShort=i==0?null:(short)i\",\n" + + " \"MyFloat=i==0?null:(float)i\",\n" + + " \"MyChar=i==0?null:(char)i\",\n" + + " \"MyByte=i==0?null:(byte)i\",\n" + + " \"MyBoolean=i==0?null:true\",\n" + + " \"MyDate=i==0?null:epochNanosToInstant(i)\"\n" + + "])"); + public void testNullTable() { + connect(tables) + .then(table("nulltable")) + .then(table -> { + delayTestFinish(5000); + + assertEquals(2., table.getSize(), 0); + assertEquals(2., table.getTotalSize(), 0); + + return Promise.resolve(table); + }) + .then(table -> { + assertEquals("int", table.findColumn("MyInt").getType()); + assertEquals("long", table.findColumn("MyLong").getType()); + assertEquals("double", table.findColumn("MyDouble").getType()); + assertEquals("short", table.findColumn("MyShort").getType()); + assertEquals("float", table.findColumn("MyFloat").getType()); + assertEquals("char", table.findColumn("MyChar").getType()); + assertEquals("byte", table.findColumn("MyByte").getType()); + assertEquals("java.lang.Boolean", table.findColumn("MyBoolean").getType()); + assertEquals("java.time.Instant", table.findColumn("MyDate").getType()); + + return Promise.resolve(table); + }) + .then(table -> { + table.setViewport(0, 1, null); + return assertUpdateReceived(table, viewport -> { + JsArray rows = viewport.getRows(); + ViewportRow nullRow = rows.getAt(0); + + JsArray columns = table.getColumns(); + for (int i = 0; i < columns.length; i++) { + assertEquals(null, nullRow.get(columns.getAt(i))); + } + + ViewportRow valueRow = rows.getAt(1); + assertEquals(1, valueRow.get(table.findColumn("MyInt")).asInt()); + assertEquals((long)1, valueRow.get(table.findColumn("MyLong")).cast().getWrapped()); + assertEquals((double)1, valueRow.get(table.findColumn("MyDouble")).asDouble()); + assertEquals((short)1, valueRow.get(table.findColumn("MyShort")).asShort()); + assertEquals((float)1., valueRow.get(table.findColumn("MyFloat")).asFloat()); + assertEquals((char)1, valueRow.get(table.findColumn("MyChar")).asChar()); + assertEquals((byte)1, valueRow.get(table.findColumn("MyByte")).asByte()); + assertEquals(true, valueRow.get(table.findColumn("MyBoolean")).asBoolean()); + assertEquals((long)1, valueRow.get(table.findColumn("MyDate")).cast().getWrapped()); + }, 1000); + }) + .then(this::finish).catch_(this::report); + } + + @Override + public String getModuleName() { + return "io.deephaven.web.DeephavenIntegrationTest"; + } +} \ No newline at end of file diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/ConcurrentTableTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ConcurrentTableTestGwt.java similarity index 95% rename from web/client-api/src/test/java/io/deephaven/web/client/api/ConcurrentTableTestGwt.java rename to web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ConcurrentTableTestGwt.java index 42fbd06078e..c6a60faca3f 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/ConcurrentTableTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ConcurrentTableTestGwt.java @@ -1,10 +1,13 @@ -package io.deephaven.web.client.api; +package io.deephaven.web.client.api.subscription; import com.google.gwt.junit.DoNotRunWith; import com.google.gwt.junit.Platform; import elemental2.core.JsArray; import elemental2.promise.IThenable; import elemental2.promise.Promise; +import io.deephaven.web.client.api.AbstractAsyncGwtTestCase; +import io.deephaven.web.client.api.Column; +import io.deephaven.web.client.api.JsTable; import io.deephaven.web.client.api.filter.FilterCondition; import jsinterop.base.Js; From 9df48592b3f68bca3a851cd1e8c742e185abfc30 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Mon, 6 Nov 2023 15:26:34 -0600 Subject: [PATCH 12/31] POtential totals table test --- .../web/ClientIntegrationTestSuite.java | 2 + .../web/client/api/TotalsTableTestGwt.java | 440 ++++++++++++++++++ 2 files changed, 442 insertions(+) create mode 100644 web/client-api/src/test/java/io/deephaven/web/client/api/TotalsTableTestGwt.java diff --git a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java index 03b4dd86deb..9695a60ae93 100644 --- a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java +++ b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java @@ -2,6 +2,7 @@ import com.google.gwt.junit.tools.GWTTestSuite; import io.deephaven.web.client.api.NullValueTestGwt; +import io.deephaven.web.client.api.TotalsTableTestGwt; import io.deephaven.web.client.api.subscription.ConcurrentTableTestGwt; import io.deephaven.web.client.api.TableManipulationTestGwt; import io.deephaven.web.client.api.subscription.ViewportTestGwt; @@ -21,6 +22,7 @@ public static Test suite() { suite.addTestSuite(TableManipulationTestGwt.class); suite.addTestSuite(ConcurrentTableTestGwt.class); suite.addTestSuite(NullValueTestGwt.class); +// suite.addTestSuite(TotalsTableTestGwt.class); return suite; } diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/TotalsTableTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/TotalsTableTestGwt.java new file mode 100644 index 00000000000..facf1b1fbb2 --- /dev/null +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/TotalsTableTestGwt.java @@ -0,0 +1,440 @@ +package io.deephaven.web.client.api; + +import elemental2.core.JsArray; +import elemental2.core.JsString; +import elemental2.dom.CustomEvent; +import elemental2.promise.IThenable; +import elemental2.promise.Promise; +import io.deephaven.web.client.api.filter.FilterCondition; +import io.deephaven.web.client.api.filter.FilterValue; +import io.deephaven.web.client.api.subscription.ViewportData; +import jsinterop.base.Js; + +import java.util.function.Consumer; + +public class TotalsTableTestGwt extends AbstractAsyncGwtTestCase { + private final TableSourceBuilder tables = new TableSourceBuilder() + .script("from deephaven import empty_table") + .script("strings", "empty_table(1).update([\"str=`abcdefg`\", \"nostr=(String)null\"])") + .script("hasTotals", + "empty_table(5).update_view([(\"I = (double)i\", \"J = (double) i * i\", \"K = (double) i % 2\")])" + + ".with_attributes({'TotalsTable':'false,false,Count;J=Min:Avg,K=Skip,;'})"); + + public void testQueryDefinedConfigs() { + connect(tables) + .then(session -> { + session.getTable("strings", true) + .then(table1 -> { + //make sure the config is null, since it wasn't defined in the config + assertNull(table1.getTotalsTableConfig()); + + //check that we get a totals table back, even though it won't have any columns + //noinspection unchecked + return (Promise) Promise.all(new Promise[]{ + table1.getTotalsTable(null).then(totals1 -> { + assertEquals(0, totals1.getColumns().length); + assertEquals(1, totals1.getSize(), DELTA); + return Promise.resolve(totals1); + }), + table1.getGrandTotalsTable(null).then(totals11 -> { + assertEquals(0, totals11.getColumns().length); + assertEquals(1, totals11.getSize(), DELTA); + return Promise.resolve(totals11); + }), + }); + }); + return Promise.resolve(session); + }) + .then(table("hasTotals")) + .then(table -> { + //make sure the config is null, since it wasn't defined in the config + assertNotNull(table.getTotalsTableConfig()); + assertEquals("Count", table.getTotalsTableConfig().defaultOperation); + assertTrue(table.getTotalsTableConfig().operationMap.has("K")); + assertEquals(1, table.getTotalsTableConfig().operationMap.get("K").length); + assertEquals(Js.cast("Skip"), table.getTotalsTableConfig().operationMap.get("K").getAt(0)); + + assertTrue(table.getTotalsTableConfig().operationMap.has("J")); + assertEquals(2, table.getTotalsTableConfig().operationMap.get("J").length); + assertEquals(Js.cast("Min"), table.getTotalsTableConfig().operationMap.get("J").getAt(0)); + assertEquals(Js.cast("Avg"), table.getTotalsTableConfig().operationMap.get("J").getAt(1)); + + assertFalse(table.getTotalsTableConfig().operationMap.has("I")); + + //check that we get a totals table back, even though it won't have any columns + //noinspection unchecked + return Promise.all((Promise[])new Promise[] { + table.getTotalsTable(null) + .then(totals -> { + assertEquals(3, totals.getColumns().length); + assertEquals(1, totals.getSize(), DELTA); + totals.setViewport(0, 100, null, null); + + return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals( totals,5, 6., 0, "a1"), 1500); + }), + table.getGrandTotalsTable(null) + .then(totals -> { + assertEquals(3, totals.getColumns().length); + assertEquals(1, totals.getSize(), DELTA); + totals.setViewport(0, 100, null, null); + + return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals(totals, 5, 6.0, 0., "a2"), 1500); + }) + }); + }) + .then(this::finish).catch_(this::report); + } + + // TODO: https://deephaven.atlassian.net/browse/DH-11196 + public void ignore_testTotalsOnFilteredTable() { + JsTotalsTable[] totalTables = {null, null}; + Promise[] totalPromises = {null, null}; + connect(tables) + .then(table("hasTotals")) + .then(table -> { + delayTestFinish(8000); + table.applyFilter(new FilterCondition[]{ + table.findColumn("K").filter().eq(FilterValue.ofNumber(0.0)) + }); + table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + + return waitForEvent(table, JsTable.EVENT_FILTERCHANGED, 2001).onInvoke(table); + }) + .then(table -> + promiseAllThen(table, + table.getTotalsTable(null) + .then(totals -> { + totalTables[0] = totals; + assertEquals(3, totals.getColumns().length); + assertEquals(1, totals.getSize(), DELTA); + totals.setViewport(0, 100, null, null); + + //confirm the normal totals match the filtered data + return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals(totals, 3, 6.666666, 0.0, "a1"), 1501); + }), + table.getGrandTotalsTable(null) + .then(totals -> { + totalTables[1] = totals; + assertEquals(3, totals.getColumns().length); + assertEquals(1, totals.getSize(), DELTA); + totals.setViewport(0, 100, null, null); + + //confirm the grand totals are unchanged + return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals(totals, 5, 6., 0., "a2"), 1502); + })) + ) + .then(table -> { + // Now, change the filter on the original table, and expect the totals tables to automatically update. + + table.applyFilter(new FilterCondition[]{ + table.findColumn("K").filter().eq(FilterValue.ofNumber(1.0)) + }); + table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + + return promiseAllThen(table, + waitForEvent(table, JsTable.EVENT_FILTERCHANGED, 2002).onInvoke(table), + totalPromises[0] = waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, + checkTotals(totalTables[0], 2, 5, 1, "b1"), 1503), + totalPromises[1] = waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, + checkTotals(totalTables[1], 5, 6, 0, "b2"), 1504) + ); + }) + .then(table -> { + // forcibly disconnect the worker and test that the total table come back up, and respond to re-filtering. + table.getConnection().forceReconnect(); + return Promise.resolve(table); + }) + .then(table->waitForEvent(table, JsTable.EVENT_RECONNECT, 5001).onInvoke(table)) + .then(table -> promiseAllThen(table, + waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, + checkTotals(totalTables[0], 2, 5, 1, "c1"), 7505), + waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, + checkTotals(totalTables[1], 5, 6, 0, "c2"), 7506) + )) + .then(table -> { + // Now... refilter the original table, and assert that the totals tables update. + table.applyFilter(new FilterCondition[]{ + table.findColumn("K").filter().eq(FilterValue.ofNumber(0.0)) + }); + table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + + return promiseAllThen(table, + waitForEvent(table, JsTable.EVENT_FILTERCHANGED, 2003).onInvoke(table), + waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, + checkTotals(totalTables[0], 3, 6.666666, 0.0, "d1"), 1507), + waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, + checkTotals(totalTables[1], 5, 6., 0., "d2"), 1508) + ); + }) + .then(this::finish).catch_(this::report); + } + + public void testClosingTotalsWhileClearingFilter() { + JsTotalsTable[] totalTables = {null}; + connect(tables) + .then(table("hasTotals")) + .then(table -> { + delayTestFinish(8000); + table.applyFilter(new FilterCondition[]{ + table.findColumn("K").filter().eq(FilterValue.ofNumber(0.0)) + }); + table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + + return waitForEvent(table, JsTable.EVENT_UPDATED, 2001).onInvoke(table); + }) + .then(table -> table.getTotalsTable(null) + .then(totals -> { + totalTables[0] = totals; + return Promise.resolve(table); + }) + ) + .then(table -> { + // Now, clear the filter on the original table, and close the totals table at the same time + table.applyFilter(new FilterCondition[]{}); + // Close the table in the same step as applying the filter. Should not throw an error. + totalTables[0].close(); + table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + + return waitForEvent(table, JsTable.EVENT_UPDATED, 2002).onInvoke(table); + }) + .then(this::finish).catch_(this::report); + } + + // TODO: https://deephaven.atlassian.net/browse/DH-11196 + public void ignore_testFilteringTotalsTable() { + JsTotalsTable[] totalTables = {null, null}; + Promise[] totalPromises = {null, null}; + connect(tables) + .then(table("hasTotals")) + .then(table -> { + delayTestFinish(8000); + /* Here is the base table: + I J K + 0 0 0 + 1 1 1 + 2 4 0 // we are going to remove this row, to test source table filtering. + 3 9 1 + 4 16 0 + */ + table.applyFilter(new FilterCondition[]{ + table.findColumn("J").filter().notEq(FilterValue.ofNumber(4.0)) + }); + table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + + return waitForEvent(table, JsTable.EVENT_FILTERCHANGED, 2001).onInvoke(table); + }) + .then(table -> { + JsTotalsTableConfig config = new JsTotalsTableConfig(); + // group by K so we can do some filtering on the output of the tables + config.groupBy.push("K"); + // copy over the rest of the operations set on the server, so we can reuse some assertion logic + config.operationMap.set("J", Js.uncheckedCast(new JsString[]{ toJsString("Avg"), toJsString("Min")})); +// config.operationMap.set("K", Js.uncheckedCast(new JsString[]{ toJsString("Skip")})); + config.defaultOperation = "Count"; + return promiseAllThen(table, + table.getTotalsTable(config) + .then(totals -> { + totalTables[0] = totals; + assertEquals(4, totals.getColumns().length); + assertEquals(2, totals.getSize(), DELTA); + totals.setViewport(0, 100, null, null); + + //confirm the normal totals match the filtered data + return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals(totals, "a1", + TotalsResults.of(2, 2, 8, 0.0), + TotalsResults.of(2, 2, 5, 1.0) + ), 1501); + }), + table.getGrandTotalsTable(config) + .then(totals -> { + totalTables[1] = totals; + assertEquals(4, totals.getColumns().length); + assertEquals(2, totals.getSize(), DELTA); + totals.setViewport(0, 100, null, null); + + //confirm the grand totals include the missing row... + return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals(totals, "a2", + TotalsResults.of(3, 3, 6.66666, 0.0), + TotalsResults.of(2, 2, 5, 1.0) + ), 1502); + })); + }) + .then(table -> { + // Now, apply a filter to each totals tables... + + totalTables[0].applyFilter(new FilterCondition[]{ + // we'll use notEq here, so that changing a filter in the source table causes this filter + // to remove nothing (instead of remove everything). + totalTables[0].findColumn("J__Avg").filter().notEq(FilterValue.ofNumber(8.0)) + }); + totalTables[1].applyFilter(new FilterCondition[]{ + totalTables[1].findColumn("J__Avg").filter().eq(FilterValue.ofNumber(5.0)) + }); + totalTables[0].setViewport(0, 100, null, null); + totalTables[1].setViewport(0, 100, null, null); + + return promiseAllThen(table, + totalPromises[0] = waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, + checkTotals(totalTables[0], "b1", TotalsResults.of(2, 2, 5, 1)), 1503), + totalPromises[1] = waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, + checkTotals(totalTables[1], "b2", TotalsResults.of(2, 2, 5, 1)), 1504) + ); + }) + .then(table -> { + // forcibly disconnect the worker and test that the total table come back up, and respond to re-filtering. + table.getConnection().forceReconnect(); + return Promise.resolve(table); + }) + .then(table->waitForEvent(table, JsTable.EVENT_RECONNECT, 5001).onInvoke(table)) + .then(table -> promiseAllThen(table, + totalPromises[0] = waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, + checkTotals(totalTables[0], "c1", TotalsResults.of(2, 2, 5, 1)), 2505), + totalPromises[1] = waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, + checkTotals(totalTables[1], "c2", TotalsResults.of(2, 2, 5, 1)), 2506) + )) + .then(table -> { + // Now... refilter the original table, and assert that the totals tables update. + table.applyFilter(new FilterCondition[]{ + table.findColumn("J").filter().notEq(FilterValue.ofNumber(9.0)) + // the != filters on the regular totals will no longer remove anything w/ updated source filter.... + // but grand totals will ignore this, and still be filtered + }); + table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + + return promiseAllThen(table, + waitForEvent(table, JsTable.EVENT_FILTERCHANGED, 5503).onInvoke(table), + waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, checkTotals(totalTables[1], "d1", + TotalsResults.of(3, 3, 6.666666, 0.0), + TotalsResults.of(1, 1, 1, 1.0) + ), 2507), + totalPromises[1] = waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, + checkTotals(totalTables[1], "d2", TotalsResults.of(2, 2, 5, 1)), 2508) + ); + }) + .then(this::finish).catch_(this::report); + } + + public void testGroupedTotals() { + connect(tables) + .then(session -> { + delayFinish(2000); + return session.getTable("hasTotals", true); + }) + .then(table -> { + // take the existing config and group by K + JsTotalsTableConfig config = table.getTotalsTableConfig(); + config.groupBy = new JsArray<>("K"); + + // use the same check for both totals and grand totals tables + IThenable.ThenOnFulfilledCallbackFn checkForBothTotalsTables = (JsTotalsTable totals) -> { + assertEquals(4, totals.getColumns().length); + assertEquals(2, totals.getSize(), DELTA); + totals.setViewport(0, 100, null, null); + + //confirm the grand totals are unchanged + return waitForEvent(totals, JsTable.EVENT_UPDATED, update -> { + ViewportData viewportData = (ViewportData) update.detail; + + //2 rows (one for k=0, one for k=1) + assertEquals(2, viewportData.getRows().length); + //4 columns (3 agg'd, and the grouped column) + assertEquals(4, viewportData.getColumns().length); + + //k=0 row + assertEquals(0, viewportData.getRows().getAt(0).get(totals.findColumn("K")).asInt()); + assertEquals(3, viewportData.getRows().getAt(0).get(totals.findColumn("I")).cast().getWrapped()); + assertEquals(6.666666, viewportData.getRows().getAt(0).get(totals.findColumn("J__Avg")).asDouble(), DELTA); + assertEquals(0.0, viewportData.getRows().getAt(0).get(totals.findColumn("J__Min")).asDouble()); + + //k=1 row + assertEquals(1, viewportData.getRows().getAt(1).get(totals.findColumn("K")).asInt()); + assertEquals(2, viewportData.getRows().getAt(1).get(totals.findColumn("I")).cast().getWrapped()); + assertEquals(5.0, viewportData.getRows().getAt(1).get(totals.findColumn("J__Avg")).asDouble(), DELTA); + assertEquals(1.0, viewportData.getRows().getAt(1).get(totals.findColumn("J__Min")).asDouble()); + }, 1500); + }; + + //noinspection unchecked + return Promise.all((Promise[])new Promise[] { + table.getTotalsTable(config).then(checkForBothTotalsTables), + table.getGrandTotalsTable(config).then(checkForBothTotalsTables) + }); + }) + .then(this::finish).catch_(this::report); + } + + private static class TotalsResults { + int k; + long i; + double avg; + double min; + + public TotalsResults(int k, long i, double avg, double min) { + this.k = k; + this.i = i; + this.avg = avg; + this.min = min; + } + + static TotalsResults of(int k, long i, double avg, double min) { + return new TotalsResults(k, i, avg, min); + } + } + + private Consumer checkTotals( + JsTotalsTable totals, + long i, + double avg, + double min, + String messages + ) { + String ext = messages; + return update -> { + ViewportData viewportData = (ViewportData) update.detail; + + assertEquals(1, viewportData.getRows().length); + assertEquals(3, viewportData.getColumns().length); + + assertEquals("I" + ext, i, viewportData.getRows().getAt(0).get(totals.findColumn("I")).cast().getWrapped()); + assertEquals("J_Avg" + ext, avg, viewportData.getRows().getAt(0).get(totals.findColumn("J__Avg")).asDouble(), 0.0001); + assertEquals("J_Min" + ext, min, viewportData.getRows().getAt(0).get(totals.findColumn("J__Min")).asDouble(), 0.0001); + + }; + } + private Consumer checkTotals( + JsTotalsTable totals, + String messages, + TotalsResults ... expected + ) { + String ext = messages; + return update -> { + ViewportData viewportData = (ViewportData) update.detail; + + assertEquals("Viewport data rows", expected.length, viewportData.getRows().length); + assertEquals("Viewport columns", 4, viewportData.getColumns().length); + + for (int ind = 0; ind < expected.length; ind ++ ) { + final TotalsResults result = expected[ind]; + assertEquals("K" + ext, result.k, viewportData.getRows().getAt(ind).get(totals.findColumn("K")).cast().getWrapped()); + assertEquals("I" + ext, result.i, viewportData.getRows().getAt(ind).get(totals.findColumn("I")).cast().getWrapped()); + assertEquals("J_Avg" + ext, result.avg, viewportData.getRows().getAt(ind).get(totals.findColumn("J__Avg")).asDouble(), 0.0001); + assertEquals("J_Min" + ext, result.min, viewportData.getRows().getAt(ind).get(totals.findColumn("J__Min")).asDouble(), 0.0001); + } + + }; + } + + /** + * Specialized waitForEvent since JsTotalsTable isn't a HasEventHandling subtype, and doesnt make sense to + * shoehorn it in just for tests. + */ + private Promise waitForEvent(JsTotalsTable table, String eventName, Consumer check, int timeout) { + return waitForEvent(table.getWrappedTable(), eventName, check::accept, timeout) + .then(t->Promise.resolve(table)); + } + + @Override + public String getModuleName() { + return "io.deephaven.web.DeephavenIntegrationTest"; + } +} From d9da1cad79600c3d7a96d20008a95d7cf4c72f71 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Thu, 21 Dec 2023 14:06:23 -0600 Subject: [PATCH 13/31] Attempt at reusing code from gwt-core test --- web/client-api/client-api.gradle | 5 +- .../web/junit/RunStyleRemoteWebDriver.java | 196 ++++++++++++++++++ 2 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index d902c655467..f050d5e5595 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -19,6 +19,8 @@ dependencies { implementation 'com.vertispan.nio:gwt-nio:1.0-alpha-1' js project(path: ':proto:raw-js-openapi', configuration: 'js') + + testImplementation 'org.seleniumhq.selenium:selenium-remote-driver:4.16.1' } Classpaths.inheritElemental(project, 'elemental2-core', 'implementation') Classpaths.inheritElemental(project, 'elemental2-promise', 'implementation') @@ -66,7 +68,8 @@ deephavenDocker { tasks.register('gwtIntegrationTest', Test) { t -> t.dependsOn(deephavenDocker.portTask) doFirst { - t.systemProperty('gwt.args', '-runStyle Manual:1 -ea -style PRETTY -setProperty dh.server=http://localhost:' + deephavenDocker.port.get()) + def webdriverUrl = 'http://localhost:4444' + t.systemProperty('gwt.args', "-runStyle io.deephaven.web.junit.RunStyleRemoteWebDriver:webdriverUrl:firefox -ea -style PRETTY -setProperty dh.server=http://localhost:${deephavenDocker.port.get()}") t.classpath += tasks.getByName('gwtCompile').src t.classpath.files.each { println it diff --git a/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java b/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java new file mode 100644 index 00000000000..01351f59fc2 --- /dev/null +++ b/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java @@ -0,0 +1,196 @@ +package io.deephaven.web.junit; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.junit.JUnitShell; +import com.google.gwt.junit.RunStyle; +import org.openqa.selenium.Platform; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.remote.RemoteWebDriver; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class RunStyleRemoteWebDriver extends RunStyle { + + public static class RemoteWebDriverConfiguration { + private String remoteWebDriverUrl; + private List> browserCapabilities; + + public String getRemoteWebDriverUrl() { + return remoteWebDriverUrl; + } + + public void setRemoteWebDriverUrl(String remoteWebDriverUrl) { + this.remoteWebDriverUrl = remoteWebDriverUrl; + } + + public List> getBrowserCapabilities() { + return browserCapabilities; + } + + public void setBrowserCapabilities(List> browserCapabilities) { + this.browserCapabilities = browserCapabilities; + } + } + + public class ConfigurationException extends Exception {} + + private List browsers = new ArrayList<>(); + private Thread keepalive; + + public RunStyleRemoteWebDriver(JUnitShell shell) { + super(shell); + } + + /** + * Validates the arguments for the specific subclass, and creates a configuration that describes + * how to run the tests. + * + * @param args the command line argument string passed from JUnitShell + * @return the configuration to use when running these tests + */ + protected RemoteWebDriverConfiguration readConfiguration(String args) + throws ConfigurationException { + RemoteWebDriverConfiguration config = new RemoteWebDriverConfiguration(); + if (args == null || args.length() == 0) { + getLogger() + .log( + TreeLogger.ERROR, + "RemoteWebDriver runstyle requires a parameter of the form protocol://hostname:port?browser1[,browser2]"); + throw new ConfigurationException(); + } + + String[] parts = args.split("\\?"); + String url = parts[0]; + URL remoteAddress = null; + try { + remoteAddress = new URL(url); + if (remoteAddress.getPath().equals("") + || (remoteAddress.getPath().equals("/") && !url.endsWith("/"))) { + getLogger() + .log( + TreeLogger.INFO, + "No path specified in webdriver remote url, using default of /wd/hub"); + config.setRemoteWebDriverUrl(url + "/wd/hub"); + } else { + config.setRemoteWebDriverUrl(url); + } + } catch (MalformedURLException e) { + getLogger().log(TreeLogger.ERROR, e.getMessage(), e); + throw new ConfigurationException(); + } + + // build each driver based on parts[1].split(",") + String[] browserNames = parts[1].split(","); + config.setBrowserCapabilities(new ArrayList<>()); + for (String browserName : browserNames) { + DesiredCapabilities capabilities = new DesiredCapabilities(browserName, "", Platform.ANY); + config.getBrowserCapabilities().add(capabilities.asMap()); + } + + return config; + } + + + @Override + public final int initialize(String args) { + final RemoteWebDriverConfiguration config; + try { + config = readConfiguration(args); + } catch (ConfigurationException failed) { + // log should already have details about what went wrong, we will just return the failure + // value + return -1; + } + + final URL remoteAddress; + try { + remoteAddress = new URL(config.getRemoteWebDriverUrl()); + } catch (MalformedURLException e) { + getLogger().log(TreeLogger.ERROR, e.getMessage(), e); + return -1; + } + + for (Map capabilityMap : config.getBrowserCapabilities()) { + DesiredCapabilities capabilities = new DesiredCapabilities(capabilityMap); + + try { + RemoteWebDriver wd = new RemoteWebDriver(remoteAddress, capabilities); + browsers.add(wd); + } catch (Exception exception) { + getLogger().log(TreeLogger.ERROR, "Failed to find desired browser", exception); + return -1; + } + } + + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + if (keepalive != null) { + keepalive.interrupt(); + } + for (RemoteWebDriver browser : browsers) { + try { + browser.quit(); + } catch (Exception ignored) { + // ignore, we're shutting down, continue shutting down others + } + } + })); + return browsers.size(); + } + + @Override + public void launchModule(String moduleName) throws UnableToCompleteException { + // since WebDriver.get is blocking, start a keepalive thread first + keepalive = + new Thread( + () -> { + while (true) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + break; + } + for (RemoteWebDriver browser : browsers) { + browser.getTitle(); // as in RunStyleSelenium, simple way to poll the browser + } + } + }); + keepalive.setDaemon(true); + keepalive.start(); + for (RemoteWebDriver browser : browsers) { + browser.get(shell.getModuleUrl(moduleName)); + } + } + + /** + * Work-around until GWT's JUnitShell handles IPv6 addresses correctly. + * https://groups.google.com/d/msg/google-web-toolkit/jLGhwUrKVRY/eQaDO6EUqdYJ + */ + public String getLocalHostName() { + String host = System.getProperty("webdriver.test.host"); + if (host != null) { + return host; + } + InetAddress a; + try { + a = InetAddress.getLocalHost(); + } catch (UnknownHostException e) { + throw new RuntimeException("Unable to determine my ip address", e); + } + if (a instanceof Inet6Address) { + return "[" + a.getHostAddress() + "]"; + } else { + return a.getHostAddress(); + } + } +} From 6e6326d7c30a98f6d59b3b0ae348f41664fbaa09 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Sat, 23 Dec 2023 14:18:00 -0600 Subject: [PATCH 14/31] Nearly working tests from gradle, with TODOs --- web/client-api/client-api.gradle | 45 ++++++++++++++++--- .../web/junit/RunStyleRemoteWebDriver.java | 6 +-- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index f050d5e5595..1154d71a800 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -1,3 +1,8 @@ +import com.bmuschko.gradle.docker.tasks.container.DockerCreateContainer +import com.bmuschko.gradle.docker.tasks.container.DockerRemoveContainer +import com.bmuschko.gradle.docker.tasks.container.DockerStartContainer +import io.deephaven.tools.docker.WaitForHealthyContainer + plugins { id 'io.deephaven.project.register' id 'io.deephaven.deephaven-in-docker' @@ -65,15 +70,43 @@ deephavenDocker { networkName.set "js-test-network-${randomSuffix}" } +def seleniumContainerId = "selenium-${randomSuffix}" + +def createSelenium = tasks.register('createSelenium', DockerCreateContainer) { t -> + t.containerName.set(seleniumContainerId) + t.targetImageId('selenium/standalone-firefox:4.16.1-20231219')// TODO create a registry entry for this + t.hostConfig.shmSize.set(2L * 1024 * 1024 * 1024) + t.hostConfig.portBindings.set(['4444:4444', '7900:7900'])//TODO don't expose 7900, and make 4444 dynamic + t.hostConfig.network.set('host') +} +def startSelenium = tasks.register('startSelenium', DockerStartContainer) {t -> + t.dependsOn(createSelenium) + t.containerId.set(seleniumContainerId) +} +def seleniumHealthy = project.tasks.register('seleniumHealthy', WaitForHealthyContainer) { task -> + task.dependsOn startSelenium + + task.awaitStatusTimeout.set 120 + task.checkInterval.set 100 + + task.containerId.set(seleniumContainerId) +} +def stopSelenium = project.tasks.register('stopSelenium', DockerRemoveContainer) { task -> + task.dependsOn startSelenium + task.targetContainerId seleniumContainerId + task.force.set true + task.removeVolumes.set true +} +// before/after: +// docker run -d -p 4444:4444 -p 7900:7900 --shm-size="2g" --net=host selenium/standalone-firefox:4.16.1-20231219 + tasks.register('gwtIntegrationTest', Test) { t -> - t.dependsOn(deephavenDocker.portTask) + t.dependsOn(deephavenDocker.portTask, seleniumHealthy) + t.finalizedBy(deephavenDocker.endTask, stopSelenium) doFirst { - def webdriverUrl = 'http://localhost:4444' - t.systemProperty('gwt.args', "-runStyle io.deephaven.web.junit.RunStyleRemoteWebDriver:webdriverUrl:firefox -ea -style PRETTY -setProperty dh.server=http://localhost:${deephavenDocker.port.get()}") + def webdriverUrl = 'http://localhost:4444/' + t.systemProperty('gwt.args', "-runStyle io.deephaven.web.junit.RunStyleRemoteWebDriver:${webdriverUrl}?firefox -ea -style PRETTY -setProperty dh.server=http://localhost:${deephavenDocker.port.get()}") t.classpath += tasks.getByName('gwtCompile').src - t.classpath.files.each { - println it - } } t.finalizedBy(deephavenDocker.endTask) t.systemProperties = [ diff --git a/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java b/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java index 01351f59fc2..ee56e3ccc93 100644 --- a/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java +++ b/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java @@ -4,7 +4,6 @@ import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.junit.JUnitShell; import com.google.gwt.junit.RunStyle; -import org.openqa.selenium.Platform; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; @@ -91,7 +90,8 @@ protected RemoteWebDriverConfiguration readConfiguration(String args) String[] browserNames = parts[1].split(","); config.setBrowserCapabilities(new ArrayList<>()); for (String browserName : browserNames) { - DesiredCapabilities capabilities = new DesiredCapabilities(browserName, "", Platform.ANY); + DesiredCapabilities capabilities = new DesiredCapabilities(); + capabilities.setBrowserName(browserName); config.getBrowserCapabilities().add(capabilities.asMap()); } @@ -139,7 +139,7 @@ public final int initialize(String args) { } for (RemoteWebDriver browser : browsers) { try { - browser.quit(); + browser.close(); } catch (Exception ignored) { // ignore, we're shutting down, continue shutting down others } From 92e6f3a8e6b91fe6213caffc201423a281be7710 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Wed, 27 Dec 2023 11:22:30 -0600 Subject: [PATCH 15/31] Checkpoint commit, gwt test tasks pass, check fails --- buildSrc/src/main/groovy/GwtTools.groovy | 24 +- web/client-api/client-api.gradle | 14 +- .../web/ClientIntegrationTestSuite.java | 2 +- .../deephaven/web/DeephavenUnitTest.gwt.xml | 6 + .../web/client/api/NullValueTestGwt.java | 19 +- .../web/client/api/TotalsTableTestGwt.java | 296 +++++++++--------- .../api/i18n/JsDateTimeFormatTestGwt.java | 2 +- .../api/i18n/JsNumberFormatTestGwt.java | 2 +- .../web/junit/RunStyleRemoteWebDriver.java | 11 +- 9 files changed, 191 insertions(+), 185 deletions(-) diff --git a/buildSrc/src/main/groovy/GwtTools.groovy b/buildSrc/src/main/groovy/GwtTools.groovy index 3a9063bfffa..78fc4ce88c6 100644 --- a/buildSrc/src/main/groovy/GwtTools.groovy +++ b/buildSrc/src/main/groovy/GwtTools.groovy @@ -1,6 +1,7 @@ import de.esoco.gwt.gradle.GwtLibPlugin import de.esoco.gwt.gradle.GwtPlugin import de.esoco.gwt.gradle.extension.GwtExtension +import de.esoco.gwt.gradle.task.GwtCheckTask import de.esoco.gwt.gradle.task.GwtCompileTask import groovy.transform.CompileStatic import org.gradle.api.Project @@ -36,6 +37,9 @@ class GwtTools { GwtCompileTask gwtc -> applyModuleSettings p, gwtc, module,description } + p.tasks.withType(GwtCheckTask).configureEach {t -> + t.onlyIf { false } + } return ext } @@ -70,8 +74,6 @@ class GwtTools { gwtDev && gwtc.doFirst { gwtc.logger.quiet('Running in gwt dev mode; saving source to {}/dh/src', extras) } - - p.tasks.findByName('gwtCheck')?.enabled = false } static void applyDefaults(Project p, GwtExtension gwt, boolean compile = false) { @@ -110,24 +112,6 @@ class GwtTools { maxHeapSize = "1024m" minHeapSize = "512m" } - - gwt.dev.with { - /** The ip address of the code server. */ - bindAddress = "127.0.0.1" - /** The port where the code server will run. */ - port = 9876 - /** Specifies Java source level ("1.6", "1.7"). - sourceLevel = "1.8" - /** The level of logging detail (ERROR, WARN, INFO, TRACE, DEBUG, SPAM, ALL) */ - logLevel = "INFO" - /** Emit extra information allow chrome dev tools to display Java identifiers in many placesinstead of JavaScript functions. (NONE, ONLY_METHOD_NAME, ABBREVIATED, FULL) */ - methodNameDisplayMode = "NONE" - /** Where to write output files */ - war = warPath -// extraArgs = ["-firstArgument", "-secondArgument"] - } - - } } diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index 1154d71a800..894b5d6f25c 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -50,10 +50,10 @@ artifacts { } } -tasks.register('gwtUnitTest', Test) { t -> +def gwtUnitTest = tasks.register('gwtUnitTest', Test) { t -> t.systemProperties = [ - 'gwt.args':'-runStyle HtmlUnit -ea -style PRETTY', - 'gwt.persistentunitcachedir':layout.buildDirectory.dir('integrationTest-unitCache').get().asFile.absolutePath, + 'gwt.args': "-runStyle HtmlUnit -ea -style PRETTY -war ${layout.buildDirectory.dir('unitTest-war').get().asFile.absolutePath}", + 'gwt.persistentunitcachedir':layout.buildDirectory.dir('unitTest-unitCache').get().asFile.absolutePath, ] t.include '**/ClientUnitTestSuite.class' t.useJUnit() @@ -100,12 +100,12 @@ def stopSelenium = project.tasks.register('stopSelenium', DockerRemoveContainer) // before/after: // docker run -d -p 4444:4444 -p 7900:7900 --shm-size="2g" --net=host selenium/standalone-firefox:4.16.1-20231219 -tasks.register('gwtIntegrationTest', Test) { t -> +def gwtIntegrationTest = tasks.register('gwtIntegrationTest', Test) { t -> t.dependsOn(deephavenDocker.portTask, seleniumHealthy) t.finalizedBy(deephavenDocker.endTask, stopSelenium) doFirst { def webdriverUrl = 'http://localhost:4444/' - t.systemProperty('gwt.args', "-runStyle io.deephaven.web.junit.RunStyleRemoteWebDriver:${webdriverUrl}?firefox -ea -style PRETTY -setProperty dh.server=http://localhost:${deephavenDocker.port.get()}") + t.systemProperty('gwt.args', "-runStyle io.deephaven.web.junit.RunStyleRemoteWebDriver:${webdriverUrl}?firefox -ea -style PRETTY -setProperty dh.server=http://localhost:${deephavenDocker.port.get()} -war ${layout.buildDirectory.dir('unitTest-war').get().asFile.absolutePath}") t.classpath += tasks.getByName('gwtCompile').src } t.finalizedBy(deephavenDocker.endTask) @@ -117,8 +117,8 @@ tasks.register('gwtIntegrationTest', Test) { t -> t.scanForTestClasses = false } -project.tasks.getByName('quick').dependsOn project.tasks.withType(de.esoco.gwt.gradle.task.GwtCompileTask) +project.tasks.getByName('quick').dependsOn(gwtUnitTest, gwtIntegrationTest) -tasks.getByName('check').dependsOn(tasks.withType(Test)) +tasks.getByName('check').dependsOn(gwtUnitTest, gwtIntegrationTest) apply from: "$rootDir/gradle/web-gwt-test.gradle" diff --git a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java index 9695a60ae93..89091f55d0f 100644 --- a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java +++ b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java @@ -22,7 +22,7 @@ public static Test suite() { suite.addTestSuite(TableManipulationTestGwt.class); suite.addTestSuite(ConcurrentTableTestGwt.class); suite.addTestSuite(NullValueTestGwt.class); -// suite.addTestSuite(TotalsTableTestGwt.class); + // suite.addTestSuite(TotalsTableTestGwt.class); return suite; } diff --git a/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml b/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml index 617e28a1095..5368885ac79 100644 --- a/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml +++ b/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml @@ -3,4 +3,10 @@ + + + + + + \ No newline at end of file diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/NullValueTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/NullValueTestGwt.java index 8cf67db724d..f2815d2c022 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/NullValueTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/NullValueTestGwt.java @@ -21,6 +21,7 @@ public class NullValueTestGwt extends AbstractAsyncGwtTestCase { " \"MyBoolean=i==0?null:true\",\n" + " \"MyDate=i==0?null:epochNanosToInstant(i)\"\n" + "])"); + public void testNullTable() { connect(tables) .then(table("nulltable")) @@ -58,14 +59,16 @@ public void testNullTable() { ViewportRow valueRow = rows.getAt(1); assertEquals(1, valueRow.get(table.findColumn("MyInt")).asInt()); - assertEquals((long)1, valueRow.get(table.findColumn("MyLong")).cast().getWrapped()); - assertEquals((double)1, valueRow.get(table.findColumn("MyDouble")).asDouble()); - assertEquals((short)1, valueRow.get(table.findColumn("MyShort")).asShort()); - assertEquals((float)1., valueRow.get(table.findColumn("MyFloat")).asFloat()); - assertEquals((char)1, valueRow.get(table.findColumn("MyChar")).asChar()); - assertEquals((byte)1, valueRow.get(table.findColumn("MyByte")).asByte()); + assertEquals((long) 1, + valueRow.get(table.findColumn("MyLong")).cast().getWrapped()); + assertEquals((double) 1, valueRow.get(table.findColumn("MyDouble")).asDouble()); + assertEquals((short) 1, valueRow.get(table.findColumn("MyShort")).asShort()); + assertEquals((float) 1., valueRow.get(table.findColumn("MyFloat")).asFloat()); + assertEquals((char) 1, valueRow.get(table.findColumn("MyChar")).asChar()); + assertEquals((byte) 1, valueRow.get(table.findColumn("MyByte")).asByte()); assertEquals(true, valueRow.get(table.findColumn("MyBoolean")).asBoolean()); - assertEquals((long)1, valueRow.get(table.findColumn("MyDate")).cast().getWrapped()); + assertEquals((long) 1, + valueRow.get(table.findColumn("MyDate")).cast().getWrapped()); }, 1000); }) .then(this::finish).catch_(this::report); @@ -75,4 +78,4 @@ public void testNullTable() { public String getModuleName() { return "io.deephaven.web.DeephavenIntegrationTest"; } -} \ No newline at end of file +} diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/TotalsTableTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/TotalsTableTestGwt.java index facf1b1fbb2..f42e7063fd1 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/TotalsTableTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/TotalsTableTestGwt.java @@ -17,20 +17,21 @@ public class TotalsTableTestGwt extends AbstractAsyncGwtTestCase { .script("from deephaven import empty_table") .script("strings", "empty_table(1).update([\"str=`abcdefg`\", \"nostr=(String)null\"])") .script("hasTotals", - "empty_table(5).update_view([(\"I = (double)i\", \"J = (double) i * i\", \"K = (double) i % 2\")])" + + "empty_table(5).update_view([(\"I = (double)i\", \"J = (double) i * i\", \"K = (double) i % 2\")])" + + ".with_attributes({'TotalsTable':'false,false,Count;J=Min:Avg,K=Skip,;'})"); public void testQueryDefinedConfigs() { connect(tables) .then(session -> { - session.getTable("strings", true) + session.getTable("strings", true) .then(table1 -> { - //make sure the config is null, since it wasn't defined in the config + // make sure the config is null, since it wasn't defined in the config assertNull(table1.getTotalsTableConfig()); - //check that we get a totals table back, even though it won't have any columns - //noinspection unchecked - return (Promise) Promise.all(new Promise[]{ + // check that we get a totals table back, even though it won't have any columns + // noinspection unchecked + return (Promise) Promise.all(new Promise[] { table1.getTotalsTable(null).then(totals1 -> { assertEquals(0, totals1.getColumns().length); assertEquals(1, totals1.getSize(), DELTA); @@ -47,7 +48,7 @@ public void testQueryDefinedConfigs() { }) .then(table("hasTotals")) .then(table -> { - //make sure the config is null, since it wasn't defined in the config + // make sure the config is null, since it wasn't defined in the config assertNotNull(table.getTotalsTableConfig()); assertEquals("Count", table.getTotalsTableConfig().defaultOperation); assertTrue(table.getTotalsTableConfig().operationMap.has("K")); @@ -61,25 +62,27 @@ public void testQueryDefinedConfigs() { assertFalse(table.getTotalsTableConfig().operationMap.has("I")); - //check that we get a totals table back, even though it won't have any columns - //noinspection unchecked - return Promise.all((Promise[])new Promise[] { + // check that we get a totals table back, even though it won't have any columns + // noinspection unchecked + return Promise.all((Promise[]) new Promise[] { table.getTotalsTable(null) .then(totals -> { - assertEquals(3, totals.getColumns().length); - assertEquals(1, totals.getSize(), DELTA); - totals.setViewport(0, 100, null, null); + assertEquals(3, totals.getColumns().length); + assertEquals(1, totals.getSize(), DELTA); + totals.setViewport(0, 100, null, null); - return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals( totals,5, 6., 0, "a1"), 1500); - }), + return waitForEvent(totals, JsTable.EVENT_UPDATED, + checkTotals(totals, 5, 6., 0, "a1"), 1500); + }), table.getGrandTotalsTable(null) .then(totals -> { - assertEquals(3, totals.getColumns().length); - assertEquals(1, totals.getSize(), DELTA); - totals.setViewport(0, 100, null, null); + assertEquals(3, totals.getColumns().length); + assertEquals(1, totals.getSize(), DELTA); + totals.setViewport(0, 100, null, null); - return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals(totals, 5, 6.0, 0., "a2"), 1500); - }) + return waitForEvent(totals, JsTable.EVENT_UPDATED, + checkTotals(totals, 5, 6.0, 0., "a2"), 1500); + }) }); }) .then(this::finish).catch_(this::report); @@ -93,78 +96,77 @@ public void ignore_testTotalsOnFilteredTable() { .then(table("hasTotals")) .then(table -> { delayTestFinish(8000); - table.applyFilter(new FilterCondition[]{ + table.applyFilter(new FilterCondition[] { table.findColumn("K").filter().eq(FilterValue.ofNumber(0.0)) }); - table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + table.setViewport(0, 100, null);// not strictly required, but part of the normal usage return waitForEvent(table, JsTable.EVENT_FILTERCHANGED, 2001).onInvoke(table); }) - .then(table -> - promiseAllThen(table, - table.getTotalsTable(null) - .then(totals -> { - totalTables[0] = totals; - assertEquals(3, totals.getColumns().length); - assertEquals(1, totals.getSize(), DELTA); - totals.setViewport(0, 100, null, null); - - //confirm the normal totals match the filtered data - return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals(totals, 3, 6.666666, 0.0, "a1"), 1501); - }), - table.getGrandTotalsTable(null) - .then(totals -> { - totalTables[1] = totals; - assertEquals(3, totals.getColumns().length); - assertEquals(1, totals.getSize(), DELTA); - totals.setViewport(0, 100, null, null); - - //confirm the grand totals are unchanged - return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals(totals, 5, 6., 0., "a2"), 1502); - })) - ) + .then(table -> promiseAllThen(table, + table.getTotalsTable(null) + .then(totals -> { + totalTables[0] = totals; + assertEquals(3, totals.getColumns().length); + assertEquals(1, totals.getSize(), DELTA); + totals.setViewport(0, 100, null, null); + + // confirm the normal totals match the filtered data + return waitForEvent(totals, JsTable.EVENT_UPDATED, + checkTotals(totals, 3, 6.666666, 0.0, "a1"), 1501); + }), + table.getGrandTotalsTable(null) + .then(totals -> { + totalTables[1] = totals; + assertEquals(3, totals.getColumns().length); + assertEquals(1, totals.getSize(), DELTA); + totals.setViewport(0, 100, null, null); + + // confirm the grand totals are unchanged + return waitForEvent(totals, JsTable.EVENT_UPDATED, + checkTotals(totals, 5, 6., 0., "a2"), 1502); + }))) .then(table -> { - // Now, change the filter on the original table, and expect the totals tables to automatically update. + // Now, change the filter on the original table, and expect the totals tables to automatically + // update. - table.applyFilter(new FilterCondition[]{ + table.applyFilter(new FilterCondition[] { table.findColumn("K").filter().eq(FilterValue.ofNumber(1.0)) }); - table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + table.setViewport(0, 100, null);// not strictly required, but part of the normal usage return promiseAllThen(table, waitForEvent(table, JsTable.EVENT_FILTERCHANGED, 2002).onInvoke(table), totalPromises[0] = waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, checkTotals(totalTables[0], 2, 5, 1, "b1"), 1503), totalPromises[1] = waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, - checkTotals(totalTables[1], 5, 6, 0, "b2"), 1504) - ); + checkTotals(totalTables[1], 5, 6, 0, "b2"), 1504)); }) .then(table -> { - // forcibly disconnect the worker and test that the total table come back up, and respond to re-filtering. + // forcibly disconnect the worker and test that the total table come back up, and respond to + // re-filtering. table.getConnection().forceReconnect(); return Promise.resolve(table); }) - .then(table->waitForEvent(table, JsTable.EVENT_RECONNECT, 5001).onInvoke(table)) + .then(table -> waitForEvent(table, JsTable.EVENT_RECONNECT, 5001).onInvoke(table)) .then(table -> promiseAllThen(table, waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, checkTotals(totalTables[0], 2, 5, 1, "c1"), 7505), waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, - checkTotals(totalTables[1], 5, 6, 0, "c2"), 7506) - )) + checkTotals(totalTables[1], 5, 6, 0, "c2"), 7506))) .then(table -> { // Now... refilter the original table, and assert that the totals tables update. - table.applyFilter(new FilterCondition[]{ + table.applyFilter(new FilterCondition[] { table.findColumn("K").filter().eq(FilterValue.ofNumber(0.0)) }); - table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + table.setViewport(0, 100, null);// not strictly required, but part of the normal usage return promiseAllThen(table, waitForEvent(table, JsTable.EVENT_FILTERCHANGED, 2003).onInvoke(table), waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, checkTotals(totalTables[0], 3, 6.666666, 0.0, "d1"), 1507), waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, - checkTotals(totalTables[1], 5, 6., 0., "d2"), 1508) - ); + checkTotals(totalTables[1], 5, 6., 0., "d2"), 1508)); }) .then(this::finish).catch_(this::report); } @@ -175,10 +177,10 @@ public void testClosingTotalsWhileClearingFilter() { .then(table("hasTotals")) .then(table -> { delayTestFinish(8000); - table.applyFilter(new FilterCondition[]{ + table.applyFilter(new FilterCondition[] { table.findColumn("K").filter().eq(FilterValue.ofNumber(0.0)) }); - table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + table.setViewport(0, 100, null);// not strictly required, but part of the normal usage return waitForEvent(table, JsTable.EVENT_UPDATED, 2001).onInvoke(table); }) @@ -186,14 +188,13 @@ public void testClosingTotalsWhileClearingFilter() { .then(totals -> { totalTables[0] = totals; return Promise.resolve(table); - }) - ) + })) .then(table -> { // Now, clear the filter on the original table, and close the totals table at the same time - table.applyFilter(new FilterCondition[]{}); + table.applyFilter(new FilterCondition[] {}); // Close the table in the same step as applying the filter. Should not throw an error. totalTables[0].close(); - table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + table.setViewport(0, 100, null);// not strictly required, but part of the normal usage return waitForEvent(table, JsTable.EVENT_UPDATED, 2002).onInvoke(table); }) @@ -208,18 +209,14 @@ public void ignore_testFilteringTotalsTable() { .then(table("hasTotals")) .then(table -> { delayTestFinish(8000); - /* Here is the base table: - I J K - 0 0 0 - 1 1 1 - 2 4 0 // we are going to remove this row, to test source table filtering. - 3 9 1 - 4 16 0 + /* + * Here is the base table: I J K 0 0 0 1 1 1 2 4 0 // we are going to remove this row, to test + * source table filtering. 3 9 1 4 16 0 */ - table.applyFilter(new FilterCondition[]{ + table.applyFilter(new FilterCondition[] { table.findColumn("J").filter().notEq(FilterValue.ofNumber(4.0)) }); - table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + table.setViewport(0, 100, null);// not strictly required, but part of the normal usage return waitForEvent(table, JsTable.EVENT_FILTERCHANGED, 2001).onInvoke(table); }) @@ -228,8 +225,9 @@ public void ignore_testFilteringTotalsTable() { // group by K so we can do some filtering on the output of the tables config.groupBy.push("K"); // copy over the rest of the operations set on the server, so we can reuse some assertion logic - config.operationMap.set("J", Js.uncheckedCast(new JsString[]{ toJsString("Avg"), toJsString("Min")})); -// config.operationMap.set("K", Js.uncheckedCast(new JsString[]{ toJsString("Skip")})); + config.operationMap.set("J", + Js.uncheckedCast(new JsString[] {toJsString("Avg"), toJsString("Min")})); + // config.operationMap.set("K", Js.uncheckedCast(new JsString[]{ toJsString("Skip")})); config.defaultOperation = "Count"; return promiseAllThen(table, table.getTotalsTable(config) @@ -239,11 +237,10 @@ public void ignore_testFilteringTotalsTable() { assertEquals(2, totals.getSize(), DELTA); totals.setViewport(0, 100, null, null); - //confirm the normal totals match the filtered data + // confirm the normal totals match the filtered data return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals(totals, "a1", TotalsResults.of(2, 2, 8, 0.0), - TotalsResults.of(2, 2, 5, 1.0) - ), 1501); + TotalsResults.of(2, 2, 5, 1.0)), 1501); }), table.getGrandTotalsTable(config) .then(totals -> { @@ -252,22 +249,21 @@ public void ignore_testFilteringTotalsTable() { assertEquals(2, totals.getSize(), DELTA); totals.setViewport(0, 100, null, null); - //confirm the grand totals include the missing row... + // confirm the grand totals include the missing row... return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals(totals, "a2", TotalsResults.of(3, 3, 6.66666, 0.0), - TotalsResults.of(2, 2, 5, 1.0) - ), 1502); + TotalsResults.of(2, 2, 5, 1.0)), 1502); })); }) .then(table -> { // Now, apply a filter to each totals tables... - totalTables[0].applyFilter(new FilterCondition[]{ + totalTables[0].applyFilter(new FilterCondition[] { // we'll use notEq here, so that changing a filter in the source table causes this filter // to remove nothing (instead of remove everything). totalTables[0].findColumn("J__Avg").filter().notEq(FilterValue.ofNumber(8.0)) }); - totalTables[1].applyFilter(new FilterCondition[]{ + totalTables[1].applyFilter(new FilterCondition[] { totalTables[1].findColumn("J__Avg").filter().eq(FilterValue.ofNumber(5.0)) }); totalTables[0].setViewport(0, 100, null, null); @@ -277,39 +273,37 @@ public void ignore_testFilteringTotalsTable() { totalPromises[0] = waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, checkTotals(totalTables[0], "b1", TotalsResults.of(2, 2, 5, 1)), 1503), totalPromises[1] = waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, - checkTotals(totalTables[1], "b2", TotalsResults.of(2, 2, 5, 1)), 1504) - ); + checkTotals(totalTables[1], "b2", TotalsResults.of(2, 2, 5, 1)), 1504)); }) .then(table -> { - // forcibly disconnect the worker and test that the total table come back up, and respond to re-filtering. + // forcibly disconnect the worker and test that the total table come back up, and respond to + // re-filtering. table.getConnection().forceReconnect(); return Promise.resolve(table); }) - .then(table->waitForEvent(table, JsTable.EVENT_RECONNECT, 5001).onInvoke(table)) + .then(table -> waitForEvent(table, JsTable.EVENT_RECONNECT, 5001).onInvoke(table)) .then(table -> promiseAllThen(table, totalPromises[0] = waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, checkTotals(totalTables[0], "c1", TotalsResults.of(2, 2, 5, 1)), 2505), totalPromises[1] = waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, - checkTotals(totalTables[1], "c2", TotalsResults.of(2, 2, 5, 1)), 2506) - )) + checkTotals(totalTables[1], "c2", TotalsResults.of(2, 2, 5, 1)), 2506))) .then(table -> { // Now... refilter the original table, and assert that the totals tables update. - table.applyFilter(new FilterCondition[]{ + table.applyFilter(new FilterCondition[] { table.findColumn("J").filter().notEq(FilterValue.ofNumber(9.0)) - // the != filters on the regular totals will no longer remove anything w/ updated source filter.... + // the != filters on the regular totals will no longer remove anything w/ updated source + // filter.... // but grand totals will ignore this, and still be filtered }); - table.setViewport(0, 100, null);//not strictly required, but part of the normal usage + table.setViewport(0, 100, null);// not strictly required, but part of the normal usage return promiseAllThen(table, waitForEvent(table, JsTable.EVENT_FILTERCHANGED, 5503).onInvoke(table), waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, checkTotals(totalTables[1], "d1", TotalsResults.of(3, 3, 6.666666, 0.0), - TotalsResults.of(1, 1, 1, 1.0) - ), 2507), + TotalsResults.of(1, 1, 1, 1.0)), 2507), totalPromises[1] = waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, - checkTotals(totalTables[1], "d2", TotalsResults.of(2, 2, 5, 1)), 2508) - ); + checkTotals(totalTables[1], "d2", TotalsResults.of(2, 2, 5, 1)), 2508)); }) .then(this::finish).catch_(this::report); } @@ -326,36 +320,47 @@ public void testGroupedTotals() { config.groupBy = new JsArray<>("K"); // use the same check for both totals and grand totals tables - IThenable.ThenOnFulfilledCallbackFn checkForBothTotalsTables = (JsTotalsTable totals) -> { - assertEquals(4, totals.getColumns().length); - assertEquals(2, totals.getSize(), DELTA); - totals.setViewport(0, 100, null, null); - - //confirm the grand totals are unchanged - return waitForEvent(totals, JsTable.EVENT_UPDATED, update -> { - ViewportData viewportData = (ViewportData) update.detail; - - //2 rows (one for k=0, one for k=1) - assertEquals(2, viewportData.getRows().length); - //4 columns (3 agg'd, and the grouped column) - assertEquals(4, viewportData.getColumns().length); - - //k=0 row - assertEquals(0, viewportData.getRows().getAt(0).get(totals.findColumn("K")).asInt()); - assertEquals(3, viewportData.getRows().getAt(0).get(totals.findColumn("I")).cast().getWrapped()); - assertEquals(6.666666, viewportData.getRows().getAt(0).get(totals.findColumn("J__Avg")).asDouble(), DELTA); - assertEquals(0.0, viewportData.getRows().getAt(0).get(totals.findColumn("J__Min")).asDouble()); - - //k=1 row - assertEquals(1, viewportData.getRows().getAt(1).get(totals.findColumn("K")).asInt()); - assertEquals(2, viewportData.getRows().getAt(1).get(totals.findColumn("I")).cast().getWrapped()); - assertEquals(5.0, viewportData.getRows().getAt(1).get(totals.findColumn("J__Avg")).asDouble(), DELTA); - assertEquals(1.0, viewportData.getRows().getAt(1).get(totals.findColumn("J__Min")).asDouble()); - }, 1500); - }; - - //noinspection unchecked - return Promise.all((Promise[])new Promise[] { + IThenable.ThenOnFulfilledCallbackFn checkForBothTotalsTables = + (JsTotalsTable totals) -> { + assertEquals(4, totals.getColumns().length); + assertEquals(2, totals.getSize(), DELTA); + totals.setViewport(0, 100, null, null); + + // confirm the grand totals are unchanged + return waitForEvent(totals, JsTable.EVENT_UPDATED, update -> { + ViewportData viewportData = (ViewportData) update.detail; + + // 2 rows (one for k=0, one for k=1) + assertEquals(2, viewportData.getRows().length); + // 4 columns (3 agg'd, and the grouped column) + assertEquals(4, viewportData.getColumns().length); + + // k=0 row + assertEquals(0, + viewportData.getRows().getAt(0).get(totals.findColumn("K")).asInt()); + assertEquals(3, viewportData.getRows().getAt(0).get(totals.findColumn("I")) + .cast().getWrapped()); + assertEquals(6.666666, + viewportData.getRows().getAt(0).get(totals.findColumn("J__Avg")).asDouble(), + DELTA); + assertEquals(0.0, viewportData.getRows().getAt(0).get(totals.findColumn("J__Min")) + .asDouble()); + + // k=1 row + assertEquals(1, + viewportData.getRows().getAt(1).get(totals.findColumn("K")).asInt()); + assertEquals(2, viewportData.getRows().getAt(1).get(totals.findColumn("I")) + .cast().getWrapped()); + assertEquals(5.0, + viewportData.getRows().getAt(1).get(totals.findColumn("J__Avg")).asDouble(), + DELTA); + assertEquals(1.0, viewportData.getRows().getAt(1).get(totals.findColumn("J__Min")) + .asDouble()); + }, 1500); + }; + + // noinspection unchecked + return Promise.all((Promise[]) new Promise[] { table.getTotalsTable(config).then(checkForBothTotalsTables), table.getGrandTotalsTable(config).then(checkForBothTotalsTables) }); @@ -386,8 +391,7 @@ private Consumer checkTotals( long i, double avg, double min, - String messages - ) { + String messages) { String ext = messages; return update -> { ViewportData viewportData = (ViewportData) update.detail; @@ -395,17 +399,20 @@ private Consumer checkTotals( assertEquals(1, viewportData.getRows().length); assertEquals(3, viewportData.getColumns().length); - assertEquals("I" + ext, i, viewportData.getRows().getAt(0).get(totals.findColumn("I")).cast().getWrapped()); - assertEquals("J_Avg" + ext, avg, viewportData.getRows().getAt(0).get(totals.findColumn("J__Avg")).asDouble(), 0.0001); - assertEquals("J_Min" + ext, min, viewportData.getRows().getAt(0).get(totals.findColumn("J__Min")).asDouble(), 0.0001); + assertEquals("I" + ext, i, + viewportData.getRows().getAt(0).get(totals.findColumn("I")).cast().getWrapped()); + assertEquals("J_Avg" + ext, avg, + viewportData.getRows().getAt(0).get(totals.findColumn("J__Avg")).asDouble(), 0.0001); + assertEquals("J_Min" + ext, min, + viewportData.getRows().getAt(0).get(totals.findColumn("J__Min")).asDouble(), 0.0001); }; } + private Consumer checkTotals( JsTotalsTable totals, String messages, - TotalsResults ... expected - ) { + TotalsResults... expected) { String ext = messages; return update -> { ViewportData viewportData = (ViewportData) update.detail; @@ -413,24 +420,29 @@ private Consumer checkTotals( assertEquals("Viewport data rows", expected.length, viewportData.getRows().length); assertEquals("Viewport columns", 4, viewportData.getColumns().length); - for (int ind = 0; ind < expected.length; ind ++ ) { + for (int ind = 0; ind < expected.length; ind++) { final TotalsResults result = expected[ind]; - assertEquals("K" + ext, result.k, viewportData.getRows().getAt(ind).get(totals.findColumn("K")).cast().getWrapped()); - assertEquals("I" + ext, result.i, viewportData.getRows().getAt(ind).get(totals.findColumn("I")).cast().getWrapped()); - assertEquals("J_Avg" + ext, result.avg, viewportData.getRows().getAt(ind).get(totals.findColumn("J__Avg")).asDouble(), 0.0001); - assertEquals("J_Min" + ext, result.min, viewportData.getRows().getAt(ind).get(totals.findColumn("J__Min")).asDouble(), 0.0001); + assertEquals("K" + ext, result.k, + viewportData.getRows().getAt(ind).get(totals.findColumn("K")).cast().getWrapped()); + assertEquals("I" + ext, result.i, + viewportData.getRows().getAt(ind).get(totals.findColumn("I")).cast().getWrapped()); + assertEquals("J_Avg" + ext, result.avg, + viewportData.getRows().getAt(ind).get(totals.findColumn("J__Avg")).asDouble(), 0.0001); + assertEquals("J_Min" + ext, result.min, + viewportData.getRows().getAt(ind).get(totals.findColumn("J__Min")).asDouble(), 0.0001); } }; } /** - * Specialized waitForEvent since JsTotalsTable isn't a HasEventHandling subtype, and doesnt make sense to - * shoehorn it in just for tests. + * Specialized waitForEvent since JsTotalsTable isn't a HasEventHandling subtype, and doesnt make sense to shoehorn + * it in just for tests. */ - private Promise waitForEvent(JsTotalsTable table, String eventName, Consumer check, int timeout) { + private Promise waitForEvent(JsTotalsTable table, String eventName, Consumer check, + int timeout) { return waitForEvent(table.getWrappedTable(), eventName, check::accept, timeout) - .then(t->Promise.resolve(table)); + .then(t -> Promise.resolve(table)); } @Override diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsDateTimeFormatTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsDateTimeFormatTestGwt.java index 4defa31fd14..804b7a9ccd0 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsDateTimeFormatTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsDateTimeFormatTestGwt.java @@ -90,6 +90,6 @@ private long assertRoundTrip(String formatString, String input) { @Override public String getModuleName() { - return "io.deephaven.web.DeephavenApiDev"; + return "io.deephaven.web.DeephavenUnitTest"; } } diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsNumberFormatTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsNumberFormatTestGwt.java index 4c2a604e3e8..657255826a5 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsNumberFormatTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/i18n/JsNumberFormatTestGwt.java @@ -70,6 +70,6 @@ public void testLongFormat() { @Override public String getModuleName() { - return "io.deephaven.web.DeephavenApiDev"; + return "io.deephaven.web.DeephavenUnitTest"; } } diff --git a/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java b/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java index ee56e3ccc93..2d01992f45c 100644 --- a/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java +++ b/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java @@ -39,7 +39,8 @@ public void setBrowserCapabilities(List> browserCapabilities) { } } - public class ConfigurationException extends Exception {} + public class ConfigurationException extends Exception { + } private List browsers = new ArrayList<>(); private Thread keepalive; @@ -49,8 +50,8 @@ public RunStyleRemoteWebDriver(JUnitShell shell) { } /** - * Validates the arguments for the specific subclass, and creates a configuration that describes - * how to run the tests. + * Validates the arguments for the specific subclass, and creates a configuration that describes how to run the + * tests. * * @param args the command line argument string passed from JUnitShell * @return the configuration to use when running these tests @@ -173,8 +174,8 @@ public void launchModule(String moduleName) throws UnableToCompleteException { } /** - * Work-around until GWT's JUnitShell handles IPv6 addresses correctly. - * https://groups.google.com/d/msg/google-web-toolkit/jLGhwUrKVRY/eQaDO6EUqdYJ + * Workaround until GWT's + * JUnitShell handles IPv6 addresses correctly. */ public String getLocalHostName() { String host = System.getProperty("webdriver.test.host"); From 37639a00fb8448960065433f6628432c0cd66e05 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Wed, 27 Dec 2023 12:29:48 -0600 Subject: [PATCH 16/31] Dirty hack to avoid esoco's assumption that compile must run after test --- buildSrc/src/main/groovy/GwtTools.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/buildSrc/src/main/groovy/GwtTools.groovy b/buildSrc/src/main/groovy/GwtTools.groovy index 78fc4ce88c6..610d45beef0 100644 --- a/buildSrc/src/main/groovy/GwtTools.groovy +++ b/buildSrc/src/main/groovy/GwtTools.groovy @@ -38,6 +38,7 @@ class GwtTools { applyModuleSettings p, gwtc, module,description } p.tasks.withType(GwtCheckTask).configureEach {t -> + t.mustRunAfter(p.tasks.withType(GwtCompileTask)) t.onlyIf { false } } From 03939d999ae21909663f1205136443c734e2d5f5 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Wed, 27 Dec 2023 12:46:53 -0600 Subject: [PATCH 17/31] Don't run gwt tests with in quick --- web/client-api/client-api.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index 894b5d6f25c..a2a01a74ac8 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -117,8 +117,8 @@ def gwtIntegrationTest = tasks.register('gwtIntegrationTest', Test) { t -> t.scanForTestClasses = false } -project.tasks.getByName('quick').dependsOn(gwtUnitTest, gwtIntegrationTest) - -tasks.getByName('check').dependsOn(gwtUnitTest, gwtIntegrationTest) +tasks.named('check').configure { + dependsOn(gwtUnitTest, gwtIntegrationTest) +} apply from: "$rootDir/gradle/web-gwt-test.gradle" From 0d15ac3959a6f81c42e1ecea9d10717c80880720 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Wed, 27 Dec 2023 13:55:59 -0600 Subject: [PATCH 18/31] Remove unused build file, raise timeout on a test --- gradle/web-gwt-test.gradle | 38 ------------------- web/client-api/client-api.gradle | 6 ++- .../subscription/ConcurrentTableTestGwt.java | 2 +- 3 files changed, 6 insertions(+), 40 deletions(-) delete mode 100644 gradle/web-gwt-test.gradle diff --git a/gradle/web-gwt-test.gradle b/gradle/web-gwt-test.gradle deleted file mode 100644 index 2a29d659315..00000000000 --- a/gradle/web-gwt-test.gradle +++ /dev/null @@ -1,38 +0,0 @@ - -configurations { - testImplementation.extendsFrom junit -} - -// for now, all gwt testing will be manual, since we need to have full -// integration testing w/ running servers to be able to use selenium -// (and htmlunit has a bug where it cannot handle Promises). -// This currently defaults to true, and we'll change the default later, -// once we can hook up the rest of the integration testing framework. -boolean manualGwt = findProperty('gwtMode') != 'auto' -boolean testPort = findProperty('gwtTestPort') -String testServer = findProperty('dhTestServer') ?: 'ws://localhost:8123/socket' -String testDir = "$buildDir/testGwt" -task 'gwtTest', type: Test, { - Test t -> - t.inputs.files(sourceSets.test.output.files) - - gradle.projectsEvaluated { - t.classpath += project(":web-client-api").tasks.getByName('gwtCompile').src - } - -// t.classpath = configurations.testRuntime - t.systemProperties = [ 'gwt.args': "${manualGwt ? '-runStyle Manual:1' : ''} ${testPort ? /-port $testPort/ : ''} -war $testDir/war -ea -style PRETTY", - 'gwt.persistentunitcachedir': "$testDir/unitCache", - 'dhTestServer': testServer - ] - t.include '**/*TestSuite.class' - t.useJUnit() - t.maxHeapSize = '2G' - t.scanForTestClasses = false - // never mark task as uptodate when using manual mode - t.outputs.upToDateWhen { !manualGwt } -} - -test { - exclude '**/*TestGwt.class', '**/*TestSuite.class' -} diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index a2a01a74ac8..9dc1713cb46 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -12,6 +12,7 @@ apply from: "$rootDir/gradle/web-client.gradle" configurations { js + testImplementation.extendsFrom junit } dependencies { @@ -121,4 +122,7 @@ tasks.named('check').configure { dependsOn(gwtUnitTest, gwtIntegrationTest) } -apply from: "$rootDir/gradle/web-gwt-test.gradle" +test { + // Configure jvm-only tests to not run any GWT-only tests + exclude '**/*TestGwt.class', '**/*TestSuite.class' +} diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ConcurrentTableTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ConcurrentTableTestGwt.java index c6a60faca3f..87a3212a5d1 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ConcurrentTableTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ConcurrentTableTestGwt.java @@ -62,7 +62,7 @@ public void testOldCopyKeepsGettingUpdates() { // noinspection unchecked return Promise.all(new IThenable[] { assertUpdateReceived(filteredCopy, 10, 3001), - assertUpdateReceived(table, 10, 1501) + assertUpdateReceived(table, 10, 2501) }); }).then(this::finish).catch_(this::report); } From 0d02ff3f81141ba3b897762b207732509224c70f Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Wed, 27 Dec 2023 14:49:31 -0600 Subject: [PATCH 19/31] Clean up readability, use a registry image --- docker/registry/selenium/build.gradle | 3 +++ docker/registry/selenium/gradle.properties | 3 +++ web/client-api/client-api.gradle | 20 ++++++++++++++++---- 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 docker/registry/selenium/build.gradle create mode 100644 docker/registry/selenium/gradle.properties diff --git a/docker/registry/selenium/build.gradle b/docker/registry/selenium/build.gradle new file mode 100644 index 00000000000..a9bb1568cd2 --- /dev/null +++ b/docker/registry/selenium/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'io.deephaven.project.register' +} diff --git a/docker/registry/selenium/gradle.properties b/docker/registry/selenium/gradle.properties new file mode 100644 index 00000000000..168b04db3dc --- /dev/null +++ b/docker/registry/selenium/gradle.properties @@ -0,0 +1,3 @@ +io.deephaven.project.ProjectType=DOCKER_REGISTRY +deephaven.registry.imageName=selenium/standalone-firefox:4.16.1-20231219 +deephaven.registry.imageId=selenium/standalone-firefox@sha256:a405fe92b3ce5d7eb31a07e1f99be3d628fdc0e5bdc81febd8dc11786edef024 diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index ed2b34400e0..d95af70a660 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -8,6 +8,8 @@ plugins { id 'io.deephaven.deephaven-in-docker' } +evaluationDependsOn(Docker.registryProject('selenium')) + apply from: "$rootDir/gradle/web-client.gradle" configurations { @@ -68,8 +70,12 @@ artifacts { def gwtUnitTest = tasks.register('gwtUnitTest', Test) { t -> t.systemProperties = [ - 'gwt.args': "-runStyle HtmlUnit -ea -style PRETTY -war ${layout.buildDirectory.dir('unitTest-war').get().asFile.absolutePath}", - 'gwt.persistentunitcachedir':layout.buildDirectory.dir('unitTest-unitCache').get().asFile.absolutePath, + 'gwt.args': ["-runStyle HtmlUnit", + "-ea", + "-style PRETTY", + "-war ${layout.buildDirectory.dir('unitTest-war').get().asFile.absolutePath}" + ].join(' '), + 'gwt.persistentunitcachedir': layout.buildDirectory.dir('unitTest-unitCache').get().asFile.absolutePath, ] t.include '**/ClientUnitTestSuite.class' t.useJUnit() @@ -89,8 +95,9 @@ deephavenDocker { def seleniumContainerId = "selenium-${randomSuffix}" def createSelenium = tasks.register('createSelenium', DockerCreateContainer) { t -> + t.dependsOn(Docker.registryTask(project, 'selenium')) + t.targetImageId('deephaven/selenium:local-build') t.containerName.set(seleniumContainerId) - t.targetImageId('selenium/standalone-firefox:4.16.1-20231219')// TODO create a registry entry for this t.hostConfig.shmSize.set(2L * 1024 * 1024 * 1024) t.hostConfig.portBindings.set(['4444:4444', '7900:7900'])//TODO don't expose 7900, and make 4444 dynamic t.hostConfig.network.set('host') @@ -121,7 +128,12 @@ def gwtIntegrationTest = tasks.register('gwtIntegrationTest', Test) { t -> t.finalizedBy(deephavenDocker.endTask, stopSelenium) doFirst { def webdriverUrl = 'http://localhost:4444/' - t.systemProperty('gwt.args', "-runStyle io.deephaven.web.junit.RunStyleRemoteWebDriver:${webdriverUrl}?firefox -ea -style PRETTY -setProperty dh.server=http://localhost:${deephavenDocker.port.get()} -war ${layout.buildDirectory.dir('integrationTest-war').get().asFile.absolutePath}") + t.systemProperty('gwt.args', ["-runStyle io.deephaven.web.junit.RunStyleRemoteWebDriver:${webdriverUrl}?firefox", + "-ea", + "-style PRETTY", + "-setProperty dh.server=http://localhost:${deephavenDocker.port.get()}", + "-war ${layout.buildDirectory.dir('integrationTest-war').get().asFile.absolutePath}" + ].join(' ')) t.classpath += tasks.getByName('gwtCompile').src } t.finalizedBy(deephavenDocker.endTask) From dd54ea86954cc40aff00df8d8b145d6afc2bebbb Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Wed, 27 Dec 2023 15:23:15 -0600 Subject: [PATCH 20/31] tidy things up to working in CI --- web/client-api/client-api.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index d95af70a660..e0d69083882 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -99,8 +99,11 @@ def createSelenium = tasks.register('createSelenium', DockerCreateContainer) { t t.targetImageId('deephaven/selenium:local-build') t.containerName.set(seleniumContainerId) t.hostConfig.shmSize.set(2L * 1024 * 1024 * 1024) - t.hostConfig.portBindings.set(['4444:4444', '7900:7900'])//TODO don't expose 7900, and make 4444 dynamic + + // Use "host" networking, so that gradle's test runner can be accessed by the browser. This means + // that port 4444 will always be bound, and if already used, this container won't be able to start. t.hostConfig.network.set('host') + t.hostConfig.portBindings.set(['4444']) } def startSelenium = tasks.register('startSelenium', DockerStartContainer) {t -> t.dependsOn(createSelenium) @@ -120,8 +123,6 @@ def stopSelenium = project.tasks.register('stopSelenium', DockerRemoveContainer) task.force.set true task.removeVolumes.set true } -// before/after: -// docker run -d -p 4444:4444 -p 7900:7900 --shm-size="2g" --net=host selenium/standalone-firefox:4.16.1-20231219 def gwtIntegrationTest = tasks.register('gwtIntegrationTest', Test) { t -> t.dependsOn(deephavenDocker.portTask, seleniumHealthy) From e16cbb71352b8eea4acae7f71665b316d0752131 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Wed, 27 Dec 2023 15:30:45 -0600 Subject: [PATCH 21/31] Revert SubscriptionTableData, WorkerConnection --- .../web/client/api/JsColumnStatistics.java | 1 - .../web/client/api/WorkerConnection.java | 2 +- .../subscription/SubscriptionTableData.java | 78 +++++-------------- 3 files changed, 22 insertions(+), 59 deletions(-) diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/JsColumnStatistics.java b/web/client-api/src/main/java/io/deephaven/web/client/api/JsColumnStatistics.java index 590e30b46cb..2b70704618e 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/JsColumnStatistics.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/JsColumnStatistics.java @@ -12,7 +12,6 @@ import jsinterop.annotations.JsIgnore; import jsinterop.annotations.JsMethod; import jsinterop.annotations.JsProperty; -import jsinterop.annotations.JsType; import java.util.Arrays; import java.util.HashMap; diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/WorkerConnection.java b/web/client-api/src/main/java/io/deephaven/web/client/api/WorkerConnection.java index d1b7af98f05..7d4ea477962 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/WorkerConnection.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/WorkerConnection.java @@ -1437,7 +1437,7 @@ private void flush() { // TODO #188 support minUpdateIntervalMs double serializationOptionsOffset = BarrageSubscriptionOptions .createBarrageSubscriptionOptions(subscriptionReq, ColumnConversionMode.Stringify, true, 1000, - 0, 1_000_000_000); + 0, 0); double tableTicketOffset = BarrageSubscriptionRequest.createTicketVector(subscriptionReq, state.getHandle().getTicket()); BarrageSubscriptionRequest.startBarrageSubscriptionRequest(subscriptionReq); diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/subscription/SubscriptionTableData.java b/web/client-api/src/main/java/io/deephaven/web/client/api/subscription/SubscriptionTableData.java index 8a6ae7faaeb..263d26f66e8 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/subscription/SubscriptionTableData.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/subscription/SubscriptionTableData.java @@ -12,7 +12,10 @@ import io.deephaven.web.shared.data.*; import io.deephaven.web.shared.data.columns.ColumnData; import jsinterop.annotations.JsFunction; +import jsinterop.annotations.JsIgnore; +import jsinterop.annotations.JsMethod; import jsinterop.annotations.JsProperty; +import jsinterop.annotations.JsType; import jsinterop.base.Any; import jsinterop.base.Js; import jsinterop.base.JsArrayLike; @@ -22,11 +25,7 @@ import java.math.BigInteger; import java.util.NavigableSet; import java.util.PrimitiveIterator; -import java.util.Spliterators; import java.util.TreeMap; -import java.util.stream.IntStream; -import java.util.stream.StreamSupport; - import static io.deephaven.web.client.api.subscription.ViewportData.NO_ROW_FORMAT_COLUMN; public class SubscriptionTableData { @@ -44,7 +43,7 @@ private interface ArrayCopy { private RangeSet index; // mappings from the index to the position of a row in the data array - private TreeMap redirectedIndexes; + private TreeMap redirectedIndexes; // rows in the data columns that no longer contain data and can be reused private RangeSet reusableDestinations; @@ -69,7 +68,6 @@ public TableData handleSnapshot(TableSnapshot snapshot) { long includedRowCount = snapshot.getIncludedRows().size(); RangeSet destination = freeRows(includedRowCount); - long[] destArray = array(destination); for (int index = 0; index < dataColumns.length; index++) { ColumnData dataColumn = dataColumns[index]; @@ -87,52 +85,19 @@ public TableData handleSnapshot(TableSnapshot snapshot) { data[index] = localCopy; PrimitiveIterator.OfLong destIter = destination.indexIterator(); PrimitiveIterator.OfLong indexIter = snapshot.getIncludedRows().indexIterator(); - long[] indexArray = array(snapshot.getIncludedRows()); - int[] positions = IntStream.range(0, indexArray.length).toArray(); - shuffle(destArray, indexArray, positions); - - for (int j = 0; j < indexArray.length; j++) { - long dest = destArray[j]; - long index2 = indexArray[j]; - redirectedIndexes.put(index2, (int) dest); - arrayCopy.copyTo(localCopy, dest, dataColumn.getData(), positions[j]); + int j = 0; + while (indexIter.hasNext()) { + assert destIter.hasNext(); + long dest = destIter.nextLong(); + redirectedIndexes.put(indexIter.nextLong(), dest); + arrayCopy.copyTo(localCopy, dest, dataColumn.getData(), j++); } - - // int j = 0; - // while (indexIter.hasNext()) { - // assert destIter.hasNext(); - // long dest = destIter.nextLong(); - // redirectedIndexes.put(indexIter.nextLong(), dest); - // arrayCopy.copyTo(localCopy, dest, dataColumn.getData(), j++); - // } - // assert !destIter.hasNext(); + assert !destIter.hasNext(); } return notifyUpdates(index, RangeSet.empty(), RangeSet.empty()); } - private long[] array(RangeSet rangeSet) { - return StreamSupport.longStream(Spliterators.spliterator(rangeSet.indexIterator(), Long.MAX_VALUE, 0), false) - .toArray(); - } - - public void shuffle(long[] destArray, long[] indexArray, int[] positions) { - for (int i = destArray.length - 1; i > 0; i--) { - int j = (int) (Math.random() * (i + 1)); - long x = destArray[i]; - destArray[i] = destArray[j]; - destArray[j] = x; - - x = indexArray[i]; - indexArray[i] = indexArray[j]; - indexArray[j] = x; - - int pos = positions[i]; - positions[i] = positions[j]; - positions[j] = pos; - } - } - /** * Helper to avoid appending many times when modifying indexes. The append() method should be called for each key * _in order_ to ensure that RangeSet.addRange isn't called excessively. When no more items will be added, flush() @@ -210,7 +175,7 @@ public TableData handleDelta(DeltaUpdates delta) { // iterate backward and move them forward for (Long key : toMove.descendingSet()) { long shiftedKey = key + offset; - Integer oldValue = redirectedIndexes.put(shiftedKey, redirectedIndexes.remove(key)); + Long oldValue = redirectedIndexes.put(shiftedKey, redirectedIndexes.remove(key)); assert oldValue == null : shiftedKey + " already has a value, " + oldValue; shifter.append(shiftedKey); } @@ -228,7 +193,7 @@ public TableData handleDelta(DeltaUpdates delta) { // iterate forward and move them backward for (Long key : toMove) { long shiftedKey = key + offset; - Integer oldValue = redirectedIndexes.put(shiftedKey, redirectedIndexes.remove(key)); + Long oldValue = redirectedIndexes.put(shiftedKey, redirectedIndexes.remove(key)); assert oldValue == null : shiftedKey + " already has a value, " + oldValue; shifter.append(shiftedKey); } @@ -254,8 +219,8 @@ public TableData handleDelta(DeltaUpdates delta) { long origIndex = addedIndexes.nextLong(); assert delta.getIncludedAdditions().contains(origIndex); assert destIter.hasNext(); - int dest = (int) destIter.nextLong(); - Integer old = redirectedIndexes.put(origIndex, dest); + long dest = destIter.nextLong(); + Long old = redirectedIndexes.put(origIndex, dest); assert old == null || old == dest; arrayCopy.copyTo(data[addedColumn.getColumnIndex()], dest, addedColumn.getValues().getData(), j++); } @@ -492,12 +457,10 @@ private RangeSet freeRows(long required) { @TsName(namespace = "dh") public class SubscriptionRow implements TableData.Row { private final long index; - private final long storageIndex; public LongWrapper indexCached; - public SubscriptionRow(long index, int storageIndex) { + public SubscriptionRow(long index) { this.index = index; - this.storageIndex = storageIndex; } @Override @@ -510,8 +473,9 @@ public LongWrapper getIndex() { @Override public Any get(Column column) { + int redirectedIndex = (int) (long) redirectedIndexes.get(this.index); JsArrayLike columnData = Js.asArrayLike(data[column.getIndex()]); - return columnData.getAtAsAny((int) storageIndex); + return columnData.getAtAsAny(redirectedIndex); } @Override @@ -581,8 +545,8 @@ public UpdateEventData(RangeSet added, RangeSet removed, RangeSet modified) { public JsArray getRows() { if (allRows == null) { allRows = new JsArray<>(); - redirectedIndexes.forEach((index, storage) -> { - allRows.push(new SubscriptionRow(index, storage)); + index.indexIterator().forEachRemaining((long index) -> { + allRows.push(new SubscriptionRow(index)); }); if (JsSettings.isDevMode()) { assert allRows.length == index.size(); @@ -604,7 +568,7 @@ public Row get(int index) { */ @Override public SubscriptionRow get(long index) { - return new SubscriptionRow(index, redirectedIndexes.get(index)); + return new SubscriptionRow(index); } @Override From 4af1341389a3476a8dcbcb216abf8de12f3aebc5 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Wed, 27 Dec 2023 15:46:48 -0600 Subject: [PATCH 22/31] Final cleanup (?) --- buildSrc/src/main/groovy/GwtTools.groovy | 3 +++ .../java/io/deephaven/web/ClientIntegrationTestSuite.java | 5 +++-- .../java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml | 2 +- .../src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/buildSrc/src/main/groovy/GwtTools.groovy b/buildSrc/src/main/groovy/GwtTools.groovy index 610d45beef0..64517239f87 100644 --- a/buildSrc/src/main/groovy/GwtTools.groovy +++ b/buildSrc/src/main/groovy/GwtTools.groovy @@ -37,6 +37,9 @@ class GwtTools { GwtCompileTask gwtc -> applyModuleSettings p, gwtc, module,description } + // This GWT plugin will fail if tests are run after compilation, instead + // we suppress running the test at all, and ensure that it doesn't check + // if it even can be run until after compile finishes. p.tasks.withType(GwtCheckTask).configureEach {t -> t.mustRunAfter(p.tasks.withType(GwtCompileTask)) t.onlyIf { false } diff --git a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java index 89091f55d0f..c0d2811e93b 100644 --- a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java +++ b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java @@ -2,7 +2,6 @@ import com.google.gwt.junit.tools.GWTTestSuite; import io.deephaven.web.client.api.NullValueTestGwt; -import io.deephaven.web.client.api.TotalsTableTestGwt; import io.deephaven.web.client.api.subscription.ConcurrentTableTestGwt; import io.deephaven.web.client.api.TableManipulationTestGwt; import io.deephaven.web.client.api.subscription.ViewportTestGwt; @@ -11,7 +10,7 @@ public class ClientIntegrationTestSuite extends GWTTestSuite { public static Test suite() { - TestSuite suite = new TestSuite("Deephaven JS API Unit Test Suite"); + TestSuite suite = new TestSuite("Deephaven JS API Integration Test Suite"); // This test doesn't actually talk to the server, but it requires the dh-internal library be available. // Disabled for now, we don't have good toString on the FilterCondition/FilterValue types. @@ -22,6 +21,8 @@ public static Test suite() { suite.addTestSuite(TableManipulationTestGwt.class); suite.addTestSuite(ConcurrentTableTestGwt.class); suite.addTestSuite(NullValueTestGwt.class); + + // Unfinished: // suite.addTestSuite(TotalsTableTestGwt.class); return suite; diff --git a/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml b/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml index 3d6f63cee15..fab73ebdec1 100644 --- a/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml +++ b/web/client-api/src/test/java/io/deephaven/web/DeephavenIntegrationTest.gwt.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml b/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml index 5368885ac79..20dec64fd92 100644 --- a/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml +++ b/web/client-api/src/test/java/io/deephaven/web/DeephavenUnitTest.gwt.xml @@ -9,4 +9,4 @@ - \ No newline at end of file + From 43a102f1c18eb656b29402cf96c58aae3710557d Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Tue, 2 Jan 2024 10:07:53 -0600 Subject: [PATCH 23/31] Parameterize port and timeouts, other cleanup --- web/client-api/client-api.gradle | 10 ++++++-- .../client/api/AbstractAsyncGwtTestCase.java | 9 +++---- .../client/api/TableManipulationTestGwt.java | 4 ++-- .../web/client/api/TotalsTableTestGwt.java | 24 +++++++++---------- .../api/subscription/ViewportTestGwt.java | 12 +++++----- 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index e0d69083882..dd20ba9b6ec 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -93,6 +93,12 @@ deephavenDocker { } def seleniumContainerId = "selenium-${randomSuffix}" +def seleniumPort +if (!hasProperty('selenium.port')) { + seleniumPort = '4444' +} else { + seleniumPort = project.getProperty('selenium.port') +} def createSelenium = tasks.register('createSelenium', DockerCreateContainer) { t -> t.dependsOn(Docker.registryTask(project, 'selenium')) @@ -101,9 +107,9 @@ def createSelenium = tasks.register('createSelenium', DockerCreateContainer) { t t.hostConfig.shmSize.set(2L * 1024 * 1024 * 1024) // Use "host" networking, so that gradle's test runner can be accessed by the browser. This means - // that port 4444 will always be bound, and if already used, this container won't be able to start. + // that the selected will always be bound, and if already used, this container won't be able to start. t.hostConfig.network.set('host') - t.hostConfig.portBindings.set(['4444']) + t.hostConfig.portBindings.set([seleniumPort]) } def startSelenium = tasks.register('startSelenium', DockerStartContainer) {t -> t.dependsOn(createSelenium) diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java index 6d3fcfb806a..8397435277c 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java @@ -6,14 +6,12 @@ import elemental2.core.JsError; import elemental2.core.JsString; import elemental2.dom.CustomEvent; -import elemental2.dom.CustomEventInit; import elemental2.dom.DomGlobal; import elemental2.promise.IThenable; import elemental2.promise.Promise; import io.deephaven.web.client.api.subscription.ViewportData; import io.deephaven.web.client.fu.CancellablePromise; import io.deephaven.web.client.ide.IdeSession; -import io.deephaven.web.shared.data.Viewport; import io.deephaven.web.shared.fu.JsRunnable; import io.deephaven.web.shared.fu.RemoverFn; import jsinterop.annotations.JsMethod; @@ -21,7 +19,6 @@ import jsinterop.annotations.JsProperty; import jsinterop.base.Js; import jsinterop.base.JsPropertyMap; -import org.apache.tapestry.INamespace; import java.util.ArrayList; import java.util.Arrays; @@ -69,7 +66,7 @@ public TableSourceBuilder script(String tableName, String python) { /** * Set this to a value higher than 1 to get more time to run debugger without timeouts failing. */ - protected static final int TIMEOUT_SCALE = 2; + protected static final int TIMEOUT_SCALE = Integer.parseInt(System.getProperty("test.timeout.scale", "1")); public static final double DELTA = 0.0001; public JsString toJsString(String k) { @@ -210,7 +207,7 @@ protected Promise finish(Object input) { } /** - * Helper method to add a listener to the promise of a table, and ensure that an update is recieved with the + * Helper method to add a listener to the promise of a table, and ensure that an update is received with the * expected number of items, within the specified timeout. * * Prereq: have already requested a viewport on that table @@ -220,7 +217,7 @@ protected Promise assertUpdateReceived(Promise tablePromise, i } /** - * Helper method to add a listener to a table, and ensure that an update is recieved with the expected number of + * Helper method to add a listener to a table, and ensure that an update is received with the expected number of * items, within the specified timeout. * * Prereq: have already requested a viewport on that table. Remember to request that within the same event loop, so diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/TableManipulationTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/TableManipulationTestGwt.java index d4b9c29854b..48577053859 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/TableManipulationTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/TableManipulationTestGwt.java @@ -36,7 +36,7 @@ public void testChangingFilters() { return waitForEvent(table, JsTable.EVENT_SIZECHANGED, e -> { assertEquals(4., table.getSize(), 0); assertEquals(8., table.getTotalSize(), 0); - }, 1000); + }, 2014); }) .then(table -> { // then set the viewport, confirm we get those items back @@ -153,7 +153,7 @@ public void testSerialFilterAndSort() { }); // no viewport, since we're going to make another change return waitForEvent(table, JsTable.EVENT_FILTERCHANGED, e -> { - }, 1000); + }, 2015); }) .then(table -> { table.applySort(new Sort[] { diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/TotalsTableTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/TotalsTableTestGwt.java index f42e7063fd1..4c8000f0d51 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/TotalsTableTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/TotalsTableTestGwt.java @@ -72,7 +72,7 @@ public void testQueryDefinedConfigs() { totals.setViewport(0, 100, null, null); return waitForEvent(totals, JsTable.EVENT_UPDATED, - checkTotals(totals, 5, 6., 0, "a1"), 1500); + checkTotals(totals, 5, 6., 0, "a1"), 2508); }), table.getGrandTotalsTable(null) .then(totals -> { @@ -81,7 +81,7 @@ public void testQueryDefinedConfigs() { totals.setViewport(0, 100, null, null); return waitForEvent(totals, JsTable.EVENT_UPDATED, - checkTotals(totals, 5, 6.0, 0., "a2"), 1500); + checkTotals(totals, 5, 6.0, 0., "a2"), 2509); }) }); }) @@ -113,7 +113,7 @@ public void ignore_testTotalsOnFilteredTable() { // confirm the normal totals match the filtered data return waitForEvent(totals, JsTable.EVENT_UPDATED, - checkTotals(totals, 3, 6.666666, 0.0, "a1"), 1501); + checkTotals(totals, 3, 6.666666, 0.0, "a1"), 2501); }), table.getGrandTotalsTable(null) .then(totals -> { @@ -124,7 +124,7 @@ public void ignore_testTotalsOnFilteredTable() { // confirm the grand totals are unchanged return waitForEvent(totals, JsTable.EVENT_UPDATED, - checkTotals(totals, 5, 6., 0., "a2"), 1502); + checkTotals(totals, 5, 6., 0., "a2"), 2502); }))) .then(table -> { // Now, change the filter on the original table, and expect the totals tables to automatically @@ -138,9 +138,9 @@ public void ignore_testTotalsOnFilteredTable() { return promiseAllThen(table, waitForEvent(table, JsTable.EVENT_FILTERCHANGED, 2002).onInvoke(table), totalPromises[0] = waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, - checkTotals(totalTables[0], 2, 5, 1, "b1"), 1503), + checkTotals(totalTables[0], 2, 5, 1, "b1"), 2503), totalPromises[1] = waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, - checkTotals(totalTables[1], 5, 6, 0, "b2"), 1504)); + checkTotals(totalTables[1], 5, 6, 0, "b2"), 2504)); }) .then(table -> { // forcibly disconnect the worker and test that the total table come back up, and respond to @@ -164,9 +164,9 @@ public void ignore_testTotalsOnFilteredTable() { return promiseAllThen(table, waitForEvent(table, JsTable.EVENT_FILTERCHANGED, 2003).onInvoke(table), waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, - checkTotals(totalTables[0], 3, 6.666666, 0.0, "d1"), 1507), + checkTotals(totalTables[0], 3, 6.666666, 0.0, "d1"), 2507), waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, - checkTotals(totalTables[1], 5, 6., 0., "d2"), 1508)); + checkTotals(totalTables[1], 5, 6., 0., "d2"), 2508)); }) .then(this::finish).catch_(this::report); } @@ -240,7 +240,7 @@ public void ignore_testFilteringTotalsTable() { // confirm the normal totals match the filtered data return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals(totals, "a1", TotalsResults.of(2, 2, 8, 0.0), - TotalsResults.of(2, 2, 5, 1.0)), 1501); + TotalsResults.of(2, 2, 5, 1.0)), 2501); }), table.getGrandTotalsTable(config) .then(totals -> { @@ -252,7 +252,7 @@ public void ignore_testFilteringTotalsTable() { // confirm the grand totals include the missing row... return waitForEvent(totals, JsTable.EVENT_UPDATED, checkTotals(totals, "a2", TotalsResults.of(3, 3, 6.66666, 0.0), - TotalsResults.of(2, 2, 5, 1.0)), 1502); + TotalsResults.of(2, 2, 5, 1.0)), 2502); })); }) .then(table -> { @@ -271,9 +271,9 @@ public void ignore_testFilteringTotalsTable() { return promiseAllThen(table, totalPromises[0] = waitForEvent(totalTables[0], JsTable.EVENT_UPDATED, - checkTotals(totalTables[0], "b1", TotalsResults.of(2, 2, 5, 1)), 1503), + checkTotals(totalTables[0], "b1", TotalsResults.of(2, 2, 5, 1)), 2503), totalPromises[1] = waitForEvent(totalTables[1], JsTable.EVENT_UPDATED, - checkTotals(totalTables[1], "b2", TotalsResults.of(2, 2, 5, 1)), 1504)); + checkTotals(totalTables[1], "b2", TotalsResults.of(2, 2, 5, 1)), 2504)); }) .then(table -> { // forcibly disconnect the worker and test that the total table come back up, and respond to diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java index 9148fdd21ff..4b843904df9 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java @@ -302,7 +302,7 @@ public void ignore_testEmptyTableWithViewport() { // when IDS-2113 is fixed, restore this stronger assertion // assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000) waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> { - }, 1000), + }, 2011), assertEventFiresOnce(table, JsTable.EVENT_SIZECHANGED, 1000) }).then(ignore -> Promise.resolve(table)); }) @@ -326,7 +326,7 @@ public void ignore_testEmptyTableWithViewport() { // when IDS-2113 is fixed, restore this stronger assertion // return assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000); return waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> { - }, 1000); + }, 2012); }) .then(this::finish).catch_(this::report); } @@ -342,7 +342,7 @@ public void testViewportOutOfRangeOfTable() { // when IDS-2113 is fixed, restore this stronger assertion // assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000) waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> { - }, 1000) + }, 2013) }).then(ignore -> Promise.resolve(table)); }) .then(this::finish).catch_(this::report); @@ -446,7 +446,7 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column assertNotNull(viewport.getRows().getAt(0).get(c)); } return true; - }, 1502); + }, 2502); }) .then(table -> { // again wait for the table to go back to zero items, make sure it makes sense @@ -457,7 +457,7 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column } assertEquals(expectedColumns.length, emptyViewport.getColumns().length); return true; - }, 1503); + }, 2503); }) .then(table -> { // one more tick later, we'll see the item back again @@ -472,7 +472,7 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column assertNotNull(viewport.getRows().getAt(0).get(c)); } return true; - }, 1504); + }, 2504); }); } From 2bab42bc0b7be9a89e1e7a5c366752cadf131cc6 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Tue, 2 Jan 2024 13:39:12 -0600 Subject: [PATCH 24/31] Rework networking so hopefully docker-desktop behaves --- web/client-api/client-api.gradle | 14 +++++++------- .../web/junit/RunStyleRemoteWebDriver.java | 14 ++++---------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index dd20ba9b6ec..853bd5af64a 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -101,15 +101,14 @@ if (!hasProperty('selenium.port')) { } def createSelenium = tasks.register('createSelenium', DockerCreateContainer) { t -> - t.dependsOn(Docker.registryTask(project, 'selenium')) + t.dependsOn(Docker.registryTask(project, 'selenium'), deephavenDocker.startTask) t.targetImageId('deephaven/selenium:local-build') t.containerName.set(seleniumContainerId) t.hostConfig.shmSize.set(2L * 1024 * 1024 * 1024) - // Use "host" networking, so that gradle's test runner can be accessed by the browser. This means - // that the selected will always be bound, and if already used, this container won't be able to start. - t.hostConfig.network.set('host') - t.hostConfig.portBindings.set([seleniumPort]) + t.hostConfig.extraHosts.add('host.docker.internal:host-gateway') + t.hostConfig.portBindings.set(["4444:$seleniumPort"]) + t.hostConfig.network.set(deephavenDocker.networkName.get()) } def startSelenium = tasks.register('startSelenium', DockerStartContainer) {t -> t.dependsOn(createSelenium) @@ -134,11 +133,11 @@ def gwtIntegrationTest = tasks.register('gwtIntegrationTest', Test) { t -> t.dependsOn(deephavenDocker.portTask, seleniumHealthy) t.finalizedBy(deephavenDocker.endTask, stopSelenium) doFirst { - def webdriverUrl = 'http://localhost:4444/' + def webdriverUrl = "http://localhost:4444/" t.systemProperty('gwt.args', ["-runStyle io.deephaven.web.junit.RunStyleRemoteWebDriver:${webdriverUrl}?firefox", "-ea", "-style PRETTY", - "-setProperty dh.server=http://localhost:${deephavenDocker.port.get()}", + "-setProperty dh.server=http://${deephavenDocker.containerName.get()}:10000", "-war ${layout.buildDirectory.dir('integrationTest-war').get().asFile.absolutePath}" ].join(' ')) t.classpath += tasks.getByName('gwtCompile').src @@ -146,6 +145,7 @@ def gwtIntegrationTest = tasks.register('gwtIntegrationTest', Test) { t -> t.finalizedBy(deephavenDocker.endTask) t.systemProperties = [ 'gwt.persistentunitcachedir':layout.buildDirectory.dir('integrationTest-unitCache').get().asFile.absolutePath, + 'webdriver.test.host':'host.docker.internal', ] t.include '**/ClientIntegrationTestSuite.class' t.useJUnit() diff --git a/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java b/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java index 2d01992f45c..bed98140119 100644 --- a/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java +++ b/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java @@ -60,10 +60,8 @@ protected RemoteWebDriverConfiguration readConfiguration(String args) throws ConfigurationException { RemoteWebDriverConfiguration config = new RemoteWebDriverConfiguration(); if (args == null || args.length() == 0) { - getLogger() - .log( - TreeLogger.ERROR, - "RemoteWebDriver runstyle requires a parameter of the form protocol://hostname:port?browser1[,browser2]"); + getLogger().log(TreeLogger.ERROR, + "RemoteWebDriver runstyle requires a parameter of the form protocol://hostname:port?browser1[,browser2]"); throw new ConfigurationException(); } @@ -74,10 +72,7 @@ protected RemoteWebDriverConfiguration readConfiguration(String args) remoteAddress = new URL(url); if (remoteAddress.getPath().equals("") || (remoteAddress.getPath().equals("/") && !url.endsWith("/"))) { - getLogger() - .log( - TreeLogger.INFO, - "No path specified in webdriver remote url, using default of /wd/hub"); + getLogger().log(TreeLogger.INFO, "No path specified in webdriver remote url, using default of /wd/hub"); config.setRemoteWebDriverUrl(url + "/wd/hub"); } else { config.setRemoteWebDriverUrl(url); @@ -106,8 +101,7 @@ public final int initialize(String args) { try { config = readConfiguration(args); } catch (ConfigurationException failed) { - // log should already have details about what went wrong, we will just return the failure - // value + // log should already have details about what went wrong, we will just return the failure value return -1; } From d61c783e46d049a521ed0ec8f8f2f882f40c533e Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Tue, 2 Jan 2024 16:29:49 -0600 Subject: [PATCH 25/31] Add healthcheck, cleanup --- web/client-api/client-api.gradle | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index 853bd5af64a..5aa62ba42bd 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -70,9 +70,9 @@ artifacts { def gwtUnitTest = tasks.register('gwtUnitTest', Test) { t -> t.systemProperties = [ - 'gwt.args': ["-runStyle HtmlUnit", - "-ea", - "-style PRETTY", + 'gwt.args': ['-runStyle HtmlUnit', + '-ea', + '-style PRETTY', "-war ${layout.buildDirectory.dir('unitTest-war').get().asFile.absolutePath}" ].join(' '), 'gwt.persistentunitcachedir': layout.buildDirectory.dir('unitTest-unitCache').get().asFile.absolutePath, @@ -104,8 +104,14 @@ def createSelenium = tasks.register('createSelenium', DockerCreateContainer) { t t.dependsOn(Docker.registryTask(project, 'selenium'), deephavenDocker.startTask) t.targetImageId('deephaven/selenium:local-build') t.containerName.set(seleniumContainerId) + // Advised by the selenium documentation t.hostConfig.shmSize.set(2L * 1024 * 1024 * 1024) + // Add our own healthcheck to confirm the container starts fully + t.healthCheck.cmd.set(['curl http://localhost:4444/wd/hub/status || exit 1']) + + // This provides a hostname that can be referenced from inside the docker container to access the host + // OS, and connect to the test server. t.hostConfig.extraHosts.add('host.docker.internal:host-gateway') t.hostConfig.portBindings.set(["4444:$seleniumPort"]) t.hostConfig.network.set(deephavenDocker.networkName.get()) @@ -133,10 +139,10 @@ def gwtIntegrationTest = tasks.register('gwtIntegrationTest', Test) { t -> t.dependsOn(deephavenDocker.portTask, seleniumHealthy) t.finalizedBy(deephavenDocker.endTask, stopSelenium) doFirst { - def webdriverUrl = "http://localhost:4444/" + def webdriverUrl = 'http://localhost:4444/' t.systemProperty('gwt.args', ["-runStyle io.deephaven.web.junit.RunStyleRemoteWebDriver:${webdriverUrl}?firefox", - "-ea", - "-style PRETTY", + '-ea', + '-style PRETTY', "-setProperty dh.server=http://${deephavenDocker.containerName.get()}:10000", "-war ${layout.buildDirectory.dir('integrationTest-war').get().asFile.absolutePath}" ].join(' ')) From a11687de7215c3eba724ccdfe37e9f823e3f3d34 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Tue, 2 Jan 2024 16:47:52 -0600 Subject: [PATCH 26/31] Clean up forked test runner --- .../web/junit/RunStyleRemoteWebDriver.java | 97 +++++++++---------- 1 file changed, 47 insertions(+), 50 deletions(-) diff --git a/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java b/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java index bed98140119..154c84c35f8 100644 --- a/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java +++ b/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java @@ -15,7 +15,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; +/** + * RunStyle implementation to delegate to Selenium RemoteWebDriver implementations. Simplified version + * of implementation found in gwt-core. + */ public class RunStyleRemoteWebDriver extends RunStyle { public static class RemoteWebDriverConfiguration { @@ -39,14 +44,27 @@ public void setBrowserCapabilities(List> browserCapabilities) { } } - public class ConfigurationException extends Exception { - } - - private List browsers = new ArrayList<>(); - private Thread keepalive; + private final List browsers = new ArrayList<>(); + private final Thread keepalive; public RunStyleRemoteWebDriver(JUnitShell shell) { super(shell); + + keepalive = new Thread(() -> { + while (true) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + break; + } + for (RemoteWebDriver browser : browsers) { + // As in RunStyleSelenium, simple way to poll the browser and ensure it is still alive, even if + // not actively being used at the moment. + browser.getTitle(); + } + } + }); + keepalive.setDaemon(true); } /** @@ -56,21 +74,20 @@ public RunStyleRemoteWebDriver(JUnitShell shell) { * @param args the command line argument string passed from JUnitShell * @return the configuration to use when running these tests */ - protected RemoteWebDriverConfiguration readConfiguration(String args) - throws ConfigurationException { + protected Optional readConfiguration(String args) { RemoteWebDriverConfiguration config = new RemoteWebDriverConfiguration(); - if (args == null || args.length() == 0) { + if (args == null || args.isEmpty()) { getLogger().log(TreeLogger.ERROR, "RemoteWebDriver runstyle requires a parameter of the form protocol://hostname:port?browser1[,browser2]"); - throw new ConfigurationException(); + return Optional.empty(); } String[] parts = args.split("\\?"); String url = parts[0]; - URL remoteAddress = null; + URL remoteAddress; try { remoteAddress = new URL(url); - if (remoteAddress.getPath().equals("") + if (remoteAddress.getPath().isEmpty() || (remoteAddress.getPath().equals("/") && !url.endsWith("/"))) { getLogger().log(TreeLogger.INFO, "No path specified in webdriver remote url, using default of /wd/hub"); config.setRemoteWebDriverUrl(url + "/wd/hub"); @@ -79,7 +96,7 @@ protected RemoteWebDriverConfiguration readConfiguration(String args) } } catch (MalformedURLException e) { getLogger().log(TreeLogger.ERROR, e.getMessage(), e); - throw new ConfigurationException(); + return Optional.empty(); } // build each driver based on parts[1].split(",") @@ -91,29 +108,27 @@ protected RemoteWebDriverConfiguration readConfiguration(String args) config.getBrowserCapabilities().add(capabilities.asMap()); } - return config; + return Optional.of(config); } @Override public final int initialize(String args) { - final RemoteWebDriverConfiguration config; - try { - config = readConfiguration(args); - } catch (ConfigurationException failed) { + final Optional config = readConfiguration(args); + if (config.isEmpty()) { // log should already have details about what went wrong, we will just return the failure value return -1; } final URL remoteAddress; try { - remoteAddress = new URL(config.getRemoteWebDriverUrl()); + remoteAddress = new URL(config.get().getRemoteWebDriverUrl()); } catch (MalformedURLException e) { getLogger().log(TreeLogger.ERROR, e.getMessage(), e); return -1; } - for (Map capabilityMap : config.getBrowserCapabilities()) { + for (Map capabilityMap : config.get().getBrowserCapabilities()) { DesiredCapabilities capabilities = new DesiredCapabilities(capabilityMap); try { @@ -125,43 +140,25 @@ public final int initialize(String args) { } } - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - if (keepalive != null) { - keepalive.interrupt(); - } - for (RemoteWebDriver browser : browsers) { - try { - browser.close(); - } catch (Exception ignored) { - // ignore, we're shutting down, continue shutting down others - } - } - })); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + keepalive.interrupt(); + for (RemoteWebDriver browser : browsers) { + try { + browser.close(); + } catch (Exception ignored) { + // ignore, we're shutting down, continue shutting down others + } + } + })); return browsers.size(); } @Override public void launchModule(String moduleName) throws UnableToCompleteException { - // since WebDriver.get is blocking, start a keepalive thread first - keepalive = - new Thread( - () -> { - while (true) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - break; - } - for (RemoteWebDriver browser : browsers) { - browser.getTitle(); // as in RunStyleSelenium, simple way to poll the browser - } - } - }); - keepalive.setDaemon(true); + // Since WebDriver.get is blocking, start the keepalive thread first keepalive.start(); + + // Starts each browser to run the tests at the url specified by JUnit+GWT. for (RemoteWebDriver browser : browsers) { browser.get(shell.getModuleUrl(moduleName)); } From e03f3e750df80cc65a90a2a12323b00f93196130 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Tue, 2 Jan 2024 16:58:31 -0600 Subject: [PATCH 27/31] Inline more wd runstyle contents --- .../web/junit/RunStyleRemoteWebDriver.java | 89 ++++--------------- 1 file changed, 15 insertions(+), 74 deletions(-) diff --git a/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java b/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java index 154c84c35f8..0a33c37f459 100644 --- a/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java +++ b/web/client-api/src/test/java/io/deephaven/web/junit/RunStyleRemoteWebDriver.java @@ -15,35 +15,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; /** - * RunStyle implementation to delegate to Selenium RemoteWebDriver implementations. Simplified version - * of implementation found in gwt-core. + * RunStyle implementation to delegate to Selenium RemoteWebDriver implementations. Simplified version of implementation + * found in gwt-core. */ public class RunStyleRemoteWebDriver extends RunStyle { - - public static class RemoteWebDriverConfiguration { - private String remoteWebDriverUrl; - private List> browserCapabilities; - - public String getRemoteWebDriverUrl() { - return remoteWebDriverUrl; - } - - public void setRemoteWebDriverUrl(String remoteWebDriverUrl) { - this.remoteWebDriverUrl = remoteWebDriverUrl; - } - - public List> getBrowserCapabilities() { - return browserCapabilities; - } - - public void setBrowserCapabilities(List> browserCapabilities) { - this.browserCapabilities = browserCapabilities; - } - } - private final List browsers = new ArrayList<>(); private final Thread keepalive; @@ -67,72 +44,36 @@ public RunStyleRemoteWebDriver(JUnitShell shell) { keepalive.setDaemon(true); } - /** - * Validates the arguments for the specific subclass, and creates a configuration that describes how to run the - * tests. - * - * @param args the command line argument string passed from JUnitShell - * @return the configuration to use when running these tests - */ - protected Optional readConfiguration(String args) { - RemoteWebDriverConfiguration config = new RemoteWebDriverConfiguration(); + + @Override + public final int initialize(String args) { + URL remoteWebDriverUrl; if (args == null || args.isEmpty()) { getLogger().log(TreeLogger.ERROR, "RemoteWebDriver runstyle requires a parameter of the form protocol://hostname:port?browser1[,browser2]"); - return Optional.empty(); + return -1; } - String[] parts = args.split("\\?"); String url = parts[0]; - URL remoteAddress; try { - remoteAddress = new URL(url); - if (remoteAddress.getPath().isEmpty() - || (remoteAddress.getPath().equals("/") && !url.endsWith("/"))) { + remoteWebDriverUrl = new URL(url); + if (remoteWebDriverUrl.getPath().isEmpty() + || (remoteWebDriverUrl.getPath().equals("/") && !url.endsWith("/"))) { getLogger().log(TreeLogger.INFO, "No path specified in webdriver remote url, using default of /wd/hub"); - config.setRemoteWebDriverUrl(url + "/wd/hub"); - } else { - config.setRemoteWebDriverUrl(url); + remoteWebDriverUrl = new URL(url + "/wd/hub"); } - } catch (MalformedURLException e) { - getLogger().log(TreeLogger.ERROR, e.getMessage(), e); - return Optional.empty(); + } catch (MalformedURLException e1) { + getLogger().log(TreeLogger.ERROR, e1.getMessage(), e1); + return -1; } - // build each driver based on parts[1].split(",") String[] browserNames = parts[1].split(","); - config.setBrowserCapabilities(new ArrayList<>()); for (String browserName : browserNames) { DesiredCapabilities capabilities = new DesiredCapabilities(); capabilities.setBrowserName(browserName); - config.getBrowserCapabilities().add(capabilities.asMap()); - } - - return Optional.of(config); - } - - - @Override - public final int initialize(String args) { - final Optional config = readConfiguration(args); - if (config.isEmpty()) { - // log should already have details about what went wrong, we will just return the failure value - return -1; - } - - final URL remoteAddress; - try { - remoteAddress = new URL(config.get().getRemoteWebDriverUrl()); - } catch (MalformedURLException e) { - getLogger().log(TreeLogger.ERROR, e.getMessage(), e); - return -1; - } - - for (Map capabilityMap : config.get().getBrowserCapabilities()) { - DesiredCapabilities capabilities = new DesiredCapabilities(capabilityMap); try { - RemoteWebDriver wd = new RemoteWebDriver(remoteAddress, capabilities); + RemoteWebDriver wd = new RemoteWebDriver(remoteWebDriverUrl, capabilities); browsers.add(wd); } catch (Exception exception) { getLogger().log(TreeLogger.ERROR, "Failed to find desired browser", exception); From ab0bb30c8c8def9305b2f2c87c1b63b0e2e29a78 Mon Sep 17 00:00:00 2001 From: Mike Bender Date: Wed, 3 Jan 2024 11:48:23 -0500 Subject: [PATCH 28/31] Change up timeouts a bit - Increased connect timeout to 1s - Added some sugar to some of the timeouts so they're easier to identify --- .../web/client/api/AbstractAsyncGwtTestCase.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java index 8397435277c..e074863b57c 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/AbstractAsyncGwtTestCase.java @@ -129,7 +129,7 @@ static Promise expectFailure(Promise state, T value) { * Imports the webpack content, including protobuf types. Does not connect to the server. */ protected Promise setupDhInternal() { - delayTestFinish(500); + delayTestFinish(504); return importDhInternal(); } @@ -137,8 +137,8 @@ protected Promise setupDhInternal() { * Connects and authenticates to the configured server and runs the specified scripts. */ protected Promise connect(TableSourceBuilder tables) { - // start by delaying test finish by .5s so we fail fast in cases where we aren't set up right - delayTestFinish(500); + // start by delaying test finish by 1.0s so we fail fast in cases where we aren't set up right + delayTestFinish(1007); return importDhInternal().then(module -> { CoreClient coreClient = new CoreClient(localServer, null); return coreClient.login(JsPropertyMap.of("type", CoreClient.LOGIN_TYPE_ANONYMOUS)) @@ -291,7 +291,7 @@ protected Promise waitForEventWhere(V evented } complete[0] = true; // complete already handled - }, timeout * TIMEOUT_SCALE); + }, timeout * TIMEOUT_SCALE + 13); }); } From 4413dcfc2882396af0f30ff98cb8ed6b5a7dac2f Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Wed, 3 Jan 2024 11:16:44 -0600 Subject: [PATCH 29/31] Increase a few more timeouts and make them unique --- .../api/subscription/ViewportTestGwt.java | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java index 4b843904df9..dae893b34d5 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/subscription/ViewportTestGwt.java @@ -50,7 +50,7 @@ public void testViewportOnStaticTable() { int size = (int) table.getSize(); int lastRow = size - 1; table.setViewport(0, lastRow, null); - return assertUpdateReceived(table, size, 500); + return assertUpdateReceived(table, size, 2500); }) .then(table -> { // table has 100 rows, go through each page of 25, make sure the offset and length is sane @@ -65,21 +65,21 @@ public void testViewportOnStaticTable() { return assertUpdateReceived(table, viewport -> { assertEquals(25, (long) viewport.getOffset()); assertEquals(25, viewport.getRows().length); - }, 1000); + }, 1001); }) .then(table -> { table.setViewport(50, 74, null); return assertUpdateReceived(table, viewport -> { assertEquals(50, (long) viewport.getOffset()); assertEquals(25, viewport.getRows().length); - }, 1000); + }, 1002); }) .then(table -> { table.setViewport(75, 99, null); return assertUpdateReceived(table, viewport -> { assertEquals(75, (long) viewport.getOffset()); assertEquals(25, viewport.getRows().length); - }, 1000); + }, 1003); }) .then(this::finish).catch_(this::report); } @@ -121,7 +121,7 @@ public void ignore_testViewportOnGrowingTable() { double lastRow = size - 1; table.setViewport(size, lastRow + 9, null); return waitFor(() -> table.getSize() == size + 1, 100, 3000, table) - .then(waitForEvent(table, JsTable.EVENT_SIZECHANGED, 2503)) + .then(waitForEvent(table, JsTable.EVENT_SIZECHANGED, 2510)) .then(JsTable::getViewportData) .then(viewportData -> { assertEquals(2, viewportData.getRows().length); @@ -140,7 +140,7 @@ public void testViewportOnUpdatingTable() { // set up a viewport, and watch it show up, and tick once table.setViewport(0, 9, null); return assertUpdateReceived(table, viewportData -> { - }, 1000); + }, 1004); }) .then(table -> { return assertUpdateReceived(table, viewportData -> { @@ -180,7 +180,7 @@ public void testViewportSubsetOfColumns() { assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("J"))); assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("K"))); - }, 500); + }, 2501); }) .then(table -> { // don't change viewport, test the same thing again, make sure deltas behave too @@ -207,7 +207,7 @@ public void testViewportSubsetOfColumns() { assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("J"))); assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("K"))); - }, 500); + }, 2502); }) .then(table -> { table.setViewport(0, 0, Js.uncheckedCast(new Column[] {table.findColumn("K")})); @@ -221,7 +221,7 @@ public void testViewportSubsetOfColumns() { assertThrowsException(() -> viewport.getRows().getAt(0).get(table.findColumn("J"))); assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("K"))); - }, 500); + }, 2503); }) .then(table -> { table.setViewport(0, 0, Js.uncheckedCast(new Column[] { @@ -239,7 +239,7 @@ public void testViewportSubsetOfColumns() { assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("J"))); assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("K"))); - }, 500); + }, 2504); }) .then(table -> { table.setViewport(0, 0, Js.uncheckedCast(new Column[] { @@ -262,7 +262,7 @@ public void testViewportSubsetOfColumns() { assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("J"))); assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("K"))); - }, 500); + }, 2505); }) .then(table -> { table.setViewport(0, 0, null); @@ -280,7 +280,7 @@ public void testViewportSubsetOfColumns() { assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("J"))); assertNotNull(viewport.getRows().getAt(0).get(table.findColumn("K"))); - }, 500); + }, 2506); }) .then(this::finish).catch_(this::report); } @@ -303,7 +303,7 @@ public void ignore_testEmptyTableWithViewport() { // assertEventFiresOnce(table, JsTable.EVENT_UPDATED, 1000) waitForEvent(table, JsTable.EVENT_UPDATED, ignore -> { }, 2011), - assertEventFiresOnce(table, JsTable.EVENT_SIZECHANGED, 1000) + assertEventFiresOnce(table, JsTable.EVENT_SIZECHANGED, 1005) }).then(ignore -> Promise.resolve(table)); }) .then(table -> { @@ -311,14 +311,14 @@ public void ignore_testEmptyTableWithViewport() { table.applyFilter(new FilterCondition[0]); table.setViewport(0, 100, null); return assertUpdateReceived(table, ignore -> { - }, 1000); + }, 1006); }) .then(table -> { // change the filter, don't set a viewport, assert only size changes table.applyFilter(new FilterCondition[] { FilterValue.ofBoolean(false).isTrue() }); - return assertEventFiresOnce(table, JsTable.EVENT_SIZECHANGED, 1000); + return assertEventFiresOnce(table, JsTable.EVENT_SIZECHANGED, 1007); }) .then(table -> { // set a viewport, assert that update fires and no size change @@ -360,7 +360,7 @@ public void testRapidChangingViewport() { return assertUpdateReceived(table, viewport -> { assertEquals(5, (int) viewport.getOffset()); assertEquals(10, (int) viewport.getRows().length); - }, 1000); + }, 1008); }) .then(table -> { // test changing the viewport over a microtask (anyone in the web api getting clever with batching?) @@ -372,7 +372,7 @@ public void testRapidChangingViewport() { return assertUpdateReceived(table, viewport -> { assertEquals(6, (int) viewport.getOffset()); assertEquals(9, (int) viewport.getRows().length); - }, 1000); + }, 1009); }) .then(table -> { table.setViewport(0, 10, null); @@ -383,7 +383,7 @@ public void testRapidChangingViewport() { .then(table -> { table.setViewport(7, 17, null); return assertUpdateReceived(table, ignored -> { - }, 1000) + }, 1010) .then(waitFor(JsTable.DEBOUNCE_TIME * 2)) .then(t -> { // force the debounce to be processed @@ -446,7 +446,7 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column assertNotNull(viewport.getRows().getAt(0).get(c)); } return true; - }, 2502); + }, 2508); }) .then(table -> { // again wait for the table to go back to zero items, make sure it makes sense @@ -472,7 +472,7 @@ private IThenable helperForViewportWithNoInitialItems(JsTable t, Column assertNotNull(viewport.getRows().getAt(0).get(c)); } return true; - }, 2504); + }, 2511); }); } From a0c4f0dc0a0bc2679a784619955ce7bf7cec4f3a Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Wed, 3 Jan 2024 13:50:11 -0600 Subject: [PATCH 30/31] Add missing basic column types --- .../web/client/api/NullValueTestGwt.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/NullValueTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/NullValueTestGwt.java index f2815d2c022..6c2e8da138a 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/NullValueTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/NullValueTestGwt.java @@ -6,6 +6,9 @@ import elemental2.promise.Promise; import io.deephaven.web.client.api.subscription.ViewportRow; +import java.math.BigDecimal; +import java.math.BigInteger; + @DoNotRunWith(Platform.HtmlUnitBug) public class NullValueTestGwt extends AbstractAsyncGwtTestCase { private final TableSourceBuilder tables = new TableSourceBuilder() @@ -19,7 +22,10 @@ public class NullValueTestGwt extends AbstractAsyncGwtTestCase { " \"MyChar=i==0?null:(char)i\",\n" + " \"MyByte=i==0?null:(byte)i\",\n" + " \"MyBoolean=i==0?null:true\",\n" + - " \"MyDate=i==0?null:epochNanosToInstant(i)\"\n" + + " \"MyString=i==0?null:``+i\",\n" + + " \"MyDate=i==0?null:epochNanosToInstant(i)\",\n" + + " \"MyBigInteger=i==0?null:java.math.BigInteger.valueOf(i)\",\n" + + " \"MyBigDecimal=i==0?null:java.math.BigDecimal.valueOf(i, 4)\"\n" + "])"); public void testNullTable() { @@ -42,7 +48,10 @@ public void testNullTable() { assertEquals("char", table.findColumn("MyChar").getType()); assertEquals("byte", table.findColumn("MyByte").getType()); assertEquals("java.lang.Boolean", table.findColumn("MyBoolean").getType()); + assertEquals("java.lang.String", table.findColumn("MyString").getType()); assertEquals("java.time.Instant", table.findColumn("MyDate").getType()); + assertEquals("java.math.BigInteger", table.findColumn("MyBigInteger").getType()); + assertEquals("java.math.BigDecimal", table.findColumn("MyBigDecimal").getType()); return Promise.resolve(table); }) @@ -67,8 +76,12 @@ public void testNullTable() { assertEquals((char) 1, valueRow.get(table.findColumn("MyChar")).asChar()); assertEquals((byte) 1, valueRow.get(table.findColumn("MyByte")).asByte()); assertEquals(true, valueRow.get(table.findColumn("MyBoolean")).asBoolean()); - assertEquals((long) 1, - valueRow.get(table.findColumn("MyDate")).cast().getWrapped()); + assertEquals("1", valueRow.get(table.findColumn("MyString")).asString()); + assertEquals(1, valueRow.get(table.findColumn("MyDate")).cast().getWrapped()); + assertEquals(BigInteger.ONE, + valueRow.get(table.findColumn("MyBigInteger")).cast().getWrapped()); + assertEquals(BigDecimal.valueOf(1, 4), + valueRow.get(table.findColumn("MyBigDecimal")).cast().getWrapped()); }, 1000); }) .then(this::finish).catch_(this::report); From 713007d368390f17cb0b9d916c503abd8505c685 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Tue, 9 Jan 2024 16:10:50 -0600 Subject: [PATCH 31/31] Fix selenium port mapping --- web/client-api/client-api.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/client-api/client-api.gradle b/web/client-api/client-api.gradle index 5aa62ba42bd..deae2f32c9c 100644 --- a/web/client-api/client-api.gradle +++ b/web/client-api/client-api.gradle @@ -113,7 +113,7 @@ def createSelenium = tasks.register('createSelenium', DockerCreateContainer) { t // This provides a hostname that can be referenced from inside the docker container to access the host // OS, and connect to the test server. t.hostConfig.extraHosts.add('host.docker.internal:host-gateway') - t.hostConfig.portBindings.set(["4444:$seleniumPort"]) + t.hostConfig.portBindings.set(["$seleniumPort:4444"]) t.hostConfig.network.set(deephavenDocker.networkName.get()) } def startSelenium = tasks.register('startSelenium', DockerStartContainer) {t -> @@ -139,7 +139,7 @@ def gwtIntegrationTest = tasks.register('gwtIntegrationTest', Test) { t -> t.dependsOn(deephavenDocker.portTask, seleniumHealthy) t.finalizedBy(deephavenDocker.endTask, stopSelenium) doFirst { - def webdriverUrl = 'http://localhost:4444/' + def webdriverUrl = "http://localhost:${seleniumPort}/" t.systemProperty('gwt.args', ["-runStyle io.deephaven.web.junit.RunStyleRemoteWebDriver:${webdriverUrl}?firefox", '-ea', '-style PRETTY',