diff --git a/src/main/kotlin/com/toasttab/pulseman/AppStrings.kt b/src/main/kotlin/com/toasttab/pulseman/AppStrings.kt index 28a905e..72b05f8 100755 --- a/src/main/kotlin/com/toasttab/pulseman/AppStrings.kt +++ b/src/main/kotlin/com/toasttab/pulseman/AppStrings.kt @@ -105,6 +105,7 @@ object AppStrings { const val PROJECT_FILE_DIALOG_TITLE = "Select Project File" const val PROTO_CLASS_NOT_SELECTED = "Proto class not selected" const val PROPERTIES = "Properties" + const val PROPERTY_FILTER = "Property Filter" const val PULSAR_URL = "Pulsar URL" const val PULSEMAN = "Pulseman" const val RECEIVE = "Receive" diff --git a/src/main/kotlin/com/toasttab/pulseman/pulsar/MessageHandling.kt b/src/main/kotlin/com/toasttab/pulseman/pulsar/MessageHandling.kt index fe185fb..a0c3fbc 100755 --- a/src/main/kotlin/com/toasttab/pulseman/pulsar/MessageHandling.kt +++ b/src/main/kotlin/com/toasttab/pulseman/pulsar/MessageHandling.kt @@ -22,4 +22,12 @@ import org.apache.pulsar.client.api.Message */ interface MessageHandling { fun parseMessage(message: Message) + + fun skipMessage(message: Message, propertyFilter: Map): Boolean { + if (propertyFilter.isEmpty()) return false + // Skip if any filter doesn't match + return propertyFilter.any { (filterKey, filterValue) -> + message.properties[filterKey] != filterValue + } + } } diff --git a/src/main/kotlin/com/toasttab/pulseman/pulsar/MessageHandlingClassImpl.kt b/src/main/kotlin/com/toasttab/pulseman/pulsar/MessageHandlingClassImpl.kt index e99113f..28a626b 100755 --- a/src/main/kotlin/com/toasttab/pulseman/pulsar/MessageHandlingClassImpl.kt +++ b/src/main/kotlin/com/toasttab/pulseman/pulsar/MessageHandlingClassImpl.kt @@ -31,12 +31,17 @@ import java.time.Instant */ class MessageHandlingClassImpl( private val selectedProtoClass: SingleSelection, + private val propertyFilter: () -> Map, private val receivedMessages: SnapshotStateList, private val setUserFeedback: (String) -> Unit ) : MessageHandling { override fun parseMessage(message: Message) { try { + val currentFilter = propertyFilter() + if (skipMessage(message, currentFilter)) { + return + } val proto = selectedProtoClass.selected ?: run { setUserFeedback(NO_CLASS_SELECTED_DESERIALIZE) return diff --git a/src/main/kotlin/com/toasttab/pulseman/pulsar/MessageHandlingImpl.kt b/src/main/kotlin/com/toasttab/pulseman/pulsar/MessageHandlingImpl.kt index 009e4ac..1d8085d 100755 --- a/src/main/kotlin/com/toasttab/pulseman/pulsar/MessageHandlingImpl.kt +++ b/src/main/kotlin/com/toasttab/pulseman/pulsar/MessageHandlingImpl.kt @@ -30,12 +30,17 @@ import java.time.Instant */ class MessageHandlingImpl( private val messageType: SingleSelection, + private val propertyFilter: () -> Map, private val receivedMessages: SnapshotStateList, private val setUserFeedback: (String) -> Unit ) : MessageHandling { override fun parseMessage(message: Message) { try { + val currentFilter = propertyFilter() + if (skipMessage(message, currentFilter)) { + return + } val messageString = messageType.selected?.deserialize(message.data) val publishTime = Instant.ofEpochMilli(message.publishTime) receivedMessages.add( diff --git a/src/main/kotlin/com/toasttab/pulseman/pulsar/Pulsar.kt b/src/main/kotlin/com/toasttab/pulseman/pulsar/Pulsar.kt index 170a15a..6816c30 100755 --- a/src/main/kotlin/com/toasttab/pulseman/pulsar/Pulsar.kt +++ b/src/main/kotlin/com/toasttab/pulseman/pulsar/Pulsar.kt @@ -24,7 +24,6 @@ import com.toasttab.pulseman.AppStrings.EXCEPTION import com.toasttab.pulseman.AppStrings.FAILED_TO_CLOSE_PULSAR import com.toasttab.pulseman.AppStrings.FAILED_TO_CREATE_CONSUMER import com.toasttab.pulseman.AppStrings.FAILED_TO_CREATE_PRODUCER -import com.toasttab.pulseman.AppStrings.FAILED_TO_DESERIALIZE_PROPERTIES import com.toasttab.pulseman.AppStrings.FAILED_TO_SETUP_PULSAR import com.toasttab.pulseman.AppStrings.MESSAGE_SENT_ID import com.toasttab.pulseman.AppStrings.NO_CLASS_GENERATED_TO_SEND @@ -129,18 +128,6 @@ class Pulsar( } } - private fun properties(): Map { - val propertiesJsonMap = pulsarSettings.propertySettings.propertyMap() - if (propertiesJsonMap.isNotBlank()) { - try { - return mapper.readValue(propertiesJsonMap, mapTypeRef) - } catch (ex: Exception) { - setUserFeedback("$FAILED_TO_DESERIALIZE_PROPERTIES=$propertiesJsonMap. $EXCEPTION=$ex") - } - } - return emptyMap() - } - fun sendMessage(message: ByteArray?): Boolean { var wrongSettings = false if (pulsarSettings.serviceUrl.value.isBlank()) { @@ -165,7 +152,7 @@ class Pulsar( ?.newMessage() ?.value(message) ?.eventTime(System.currentTimeMillis()) - ?.properties(properties()) + ?.properties(pulsarSettings.propertySettings.propertyMap(setUserFeedback)) ?.send() ?.let { messageId -> setUserFeedback("$MESSAGE_SENT_ID $messageId $ON_TOPIC $topic") diff --git a/src/main/kotlin/com/toasttab/pulseman/state/DropdownSelector.kt b/src/main/kotlin/com/toasttab/pulseman/state/DropdownSelector.kt index 64a94a7..f21c0c4 100644 --- a/src/main/kotlin/com/toasttab/pulseman/state/DropdownSelector.kt +++ b/src/main/kotlin/com/toasttab/pulseman/state/DropdownSelector.kt @@ -25,11 +25,12 @@ class DropdownSelector( ) { private val expanded = mutableStateOf(false) - fun getUI(currentlySelected: String): @Composable () -> Unit { + fun getUI(currentlySelected: String?, noOptionSelected: String = ""): @Composable () -> Unit { return { dropdownSelectorUI( expanded = expanded.value, currentlySelected = currentlySelected, + noOptionSelected = noOptionSelected, options = options, onChangeExpanded = expanded::onChange, onSelectedOption = onSelected diff --git a/src/main/kotlin/com/toasttab/pulseman/state/MultiSelectDropdown.kt b/src/main/kotlin/com/toasttab/pulseman/state/MultiSelectDropdown.kt new file mode 100644 index 0000000..d915791 --- /dev/null +++ b/src/main/kotlin/com/toasttab/pulseman/state/MultiSelectDropdown.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2021 Toast Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.toasttab.pulseman.state + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import com.toasttab.pulseman.view.multiSelectDropdownUI + +class MultiSelectDropdown( + private val label: String, + private val optionsProvider: () -> Map, + private val selectedItems: () -> Set, + private val onSelectionChanged: (Map) -> Unit +) { + private val expanded = mutableStateOf(false) + + fun getUI(): @Composable () -> Unit { + return { + val currentOptions = optionsProvider() + val currentSelectedKeys = selectedItems() + + // Clean up selected items that no longer exist in options + val validSelectedKeys = currentSelectedKeys.filter { it in currentOptions.keys } + if (validSelectedKeys.size != currentSelectedKeys.size) { + // Notify about the cleaned selection + val selectedMap = validSelectedKeys.associateWith { key -> + currentOptions[key] ?: "" + }.filterValues { it.isNotEmpty() } + onSelectionChanged(selectedMap) + } + + multiSelectDropdownUI( + label = label, + expanded = expanded.value, + selectedKeys = validSelectedKeys.toSet(), + options = currentOptions, + onChangeExpanded = { expanded.value = !expanded.value }, + onSelectionChanged = { selectedKeys -> + val selectedMap = selectedKeys.associateWith { key -> + currentOptions[key] ?: "" + }.filterValues { it.isNotEmpty() } + onSelectionChanged(selectedMap) + } + ) + } + } +} diff --git a/src/main/kotlin/com/toasttab/pulseman/state/PropertyConfiguration.kt b/src/main/kotlin/com/toasttab/pulseman/state/PropertyConfiguration.kt index a9a9023..3d0d9a7 100755 --- a/src/main/kotlin/com/toasttab/pulseman/state/PropertyConfiguration.kt +++ b/src/main/kotlin/com/toasttab/pulseman/state/PropertyConfiguration.kt @@ -15,6 +15,10 @@ package com.toasttab.pulseman.state +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.toasttab.pulseman.AppStrings import com.toasttab.pulseman.entities.TabValuesV3 import com.toasttab.pulseman.thirdparty.rsyntaxtextarea.RSyntaxTextArea import org.fife.ui.rsyntaxtextarea.SyntaxConstants @@ -36,5 +40,22 @@ class PropertyConfiguration( val sp = RTextScrollPane(textArea) - fun propertyMap(): String = textArea.text + fun propertyText(): String = textArea.text + + fun propertyMap(setUserFeedback: (String) -> Unit): Map { + val propertiesJsonMap = textArea.text + if (textArea.text.isNotBlank()) { + try { + return mapper.readValue(propertiesJsonMap, mapTypeRef) + } catch (ex: Exception) { + setUserFeedback("${AppStrings.FAILED_TO_DESERIALIZE_PROPERTIES}=$propertiesJsonMap. ${AppStrings.EXCEPTION}=$ex") + } + } + return emptyMap() + } + + companion object { + private val mapper = ObjectMapper().registerModule(KotlinModule.Builder().build()) + private val mapTypeRef = object : TypeReference>() {} + } } diff --git a/src/main/kotlin/com/toasttab/pulseman/state/ReceiveMessage.kt b/src/main/kotlin/com/toasttab/pulseman/state/ReceiveMessage.kt index 981e84a..d3c8718 100755 --- a/src/main/kotlin/com/toasttab/pulseman/state/ReceiveMessage.kt +++ b/src/main/kotlin/com/toasttab/pulseman/state/ReceiveMessage.kt @@ -24,6 +24,7 @@ import com.toasttab.pulseman.AppStrings.CLEARED_HISTORY import com.toasttab.pulseman.AppStrings.CONNECTION_CLOSED import com.toasttab.pulseman.AppStrings.FAILED_TO_CLOSE_PULSAR import com.toasttab.pulseman.AppStrings.FAIL_TO_SUBSCRIBE +import com.toasttab.pulseman.AppStrings.PROPERTY_FILTER import com.toasttab.pulseman.AppStrings.SUBSCRIBED import com.toasttab.pulseman.entities.ButtonState import com.toasttab.pulseman.entities.ReceivedMessages @@ -44,7 +45,8 @@ class ReceiveMessage( private val pulsarSettings: PulsarSettings, private val receivedMessages: SnapshotStateList, private val messageHandling: MessageHandling, - private val runTimeJarLoader: RunTimeJarLoader + private val runTimeJarLoader: RunTimeJarLoader, + private val propertyFilter: MutableState> ) { val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -52,6 +54,15 @@ class ReceiveMessage( private val clearState = mutableStateOf(ButtonState.WAITING) private val closeState = mutableStateOf(ButtonState.WAITING) + private val propertyFilterSelectorUI = MultiSelectDropdown( + label = PROPERTY_FILTER, + optionsProvider = { pulsarSettings.propertySettings.propertyMap(setUserFeedback) }, + selectedItems = { propertyFilter.value.keys }, + onSelectionChanged = { selectedMap -> + propertyFilter.value = selectedMap + } + ).getUI() + private val pulsar: MutableState = mutableStateOf(null) private var consumer: Consumer? = null @@ -117,7 +128,8 @@ class ReceiveMessage( onClear = ::onClear, onCloseConnection = ::onCloseConnection, receivedMessages = receivedMessages, - scrollState = stateVertical + scrollState = stateVertical, + propertyFilterSelectorUI = propertyFilterSelectorUI ) } } diff --git a/src/main/kotlin/com/toasttab/pulseman/state/TabState.kt b/src/main/kotlin/com/toasttab/pulseman/state/TabState.kt index e49e104..f82aff3 100755 --- a/src/main/kotlin/com/toasttab/pulseman/state/TabState.kt +++ b/src/main/kotlin/com/toasttab/pulseman/state/TabState.kt @@ -124,7 +124,7 @@ class TabState( serviceUrl = pulsarSettings.serviceUrl.value, selectedAuthClass = authSelector.selectedAuthClass.selected?.cls?.name, authJsonParameters = authSelector.authJsonParameters(), - propertyMap = propertySettings.propertyMap(), + propertyMap = propertySettings.propertyText(), serializationFormat = serializationFormat.value, protobufSettings = serializationState.protobufState.toProtobufTabValues(), textSettings = serializationState.textState.toTextTabValues(), diff --git a/src/main/kotlin/com/toasttab/pulseman/state/protocol/protobuf/ProtobufState.kt b/src/main/kotlin/com/toasttab/pulseman/state/protocol/protobuf/ProtobufState.kt index 3c99c3c..496d6f5 100644 --- a/src/main/kotlin/com/toasttab/pulseman/state/protocol/protobuf/ProtobufState.kt +++ b/src/main/kotlin/com/toasttab/pulseman/state/protocol/protobuf/ProtobufState.kt @@ -17,6 +17,7 @@ package com.toasttab.pulseman.state.protocol.protobuf import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList @@ -81,9 +82,11 @@ class ProtobufState( onChange = onChange ) + private val propertyFilter: MutableState> = mutableStateOf(emptyMap()) private val receivedMessages: SnapshotStateList = mutableStateListOf() private val messageHandling = MessageHandlingClassImpl( selectedProtoClass = protobufSelector.selectedClass, + propertyFilter = { propertyFilter.value }, receivedMessages = receivedMessages, setUserFeedback = setUserFeedback ) @@ -93,7 +96,8 @@ class ProtobufState( pulsarSettings = pulsarSettings, receivedMessages = receivedMessages, messageHandling = messageHandling, - runTimeJarLoader = pulsarMessageJars.runTimeJarLoader + runTimeJarLoader = pulsarMessageJars.runTimeJarLoader, + propertyFilter = propertyFilter ) private val convertProtoBufMessage = ConvertProtobufMessage( diff --git a/src/main/kotlin/com/toasttab/pulseman/state/protocol/text/TextState.kt b/src/main/kotlin/com/toasttab/pulseman/state/protocol/text/TextState.kt index 76c037f..b381cca 100644 --- a/src/main/kotlin/com/toasttab/pulseman/state/protocol/text/TextState.kt +++ b/src/main/kotlin/com/toasttab/pulseman/state/protocol/text/TextState.kt @@ -17,6 +17,7 @@ package com.toasttab.pulseman.state.protocol.text import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList @@ -59,10 +60,12 @@ class TextState( onChange = onChange ) + private val propertyFilter: MutableState> = mutableStateOf(emptyMap()) private val receivedMessages: SnapshotStateList = mutableStateListOf() private val messageHandling = MessageHandlingImpl( messageType = serializationTypeSelector.selectedEncoding, + propertyFilter = { propertyFilter.value }, receivedMessages = receivedMessages, setUserFeedback = setUserFeedback ) @@ -72,7 +75,8 @@ class TextState( pulsarSettings = pulsarSettings, receivedMessages = receivedMessages, messageHandling = messageHandling, - runTimeJarLoader = runTimeJarLoader + runTimeJarLoader = runTimeJarLoader, + propertyFilter = propertyFilter ) fun toTextTabValues() = TextTabValuesV3( diff --git a/src/main/kotlin/com/toasttab/pulseman/view/DropdownSelectorUI.kt b/src/main/kotlin/com/toasttab/pulseman/view/DropdownSelectorUI.kt index efb019f..461ae28 100644 --- a/src/main/kotlin/com/toasttab/pulseman/view/DropdownSelectorUI.kt +++ b/src/main/kotlin/com/toasttab/pulseman/view/DropdownSelectorUI.kt @@ -43,7 +43,8 @@ import com.toasttab.pulseman.AppStrings @Composable fun dropdownSelectorUI( expanded: Boolean, - currentlySelected: String, + currentlySelected: String?, + noOptionSelected: String = "", options: List, onChangeExpanded: () -> Unit, onSelectedOption: (String) -> Unit @@ -58,7 +59,11 @@ fun dropdownSelectorUI( .border(width = 0.8.dp, color = Color.White.copy(alpha = 0.5f), shape = RoundedCornerShape(8.dp)) ) { Row(modifier = Modifier.background(Color.Transparent).padding(8.dp, 8.dp)) { - Text(currentlySelected) + if (currentlySelected != null) { + Text(currentlySelected) + } else { + Text(noOptionSelected) + } Icon(Icons.Filled.ArrowDropDown, contentDescription = AppStrings.CHOOSE_OPTION) } } diff --git a/src/main/kotlin/com/toasttab/pulseman/view/MultiSelectDropdownUI.kt b/src/main/kotlin/com/toasttab/pulseman/view/MultiSelectDropdownUI.kt new file mode 100644 index 0000000..40adb87 --- /dev/null +++ b/src/main/kotlin/com/toasttab/pulseman/view/MultiSelectDropdownUI.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2021 Toast Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.toasttab.pulseman.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Checkbox +import androidx.compose.material.Divider +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.toasttab.pulseman.AppStrings + +/** + * This view allows the user to select multiple options from a dropdown list + */ +@Composable +fun multiSelectDropdownUI( + label: String, + expanded: Boolean, + selectedKeys: Set, + options: Map, + onChangeExpanded: () -> Unit, + onSelectionChanged: (Set) -> Unit +) { + Box { + IconButton( + onClick = { onChangeExpanded() } + ) { + Box( + modifier = Modifier + .background(Color.Transparent) + .border(width = 0.8.dp, color = Color.White.copy(alpha = 0.5f), shape = RoundedCornerShape(8.dp)) + ) { + Row(modifier = Modifier.background(Color.Transparent).padding(8.dp, 8.dp)) { + Text( + text = if (selectedKeys.isEmpty()) { + label + } else { + "$label (${selectedKeys.size})" + } + ) + Icon(Icons.Filled.ArrowDropDown, contentDescription = AppStrings.CHOOSE_OPTION) + } + } + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { onChangeExpanded() } + ) { + options.entries.forEachIndexed { index, (key, value) -> + DropdownMenuItem( + onClick = { + val newSelection = if (selectedKeys.contains(key)) { + selectedKeys - key + } else { + selectedKeys + key + } + onSelectionChanged(newSelection) + } + ) { + Row( + modifier = Modifier.weight(1F), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = selectedKeys.contains(key), + onCheckedChange = null + ) + Text( + text = key, + modifier = Modifier.padding(start = 8.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + if (index < options.size - 1) { + Divider() + } + } + } + } +} diff --git a/src/main/kotlin/com/toasttab/pulseman/view/ReceiveMessageUI.kt b/src/main/kotlin/com/toasttab/pulseman/view/ReceiveMessageUI.kt index 9ea64cb..ec3ffd4 100755 --- a/src/main/kotlin/com/toasttab/pulseman/view/ReceiveMessageUI.kt +++ b/src/main/kotlin/com/toasttab/pulseman/view/ReceiveMessageUI.kt @@ -76,7 +76,8 @@ fun receiveMessageUI( onClear: () -> Unit, onCloseConnection: () -> Unit, receivedMessages: List, - scrollState: ScrollState + scrollState: ScrollState, + propertyFilterSelectorUI: @Composable () -> Unit ) { Column { Row { @@ -109,6 +110,8 @@ fun receiveMessageUI( ) { onCloseConnection() } + + propertyFilterSelectorUI() } Box(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.verticalScroll(scrollState)) { diff --git a/src/test/kotlin/com/toasttab/pulseman/pulsar/PulsarITest.kt b/src/test/kotlin/com/toasttab/pulseman/pulsar/PulsarITest.kt index f891a6e..d98a4d4 100644 --- a/src/test/kotlin/com/toasttab/pulseman/pulsar/PulsarITest.kt +++ b/src/test/kotlin/com/toasttab/pulseman/pulsar/PulsarITest.kt @@ -61,7 +61,7 @@ class PulsarITest : PulsarITestSupport() { every { serviceUrl } returns mutableStateOf(pulsarContainer.pulsarBrokerUrl) every { topic } returns mutableStateOf(testTopic) every { propertySettings } returns mockk(relaxed = true) { - every { propertyMap() } returns testPropertyString + every { propertyMap(any()) } returns mapOf("key1" to "value1", "key2" to "value2") } } runTimeJarLoader = RunTimeJarLoader() diff --git a/src/test/kotlin/com/toasttab/pulseman/pulsar/PulsarTextMessageITest.kt b/src/test/kotlin/com/toasttab/pulseman/pulsar/PulsarTextMessageITest.kt index 63fc780..7e93295 100644 --- a/src/test/kotlin/com/toasttab/pulseman/pulsar/PulsarTextMessageITest.kt +++ b/src/test/kotlin/com/toasttab/pulseman/pulsar/PulsarTextMessageITest.kt @@ -49,7 +49,7 @@ class PulsarTextMessageITest : PulsarITestSupport() { every { serviceUrl } returns mutableStateOf(pulsarContainer.pulsarBrokerUrl) every { topic } returns mutableStateOf(testTopic) every { propertySettings } returns mockk(relaxed = true) { - every { propertyMap() } returns testPropertyString + every { propertyMap(any()) } returns mapOf("key1" to "value1", "key2" to "value2") } } runTimeJarLoader = RunTimeJarLoader()