diff --git a/pom.xml b/pom.xml
index 95dd861..70635c4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
org.janelia.saalfeldlab
saalfx
- 0.7.1-SNAPSHOT
+ 0.8.0-SNAPSHOT
Saal FX
Saalfeld lab JavaFX tools and extensions
diff --git a/src/main/java/org/janelia/saalfeldlab/control/mcu/MCUControlPanel.java b/src/main/java/org/janelia/saalfeldlab/control/mcu/MCUControlPanel.java
index 4830f07..69fede6 100644
--- a/src/main/java/org/janelia/saalfeldlab/control/mcu/MCUControlPanel.java
+++ b/src/main/java/org/janelia/saalfeldlab/control/mcu/MCUControlPanel.java
@@ -3,11 +3,7 @@
*/
package org.janelia.saalfeldlab.control.mcu;
-import javax.sound.midi.InvalidMidiDataException;
-import javax.sound.midi.MidiMessage;
-import javax.sound.midi.Receiver;
-import javax.sound.midi.ShortMessage;
-import javax.sound.midi.Transmitter;
+import javax.sound.midi.*;
/**
* @author Stephan Saalfeld <saalfelds@janelia.hhmi.org>
@@ -18,11 +14,13 @@ public abstract class MCUControlPanel implements Receiver {
private static final int STATUS_KEY = 0x90;
private static final int STATUS_FADER = 0xe8;
+ private final MidiDevice device;
private final Transmitter trans;
private final Receiver rec;
- public MCUControlPanel(final Transmitter trans, final Receiver rec) {
+ public MCUControlPanel(final MidiDevice device, final Transmitter trans, final Receiver rec) {
+ this.device = device;
this.trans = trans;
this.rec = rec;
trans.setReceiver(this);
@@ -97,5 +95,6 @@ public void close() {
trans.close();
rec.close();
+ device.close();
}
}
diff --git a/src/main/java/org/janelia/saalfeldlab/control/mcu/XTouchMiniMCUControlPanel.java b/src/main/java/org/janelia/saalfeldlab/control/mcu/XTouchMiniMCUControlPanel.java
index bfa5a81..18d9e45 100644
--- a/src/main/java/org/janelia/saalfeldlab/control/mcu/XTouchMiniMCUControlPanel.java
+++ b/src/main/java/org/janelia/saalfeldlab/control/mcu/XTouchMiniMCUControlPanel.java
@@ -60,10 +60,9 @@ public class XTouchMiniMCUControlPanel extends MCUControlPanel {
}
private final MCUFaderControl fader = new MCUFaderControl();
+ public XTouchMiniMCUControlPanel(final MidiDevice device, final Transmitter trans, final Receiver rec) {
- public XTouchMiniMCUControlPanel(final Transmitter trans, final Receiver rec) {
-
- super(trans, rec);
+ super(device, trans, rec);
for (int i = 0; i < vpots.length; ++i)
vpots[i] = new MCUVPotControl(vpotLedIds[i], rec);
@@ -149,8 +148,9 @@ public static XTouchMiniMCUControlPanel build(final String deviceDescription) th
MidiDevice recDev = null;
Receiver rec = null;
+ MidiDevice device = null;
for (final Info info : MidiSystem.getMidiDeviceInfo()) {
- final MidiDevice device = MidiSystem.getMidiDevice(info);
+ device = MidiSystem.getMidiDevice(info);
final String lowerDeviceDescription = deviceDescription.toLowerCase();
if (info.getDescription().toLowerCase().contains(lowerDeviceDescription) || info.getName().toLowerCase().contains(lowerDeviceDescription)) {
if (device.getMaxTransmitters() != 0) {
@@ -164,10 +164,10 @@ public static XTouchMiniMCUControlPanel build(final String deviceDescription) th
}
}
- if (!(trans == null || rec == null)) {
+ if (!(device == null || trans == null || rec == null)) {
transDev.open();
recDev.open();
- final XTouchMiniMCUControlPanel panel = new XTouchMiniMCUControlPanel(trans, rec);
+ final XTouchMiniMCUControlPanel panel = new XTouchMiniMCUControlPanel(device, trans, rec);
trans.setReceiver(panel);
panel.reset();
return panel;
diff --git a/src/main/kotlin/org/janelia/saalfeldlab/fx/Tasks.kt b/src/main/kotlin/org/janelia/saalfeldlab/fx/Tasks.kt
index 6a1b6af..0b64ad5 100644
--- a/src/main/kotlin/org/janelia/saalfeldlab/fx/Tasks.kt
+++ b/src/main/kotlin/org/janelia/saalfeldlab/fx/Tasks.kt
@@ -1,9 +1,12 @@
package org.janelia.saalfeldlab.fx
import com.google.common.util.concurrent.ThreadFactoryBuilder
+import javafx.beans.value.ChangeListener
import javafx.concurrent.Task
+import javafx.concurrent.Worker
import javafx.concurrent.Worker.State.*
import javafx.concurrent.WorkerStateEvent
+import javafx.event.EventHandler
import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread
import org.slf4j.LoggerFactory
import java.util.concurrent.ExecutorService
@@ -41,7 +44,7 @@ private val THREAD_FACTORY: ThreadFactory = ThreadFactoryBuilder()
.setNameFormat("task-thread-%d")
.build()
-private val TASK_SERVICE = Executors.newCachedThreadPool(THREAD_FACTORY)
+private val TASK_SERVICE = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() - 1, THREAD_FACTORY)
/**
* Convenience wrapper class around [Task]
@@ -52,6 +55,7 @@ private val TASK_SERVICE = Executors.newCachedThreadPool(THREAD_FACTORY)
*/
class UtilityTask(private val onCall: (UtilityTask) -> V) : Task() {
+ private var executorService : ExecutorService = TASK_SERVICE
private var onFailedSet = false
companion object {
@@ -61,11 +65,11 @@ class UtilityTask(private val onCall: (UtilityTask) -> V) : Task() {
override fun call(): V? {
try {
/* If no `onEnd/onFail` has been set, then we should listen for thrown exceptions and throw them */
- if (!this.onFailedSet) setDefaultExceptionHandler()
+ if (!onFailedSet) setDefaultOnFailed()
return onCall(this)
} catch (e: Exception) {
+ LOG.trace("Task Exception (cancelled=$isCancelled): ", e)
if (isCancelled) {
- LOG.debug("Task was cancelled")
return null
}
throw e
@@ -76,7 +80,7 @@ class UtilityTask(private val onCall: (UtilityTask) -> V) : Task() {
super.updateValue(value)
}
- private fun setDefaultExceptionHandler() {
+ private fun setDefaultOnFailed() {
InvokeOnJavaFXApplicationThread {
this.onFailed { _, task -> LOG.error(task.exception.stackTraceToString()) }
}
@@ -85,80 +89,152 @@ class UtilityTask(private val onCall: (UtilityTask) -> V) : Task() {
/**
* Builder-style function to set [SUCCEEDED] callback.
*
+ * @param append flag to determine behavior if an existing `onSuccess` callback is present:
+ * - if `true`, the current callback will be called prior to this `consumer` being called
+ * - if `false`, the prior callback will be removed and never called.
+ * - if `null`, this will throw a runtime exception if an existing callback is present.
+ * - This is meant to help unintended overrides of existing callbacks when `append` is not explicitly specified
* @param consumer to be called when [SUCCEEDED]
* @return this
*/
@JvmSynthetic
- fun onSuccess(consumer: (WorkerStateEvent, UtilityTask) -> Unit): UtilityTask {
- this.setOnSucceeded { event -> consumer(event, this) }
+ fun onSuccess(append: Boolean? = null, consumer: (WorkerStateEvent, UtilityTask) -> Unit): UtilityTask {
+ val appendCallbacks = onSucceeded?.appendCallbacks(append, consumer)
+ val consumerEvent = EventHandler { event -> consumer(event, this) }
+ setOnSucceeded(appendCallbacks ?: consumerEvent)
return this
}
/**
* Builder-style function to set [CANCELLED] callback.
*
+ * @param append flag to determine behavior if an existing `onCancelled` callback is present:
+ * - if `true`, the current callback will be called prior to this `consumer` being called
+ * - if `false`, the prior callback will be removed and never called.
+ * - if `null`, this will throw a runtime exception if an existing callback is present.
+ * - This is meant to help unintended overrides of existing callbacks when `append` is not explicitly specified
* @param consumer to be called when [CANCELLED]
* @return this
*/
@JvmSynthetic
- fun onCancelled(consumer: (WorkerStateEvent, UtilityTask) -> Unit): UtilityTask {
- this.setOnCancelled { event -> consumer(event, this) }
+ fun onCancelled(append: Boolean? = null, consumer: (WorkerStateEvent, UtilityTask) -> Unit): UtilityTask {
+ val appendCallbacks = onCancelled?.appendCallbacks(append, consumer)
+ val consumerEvent = EventHandler { event -> consumer(event, this) }
+ setOnCancelled(appendCallbacks ?: consumerEvent)
return this
}
/**
* Builder-style function to set [FAILED] callback.
*
+ * @param append flag to determine behavior if an existing `onFailed` callback is present:
+ * - if `true`, the current callback will be called prior to this `consumer` being called
+ * - if `false`, the prior callback will be removed and never called.
+ * - if `null`, this will throw a runtime exception if an existing callback is present.
+ * - This is meant to help unintended overrides of existing callbacks when `append` is not explicitly specified
* @param consumer to be called when [FAILED]
* @return this
*/
@JvmSynthetic
- fun onFailed(consumer: (WorkerStateEvent, UtilityTask) -> Unit): UtilityTask {
- onFailedSet = true
- this.setOnFailed { event -> consumer(event, this) }
+ fun onFailed(append: Boolean? = null, consumer: (WorkerStateEvent, UtilityTask) -> Unit): UtilityTask {
+ this.onFailedSet = true
+ val eventHandler = onFailed?.appendCallbacks(append, consumer) ?: EventHandler { event -> consumer(event, this) }
+ this.setOnFailed(eventHandler)
return this
}
+
+ private var onEndListener: ChangeListener? = null
+
/**
* Builder-style function to set when the task ends, either by [SUCCEEDED], [CANCELLED], or [FAILED].
*
+ * @param append flag to determine behavior if an existing `onEnd` callback is present:
+ * - if `true`, the current callback will be called prior to this `consumer` being called
+ * - if `false`, the prior callback will be removed and never called.
+ * - if `null`, this will throw a runtime exception if an existing callback is present.
+ * - This is meant to help unintended overrides of existing callbacks when `append` is not explicitly specified
* @param consumer to be called when task ends
* @return this
*/
@JvmSynthetic
- fun onEnd(consumer: (UtilityTask) -> Unit): UtilityTask {
- this.stateProperty().addListener { _, _, newv ->
+ fun onEnd(append: Boolean? = null, consumer: (UtilityTask) -> Unit): UtilityTask {
+ //TODO Caleb: Consider renaming `onEnd` to `finally` since this is trigger on end for ANY reason, even if an
+ // Exception was thrown. Or Maybe a separate `finally` which does what this currently does, and then change `onEnd`
+ // to NOT trigger if an excpetion occures (that isn't handled by the exception handler)
+ onEndListener = onEndListener?.let { oldListener ->
+ stateProperty().removeListener(oldListener)
+ if (append == null)
+ throw TaskStateCallbackOverrideException("Overriding existing handler; If intentional, pass `false` for `append`")
+ if (append) {
+ ChangeListener { obs, oldv, newv ->
+ when (newv) {
+ SUCCEEDED, CANCELLED, FAILED -> {
+ oldListener.changed(obs, oldv, newv)
+ consumer(this)
+ }
+
+ else -> Unit
+ }
+ }
+ } else null
+ } ?: ChangeListener { _, _, newv ->
when (newv) {
SUCCEEDED, CANCELLED, FAILED -> consumer(this)
else -> Unit
}
}
+ this.stateProperty().addListener(onEndListener)
return this
}
- fun onSuccess(consumer: BiConsumer>): UtilityTask {
- return onSuccess { e, t -> consumer.accept(e, t) }
+
+ /**
+ *
+ * @see [onSuccess]
+ */
+ @JvmOverloads
+ fun onSuccess(append: Boolean? = null, consumer: BiConsumer>): UtilityTask {
+ return onSuccess(append) { e, t -> consumer.accept(e, t) }
}
- fun onCancelled(consumer: BiConsumer>): UtilityTask {
- return onCancelled { e, t -> consumer.accept(e, t) }
+ /**
+ *
+ * @see [onCancelled]
+ */
+ @JvmOverloads
+ fun onCancelled(append: Boolean? = null, consumer: BiConsumer>): UtilityTask {
+ return onCancelled(append) { e, t -> consumer.accept(e, t) }
}
- fun onFailed(consumer: BiConsumer>): UtilityTask {
- return onFailed { e, t -> consumer.accept(e, t) }
+ /**
+ *
+ * @see [onFailed]
+ */@JvmOverloads
+ fun onFailed(append: Boolean? = null, consumer: BiConsumer>): UtilityTask {
+ return onFailed(append) { e, t -> consumer.accept(e, t) }
}
- fun onEnd(consumer: Consumer>): UtilityTask {
- return onEnd { t -> consumer.accept(t) }
+ /**
+ *
+ * @see [onEnd]
+ */
+ @JvmOverloads
+ fun onEnd(append: Boolean? = null, consumer: Consumer>): UtilityTask {
+ return onEnd(append) { t -> consumer.accept(t) }
}
/**
* Submit this task to the [executorService].
*
* @param executorService to execute this task on.
+ * @return this task
*/
- fun submit(executorService: ExecutorService) {
- executorService.submit(this)
+ @JvmOverloads
+ fun submit(executorService: ExecutorService = this.executorService) : UtilityTask {
+ this.executorService = executorService;
+ this.executorService.submit(this)
+ return this
}
/**
@@ -166,20 +242,23 @@ class UtilityTask(private val onCall: (UtilityTask) -> V) : Task() {
* This will return after the task completes, but possibbly BEFORE the [onSuccess]/[onEnd] call finish.
*
* @param executorService to execute this task on.
+ * @return the result of this task, blocking if not yet done.
*/
@JvmOverloads
- fun submitAndWait(executorService: ExecutorService = TASK_SERVICE): V {
- executorService.submit(this)
+ fun submitAndWait(executorService: ExecutorService = this.executorService): V {
+ this.executorService = executorService
+ this.executorService.submit(this)
return this.get()
}
- /**
- * Submit this task on a default [ExecutorService].
- *
- * @return this
- */
- fun submit(): UtilityTask {
- submit(TASK_SERVICE)
- return this
+ private fun EventHandler.appendCallbacks(append: Boolean? = false, consumer: (WorkerStateEvent, UtilityTask) -> Unit): EventHandler? {
+ if (append == null) throw TaskStateCallbackOverrideException("Overriding existing handler; If intentional, pass `false` for `append`")
+ if (!append) return null
+ return EventHandler { event ->
+ this.handle(event)
+ consumer(event, this@UtilityTask)
+ }
}
+
+ private class TaskStateCallbackOverrideException(override val message: String?) : RuntimeException(message)
}
diff --git a/src/main/kotlin/org/janelia/saalfeldlab/fx/extensions/ObservableExtensions.kt b/src/main/kotlin/org/janelia/saalfeldlab/fx/extensions/ObservableExtensions.kt
index 824eae0..8d4ab35 100644
--- a/src/main/kotlin/org/janelia/saalfeldlab/fx/extensions/ObservableExtensions.kt
+++ b/src/main/kotlin/org/janelia/saalfeldlab/fx/extensions/ObservableExtensions.kt
@@ -116,8 +116,27 @@ class WritableSubclassDelegate(private val obs: WritableValue, pri
}
}
-fun ObservableValue.addTriggeredListener(triggerWith : T = value, listener : (ObservableValue?, T, T) -> Unit) {
- val changeListener = ChangeListener { observable, oldValue, newValue -> listener(observable, oldValue, newValue) }
- addListener(changeListener)
- listener(this, value, triggerWith)
+fun interface SelfRefrentialListener : ChangeListener {
+
+ override fun changed(observable: ObservableValue?, oldValue: T, newValue: T) {
+ changedWithSelf(observable, oldValue, newValue)
+ }
+
+ fun ChangeListener.changedWithSelf(observable: ObservableValue?, oldValue: T, newValue: T)
+}
+
+fun ObservableValue.addWithListener(triggerWith : T? = value, listener: SelfRefrentialListener) {
+ this.addListener(listener)
+ triggerWith?.let {
+ listener.changed(this, value, value)
+ }
+}
+
+fun ObservableValue.addTriggeredWithListener(triggerWith : T = value, listener: SelfRefrentialListener) {
+ addWithListener(triggerWith, listener)
+}
+
+fun ObservableValue.addTriggeredListener(triggerWith: T = value, listener: ChangeListener) {
+ this.addListener(listener)
+ listener.changed(this, value, value)
}
diff --git a/src/test/kotlin/org/janelia/saalfeldlab/fx/TasksTest.kt b/src/test/kotlin/org/janelia/saalfeldlab/fx/TasksTest.kt
index 2ab1eb3..e0832af 100644
--- a/src/test/kotlin/org/janelia/saalfeldlab/fx/TasksTest.kt
+++ b/src/test/kotlin/org/janelia/saalfeldlab/fx/TasksTest.kt
@@ -12,12 +12,16 @@ import org.junit.Assert
import org.slf4j.LoggerFactory
import org.testfx.framework.junit.ApplicationTest
import org.testfx.util.WaitForAsyncUtils
+import java.io.File
+import java.io.PrintStream
import java.lang.invoke.MethodHandles
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
import kotlin.coroutines.cancellation.CancellationException
import kotlin.test.BeforeTest
import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
class TasksTest : ApplicationTest() {
@@ -26,9 +30,10 @@ class TasksTest : ApplicationTest() {
override fun start(stage: Stage) {
list = ListView()
val pane = Pane(list)
- stage.scene = Scene(pane, SCENE_WIDTH, SCENE_HEIGHT)
- .also { it.addEventFilter(Event.ANY) { LOG.trace("Filtering event in scene: {}", it) } }
- .also { it.addEventFilter(MouseEvent.ANY) { LOG.trace("Filtering mouse event in scene: {}", it) } }
+ stage.scene = Scene(pane, SCENE_WIDTH, SCENE_HEIGHT).apply {
+ addEventFilter(Event.ANY) { LOG.trace("Filtering event in scene: {}", it) }
+ addEventFilter(MouseEvent.ANY) { LOG.trace("Filtering mouse event in scene: {}", it) }
+ }
stage.show()
// This is necessary to make sure that the stage grabs focus from OS and events are registered
// https://stackoverflow.com/a/47685356/1725687
@@ -43,9 +48,9 @@ class TasksTest : ApplicationTest() {
}
@Test
- fun testOnSucess() {
+ fun `onSuccess runs when successful`() {
val testText = "Single onSuccess Test"
- Tasks.createTask { testText }
+ Tasks.createTask { testText }
.onSuccess { _, t -> list.items.add(t.value) }
.submit()
@@ -57,9 +62,9 @@ class TasksTest : ApplicationTest() {
@Test
- fun testOnEndWithSuccess() {
+ fun `onEnd and OnSuccess run when successful`() {
val endOnSuccessText = "Single onEnd Test, expecting success"
- val task = Tasks.createTask { endOnSuccessText }
+ val task = Tasks.createTask { endOnSuccessText }
.onSuccess { _, t -> list.items.add(t.value) }
.onEnd { t -> list.items.add(t.value) }
.submit()
@@ -73,9 +78,9 @@ class TasksTest : ApplicationTest() {
}
@Test
- fun testOnEndWithSuccessBlocking() {
+ fun `onEnd and onSuccess run after blocking when successful`() {
val endOnSuccessText = "Single onEnd Test, expecting success"
- val result = Tasks.createTask { endOnSuccessText }
+ val result = Tasks.createTask { endOnSuccessText }
.onSuccess { _, t -> list.items.add(t.value) }
.onEnd { t -> list.items.add(t.value) }
.submitAndWait()
@@ -88,13 +93,13 @@ class TasksTest : ApplicationTest() {
}
@Test
- fun testOnEndWithCancel() {
+ fun `onEnd runs when cancelled`() {
val items = list.items
val textWithoutCancel = "Single onEnd Test, expecting to never see this"
val textWithCancel = "Single onEnd Test, expecting cancel"
var canceled = false
val maxTime = LocalDateTime.now().plus(5, ChronoUnit.SECONDS)
- val task = Tasks.createTask {
+ val task = Tasks.createTask {
/* waiting for the task to be canceled. If too long, we have failed. */
while (!canceled || LocalDateTime.now().isBefore(maxTime)) {
sleep(20)
@@ -115,30 +120,55 @@ class TasksTest : ApplicationTest() {
Assert.assertArrayEquals(arrayOf(textWithCancel), items.toTypedArray())
}
+ private class ExceptionTestException : RuntimeException("Intentional Exception Test!")
+
@Test
- fun testOnEndWithFailure() {
+ fun `onEnd and default onFailed run when failed`() {
+
val items = list.items
val textWithFailure = "Single onEnd Test, expecting failure"
- /* Intentionally trigger failed*/
- val task = Tasks.createTask { throw RuntimeException("Forced failure!") }
- .onSuccess { _, t -> list.items.add(t.get()) }
- .onEnd { list.items.add(textWithFailure) }
- .submit()
- WaitForAsyncUtils.waitForFxEvents()
+
+ val stdout = System.out
+ val stderr = System.err
+ val devnull = object : PrintStream(nullOutputStream()) {
+ override fun write(b: Int) = Unit
+ }
+ System.setOut(devnull)
+ System.setErr(devnull)
+
+
+ /* Intentionally trigger failed, ensure `onEnd` is still triggered */
+ val task: UtilityTask<*>
+ try {
+ task = Tasks.createTask { throw ExceptionTestException() }
+ .onSuccess { _, t -> list.items.add(t.get()) }
+ .onEnd { list.items.add(textWithFailure) }
+ .submit()
+ WaitForAsyncUtils.waitForFxEvents()
+ } finally {
+ System.setOut(stdout)
+ System.setErr(stderr)
+ }
+
+
Assert.assertFalse(task.isCancelled)
Assert.assertTrue(task.isDone)
Assert.assertArrayEquals(arrayOf(textWithFailure), items.toTypedArray())
+
+ InvokeOnJavaFXApplicationThread.invokeAndWait {
+ assertIs(task.exception)
+ }
}
@Test
- fun testOnEndOnFailed() {
+ fun `onEnd and custom onFailed run when failed`() {
val items = list.items
val textWithEnd = "Single onFailure Test, expecting end"
val textWithFailure = "Single onFailure Test, expecting failure"
/* Intentionally trigger failure, with custom onFailed */
- val task = Tasks.createTask { throw RuntimeException("Forced failure!") }
+ val task = Tasks.createTask { throw ExceptionTestException() }
.onSuccess { _, t -> list.items.add(t.get()) }
.onEnd { list.items.add(textWithEnd) }
.onFailed { _, _ -> list.items.add(textWithFailure) }
@@ -149,40 +179,89 @@ class TasksTest : ApplicationTest() {
Assert.assertFalse(task.isCancelled)
Assert.assertTrue(task.isDone)
Assert.assertArrayEquals(arrayOf(textWithEnd, textWithFailure), items.toTypedArray())
- }
+ InvokeOnJavaFXApplicationThread.invokeAndWait {
+ assertIs(task.exception)
+ }
+ }
@Test
- fun testOnFailedDefaultExceptionHandler() {
-
- class IntentionalTestException(msg: String) : Throwable(msg)
-
- val items = list.items
- val textWithEnd = "Single onFailure Test, expecting end"
- val textWithFailure = "Single onFailure Test, expecting failure"
- /* Intentionally trigger failure, with custom onFailed. onEnd should also trigger*/
- val task = Tasks.createTask { throw IntentionalTestException("Forced failure!") }
- .onSuccess { _, t -> list.items.add(t.get()) }
- .onEnd { list.items.add(textWithEnd) }
- .onFailed { _, _ -> list.items.add(textWithFailure) }
- .submit()
+ fun `appending callbacks run in order when successful`() {
+ var success = 0
+ var end = 0
+ Tasks.createTask { "asdf" }
+ .onSuccess { _, _ -> success += 1 }
+ .onSuccess(true) { _, _ -> success *= 3 }
+ .onEnd { end += 1 }
+ .onEnd(true) { end *= 3 }
+ .submitAndWait()
WaitForAsyncUtils.waitForFxEvents()
+ assertEquals(3, success)
+ assertEquals(3, end)
- var thrownException: Throwable? = null
- InvokeOnJavaFXApplicationThread {
- thrownException = task.exception
+ var cancelled = 0
+ Tasks.createTask {
+ "asdf"
+ Thread.sleep(100)
}
+ .onSuccess { _, _ -> success += 1 }
+ .onSuccess(true) { _, _ -> success *= 3 }
+ .onCancelled { _, _ -> cancelled += 1 }
+ .onCancelled(true) { _, _ -> cancelled *= 3 }
+ .onEnd { end += 1 }
+ .onEnd(true) { end *= 3 }
+ .submit().also { it.cancel() }
WaitForAsyncUtils.waitForFxEvents()
+ assertEquals(3, success)
+ assertEquals(3, cancelled)
+ assertEquals(12, end)
+
+ var failed = 0
+ Tasks.createTask {
+ "asdf"
+ throw ExceptionTestException()
+ }
+ .onSuccess { _, _ -> success += 1 }
+ .onSuccess(true) { _, _ -> success *= 3 }
+ .onCancelled { _, _ -> cancelled += 1 }
+ .onCancelled(true) { _, _ -> cancelled *= 3 }
+ .onEnd { end += 1 }
+ .onEnd(true) { end *= 3 }
+ .onFailed { _, _ -> failed += 1}
+ .onFailed(true) { _, _ -> failed *= 3}
+ .submit()
- Assert.assertFalse(task.isCancelled)
- Assert.assertTrue(task.isDone)
- Assert.assertNotNull(thrownException)
+ WaitForAsyncUtils.waitForFxEvents()
+ assertEquals(3, success)
+ assertEquals(3, cancelled)
+ assertEquals(3, failed)
+ assertEquals(39, end)
+ }
- @Suppress("AssertBetweenInconvertibleTypes") /*Intentional, to trigger the failure case */
- Assert.assertEquals(IntentionalTestException::class.java, thrownException!!::class.java)
- Assert.assertArrayEquals(arrayOf(textWithEnd, textWithFailure), items.toTypedArray())
+ @Test
+ fun `append callbacks works as expected`() {
+ Assert.assertThrows(RuntimeException::class.java) {
+ Tasks.createTask { "asdf" }
+ .onSuccess { _, _ -> }
+ .onSuccess { _, _ -> }
+ }
+ Assert.assertThrows(RuntimeException::class.java) {
+ Tasks.createTask { "asdf" }
+ .onEnd { }
+ .onEnd { }
+ }
+ Assert.assertThrows(RuntimeException::class.java) {
+ Tasks.createTask { "asdf" }
+ .onFailed{ _, _ -> }
+ .onFailed{ _, _ -> }
+ }
+ Assert.assertThrows(RuntimeException::class.java) {
+ Tasks.createTask { "asdf" }
+ .onCancelled { _, _ -> }
+ .onCancelled { _, _ -> }
+ }
}
companion object {
diff --git a/src/test/kotlin/org/janelia/saalfeldlab/fx/midi/MidiActionSetTest.kt b/src/test/kotlin/org/janelia/saalfeldlab/fx/midi/MidiActionSetTest.kt
index 91e9e6d..ac69c72 100644
--- a/src/test/kotlin/org/janelia/saalfeldlab/fx/midi/MidiActionSetTest.kt
+++ b/src/test/kotlin/org/janelia/saalfeldlab/fx/midi/MidiActionSetTest.kt
@@ -12,6 +12,7 @@ import org.janelia.saalfeldlab.fx.midi.MidiPotentiometerEvent.Companion.POTENTIO
import org.janelia.saalfeldlab.fx.midi.MidiPotentiometerEvent.Companion.POTENTIOMETER_RELATIVE
import org.testfx.framework.junit.ApplicationTest
import org.testfx.util.WaitForAsyncUtils
+import javax.sound.midi.MidiDevice
import javax.sound.midi.MidiMessage
import javax.sound.midi.Receiver
import javax.sound.midi.Transmitter
@@ -23,18 +24,32 @@ class MidiActionSetTest : ApplicationTest() {
companion object XTouchMiniFxTest {
- private val dummyTransmitter = object : Transmitter {
- override fun close() {}
- override fun setReceiver(receiver: Receiver?) {}
- override fun getReceiver() = dummyReceiver
+ private val mockTransmitter = object : Transmitter {
+ override fun close() = Unit
+ override fun setReceiver(receiver: Receiver?) = Unit
+ override fun getReceiver() = mockReceiver
}
- private val dummyReceiver = object : Receiver {
- override fun close() {}
- override fun send(message: MidiMessage?, timeStamp: Long) {}
+ private val mockReceiver = object : Receiver {
+ override fun close() = Unit
+ override fun send(message: MidiMessage?, timeStamp: Long) = Unit
}
- private val dummyDevice = object : MCUControlPanel(dummyTransmitter, dummyReceiver) {
+ private val mockDevice = object : MidiDevice {
+ override fun close() = Unit
+ override fun getDeviceInfo(): MidiDevice.Info? = null
+ override fun open() = Unit
+ override fun isOpen(): Boolean = false
+ override fun getMicrosecondPosition(): Long = 0L
+ override fun getMaxReceivers(): Int = 0
+ override fun getMaxTransmitters(): Int = 0
+ override fun getReceiver(): Receiver? = null
+ override fun getReceivers(): MutableList? = null
+ override fun getTransmitter(): Transmitter? = null
+ override fun getTransmitters(): MutableList? = null
+ }
+
+ private val mockMidiControlPanel = object : MCUControlPanel(mockDevice, mockTransmitter, mockReceiver) {
val vpotControls = mutableMapOf()
val vpotControlsId = mutableMapOf()
@@ -44,12 +59,12 @@ class MidiActionSetTest : ApplicationTest() {
val buttonControls = mutableMapOf()
val buttonControlsId = mutableMapOf()
- override fun getVPotControl(i: Int) = vpotControls.putIfAbsent(i, MCUVPotControl(0, dummyReceiver)).let { vpotControls[i]!! }
- override fun getVPotControlById(i: Int) = vpotControlsId.putIfAbsent(i, MCUVPotControl(0, dummyReceiver)).let { vpotControlsId[i]!! }
+ override fun getVPotControl(i: Int) = vpotControls.putIfAbsent(i, MCUVPotControl(0, mockReceiver)).let { vpotControls[i]!! }
+ override fun getVPotControlById(i: Int) = vpotControlsId.putIfAbsent(i, MCUVPotControl(0, mockReceiver)).let { vpotControlsId[i]!! }
override fun getFaderControl(i: Int) = faderControls.putIfAbsent(i, MCUFaderControl()).let { faderControls[i]!! }
override fun getFaderControlById(i: Int) = faderControlsId.putIfAbsent(i, MCUFaderControl()).let { faderControlsId[i]!! }
- override fun getButtonControl(i: Int) = buttonControls.putIfAbsent(i, MCUButtonControl(0, dummyReceiver)).let { buttonControls[i]!! }
- override fun getButtonControlById(i: Int) = buttonControlsId.putIfAbsent(i, MCUButtonControl(0, dummyReceiver)).let { buttonControlsId[i]!! }
+ override fun getButtonControl(i: Int) = buttonControls.putIfAbsent(i, MCUButtonControl(0, mockReceiver)).let { buttonControls[i]!! }
+ override fun getButtonControlById(i: Int) = buttonControlsId.putIfAbsent(i, MCUButtonControl(0, mockReceiver)).let { buttonControlsId[i]!! }
override fun getNumVPotControls() = 8
override fun getNumButtonControls() = 26
override fun getNumFaderControls() = 1
@@ -66,18 +81,18 @@ class MidiActionSetTest : ApplicationTest() {
@AfterTest
fun resetControl() {
- dummyDevice.vpotControls.clear()
- dummyDevice.vpotControlsId.clear()
- dummyDevice.faderControls.clear()
- dummyDevice.faderControlsId.clear()
- dummyDevice.buttonControls.clear()
- dummyDevice.buttonControlsId.clear()
+ mockMidiControlPanel.vpotControls.clear()
+ mockMidiControlPanel.vpotControlsId.clear()
+ mockMidiControlPanel.faderControls.clear()
+ mockMidiControlPanel.faderControlsId.clear()
+ mockMidiControlPanel.buttonControls.clear()
+ mockMidiControlPanel.buttonControlsId.clear()
}
@Test
fun `absolute potentiometer`() {
var prevValue = -1
- MidiActionSet("Abs Pot", dummyDevice, root) {
+ MidiActionSet("Abs Pot", mockMidiControlPanel, root) {
POTENTIOMETER_ABSOLUTE(0) {
min = -223
max = 223
@@ -88,15 +103,15 @@ class MidiActionSetTest : ApplicationTest() {
root.installActionSet(this)
}
- dummyDevice.getVPotControl(0).value = -1000
+ mockMidiControlPanel.getVPotControl(0).value = -1000
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == -223)
- dummyDevice.getVPotControl(0).value = 1000
+ mockMidiControlPanel.getVPotControl(0).value = 1000
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 223)
- dummyDevice.getVPotControl(1).value = 30
+ mockMidiControlPanel.getVPotControl(1).value = 30
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 30)
@@ -105,7 +120,7 @@ class MidiActionSetTest : ApplicationTest() {
@Test
fun `relative potentiometer`() {
var prevValue = -1
- MidiActionSet("Rel Pot", dummyDevice, root) {
+ MidiActionSet("Rel Pot", mockMidiControlPanel, root) {
POTENTIOMETER_RELATIVE(0) {
min = -223
max = 223
@@ -116,15 +131,15 @@ class MidiActionSetTest : ApplicationTest() {
root.installActionSet(this)
}
- dummyDevice.getVPotControl(0).value = -1000
+ mockMidiControlPanel.getVPotControl(0).value = -1000
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == -7)
- dummyDevice.getVPotControl(0).value = 1000
+ mockMidiControlPanel.getVPotControl(0).value = 1000
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 7)
- dummyDevice.getVPotControl(1).value = 3
+ mockMidiControlPanel.getVPotControl(1).value = 3
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 3)
}
@@ -132,7 +147,7 @@ class MidiActionSetTest : ApplicationTest() {
@Test
fun `midi button`() {
var prevValue = Int.MIN_VALUE
- MidiActionSet("Button", dummyDevice, root) {
+ MidiActionSet("Button", mockMidiControlPanel, root) {
MidiButtonEvent.BUTTON(0) { onAction { prevValue = it!!.value } }
MidiButtonEvent.BUTTON_PRESED(1) { onAction { prevValue = it!!.value } }
@@ -144,32 +159,32 @@ class MidiActionSetTest : ApplicationTest() {
root.installActionSet(this)
}
- dummyDevice.getButtonControl(0).value = 10
+ mockMidiControlPanel.getButtonControl(0).value = 10
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 10)
- dummyDevice.getButtonControl(0).value = 0
+ mockMidiControlPanel.getButtonControl(0).value = 0
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 0)
- dummyDevice.getButtonControl(1).value = 100
+ mockMidiControlPanel.getButtonControl(1).value = 100
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 100)
- dummyDevice.getButtonControl(1).value = 0
+ mockMidiControlPanel.getButtonControl(1).value = 0
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 100)
- dummyDevice.getButtonControl(2).value = 50
+ mockMidiControlPanel.getButtonControl(2).value = 50
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 100)
- dummyDevice.getButtonControl(2).value = 0
+ mockMidiControlPanel.getButtonControl(2).value = 0
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 0)
- dummyDevice.getButtonControl(0).value = 50
+ mockMidiControlPanel.getButtonControl(0).value = 50
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 50)
- dummyDevice.getButtonControl(0).value = 0
+ mockMidiControlPanel.getButtonControl(0).value = 0
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 0)
}
@@ -178,18 +193,18 @@ class MidiActionSetTest : ApplicationTest() {
@Test
fun `toggle button`() {
var prevValue = false
- MidiActionSet("Toggle Button", dummyDevice, root) {
+ MidiActionSet("Toggle Button", mockMidiControlPanel, root) {
MidiToggleEvent.BUTTON_TOGGLE(0) { onAction { prevValue = it!!.isOn } }
root.installActionSet(this)
}
- dummyDevice.getButtonControl(0).value = 10
+ mockMidiControlPanel.getButtonControl(0).value = 10
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue)
- dummyDevice.getButtonControl(0).value = 0
+ mockMidiControlPanel.getButtonControl(0).value = 0
WaitForAsyncUtils.waitForFxEvents()
assert(!prevValue)
- dummyDevice.getButtonControl(0).value = 8
+ mockMidiControlPanel.getButtonControl(0).value = 8
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue)
}
@@ -197,7 +212,7 @@ class MidiActionSetTest : ApplicationTest() {
@Test
fun `midi fader`() {
var prevValue = Int.MIN_VALUE
- MidiActionSet("Fader", dummyDevice, root) {
+ MidiActionSet("Fader", mockMidiControlPanel, root) {
MidiFaderEvent.FADER(0) { onAction { prevValue = it!!.value } }
MidiFaderEvent.FADER(1) {
min = -100
@@ -207,34 +222,34 @@ class MidiActionSetTest : ApplicationTest() {
root.installActionSet(this)
}
- dummyDevice.getFaderControl(0).value = 10
+ mockMidiControlPanel.getFaderControl(0).value = 10
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 10)
- dummyDevice.getFaderControl(0).value = 0
+ mockMidiControlPanel.getFaderControl(0).value = 0
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 0)
- dummyDevice.getFaderControl(0).value = 8
+ mockMidiControlPanel.getFaderControl(0).value = 8
WaitForAsyncUtils.waitForFxEvents()
assert(prevValue == 8)
- dummyDevice.getFaderControl(1).value = (.5 * 127).toInt()
+ mockMidiControlPanel.getFaderControl(1).value = (.5 * 127).toInt()
WaitForAsyncUtils.waitForFxEvents()
assertEquals(0, prevValue)
- dummyDevice.getFaderControl(1).value = 127
+ mockMidiControlPanel.getFaderControl(1).value = 127
WaitForAsyncUtils.waitForFxEvents()
assertEquals(100, prevValue)
- dummyDevice.getFaderControl(1).value = 0
+ mockMidiControlPanel.getFaderControl(1).value = 0
WaitForAsyncUtils.waitForFxEvents()
assertEquals(-100, prevValue)
- dummyDevice.getFaderControl(1).value = 1270
+ mockMidiControlPanel.getFaderControl(1).value = 1270
WaitForAsyncUtils.waitForFxEvents()
assertEquals(100, prevValue)
- dummyDevice.getFaderControl(1).value = -1
+ mockMidiControlPanel.getFaderControl(1).value = -1
WaitForAsyncUtils.waitForFxEvents()
assertEquals(-100, prevValue)
}