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) }