diff --git a/CHANGELOG.md b/CHANGELOG.md index 466f1331c..d545b4a52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to the Zowe™ Explorer plug-in for IntelliJ IDEA™ will be documented in this file. +## `1.2.0 (2024-xx-yy)` + +* Feature: TSO CLI PA1 button functionality added ([48834cac](https://github.com/zowe/zowe-explorer-intellij/commit/48834cac)) +* Feature: Allocation units clarification added ([1ff218e4](https://github.com/zowe/zowe-explorer-intellij/commit/1ff218e4)) +* Feature: Now after allocating a dataset, a notification is shown instead of a dialog window ([20343651](https://github.com/zowe/zowe-explorer-intellij/commit/20343651)) +* Feature: VFS_CHANGES topic rework for encoding purposes ([3adaded3](https://github.com/zowe/zowe-explorer-intellij/commit/3adaded3)) +* Feature: Close all files in editor, related to the file/folder/dataset/member being deleted ([8a0d9980](https://github.com/zowe/zowe-explorer-intellij/commit/8a0d9980)) + + +* Bugfix: GitHub issue #159: Zowe config detection doesn't work correctly ([c73226f6](https://github.com/zowe/zowe-explorer-intellij/commit/c73226f6)) +* Bugfix: Warning during working set creation without masks/job filters is missing ([4fd0b22c](https://github.com/zowe/zowe-explorer-intellij/commit/4fd0b22c)) +* Bugfix: When opened file tabs bar is full, opening any dataset or USS file preserves only last 2 items Explorer items in the bar ([3a822fbb](https://github.com/zowe/zowe-explorer-intellij/commit/3a822fbb)) +* Bugfix: Unclear error message for list datasets/jobs when password is expired ([74fe5e86](https://github.com/zowe/zowe-explorer-intellij/commit/74fe5e86)) +* Bugfix: In dataset allocation dialog window, dataset name is reset when an error is received ([218f5a3a](https://github.com/zowe/zowe-explorer-intellij/commit/218f5a3a)) +* Bugfix: Issue creating member with the same name as the existing one ([6ebae1a0](https://github.com/zowe/zowe-explorer-intellij/commit/6ebae1a0)) +* Bugfix: "Overwrite for All" causes caches conflicts when dataset member is being copied to a USS with rewrite ([90b9ce17](https://github.com/zowe/zowe-explorer-intellij/commit/90b9ce17)) +* Bugfix: "Cut/Paste" doesn't work when moving a sequential dataset to a partitioned dataset ([f1cf4a9d](https://github.com/zowe/zowe-explorer-intellij/commit/f1cf4a9d)) +* Bugfix: Incorrect warning message when uploading a local file to a PDS ([6d9e5de3](https://github.com/zowe/zowe-explorer-intellij/commit/6d9e5de3)) +* Bugfix: It is possible to create a dataset when a connection is removed or invalid ([3df02fde](https://github.com/zowe/zowe-explorer-intellij/commit/3df02fde)) +* Bugfix: TSO EXEC command without operands causes the CLI to hang ([d071960a](https://github.com/zowe/zowe-explorer-intellij/commit/d071960a)) +* Bugfix: Connection is not fully reset to the last successful state and it causes errors ([f6d5a72e](https://github.com/zowe/zowe-explorer-intellij/commit/f6d5a72e)) + ## `1.1.2 (2024-01-22)` * Bugfix: Sync action does not work after file download ([bfb125d7](https://github.com/zowe/zowe-explorer-intellij/commit/bfb125d7)) diff --git a/build.gradle.kts b/build.gradle.kts index ac5d038aa..4992f6318 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,8 +19,8 @@ buildscript { plugins { id("org.sonarqube") version "3.3" - id("org.jetbrains.intellij") version "1.14.2" - kotlin("jvm") version "1.8.10" + id("org.jetbrains.intellij") version "1.17.2" + kotlin("jvm") version "1.9.22" java id("org.jetbrains.kotlinx.kover") version "0.6.1" } @@ -32,10 +32,10 @@ apply(plugin = "org.jetbrains.intellij") apply(from = "gradle/sonar.gradle") group = "org.zowe" -version = "1.1.2-223" -val remoteRobotVersion = "0.11.21" +version = "1.2.0-223" +val remoteRobotVersion = "0.11.22" val okHttp3Version = "4.12.0" -val kotestVersion = "5.6.2" +val kotestVersion = "5.8.1" repositories { mavenCentral() @@ -65,23 +65,20 @@ dependencies { implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("com.squareup.retrofit2:converter-scalars:2.9.0") implementation("com.squareup.okhttp3:okhttp:$okHttp3Version") - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.20") - implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.20") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") - implementation("org.jgrapht:jgrapht-core:1.5.1") + implementation("org.jgrapht:jgrapht-core:1.5.2") implementation("com.starxg:java-keytar:1.0.0") implementation("org.zowe.sdk:zowe-kotlin-sdk:0.4.0") implementation("com.ibm.mq:com.ibm.mq.allclient:9.3.4.1") - testImplementation("io.mockk:mockk:1.13.5") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.2") + testImplementation("io.mockk:mockk:1.13.9") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1") testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion") testImplementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion") testImplementation("com.squareup.okhttp3:mockwebserver:$okHttp3Version") testImplementation("com.squareup.okhttp3:okhttp-tls:$okHttp3Version") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2") - testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.9.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.1") + testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.1") } intellij { @@ -96,6 +93,10 @@ tasks { } } + withType { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + } + patchPluginXml { sinceBuild.set("223.7571") untilBuild.set("223.*") @@ -167,6 +168,15 @@ tasks { dependsOn(createOpenApiSourceJar) from(createOpenApiSourceJar) { into("lib/src") } } + + downloadRobotServerPlugin { + version.set(remoteRobotVersion) + } + + runIdeForUiTests { + systemProperty("idea.trust.all.projects", "true") + systemProperty("ide.show.tips.on.startup.default.value", "false") + } } /** @@ -256,18 +266,3 @@ val SmokeUiTest = task("smokeUiTest") { isDisabled.set(true) } } - -tasks { - withType { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - } -} - -tasks.downloadRobotServerPlugin { - version.set(remoteRobotVersion) -} - -tasks.runIdeForUiTests { - systemProperty("idea.trust.all.projects", "true") - systemProperty("ide.show.tips.on.startup.default.value", "false") -} diff --git a/src/main/kotlin/org/zowe/explorer/common/SettingsPropertyManager.kt b/src/main/kotlin/org/zowe/explorer/common/SettingsPropertyManager.kt new file mode 100644 index 000000000..d1d2a3457 --- /dev/null +++ b/src/main/kotlin/org/zowe/explorer/common/SettingsPropertyManager.kt @@ -0,0 +1,31 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ + +package org.zowe.explorer.common + +import java.util.* + +class SettingsPropertyManager + +/** + * Properties from the settings.properties file + */ +internal val settingsProperties by lazy { + Properties().apply { + load(SettingsPropertyManager::class.java.classLoader.getResourceAsStream("settings.properties")) + } +} + +/** + * Check if the debug mode is enabled + */ +fun isDebugModeEnabled(): Boolean { + return settingsProperties.getProperty("debug.mode")?.toBoolean() ?: false +} diff --git a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt index 429befcfe..a3b5eb4a9 100644 --- a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt +++ b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt @@ -31,6 +31,7 @@ import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.operations.* import org.zowe.explorer.utils.* import org.zowe.explorer.utils.crudable.Crudable +import org.zowe.explorer.utils.crudable.find import org.zowe.explorer.utils.crudable.getAll import org.zowe.kotlinsdk.ChangePassword import org.zowe.kotlinsdk.annotations.ZVersion @@ -53,7 +54,11 @@ class ConnectionDialog( * Private field * In case of DialogMode.UPDATE takes the last successful state from crudable, takes default state otherwise */ - private val lastSuccessfulState: ConnectionDialogState = if(state.mode == DialogMode.UPDATE) state.connectionConfig.toDialogState(crudable) else ConnectionDialogState() + private val lastSuccessfulState: ConnectionDialogState = + if(state.mode == DialogMode.UPDATE) crudable.find { it.uuid == state.connectionUuid } + .findAny() + .orElseGet { state.connectionConfig } + .toDialogState(crudable) else ConnectionDialogState() companion object { /** Show Test connection dialog and test the connection regarding the dialog state. diff --git a/src/main/kotlin/org/zowe/explorer/config/ws/ui/AbstractWsDialog.kt b/src/main/kotlin/org/zowe/explorer/config/ws/ui/AbstractWsDialog.kt index b7990e150..5c386e637 100644 --- a/src/main/kotlin/org/zowe/explorer/config/ws/ui/AbstractWsDialog.kt +++ b/src/main/kotlin/org/zowe/explorer/config/ws/ui/AbstractWsDialog.kt @@ -53,6 +53,15 @@ abstract class AbstractWsDialog { + companion object { + + // TODO: Remove when it becomes possible to mock class constructor with init section. + /** Wrapper for init() method. It is necessary only for test purposes for now. */ + private fun initialize(init: () -> Unit) { + init() + } + } + abstract val wsConfigClass: Class abstract val connectionClass: Class @@ -149,6 +158,14 @@ abstract class AbstractWsDialog + isOKActionEnabled = map.isEmpty() + } + } + override fun createCenterPanel(): JComponent { return panel } diff --git a/src/main/kotlin/org/zowe/explorer/dataops/DataOpsManager.kt b/src/main/kotlin/org/zowe/explorer/dataops/DataOpsManager.kt index f863560fb..c69dad2f7 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/DataOpsManager.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/DataOpsManager.kt @@ -26,6 +26,7 @@ import org.zowe.explorer.dataops.fetch.FileFetchProvider import org.zowe.explorer.dataops.log.LogFetcher import org.zowe.explorer.dataops.log.MFLogger import org.zowe.explorer.dataops.log.MFProcessInfo +import org.zowe.explorer.dataops.operations.mover.names.CopyPasteNameResolver interface DataOpsManager : Disposable { @@ -65,6 +66,8 @@ interface DataOpsManager : Disposable { fun getMFContentAdapter(file: VirtualFile): MFContentAdapter + fun getNameResolver(source: VirtualFile, destination: VirtualFile): CopyPasteNameResolver + fun isOperationSupported(operation: Operation<*>): Boolean @Throws(Throwable::class) diff --git a/src/main/kotlin/org/zowe/explorer/dataops/DataOpsManagerImpl.kt b/src/main/kotlin/org/zowe/explorer/dataops/DataOpsManagerImpl.kt index c15e185b2..3a5c0eaa1 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/DataOpsManagerImpl.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/DataOpsManagerImpl.kt @@ -27,6 +27,8 @@ import org.zowe.explorer.dataops.log.LogFetcher import org.zowe.explorer.dataops.log.MFLogger import org.zowe.explorer.dataops.log.MFProcessInfo import org.zowe.explorer.dataops.operations.OperationRunner +import org.zowe.explorer.dataops.operations.mover.names.CopyPasteNameResolver +import org.zowe.explorer.dataops.operations.mover.names.DefaultNameResolver import org.zowe.explorer.utils.associateListedBy import org.zowe.explorer.utils.findAnyNullable import org.zowe.explorer.utils.log @@ -137,6 +139,15 @@ class DataOpsManagerImpl : DataOpsManager { } private val mfContentAdapters by mfContentAdaptersDelegate + private val nameResolversDelegate = lazy { + CopyPasteNameResolver.EP.extensionList.buildComponents() + } + private val nameResolvers by nameResolversDelegate + + override fun getNameResolver(source: VirtualFile, destination: VirtualFile): CopyPasteNameResolver { + return nameResolvers.firstOrNull { it.accepts(source, destination) } ?: DefaultNameResolver() + } + /** * Checks if sync with mainframe is supported for provided object * @param file object on mainframe that should be checked on availability of synchronization diff --git a/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/ContentSynchronizer.kt b/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/ContentSynchronizer.kt index 21538189a..209cde815 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/ContentSynchronizer.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/ContentSynchronizer.kt @@ -69,4 +69,10 @@ interface ContentSynchronizer { */ fun isFileUploadNeeded(syncProvider: SyncProvider): Boolean + /** + * Marks file as not needed for synchronisation until the next time file is modified. + * @param syncProvider instance of [SyncProvider] class that contains file to mark. + */ + fun markAsNotNeededForSync(syncProvider: SyncProvider) + } diff --git a/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/DocumentedSyncProvider.kt b/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/DocumentedSyncProvider.kt index 312194c3a..0a588a0b8 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/DocumentedSyncProvider.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/DocumentedSyncProvider.kt @@ -17,12 +17,14 @@ import com.intellij.openapi.components.service import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.impl.DocumentImpl import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.StringUtilRt import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.LineSeparator import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.attributes.RemoteUssAttributes -import org.zowe.explorer.editor.DocumentChangeListener import org.zowe.explorer.utils.castOrNull +import org.zowe.explorer.utils.changeEncodingTo import org.zowe.explorer.vfs.MFVirtualFile import java.nio.charset.Charset import java.util.concurrent.atomic.AtomicBoolean @@ -89,6 +91,19 @@ class DocumentedSyncProvider( override val isReadOnly: Boolean get() = getDocument()?.isWritable != true + /** + * Initialize the file properties (charset, line separator) + * when the file is opened for the first time. + * @param content bytes of the content to init. + */ + private fun initFileProperties(content: ByteArray) { + val charset = getCurrentCharset() + val text = content.toString(charset) + val detectedLineSeparator = runCatching { StringUtil.detectSeparators(text) }.getOrNull() ?: LineSeparator.LF + file.detectedLineSeparator = detectedLineSeparator.separatorString + changeEncodingTo(file, charset) + } + /** * Puts initial content in file document. * @see SyncProvider.putInitialContent @@ -96,9 +111,7 @@ class DocumentedSyncProvider( override fun putInitialContent(content: ByteArray) { if (isInitialContentSet.compareAndSet(false, true)) { runCatching { - file.getOutputStream(null).use { - it.write(content) - } + initFileProperties(content) loadNewContent(content) }.onFailure { isInitialContentSet.set(false) @@ -106,11 +119,18 @@ class DocumentedSyncProvider( } } + /** Checks if the new content needs to be load */ + private fun isLoadNeeded(content: ByteArray): Boolean { + val currentContent = retrieveCurrentContent() + return !content.contentEquals(currentContent) + } + /** * Update content in file document. * @see SyncProvider.loadNewContent */ override fun loadNewContent(content: ByteArray) { + if (!isLoadNeeded(content)) return val document = getDocument() document.castOrNull()?.setAcceptSlashR(true) val wasReadOnly = isReadOnly @@ -118,10 +138,12 @@ class DocumentedSyncProvider( document?.setReadOnly(false) } val charset = getCurrentCharset() - document?.setText(String(content, charset)) + val text = content.toString(charset) + document?.setText(text) if (wasReadOnly) { document?.setReadOnly(true) } + saveDocument() } /** diff --git a/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt b/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt index 0e63ab91f..21c4d8c86 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt @@ -11,7 +11,10 @@ package org.zowe.explorer.dataops.content.synchronizer import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.vfs.VirtualFile import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.attributes.FileAttributes @@ -104,6 +107,29 @@ abstract class RemoteAttributedContentSynchronizer return fetchedAtLeastOnce.firstOrNull { syncProvider == it } != null } + /** + * It is only necessary to remove old file from cache while force overwriting. + * TODO: Not the best solution. Think on how to rework. + * @param file - file to remove. + */ + fun removeFromCacheAfterForceOverwriting(file: VirtualFile) { + val syncProvider = fetchedAtLeastOnce.firstOrNull { it.file == file } ?: return + fetchedAtLeastOnce.removeIf { it.file == file } + // if you will not delete the file than "Local cache conflict" dialog appear. + runWriteActionInEdtAndWait { + // close editor if file is opened to avoid IDE crash. + ProjectManager.getInstance().openProjects.forEach { + syncProvider.getDocument()?.let { document -> + val fileEditorManager = FileEditorManager.getInstance(it) + FileDocumentManager.getInstance().getFile(document)?.let { vf -> + fileEditorManager.closeFile(vf) + } + } + } + file.delete(this@RemoteAttributedContentSynchronizer) + } + } + /** * Base implementation of [ContentSynchronizer.synchronizeWithRemote] method for each synchronizer. * Doesn't need to be overridden in most cases @@ -118,12 +144,7 @@ abstract class RemoteAttributedContentSynchronizer progressIndicator?.text = "Synchronizing file ${syncProvider.file.name} with mainframe" val recordId = handlerToStorageIdMap.getOrPut(syncProvider) { successfulStatesStorage.createNewRecord() } val attributes = attributesService.getAttributes(syncProvider.file) ?: throw IOException("No Attributes found") - val ussAttributes = attributes.castOrNull() - if (!wasFetchedBefore(syncProvider)) { - ussAttributes?.let { checkUssFileTag(it) } - } - val currentCharset = ussAttributes?.charset ?: DEFAULT_TEXT_CHARSET val fetchedRemoteContentBytes = fetchRemoteContentBytes(attributes, progressIndicator) val contentAdapter = dataOpsManager.getMFContentAdapter(syncProvider.file) @@ -131,9 +152,8 @@ abstract class RemoteAttributedContentSynchronizer if (!wasFetchedBefore(syncProvider)) { log.info("Setting initial content for file ${syncProvider.file.name}") + ussAttributes?.let { checkUssFileTag(it) } runWriteActionInEdtAndWait { syncProvider.putInitialContent(adaptedFetchedBytes) } - changeFileEncodingTo(syncProvider.file, currentCharset) - initLineSeparator(syncProvider) successfulStatesStorage.writeStream(recordId).use { it.write(adaptedFetchedBytes) } fetchedAtLeastOnce.add(syncProvider) } else { @@ -183,4 +203,11 @@ abstract class RemoteAttributedContentSynchronizer override fun isFileUploadNeeded(syncProvider: SyncProvider): Boolean { return needToUpload.firstOrNull { syncProvider == it } != null } + + /** + * Base implementation of [ContentSynchronizer.markAsNotNeededForSync] method for each content synchronizer. + */ + override fun markAsNotNeededForSync(syncProvider: SyncProvider) { + needToUpload.remove(syncProvider) + } } diff --git a/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/SyncAction.kt b/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/SyncAction.kt index 0a57dfb3a..8359e8a35 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/SyncAction.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/SyncAction.kt @@ -15,7 +15,6 @@ import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.components.service import com.intellij.openapi.editor.ex.EditorEx -import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.progress.runBackgroundableTask import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.vfs.VirtualFile @@ -64,7 +63,6 @@ class SyncAction : DumbAwareAction() { val vFile = getSupportedVirtualFile(e) ?: return val incompatibleEncoding = e.project?.let { !checkEncodingCompatibility(vFile, it) } ?: false if (incompatibleEncoding && !showSaveAnywayDialog(vFile.charset)) return - val editor = getEditor(e) ?: return val syncProvider = DocumentedSyncProvider(vFile, SaveStrategy.default(e.project)) runBackgroundableTask( title = "Synchronizing ${vFile.name}...", @@ -72,7 +70,7 @@ class SyncAction : DumbAwareAction() { cancellable = true ) { indicator -> runWriteActionInEdtAndWait { - FileDocumentManager.getInstance().saveDocument(editor.document) + syncProvider.saveDocument() service().getContentSynchronizer(vFile)?.synchronizeWithRemote(syncProvider, indicator) } } diff --git a/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/syncUtils.kt b/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/syncUtils.kt index 65f0534b4..316a989f4 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/syncUtils.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/content/synchronizer/syncUtils.kt @@ -10,9 +10,6 @@ package org.zowe.explorer.dataops.content.synchronizer -import com.intellij.openapi.fileEditor.impl.LoadTextUtil -import com.intellij.openapi.project.Project -import org.zowe.explorer.utils.runWriteActionInEdtAndWait import java.nio.charset.Charset private const val NEW_LINE = "\n" @@ -21,10 +18,6 @@ val DEFAULT_TEXT_CHARSET: Charset = Charset.forName("ISO8859_1") val DEFAULT_BINARY_CHARSET: Charset = Charset.forName("IBM-1047") -const val LF_LINE_SEPARATOR: String = "\n" - -const val CR_LINE_SEPARATOR: String = "\r" - /** Remove string's last blank line */ fun String.removeLastNewLine(): String { return if (endsWith(NEW_LINE)) { @@ -34,12 +27,11 @@ fun String.removeLastNewLine(): String { } } +/** Remove last blank line in byte array by converting it to string and back again */ fun ByteArray.removeLastNewLine(): ByteArray { - return if (last() == NEW_LINE.toByte()) { - dropLast(1).toByteArray() - } else { - this - } + return toString(DEFAULT_TEXT_CHARSET) + .removeLastNewLine() + .toByteArray(DEFAULT_TEXT_CHARSET) } /** Add new blank line to the string */ @@ -47,16 +39,8 @@ fun ByteArray.addNewLine(): ByteArray { return this.plus(NEW_LINE.toByteArray()) } -/** Initializes the line separator to the contents of the file (by default "\n"). */ -fun initLineSeparator(syncProvider: SyncProvider, project: Project? = null) { - val file = syncProvider.file - runWriteActionInEdtAndWait { - syncProvider.saveDocument() - LoadTextUtil.changeLineSeparators( - project, - file, - LoadTextUtil.detectLineSeparator(file, true) ?: LF_LINE_SEPARATOR, - file - ) - } +// TODO: Remove when it becomes possible to mock kotlin inline function. +/** Wrapper for [String.toByteArray] function. It is necessary only for test purposes for now. */ +fun String.convertToByteArray(charset: Charset = Charsets.UTF_8): ByteArray { + return toByteArray(charset) } diff --git a/src/main/kotlin/org/zowe/explorer/dataops/exceptions/CallException.kt b/src/main/kotlin/org/zowe/explorer/dataops/exceptions/CallException.kt index adc45b393..75aeb85c7 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/exceptions/CallException.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/exceptions/CallException.kt @@ -14,6 +14,12 @@ import org.zowe.explorer.utils.castOrNull import org.zowe.explorer.utils.gson import retrofit2.Response +/** The map contains the correspondence between the response message and the message for the user. */ +val responseMessageMap = mapOf( + Pair("Unauthorized", "Credentials are not valid"), + Pair("Not Found", "Endpoint not found") +) + /** * Generating an exception message string. * @param code exception code. diff --git a/src/main/kotlin/org/zowe/explorer/dataops/fetch/JobFetchProvider.kt b/src/main/kotlin/org/zowe/explorer/dataops/fetch/JobFetchProvider.kt index 2122b3a28..2a1434158 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/fetch/JobFetchProvider.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/fetch/JobFetchProvider.kt @@ -20,6 +20,7 @@ import org.zowe.explorer.dataops.RemoteQuery import org.zowe.explorer.dataops.attributes.JobsRequester import org.zowe.explorer.dataops.attributes.RemoteJobAttributes import org.zowe.explorer.dataops.exceptions.CallException +import org.zowe.explorer.dataops.exceptions.responseMessageMap import org.zowe.explorer.utils.asMutableList import org.zowe.explorer.utils.cancelByIndicator import org.zowe.explorer.utils.log @@ -119,7 +120,8 @@ class JobFetchProvider(dataOpsManager: DataOpsManager) : log.info("No jobs returned for query $query. Skipping") } } else { - exception = CallException(response, "Cannot retrieve Job files list") + val headMessage = responseMessageMap[response.message()] ?: "Cannot retrieve Job files list" + exception = CallException(response, headMessage) } if (exception != null) { diff --git a/src/main/kotlin/org/zowe/explorer/dataops/fetch/RemoteBatchedFileFetchProviderBase.kt b/src/main/kotlin/org/zowe/explorer/dataops/fetch/RemoteBatchedFileFetchProviderBase.kt index 59caf4552..50b4a59bc 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/fetch/RemoteBatchedFileFetchProviderBase.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/fetch/RemoteBatchedFileFetchProviderBase.kt @@ -20,6 +20,7 @@ import org.zowe.explorer.dataops.RemoteQuery import org.zowe.explorer.dataops.UnitRemoteQueryImpl import org.zowe.explorer.dataops.attributes.FileAttributes import org.zowe.explorer.dataops.exceptions.CallException +import org.zowe.explorer.dataops.exceptions.responseMessageMap import org.zowe.explorer.utils.castOrNull /** @@ -99,7 +100,8 @@ abstract class RemoteBatchedFileFetchProviderBase) { errorMessage = details[0] as String } errorMessages[query] = service().separateErrorMessage(errorMessage)["error.description"] as String } else { - val errorMessage = throwable.message ?: "Error" errorMessages[query] = errorMessage } cache[query] = listOf() diff --git a/src/main/kotlin/org/zowe/explorer/dataops/fetch/SpoolFileFetchProvider.kt b/src/main/kotlin/org/zowe/explorer/dataops/fetch/SpoolFileFetchProvider.kt index 852c12b44..3460270f7 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/fetch/SpoolFileFetchProvider.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/fetch/SpoolFileFetchProvider.kt @@ -20,6 +20,7 @@ import org.zowe.explorer.dataops.RemoteQuery import org.zowe.explorer.dataops.attributes.RemoteJobAttributes import org.zowe.explorer.dataops.attributes.RemoteSpoolFileAttributes import org.zowe.explorer.dataops.exceptions.CallException +import org.zowe.explorer.dataops.exceptions.responseMessageMap import org.zowe.explorer.dataops.getAttributesService import org.zowe.explorer.utils.cancelByIndicator import org.zowe.explorer.utils.log @@ -89,7 +90,8 @@ class SpoolFileFetchProvider(dataOpsManager: DataOpsManager) : attributes?.joinToString("\n") ?: "" } } else { - exception = CallException(response, "Cannot retrieve Job files list") + val headMessage = responseMessageMap[response.message()] ?: "Cannot retrieve Job files list" + exception = CallException(response, headMessage) } if (exception != null) { diff --git a/src/main/kotlin/org/zowe/explorer/dataops/fetch/UssFileFetchProvider.kt b/src/main/kotlin/org/zowe/explorer/dataops/fetch/UssFileFetchProvider.kt index 98b4f248a..6fc542e04 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/fetch/UssFileFetchProvider.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/fetch/UssFileFetchProvider.kt @@ -19,6 +19,7 @@ import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.RemoteQuery import org.zowe.explorer.dataops.attributes.RemoteUssAttributes import org.zowe.explorer.dataops.exceptions.CallException +import org.zowe.explorer.dataops.exceptions.responseMessageMap import org.zowe.explorer.utils.cancelByIndicator import org.zowe.explorer.utils.log import org.zowe.explorer.vfs.MFVirtualFile @@ -91,7 +92,8 @@ class UssFileFetchProvider( attributes?.joinToString("\n") ?: "" } } else { - exception = CallException(response, "Cannot retrieve USS files list") + val headMessage = responseMessageMap[response.message()] ?: "Cannot retrieve USS files list" + exception = CallException(response, headMessage) } if (exception != null) { diff --git a/src/main/kotlin/org/zowe/explorer/dataops/operations/InfoOperationRunner.kt b/src/main/kotlin/org/zowe/explorer/dataops/operations/InfoOperationRunner.kt index 3f2da5a35..c2b4d790c 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/operations/InfoOperationRunner.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/operations/InfoOperationRunner.kt @@ -15,6 +15,7 @@ import org.zowe.explorer.api.api import org.zowe.explorer.config.connect.authToken import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.exceptions.CallException +import org.zowe.explorer.dataops.exceptions.responseMessageMap import org.zowe.explorer.utils.cancelByIndicator import org.zowe.explorer.utils.log import org.zowe.kotlinsdk.SystemsApi @@ -56,11 +57,7 @@ class InfoOperationRunner : OperationRunner { .cancelByIndicator(progressIndicator) .execute() if (!response.isSuccessful) { - val headMessage = when (response.message()) { - "Unauthorized" -> "Credentials are not valid" - "Not Found" -> "Endpoint not found" - else -> response.message() - } + val headMessage = responseMessageMap[response.message()] ?: response.message() throw CallException(response, headMessage) } return response.body() ?: throw CallException(response, "Cannot parse z/OSMF info request body") diff --git a/src/main/kotlin/org/zowe/explorer/dataops/operations/MemberAllocator.kt b/src/main/kotlin/org/zowe/explorer/dataops/operations/MemberAllocator.kt index d07569551..90b092fb1 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/operations/MemberAllocator.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/operations/MemberAllocator.kt @@ -11,6 +11,7 @@ package org.zowe.explorer.dataops.operations import com.intellij.openapi.progress.ProgressIndicator +import org.zowe.explorer.api.api import org.zowe.explorer.api.apiWithBytesConverter import org.zowe.explorer.config.connect.ConnectionConfig import org.zowe.explorer.config.connect.authToken @@ -18,6 +19,7 @@ import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.exceptions.CallException import org.zowe.explorer.utils.cancelByIndicator import org.zowe.explorer.utils.log +import io.ktor.util.* import org.zowe.kotlinsdk.DataAPI /** @@ -58,17 +60,39 @@ class MemberAllocator : Allocator { progressIndicator: ProgressIndicator ) { progressIndicator.checkCanceled() - val request = apiWithBytesConverter(operation.connectionConfig).writeToDatasetMember( + val newMemberName = operation.request.memberName.toUpperCasePreservingASCIIRules() + val memberAllocationErrorString = "Cannot create member $newMemberName in ${operation.request.datasetName} " + + "on ${operation.connectionConfig.name}." + val listMembersRequest = api(operation.connectionConfig).listDatasetMembers( authorizationToken = operation.connectionConfig.authToken, - datasetName = operation.request.datasetName, - memberName = operation.request.memberName, - content = byteArrayOf() + datasetName = operation.request.datasetName ).cancelByIndicator(progressIndicator).execute() - if (!request.isSuccessful) { + if (listMembersRequest.isSuccessful) { + val membersList = listMembersRequest.body() + val duplicateMember = membersList?.items?.map { it.name.toUpperCasePreservingASCIIRules() }?.find { it == newMemberName } + if (duplicateMember != null) { + throw CallException( + listMembersRequest, + "$memberAllocationErrorString Member with name $newMemberName already exists." + ) + } else { + val writeRequest = apiWithBytesConverter(operation.connectionConfig).writeToDatasetMember( + authorizationToken = operation.connectionConfig.authToken, + datasetName = operation.request.datasetName, + memberName = newMemberName, + content = byteArrayOf() + ).cancelByIndicator(progressIndicator).execute() + if (!writeRequest.isSuccessful) { + throw CallException( + writeRequest, + memberAllocationErrorString + ) + } + } + } else { throw CallException( - request, - "Cannot create member ${operation.request.memberName} in ${operation.request.datasetName} " + - "on ${operation.connectionConfig.name}" + listMembersRequest, + "Cannot fetch member list for ${operation.request.datasetName}" ) } } diff --git a/src/main/kotlin/org/zowe/explorer/dataops/operations/TsoOperationRunner.kt b/src/main/kotlin/org/zowe/explorer/dataops/operations/TsoOperationRunner.kt index 1b1bcce2f..54787b5e9 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/operations/TsoOperationRunner.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/operations/TsoOperationRunner.kt @@ -19,14 +19,13 @@ import org.zowe.explorer.ui.build.tso.config.TSOConfigWrapper import org.zowe.explorer.ui.build.tso.ui.TSOSessionParams import org.zowe.explorer.utils.cancelByIndicator import org.zowe.explorer.utils.log +import org.zowe.explorer.dataops.operations.MessageType as MessageTypeEnum import org.zowe.kotlinsdk.MessageType import org.zowe.kotlinsdk.TsoApi import org.zowe.kotlinsdk.TsoData import org.zowe.kotlinsdk.TsoResponse import io.ktor.util.* import retrofit2.Response -import java.nio.charset.Charset -import java.util.* /** * Factory class which represents a TSO operation runner. Defined in plugin.xml @@ -85,12 +84,7 @@ class TsoOperationRunner : OperationRunner { response = api(state.getConnectionConfig()) .sendMessageToTso( state.getConnectionConfig().authToken, - body = TsoData( - tsoResponse = MessageType( - version = "0100", - data = operation.message - ) - ), + body = createTsoData(operation), servletKey = servletKey ) .cancelByIndicator(progressIndicator) @@ -140,4 +134,51 @@ class TsoOperationRunner : OperationRunner { return response?.body() ?: throw Exception("Cannot retrieve response from server.") } + /** + * Create TsoData object depending on the specified message type + * @throws Exception if message type not specified + */ + private fun createTsoData(operation: TsoOperation): TsoData { + return when (operation.messageType) { + MessageTypeEnum.TSO_MESSAGE -> TsoData( + tsoMessage = createMessageType(operation) + ) + + MessageTypeEnum.TSO_PROMPT -> TsoData( + tsoPrompt = createMessageType(operation) + ) + + MessageTypeEnum.TSO_RESPONSE -> TsoData( + tsoResponse = createMessageType(operation) + ) + + null -> throw Exception("Message type not specified") + } + } + + /** + * Create MessageType object depending on the specified message data + * @throws Exception if message data not specified + */ + private fun createMessageType(operation: TsoOperation): MessageType { + return when (operation.messageData) { + MessageData.DATA_DATA -> MessageType( + version = "0100", + data = operation.message + ) + + MessageData.DATA_HIDDEN -> MessageType( + version = "0100", + hidden = operation.message + ) + + MessageData.DATA_ACTION -> MessageType( + version = "0100", + action = operation.message + ) + + null -> throw Exception("Message data not specified") + } + } + } diff --git a/src/main/kotlin/org/zowe/explorer/dataops/operations/jobs/GetJclRecordsOperationRunner.kt b/src/main/kotlin/org/zowe/explorer/dataops/operations/jobs/GetJclRecordsOperationRunner.kt index b4faf1b83..ffe23a9f6 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/operations/jobs/GetJclRecordsOperationRunner.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/operations/jobs/GetJclRecordsOperationRunner.kt @@ -16,6 +16,7 @@ import org.zowe.explorer.config.connect.ConnectionConfig import org.zowe.explorer.config.connect.authToken import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.RemoteQuery +import org.zowe.explorer.dataops.content.synchronizer.removeLastNewLine import org.zowe.explorer.dataops.exceptions.CallException import org.zowe.explorer.dataops.operations.OperationRunner import org.zowe.explorer.dataops.operations.OperationRunnerFactory @@ -75,7 +76,7 @@ class GetJclRecordsOperationRunner : OperationRunner throw Exception("Method with such parameters not found") } - val body = response.body() + val body = response.body()?.removeLastNewLine() if (!response.isSuccessful || body == null) { throw CallException( response, diff --git a/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/RemoteToLocalFileMover.kt b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/RemoteToLocalFileMover.kt index 82319b279..cca046161 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/RemoteToLocalFileMover.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/RemoteToLocalFileMover.kt @@ -9,21 +9,17 @@ */ package org.zowe.explorer.dataops.operations.mover -import com.intellij.openapi.fileEditor.impl.LoadTextUtil import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry +import com.intellij.util.LineSeparator import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.attributes.RemoteUssAttributes import org.zowe.explorer.dataops.content.synchronizer.DocumentedSyncProvider -import org.zowe.explorer.dataops.content.synchronizer.LF_LINE_SEPARATOR import org.zowe.explorer.dataops.operations.OperationRunner import org.zowe.explorer.dataops.operations.OperationRunnerFactory -import org.zowe.explorer.utils.changeFileEncodingTo -import org.zowe.explorer.utils.log -import org.zowe.explorer.utils.runReadActionInEdtAndWait -import org.zowe.explorer.utils.runWriteActionInEdtAndWait +import org.zowe.explorer.utils.* import org.zowe.explorer.vfs.MFVirtualFile import org.zowe.kotlinsdk.XIBMDataType import java.io.File @@ -99,10 +95,10 @@ class RemoteToLocalFileMover(val dataOpsManager: DataOpsManager) : AbstractFileM } } val createdFileJava = Paths.get(destFile.path, newFileName).toFile().apply { createNewFile() } - createdFileJava.writeBytes(sourceContent) if (!sourceFile.fileType.isBinary) { setCreatedFileParams(createdFileJava, sourceFile) } + createdFileJava.writeBytes(sourceContent) runReadActionInEdtAndWait { destFile.refresh(false, false) } @@ -117,11 +113,9 @@ class RemoteToLocalFileMover(val dataOpsManager: DataOpsManager) : AbstractFileM private fun setCreatedFileParams(createdFileJava: File, sourceFile: VirtualFile) { val createdVirtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(createdFileJava) createdVirtualFile?.let { - changeFileEncodingTo(it, sourceFile.charset) - val lineSeparator = sourceFile.detectedLineSeparator ?: LF_LINE_SEPARATOR - runWriteActionInEdtAndWait { - LoadTextUtil.changeLineSeparators(null, it, lineSeparator, it) - } + val lineSeparator = sourceFile.detectedLineSeparator ?: LineSeparator.LF.separatorString + it.detectedLineSeparator = lineSeparator + runWriteActionInEdtAndWait { changeEncodingTo(it, sourceFile.charset) } } } diff --git a/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/UssFileToPdsMover.kt b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/UssFileToPdsMover.kt index 059d57754..486a2a9d2 100644 --- a/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/UssFileToPdsMover.kt +++ b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/UssFileToPdsMover.kt @@ -74,7 +74,7 @@ class UssFileToPdsMover(private val dataOpsManager: DataOpsManager) : AbstractFi val from = sourceAttributes.path val to = destinationAttributes.name val api = api(connectionConfig) - var memberName = sourceAttributes.name.filter { it.isLetterOrDigit() }.take(8) + var memberName = operation.newName ?: sourceAttributes.name.filter { it.isLetterOrDigit() }.take(8) if (memberName.isEmpty()) { memberName = "empty" } diff --git a/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/CopyPasteNameResolver.kt b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/CopyPasteNameResolver.kt new file mode 100644 index 000000000..f3fa3c34a --- /dev/null +++ b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/CopyPasteNameResolver.kt @@ -0,0 +1,51 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ +package org.zowe.explorer.dataops.operations.mover.names + +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.vfs.VirtualFile +import org.zowe.explorer.dataops.DataOpsComponentFactory + +interface CopyPasteNameResolverFactory: DataOpsComponentFactory + +/** + * Class to represent a name resolution for conflicting situation. + * @author Valiantsin Krus + */ +interface CopyPasteNameResolver { + companion object { + @JvmField + val EP = ExtensionPointName.create("org.zowe.explorer.nameResolver") + } + + /** + * Determines whether this name resolver could resolve conflict for passed files or not. + * @param source source file to copy in destination folder (or folder-like entity). + * @param destination folder-like entity to copy file to. + * @return true if this name resolver could dot it or false otherwise. + */ + fun accepts(source: VirtualFile, destination: VirtualFile): Boolean + + /** + * Finds child in destination folder that conflicts with source file. + * @param source source file to copy in destination folder (or folder-like entity). + * @param destination folder-like entity to copy file to. + * @return instance of conflicting child file or null if it was not found. + */ + fun getConflictingChild(source: VirtualFile, destination: VirtualFile): VirtualFile? + + /** + * Creates new name for source file to make it possible to be copied in destination folder. + * @param source source file to copy in destination folder (or folder-like entity). + * @param destination folder-like entity to copy file to. + * @return string with new file name. + */ + fun resolve(source: VirtualFile, destination: VirtualFile): String +} diff --git a/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/DatasetOrDirResolver.kt b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/DatasetOrDirResolver.kt new file mode 100644 index 000000000..50a720d14 --- /dev/null +++ b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/DatasetOrDirResolver.kt @@ -0,0 +1,37 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ +package org.zowe.explorer.dataops.operations.mover.names + +import com.intellij.openapi.vfs.VirtualFile +import org.zowe.explorer.dataops.DataOpsManager +import org.zowe.explorer.dataops.attributes.RemoteDatasetAttributes + +class DatasetOrDirResolverFactory : CopyPasteNameResolverFactory { + override fun buildComponent(dataOpsManager: DataOpsManager): CopyPasteNameResolver { + return DatasetOrDirResolver(dataOpsManager) + } +} + +/** + * Implementation of [CopyPasteNameResolver] for copying dataset or directory to uss or local system. + * @author Valiantsin Krus + */ +class DatasetOrDirResolver(val dataOpsManager: DataOpsManager): IndexedNameResolver() { + override fun accepts(source: VirtualFile, destination: VirtualFile): Boolean { + val sourceAttributes = dataOpsManager.tryToGetAttributes(source) + val destinationAttributes = dataOpsManager.tryToGetAttributes(destination) + return (source.isDirectory || sourceAttributes is RemoteDatasetAttributes) && destinationAttributes !is RemoteDatasetAttributes + } + + + override fun resolveNameWithIndex(source: VirtualFile, destination: VirtualFile, index: Int?): String { + return if (index == null) source.name else "${source.name}_(${index})" + } +} diff --git a/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/DefaultNameResolver.kt b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/DefaultNameResolver.kt new file mode 100644 index 000000000..eed6f98e6 --- /dev/null +++ b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/DefaultNameResolver.kt @@ -0,0 +1,35 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ +package org.zowe.explorer.dataops.operations.mover.names + +import com.intellij.openapi.vfs.VirtualFile + +/** + * Implementation of [IndexedNameResolver] that is used by default (if no one other name resolver was found) + * It just adds _() to the end of the file name before extension. + * @author Valiantsin Krus + */ +class DefaultNameResolver: IndexedNameResolver() { + override fun accepts(source: VirtualFile, destination: VirtualFile): Boolean { + return true + } + + override fun resolveNameWithIndex(source: VirtualFile, destination: VirtualFile, index: Int?): String { + val sourceName = source.name + return if (index == null) { + sourceName + } else { + val extension = if (sourceName.contains(".")) sourceName.substringAfterLast(".") else null + val newNameWithoutExtension = "${sourceName.substringBeforeLast(".")}_(${index})" + if (extension != null) "$newNameWithoutExtension.$extension" else newNameWithoutExtension + } + } + +} diff --git a/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/IndexedNameResolver.kt b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/IndexedNameResolver.kt new file mode 100644 index 000000000..6fa2db879 --- /dev/null +++ b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/IndexedNameResolver.kt @@ -0,0 +1,44 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ +package org.zowe.explorer.dataops.operations.mover.names + +import com.intellij.openapi.vfs.VirtualFile + +/** + * Name resolver that generates new name based on the index (e.g. file_(1), file_(2) and etc.) + * This is an abstract class, it only finds the necessary index but doesn't create a new name. + * It could be, for example file1, file2, file3. It all depends on implementation needs. + * @author Valiantsin Krus + */ +abstract class IndexedNameResolver: CopyPasteNameResolver { + + override fun getConflictingChild(source: VirtualFile, destination: VirtualFile): VirtualFile? { + val rowNameToCopy = resolveNameWithIndex(source, destination, null) + return destination.children.firstOrNull { it.name == rowNameToCopy } + } + + /** + * Creates new name for a source file based on passed index. + * @param source source file to copy in destination folder (or folder-like entity). + * @param destination folder-like entity to copy file to. + * @param index generated index to add to the source name. If it is null, then no index is needed to add. + * @return new name with joined index. + */ + abstract fun resolveNameWithIndex(source: VirtualFile, destination: VirtualFile, index: Int?): String + + override fun resolve(source: VirtualFile, destination: VirtualFile): String { + var newName = resolveNameWithIndex(source, destination, null) + var index = 1 + while (destination.children.any { it.name == newName }) { + newName = resolveNameWithIndex(source, destination, index++) + } + return newName + } +} diff --git a/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/NotSeqToPDSResolver.kt b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/NotSeqToPDSResolver.kt new file mode 100644 index 000000000..f696fe687 --- /dev/null +++ b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/NotSeqToPDSResolver.kt @@ -0,0 +1,39 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ +package org.zowe.explorer.dataops.operations.mover.names + +import com.intellij.openapi.vfs.VirtualFile +import org.zowe.explorer.dataops.DataOpsManager +import org.zowe.explorer.dataops.attributes.RemoteDatasetAttributes + +class NotSeqToPDSResolverFactory : CopyPasteNameResolverFactory { + override fun buildComponent(dataOpsManager: DataOpsManager): CopyPasteNameResolver { + return NotSeqToPDSResolver(dataOpsManager) + } +} + +/** + * Implementation of [CopyPasteNameResolver] for copying anything except of Sequential Dataset to PDS. + * @author Valiantsin Krus + */ +class NotSeqToPDSResolver(val dataOpsManager: DataOpsManager) : IndexedNameResolver() { + override fun accepts(source: VirtualFile, destination: VirtualFile): Boolean { + val sourceAttributes = dataOpsManager.tryToGetAttributes(source) + val destinationAttributes = dataOpsManager.tryToGetAttributes(destination) + return destinationAttributes is RemoteDatasetAttributes && + sourceAttributes !is RemoteDatasetAttributes + } + + override fun resolveNameWithIndex(source: VirtualFile, destination: VirtualFile, index: Int?): String { + val memberName = source.name.filter { it.isLetterOrDigit() }.uppercase().ifEmpty { "EMPTY" } + return if (index == null) memberName.take(8) else "${memberName.take(7)}$index" + } + +} diff --git a/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/SeqToPDSResolver.kt b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/SeqToPDSResolver.kt new file mode 100644 index 000000000..815255927 --- /dev/null +++ b/src/main/kotlin/org/zowe/explorer/dataops/operations/mover/names/SeqToPDSResolver.kt @@ -0,0 +1,39 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ +package org.zowe.explorer.dataops.operations.mover.names + +import com.intellij.openapi.vfs.VirtualFile +import org.zowe.explorer.dataops.DataOpsManager +import org.zowe.explorer.dataops.attributes.RemoteDatasetAttributes + +class SeqToPDSResolverFactory : CopyPasteNameResolverFactory { + override fun buildComponent(dataOpsManager: DataOpsManager): CopyPasteNameResolver { + return SeqToPDSResolver(dataOpsManager) + } +} + +/** + * Implementation of [CopyPasteNameResolver] for copying Sequential Dataset to PDS. + * @author Valiantsin Krus + */ +class SeqToPDSResolver(val dataOpsManager: DataOpsManager) : IndexedNameResolver() { + override fun accepts(source: VirtualFile, destination: VirtualFile): Boolean { + val sourceAttributes = dataOpsManager.tryToGetAttributes(source) + val destinationAttributes = dataOpsManager.tryToGetAttributes(destination) + return sourceAttributes is RemoteDatasetAttributes && + !source.isDirectory && + destinationAttributes is RemoteDatasetAttributes + } + + override fun resolveNameWithIndex(source: VirtualFile, destination: VirtualFile, index: Int?): String { + val lastQualifier = source.name.split(".").last() + return if (index == null) lastQualifier else "${lastQualifier.take(7)}${index}" + } +} diff --git a/src/main/kotlin/org/zowe/explorer/editor/FileEditorEventsListener.kt b/src/main/kotlin/org/zowe/explorer/editor/FileEditorEventsListener.kt index 4d92892c4..39f14913e 100644 --- a/src/main/kotlin/org/zowe/explorer/editor/FileEditorEventsListener.kt +++ b/src/main/kotlin/org/zowe/explorer/editor/FileEditorEventsListener.kt @@ -90,7 +90,6 @@ class FileEditorBeforeEventsListener : FileEditorManagerListener.Before { if (incompatibleEncoding && !showSaveAnywayDialog(file.charset)) { return } - runWriteActionInEdtAndWait { syncProvider.saveDocument() } sendTopic(AutoSyncFileListener.AUTO_SYNC_FILE, project).sync(file) } } diff --git a/src/main/kotlin/org/zowe/explorer/editor/FileEditorFocusListener.kt b/src/main/kotlin/org/zowe/explorer/editor/FileEditorFocusListener.kt index a6903770b..f59706013 100644 --- a/src/main/kotlin/org/zowe/explorer/editor/FileEditorFocusListener.kt +++ b/src/main/kotlin/org/zowe/explorer/editor/FileEditorFocusListener.kt @@ -22,6 +22,7 @@ import org.zowe.explorer.dataops.content.synchronizer.DocumentedSyncProvider import org.zowe.explorer.dataops.content.synchronizer.SaveStrategy import org.zowe.explorer.utils.* import org.zowe.explorer.vfs.MFVirtualFile +import java.awt.IllegalComponentStateException import java.awt.event.FocusEvent import javax.swing.SwingUtilities @@ -40,10 +41,19 @@ class FileEditorFocusListener: FocusChangeListener { val mouseClickInEditor = editor.component.isComponentUnderMouse() if (!mouseClickInEditor) { event.oppositeComponent?.let { focusedComponent -> - val point = focusedComponent.locationOnScreen - SwingUtilities.convertPointFromScreen(point, editor.component) - if (editor.component.contains(point)) { - return + try { + val point = focusedComponent.locationOnScreen + SwingUtilities.convertPointFromScreen(point, editor.component) + if (editor.component.contains(point)) { + return + } + } catch (e : IllegalComponentStateException) { + val diagnosticMessage = "Error happened while dispatching focusLost event. Content will be synchronized anyway.\n" + + "Diagnostic info:\n" + "Editor component name: ${editor.component.name}\n" + "Focused component name: ${focusedComponent.name}\n" + + "Focused component location on screen: x coordinate is ${focusedComponent.location.x}, y coordinate is ${focusedComponent.location.y}.\n" + + "isShowing: ${focusedComponent.isShowing}\n" + log().error(diagnosticMessage, e) + return@let } } if (ConfigService.instance.isAutoSyncEnabled) { @@ -61,7 +71,6 @@ class FileEditorFocusListener: FocusChangeListener { if (incompatibleEncoding && !showSaveAnywayDialog(file.charset)) { return } - runWriteActionInEdtAndWait { syncProvider.saveDocument() } sendTopic(AutoSyncFileListener.AUTO_SYNC_FILE, project).sync(file) } } diff --git a/src/main/kotlin/org/zowe/explorer/editor/status/MfLineSeparatorPanel.kt b/src/main/kotlin/org/zowe/explorer/editor/status/MfLineSeparatorPanel.kt index d46f3dd27..57ba6eae1 100644 --- a/src/main/kotlin/org/zowe/explorer/editor/status/MfLineSeparatorPanel.kt +++ b/src/main/kotlin/org/zowe/explorer/editor/status/MfLineSeparatorPanel.kt @@ -11,6 +11,7 @@ package org.zowe.explorer.editor.status import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.wm.StatusBar import com.intellij.openapi.wm.StatusBarWidget @@ -34,6 +35,11 @@ class MfLineSeparatorPanel(project: Project): LineSeparatorPanel(project) { if (file != null && file.isMfVirtualFile() && !file.isUssVirtualFile()) { return false } + // need to disable changing line separator when more than one project is open + // see https://youtrack.jetbrains.com/issue/IDEA-346634/ + if (ProjectManager.getInstance().openProjects.size > 1) { + return false + } return super.isEnabledForFile(file) } diff --git a/src/main/kotlin/org/zowe/explorer/explorer/actions/AddMemberAction.kt b/src/main/kotlin/org/zowe/explorer/explorer/actions/AddMemberAction.kt index a458ffe70..7bbcda246 100644 --- a/src/main/kotlin/org/zowe/explorer/explorer/actions/AddMemberAction.kt +++ b/src/main/kotlin/org/zowe/explorer/explorer/actions/AddMemberAction.kt @@ -12,6 +12,7 @@ package org.zowe.explorer.explorer.actions import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service import com.intellij.openapi.progress.runBackgroundableTask import org.zowe.explorer.config.connect.ConnectionConfig import org.zowe.explorer.dataops.DataOpsManager @@ -32,7 +33,6 @@ import org.zowe.explorer.explorer.ui.FileExplorerView import org.zowe.explorer.explorer.ui.FileLikeDatasetNode import org.zowe.explorer.explorer.ui.LibraryNode import org.zowe.explorer.explorer.ui.getExplorerView -import org.zowe.explorer.utils.service import org.zowe.explorer.vfs.MFVirtualFile /** Class that represents "Add member" action */ @@ -45,14 +45,13 @@ class AddMemberAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { val view = e.getExplorerView() ?: return var currentNode = view.mySelectedNodesData[0].node - DataOpsManager.instance if (currentNode !is FetchNode) { currentNode = currentNode.parent ?: return if (currentNode !is LibraryNode) return } if ((currentNode as ExplorerUnitTreeNodeBase>).unit is FilesWorkingSet) { val connectionConfig = currentNode.unit.connectionConfig - val dataOpsManager = currentNode.explorer.componentManager.service() + val dataOpsManager = service() if (currentNode is LibraryNode && connectionConfig != null) { val parentName = dataOpsManager .getAttributesService() @@ -77,9 +76,9 @@ class AddMemberAction : AnAction() { ) }.onSuccess { currentNode.cleanCache(cleanBatchedQuery = true) - }.onFailure { - var throwable = it - if (it is CallException && it.code == 500 && it.message?.contains("Directory full") == true) { + }.onFailure { failObj: Throwable -> + var throwable = failObj + if (failObj is CallException && failObj.code == 500 && failObj.message?.contains("Directory full") == true) { runCatching { dataOpsManager.performOperation( operation = DeleteMemberOperation( @@ -90,8 +89,8 @@ class AddMemberAction : AnAction() { connectionConfig = connectionConfig ) ) - }.onFailure { th -> - throwable = Throwable("Directory is FULL. Invalid member created.\n" + th.message) + }.onFailure { innerFailObj: Throwable -> + throwable = Throwable("Directory is FULL. Invalid member created.\n" + innerFailObj.message) currentNode.cleanCache(cleanBatchedQuery = true) } } diff --git a/src/main/kotlin/org/zowe/explorer/explorer/actions/AllocateActionBase.kt b/src/main/kotlin/org/zowe/explorer/explorer/actions/AllocateActionBase.kt index 95331891a..047f773ec 100644 --- a/src/main/kotlin/org/zowe/explorer/explorer/actions/AllocateActionBase.kt +++ b/src/main/kotlin/org/zowe/explorer/explorer/actions/AllocateActionBase.kt @@ -10,12 +10,14 @@ package org.zowe.explorer.explorer.actions +import com.intellij.notification.Notification +import com.intellij.notification.NotificationAction +import com.intellij.notification.NotificationType +import com.intellij.notification.Notifications import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.service import com.intellij.openapi.progress.runModalTask -import com.intellij.openapi.ui.showOkNoDialog import org.zowe.explorer.common.ui.cleanInvalidateOnExpand import org.zowe.explorer.common.ui.showUntilDone import org.zowe.explorer.config.configCrudable @@ -24,6 +26,7 @@ import org.zowe.explorer.config.ws.FilesWorkingSetConfig import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.operations.DatasetAllocationOperation import org.zowe.explorer.dataops.operations.DatasetAllocationParams +import org.zowe.explorer.explorer.ExplorerUnit import org.zowe.explorer.explorer.FilesWorkingSet import org.zowe.explorer.explorer.ui.AllocationDialog import org.zowe.explorer.explorer.ui.DSMaskNode @@ -39,6 +42,8 @@ import org.zowe.kotlinsdk.Dataset import org.zowe.kotlinsdk.DatasetOrganization import org.zowe.kotlinsdk.DsnameType +const val ALLOCATE_ACTION_NOTIFICATION_GROUP_ID = "org.zowe.explorer.explorer.AllocateActionNotificationGroup" + abstract class AllocateActionBase : AnAction() { /** @@ -134,34 +139,10 @@ abstract class AllocateActionBase : AnAction() { } val nodeToClean = parentProbablyDSMaskNode?.castOrNull>() nodeToClean?.let { cleanInvalidateOnExpand(nodeToClean, view) } - - var nodeCleaned = false - runInEdt { - if ( - showOkNoDialog( - title = "Dataset ${state.datasetName} Has Been Created", - message = "Would you like to add mask \"${state.datasetName}\" to ${workingSet.name}", - project = e.project, - okText = "Yes", - noText = "No" - ) - ) { - val filesWorkingSetConfig = - configCrudable.getByUniqueKey(workingSet.uuid)?.clone() - if (filesWorkingSetConfig != null) { - nodeToClean?.cleanCache(recursively = false, cleanBatchedQuery = true, sendTopic = false) - nodeCleaned = true - - filesWorkingSetConfig.dsMasks.add(DSMask().apply { mask = state.datasetName }) - configCrudable.update(filesWorkingSetConfig) - } - } - - if (!nodeCleaned) { - nodeToClean?.cleanCache(recursively = false, cleanBatchedQuery = true) - } - } + nodeToClean?.cleanCache(recursively = false, cleanBatchedQuery = true) initialState.errorMessage = "" + + showNotification(state, workingSet) } .onFailure { t -> explorer.reportThrowable(t, e.project) @@ -181,4 +162,34 @@ abstract class AllocateActionBase : AnAction() { override fun isDumbAware(): Boolean { return true } + + /** + * Shows a notification about successful allocation and suggest adding a mask to the working set + */ + private fun showNotification( + state: DatasetAllocationParams, + workingSet: ExplorerUnit<*> + ) { + val notification = Notification( + ALLOCATE_ACTION_NOTIFICATION_GROUP_ID, + "Dataset ${state.datasetName} has been created", + "Would you like to add mask \"${state.datasetName}\" to ${workingSet.name}?", + NotificationType.INFORMATION + ) + notification.addActions( + setOf( + NotificationAction.createSimpleExpiring("Add mask") { + val filesWorkingSetConfig = + configCrudable.getByUniqueKey(workingSet.uuid)?.clone() + if (filesWorkingSetConfig != null) { + filesWorkingSetConfig.dsMasks.add(DSMask().apply { mask = state.datasetName }) + configCrudable.update(filesWorkingSetConfig) + } + }, + NotificationAction.createSimpleExpiring("Skip") { } + ) + ) + notification.setSuggestionType(true) + Notifications.Bus.notify(notification) + } } diff --git a/src/main/kotlin/org/zowe/explorer/explorer/actions/AllocateDatasetAction.kt b/src/main/kotlin/org/zowe/explorer/explorer/actions/AllocateDatasetAction.kt index fc7fc9b28..6458d2af1 100644 --- a/src/main/kotlin/org/zowe/explorer/explorer/actions/AllocateDatasetAction.kt +++ b/src/main/kotlin/org/zowe/explorer/explorer/actions/AllocateDatasetAction.kt @@ -13,12 +13,8 @@ package org.zowe.explorer.explorer.actions import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.util.IconUtil -import org.zowe.explorer.explorer.ui.DSMaskNode -import org.zowe.explorer.explorer.ui.FileExplorerView -import org.zowe.explorer.explorer.ui.FileLikeDatasetNode -import org.zowe.explorer.explorer.ui.FilesWorkingSetNode -import org.zowe.explorer.explorer.ui.LibraryNode -import org.zowe.explorer.explorer.ui.getExplorerView +import org.zowe.explorer.explorer.ui.* +import org.zowe.explorer.utils.castOrNull /** * Class that represents dataset allocation action with parameters, defined by a user @@ -40,18 +36,20 @@ class AllocateDatasetAction : AllocateActionBase() { * 2. The first selected node is [FilesWorkingSetNode], [DSMaskNode], [LibraryNode] or [FileLikeDatasetNode] */ override fun update(e: AnActionEvent) { + e.presentation.icon = IconUtil.addText(AllIcons.FileTypes.Any_type, "DS") + val view = e.getExplorerView() ?: let { e.presentation.isEnabledAndVisible = false return } val selectedNodesData = view.mySelectedNodesData val node = selectedNodesData.getOrNull(0)?.node - if (node !is FilesWorkingSetNode && node !is DSMaskNode && node !is LibraryNode && node !is FileLikeDatasetNode) { - e.presentation.isEnabledAndVisible = false - return + e.presentation.isEnabledAndVisible = + node is FilesWorkingSetNode || node is DSMaskNode || node is LibraryNode || node is FileLikeDatasetNode + + if (node.castOrNull>()?.unit?.connectionConfig == null) { + e.presentation.isEnabled = false } - e.presentation.isEnabledAndVisible = true - e.presentation.icon = IconUtil.addText(AllIcons.FileTypes.Any_type, "DS") } } diff --git a/src/main/kotlin/org/zowe/explorer/explorer/actions/CreateUssEntityAction.kt b/src/main/kotlin/org/zowe/explorer/explorer/actions/CreateUssEntityAction.kt index da6edab5e..593c7838b 100644 --- a/src/main/kotlin/org/zowe/explorer/explorer/actions/CreateUssEntityAction.kt +++ b/src/main/kotlin/org/zowe/explorer/explorer/actions/CreateUssEntityAction.kt @@ -166,8 +166,12 @@ abstract class CreateUssEntityAction : AnAction() { e.presentation.isEnabledAndVisible = false return } - val selected = view.mySelectedNodesData - e.presentation.isEnabledAndVisible = - selected.size == 1 && (selected[0].node is UssDirNode || selected[0].node is UssFileNode) + val selectedNodes = view.mySelectedNodesData + val node = selectedNodes.getOrNull(0)?.node + e.presentation.isEnabledAndVisible = node is UssDirNode || node is UssFileNode + + if (node.castOrNull>()?.unit?.connectionConfig == null) { + e.presentation.isEnabled = false + } } } diff --git a/src/main/kotlin/org/zowe/explorer/explorer/actions/EditJclAction.kt b/src/main/kotlin/org/zowe/explorer/explorer/actions/EditJclAction.kt index 799b24663..1b68231f5 100644 --- a/src/main/kotlin/org/zowe/explorer/explorer/actions/EditJclAction.kt +++ b/src/main/kotlin/org/zowe/explorer/explorer/actions/EditJclAction.kt @@ -17,7 +17,6 @@ import com.intellij.openapi.fileEditor.OpenFileDescriptor import com.intellij.openapi.progress.runBackgroundableTask import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.attributes.RemoteJobAttributes -import org.zowe.explorer.dataops.content.synchronizer.DEFAULT_TEXT_CHARSET import org.zowe.explorer.dataops.content.synchronizer.DocumentedSyncProvider import org.zowe.explorer.dataops.content.synchronizer.SaveStrategy import org.zowe.explorer.dataops.operations.jobs.BasicGetJclRecordsParams @@ -25,7 +24,6 @@ import org.zowe.explorer.dataops.operations.jobs.GetJclRecordsOperation import org.zowe.explorer.explorer.ui.JesExplorerView import org.zowe.explorer.explorer.ui.JobNode import org.zowe.explorer.explorer.ui.getExplorerView -import org.zowe.explorer.utils.changeFileEncodingTo import org.zowe.explorer.utils.runWriteActionInEdtAndWait import org.zowe.explorer.vfs.MFVirtualFile @@ -84,14 +82,12 @@ class EditJclAction : AnAction() { DocumentedSyncProvider(file = cachedFile, saveStrategy = SaveStrategy.default(e.project)) if (!wasCreatedBefore) { syncProvider.putInitialContent(jclContentBytes) - changeFileEncodingTo(cachedFile, DEFAULT_TEXT_CHARSET) } else { val currentContent = syncProvider.retrieveCurrentContent() if (!(currentContent contentEquals jclContentBytes)) { syncProvider.loadNewContent(jclContentBytes) } } - syncProvider.saveDocument() it.navigate(true) } } diff --git a/src/main/kotlin/org/zowe/explorer/explorer/actions/GetFilePropertiesAction.kt b/src/main/kotlin/org/zowe/explorer/explorer/actions/GetFilePropertiesAction.kt index 76f05f445..6514b81a6 100644 --- a/src/main/kotlin/org/zowe/explorer/explorer/actions/GetFilePropertiesAction.kt +++ b/src/main/kotlin/org/zowe/explorer/explorer/actions/GetFilePropertiesAction.kt @@ -47,6 +47,12 @@ class GetFilePropertiesAction : AnAction() { } is RemoteUssAttributes -> { + // TODO: need to think whether this sync is necessary + // synchronize charset from file attributes with charset from file properties + // if (attributes.charset != virtualFile.charset) { + // attributes.charset = virtualFile.charset + // updateFileTag(attributes) + // } val oldCharset = attributes.charset val initFileMode = attributes.fileMode?.clone() val dialog = UssFilePropertiesDialog(e.project, UssFileState(attributes, virtualFile.isBeingEditingNow())) @@ -75,7 +81,7 @@ class GetFilePropertiesAction : AnAction() { } val charset = attributes.charset if (!virtualFile.isDirectory && oldCharset != charset) { - val changed = changeFileEncodingAction(virtualFile, attributes, charset) + val changed = changeFileEncodingAction(e.project, virtualFile, attributes, charset) if (!changed) { attributes.charset = oldCharset } diff --git a/src/main/kotlin/org/zowe/explorer/explorer/ui/AllocationDialog.kt b/src/main/kotlin/org/zowe/explorer/explorer/ui/AllocationDialog.kt index ba1273b20..64ce37b44 100644 --- a/src/main/kotlin/org/zowe/explorer/explorer/ui/AllocationDialog.kt +++ b/src/main/kotlin/org/zowe/explorer/explorer/ui/AllocationDialog.kt @@ -22,6 +22,7 @@ import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.builder.toNullableProperty import com.intellij.ui.dsl.gridLayout.HorizontalAlign import com.intellij.ui.layout.selectedValueMatches +import org.zowe.explorer.common.message import org.zowe.explorer.common.ui.StatefulDialog import org.zowe.explorer.config.connect.ConnectionConfig import org.zowe.explorer.config.connect.getUsername @@ -85,7 +86,7 @@ class AllocationDialog(project: Project?, config: ConnectionConfig, override var .bindText(state::datasetName) .also { datasetNameField = it.component - datasetNameField.text = "${HLQ}." + datasetNameField.text.ifEmpty { datasetNameField.text = "${HLQ}." } } .onApply { state.datasetName = state.datasetName.uppercase() } .horizontalAlign(HorizontalAlign.FILL) @@ -98,7 +99,7 @@ class AllocationDialog(project: Project?, config: ConnectionConfig, override var .bindText(state::memberName) .also { memberNameField = it.component - memberNameField.text = "SAMPLE" + memberNameField.text.ifEmpty { memberNameField.text = "SAMPLE" } } .onApply { state.memberName = state.memberName.uppercase() } .horizontalAlign(HorizontalAlign.FILL) @@ -133,6 +134,10 @@ class AllocationDialog(project: Project?, config: ConnectionConfig, override var .bindItem(state.allocationParameters::allocationUnit.toNullableProperty()) .also { spaceUnitBox = it.component } .widthGroup(sameWidthComboBoxGroup) + contextHelp( + description = message("allocation.dialog.unit.size.hint.description"), + title = message("allocation.dialog.unit.size.hint.title") + ) } row { label("Primary allocation: ") @@ -301,7 +306,7 @@ class AllocationDialog(project: Project?, config: ConnectionConfig, override var */ private fun doPresetAssignment(preset: Presets) { val dataContainer = Presets.initDataClass(preset) - memberNameField.text = "SAMPLE" + memberNameField.text.ifEmpty { memberNameField.text = "SAMPLE" } datasetOrganizationBox.selectedItem = dataContainer.datasetOrganization spaceUnitBox.selectedItem = dataContainer.spaceUnit primaryAllocationField.text = dataContainer.primaryAllocation.toString() diff --git a/src/main/kotlin/org/zowe/explorer/explorer/ui/ChangeEncodingDialog.kt b/src/main/kotlin/org/zowe/explorer/explorer/ui/ChangeEncodingDialog.kt index 8801a63a9..7cf1831c8 100644 --- a/src/main/kotlin/org/zowe/explorer/explorer/ui/ChangeEncodingDialog.kt +++ b/src/main/kotlin/org/zowe/explorer/explorer/ui/ChangeEncodingDialog.kt @@ -13,6 +13,8 @@ package org.zowe.explorer.explorer.ui import com.intellij.CommonBundle import com.intellij.icons.AllIcons import com.intellij.ide.IdeBundle +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.SystemInfo @@ -35,6 +37,7 @@ import javax.swing.JLabel /** Class that represent dialog for changing file encoding. */ class ChangeEncodingDialog( + private val project: Project?, private val virtualFile: VirtualFile, private val attributes: RemoteUssAttributes, private val charset: Charset, @@ -97,7 +100,7 @@ class ChangeEncodingDialog( } attributes.charset = charset updateFileTag(attributes) - reloadIn(null, virtualFile, charset) + reloadIn(project, virtualFile, charset) close(RELOAD_EXIT_CODE) } } @@ -106,6 +109,7 @@ class ChangeEncodingDialog( } actions.add(reloadAction) reloadAction.putValue(Action.MNEMONIC_KEY, 'R'.code) + val severalProjectsOpen = ProjectManager.getInstance().openProjects.size > 1 val convertAction = object : DialogWrapperAction(IdeBundle.message("button.convert")) { override fun doAction(e: ActionEvent?) { if (safeToConvert == Magic8.NO_WAY && !showConvertAnywayDialog()) { @@ -114,13 +118,23 @@ class ChangeEncodingDialog( } attributes.charset = charset updateFileTag(attributes) - saveIn(null, virtualFile, charset) + saveIn(project, virtualFile, charset) close(CONVERT_EXIT_CODE) } + + override fun isEnabled(): Boolean { + // need to disable encoding conversion when more than one project is open + // see https://youtrack.jetbrains.com/issue/IDEA-346634/ + return !severalProjectsOpen + } } - if (!SystemInfo.isMac && safeToConvert == Magic8.NO_WAY) { + if (!SystemInfo.isMac && convertAction.isEnabled && safeToConvert == Magic8.NO_WAY) { convertAction.putValue(Action.SMALL_ICON, AllIcons.General.Warning) } + if (severalProjectsOpen) { + val tooltipText = message("encoding.convert.button.error.tooltip") + convertAction.putValue(Action.SHORT_DESCRIPTION, tooltipText) + } if (possibleToConvert) { actions.add(convertAction) } diff --git a/src/main/kotlin/org/zowe/explorer/explorer/ui/ExplorerPasteProvider.kt b/src/main/kotlin/org/zowe/explorer/explorer/ui/ExplorerPasteProvider.kt index d32eaa093..020992aed 100644 --- a/src/main/kotlin/org/zowe/explorer/explorer/ui/ExplorerPasteProvider.kt +++ b/src/main/kotlin/org/zowe/explorer/explorer/ui/ExplorerPasteProvider.kt @@ -25,8 +25,8 @@ import com.intellij.openapi.vfs.newvfs.impl.VirtualFileImpl import org.zowe.explorer.common.ui.cleanInvalidateOnExpand import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.attributes.RemoteDatasetAttributes -import org.zowe.explorer.dataops.attributes.RemoteMemberAttributes import org.zowe.explorer.dataops.attributes.RemoteUssAttributes +import org.zowe.explorer.dataops.content.synchronizer.RemoteAttributedContentSynchronizer import org.zowe.explorer.dataops.operations.mover.MoveCopyOperation import org.zowe.explorer.explorer.FileExplorerContentProvider import org.zowe.explorer.utils.castOrNull @@ -205,6 +205,17 @@ class ExplorerPasteProvider : PasteProvider { if (explorerView.isCut.get()) { copyPasteSupport.removeFromBuffer { node -> node.file == op.source } } + + // this step is necessary to clean old file after force overwriting performed. + if (op.forceOverwriting) { + val nameResolver = dataOpsManager.getNameResolver(op.source, op.destination) + op.destination.children + .filter { file -> file == nameResolver.getConflictingChild(op.source, op.destination) } + .forEach { file -> + dataOpsManager.getContentSynchronizer(file) + .castOrNull>()?.removeFromCacheAfterForceOverwriting(file) + } + } } .onFailure { throwable -> explorerView.explorer.reportThrowable(throwable, project) @@ -301,7 +312,7 @@ class ExplorerPasteProvider : PasteProvider { // conflicts end // specific configs resolution - val ussToPdsWarnings = pasteDestinations + val ussOrLocalFileToPdsWarnings = pasteDestinations .mapNotNull { destFile -> val destAttributes = dataOpsManager.tryToGetAttributes(destFile) if (destAttributes !is RemoteDatasetAttributes) null @@ -309,27 +320,28 @@ class ExplorerPasteProvider : PasteProvider { val sourceUssAttributes = sourceFiles .filter { sourceFile -> val sourceAttributes = dataOpsManager.tryToGetAttributes(sourceFile) - sourceAttributes is RemoteUssAttributes || sourceFile is VirtualFileImpl + sourceAttributes is RemoteUssAttributes || sourceFile.isInLocalFileSystem } sourceUssAttributes.map { Pair(destFile, it) }.ifEmpty { null } } } .flatten() - if ( - ussToPdsWarnings.isNotEmpty() && - !showYesNoDialog( - "USS File To PDS Placing", - "You are about to place USS file to PDS. All lines exceeding the record length will be truncated.", - null, - "Ok", - "Skip This Files", - AllIcons.General.WarningDialog - ) - ) { - conflictsResolutions.addAll( - ussToPdsWarnings.map { ConflictResolution(it.second, it.first).apply { resolveBySkip() } } - ) + if (ussOrLocalFileToPdsWarnings.isNotEmpty()) { + val isLocalFilesPresent = ussOrLocalFileToPdsWarnings.find { it.second.isInLocalFileSystem } != null + val fileTypesPattern = if (isLocalFilesPresent) "Local Files" else "USS Files" + if (!showYesNoDialog( + "$fileTypesPattern to PDS Placing", + "You are about to place $fileTypesPattern to PDS. All lines exceeding the record length will be truncated.", + null, + "Ok", + "Skip This Files", + AllIcons.General.WarningDialog + )) { + conflictsResolutions.addAll( + ussOrLocalFileToPdsWarnings.map { ConflictResolution(it.second, it.first).apply { resolveBySkip() } } + ) + } } // specific conflicts resolution end @@ -421,45 +433,23 @@ class ExplorerPasteProvider : PasteProvider { ): List { val result = mutableListOf() -// val skipDestinationSourceList = mutableListOf>() -// val overwriteDestinationSourceList = mutableListOf>() - - val listOfAllConflicts = pasteDestinations - .mapNotNull { destFile -> - val destAttributes = dataOpsManager.tryToGetAttributes(destFile) - destFile.children - ?.map conflicts@{ destChild -> - val filteredSourceFiles = sourceFiles.filter { source -> - val sourceAttributes = dataOpsManager.tryToGetAttributes(source) - if ( - destAttributes is RemoteDatasetAttributes && - (sourceAttributes is RemoteUssAttributes || source is VirtualFileImpl) - ) { - val memberName = source.name.filter { it.isLetterOrDigit() }.take(8).uppercase() - if (memberName.isNotEmpty()) memberName == destChild.name else "EMPTY" == destChild.name - } else if ( - destAttributes is RemoteDatasetAttributes && - sourceAttributes is RemoteDatasetAttributes - ) { - sourceAttributes.name.split(".").last() == destChild.name - } else { - source.name == destChild.name - } - } - val foundConflicts = mutableListOf>() - if (filteredSourceFiles.isNotEmpty()) { - filteredSourceFiles.forEach { foundConflict -> foundConflicts.add(Pair(destFile, foundConflict)) } - } - foundConflicts - } + val conflicts = pasteDestinations.map { destFile -> + + val conflictingSources = sourceFiles.filter { source -> + val nameResolver = dataOpsManager.getNameResolver(source, destFile) + nameResolver.getConflictingChild(source, destFile) != null + } + val foundConflicts = mutableListOf>() + if (conflictingSources.isNotEmpty()) { + conflictingSources.forEach { foundConflict -> foundConflicts.add(Pair(destFile, foundConflict)) } } + foundConflicts + } .flatten() - val conflicts = mutableListOf>() - listOfAllConflicts.forEach { conflictList -> conflicts.addAll(conflictList) } + .toMutableList() // Handle conflicts with different file type (file - directory, directory - file) -// skipDestinationSourceList.addAll(conflictsThatCannotBeSolved) val conflictsThatCannotBeOverwritten = conflicts.filter { val conflictChild = it.first.findChild(it.second.name) (conflictChild?.isDirectory == true && !it.second.isDirectory) @@ -548,26 +538,8 @@ class ExplorerPasteProvider : PasteProvider { } allConflicts.forEach { conflict -> - var copyIndex = 1 - val sourceName = conflict.second.name - val isSourceDirectory = conflict.second.isDirectory - var newName: String - val destAttributes = dataOpsManager.tryToGetAttributes(conflict.first) - val sourceAttributes = dataOpsManager.tryToGetAttributes(conflict.second) - do { - newName = if (destAttributes is RemoteDatasetAttributes && sourceAttributes is RemoteDatasetAttributes) { - "${sourceAttributes.name.split(".").last().take(7)}$copyIndex" - } else if (destAttributes is RemoteDatasetAttributes) { - if (sourceName.length >= 8) "${sourceName.take(7)}$copyIndex" else "$sourceName$copyIndex" - } else if (isSourceDirectory || sourceAttributes is RemoteDatasetAttributes) { - "${sourceName}_(${copyIndex})" - } else { - val extension = if (sourceName.contains(".")) sourceName.substringAfterLast(".") else null - val newNameWithoutExtension = "${sourceName.substringBeforeLast(".")}_(${copyIndex})" - if (extension != null) "$newNameWithoutExtension.$extension" else newNameWithoutExtension - } - ++copyIndex - } while (conflict.first.children.any { it.name == newName }) + + val newName = dataOpsManager.getNameResolver(conflict.second, conflict.first).resolve(conflict.second, conflict.first) val newNameMessage = "If you select option \"Use new name\", the following name will be selected: $newName" diff --git a/src/main/kotlin/org/zowe/explorer/explorer/ui/ExplorerTreeView.kt b/src/main/kotlin/org/zowe/explorer/explorer/ui/ExplorerTreeView.kt index f44032bc9..bfd3d0168 100644 --- a/src/main/kotlin/org/zowe/explorer/explorer/ui/ExplorerTreeView.kt +++ b/src/main/kotlin/org/zowe/explorer/explorer/ui/ExplorerTreeView.kt @@ -4,11 +4,13 @@ import com.intellij.ide.dnd.aware.DnDAwareTree import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.* import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.impl.text.EditorHighlighterUpdater import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.vfs.newvfs.BulkFileListener @@ -71,7 +73,7 @@ inline fun > AnActionEvent.getEx abstract class ExplorerTreeView, UnitConfig : EntityWithUuid> ( val explorer: Explorer, - project: Project, + private val project: Project, parentDisposable: Disposable, private val contextMenu: ActionGroup, rootNodeProvider: (explorer: Explorer, project: Project, treeStructure: ExplorerTreeStructureBase) -> ExplorerTreeNode, @@ -227,6 +229,14 @@ abstract class ExplorerTreeView) { + // listens for virtual file delete events and + // closes files opened in editor if file to be deleted is an ancestor of these files + events.filterIsInstance().forEach { + closeChildrenInEditor(it.file) + } + } + override fun after(events: List) { events .mapNotNull { @@ -374,5 +384,20 @@ abstract class ExplorerTreeView + if (VfsUtilCore.isAncestor(selectedFile, openFile, false)) { + val contentSynchronizer = service().getContentSynchronizer(openFile) + val syncProvider = DocumentedSyncProvider(openFile) + contentSynchronizer?.markAsNotNeededForSync(syncProvider) + runWriteActionInEdtAndWait { + fileEditorManager.closeFile(openFile) + } + } + } + } } diff --git a/src/main/kotlin/org/zowe/explorer/explorer/ui/ExplorerWindowFactory.kt b/src/main/kotlin/org/zowe/explorer/explorer/ui/ExplorerWindowFactory.kt index 6d7d9a0f1..b2d198535 100755 --- a/src/main/kotlin/org/zowe/explorer/explorer/ui/ExplorerWindowFactory.kt +++ b/src/main/kotlin/org/zowe/explorer/explorer/ui/ExplorerWindowFactory.kt @@ -22,6 +22,7 @@ import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.content.synchronizer.AutoSyncFileListener import org.zowe.explorer.dataops.content.synchronizer.DocumentedSyncProvider import org.zowe.explorer.explorer.UIComponentManager +import org.zowe.explorer.utils.runWriteActionInEdtAndWait import org.zowe.explorer.utils.subscribe /** Explorer window. This is the main class to represent the plugin */ @@ -50,7 +51,11 @@ class ExplorerWindowFactory : ToolWindowFactory, DumbAware { if (dataOpsManager.isSyncSupported(file)) { val contentSynchronizer = dataOpsManager.getContentSynchronizer(file) ?: return runBackgroundableTask("Synchronizing file ${file.name} with mainframe") { indicator -> - contentSynchronizer.synchronizeWithRemote(DocumentedSyncProvider(file), indicator) + val syncProvider = DocumentedSyncProvider(file) + runWriteActionInEdtAndWait { + syncProvider.saveDocument() + contentSynchronizer.synchronizeWithRemote(syncProvider, indicator) + } } } } diff --git a/src/main/kotlin/org/zowe/explorer/ui/build/TerminalCommandReceiver.kt b/src/main/kotlin/org/zowe/explorer/ui/build/TerminalCommandReceiver.kt index fb9a2cfe7..a494e6229 100644 --- a/src/main/kotlin/org/zowe/explorer/ui/build/TerminalCommandReceiver.kt +++ b/src/main/kotlin/org/zowe/explorer/ui/build/TerminalCommandReceiver.kt @@ -13,6 +13,7 @@ package org.zowe.explorer.ui.build import com.intellij.execution.process.NopProcessHandler import com.intellij.execution.process.ProcessHandler import com.intellij.execution.process.ProcessOutputType +import com.intellij.openapi.util.Key import com.intellij.terminal.TerminalExecutionConsole import com.jediterm.terminal.TerminalKeyEncoder import org.zowe.explorer.ui.build.tso.utils.InputRecognizer @@ -33,6 +34,7 @@ class TerminalCommandReceiver(terminalConsole: TerminalExecutionConsole) { private var needToWaitForCommandInput = false private var commandsInQueue: Queue = LinkedList() private var expectParameters = false + private var prevCommandEndsWithReady = false var initialized = false private var onCommandEntered: (String) -> Unit = {} @@ -146,15 +148,34 @@ class TerminalCommandReceiver(terminalConsole: TerminalExecutionConsole) { override fun getProcessInput(): OutputStream { return this@TerminalCommandReceiver.processInput } + + /** + * Override notifyTextAvailable() method to check what the command ends with + */ + override fun notifyTextAvailable(text: String, outputType: Key<*>) { + if (text != "\n" && text.endsWith("\n")) { + prevCommandEndsWithReady = isTextEndsWithReady(text) + } + super.notifyTextAvailable(text, outputType) + } } terminalConsole.withConvertLfToCrlfForNonPtyProcess(true) terminalConsole.attachToProcess(processHandler) } + /** + * Check if the text ends with "READY" - successful completion of TSO command + */ + private fun isTextEndsWithReady(text: String): Boolean { + val successfulEnding = "READY" + val trimmedText = text.trimEnd() + return trimmedText.endsWith(successfulEnding, true) + } + /** * Called when command is submitted. Clean up the entered command for follow up user input */ - private fun cleanCommand() { + fun cleanCommand() { this.typedCommand = "" this.textAfterCursor = "" this.cursorPosition = 0 @@ -193,6 +214,20 @@ class TerminalCommandReceiver(terminalConsole: TerminalExecutionConsole) { } } + /** + * Return true if console is waiting for command input or else otherwise + */ + fun isNeedToWaitForCommandInput(): Boolean { + return needToWaitForCommandInput + } + + /** + * Return true if previous command ends with "READY" or else otherwise + */ + fun isPrevCommandEndsWithReady(): Boolean { + return prevCommandEndsWithReady + } + /** * Called when user finished typing the command and pressed Enter */ diff --git a/src/main/kotlin/org/zowe/explorer/ui/build/tso/TSOWindowFactory.kt b/src/main/kotlin/org/zowe/explorer/ui/build/tso/TSOWindowFactory.kt index ec36412e9..a76def65c 100644 --- a/src/main/kotlin/org/zowe/explorer/ui/build/tso/TSOWindowFactory.kt +++ b/src/main/kotlin/org/zowe/explorer/ui/build/tso/TSOWindowFactory.kt @@ -350,7 +350,7 @@ class TSOWindowFactory : ToolWindowFactory { try { sendTopic(SESSION_RECONNECT_TOPIC, project).reconnect(project, console, session) processHandler.notifyTextAvailable( - "Successfully reconnected to the TSO session.\n", + "Successfully reconnected to the TSO session.\nREADY\n", ProcessOutputType.STDOUT ) } catch (e: Exception) { diff --git a/src/main/kotlin/org/zowe/explorer/ui/build/tso/ui/TSOConsoleView.kt b/src/main/kotlin/org/zowe/explorer/ui/build/tso/ui/TSOConsoleView.kt index f3156d469..72f7d5e5d 100644 --- a/src/main/kotlin/org/zowe/explorer/ui/build/tso/ui/TSOConsoleView.kt +++ b/src/main/kotlin/org/zowe/explorer/ui/build/tso/ui/TSOConsoleView.kt @@ -11,15 +11,21 @@ package org.zowe.explorer.ui.build.tso.ui //import com.intellij.ui.layout.cellPanel +import com.intellij.execution.process.ProcessEvent import com.intellij.execution.process.ProcessHandler +import com.intellij.execution.process.ProcessListener import com.intellij.execution.ui.ExecutionConsole import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.Key import com.intellij.terminal.TerminalExecutionConsole import com.intellij.ui.CollectionComboBoxModel import com.intellij.ui.SimpleListCellRenderer import com.intellij.ui.components.JBPanel import com.intellij.ui.dsl.builder.panel +import com.intellij.util.ui.JBEmptyBorder +import org.zowe.explorer.common.isDebugModeEnabled import org.zowe.explorer.dataops.operations.MessageData import org.zowe.explorer.dataops.operations.MessageType import org.zowe.explorer.ui.build.TerminalCommandReceiver @@ -29,7 +35,7 @@ import org.zowe.explorer.ui.build.tso.utils.InputRecognizer import org.zowe.explorer.utils.log import org.zowe.explorer.utils.sendTopic import java.awt.BorderLayout -import javax.swing.JComboBox +import javax.swing.JButton import javax.swing.JComponent /** @@ -42,12 +48,13 @@ class TSOConsoleView( private var tsoSession: TSOConfigWrapper ) : ExecutionConsole, JBPanel() { - private lateinit var tsoMessageType: JComboBox - private lateinit var tsoDataType: JComboBox + private lateinit var tsoMessageTypeBox: ComboBox + private lateinit var tsoDataTypeBox: ComboBox + private lateinit var cancelCommandButton: JButton private val tsoWidthGroup: String = "TSO_WIDTH_GROUP" private val tsoMessageTypes: List = - listOf(MessageType.TSO_MESSAGE, MessageType.TSO_PROMPT, MessageType.TSO_RESPONSE) + listOf(MessageType.TSO_RESPONSE, MessageType.TSO_MESSAGE, MessageType.TSO_PROMPT) private val tsoDataTypes: List = listOf(MessageData.DATA_DATA, MessageData.DATA_HIDDEN, MessageData.DATA_ACTION) @@ -61,6 +68,8 @@ class TSOConsoleView( private val log = log() + private val debugMode = isDebugModeEnabled() + /** * UI panel which contains 2 combo boxes of TSO message type and message data type */ @@ -73,18 +82,35 @@ class TSOConsoleView( model = tsoMessageTypeComboBoxModel, renderer = SimpleListCellRenderer.create("") { it.type } ).also { - tsoMessageType = it.component + tsoMessageTypeBox = it.component } - } + }.visible(debugMode) row { label("TSO data type").widthGroup(tsoWidthGroup) comboBox( model = tsoDataTypeComboBoxModel, renderer = SimpleListCellRenderer.create("") { it.data } ).also { - tsoDataType = it.component + tsoDataTypeBox = it.component + } + }.visible(debugMode) + row { + button("Cancel Command (PA1)") { + log.info("CANCEL COMMAND (PA1)") + val prevTsoMessageType = tsoMessageTypeBox.item + val prevTsoDataType = tsoDataTypeBox.item + tsoMessageTypeBox.item = MessageType.TSO_RESPONSE + tsoDataTypeBox.item = MessageData.DATA_ACTION + terminalCommandReceiver.cleanCommand() + processHandler.processInput?.write(("\r").toByteArray()) + tsoMessageTypeBox.item = prevTsoMessageType + tsoDataTypeBox.item = prevTsoDataType + }.also { + cancelCommandButton = it.component } } + }.also { + it.border = JBEmptyBorder(10, 15, 10, 15) } } @@ -102,8 +128,8 @@ class TSOConsoleView( this, tsoSession, enteredCommand.trim(), - tsoMessageType.selectedItem as MessageType, - tsoDataType.selectedItem as MessageData, + tsoMessageTypeBox.item, + tsoDataTypeBox.item, processHandler ) terminalCommandReceiver.waitForCommandInput() @@ -111,6 +137,17 @@ class TSOConsoleView( terminalCommandReceiver.initialized = true + processHandler.addProcessListener(object : ProcessListener { + override fun startNotified(event: ProcessEvent) {} + + override fun processTerminated(event: ProcessEvent) {} + + override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { + cancelCommandButton.isEnabled = + !terminalCommandReceiver.isPrevCommandEndsWithReady() && terminalCommandReceiver.isNeedToWaitForCommandInput() + } + }) + Disposer.register(this, consoleView) layout = BorderLayout() add(this.component, BorderLayout.WEST) diff --git a/src/main/kotlin/org/zowe/explorer/ui/build/tso/utils/InputRecognizer.kt b/src/main/kotlin/org/zowe/explorer/ui/build/tso/utils/InputRecognizer.kt index 446fa9456..59f9aea68 100644 --- a/src/main/kotlin/org/zowe/explorer/ui/build/tso/utils/InputRecognizer.kt +++ b/src/main/kotlin/org/zowe/explorer/ui/build/tso/utils/InputRecognizer.kt @@ -39,6 +39,10 @@ class InputRecognizer( */ fun recognizeMessage(command: String) : Message<*> { val upperCommand = command.toUpperCasePreservingASCIIRules().trim() + if (upperCommand.isEmpty()) { + previousCommand = upperCommand + return OrdinaryMessage(command, session) + } val parseExecStm = upperCommand.substringBefore(" ") if (parseExecStm == "EX" || parseExecStm == "EXEC" || previousCommand == "EX" || previousCommand == "EXEC") { val parseAfterExec = upperCommand.substringAfter(" ").trim() diff --git a/src/main/kotlin/org/zowe/explorer/utils/encodingUtils.kt b/src/main/kotlin/org/zowe/explorer/utils/encodingUtils.kt index 1f88cab03..65825a235 100644 --- a/src/main/kotlin/org/zowe/explorer/utils/encodingUtils.kt +++ b/src/main/kotlin/org/zowe/explorer/utils/encodingUtils.kt @@ -19,7 +19,6 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.fileEditor.impl.LoadTextUtil import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages @@ -48,24 +47,14 @@ import javax.swing.Icon * @param charset new encoding. */ fun saveIn(project: Project?, virtualFile: VirtualFile, charset: Charset) { - val contentSynchronizer = DataOpsManager.instance.getContentSynchronizer(virtualFile) ?: return - val syncProvider = DocumentedSyncProvider(virtualFile) - val fileUploadNeeded = contentSynchronizer.isFileUploadNeeded(syncProvider) - val document = syncProvider.getDocument() ?: return + val syncProvider = DocumentedSyncProvider(virtualFile, SaveStrategy.default(project)) syncProvider.saveDocument() - val bytes = contentSynchronizer.successfulContentStorage(syncProvider) - val text = if (fileUploadNeeded) document.text - else LoadTextUtil.getTextByBinaryPresentation(bytes, virtualFile).toString() - EncodingManager.getInstance().setEncoding(virtualFile, charset) - virtualFile.charset = charset + val bytes = syncProvider.retrieveCurrentContent() runWriteActionInEdtAndWait { - LoadTextUtil.write( - project, - virtualFile, - virtualFile, - text, - document.modificationStamp - ) + changeEncodingTo(virtualFile, charset) + virtualFile.getOutputStream(null).use { + it.write(bytes) + } } } @@ -77,20 +66,17 @@ fun saveIn(project: Project?, virtualFile: VirtualFile, charset: Charset) { */ fun reloadIn(project: Project?, virtualFile: VirtualFile, charset: Charset) { val syncProvider = DocumentedSyncProvider(virtualFile, SaveStrategy.syncOnOpen(project)) + val contentSynchronizer = DataOpsManager.instance.getContentSynchronizer(virtualFile) runWriteActionInEdtAndWait { - EncodingManager.getInstance().setEncoding(virtualFile, charset) - virtualFile.charset = charset - val contentSynchronizer = DataOpsManager.instance.getContentSynchronizer(virtualFile) + changeEncodingTo(virtualFile, charset) contentSynchronizer?.synchronizeWithRemote(syncProvider) } } /** Changes the file encoding to the specified one. */ -fun changeFileEncodingTo(file: VirtualFile, charset: Charset) { - runWriteActionInEdtAndWait { - EncodingManager.getInstance().setEncoding(file, charset) - file.charset = charset - } +fun changeEncodingTo(file: VirtualFile, charset: Charset) { + EncodingManager.getInstance().setEncoding(file, charset) + file.charset = charset } /** Checks if it is safe to reload file in the specified encoding. */ @@ -152,11 +138,11 @@ fun inspectSafeEncodingChange(virtualFile: VirtualFile, charset: Charset): Encod * Change file encoding action that invokes the change encoding dialog. * @return true if changed or false otherwise. */ -fun changeFileEncodingAction(virtualFile: VirtualFile, attributes: RemoteUssAttributes, charset: Charset): Boolean { +fun changeFileEncodingAction(project: Project?, virtualFile: VirtualFile, attributes: RemoteUssAttributes, charset: Charset): Boolean { val encodingInspection = inspectSafeEncodingChange(virtualFile, charset) val safeToReload = encodingInspection.safeToReload val safeToConvert = encodingInspection.safeToConvert - val dialog = ChangeEncodingDialog(virtualFile, attributes, charset, safeToReload, safeToConvert) + val dialog = ChangeEncodingDialog(project, virtualFile, attributes, charset, safeToReload, safeToConvert) dialog.show() return dialog.exitCode == ChangeEncodingDialog.RELOAD_EXIT_CODE || dialog.exitCode == ChangeEncodingDialog.CONVERT_EXIT_CODE } @@ -171,7 +157,7 @@ fun createCharsetsActionGroup(virtualFile: VirtualFile, attributes: RemoteUssAtt val action: (charset: Charset, icon: Icon?) -> DumbAwareAction = { charset, icon -> object : DumbAwareAction(charset.name(), null, icon) { override fun actionPerformed(e: AnActionEvent) { - changeFileEncodingAction(virtualFile, attributes, charset) + changeFileEncodingAction(e.project, virtualFile, attributes, charset) } override fun update(e: AnActionEvent) { diff --git a/src/main/kotlin/org/zowe/explorer/vfs/MFVirtualFileSystemModel.kt b/src/main/kotlin/org/zowe/explorer/vfs/MFVirtualFileSystemModel.kt index 60381d2ef..cf8115605 100755 --- a/src/main/kotlin/org/zowe/explorer/vfs/MFVirtualFileSystemModel.kt +++ b/src/main/kotlin/org/zowe/explorer/vfs/MFVirtualFileSystemModel.kt @@ -148,8 +148,8 @@ class MFVirtualFileSystemModel { return FilteringBFSIterator(fsGraph, root) { v, e -> v.validReadLock(false) { (pointerIndex < pathElements.size - && e.type == FSEdgeType.DIR - && pathElements[pointerIndex] == v.name) + && e.type == FSEdgeType.DIR + && pathElements[pointerIndex] == v.name) .also { successful -> if (successful) ++pointerIndex } @@ -158,11 +158,12 @@ class MFVirtualFileSystemModel { } /** - * Find the mainframe virtual file by provided path + * Find the mainframe virtual file by provided path. Usually, the path is a URL string with "/" at the end, + * so the filter is used to remove empty element at the end * @param path string path to search for the virtual file */ fun findFileByPath(path: String): MFVirtualFile? { - val pathElements = path.formatPath().split(MFVirtualFileSystem.SEPARATOR) + val pathElements = path.formatPath().split(MFVirtualFileSystem.SEPARATOR).filter(String::isEmpty) return findFileByPath(pathElements) } @@ -635,7 +636,7 @@ class MFVirtualFileSystemModel { } } try { - sendVfsChangesTopic().after(event) + sendVfsChangesTopic().after(event) } catch (ignored: InvalidPathException) { log.warn(ignored) } @@ -750,13 +751,14 @@ class MFVirtualFileSystemModel { private fun String.formatPath(): String { val separator = MFVirtualFileSystem.SEPARATOR - return replace(Regex("$separator+"), separator).let { - if (it.startsWith(separator)) { - it.substringAfter(separator) - } else { - it + return replace(Regex("$separator+"), separator) + .let { + if (it.startsWith(separator)) { + it.substringAfter(separator) + } else { + it + } } - } } fun findFileByPathIfCached(path: String): MFVirtualFile? { diff --git a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt index b3fe7e87b..0b858693e 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt @@ -10,8 +10,10 @@ package org.zowe.explorer.zowe.service +import com.intellij.notification.Notification import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType +import com.intellij.notification.Notifications import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.components.service import com.intellij.openapi.project.DumbAwareAction @@ -35,6 +37,8 @@ import java.nio.file.Path import java.util.* import java.util.stream.Collectors +const val ZOWE_CONFIG_NOTIFICATION_GROUP_ID = "org.zowe.explorerzowe.service.ZoweConfigNotificationGroupId" + val ZOWE_PROJECT_PREFIX = "zowe-" /** @@ -53,6 +57,22 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService private val zoweConnectionName: String get() = "$ZOWE_PROJECT_PREFIX${myProject.name}" + /** + * Displays an error notification if an error was received. + * @param t thrown error. + * @param title error text. + */ + private fun notifyError(t: Throwable, title: String? = null) { + Notifications.Bus.notify( + Notification( + ZOWE_CONFIG_NOTIFICATION_GROUP_ID, + title ?: "Error with Zowe config file", + t.message ?: t.toString(), + NotificationType.ERROR + ) + ) + } + /** * Checks project contains zowe.config.json. If zowe config presented * it will parse it and save to object model inside zoweConfig field. @@ -69,7 +89,7 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService zoweConfig = it } } catch (e: Exception) { - null + throw Exception("Cannot parse Zowe config file") } } @@ -155,30 +175,35 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService * @see ZoweConfigService.addOrUpdateZoweConfig */ override fun addOrUpdateZoweConfig(scanProject: Boolean, checkConnection: Boolean): ConnectionConfig? { - val zoweConfig = if (scanProject) { - scanForZoweConfig() - } else this.zoweConfig - zoweConfig ?: return null - val username = zoweConfig.user ?: return null - val password = zoweConfig.password ?: return null - val zoweConnection = findExistingConnection()?.let { - zoweConfig.toConnectionConfig(it.uuid, it.zVersion) - } ?: zoweConfig.toConnectionConfig(UUID.randomUUID().toString()) - CredentialService.instance.setCredentials(zoweConnection.uuid, username, password) - - if (checkConnection) { - try { - testAndPrepareConnection(zoweConnection) - } catch (t: Throwable) { - notifyUiOnConnectionFailure("Connection to ${zoweConnection.url} failed.", t.message ?: "") - return null + return try { + val zoweConfig = if (scanProject) { + scanForZoweConfig() + } else this.zoweConfig + zoweConfig ?: throw Exception("Cannot get Zowe config") + val username = zoweConfig.user ?: throw Exception("Cannot get username for Zowe config") + val password = zoweConfig.password ?: throw Exception("Cannot get password for Zowe config") + val zoweConnection = findExistingConnection()?.let { + zoweConfig.toConnectionConfig(it.uuid, it.zVersion) + } ?: zoweConfig.toConnectionConfig(UUID.randomUUID().toString()) + CredentialService.instance.setCredentials(zoweConnection.uuid, username, password) + + if (checkConnection) { + try { + testAndPrepareConnection(zoweConnection) + } catch (t: Throwable) { + notifyUiOnConnectionFailure("Connection to ${zoweConnection.url} failed.", t.message ?: "") + return null + } } - } - val connectionOpt = configCrudable.addOrUpdate(zoweConnection) - return if (connectionOpt.isEmpty) null else connectionOpt.get().also { - CredentialService.instance.setCredentials(it.uuid, username, password) - sendTopic(ZOWE_CONFIG_CHANGED).onConfigSaved(zoweConfig, zoweConnection) + val connectionOpt = configCrudable.addOrUpdate(zoweConnection) + return if (connectionOpt.isEmpty) null else connectionOpt.get().also { + CredentialService.instance.setCredentials(it.uuid, username, password) + sendTopic(ZOWE_CONFIG_CHANGED).onConfigSaved(zoweConfig, zoweConnection) + } + } catch (e: Exception) { + notifyError(e) + null } } @@ -218,7 +243,11 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService */ override fun getZoweConfigState(scanProject: Boolean): ZoweConfigState { if (scanProject) { - scanForZoweConfig() + try { + scanForZoweConfig() + } catch (e: Exception) { + notifyError(e) + } } val zoweConfig = zoweConfig ?: return ZoweConfigState.NOT_EXISTS val existingConnection = findExistingConnection() ?: return ZoweConfigState.NEED_TO_ADD diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 7d819af61..22a8c7322 100755 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -14,9 +14,7 @@ Zowe com.intellij.modules.platform If you are going to use or are using the plugin in Android Studio, please write to us: YKirkorava@ibagroup.eu -
-Zowe™ Explorer plug-in for IntelliJ IDEA™ is an IBA Group-developed open-source plugin that gives access to mainframes from a modern IDE. +Zowe Explorer for IntelliJ IDEA is an open-source plugin by the Zowe Community which gives access to mainframes from a modern IDE. With the help of the plugin, developers can work with mainframes through a modern interface, using a single toolbar, structured trees, and drag-and-drop operations. The solution suits those who need to work with z/OS datasets, USS files, and Jobs from the IntelliJ IDEA.
@@ -55,6 +53,9 @@ Feel free to recommend or report everything on our Slack
About us:
+Under the Open Mainframe Project umbrella, Zowe is an integrated and extensible open source framework for z/OS that combines the past and present to build the future of mainframes. The Zowe Explorer for Intellij IDEA is developed by the Zowe community, and you can visit the Zowe website to learn more about Zowe. Contributing to the Zowe Explorer for Intellij IDEA is the IBA Group, and other contributors to Zowe community projects include Broadcom, IBM, and Rocket Software. + +About the IBA Group: With over 30 years of experience in the mainframe domain, IBA Group is committed to maximizing your mainframe investment and enhancing your IT flexibility. One of the services we offer is Mainframe DevOps. Our approach is highly flexible, as we work with customers to choose the essential toolset for establishing a pipeline based on their preferences, existing tools, and the latest open-source opportunities (such as Zowe™ and plugins).
@@ -95,6 +96,9 @@ Thank you for considering IBA Group for your mainframe needs. + + @@ -218,6 +222,18 @@ Thank you for considering IBA Group for your mainframe needs. + + + + + + + + zowe-logo-icon-color + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/messages/FMBundle.properties b/src/main/resources/messages/FMBundle.properties index d40e28a74..f3234c9a3 100755 --- a/src/main/resources/messages/FMBundle.properties +++ b/src/main/resources/messages/FMBundle.properties @@ -24,3 +24,6 @@ encoding.reload.or.convert.dialog.title={0}: Reload or Convert to {1} encoding.reload.or.convert.dialog.message=The encoding you'''ve chosen ('{1}') may change the contents of '{0}'.
Do you want to
1. Reload the file from remote in the new encoding '{1}' and overwrite contents (may not display correctly) or
2. Convert the text and overwrite file in the new encoding?
encoding.reload.dialog.title={0}: Reload to {1} encoding.reload.dialog.message=The encoding you'''ve chosen ('{1}') may change the contents of '{0}'.
Do you want to Reload the file from remote in the new encoding '{1}' and overwrite contents (may not display correctly).
+encoding.convert.button.error.tooltip=Encoding conversion is not available because more than one project is open +allocation.dialog.unit.size.hint.description=For IBM 3390 direct access storage device:
1 CYLINDER = 15 TRACKS
1 TRACK = 56664 BYTES +allocation.dialog.unit.size.hint.title=Allocation unit Size \ No newline at end of file diff --git a/src/main/resources/settings.properties b/src/main/resources/settings.properties new file mode 100644 index 000000000..ccee7c8ba --- /dev/null +++ b/src/main/resources/settings.properties @@ -0,0 +1 @@ +debug.mode=false \ No newline at end of file diff --git a/src/test/kotlin/org/zowe/explorer/common/CommonTestSpec.kt b/src/test/kotlin/org/zowe/explorer/common/CommonTestSpec.kt index 029799717..6803e2686 100644 --- a/src/test/kotlin/org/zowe/explorer/common/CommonTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/common/CommonTestSpec.kt @@ -10,9 +10,16 @@ package org.zowe.explorer.common -import io.kotest.core.spec.style.ShouldSpec +import org.zowe.explorer.testutils.WithApplicationShouldSpec +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.shouldBe +import io.mockk.* +import java.util.Properties -class CommonTestSpec : ShouldSpec({ +class CommonTestSpec : WithApplicationShouldSpec({ + afterSpec { + clearAllMocks() + } context("common module: ui") { // ValidatingCellRenderer.getTableCellRendererComponent should("get table cell renderer") {} @@ -26,4 +33,45 @@ class CommonTestSpec : ShouldSpec({ // StatefulDialog.showUntilDone should("show dialog until it is fulfilled") {} } + context("common module: SettingsPropertyManager") { + val propertyName = "debug.mode" + + mockkConstructor(Properties::class) + + // isDebugModeEnabled + should("debug mode enabled") { + every { anyConstructed().getProperty(propertyName) } returns "true" + val debugMode = isDebugModeEnabled() + + assertSoftly { + debugMode shouldBe true + } + } + should("debug mode disabled") { + every { anyConstructed().getProperty(propertyName) } returns "false" + val debugMode = isDebugModeEnabled() + + assertSoftly { + debugMode shouldBe false + } + } + should("debug mode property not found") { + every { anyConstructed().getProperty(propertyName) } returns null + val debugMode = isDebugModeEnabled() + + assertSoftly { + debugMode shouldBe false + } + } + should("debug mode property contains a non-boolean value") { + every { anyConstructed().getProperty(propertyName) } returns "123" + val debugMode = isDebugModeEnabled() + + assertSoftly { + debugMode shouldBe false + } + } + + unmockkAll() + } }) diff --git a/src/test/kotlin/org/zowe/explorer/config/ConfigTestSpec.kt b/src/test/kotlin/org/zowe/explorer/config/ConfigTestSpec.kt index 83c067367..f3bea8222 100644 --- a/src/test/kotlin/org/zowe/explorer/config/ConfigTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/config/ConfigTestSpec.kt @@ -12,6 +12,8 @@ package org.zowe.explorer.config import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.ValidationInfo import org.zowe.explorer.config.connect.ConnectionConfig import org.zowe.explorer.config.connect.Credentials import org.zowe.explorer.config.connect.CredentialsConfigDeclaration @@ -24,6 +26,9 @@ import org.zowe.explorer.config.connect.ui.zosmf.initEmptyUuids import org.zowe.explorer.config.connect.whoAmI import org.zowe.explorer.config.ws.FilesWorkingSetConfig import org.zowe.explorer.config.ws.JesWorkingSetConfig +import org.zowe.explorer.config.ws.ui.AbstractWsDialog +import org.zowe.explorer.config.ws.ui.FilesWorkingSetDialogState +import org.zowe.explorer.config.ws.ui.files.FilesWorkingSetDialog import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.Operation import org.zowe.explorer.dataops.operations.TsoOperation @@ -38,16 +43,14 @@ import org.zowe.explorer.utils.service import io.kotest.assertions.assertSoftly import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe -import io.mockk.clearAllMocks -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.mockkStatic -import io.mockk.unmockkAll +import io.mockk.* import org.zowe.kotlinsdk.MessageType import org.zowe.kotlinsdk.TsoData import org.zowe.kotlinsdk.TsoResponse import org.zowe.kotlinsdk.annotations.ZVersion +import java.util.* +import java.util.stream.Stream +import javax.swing.JComponent import kotlin.reflect.KFunction class ConfigTestSpec : WithApplicationShouldSpec({ @@ -322,6 +325,31 @@ class ConfigTestSpec : WithApplicationShouldSpec({ should("check if the sandbox is modified") {} } context("config module: ws") { + + lateinit var crudableMockk: Crudable + + beforeEach { + mockkObject(AbstractWsDialog) + every { AbstractWsDialog["initialize"](any<() -> Unit>()) } returns Unit + + crudableMockk = mockk() + every { crudableMockk.getAll(ConnectionConfig::class.java) } returns Stream.of() + every { + crudableMockk.getByUniqueKey(ConnectionConfig::class.java, any()) + } returns Optional.of(ConnectionConfig()) + + mockkConstructor(DialogPanel::class) + every { anyConstructed().registerValidators(any(), any()) } answers { + val componentValidityChangedCallback = secondArg<(Map) -> Unit>() + componentValidityChangedCallback(mapOf()) + } + } + + afterEach { + unmockkAll() + clearAllMocks() + } + // WSNameColumn.validateEntered should("check that the entered working set name is not empty") {} should("check that the entered working set name is not blank") {} @@ -335,5 +363,13 @@ class ConfigTestSpec : WithApplicationShouldSpec({ should("check that the error appears on any errors for file masks") {} should("check that the error appears on empty file working set") {} should("check that the error appears on adding the same file mask again") {} + // ui/AbstractWsDialog.init + should("check that OK action is enabled if validation map is empty") { + + val dialog = FilesWorkingSetDialog(crudableMockk, FilesWorkingSetDialogState()) + + verify { anyConstructed().registerValidators(any(), any()) } + assertSoftly { dialog.isOKActionEnabled shouldBe true } + } } }) diff --git a/src/test/kotlin/org/zowe/explorer/dataops/OperationsTestSpec.kt b/src/test/kotlin/org/zowe/explorer/dataops/OperationsTestSpec.kt index e2d152c57..2d4d16ad6 100644 --- a/src/test/kotlin/org/zowe/explorer/dataops/OperationsTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/dataops/OperationsTestSpec.kt @@ -11,10 +11,10 @@ package org.zowe.explorer.dataops import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.fileEditor.impl.LoadTextUtil import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.LineSeparator import org.zowe.explorer.api.ZosmfApi import org.zowe.explorer.config.connect.ConnectionConfig import org.zowe.explorer.config.ws.DSMask @@ -29,7 +29,6 @@ import org.zowe.explorer.dataops.attributes.Requester import org.zowe.explorer.dataops.attributes.UssRequester import org.zowe.explorer.dataops.content.synchronizer.ContentSynchronizer import org.zowe.explorer.dataops.content.synchronizer.DocumentedSyncProvider -import org.zowe.explorer.dataops.content.synchronizer.LF_LINE_SEPARATOR import org.zowe.explorer.dataops.operations.DeleteOperation import org.zowe.explorer.dataops.operations.mover.CrossSystemMemberOrUssFileOrSequentialToUssDirMover import org.zowe.explorer.dataops.operations.mover.MoveCopyOperation @@ -37,10 +36,7 @@ import org.zowe.explorer.dataops.operations.mover.RemoteToLocalFileMover import org.zowe.explorer.testutils.WithApplicationShouldSpec import org.zowe.explorer.testutils.testServiceImpl.TestDataOpsManagerImpl import org.zowe.explorer.testutils.testServiceImpl.TestZosmfApiImpl -import org.zowe.explorer.utils.castOrNull -import org.zowe.explorer.utils.changeFileEncodingTo -import org.zowe.explorer.utils.service -import org.zowe.explorer.utils.setUssFileTag +import org.zowe.explorer.utils.* import org.zowe.explorer.vfs.MFVirtualFile import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow @@ -136,7 +132,7 @@ class OperationsTestSpec : WithApplicationShouldSpec({ val virtualFileMockk = mockk() val charsetMockk = mockk() every { virtualFileMockk.charset } returns charsetMockk - every { virtualFileMockk.detectedLineSeparator } returns LF_LINE_SEPARATOR + every { virtualFileMockk.detectedLineSeparator } returns LineSeparator.LF.separatorString var encodingChanged = false var lineSeparatorChanged = false @@ -146,16 +142,15 @@ class OperationsTestSpec : WithApplicationShouldSpec({ lineSeparatorChanged = false mockkStatic(LocalFileSystem::getInstance) - every { LocalFileSystem.getInstance().refreshAndFindFileByIoFile(fileMockk) } returns mockk() - - mockkStatic(::changeFileEncodingTo) - every { changeFileEncodingTo(any(), charsetMockk) } answers { - encodingChanged = true + every { LocalFileSystem.getInstance().refreshAndFindFileByIoFile(fileMockk) } returns mockk { + every { detectedLineSeparator = any() } answers { + lineSeparatorChanged = true + } } - mockkStatic(LoadTextUtil::changeLineSeparators) - every { LoadTextUtil.changeLineSeparators(null, any(), LF_LINE_SEPARATOR, any()) } answers { - lineSeparatorChanged = true + mockkStatic(::changeEncodingTo) + every { changeEncodingTo(any(), charsetMockk) } answers { + encodingChanged = true } } afterEach { diff --git a/src/test/kotlin/org/zowe/explorer/dataops/operations/MemberAllocatorTestSpec.kt b/src/test/kotlin/org/zowe/explorer/dataops/operations/MemberAllocatorTestSpec.kt new file mode 100644 index 000000000..994fe7d2d --- /dev/null +++ b/src/test/kotlin/org/zowe/explorer/dataops/operations/MemberAllocatorTestSpec.kt @@ -0,0 +1,155 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ + +package org.zowe.explorer.dataops.operations + +import com.intellij.openapi.progress.ProgressIndicator +import org.zowe.explorer.api.ZosmfApi +import org.zowe.explorer.config.connect.ConnectionConfig +import org.zowe.explorer.config.connect.authToken +import org.zowe.explorer.dataops.exceptions.CallException +import org.zowe.explorer.testutils.WithApplicationShouldSpec +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.* +import org.junit.jupiter.api.assertThrows +import org.zowe.kotlinsdk.* +import retrofit2.Call +import retrofit2.Response + +class MemberAllocatorTestSpec : WithApplicationShouldSpec({ + + afterSpec { + clearAllMocks() + } + + context("MemberAllocator test spec") { + + val classUnderTest = spyk() + + context("run operation") { + + val progressIndicator = mockk() + every { progressIndicator.checkCanceled() } just Runs + val connectionConfig = mockk() + every { connectionConfig.name } returns "test_connection" + every { connectionConfig.authToken } returns "auth_token" + val memberAllocationParams = mockk() + val memberAllocationOperation = mockk() + every { memberAllocationParams.memberName } returns "test" + every { memberAllocationParams.datasetName } returns "ZOSMFAD.TEST" + every { memberAllocationOperation.request } returns memberAllocationParams + every { memberAllocationOperation.connectionConfig } returns connectionConfig + + val dataApi = mockk() + val listCall = mockk>() + val listResponse = mockk>() + val writeCall = mockk>() + val writeResponse = mockk>() + mockkObject(ZosmfApi) + every { ZosmfApi.instance.hint(DataAPI::class).getApi(any(), any()) } returns dataApi + every { ZosmfApi.instance.hint(DataAPI::class).getApiWithBytesConverter(any(), any()) } returns dataApi + every { dataApi.listDatasetMembers(any(), any(), any(), any(), any(), any(), any()) } returns listCall + every { dataApi.writeToDatasetMember(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } returns writeCall + + val membersList = mockk() + val member1 = mockk() + val member2 = mockk() + + should("run successfully given valid params and members list is empty") { + //given + every { membersList.items } returns mutableListOf() + every { listResponse.body() } returns membersList + every { listCall.execute() } returns listResponse + every { writeCall.execute() } returns writeResponse + every { listResponse.isSuccessful } returns true + every { writeResponse.isSuccessful } returns true + + //when + classUnderTest.run(memberAllocationOperation, progressIndicator) + + //then + verify(exactly = 1) { dataApi.writeToDatasetMember(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } + clearMocks(dataApi, answers = false, childMocks = false) + } + + should("run successfully given valid params") { + //given + every { member1.name } returns "AAAA" + every { member2.name } returns "BBBB" + every { membersList.items } returns mutableListOf(member1, member2) + every { listResponse.body() } returns membersList + every { listCall.execute() } returns listResponse + every { writeCall.execute() } returns writeResponse + every { listResponse.isSuccessful } returns true + every { writeResponse.isSuccessful } returns true + + //when + classUnderTest.run(memberAllocationOperation, progressIndicator) + + //then + verify(exactly = 1) { dataApi.writeToDatasetMember(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } + clearMocks(dataApi, answers = false, childMocks = false) + } + + should("throw error if writeResponse was not successful") { + //given + every { writeResponse.isSuccessful } returns false + every { writeResponse.code() } returns 403 + + //when + val exception = assertThrows { classUnderTest.run(memberAllocationOperation, progressIndicator) } + + //then + assertSoftly { + exception shouldNotBe null + exception.message shouldBe "Cannot create member TEST in ZOSMFAD.TEST on test_connection.\n" + "Code: 403" + } + } + + should("throw error if listResponse was not successful") { + //given + every { listResponse.isSuccessful } returns false + every { listResponse.code() } returns 403 + + //when + val exception = assertThrows { classUnderTest.run(memberAllocationOperation, progressIndicator) } + + //then + assertSoftly { + exception shouldNotBe null + exception.message shouldBe "Cannot fetch member list for ZOSMFAD.TEST\n" + "Code: 403" + } + } + + should("throw error when listResponse was successful, but membersList contains duplicate member") { + //given + every { member1.name } returns "AAAA" + every { member2.name } returns "TEST" + every { membersList.items } returns mutableListOf(member1, member2) + every { listResponse.body() } returns membersList + every { listCall.execute() } returns listResponse + every { listResponse.isSuccessful } returns true + every { listResponse.code() } returns 404 + + //when + val exception = assertThrows { classUnderTest.run(memberAllocationOperation, progressIndicator) } + + //then + assertSoftly { + exception shouldNotBe null + exception.message shouldBe "Cannot create member TEST in ZOSMFAD.TEST on test_connection. Member with name TEST already exists.\n" + "Code: 404" + } + } + unmockkAll() + } + } +}) diff --git a/src/test/kotlin/org/zowe/explorer/editor/EditorTestSpec.kt b/src/test/kotlin/org/zowe/explorer/editor/EditorTestSpec.kt index 16772740c..f28a3c76a 100644 --- a/src/test/kotlin/org/zowe/explorer/editor/EditorTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/editor/EditorTestSpec.kt @@ -74,7 +74,6 @@ import java.nio.charset.CoderResult import javax.swing.JComponent import javax.swing.SwingUtilities import kotlin.reflect.KFunction -import kotlin.reflect.full.declaredFunctions class EditorTestSpec : WithApplicationShouldSpec({ afterSpec { @@ -299,10 +298,8 @@ class EditorTestSpec : WithApplicationShouldSpec({ val decoderResultMock = mockk() every { decoderResultMock.length() } returns 1 - val getLastItemRef = ContainerUtil::class.declaredFunctions - .filter { it.name == "getLastItem" } - .first { it.parameters.size == 2 } - mockkStatic(getLastItemRef) + val getLastItemRef: (MutableList) -> Any = ContainerUtil::getLastItem + mockkStatic(getLastItemRef as KFunction<*>) every { ContainerUtil.getLastItem(any>()) } answers { val descriptors = firstArg>() if (descriptors.isNotEmpty()) descriptors.last() else null @@ -501,10 +498,8 @@ class EditorTestSpec : WithApplicationShouldSpec({ decoderResultMock } - val commonPrefixLengthRef = StringUtil::class.declaredFunctions - .filter { it.name == "commonPrefixLength" } - .first { it.parameters.size == 2 } - mockkStatic(commonPrefixLengthRef) + val commonPrefixLengthRef: (CharSequence, CharSequence) -> Int = StringUtil::commonPrefixLength + mockkStatic(commonPrefixLengthRef as KFunction<*>) every { StringUtil.commonPrefixLength(any(), any()) } returns text.length val descriptors = lossyEncodingInspection.checkFile(psiFileMock, inspectionManagerMock, isOnTheFly) @@ -620,10 +615,8 @@ class EditorTestSpec : WithApplicationShouldSpec({ decoderResultMock } - val commonPrefixLengthRef = StringUtil::class.declaredFunctions - .filter { it.name == "commonPrefixLength" } - .first { it.parameters.size == 2 } - mockkStatic(commonPrefixLengthRef) + val commonPrefixLengthRef: (CharSequence, CharSequence) -> Int = StringUtil::commonPrefixLength + mockkStatic(commonPrefixLengthRef as KFunction<*>) every { StringUtil.commonPrefixLength(any(), any()) } returns text.length - 1 val descriptors = lossyEncodingInspection.checkFile(psiFileMock, inspectionManagerMock, isOnTheFly) diff --git a/src/test/kotlin/org/zowe/explorer/explorer/actions/AllocateDatasetActionTestSpec.kt b/src/test/kotlin/org/zowe/explorer/explorer/actions/AllocateDatasetActionTestSpec.kt index fdfab5554..4068040e7 100644 --- a/src/test/kotlin/org/zowe/explorer/explorer/actions/AllocateDatasetActionTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/explorer/actions/AllocateDatasetActionTestSpec.kt @@ -10,12 +10,24 @@ package org.zowe.explorer.explorer.actions +import com.intellij.notification.Notification +import com.intellij.notification.Notifications +import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.components.ComponentManager import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.showOkNoDialog +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.shouldBe +import io.mockk.Runs +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkAll import org.zowe.explorer.common.ui.StatefulDialog import org.zowe.explorer.common.ui.cleanInvalidateOnExpand import org.zowe.explorer.common.ui.showUntilDone @@ -38,22 +50,10 @@ import org.zowe.explorer.explorer.ui.LibraryNode import org.zowe.explorer.explorer.ui.NodeData import org.zowe.explorer.explorer.ui.getExplorerView import org.zowe.explorer.testutils.WithApplicationShouldSpec -import io.kotest.assertions.assertSoftly -import io.kotest.matchers.shouldBe -import io.mockk.Runs -import io.mockk.clearAllMocks -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.mockkStatic -import io.mockk.unmockkAll -import org.junit.jupiter.api.fail import org.zowe.kotlinsdk.DatasetOrganization import org.zowe.kotlinsdk.DsnameType import java.util.* import javax.swing.Icon -import javax.swing.SwingUtilities import kotlin.reflect.KFunction class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ @@ -72,6 +72,7 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ val dataOpsManagerMock = mockk() val componentManagerMock = mockk() val explorerMock = mockk>() + lateinit var addMaskActionInst: AnAction beforeEach { isCleanInvalidateOnExpandTriggered = false @@ -102,6 +103,15 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ } answers { isCleanInvalidateOnExpandTriggered = true } + + val notifyRef: (Notification) -> Unit = Notifications.Bus::notify + mockkStatic(notifyRef as KFunction<*>) + mockkStatic(Notification::get) + every { Notifications.Bus.notify(any()) } answers { + val notification = firstArg() + every { Notification.get(any()) } returns notification + addMaskActionInst = notification.actions.first { it.templateText == "Add mask" } + } } afterEach { @@ -117,7 +127,6 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ val dsMaskNodeMock = mockk() lateinit var initState: DatasetAllocationParams var isOperationPerformed = false - var isCleanCacheTriggered = false var isUpdateOnConfigCrudableCalled = false var isShowUntilDoneSucceeded = false @@ -146,15 +155,7 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ configCrudable.getByUniqueKey(any(), any()) } returns Optional.of(filesWorkingSetConfigMock) - every { - dsMaskNodeMock.cleanCache(any(), any(), any(), any()) - } answers { - isCleanCacheTriggered = true - val isSendTopic = lastArg() - if (isSendTopic) { - fail("cleanCache should not send topic in this testcase") - } - } + every { dsMaskNodeMock.cleanCache(any(), any(), any(), any()) } returns Unit every { nodeMock.parent } returns dsMaskNodeMock every { nodeMock.hint(FilesWorkingSet::class).unit } returns workingSetMock every { viewMock.mySelectedNodesData } returns selectedNodesData @@ -173,33 +174,16 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ Optional.of(mockk()) } - val showOkNoDialogMock: ( - String, - String, - Project?, - String, - String, - Icon? - ) -> Boolean = ::showOkNoDialog - mockkStatic(showOkNoDialogMock as KFunction<*>) - every { - hint(Boolean::class) - showOkNoDialogMock(any(), any(), any(), any(), any(), any()) - } answers { - true - } - allocateDsActionInst.actionPerformed(anActionEventMock) + addMaskActionInst.actionPerformed(anActionEventMock) - // Pause to wait until all EDT events are finished - SwingUtilities.invokeAndWait { - assertSoftly { isCleanInvalidateOnExpandTriggered shouldBe true } - assertSoftly { isShowUntilDoneSucceeded shouldBe true } - assertSoftly { isOperationPerformed shouldBe true } - assertSoftly { isCleanCacheTriggered shouldBe true } - assertSoftly { isUpdateOnConfigCrudableCalled shouldBe true } - assertSoftly { isThrowableReported shouldBe false } - assertSoftly { initState.errorMessage shouldBe "" } + assertSoftly { + isCleanInvalidateOnExpandTriggered shouldBe true + isShowUntilDoneSucceeded shouldBe true + isOperationPerformed shouldBe true + isUpdateOnConfigCrudableCalled shouldBe true + isThrowableReported shouldBe false + initState.errorMessage shouldBe "" } } should("perform allocate PS dataset action creating a new dataset mask") { @@ -210,7 +194,6 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ val dsMaskNodeMock = mockk() lateinit var initState: DatasetAllocationParams var isOperationPerformed = false - var isCleanCacheTriggered = false var isUpdateOnConfigCrudableCalled = false var isShowUntilDoneSucceeded = false @@ -246,15 +229,7 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ configCrudable.getByUniqueKey(any(), any()) } returns Optional.of(filesWorkingSetConfigMock) - every { - dsMaskNodeMock.cleanCache(any(), any(), any(), any()) - } answers { - isCleanCacheTriggered = true - val isSendTopic = lastArg() - if (isSendTopic) { - fail("cleanCache should not send topic in this testcase") - } - } + every { dsMaskNodeMock.cleanCache(any(), any(), any(), any()) } returns Unit every { nodeMock.parent } returns dsMaskNodeMock every { nodeMock.hint(FilesWorkingSet::class).unit } returns workingSetMock every { viewMock.mySelectedNodesData } returns selectedNodesData @@ -273,34 +248,17 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ Optional.of(mockk()) } - val showOkNoDialogMock: ( - String, - String, - Project?, - String, - String, - Icon? - ) -> Boolean = ::showOkNoDialog - mockkStatic(showOkNoDialogMock as KFunction<*>) - every { - hint(Boolean::class) - showOkNoDialogMock(any(), any(), any(), any(), any(), any()) - } answers { - true - } - allocateDsActionInst.actionPerformed(anActionEventMock) + addMaskActionInst.actionPerformed(anActionEventMock) - // Pause to wait until all EDT events are finished - SwingUtilities.invokeAndWait { - assertSoftly { isCleanInvalidateOnExpandTriggered shouldBe true } - assertSoftly { isShowUntilDoneSucceeded shouldBe true } - assertSoftly { isOperationPerformed shouldBe true } - assertSoftly { isCleanCacheTriggered shouldBe true } - assertSoftly { isUpdateOnConfigCrudableCalled shouldBe true } - assertSoftly { isThrowableReported shouldBe false } - assertSoftly { initState.errorMessage shouldBe "" } - assertSoftly { initState.allocationParameters.directoryBlocks shouldBe null } + assertSoftly { + isCleanInvalidateOnExpandTriggered shouldBe true + isShowUntilDoneSucceeded shouldBe true + isOperationPerformed shouldBe true + isUpdateOnConfigCrudableCalled shouldBe true + isThrowableReported shouldBe false + initState.errorMessage shouldBe "" + initState.allocationParameters.directoryBlocks shouldBe null } } should("perform allocate PO-E dataset action creating a new dataset mask") { @@ -311,7 +269,6 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ val dsMaskNodeMock = mockk() lateinit var initState: DatasetAllocationParams var isOperationPerformed = false - var isCleanCacheTriggered = false var isUpdateOnConfigCrudableCalled = false var isShowUntilDoneSucceeded = false @@ -344,15 +301,7 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ configCrudable.getByUniqueKey(any(), any()) } returns Optional.of(filesWorkingSetConfigMock) - every { - dsMaskNodeMock.cleanCache(any(), any(), any(), any()) - } answers { - isCleanCacheTriggered = true - val isSendTopic = lastArg() - if (isSendTopic) { - fail("cleanCache should not send topic in this testcase") - } - } + every { dsMaskNodeMock.cleanCache(any(), any(), any(), any()) } returns Unit every { nodeMock.parent } returns dsMaskNodeMock every { nodeMock.hint(FilesWorkingSet::class).unit } returns workingSetMock every { viewMock.mySelectedNodesData } returns selectedNodesData @@ -371,35 +320,18 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ Optional.of(mockk()) } - val showOkNoDialogMock: ( - String, - String, - Project?, - String, - String, - Icon? - ) -> Boolean = ::showOkNoDialog - mockkStatic(showOkNoDialogMock as KFunction<*>) - every { - hint(Boolean::class) - showOkNoDialogMock(any(), any(), any(), any(), any(), any()) - } answers { - true - } - allocateDsActionInst.actionPerformed(anActionEventMock) - - // Pause to wait until all EDT events are finished - SwingUtilities.invokeAndWait { - assertSoftly { isCleanInvalidateOnExpandTriggered shouldBe true } - assertSoftly { isShowUntilDoneSucceeded shouldBe true } - assertSoftly { isOperationPerformed shouldBe true } - assertSoftly { isCleanCacheTriggered shouldBe true } - assertSoftly { isUpdateOnConfigCrudableCalled shouldBe true } - assertSoftly { isThrowableReported shouldBe false } - assertSoftly { initState.errorMessage shouldBe "" } - assertSoftly { initState.allocationParameters.datasetOrganization shouldBe DatasetOrganization.PO } - assertSoftly { initState.allocationParameters.dsnType shouldBe DsnameType.LIBRARY } + addMaskActionInst.actionPerformed(anActionEventMock) + + assertSoftly { + isCleanInvalidateOnExpandTriggered shouldBe true + isShowUntilDoneSucceeded shouldBe true + isOperationPerformed shouldBe true + isUpdateOnConfigCrudableCalled shouldBe true + isThrowableReported shouldBe false + initState.errorMessage shouldBe "" + initState.allocationParameters.datasetOrganization shouldBe DatasetOrganization.PO + initState.allocationParameters.dsnType shouldBe DsnameType.LIBRARY } } should("perform allocate dataset action without creating a new dataset mask") { @@ -410,7 +342,6 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ val dsMaskNodeMock = mockk() lateinit var initState: DatasetAllocationParams var isOperationPerformed = false - var isCleanCacheTriggered = false var isUpdateOnConfigCrudableCalled = false var isShowUntilDoneSucceeded = false @@ -439,15 +370,7 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ configCrudable.getByUniqueKey(any(), any()) } returns Optional.of(filesWorkingSetConfigMock) - every { - dsMaskNodeMock.cleanCache(any(), any(), any(), any()) - } answers { - isCleanCacheTriggered = true - val isSendTopic = lastArg() - if (!isSendTopic) { - fail("cleanCache should send topic in this testcase") - } - } + every { dsMaskNodeMock.cleanCache(any(), any(), any(), any()) } returns Unit every { nodeMock.parent } returns dsMaskNodeMock every { nodeMock.hint(FilesWorkingSet::class).unit } returns workingSetMock every { viewMock.mySelectedNodesData } returns selectedNodesData @@ -466,117 +389,18 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ Optional.of(mockk()) } - val showOkNoDialogMock: ( - String, - String, - Project?, - String, - String, - Icon? - ) -> Boolean = ::showOkNoDialog - mockkStatic(showOkNoDialogMock as KFunction<*>) - every { - hint(Boolean::class) - showOkNoDialogMock(any(), any(), any(), any(), any(), any()) - } answers { - false - } - allocateDsActionInst.actionPerformed(anActionEventMock) - // Pause to wait until all EDT events are finished - SwingUtilities.invokeAndWait { - assertSoftly { isCleanInvalidateOnExpandTriggered shouldBe true } - assertSoftly { isShowUntilDoneSucceeded shouldBe true } - assertSoftly { isOperationPerformed shouldBe true } - assertSoftly { isCleanCacheTriggered shouldBe true } - assertSoftly { isUpdateOnConfigCrudableCalled shouldBe false } - assertSoftly { isThrowableReported shouldBe false } - assertSoftly { initState.errorMessage shouldBe "" } + assertSoftly { + isCleanInvalidateOnExpandTriggered shouldBe true + isShowUntilDoneSucceeded shouldBe true + isOperationPerformed shouldBe true + isUpdateOnConfigCrudableCalled shouldBe false + isThrowableReported shouldBe false + initState.errorMessage shouldBe "" } } - should("perform allocate dataset action without refreshing dataset mask as the selected node is a working set") { - val workingSetMock = mockk() - val nodeMock = mockk() - val nodeDataMock = NodeData(nodeMock, null, null) - val selectedNodesData = listOf(nodeDataMock) - lateinit var initState: DatasetAllocationParams - var isOperationPerformed = false - var isUpdateOnConfigCrudableCalled = false - var isShowUntilDoneSucceeded = false - - val showUntilDoneMockk: ( - DatasetAllocationParams, - (DatasetAllocationParams) -> StatefulDialog, - (DatasetAllocationParams) -> Boolean - ) -> DatasetAllocationParams? = ::showUntilDone - mockkStatic(showUntilDoneMockk as KFunction<*>) - every { - hint(DatasetAllocationParams::class) - showUntilDoneMockk( - any(), - any<(DatasetAllocationParams) -> StatefulDialog>(), - any<(DatasetAllocationParams) -> Boolean>() - ) - } answers { - initState = firstArg() - val thirdBlockResult = thirdArg<(DatasetAllocationParams) -> Boolean>() - isShowUntilDoneSucceeded = thirdBlockResult(initState) - initState - } - - mockkObject(configCrudable) - every { - configCrudable.getByUniqueKey(any(), any()) - } returns Optional.of(filesWorkingSetConfigMock) - - every { nodeMock.parent } returns null - every { nodeMock.hint(FilesWorkingSet::class).unit } returns workingSetMock - every { viewMock.mySelectedNodesData } returns selectedNodesData - every { workingSetMock.name } returns "test" - every { workingSetMock.uuid } returns "test" - every { workingSetMock.hint(ConnectionConfig::class).connectionConfig } returns mockk() - every { - dataOpsManagerMock.hint(Boolean::class).performOperation(any>(), any()) - } answers { - isOperationPerformed = true - true - } - every { workingSetMock.explorer } returns explorerMock - every { configCrudable.update(any(), any()) } answers { - isUpdateOnConfigCrudableCalled = true - Optional.of(mockk()) - } - - val showOkNoDialogMock: ( - String, - String, - Project?, - String, - String, - Icon? - ) -> Boolean = ::showOkNoDialog - mockkStatic(showOkNoDialogMock as KFunction<*>) - every { - hint(Boolean::class) - showOkNoDialogMock(any(), any(), any(), any(), any(), any()) - } answers { - true - } - - allocateDsActionInst.actionPerformed(anActionEventMock) - - // Pause to wait until all EDT events are finished - SwingUtilities.invokeAndWait { - assertSoftly { isCleanInvalidateOnExpandTriggered shouldBe false } - assertSoftly { isShowUntilDoneSucceeded shouldBe true } - assertSoftly { isOperationPerformed shouldBe true } - assertSoftly { isUpdateOnConfigCrudableCalled shouldBe true } - assertSoftly { isThrowableReported shouldBe false } - assertSoftly { initState.errorMessage shouldBe "" } - } - } - should("perform allocate dataset action creating new dataset mask without refreshing the existing on as the connection config is not found") { + should("perform allocate dataset action creating new dataset mask without adding as the connection config is not found") { val workingSetMock = mockk() val nodeMock = mockk() val nodeDataMock = NodeData(nodeMock, null, null) @@ -629,32 +453,16 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ Optional.of(mockk()) } - val showOkNoDialogMock: ( - String, - String, - Project?, - String, - String, - Icon? - ) -> Boolean = ::showOkNoDialog - mockkStatic(showOkNoDialogMock as KFunction<*>) - every { - hint(Boolean::class) - showOkNoDialogMock(any(), any(), any(), any(), any(), any()) - } answers { - true - } - allocateDsActionInst.actionPerformed(anActionEventMock) + addMaskActionInst.actionPerformed(anActionEventMock) - // Pause to wait until all EDT events are finished - SwingUtilities.invokeAndWait { - assertSoftly { isCleanInvalidateOnExpandTriggered shouldBe false } - assertSoftly { isShowUntilDoneSucceeded shouldBe true } - assertSoftly { isOperationPerformed shouldBe true } - assertSoftly { isUpdateOnConfigCrudableCalled shouldBe false } - assertSoftly { isThrowableReported shouldBe false } - assertSoftly { initState.errorMessage shouldBe "" } + assertSoftly { + isCleanInvalidateOnExpandTriggered shouldBe false + isShowUntilDoneSucceeded shouldBe true + isOperationPerformed shouldBe true + isUpdateOnConfigCrudableCalled shouldBe false + isThrowableReported shouldBe false + initState.errorMessage shouldBe "" } } should("perform allocate dataset action with failure on operation performing") { @@ -698,18 +506,18 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ allocateDsActionInst.actionPerformed(anActionEventMock) - // Pause to wait until all EDT events are finished - SwingUtilities.invokeAndWait { - assertSoftly { isCleanInvalidateOnExpandTriggered shouldBe false } - assertSoftly { isShowUntilDoneSucceeded shouldBe false } - assertSoftly { isThrowableReported shouldBe true } - assertSoftly { initState.errorMessage shouldBe exceptionMsg } + assertSoftly { + isCleanInvalidateOnExpandTriggered shouldBe false + isShowUntilDoneSucceeded shouldBe false + isThrowableReported shouldBe true + initState.errorMessage shouldBe exceptionMsg } } } context("update") { val presentationMock = mockk() var isPresentationEnabledAndVisible = false + var isPresentationEnabled = false beforeEach { every { @@ -717,6 +525,11 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ } answers { isPresentationEnabledAndVisible = firstArg() } + every { + presentationMock.isEnabled = any() + } answers { + isPresentationEnabled = firstArg() + } every { presentationMock.icon = any() } just Runs every { anActionEventMock.presentation } returns presentationMock } @@ -732,6 +545,7 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ every { viewMock.mySelectedNodesData } returns selectedNodesData every { anActionEventMock.getExplorerView() } returns viewMock + every { nodeMock.unit.connectionConfig } returns mockk() allocateDsActionInst.update(anActionEventMock) @@ -744,6 +558,7 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ every { viewMock.mySelectedNodesData } returns selectedNodesData every { anActionEventMock.getExplorerView() } returns viewMock + every { nodeMock.unit.connectionConfig } returns mockk() allocateDsActionInst.update(anActionEventMock) @@ -756,6 +571,7 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ every { viewMock.mySelectedNodesData } returns selectedNodesData every { anActionEventMock.getExplorerView() } returns viewMock + every { nodeMock.unit.connectionConfig } returns mockk() allocateDsActionInst.update(anActionEventMock) @@ -768,6 +584,7 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ every { viewMock.mySelectedNodesData } returns selectedNodesData every { anActionEventMock.getExplorerView() } returns viewMock + every { nodeMock.unit.connectionConfig } returns mockk() allocateDsActionInst.update(anActionEventMock) @@ -780,6 +597,7 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ every { viewMock.mySelectedNodesData } returns selectedNodesData every { anActionEventMock.getExplorerView() } returns viewMock + every { nodeMock.unit.connectionConfig } returns mockk() allocateDsActionInst.update(anActionEventMock) @@ -793,7 +611,10 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ allocateDsActionInst.update(anActionEventMock) - assertSoftly { isPresentationEnabledAndVisible shouldBe false } + assertSoftly { + isPresentationEnabledAndVisible shouldBe false + isPresentationEnabled shouldBe false + } } should("not show the action on update function is triggered outside the file explorer view") { every { anActionEventMock.getExplorerView() } returns null @@ -802,6 +623,19 @@ class AllocateDatasetActionTestSpec : WithApplicationShouldSpec({ assertSoftly { isPresentationEnabledAndVisible shouldBe false } } + should("not enable the action on update function is triggered without connection config") { + val nodeMock = mockk() + val nodeDataMock = NodeData(nodeMock, null, null) + val selectedNodesData = listOf(nodeDataMock) + + every { viewMock.mySelectedNodesData } returns selectedNodesData + every { anActionEventMock.getExplorerView() } returns viewMock + every { nodeMock.unit.connectionConfig } returns null + + allocateDsActionInst.update(anActionEventMock) + + assertSoftly { isPresentationEnabled shouldBe false } + } } } }) diff --git a/src/test/kotlin/org/zowe/explorer/explorer/actions/AllocateLikeActionTestSpec.kt b/src/test/kotlin/org/zowe/explorer/explorer/actions/AllocateLikeActionTestSpec.kt index b10d4140e..21b9d5531 100644 --- a/src/test/kotlin/org/zowe/explorer/explorer/actions/AllocateLikeActionTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/explorer/actions/AllocateLikeActionTestSpec.kt @@ -10,13 +10,25 @@ package org.zowe.explorer.explorer.actions +import com.intellij.notification.Notification +import com.intellij.notification.Notifications +import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.components.ComponentManager import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages.showWarningDialog -import com.intellij.openapi.ui.showOkNoDialog +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.shouldBe +import io.mockk.Runs +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkAll import org.zowe.explorer.common.ui.StatefulDialog import org.zowe.explorer.common.ui.cleanInvalidateOnExpand import org.zowe.explorer.common.ui.showUntilDone @@ -39,24 +51,12 @@ import org.zowe.explorer.explorer.ui.LibraryNode import org.zowe.explorer.explorer.ui.NodeData import org.zowe.explorer.explorer.ui.getExplorerView import org.zowe.explorer.testutils.WithApplicationShouldSpec -import io.kotest.assertions.assertSoftly -import io.kotest.matchers.shouldBe -import io.mockk.Runs -import io.mockk.clearAllMocks -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.mockkStatic -import io.mockk.unmockkAll -import org.junit.jupiter.api.fail import org.zowe.kotlinsdk.Dataset import org.zowe.kotlinsdk.DatasetOrganization import org.zowe.kotlinsdk.RecordFormat import org.zowe.kotlinsdk.SpaceUnits import java.util.* import javax.swing.Icon -import javax.swing.SwingUtilities import kotlin.reflect.KFunction class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ @@ -74,6 +74,7 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ val dataOpsManagerMock = mockk() val componentManagerMock = mockk() val explorerMock = mockk>() + lateinit var addMaskActionInst: AnAction beforeEach { isCleanInvalidateOnExpandTriggered = false @@ -103,6 +104,15 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ } answers { isCleanInvalidateOnExpandTriggered = true } + + val notifyRef: (Notification) -> Unit = Notifications.Bus::notify + mockkStatic(notifyRef as KFunction<*>) + mockkStatic(Notification::get) + every { Notifications.Bus.notify(any()) } answers { + val notification = firstArg() + every { Notification.get(any()) } returns notification + addMaskActionInst = notification.actions.first { it.templateText == "Add mask" } + } } afterEach { @@ -121,7 +131,6 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ val dsMaskNodeMock = mockk() lateinit var initState: DatasetAllocationParams var isOperationPerformed = false - var isCleanCacheTriggered = false var isShowUntilDoneSucceeded = false every { anActionEventMock.getExplorerView() } returns viewMock @@ -152,15 +161,7 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ configCrudable.getByUniqueKey(any(), any()) } returns Optional.of(filesWorkingSetConfigMock) - every { - dsMaskNodeMock.cleanCache(any(), any(), any(), any()) - } answers { - isCleanCacheTriggered = true - val isSendTopic = lastArg() - if (!isSendTopic) { - fail("cleanCache should send topic in this testcase") - } - } + every { dsMaskNodeMock.cleanCache(any(), any(), any(), any()) } returns Unit every { nodeMock.parent } returns dsMaskNodeMock every { nodeMock.hint(FilesWorkingSet::class).unit } returns workingSetMock every { viewMock.mySelectedNodesData } returns selectedNodesData @@ -175,32 +176,15 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ } every { workingSetMock.explorer } returns explorerMock - val showOkNoDialogMock: ( - String, - String, - Project?, - String, - String, - Icon? - ) -> Boolean = ::showOkNoDialog - mockkStatic(showOkNoDialogMock as KFunction<*>) - every { - hint(Boolean::class) - showOkNoDialogMock(any(), any(), any(), any(), any(), any()) - } answers { - false - } - allocateDsActionInst.actionPerformed(anActionEventMock) - - // Pause to wait until all EDT events are finished - SwingUtilities.invokeAndWait { - assertSoftly { isCleanInvalidateOnExpandTriggered shouldBe true } - assertSoftly { isShowUntilDoneSucceeded shouldBe true } - assertSoftly { isOperationPerformed shouldBe true } - assertSoftly { isCleanCacheTriggered shouldBe true } - assertSoftly { isThrowableReported shouldBe false } - assertSoftly { initState.errorMessage shouldBe "" } + addMaskActionInst.actionPerformed(anActionEventMock) + + assertSoftly { + isCleanInvalidateOnExpandTriggered shouldBe true + isShowUntilDoneSucceeded shouldBe true + isOperationPerformed shouldBe true + isThrowableReported shouldBe false + initState.errorMessage shouldBe "" } } should("perform allocate PDS dataset with TRACKS action without creating a new dataset mask") { @@ -219,7 +203,6 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ val dsMaskNodeMock = mockk() lateinit var initState: DatasetAllocationParams var isOperationPerformed = false - var isCleanCacheTriggered = false var isShowUntilDoneSucceeded = false every { anActionEventMock.getExplorerView() } returns viewMock @@ -250,15 +233,7 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ configCrudable.getByUniqueKey(any(), any()) } returns Optional.of(filesWorkingSetConfigMock) - every { - dsMaskNodeMock.cleanCache(any(), any(), any(), any()) - } answers { - isCleanCacheTriggered = true - val isSendTopic = lastArg() - if (!isSendTopic) { - fail("cleanCache should send topic in this testcase") - } - } + every { dsMaskNodeMock.cleanCache(any(), any(), any(), any()) } returns Unit every { nodeMock.parent } returns dsMaskNodeMock every { nodeMock.hint(FilesWorkingSet::class).unit } returns workingSetMock every { viewMock.mySelectedNodesData } returns selectedNodesData @@ -273,32 +248,14 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ } every { workingSetMock.explorer } returns explorerMock - val showOkNoDialogMock: ( - String, - String, - Project?, - String, - String, - Icon? - ) -> Boolean = ::showOkNoDialog - mockkStatic(showOkNoDialogMock as KFunction<*>) - every { - hint(Boolean::class) - showOkNoDialogMock(any(), any(), any(), any(), any(), any()) - } answers { - false - } - allocateDsActionInst.actionPerformed(anActionEventMock) - // Pause to wait until all EDT events are finished - SwingUtilities.invokeAndWait { - assertSoftly { isCleanInvalidateOnExpandTriggered shouldBe true } - assertSoftly { isShowUntilDoneSucceeded shouldBe true } - assertSoftly { isOperationPerformed shouldBe true } - assertSoftly { isCleanCacheTriggered shouldBe true } - assertSoftly { isThrowableReported shouldBe false } - assertSoftly { initState.errorMessage shouldBe "" } + assertSoftly { + isCleanInvalidateOnExpandTriggered shouldBe true + isShowUntilDoneSucceeded shouldBe true + isOperationPerformed shouldBe true + isThrowableReported shouldBe false + initState.errorMessage shouldBe "" } } should("perform allocate PDS/E dataset with CYLINDERS action without creating a new dataset mask") { @@ -317,7 +274,6 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ val dsMaskNodeMock = mockk() lateinit var initState: DatasetAllocationParams var isOperationPerformed = false - var isCleanCacheTriggered = false var isShowUntilDoneSucceeded = false every { anActionEventMock.getExplorerView() } returns viewMock @@ -348,15 +304,7 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ configCrudable.getByUniqueKey(any(), any()) } returns Optional.of(filesWorkingSetConfigMock) - every { - dsMaskNodeMock.cleanCache(any(), any(), any(), any()) - } answers { - isCleanCacheTriggered = true - val isSendTopic = lastArg() - if (!isSendTopic) { - fail("cleanCache should send topic in this testcase") - } - } + every { dsMaskNodeMock.cleanCache(any(), any(), any(), any()) } returns Unit every { nodeMock.parent } returns dsMaskNodeMock every { nodeMock.hint(FilesWorkingSet::class).unit } returns workingSetMock every { viewMock.mySelectedNodesData } returns selectedNodesData @@ -371,32 +319,14 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ } every { workingSetMock.explorer } returns explorerMock - val showOkNoDialogMock: ( - String, - String, - Project?, - String, - String, - Icon? - ) -> Boolean = ::showOkNoDialog - mockkStatic(showOkNoDialogMock as KFunction<*>) - every { - hint(Boolean::class) - showOkNoDialogMock(any(), any(), any(), any(), any(), any()) - } answers { - false - } - allocateDsActionInst.actionPerformed(anActionEventMock) - // Pause to wait until all EDT events are finished - SwingUtilities.invokeAndWait { - assertSoftly { isCleanInvalidateOnExpandTriggered shouldBe true } - assertSoftly { isShowUntilDoneSucceeded shouldBe true } - assertSoftly { isOperationPerformed shouldBe true } - assertSoftly { isCleanCacheTriggered shouldBe true } - assertSoftly { isThrowableReported shouldBe false } - assertSoftly { initState.errorMessage shouldBe "" } + assertSoftly { + isCleanInvalidateOnExpandTriggered shouldBe true + isShowUntilDoneSucceeded shouldBe true + isOperationPerformed shouldBe true + isThrowableReported shouldBe false + initState.errorMessage shouldBe "" } } should("perform allocate PS dataset with BLOCKS action without creating a new dataset mask, changing BLOCKS to TRACKS") { @@ -410,7 +340,6 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ val dsMaskNodeMock = mockk() lateinit var initState: DatasetAllocationParams var isOperationPerformed = false - var isCleanCacheTriggered = false var isShowUntilDoneSucceeded = false var isBlocksChangedToTracks = false @@ -451,15 +380,7 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ configCrudable.getByUniqueKey(any(), any()) } returns Optional.of(filesWorkingSetConfigMock) - every { - dsMaskNodeMock.cleanCache(any(), any(), any(), any()) - } answers { - isCleanCacheTriggered = true - val isSendTopic = lastArg() - if (!isSendTopic) { - fail("cleanCache should send topic in this testcase") - } - } + every { dsMaskNodeMock.cleanCache(any(), any(), any(), any()) } returns Unit every { nodeMock.parent } returns dsMaskNodeMock every { nodeMock.hint(FilesWorkingSet::class).unit } returns workingSetMock every { viewMock.mySelectedNodesData } returns selectedNodesData @@ -474,33 +395,15 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ } every { workingSetMock.explorer } returns explorerMock - val showOkNoDialogMock: ( - String, - String, - Project?, - String, - String, - Icon? - ) -> Boolean = ::showOkNoDialog - mockkStatic(showOkNoDialogMock as KFunction<*>) - every { - hint(Boolean::class) - showOkNoDialogMock(any(), any(), any(), any(), any(), any()) - } answers { - false - } - allocateDsActionInst.actionPerformed(anActionEventMock) - // Pause to wait until all EDT events are finished - SwingUtilities.invokeAndWait { - assertSoftly { isCleanInvalidateOnExpandTriggered shouldBe true } - assertSoftly { isShowUntilDoneSucceeded shouldBe true } - assertSoftly { isOperationPerformed shouldBe true } - assertSoftly { isCleanCacheTriggered shouldBe true } - assertSoftly { isBlocksChangedToTracks shouldBe true } - assertSoftly { isThrowableReported shouldBe false } - assertSoftly { initState.errorMessage shouldBe "" } + assertSoftly { + isCleanInvalidateOnExpandTriggered shouldBe true + isShowUntilDoneSucceeded shouldBe true + isOperationPerformed shouldBe true + isBlocksChangedToTracks shouldBe true + isThrowableReported shouldBe false + initState.errorMessage shouldBe "" } } should("not perform 'allocate like' action as the file explorer view is not found") { @@ -517,11 +420,10 @@ class AllocateLikeActionTestSpec : WithApplicationShouldSpec({ allocateDsActionInst.actionPerformed(anActionEventMock) - // Pause to wait until all EDT events are finished - SwingUtilities.invokeAndWait { - assertSoftly { isCleanInvalidateOnExpandTriggered shouldBe false } - assertSoftly { isOperationPerformed shouldBe false } - assertSoftly { isThrowableReported shouldBe false } + assertSoftly { + isCleanInvalidateOnExpandTriggered shouldBe false + isOperationPerformed shouldBe false + isThrowableReported shouldBe false } } } diff --git a/src/test/kotlin/org/zowe/explorer/explorer/ui/ChangeEncodingDialogTestSpec.kt b/src/test/kotlin/org/zowe/explorer/explorer/ui/ChangeEncodingDialogTestSpec.kt index 4b695c4ea..587e1b1f3 100644 --- a/src/test/kotlin/org/zowe/explorer/explorer/ui/ChangeEncodingDialogTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/explorer/ui/ChangeEncodingDialogTestSpec.kt @@ -14,6 +14,8 @@ import com.intellij.icons.AllIcons import com.intellij.ide.IdeBundle import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Document +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.Messages import com.intellij.openapi.vfs.VirtualFile @@ -47,7 +49,7 @@ import java.awt.event.ActionEvent import java.nio.charset.Charset import javax.swing.Action import javax.swing.Icon -import kotlin.reflect.full.declaredFunctions +import kotlin.reflect.KFunction class ChangeEncodingDialogTestSpec : WithApplicationShouldSpec({ afterSpec { @@ -58,6 +60,8 @@ class ChangeEncodingDialogTestSpec : WithApplicationShouldSpec({ lateinit var changeEncodingDialog: ChangeEncodingDialog var expectedExitCode = 0 + val projectMock = mockk() + val fileName = "fileName" val virtualFileMock = mockk() every { virtualFileMock.name } returns fileName @@ -91,9 +95,8 @@ class ChangeEncodingDialogTestSpec : WithApplicationShouldSpec({ val actionEventMock = mockk() - val showDialogRef = Messages::class.declaredFunctions - .first { it.name == "showDialog" && it.parameters.size == 5 } - mockkStatic(showDialogRef) + val showDialogRef: (String, String, Array, Int, Icon) -> Int = Messages::showDialog + mockkStatic(showDialogRef as KFunction<*>) every { contentSynchronizerMock.synchronizeWithRemote(any()) } returns Unit @@ -102,6 +105,11 @@ class ChangeEncodingDialogTestSpec : WithApplicationShouldSpec({ mockkStatic(::updateFileTag) every { updateFileTag(attributesMock) } returns Unit + val projectManagerMock = mockk() + + mockkStatic(ProjectManager::getInstance) + every { ProjectManager.getInstance() } returns projectManagerMock + beforeEach { safeToReload = Magic8.ABSOLUTELY safeToConvert = Magic8.ABSOLUTELY @@ -113,8 +121,11 @@ class ChangeEncodingDialogTestSpec : WithApplicationShouldSpec({ } every { attributesMock.isWritable } returns true + every { projectManagerMock.openProjects } returns arrayOf(mockk()) + changeEncodingDialog = spyk( ChangeEncodingDialog( + projectMock, virtualFileMock, attributesMock, charsetMock, @@ -154,6 +165,7 @@ class ChangeEncodingDialogTestSpec : WithApplicationShouldSpec({ changeEncodingDialog = spyk( ChangeEncodingDialog( + projectMock, virtualFileMock, attributesMock, charsetMock, @@ -171,12 +183,27 @@ class ChangeEncodingDialogTestSpec : WithApplicationShouldSpec({ assertSoftly { messageRef.get(changeEncodingDialog) shouldBe expectedMessage } assertSoftly { actions.size shouldBe 2 } } + should("create actions when conversion is disabled") { + every { projectManagerMock.openProjects } returns arrayOf(mockk(), mockk()) + + val actions = createActionsRef.invoke(changeEncodingDialog).castOrNull>() + val actualConvertAction = actions?.get(1) + + val expectedTooltip = message("encoding.convert.button.error.tooltip") + + assertSoftly { + actualConvertAction?.isEnabled shouldBe false + actualConvertAction?.getValue(Action.SHORT_DESCRIPTION) shouldBe expectedTooltip + actions?.size shouldBe 3 + } + } should("create actions when encoding is incompatible") { safeToReload = Magic8.NO_WAY safeToConvert = Magic8.NO_WAY changeEncodingDialog = spyk( ChangeEncodingDialog( + projectMock, virtualFileMock, attributesMock, charsetMock, @@ -224,6 +251,7 @@ class ChangeEncodingDialogTestSpec : WithApplicationShouldSpec({ changeEncodingDialog = spyk( ChangeEncodingDialog( + projectMock, virtualFileMock, attributesMock, charsetMock, @@ -251,6 +279,7 @@ class ChangeEncodingDialogTestSpec : WithApplicationShouldSpec({ changeEncodingDialog = spyk( ChangeEncodingDialog( + projectMock, virtualFileMock, attributesMock, charsetMock, @@ -282,6 +311,7 @@ class ChangeEncodingDialogTestSpec : WithApplicationShouldSpec({ changeEncodingDialog = spyk( ChangeEncodingDialog( + projectMock, virtualFileMock, attributesMock, charsetMock, @@ -317,6 +347,7 @@ class ChangeEncodingDialogTestSpec : WithApplicationShouldSpec({ changeEncodingDialog = spyk( ChangeEncodingDialog( + projectMock, virtualFileMock, attributesMock, charsetMock, @@ -348,6 +379,7 @@ class ChangeEncodingDialogTestSpec : WithApplicationShouldSpec({ changeEncodingDialog = spyk( ChangeEncodingDialog( + projectMock, virtualFileMock, attributesMock, charsetMock, diff --git a/src/test/kotlin/org/zowe/explorer/explorer/ui/ExplorerPasteProviderTestSpec.kt b/src/test/kotlin/org/zowe/explorer/explorer/ui/ExplorerPasteProviderTestSpec.kt index 9f2b1c50b..d13cdc593 100644 --- a/src/test/kotlin/org/zowe/explorer/explorer/ui/ExplorerPasteProviderTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/explorer/ui/ExplorerPasteProviderTestSpec.kt @@ -13,7 +13,7 @@ package org.zowe.explorer.explorer.ui import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper @@ -30,6 +30,9 @@ import org.zowe.explorer.dataops.attributes.RemoteDatasetAttributes import org.zowe.explorer.dataops.attributes.RemoteMemberAttributes import org.zowe.explorer.dataops.attributes.RemoteUssAttributes import org.zowe.explorer.dataops.operations.mover.MoveCopyOperation +import org.zowe.explorer.dataops.operations.mover.names.DatasetOrDirResolver +import org.zowe.explorer.dataops.operations.mover.names.DefaultNameResolver +import org.zowe.explorer.dataops.operations.mover.names.SeqToPDSResolver import org.zowe.explorer.explorer.AbstractExplorerBase import org.zowe.explorer.explorer.FileExplorer import org.zowe.explorer.explorer.FileExplorerContentProvider @@ -37,7 +40,6 @@ import org.zowe.explorer.explorer.FilesWorkingSet import org.zowe.explorer.testutils.WithApplicationShouldSpec import org.zowe.explorer.testutils.testServiceImpl.TestDataOpsManagerImpl import org.zowe.explorer.utils.castOrNull -import org.zowe.explorer.utils.service import org.zowe.explorer.vfs.MFVirtualFile import io.kotest.assertions.assertSoftly import io.kotest.matchers.shouldBe @@ -73,9 +75,7 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ ExplorerPasteProvider(), recordPrivateCalls = true ) - var dataOpsManagerService = - ApplicationManager.getApplication().service() as TestDataOpsManagerImpl - every { mockedFileExplorer.componentManager } returns ApplicationManager.getApplication() + var dataOpsManagerService = service() as TestDataOpsManagerImpl dataOpsManagerService.testInstance = object : TestDataOpsManagerImpl(mockedFileExplorer.componentManager) { override fun tryToGetAttributes(file: VirtualFile): FileAttributes { return mockk() @@ -127,6 +127,10 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ val nodeToRefreshSource = mockk() val nodeToRefreshTarget = mockk() beforeEach { + every { + dataOpsManagerService.testInstance.getNameResolver(any() as VirtualFile, any() as VirtualFile) + } returns DefaultNameResolver() + // we do not need to refresh the nodes in below tests, so lets return default list for each node to refresh (USS for example) every { mockedFileExplorerView.myFsTreeStructure } returns mockk() every { mockedFileExplorerView.myStructure } returns mockk() @@ -292,6 +296,7 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ every { mockedSourceAttributes.isDirectory } returns false every { mockedSourceNodeData.node } returns mockedSourceNode every { mockedSourceNodeData.file } returns mockedSourceFile + every { mockedSourceFile.isInLocalFileSystem } returns false every { mockedSourceNodeData.attributes } returns mockedSourceAttributes // children of target @@ -387,6 +392,7 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ val mockedSourceFile = mockk() val mockedSourceAttributes = mockk() every { mockedSourceFile.name } returns "TEST.FILE" + every { mockedSourceFile.isInLocalFileSystem } returns false every { mockedSourceAttributes.name } returns "TEST.FILE" every { mockedSourceAttributes.isPastePossible } returns false every { mockedSourceAttributes.isDirectory } returns false @@ -991,8 +997,7 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ true } - dataOpsManagerService = ApplicationManager.getApplication().service() as TestDataOpsManagerImpl - every { mockedFileExplorer.componentManager } returns ApplicationManager.getApplication() + dataOpsManagerService = service() as TestDataOpsManagerImpl dataOpsManagerService.testInstance = object : TestDataOpsManagerImpl(mockedFileExplorer.componentManager) { override fun performOperation(operation: Operation, progressIndicator: ProgressIndicator): R { throw IllegalStateException("Test Error") @@ -1041,6 +1046,9 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ every { dataOpsManagerService.testInstance.tryToGetAttributes(childDestinationVirtualFile) } returns childDestFileAttributes every { dataOpsManagerService.testInstance.tryToGetAttributes(mockedSourceFile) } returns mockedSourceAttributes every { dataOpsManagerService.testInstance.tryToGetAttributes(mockedTargetFile) } returns targetAttributes + every { + dataOpsManagerService.testInstance.getNameResolver(any() as VirtualFile, any() as VirtualFile) + } returns DefaultNameResolver() every { mockedDataContext.getData(IS_DRAG_AND_DROP_KEY) } returns true every { mockedDataContext.getData(CommonDataKeys.PROJECT) } returns mockedProject @@ -1241,6 +1249,7 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ every { mockedSourceFile.name } returns fileName every { mockedSourceFile.isDirectory } returns isDirectory every { mockedSourceFile.parent } returns parent + every { mockedSourceFile.isInLocalFileSystem } returns false val mockedSourceAttributes = if (sourceAttributes != null) { sourceAttributes @@ -1298,10 +1307,14 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ } answers { copyPasteNodeDataList.mapNotNull { nodeData -> nodeData.file?.let { Pair(mockedTargetFile, it) } } } should("Skip 2 files one by one") { + addMockedSourceFile("file.txt") addMockedSourceFile("file1.txt") addMockedTargetChildFile("file.txt") addMockedTargetChildFile("file1.txt") + every { + dataOpsManagerService.testInstance.getNameResolver(any() as VirtualFile, any() as VirtualFile) + } returns DefaultNameResolver() mockkStatic(Messages::class) var decideOptionSelected = false var skipNumber = 0 @@ -1341,6 +1354,10 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ addMockedSourceFile("file1.txt") addMockedTargetChildFile("file.txt") addMockedTargetChildFile("file1.txt") + every { + dataOpsManagerService.testInstance.getNameResolver(any() as VirtualFile, any() as VirtualFile) + } returns DefaultNameResolver() + mockkStatic(Messages::class) var decideOptionSelected = false var overwriteSelected = false @@ -1403,6 +1420,10 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ addMockedTargetChildFile("file.txt") addMockedTargetChildFile("file_(1).txt") addMockedTargetChildFile("file1.txt") + every { + dataOpsManagerService.testInstance.getNameResolver(any() as VirtualFile, any() as VirtualFile) + } returns DefaultNameResolver() + mockkStatic(Messages::class) var decideOptionSelected = false var overwriteSelected = false @@ -1468,6 +1489,9 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ addMockedSourceFile("DATASET.TEST", sourceAttributes = datasetAttributes) addMockedTargetChildFile("directory.test", true) addMockedTargetChildFile("DATASET.TEST") + every { + dataOpsManagerService.testInstance.getNameResolver(any() as VirtualFile, any() as VirtualFile) + } returns DatasetOrDirResolver(dataOpsManagerService) mockkStatic(Messages::class) var decideOptionSelected = false @@ -1527,6 +1551,9 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ addMockedSourceFile("DATASET.TEST", sourceAttributes = datasetAttributes) addMockedTargetChildFile("TEST") + every { + dataOpsManagerService.testInstance.getNameResolver(any() as VirtualFile, any() as VirtualFile) + } returns SeqToPDSResolver(dataOpsManagerService) every { dataOpsManagerService.testInstance.tryToGetAttributes(mockedTargetFile) } returns targetAttributesPDS mockkStatic(Messages::class) @@ -1576,6 +1603,10 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ addMockedSourceFile("dir2", isPastePossible = true, isDirectory = true) addMockedTargetChildFile("dir1", true) addMockedTargetChildFile("dir2") + every { + dataOpsManagerService.testInstance.getNameResolver(any() as VirtualFile, any() as VirtualFile) + } returns DefaultNameResolver() + mockkStatic(Messages::class) var decideOptionSelected = false @@ -1629,6 +1660,7 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ isPastePerformed = false val sourceFile = addMockedSourceFile("file.txt", parent = mockedTargetFile) destinationChildFiles.add(sourceFile) + mockkStatic(Messages::class) var skipSelected = false @@ -1725,6 +1757,9 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ destinationChildFiles.add(sourceFile1) val sourceFile2 = addMockedSourceFile("file2.txt", parent = mockedTargetFile) destinationChildFiles.add(sourceFile2) + every { + dataOpsManagerService.testInstance.getNameResolver(any() as VirtualFile, any() as VirtualFile) + } returns DefaultNameResolver() mockkStatic(Messages::class) var decideEachSelected = false diff --git a/src/test/kotlin/org/zowe/explorer/explorer/ui/ExplorerTreeViewTestSpec.kt b/src/test/kotlin/org/zowe/explorer/explorer/ui/ExplorerTreeViewTestSpec.kt new file mode 100644 index 000000000..f15a36ee4 --- /dev/null +++ b/src/test/kotlin/org/zowe/explorer/explorer/ui/ExplorerTreeViewTestSpec.kt @@ -0,0 +1,104 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ + +package org.zowe.explorer.explorer.ui + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import org.zowe.explorer.config.connect.ConnectionConfig +import org.zowe.explorer.dataops.DataOpsManager +import org.zowe.explorer.dataops.content.synchronizer.ContentSynchronizer +import org.zowe.explorer.explorer.* +import org.zowe.explorer.testutils.WithApplicationShouldSpec +import org.zowe.explorer.testutils.testServiceImpl.TestDataOpsManagerImpl +import org.zowe.explorer.utils.service +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.shouldBe +import io.mockk.* +import kotlin.reflect.KFunction + +class ExplorerTreeViewTestSpec: WithApplicationShouldSpec({ + afterSpec { + clearAllMocks() + } + context("Explorer module: ui/ExplorerTreeView") { + + lateinit var fileExplorerView: ExplorerTreeView<*, *, *> + + val explorerMock = mockk>() + every { explorerMock.componentManager } returns ApplicationManager.getApplication() + + val openFilesMock = arrayOf(mockk(), mockk()) + var closedFileSize = 0 + + val dataOpsManagerService = + ApplicationManager.getApplication().service() as TestDataOpsManagerImpl + + val contentSynchronizerMock = mockk() + every { contentSynchronizerMock.markAsNotNeededForSync(any()) } returns Unit + + beforeEach { + mockkConstructor(CommonExplorerTreeStructure::class) + every { anyConstructed>().rootElement } returns Unit + + fileExplorerView = spyk( + FileExplorerView( + explorerMock, + mockk(), + mockk(), + mockk(), + { _, _, _ -> mockk() } + ) { } + ) + + closedFileSize = 0 + mockkStatic(FileEditorManager::getInstance) + every { FileEditorManager.getInstance(any()) } returns object : TestFileEditorManager() { + override fun getOpenFiles(): Array { + return openFilesMock + } + + override fun closeFile(file: VirtualFile) { + closedFileSize++ + } + } + + val isAncestorRef: (VirtualFile, VirtualFile, Boolean) -> Boolean = VfsUtilCore::isAncestor + mockkStatic(isAncestorRef as KFunction<*>) + every { VfsUtilCore.isAncestor(any(), any(), any()) } returns true + + dataOpsManagerService.testInstance = object : TestDataOpsManagerImpl(explorerMock.componentManager) { + override fun getContentSynchronizer(file: VirtualFile): ContentSynchronizer { + return contentSynchronizerMock + } + } + } + + afterEach { + unmockkAll() + } + + // closeChildrenInEditor + should("close files in editor if selected file is their ancestor") { + fileExplorerView.closeChildrenInEditor(mockk()) + + assertSoftly { closedFileSize shouldBe openFilesMock.size } + } + should("don't close files in editor if selected file is not their ancestor") { + every { VfsUtilCore.isAncestor(any(), any(), any()) } returns false + + fileExplorerView.closeChildrenInEditor(mockk()) + + assertSoftly { closedFileSize shouldBe 0 } + } + } +}) diff --git a/src/test/kotlin/org/zowe/explorer/explorer/ui/UssFileNodeTestSpec.kt b/src/test/kotlin/org/zowe/explorer/explorer/ui/UssFileNodeTestSpec.kt index 35724993c..79127c739 100644 --- a/src/test/kotlin/org/zowe/explorer/explorer/ui/UssFileNodeTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/explorer/ui/UssFileNodeTestSpec.kt @@ -401,7 +401,6 @@ class UssFileNodeTestSpec : WithApplicationShouldSpec({ context("ExplorerTreeNode.updateNodeTitleUsingCutBuffer") { every { virtualFileMock.presentableName } returns "test" every { virtualFileMock.isValid } returns false - every { explorer.nullableProject } returns mockedProject should("perform an update of the node if virtual file in the cut buffer and navigate is true") { every { explorerContentProviderMock.isFileInCutBuffer(virtualFileMock) } returns true @@ -455,7 +454,6 @@ class UssFileNodeTestSpec : WithApplicationShouldSpec({ updatePerformed = true true } - every { explorerToTest.nullableProject } returns mockedProject every { ussFileMockToSpyTest.virtualFile } returns virtualFileMock every { ussFileMockToSpyTest.value } returns virtualFileMock ussFileMockToSpyTest.update() diff --git a/src/test/kotlin/org/zowe/explorer/testutils/testServiceImpl/TestDataOpsManagerImpl.kt b/src/test/kotlin/org/zowe/explorer/testutils/testServiceImpl/TestDataOpsManagerImpl.kt index 3c828c42f..16b218f20 100644 --- a/src/test/kotlin/org/zowe/explorer/testutils/testServiceImpl/TestDataOpsManagerImpl.kt +++ b/src/test/kotlin/org/zowe/explorer/testutils/testServiceImpl/TestDataOpsManagerImpl.kt @@ -25,6 +25,7 @@ import org.zowe.explorer.dataops.fetch.FileFetchProvider import org.zowe.explorer.dataops.log.LogFetcher import org.zowe.explorer.dataops.log.MFLogger import org.zowe.explorer.dataops.log.MFProcessInfo +import org.zowe.explorer.dataops.operations.mover.names.CopyPasteNameResolver import io.mockk.every import io.mockk.mockk @@ -73,6 +74,10 @@ open class TestDataOpsManagerImpl(override val componentManager: ComponentManage TODO("Not yet implemented") } + override fun getNameResolver(source: VirtualFile, destination: VirtualFile): CopyPasteNameResolver { + TODO("Not yet implemented") + } + override fun isOperationSupported(operation: Operation<*>): Boolean { TODO("Not yet implemented") } @@ -132,6 +137,10 @@ open class TestDataOpsManagerImpl(override val componentManager: ComponentManage return this.testInstance.getMFContentAdapter(file) } + override fun getNameResolver(source: VirtualFile, destination: VirtualFile): CopyPasteNameResolver { + return this.testInstance.getNameResolver(source, destination) + } + override fun isOperationSupported(operation: Operation<*>): Boolean { return this.testInstance.isOperationSupported(operation) } diff --git a/src/test/kotlin/org/zowe/explorer/utils/EncodingUtilsTestSpec.kt b/src/test/kotlin/org/zowe/explorer/utils/EncodingUtilsTestSpec.kt index 7b6ed5291..dd0a918de 100644 --- a/src/test/kotlin/org/zowe/explorer/utils/EncodingUtilsTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/utils/EncodingUtilsTestSpec.kt @@ -19,7 +19,6 @@ import com.intellij.codeInspection.ex.InspectionToolWrapper import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Document import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.fileEditor.impl.LoadTextUtil import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages import com.intellij.openapi.vfs.VirtualFile @@ -53,7 +52,7 @@ import java.nio.charset.CharsetDecoder import java.nio.charset.CharsetEncoder import java.nio.charset.UnsupportedCharsetException import javax.swing.Icon -import kotlin.reflect.full.declaredFunctions +import kotlin.reflect.KFunction class EncodingUtilsTestSpec : WithApplicationShouldSpec({ context("utils module: encodingUtils") { @@ -80,13 +79,17 @@ class EncodingUtilsTestSpec : WithApplicationShouldSpec({ every { virtualFileMock.name } returns "fileName" every { virtualFileMock.charset = charsetMock } returns Unit every { virtualFileMock.charset } returns charsetMock + every { virtualFileMock.getOutputStream(null) } returns mockk { + every { close() } returns Unit + every { write(any()) } returns Unit + } mockkConstructor(DocumentedSyncProvider::class) every { anyConstructed().saveDocument() } returns Unit - - val documentMockk = mockk() + every { anyConstructed().loadNewContent(any()) } returns Unit every { anyConstructed().retrieveCurrentContent() } returns bytes + val documentMockk = mockk() every { documentMockk.text } returns text every { documentMockk.modificationStamp } returns 0L @@ -100,10 +103,6 @@ class EncodingUtilsTestSpec : WithApplicationShouldSpec({ } } - mockkStatic(LoadTextUtil::class) - every { LoadTextUtil.write(any(), virtualFileMock, virtualFileMock, any(), any()) } returns Unit - every { LoadTextUtil.getTextByBinaryPresentation(bytes, virtualFileMock) } returns text - val decoderMock = mockk() every { charsetMock.newDecoder() } returns decoderMock @@ -140,9 +139,8 @@ class EncodingUtilsTestSpec : WithApplicationShouldSpec({ mockkStatic(InspectionEngine::runInspectionOnFile) - val showDialogRef = Messages::class.declaredFunctions - .first { it.name == "showDialog" && it.parameters.size == 5 } - mockkStatic(showDialogRef) + val showDialogRef: (String, String, Array, Int, Icon) -> Int = Messages::showDialog + mockkStatic(showDialogRef as KFunction<*>) beforeEach { dataOpsManagerService.testInstance = object : TestDataOpsManagerImpl(explorerMock.componentManager) { @@ -186,9 +184,9 @@ class EncodingUtilsTestSpec : WithApplicationShouldSpec({ assertSoftly { isEncodingSet shouldBe true } } - // changeFileEncodingTo + // changeEncodingTo should("change file encoding to new one") { - changeFileEncodingTo(virtualFileMock, charsetMock) + changeEncodingTo(virtualFileMock, charsetMock) assertSoftly { isEncodingSet shouldBe true } } @@ -294,7 +292,7 @@ class EncodingUtilsTestSpec : WithApplicationShouldSpec({ every { anyConstructed().exitCode } returns ChangeEncodingDialog.RELOAD_EXIT_CODE - val actual = changeFileEncodingAction(virtualFileMock, attributesMock, charsetMock) + val actual = changeFileEncodingAction(projectMock, virtualFileMock, attributesMock, charsetMock) assertSoftly { actual shouldBe true } } @@ -304,7 +302,7 @@ class EncodingUtilsTestSpec : WithApplicationShouldSpec({ every { anyConstructed().exitCode } returns ChangeEncodingDialog.CONVERT_EXIT_CODE - val actual = changeFileEncodingAction(virtualFileMock, attributesMock, charsetMock) + val actual = changeFileEncodingAction(projectMock, virtualFileMock, attributesMock, charsetMock) assertSoftly { actual shouldBe true } } @@ -314,7 +312,7 @@ class EncodingUtilsTestSpec : WithApplicationShouldSpec({ every { anyConstructed().exitCode } returns 1 - val actual = changeFileEncodingAction(virtualFileMock, attributesMock, charsetMock) + val actual = changeFileEncodingAction(projectMock, virtualFileMock, attributesMock, charsetMock) assertSoftly { actual shouldBe false } }