From c47b919a21e0db3d5f8b3916b30ddfbba543896a Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Tue, 26 Mar 2024 12:24:35 +0300 Subject: [PATCH 01/14] refactor settings into base class --- .../org/move/cli/MoveProjectsService.kt | 19 +- .../CommandConfigurationProducerBase.kt | 6 +- .../TestCommandConfigurationProducerBase.kt | 15 +- .../sui/SuiCommandConfiguration.kt | 2 +- .../settings/MoveProjectSettingsService.kt | 199 ------------------ .../cli/settings/MvProjectSettingsService.kt | 117 ++++++++++ .../settings/MvProjectSettingsServiceBase.kt | 89 ++++++++ .../settings/PerProjectMoveConfigurable.kt | 158 ++++++++------ .../org/move/cli/settings/aptos/AptosExec.kt | 4 +- .../cli/settings/sui/ChooseSuiCliPanel.kt | 6 +- .../org/move/ide/folding/MvFoldingBuilder.kt | 4 +- .../InvalidBlockchainCliConfiguration.kt | 2 +- .../MvEditorNotificationProvider.kt | 7 +- ...MoveProjectDetectedNotificationProvider.kt | 4 +- .../org/move/utils/tests/MvProjectTestBase.kt | 10 +- .../kotlin/org/move/utils/tests/MvTestBase.kt | 10 +- .../resources/META-INF/intellij-move-core.xml | 4 +- 17 files changed, 340 insertions(+), 316 deletions(-) delete mode 100644 src/main/kotlin/org/move/cli/settings/MoveProjectSettingsService.kt create mode 100644 src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt create mode 100644 src/main/kotlin/org/move/cli/settings/MvProjectSettingsServiceBase.kt diff --git a/src/main/kotlin/org/move/cli/MoveProjectsService.kt b/src/main/kotlin/org/move/cli/MoveProjectsService.kt index 444de64df..d8286fbe3 100644 --- a/src/main/kotlin/org/move/cli/MoveProjectsService.kt +++ b/src/main/kotlin/org/move/cli/MoveProjectsService.kt @@ -24,9 +24,8 @@ import com.intellij.psi.PsiFile import com.intellij.psi.PsiInvalidElementAccessException import com.intellij.psi.util.parents import com.intellij.util.messages.Topic -import org.move.cli.settings.MoveProjectSettingsService -import org.move.cli.settings.MoveSettingsChangedEvent -import org.move.cli.settings.MoveSettingsListener +import org.move.cli.settings.MvProjectSettingsServiceBase.* +import org.move.cli.settings.MvProjectSettingsServiceBase.Companion.MOVE_SETTINGS_TOPIC import org.move.cli.settings.debugErrorOrFallback import org.move.lang.core.psi.ext.elementType import org.move.lang.toNioPathOrNull @@ -62,12 +61,14 @@ class MoveProjectsService(val project: Project): Disposable { scheduleProjectsRefresh("Move.toml changed") }) } - subscribe(MoveProjectSettingsService.MOVE_SETTINGS_TOPIC, object: MoveSettingsListener { - override fun moveSettingsChanged(e: MoveSettingsChangedEvent) { - // on every Move Language plugin settings change - scheduleProjectsRefresh("plugin settings changed") - } - }) + subscribe( + MOVE_SETTINGS_TOPIC, + object: MoveSettingsListener { + override fun > settingsChanged(e: SettingsChangedEventBase) { + // on every Move Language plugin settings change + scheduleProjectsRefresh("plugin settings changed") + } + }) } } diff --git a/src/main/kotlin/org/move/cli/runConfigurations/producers/CommandConfigurationProducerBase.kt b/src/main/kotlin/org/move/cli/runConfigurations/producers/CommandConfigurationProducerBase.kt index af3819351..1a605d77a 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/producers/CommandConfigurationProducerBase.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/producers/CommandConfigurationProducerBase.kt @@ -14,7 +14,7 @@ abstract class CommandConfigurationProducerBase(val blockchain: Blockchain): fun configFromLocation(location: PsiElement, climbUp: Boolean = true): CommandLineArgsFromContext? { val project = location.project - if (project.moveSettings.state.blockchain != blockchain) { + if (project.moveSettings.blockchain != blockchain) { return null } return fromLocation(location, climbUp) @@ -36,7 +36,7 @@ abstract class CommandConfigurationProducerBase(val blockchain: Blockchain): var envVars = commandLine.environmentVariables if (blockchain == Blockchain.APTOS - && context.project.moveSettings.state.disableTelemetry + && context.project.moveSettings.disableTelemetry ) { envVars = envVars.with(mapOf("APTOS_DISABLE_TELEMETRY" to "true")) } @@ -49,7 +49,7 @@ abstract class CommandConfigurationProducerBase(val blockchain: Blockchain): context: ConfigurationContext ): Boolean { val project = context.project - if (project.moveSettings.state.blockchain != blockchain) { + if (project.moveSettings.blockchain != blockchain) { return false } val location = context.psiLocation ?: return false diff --git a/src/main/kotlin/org/move/cli/runConfigurations/producers/TestCommandConfigurationProducerBase.kt b/src/main/kotlin/org/move/cli/runConfigurations/producers/TestCommandConfigurationProducerBase.kt index 47423cd05..da81961b8 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/producers/TestCommandConfigurationProducerBase.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/producers/TestCommandConfigurationProducerBase.kt @@ -6,8 +6,7 @@ import com.intellij.psi.PsiFileSystemItem import org.move.cli.MoveProject import org.move.cli.runConfigurations.CliCommandLineArgs import org.move.cli.settings.Blockchain -import org.move.cli.settings.dumpStateOnTestFailure -import org.move.cli.settings.skipFetchLatestGitDeps +import org.move.cli.settings.moveSettings import org.move.lang.MoveFile import org.move.lang.core.psi.MvFunction import org.move.lang.core.psi.MvModule @@ -59,10 +58,10 @@ abstract class TestCommandConfigurationProducerBase(blockchain: Blockchain): Blockchain.APTOS -> subCommand += " --filter $modName::$functionName" Blockchain.SUI -> subCommand += " $modName::$functionName" } - if (psi.project.skipFetchLatestGitDeps) { + if (psi.project.moveSettings.skipFetchLatestGitDeps) { subCommand += " --skip-fetch-latest-git-deps" } - if (blockchain == Blockchain.APTOS && psi.project.dumpStateOnTestFailure) { + if (blockchain == Blockchain.APTOS && psi.project.moveSettings.dumpStateOnTestFailure) { subCommand += " --dump" } @@ -87,10 +86,10 @@ abstract class TestCommandConfigurationProducerBase(blockchain: Blockchain): Blockchain.APTOS -> subCommand += " --filter $modName" Blockchain.SUI -> subCommand += " $modName" } - if (psi.project.skipFetchLatestGitDeps) { + if (psi.project.moveSettings.skipFetchLatestGitDeps) { subCommand += " --skip-fetch-latest-git-deps" } - if (blockchain == Blockchain.APTOS && psi.project.dumpStateOnTestFailure) { + if (blockchain == Blockchain.APTOS && psi.project.moveSettings.dumpStateOnTestFailure) { subCommand += " --dump" } @@ -112,10 +111,10 @@ abstract class TestCommandConfigurationProducerBase(blockchain: Blockchain): val confName = "Test $packageName" var subCommand = "move test" - if (location.project.skipFetchLatestGitDeps) { + if (location.project.moveSettings.skipFetchLatestGitDeps) { subCommand += " --skip-fetch-latest-git-deps" } - if (location.project.dumpStateOnTestFailure) { + if (location.project.moveSettings.dumpStateOnTestFailure) { subCommand += " --dump" } diff --git a/src/main/kotlin/org/move/cli/runConfigurations/sui/SuiCommandConfiguration.kt b/src/main/kotlin/org/move/cli/runConfigurations/sui/SuiCommandConfiguration.kt index 465c159d9..091a8c3d3 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/sui/SuiCommandConfiguration.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/sui/SuiCommandConfiguration.kt @@ -23,7 +23,7 @@ class SuiCommandConfiguration( } override fun getCliPath(project: Project): Path? { - return project.moveSettings.state.suiPath + return project.moveSettings.suiPath .takeIf { it.isNotBlank() } ?.toPathOrNull() } diff --git a/src/main/kotlin/org/move/cli/settings/MoveProjectSettingsService.kt b/src/main/kotlin/org/move/cli/settings/MoveProjectSettingsService.kt deleted file mode 100644 index 6964880bf..000000000 --- a/src/main/kotlin/org/move/cli/settings/MoveProjectSettingsService.kt +++ /dev/null @@ -1,199 +0,0 @@ -package org.move.cli.settings - -import com.intellij.configurationStore.serializeObjectInto -import com.intellij.openapi.Disposable -import com.intellij.openapi.components.* -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer -import com.intellij.psi.PsiManager -import com.intellij.util.messages.Topic -import com.intellij.util.xmlb.XmlSerializer -import org.jdom.Element -import org.jetbrains.annotations.TestOnly -import org.move.cli.settings.aptos.AptosExec -import org.move.openapiext.debugInProduction -import org.move.stdext.exists -import org.move.stdext.isExecutableFile -import org.move.stdext.toPathOrNull -import java.nio.file.Path -import kotlin.reflect.KProperty1 -import kotlin.reflect.full.memberProperties - -data class MoveSettingsChangedEvent( - val oldState: MoveProjectSettingsService.State, - val newState: MoveProjectSettingsService.State, -) { - /** Use it like `event.isChanged(State::foo)` to check whether `foo` property is changed or not */ - fun isChanged(prop: KProperty1): Boolean = - prop.get(oldState) != prop.get(newState) -} - -interface MoveSettingsListener { - fun moveSettingsChanged(e: MoveSettingsChangedEvent) -} - -enum class Blockchain { - APTOS, SUI; - - override fun toString(): String = if (this == APTOS) "Aptos" else "Sui" -} - -private const val settingsServiceName: String = "MoveProjectSettingsService_1" - -@Service(Service.Level.PROJECT) -@State( - name = settingsServiceName, - storages = [ - Storage(StoragePathMacros.WORKSPACE_FILE), - Storage("misc.xml", deprecated = true) - ] -) -class MoveProjectSettingsService(private val project: Project): PersistentStateComponent { - - // default values for settings - data class State( - // null not Mac -> Bundled, null and Mac -> Local(""), not null -> Local(value) - var blockchain: Blockchain = Blockchain.APTOS, - var aptosPath: String? = if (AptosExec.isBundledSupportedForThePlatform()) null else "", - var suiPath: String = "", - var foldSpecs: Boolean = false, - var disableTelemetry: Boolean = true, - var debugMode: Boolean = false, - var skipFetchLatestGitDeps: Boolean = false, - var dumpStateOnTestFailure: Boolean = false, - ) { - fun aptosExec(): AptosExec { - val path = aptosPath - return when (path) { - null -> AptosExec.Bundled - else -> AptosExec.LocalPath(path) - } - } - } - - @Volatile - private var _state = State() - - var state: State - get() = _state.copy() - set(newState) { - if (_state != newState) { - val oldState = _state - _state = newState.copy() - notifySettingsChanged(oldState, newState) - } - } - - private fun notifySettingsChanged( - oldState: State, - newState: State, - ) { - val event = MoveSettingsChangedEvent(oldState, newState) - - for (prop in State::class.memberProperties) { - if (event.isChanged(prop)) { - val oldValue = prop.get(oldState) - val newValue = prop.get(newState) - LOG.debugInProduction("SETTINGS updated [${prop.name}: $oldValue -> $newValue]") - } - } - - project.messageBus.syncPublisher(MOVE_SETTINGS_TOPIC).moveSettingsChanged(event) - - if (event.isChanged(State::foldSpecs)) { - PsiManager.getInstance(project).dropPsiCaches() - } - } - - override fun getState(): Element { - val element = Element(settingsServiceName) - serializeObjectInto(_state, element) - return element - } - - override fun loadState(element: Element) { - val rawState = element.clone() - XmlSerializer.deserializeInto(_state, rawState) - } - - /** - * Allows to modify settings. - * After setting change, - */ - fun modify(action: (State) -> Unit) { - val oldState = state.copy() - val newState = state.copy().also(action) - state = newState - - notifySettingsChanged(oldState, newState) -// val event = MoveSettingsChangedEvent(oldState, newState) -// project.messageBus.syncPublisher(MOVE_SETTINGS_TOPIC).moveSettingsChanged(event) - } - - /** - * Allows to modify settings. - * After setting change, - */ - @TestOnly - fun modifyTemporary(parentDisposable: Disposable, action: (State) -> Unit) { - val oldState = state - state = oldState.also(action) - Disposer.register(parentDisposable) { - _state = oldState - } - } - - /** - * Returns current state of the service. - * Note, result is a copy of service state, so you need to set modified state back to apply changes - */ - companion object { - val MOVE_SETTINGS_TOPIC = Topic("move settings changes", MoveSettingsListener::class.java) - - private val LOG = logger() - } -} - -val Project.moveSettings: MoveProjectSettingsService get() = service() - -val Project.collapseSpecs: Boolean get() = this.moveSettings.state.foldSpecs - -val Project.blockchain: Blockchain get() = this.moveSettings.state.blockchain - -val Project.aptosExec: AptosExec get() = this.moveSettings.state.aptosExec() - -val Project.aptosPath: Path? get() = this.aptosExec.toPathOrNull() - -val Project.suiPath: Path? get() = this.moveSettings.state.suiPath.toPathOrNull() - -fun Path?.isValidExecutable(): Boolean { - return this != null - && this.toString().isNotBlank() - && this.exists() - && this.isExecutableFile() -} - -val Project.isDebugModeEnabled: Boolean get() = this.moveSettings.state.debugMode - -fun Project.debugErrorOrFallback(message: String, fallback: T): T { - if (this.isDebugModeEnabled) { - error(message) - } - return fallback -} - -fun Project.debugErrorOrFallback(message: String, cause: Throwable?, fallback: () -> T): T { - if (this.isDebugModeEnabled) { - throw IllegalStateException(message, cause) - } - return fallback() -} - -val Project.skipFetchLatestGitDeps: Boolean - get() = - this.moveSettings.state.skipFetchLatestGitDeps - -val Project.dumpStateOnTestFailure: Boolean - get() = - this.moveSettings.state.dumpStateOnTestFailure diff --git a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt new file mode 100644 index 000000000..cdb966269 --- /dev/null +++ b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt @@ -0,0 +1,117 @@ +package org.move.cli.settings + +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.StoragePathMacros +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiManager +import org.move.cli.settings.MvProjectSettingsService.MoveProjectSettings +import org.move.cli.settings.aptos.AptosExec +import org.move.stdext.exists +import org.move.stdext.isExecutableFile +import org.move.stdext.toPathOrNull +import java.nio.file.Path + +enum class Blockchain { + APTOS, SUI; + + override fun toString(): String = if (this == APTOS) "Aptos" else "Sui" +} + +val Project.moveSettings: MvProjectSettingsService get() = service() + +private const val SERVICE_NAME: String = "MoveProjectSettingsService_1" + +@State( + name = SERVICE_NAME, + storages = [Storage(StoragePathMacros.WORKSPACE_FILE)] +) +class MvProjectSettingsService( + project: Project +): + MvProjectSettingsServiceBase(project, MoveProjectSettings()) { + + val blockchain: Blockchain get() = state.blockchain + val aptosPath: String? get() = state.aptosPath + val suiPath: String get() = state.suiPath + + val disableTelemetry: Boolean get() = state.disableTelemetry + val foldSpecs: Boolean get() = state.foldSpecs + val skipFetchLatestGitDeps: Boolean get() = state.skipFetchLatestGitDeps + val dumpStateOnTestFailure: Boolean get() = state.dumpStateOnTestFailure + + // default values for settings + class MoveProjectSettings( + // null not Mac -> Bundled, null and Mac -> Local(""), not null -> Local(value) + var blockchain: Blockchain = Blockchain.APTOS, + var aptosPath: String? = if (AptosExec.isBundledSupportedForThePlatform()) null else "", + var suiPath: String = "", + var foldSpecs: Boolean = false, + var disableTelemetry: Boolean = true, + var debugMode: Boolean = false, + var skipFetchLatestGitDeps: Boolean = false, + var dumpStateOnTestFailure: Boolean = false, + ): MvProjectSettingsBase() { + fun aptosExec(): AptosExec { + val path = aptosPath + return when (path) { + null -> AptosExec.Bundled + else -> AptosExec.LocalPath(path) + } + } + + override fun copy(): MoveProjectSettings { + val state = MoveProjectSettings() + state.copyFrom(this) + return state + } + } + + override fun notifySettingsChanged(event: SettingsChangedEventBase) { + super.notifySettingsChanged(event) + + if (event.isChanged(MoveProjectSettings::foldSpecs)) { + PsiManager.getInstance(project).dropPsiCaches() + } + } + + override fun createSettingsChangedEvent( + oldEvent: MoveProjectSettings, + newEvent: MoveProjectSettings + ): SettingsChangedEvent = SettingsChangedEvent(oldEvent, newEvent) + + class SettingsChangedEvent( + oldState: MoveProjectSettings, + newState: MoveProjectSettings + ): SettingsChangedEventBase(oldState, newState) +} + +val Project.aptosExec: AptosExec get() = this.moveSettings.state.aptosExec() + +val Project.aptosPath: Path? get() = this.aptosExec.toPathOrNull() + +val Project.suiPath: Path? get() = this.moveSettings.suiPath.toPathOrNull() + +fun Path?.isValidExecutable(): Boolean { + return this != null + && this.toString().isNotBlank() + && this.exists() + && this.isExecutableFile() +} + +val Project.isDebugModeEnabled: Boolean get() = this.moveSettings.state.debugMode + +fun Project.debugErrorOrFallback(message: String, fallback: T): T { + if (this.isDebugModeEnabled) { + error(message) + } + return fallback +} + +fun Project.debugErrorOrFallback(message: String, cause: Throwable?, fallback: () -> T): T { + if (this.isDebugModeEnabled) { + throw IllegalStateException(message, cause) + } + return fallback() +} diff --git a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsServiceBase.kt b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsServiceBase.kt new file mode 100644 index 000000000..bb8aba2af --- /dev/null +++ b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsServiceBase.kt @@ -0,0 +1,89 @@ +package org.move.cli.settings + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.BaseState +import com.intellij.openapi.components.SimplePersistentStateComponent +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.util.messages.Topic +import org.jetbrains.annotations.TestOnly +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.memberProperties +import org.move.cli.settings.MvProjectSettingsServiceBase.MvProjectSettingsBase + +abstract class MvProjectSettingsServiceBase>( + val project: Project, + state: T +) : SimplePersistentStateComponent(state) { + + abstract class MvProjectSettingsBase> : BaseState() { + abstract fun copy(): T + } + + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.PROPERTY) + protected annotation class AffectsMoveProjectsMetadata + + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.PROPERTY) + protected annotation class AffectsHighlighting + + fun modify(modifySettings: (T) -> Unit) { + val oldState = this.state.copy() + // assigns new values to `this.state` + val newState = this.state.also(modifySettings) + + val event = createSettingsChangedEvent(oldState, newState) + notifySettingsChanged(event) + } + + @TestOnly + fun modifyTemporary(parentDisposable: Disposable, modifySettings: (T) -> Unit) { + val oldState = state + loadState(oldState.copy().also(modifySettings)) + Disposer.register(parentDisposable) { + loadState(oldState) + } + } + + companion object { + val MOVE_SETTINGS_TOPIC: Topic = Topic.create( + "move settings changes", + MoveSettingsListener::class.java, +// Topic.BroadcastDirection.TO_PARENT + ) + } + + interface MoveSettingsListener { + fun > settingsChanged(e: SettingsChangedEventBase) + } + + protected abstract fun createSettingsChangedEvent(oldEvent: T, newEvent: T): SettingsChangedEventBase + + protected open fun notifySettingsChanged(event: SettingsChangedEventBase) { + project.messageBus.syncPublisher(MOVE_SETTINGS_TOPIC).settingsChanged(event) + + if (event.affectsHighlighting) { + DaemonCodeAnalyzer.getInstance(project).restart() + } + } + + abstract class SettingsChangedEventBase>(val oldState: T, val newState: T) { + private val moveProjectsMetadataAffectingProps: List> = + oldState.javaClass.kotlin.memberProperties.filter { it.findAnnotation() != null } + + private val highlightingAffectingProps: List> = + oldState.javaClass.kotlin.memberProperties.filter { it.findAnnotation() != null } + + val affectsMoveProjectsMetadata: Boolean + get() = moveProjectsMetadataAffectingProps.any(::isChanged) + + val affectsHighlighting: Boolean + get() = highlightingAffectingProps.any(::isChanged) + + /** Use it like `event.isChanged(State::foo)` to check whether `foo` property is changed or not */ + fun isChanged(prop: KProperty1): Boolean = prop.get(oldState) != prop.get(newState) + } +} diff --git a/src/main/kotlin/org/move/cli/settings/PerProjectMoveConfigurable.kt b/src/main/kotlin/org/move/cli/settings/PerProjectMoveConfigurable.kt index 9e524039f..d8652a51c 100644 --- a/src/main/kotlin/org/move/cli/settings/PerProjectMoveConfigurable.kt +++ b/src/main/kotlin/org/move/cli/settings/PerProjectMoveConfigurable.kt @@ -18,74 +18,90 @@ class PerProjectMoveConfigurable(val project: Project): _id = "org.move.settings" ) { - private val settingsState: MoveProjectSettingsService.State = project.moveSettings.state + private val settingsState: MvProjectSettingsService.MoveProjectSettings = project.moveSettings.state private val chooseAptosCliPanel = ChooseAptosCliPanel(versionUpdateListener = null) private val chooseSuiCliPanel = ChooseSuiCliPanel() - override fun createPanel(): DialogPanel { - return panel { - group { - var aptosRadioButton: Cell? = null - var suiRadioButton: Cell? = null - buttonsGroup("Blockchain") { - row { - aptosRadioButton = radioButton("Aptos") - .bindSelected( - { settingsState.blockchain == Blockchain.APTOS }, - { settingsState.blockchain = Blockchain.APTOS }, - ) - suiRadioButton = radioButton("Sui") - .bindSelected( - { settingsState.blockchain == Blockchain.SUI }, - { settingsState.blockchain = Blockchain.SUI }, - ) - } - } - chooseAptosCliPanel.attachToLayout(this) - .visibleIf(aptosRadioButton!!.selected) - chooseSuiCliPanel.attachToLayout(this) - .visibleIf(suiRadioButton!!.selected) - } - group { - row { - checkBox("Auto-fold specs in opened files") - .bindSelected(settingsState::foldSpecs) - } - row { - checkBox("Disable telemetry for new Run Configurations") - .bindSelected(settingsState::disableTelemetry) - } - row { - checkBox("Enable debug mode") - .bindSelected(settingsState::debugMode) - comment( - "Enables some explicit crashes in the plugin code. Useful for the error reporting." - ) - } - row { - checkBox("Skip fetching latest git dependencies for tests") - .bindSelected(settingsState::skipFetchLatestGitDeps) - comment( - "Adds --skip-fetch-latest-git-deps to the test runs." - ) - } + override fun createPanel(): DialogPanel = panel { + val settings = project.moveSettings + val state = settings.state.copy() + + group { + var aptosRadioButton: Cell? = null + var suiRadioButton: Cell? = null + buttonsGroup("Blockchain") { row { - checkBox("Dump storage to console on test failures") - .bindSelected(settingsState::dumpStateOnTestFailure) - comment( - "Adds --dump to the test runs (aptos only)." - ) + aptosRadioButton = radioButton("Aptos") + .bindSelected( + { state.blockchain == Blockchain.APTOS }, + { state.blockchain = Blockchain.APTOS }, + ) + suiRadioButton = radioButton("Sui") + .bindSelected( + { state.blockchain == Blockchain.SUI }, + { state.blockchain = Blockchain.SUI }, + ) } } - if (!project.isDefault) { - row { - link("Set default project settings") { - ProjectManager.getInstance().defaultProject.showSettings() - } -// .visible(true) - .align(AlignX.RIGHT) + chooseAptosCliPanel.attachToLayout(this) + .visibleIf(aptosRadioButton!!.selected) + chooseSuiCliPanel.attachToLayout(this) + .visibleIf(suiRadioButton!!.selected) + } + group { + row { + checkBox("Auto-fold specs in opened files") + .bindSelected(state::foldSpecs) + } + row { + checkBox("Disable telemetry for new Run Configurations") + .bindSelected(state::disableTelemetry) + } + row { + checkBox("Enable debug mode") + .bindSelected(state::debugMode) + comment( + "Enables some explicit crashes in the plugin code. Useful for the error reporting." + ) + } + row { + checkBox("Skip fetching latest git dependencies for tests") + .bindSelected(state::skipFetchLatestGitDeps) + comment( + "Adds --skip-fetch-latest-git-deps to the test runs." + ) + } + row { + checkBox("Dump storage to console on test failures") + .bindSelected(state::dumpStateOnTestFailure) + comment( + "Adds --dump to the test runs (aptos only)." + ) + } + } + + if (!project.isDefault) { + row { + link("Set default project settings") { + ProjectManager.getInstance().defaultProject.showSettings() } +// .visible(true) + .align(AlignX.RIGHT) + } + } + + onApply { + settings.modify { + it.aptosPath = chooseAptosCliPanel.selectedAptosExec.pathToSettingsFormat() + it.suiPath = chooseSuiCliPanel.getSuiCliPath() + + it.blockchain = state.blockchain + it.foldSpecs = state.foldSpecs + it.disableTelemetry = state.disableTelemetry + it.debugMode = state.debugMode + it.skipFetchLatestGitDeps = state.skipFetchLatestGitDeps + it.dumpStateOnTestFailure = state.dumpStateOnTestFailure } } } @@ -116,13 +132,17 @@ class PerProjectMoveConfigurable(val project: Project): } /// saves values from Swing form back to configurable (OK / Apply) - override fun apply() { - // calls apply() for createPanel().value - super.apply() - project.moveSettings.state = - settingsState.copy( - aptosPath = chooseAptosCliPanel.selectedAptosExec.pathToSettingsFormat(), - suiPath = chooseSuiCliPanel.getSuiCliPath() - ) - } +// override fun apply() { +// // calls apply() for createPanel().value +// super.apply() +// project.moveSettings.modify { +// it.aptosPath = chooseAptosCliPanel.selectedAptosExec.pathToSettingsFormat() +// it.suiPath = chooseSuiCliPanel.getSuiCliPath() +// } +//// project.moveSettings.state = +//// settingsState.copy( +//// aptosPath = chooseAptosCliPanel.selectedAptosExec.pathToSettingsFormat(), +//// suiPath = chooseSuiCliPanel.getSuiCliPath() +//// ) +// } } diff --git a/src/main/kotlin/org/move/cli/settings/aptos/AptosExec.kt b/src/main/kotlin/org/move/cli/settings/aptos/AptosExec.kt index 5f0e2ebc8..a127c29b4 100644 --- a/src/main/kotlin/org/move/cli/settings/aptos/AptosExec.kt +++ b/src/main/kotlin/org/move/cli/settings/aptos/AptosExec.kt @@ -2,7 +2,7 @@ package org.move.cli.settings.aptos import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.util.SystemInfo -import org.move.cli.settings.MoveProjectSettingsService +import org.move.cli.settings.MvProjectSettingsService import org.move.cli.settings.isValidExecutable import org.move.openapiext.PluginPathManager import org.move.stdext.toPathOrNull @@ -36,7 +36,7 @@ sealed class AptosExec { // for default project after dynamic plugin loading. As a result, you can get // `java.lang.IllegalStateException`. So let's handle it manually: val defaultProjectSettings = - ProjectManager.getInstance().defaultProject.getService(MoveProjectSettingsService::class.java) + ProjectManager.getInstance().defaultProject.getService(MvProjectSettingsService::class.java) val defaultProjectAptosExec = defaultProjectSettings?.state?.aptosExec() return defaultProjectAptosExec diff --git a/src/main/kotlin/org/move/cli/settings/sui/ChooseSuiCliPanel.kt b/src/main/kotlin/org/move/cli/settings/sui/ChooseSuiCliPanel.kt index 250254c81..ffcd38cf9 100644 --- a/src/main/kotlin/org/move/cli/settings/sui/ChooseSuiCliPanel.kt +++ b/src/main/kotlin/org/move/cli/settings/sui/ChooseSuiCliPanel.kt @@ -5,7 +5,7 @@ import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.util.Disposer import com.intellij.ui.dsl.builder.* -import org.move.cli.settings.MoveProjectSettingsService +import org.move.cli.settings.MvProjectSettingsService import org.move.cli.settings.VersionLabel import org.move.openapiext.* import org.move.stdext.toPathOrNull @@ -38,8 +38,8 @@ class ChooseSuiCliPanel( val panel = this if (!panel::_suiCliPath.isInitialized) { val defaultProjectSettings = - ProjectManager.getInstance().defaultProject.getService(MoveProjectSettingsService::class.java) - panel._suiCliPath = defaultProjectSettings.state.suiPath + ProjectManager.getInstance().defaultProject.getService(MvProjectSettingsService::class.java) + panel._suiCliPath = defaultProjectSettings.suiPath } val resultRow = with(layout) { group("Sui CLI") { diff --git a/src/main/kotlin/org/move/ide/folding/MvFoldingBuilder.kt b/src/main/kotlin/org/move/ide/folding/MvFoldingBuilder.kt index 663a06828..d77d9e2b3 100644 --- a/src/main/kotlin/org/move/ide/folding/MvFoldingBuilder.kt +++ b/src/main/kotlin/org/move/ide/folding/MvFoldingBuilder.kt @@ -14,7 +14,7 @@ import com.intellij.psi.PsiWhiteSpace import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.elementType import com.intellij.psi.util.nextLeaf -import org.move.cli.settings.collapseSpecs +import org.move.cli.settings.moveSettings import org.move.lang.MoveFile import org.move.lang.MoveParserDefinition.Companion.BLOCK_COMMENT import org.move.lang.MoveParserDefinition.Companion.EOL_DOC_COMMENT @@ -55,7 +55,7 @@ class MvFoldingBuilder : CustomFoldingBuilder(), DumbAware { } override fun isRegionCollapsedByDefault(node: ASTNode): Boolean { - return node.psi.project.collapseSpecs && node.elementType == MODULE_SPEC_BLOCK + return node.psi.project.moveSettings.foldSpecs && node.elementType == MODULE_SPEC_BLOCK || CodeFoldingSettings.getInstance().isDefaultCollapsedNode(node) } diff --git a/src/main/kotlin/org/move/ide/notifications/InvalidBlockchainCliConfiguration.kt b/src/main/kotlin/org/move/ide/notifications/InvalidBlockchainCliConfiguration.kt index 1b19bc874..d497a0a76 100644 --- a/src/main/kotlin/org/move/ide/notifications/InvalidBlockchainCliConfiguration.kt +++ b/src/main/kotlin/org/move/ide/notifications/InvalidBlockchainCliConfiguration.kt @@ -23,7 +23,7 @@ class InvalidBlockchainCliConfiguration(project: Project): MvEditorNotificationP if (!project.isTrusted()) return null if (isNotificationDisabled(file)) return null - val blockchain = project.blockchain + val blockchain = project.moveSettings.blockchain when (blockchain) { Blockchain.APTOS -> { if (project.aptosExec.isValid()) return null diff --git a/src/main/kotlin/org/move/ide/notifications/MvEditorNotificationProvider.kt b/src/main/kotlin/org/move/ide/notifications/MvEditorNotificationProvider.kt index f2626aff8..ef073647d 100644 --- a/src/main/kotlin/org/move/ide/notifications/MvEditorNotificationProvider.kt +++ b/src/main/kotlin/org/move/ide/notifications/MvEditorNotificationProvider.kt @@ -7,8 +7,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.EditorNotificationPanel import com.intellij.ui.EditorNotificationProvider import com.intellij.ui.EditorNotifications -import org.move.cli.settings.MoveSettingsChangedEvent -import org.move.cli.settings.MoveSettingsListener +import org.move.cli.settings.MvProjectSettingsServiceBase.* import java.util.function.Function import javax.swing.JComponent @@ -16,9 +15,9 @@ fun updateAllNotifications(project: Project) { EditorNotifications.getInstance(project).updateAllNotifications() } -class UpdateNotificationsOnSettingsChangeListener(val project: Project) : MoveSettingsListener { +class UpdateNotificationsOnSettingsChangeListener(val project: Project): MoveSettingsListener { - override fun moveSettingsChanged(e: MoveSettingsChangedEvent) { + override fun > settingsChanged(e: SettingsChangedEventBase) { updateAllNotifications(project) } } diff --git a/src/main/kotlin/org/move/ide/notifications/NoMoveProjectDetectedNotificationProvider.kt b/src/main/kotlin/org/move/ide/notifications/NoMoveProjectDetectedNotificationProvider.kt index 77aae3c2c..bcae6a956 100644 --- a/src/main/kotlin/org/move/ide/notifications/NoMoveProjectDetectedNotificationProvider.kt +++ b/src/main/kotlin/org/move/ide/notifications/NoMoveProjectDetectedNotificationProvider.kt @@ -7,7 +7,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.EditorNotificationPanel import org.move.cli.moveProjectsService -import org.move.cli.settings.blockchain +import org.move.cli.settings.moveSettings import org.move.lang.isMoveFile import org.move.lang.isMoveTomlManifestFile import org.move.openapiext.common.isDispatchThread @@ -25,7 +25,7 @@ class NoMoveProjectDetectedNotificationProvider(project: Project): MvEditorNotif @Suppress("UnstableApiUsage") if (!project.isTrusted()) return null - val blockchain = project.blockchain + val blockchain = project.moveSettings.blockchain val moveProjectsService = project.moveProjectsService // HACK: Reloads projects once on an opening of any Move file, if not yet reloaded. // It should be invoked somewhere else where it's more appropriate, diff --git a/src/main/kotlin/org/move/utils/tests/MvProjectTestBase.kt b/src/main/kotlin/org/move/utils/tests/MvProjectTestBase.kt index 618a1c841..a4bb16018 100644 --- a/src/main/kotlin/org/move/utils/tests/MvProjectTestBase.kt +++ b/src/main/kotlin/org/move/utils/tests/MvProjectTestBase.kt @@ -25,15 +25,13 @@ abstract class MvProjectTestBase : CodeInsightFixtureTestCase()?.enabled ?: true val blockchain = this.findAnnotationInstance()?.blockchain ?: Blockchain.APTOS // triggers projects refresh - project.moveSettings.state = settingsState.copy( - debugMode = debugMode, - blockchain = blockchain - ) + project.moveSettings.modify { + it.debugMode = debugMode + it.blockchain = blockchain + } } override fun tearDown() { diff --git a/src/main/kotlin/org/move/utils/tests/MvTestBase.kt b/src/main/kotlin/org/move/utils/tests/MvTestBase.kt index ae6867b17..9fc1869cb 100644 --- a/src/main/kotlin/org/move/utils/tests/MvTestBase.kt +++ b/src/main/kotlin/org/move/utils/tests/MvTestBase.kt @@ -43,15 +43,13 @@ abstract class MvTestBase: BasePlatformTestCase(), setupInspections() - val settingsState = project.moveSettings.state - val debugMode = this.findAnnotationInstance()?.enabled ?: true val blockchain = this.findAnnotationInstance()?.blockchain ?: Blockchain.APTOS // triggers projects refresh - project.moveSettings.state = settingsState.copy( - debugMode = debugMode, - blockchain = blockchain - ) + project.moveSettings.modify { + it.debugMode = debugMode + it.blockchain = blockchain + } } private fun setupInspections() { diff --git a/src/main/resources/META-INF/intellij-move-core.xml b/src/main/resources/META-INF/intellij-move-core.xml index ff5f753bd..fdb8b66ac 100644 --- a/src/main/resources/META-INF/intellij-move-core.xml +++ b/src/main/resources/META-INF/intellij-move-core.xml @@ -32,6 +32,8 @@ + + From 55b1f5d18bb946e978fa3693258fe45ea0576534 Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Thu, 28 Mar 2024 12:05:01 +0300 Subject: [PATCH 02/14] rework exec settings panels, correct default project behaviour --- .../cli/runConfigurations/InitProjectCli.kt | 14 +- .../aptos/FunctionCallConfigurationBase.kt | 4 +- .../aptos/any/AnyCommandConfiguration.kt | 4 +- .../legacy/MoveCommandConfiguration.kt | 4 +- .../sui/SuiCommandConfiguration.kt | 7 +- .../cli/settings/MvProjectSettingsService.kt | 53 ++++--- .../settings/PerProjectMoveConfigurable.kt | 60 +++---- .../org/move/cli/settings/VersionLabel.kt | 4 +- .../org/move/cli/settings/aptos/AptosExec.kt | 48 ------ .../cli/settings/aptos/ChooseAptosCliPanel.kt | 150 ++++++++++-------- .../cli/settings/sui/ChooseSuiCliPanel.kt | 62 ++++---- .../ide/newProject/MoveProjectGenerator.kt | 6 +- .../newProject/MoveProjectGeneratorPeer.kt | 80 +++++++--- .../InvalidBlockchainCliConfiguration.kt | 4 +- src/main/kotlin/org/move/stdext/Paths.kt | 2 + 15 files changed, 239 insertions(+), 263 deletions(-) delete mode 100644 src/main/kotlin/org/move/cli/settings/aptos/AptosExec.kt diff --git a/src/main/kotlin/org/move/cli/runConfigurations/InitProjectCli.kt b/src/main/kotlin/org/move/cli/runConfigurations/InitProjectCli.kt index da722f356..84d9ada87 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/InitProjectCli.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/InitProjectCli.kt @@ -4,11 +4,11 @@ import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import org.move.cli.Consts -import org.move.cli.settings.Blockchain -import org.move.cli.settings.aptos.AptosExec +import org.move.cli.settings.aptos.AptosExecType import org.move.openapiext.* import org.move.openapiext.common.isUnitTestMode import org.move.stdext.RsResult +import org.move.stdext.toPathOrNull import org.move.stdext.unwrapOrElse import java.nio.file.Path @@ -20,7 +20,7 @@ sealed class InitProjectCli { packageName: String, ): MvProcessResult - data class Aptos(val aptosExec: AptosExec): InitProjectCli() { + data class Aptos(val aptosExecType: AptosExecType, val localAptosPath: String?): InitProjectCli() { override fun init( project: Project, parentDisposable: Disposable, @@ -39,8 +39,12 @@ sealed class InitProjectCli { ), workingDirectory = project.rootPath ) - val aptosPath = this.aptosExec.toPathOrNull() ?: error("unreachable") - commandLine.toGeneralCommandLine(aptosPath) + + val aptosExecPath = + AptosExecType.aptosPath(this.aptosExecType, this.localAptosPath).toPathOrNull() + ?: error("Provided aptosPath should be validated before calling init()") + commandLine + .toGeneralCommandLine(aptosExecPath) .execute(parentDisposable) .unwrapOrElse { return RsResult.Err(it) } fullyRefreshDirectory(rootDirectory) diff --git a/src/main/kotlin/org/move/cli/runConfigurations/aptos/FunctionCallConfigurationBase.kt b/src/main/kotlin/org/move/cli/runConfigurations/aptos/FunctionCallConfigurationBase.kt index 54796a53c..1174134c1 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/aptos/FunctionCallConfigurationBase.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/aptos/FunctionCallConfigurationBase.kt @@ -5,7 +5,7 @@ import com.intellij.openapi.project.Project import org.move.cli.MoveProject import org.move.cli.moveProjectsService import org.move.cli.runConfigurations.CommandConfigurationBase -import org.move.cli.settings.aptosPath +import org.move.cli.settings.aptosExecPath import java.nio.file.Path abstract class FunctionCallConfigurationBase( @@ -20,7 +20,7 @@ abstract class FunctionCallConfigurationBase( workingDirectory = value?.contentRootPath } - override fun getCliPath(project: Project): Path? = project.aptosPath + override fun getCliPath(project: Project): Path? = project.aptosExecPath fun firstRunShouldOpenEditor(): Boolean { val moveProject = moveProjectFromWorkingDirectory ?: return true diff --git a/src/main/kotlin/org/move/cli/runConfigurations/aptos/any/AnyCommandConfiguration.kt b/src/main/kotlin/org/move/cli/runConfigurations/aptos/any/AnyCommandConfiguration.kt index 1fcaefe96..f18d5b039 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/aptos/any/AnyCommandConfiguration.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/aptos/any/AnyCommandConfiguration.kt @@ -4,7 +4,7 @@ import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.openapi.project.Project import org.move.cli.moveProjectsService import org.move.cli.runConfigurations.CommandConfigurationBase -import org.move.cli.settings.aptosPath +import org.move.cli.settings.aptosExecPath import java.nio.file.Path class AnyCommandConfiguration( @@ -21,7 +21,7 @@ class AnyCommandConfiguration( } } - override fun getCliPath(project: Project): Path? = project.aptosPath + override fun getCliPath(project: Project): Path? = project.aptosExecPath override fun getConfigurationEditor() = AnyCommandConfigurationEditor() } diff --git a/src/main/kotlin/org/move/cli/runConfigurations/legacy/MoveCommandConfiguration.kt b/src/main/kotlin/org/move/cli/runConfigurations/legacy/MoveCommandConfiguration.kt index b83d914a2..3509e3ca1 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/legacy/MoveCommandConfiguration.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/legacy/MoveCommandConfiguration.kt @@ -9,7 +9,7 @@ import com.intellij.openapi.util.NlsContexts import com.intellij.util.execution.ParametersListUtil import org.jdom.Element import org.move.cli.* -import org.move.cli.settings.aptosPath +import org.move.cli.settings.aptosExecPath import org.move.stdext.exists import java.nio.file.Path @@ -66,7 +66,7 @@ class MoveCommandConfiguration( parsed.additionalArguments, environmentVariables ) - val aptosPath = project.aptosPath + val aptosPath = project.aptosExecPath ?: return CleanConfiguration.error("No Aptos CLI specified") if (!aptosPath.exists()) { diff --git a/src/main/kotlin/org/move/cli/runConfigurations/sui/SuiCommandConfiguration.kt b/src/main/kotlin/org/move/cli/runConfigurations/sui/SuiCommandConfiguration.kt index 091a8c3d3..90f122eb5 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/sui/SuiCommandConfiguration.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/sui/SuiCommandConfiguration.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.project.Project import org.move.cli.moveProjectsService import org.move.cli.runConfigurations.CommandConfigurationBase import org.move.cli.settings.moveSettings +import org.move.cli.settings.suiExecPath import org.move.stdext.toPathOrNull import java.nio.file.Path @@ -22,11 +23,7 @@ class SuiCommandConfiguration( } } - override fun getCliPath(project: Project): Path? { - return project.moveSettings.suiPath - .takeIf { it.isNotBlank() } - ?.toPathOrNull() - } + override fun getCliPath(project: Project): Path? = project.suiExecPath override fun getConfigurationEditor() = SuiCommandConfigurationEditor() } diff --git a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt index cdb966269..76895be3e 100644 --- a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt +++ b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt @@ -7,7 +7,7 @@ import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.psi.PsiManager import org.move.cli.settings.MvProjectSettingsService.MoveProjectSettings -import org.move.cli.settings.aptos.AptosExec +import org.move.cli.settings.aptos.AptosExecType import org.move.stdext.exists import org.move.stdext.isExecutableFile import org.move.stdext.toPathOrNull @@ -33,33 +33,32 @@ class MvProjectSettingsService( MvProjectSettingsServiceBase(project, MoveProjectSettings()) { val blockchain: Blockchain get() = state.blockchain - val aptosPath: String? get() = state.aptosPath - val suiPath: String get() = state.suiPath + + val aptosExecType: AptosExecType get() = state.aptosExecType + val localAptosPath: String? get() = state.localAptosPath + val localSuiPath: String? get() = state.localSuiPath val disableTelemetry: Boolean get() = state.disableTelemetry val foldSpecs: Boolean get() = state.foldSpecs val skipFetchLatestGitDeps: Boolean get() = state.skipFetchLatestGitDeps val dumpStateOnTestFailure: Boolean get() = state.dumpStateOnTestFailure + val aptosExecPath: String get() = AptosExecType.aptosPath(aptosExecType, localAptosPath) + // default values for settings - class MoveProjectSettings( - // null not Mac -> Bundled, null and Mac -> Local(""), not null -> Local(value) - var blockchain: Blockchain = Blockchain.APTOS, - var aptosPath: String? = if (AptosExec.isBundledSupportedForThePlatform()) null else "", - var suiPath: String = "", - var foldSpecs: Boolean = false, - var disableTelemetry: Boolean = true, - var debugMode: Boolean = false, - var skipFetchLatestGitDeps: Boolean = false, - var dumpStateOnTestFailure: Boolean = false, - ): MvProjectSettingsBase() { - fun aptosExec(): AptosExec { - val path = aptosPath - return when (path) { - null -> AptosExec.Bundled - else -> AptosExec.LocalPath(path) - } - } + class MoveProjectSettings: MvProjectSettingsBase() { + var blockchain: Blockchain by enum(Blockchain.APTOS) + + var aptosExecType: AptosExecType by enum(defaultAptosExecType) + var localAptosPath: String? by string() + + var localSuiPath: String? by string() + + var foldSpecs: Boolean by property(false) + var disableTelemetry: Boolean by property(true) + var debugMode: Boolean by property(false) + var skipFetchLatestGitDeps: Boolean by property(false) + var dumpStateOnTestFailure: Boolean by property(false) override fun copy(): MoveProjectSettings { val state = MoveProjectSettings() @@ -85,13 +84,17 @@ class MvProjectSettingsService( oldState: MoveProjectSettings, newState: MoveProjectSettings ): SettingsChangedEventBase(oldState, newState) -} -val Project.aptosExec: AptosExec get() = this.moveSettings.state.aptosExec() + companion object { + private val defaultAptosExecType + get() = + if (AptosExecType.isBundledSupportedForThePlatform) AptosExecType.BUNDLED else AptosExecType.LOCAL; + } +} -val Project.aptosPath: Path? get() = this.aptosExec.toPathOrNull() +val Project.aptosExecPath: Path? get() = this.moveSettings.aptosExecPath.toPathOrNull() -val Project.suiPath: Path? get() = this.moveSettings.suiPath.toPathOrNull() +val Project.suiExecPath: Path? get() = this.moveSettings.localSuiPath?.toPathOrNull() fun Path?.isValidExecutable(): Boolean { return this != null diff --git a/src/main/kotlin/org/move/cli/settings/PerProjectMoveConfigurable.kt b/src/main/kotlin/org/move/cli/settings/PerProjectMoveConfigurable.kt index d8652a51c..31f9faab2 100644 --- a/src/main/kotlin/org/move/cli/settings/PerProjectMoveConfigurable.kt +++ b/src/main/kotlin/org/move/cli/settings/PerProjectMoveConfigurable.kt @@ -18,8 +18,6 @@ class PerProjectMoveConfigurable(val project: Project): _id = "org.move.settings" ) { - private val settingsState: MvProjectSettingsService.MoveProjectSettings = project.moveSettings.state - private val chooseAptosCliPanel = ChooseAptosCliPanel(versionUpdateListener = null) private val chooseSuiCliPanel = ChooseSuiCliPanel() @@ -91,10 +89,13 @@ class PerProjectMoveConfigurable(val project: Project): } } + // saves values from Swing form back to configurable (OK / Apply) onApply { settings.modify { - it.aptosPath = chooseAptosCliPanel.selectedAptosExec.pathToSettingsFormat() - it.suiPath = chooseSuiCliPanel.getSuiCliPath() + it.aptosExecType = chooseAptosCliPanel.data.aptosExecType + it.localAptosPath = chooseAptosCliPanel.data.localAptosPath + + it.localSuiPath = chooseSuiCliPanel.data.localSuiPath it.blockchain = state.blockchain it.foldSpecs = state.foldSpecs @@ -104,6 +105,23 @@ class PerProjectMoveConfigurable(val project: Project): it.dumpStateOnTestFailure = state.dumpStateOnTestFailure } } + + // loads settings from configurable to swing form + onReset { + chooseAptosCliPanel.data = + ChooseAptosCliPanel.Data(state.aptosExecType, state.localAptosPath) + chooseSuiCliPanel.data = + ChooseSuiCliPanel.Data(state.localSuiPath) + } + + /// checks whether any settings are modified (should be fast) + onIsModified { + val aptosPanelData = chooseAptosCliPanel.data + val suiPanelData = chooseSuiCliPanel.data + aptosPanelData.aptosExecType != settings.aptosExecType + || aptosPanelData.localAptosPath != settings.localAptosPath + || suiPanelData.localSuiPath != settings.localSuiPath + } } override fun disposeUIResources() { @@ -111,38 +129,4 @@ class PerProjectMoveConfigurable(val project: Project): Disposer.dispose(chooseAptosCliPanel) Disposer.dispose(chooseSuiCliPanel) } - - /// checks whether any settings are modified (should be fast) - override fun isModified(): Boolean { - // checks whether panel created in the createPanel() is modified, defined in DslConfigurableBase - if (super.isModified()) return true - val selectedAptosExec = chooseAptosCliPanel.selectedAptosExec - val selectedSui = chooseSuiCliPanel.getSuiCliPath() - return selectedAptosExec != settingsState.aptosExec() - || selectedSui != settingsState.suiPath - } - - /// loads settings from configurable to swing form - override fun reset() { - chooseAptosCliPanel.selectedAptosExec = settingsState.aptosExec() - chooseSuiCliPanel.setSuiCliPath(settingsState.suiPath) - // resets panel created in createPanel(), see DslConfigurableBase - // should be invoked at the end - super.reset() - } - - /// saves values from Swing form back to configurable (OK / Apply) -// override fun apply() { -// // calls apply() for createPanel().value -// super.apply() -// project.moveSettings.modify { -// it.aptosPath = chooseAptosCliPanel.selectedAptosExec.pathToSettingsFormat() -// it.suiPath = chooseSuiCliPanel.getSuiCliPath() -// } -//// project.moveSettings.state = -//// settingsState.copy( -//// aptosPath = chooseAptosCliPanel.selectedAptosExec.pathToSettingsFormat(), -//// suiPath = chooseSuiCliPanel.getSuiCliPath() -//// ) -// } } diff --git a/src/main/kotlin/org/move/cli/settings/VersionLabel.kt b/src/main/kotlin/org/move/cli/settings/VersionLabel.kt index a55f8e7e3..420daf07f 100644 --- a/src/main/kotlin/org/move/cli/settings/VersionLabel.kt +++ b/src/main/kotlin/org/move/cli/settings/VersionLabel.kt @@ -18,13 +18,13 @@ class VersionLabel( private val versionUpdateDebouncer = UiDebouncer(parentDisposable) - fun updateAndNotifyListeners(execPath: Path) { + fun updateAndNotifyListeners(execPath: Path?) { versionUpdateDebouncer.update( onPooledThread = { if (!isUnitTestMode) { checkIsBackgroundThread() } - if (!execPath.isValidExecutable()) { + if (execPath == null || !execPath.isValidExecutable()) { return@update null } diff --git a/src/main/kotlin/org/move/cli/settings/aptos/AptosExec.kt b/src/main/kotlin/org/move/cli/settings/aptos/AptosExec.kt deleted file mode 100644 index a127c29b4..000000000 --- a/src/main/kotlin/org/move/cli/settings/aptos/AptosExec.kt +++ /dev/null @@ -1,48 +0,0 @@ -package org.move.cli.settings.aptos - -import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.util.SystemInfo -import org.move.cli.settings.MvProjectSettingsService -import org.move.cli.settings.isValidExecutable -import org.move.openapiext.PluginPathManager -import org.move.stdext.toPathOrNull - -sealed class AptosExec { - abstract val execPath: String - - object Bundled: AptosExec() { - override val execPath: String - get() = PluginPathManager.bundledAptosCli ?: "" - } - - data class LocalPath(override val execPath: String): AptosExec() - - fun isValid(): Boolean { - if (this is Bundled && !isBundledSupportedForThePlatform()) return false - return this.toPathOrNull().isValidExecutable() - } - - fun toPathOrNull() = this.execPath.toPathOrNull() - - fun pathToSettingsFormat(): String? = - when (this) { - is LocalPath -> this.execPath - is Bundled -> null - } - - companion object { - fun default(): AptosExec { - // Don't use `Project.moveSettings` here because `getService` can return `null` - // for default project after dynamic plugin loading. As a result, you can get - // `java.lang.IllegalStateException`. So let's handle it manually: - val defaultProjectSettings = - ProjectManager.getInstance().defaultProject.getService(MvProjectSettingsService::class.java) - - val defaultProjectAptosExec = defaultProjectSettings?.state?.aptosExec() - return defaultProjectAptosExec - ?: if (isBundledSupportedForThePlatform()) Bundled else LocalPath("") - } - - fun isBundledSupportedForThePlatform(): Boolean = !SystemInfo.isMac - } -} diff --git a/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt b/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt index 26f6ee9f3..e42522b96 100644 --- a/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt +++ b/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt @@ -3,110 +3,120 @@ package org.move.cli.settings.aptos import com.intellij.openapi.Disposable import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.SystemInfo +import com.intellij.ui.components.JBRadioButton import com.intellij.ui.dsl.builder.* +import com.intellij.ui.layout.selected import org.move.cli.settings.VersionLabel +import org.move.cli.settings.aptos.AptosExecType.BUNDLED +import org.move.cli.settings.aptos.AptosExecType.LOCAL +import org.move.openapiext.PluginPathManager import org.move.openapiext.pathField +import org.move.stdext.blankToNull import org.move.stdext.toPathOrNull +enum class AptosExecType { + BUNDLED, + LOCAL; + + companion object { + val isBundledSupportedForThePlatform: Boolean get() = !SystemInfo.isMac + + fun bundledPath(): String? = PluginPathManager.bundledAptosCli + fun aptosPath(execType: AptosExecType, localAptosPath: String?): String { + return when (execType) { + BUNDLED -> bundledPath() ?: "" + LOCAL -> localAptosPath ?: "" + } + } + } +} + class ChooseAptosCliPanel(versionUpdateListener: (() -> Unit)?): Disposable { + data class Data( + val aptosExecType: AptosExecType, + val localAptosPath: String? + ) + + var data: Data + get() { + val execType = if (bundledRadioButton.isSelected) BUNDLED else LOCAL + val path = localPathField.text.blankToNull() + return Data( + aptosExecType = execType, + localAptosPath = path + ) + } + set(value) { + when (value.aptosExecType) { + BUNDLED -> { + bundledRadioButton.isSelected = true + localRadioButton.isSelected = false + } + LOCAL -> { + bundledRadioButton.isSelected = false + localRadioButton.isSelected = true + } + } + localPathField.text = value.localAptosPath ?: "" + updateVersion() + } + private val localPathField = pathField( FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor(), this, "Choose Aptos CLI", - onTextChanged = { text -> - val exec = AptosExec.LocalPath(text) - _aptosExec = exec - exec.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } + onTextChanged = { _ -> + updateVersion() }) - private val versionLabel = VersionLabel(this, versionUpdateListener) - private lateinit var _aptosExec: AptosExec - - var selectedAptosExec: AptosExec - get() = _aptosExec - set(aptosExec) { - this._aptosExec = aptosExec - when (_aptosExec) { - is AptosExec.Bundled -> localPathField.text = "" - else -> - localPathField.text = aptosExec.execPath - } - aptosExec.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } - } + private val bundledRadioButton = JBRadioButton("Bundled") + private val localRadioButton = JBRadioButton("Local") fun attachToLayout(layout: Panel): Row { - val panel = this - if (!panel::_aptosExec.isInitialized) { - panel._aptosExec = AptosExec.default() - } val resultRow = with(layout) { group("Aptos CLI") { buttonsGroup { row { - radioButton("Bundled", AptosExec.Bundled) - .bindSelected( - { _aptosExec is AptosExec.Bundled }, - { - _aptosExec = AptosExec.Bundled - val bundledPath = AptosExec.Bundled.toPathOrNull() - if (bundledPath != null) { - versionLabel.updateAndNotifyListeners(bundledPath) - } - } - ) -// .actionListener { _, _ -> -// _aptosExec = AptosExec.Bundled -// AptosExec.Bundled.toPathOrNull() -// ?.let { versionLabel.updateValueWithListener(it) } -// } - .enabled(AptosExec.isBundledSupportedForThePlatform()) + cell(bundledRadioButton) + .enabled(AptosExecType.isBundledSupportedForThePlatform) + .actionListener { _, _ -> + updateVersion() + } comment( "Bundled version is not available for this platform (refer to the official Aptos docs for more)" ) - .visible(!AptosExec.isBundledSupportedForThePlatform()) } row { - val button = radioButton("Local", AptosExec.LocalPath("")) - .bindSelected( - { _aptosExec is AptosExec.LocalPath }, - { - _aptosExec = AptosExec.LocalPath(localPathField.text) - val localPath = localPathField.text.toPathOrNull() - if (localPath != null) { - versionLabel.updateAndNotifyListeners(localPath) - } - } - ) -// .actionListener { _, _ -> -// _aptosExec = AptosExec.LocalPath(localPathField.text) -// localPathField.text.toPathOrNull() -// ?.let { versionLabel.updateAndNotifyListeners(it) } -// } - cell(localPathField) - .enabledIf(button.selected) - .align(AlignX.FILL).resizableColumn() + cell(localRadioButton) + .actionListener { _, _ -> + updateVersion() + } + cell(localPathField ) + .enabledIf(localRadioButton.selected) + .align(AlignX.FILL) + .resizableColumn() } row("--version :") { cell(versionLabel) } } - .bind( - { _aptosExec }, - { - _aptosExec = - when (it) { - is AptosExec.Bundled -> it - is AptosExec.LocalPath -> AptosExec.LocalPath(localPathField.text) - } - } - ) } } - _aptosExec.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } + updateVersion() return resultRow } + private fun updateVersion() { + val aptosPath = + when { + bundledRadioButton.isSelected -> AptosExecType.bundledPath() + else -> localPathField.text + }?.toPathOrNull() + versionLabel.updateAndNotifyListeners(aptosPath) + } + override fun dispose() { Disposer.dispose(localPathField) } diff --git a/src/main/kotlin/org/move/cli/settings/sui/ChooseSuiCliPanel.kt b/src/main/kotlin/org/move/cli/settings/sui/ChooseSuiCliPanel.kt index ffcd38cf9..da8e0dfc7 100644 --- a/src/main/kotlin/org/move/cli/settings/sui/ChooseSuiCliPanel.kt +++ b/src/main/kotlin/org/move/cli/settings/sui/ChooseSuiCliPanel.kt @@ -2,65 +2,59 @@ package org.move.cli.settings.sui import com.intellij.openapi.Disposable import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory -import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.util.Disposer -import com.intellij.ui.dsl.builder.* -import org.move.cli.settings.MvProjectSettingsService +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.Row import org.move.cli.settings.VersionLabel -import org.move.openapiext.* +import org.move.openapiext.pathField import org.move.stdext.toPathOrNull -class ChooseSuiCliPanel( - private val versionUpdateListener: (() -> Unit)? = null -): Disposable { +class ChooseSuiCliPanel(versionUpdateListener: (() -> Unit)? = null): Disposable { + + data class Data( + val localSuiPath: String?, + ) + + var data: Data + get() { + return Data(localSuiPath = localPathField.text) + } + set(value) { + localPathField.text = value.localSuiPath ?: "" + updateVersion() + } private val localPathField = pathField( FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor(), this, "Choose Sui CLI", - onTextChanged = { text -> - _suiCliPath = text - _suiCliPath.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } + onTextChanged = { _ -> + updateVersion() }) private val versionLabel = VersionLabel(this, versionUpdateListener) - private lateinit var _suiCliPath: String - - fun getSuiCliPath(): String { return _suiCliPath } - fun setSuiCliPath(path: String) { - this._suiCliPath = path - localPathField.text = path - path.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } - } - fun attachToLayout(layout: Panel): Row { - val panel = this - if (!panel::_suiCliPath.isInitialized) { - val defaultProjectSettings = - ProjectManager.getInstance().defaultProject.getService(MvProjectSettingsService::class.java) - panel._suiCliPath = defaultProjectSettings.suiPath - } val resultRow = with(layout) { group("Sui CLI") { row { cell(localPathField) - .bindText( - { _suiCliPath }, - { _suiCliPath = it } - ) - .onChanged { - localPathField.text.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } - } - .align(AlignX.FILL).resizableColumn() + .align(AlignX.FILL) + .resizableColumn() } row("--version :") { cell(versionLabel) } } } - _suiCliPath.toPathOrNull()?.let { versionLabel.updateAndNotifyListeners(it) } + updateVersion() return resultRow } + private fun updateVersion() { + val localSuiPath = localPathField.text.toPathOrNull() + versionLabel.updateAndNotifyListeners(localSuiPath) + } + override fun dispose() { Disposer.dispose(localPathField) } diff --git a/src/main/kotlin/org/move/ide/newProject/MoveProjectGenerator.kt b/src/main/kotlin/org/move/ide/newProject/MoveProjectGenerator.kt index e0cfe853b..086a1342e 100644 --- a/src/main/kotlin/org/move/ide/newProject/MoveProjectGenerator.kt +++ b/src/main/kotlin/org/move/ide/newProject/MoveProjectGenerator.kt @@ -17,6 +17,7 @@ import org.move.cli.settings.Blockchain import org.move.cli.settings.moveSettings import org.move.ide.MoveIcons import org.move.openapiext.computeWithCancelableProgress +import org.move.stdext.blankToNull import org.move.stdext.unwrapOrThrow data class MoveProjectConfig(val blockchain: Blockchain, val initCli: InitProjectCli) @@ -56,10 +57,11 @@ class MoveProjectGenerator: DirectoryProjectGeneratorBase(), it.blockchain = blockchain when (projectCli) { is InitProjectCli.Aptos -> { - it.aptosPath = projectCli.aptosExec.pathToSettingsFormat() + it.aptosExecType = projectCli.aptosExecType + it.localAptosPath = projectCli.localAptosPath?.blankToNull() } is InitProjectCli.Sui -> { - it.suiPath = projectCli.cliLocation.toString() + it.localSuiPath = projectCli.cliLocation.toString() } } } diff --git a/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt b/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt index 65b58fcf7..129694ba0 100644 --- a/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt +++ b/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt @@ -6,15 +6,20 @@ package org.move.ide.newProject import com.intellij.openapi.Disposable -import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.openapi.ui.ValidationInfo import com.intellij.openapi.util.Disposer import com.intellij.platform.GeneratorPeerImpl import com.intellij.ui.components.JBRadioButton -import com.intellij.ui.dsl.builder.* +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.selected import org.move.cli.runConfigurations.InitProjectCli import org.move.cli.settings.Blockchain +import org.move.cli.settings.MvProjectSettingsService +import org.move.cli.settings.aptos.AptosExecType import org.move.cli.settings.aptos.ChooseAptosCliPanel import org.move.cli.settings.isValidExecutable import org.move.cli.settings.sui.ChooseSuiCliPanel @@ -26,27 +31,38 @@ class MoveProjectGeneratorPeer(val parentDisposable: Disposable): GeneratorPeerI private val chooseAptosCliPanel = ChooseAptosCliPanel { checkValid?.run() } private val chooseSuiCliPanel = ChooseSuiCliPanel { checkValid?.run() } + private var blockchain: Blockchain + init { Disposer.register(parentDisposable, chooseAptosCliPanel) Disposer.register(parentDisposable, chooseSuiCliPanel) + + // set values from the default project settings + val defaultProjectSettings = + ProjectManager.getInstance().defaultProject.getService(MvProjectSettingsService::class.java) + blockchain = defaultProjectSettings.blockchain + chooseAptosCliPanel.data = + ChooseAptosCliPanel.Data(defaultProjectSettings.aptosExecType, defaultProjectSettings.localAptosPath) + chooseSuiCliPanel.data = ChooseSuiCliPanel.Data(defaultProjectSettings.localSuiPath) } private var checkValid: Runnable? = null - private var blockchain: Blockchain = Blockchain.SUI override fun getSettings(): MoveProjectConfig { - val initCli = + val initProjectCli = when (blockchain) { Blockchain.APTOS -> { - InitProjectCli.Aptos(this.chooseAptosCliPanel.selectedAptosExec) + val aptosExecType = this.chooseAptosCliPanel.data.aptosExecType + val localAptosPath = this.chooseAptosCliPanel.data.localAptosPath + InitProjectCli.Aptos(aptosExecType, localAptosPath) } Blockchain.SUI -> { - val suiPath = this.chooseSuiCliPanel.getSuiCliPath().toPathOrNull() + val suiPath = this.chooseSuiCliPanel.data.localSuiPath?.toPathOrNull() ?: error("Should be validated separately") InitProjectCli.Sui(suiPath) } } - return MoveProjectConfig(blockchain, initCli) + return MoveProjectConfig(blockchain, initProjectCli) } override fun getComponent(myLocationField: TextFieldWithBrowseButton, checkValid: Runnable): JComponent { @@ -55,29 +71,35 @@ class MoveProjectGeneratorPeer(val parentDisposable: Disposable): GeneratorPeerI } override fun getComponent(): JComponent { + val generatorPeer = this return panel { var aptosRadioButton: Cell? = null var suiRadioButton: Cell? = null - buttonsGroup("Blockchain") { row { - aptosRadioButton = radioButton("Aptos", Blockchain.APTOS) - .actionListener { _, _ -> - blockchain = Blockchain.APTOS - checkValid?.run() - } - suiRadioButton = radioButton("Sui", Blockchain.SUI) - .actionListener { _, _ -> - blockchain = Blockchain.SUI - checkValid?.run() - } + aptosRadioButton = radioButton("Aptos") + .bindSelected( + { generatorPeer.blockchain == Blockchain.APTOS }, + { + generatorPeer.blockchain = Blockchain.APTOS + checkValid?.run() + } + ) + suiRadioButton = radioButton("Sui") + .bindSelected( + { generatorPeer.blockchain == Blockchain.SUI }, + { + generatorPeer.blockchain = Blockchain.SUI + checkValid?.run() + } + ) } } - .bind({ blockchain }, { blockchain = it }) - - chooseAptosCliPanel.attachToLayout(this) + chooseAptosCliPanel + .attachToLayout(this) .visibleIf(aptosRadioButton!!.selected) - chooseSuiCliPanel.attachToLayout(this) + chooseSuiCliPanel + .attachToLayout(this) .visibleIf(suiRadioButton!!.selected) } } @@ -85,14 +107,20 @@ class MoveProjectGeneratorPeer(val parentDisposable: Disposable): GeneratorPeerI override fun validate(): ValidationInfo? { when (blockchain) { Blockchain.APTOS -> { - val aptosPath = this.chooseAptosCliPanel.selectedAptosExec.toPathOrNull() - if (aptosPath == null || !aptosPath.isValidExecutable()) { + val panelData = this.chooseAptosCliPanel.data + val aptosExecPath = + AptosExecType.aptosPath(panelData.aptosExecType, panelData.localAptosPath).toPathOrNull() + if (aptosExecPath == null + || !aptosExecPath.isValidExecutable() + ) { return ValidationInfo("Invalid path to $blockchain executable") } } Blockchain.SUI -> { - val suiPath = this.chooseSuiCliPanel.getSuiCliPath().toPathOrNull() - if (suiPath == null || !suiPath.isValidExecutable()) { + val suiExecPath = this.chooseSuiCliPanel.data.localSuiPath?.toPathOrNull() + if (suiExecPath == null + || !suiExecPath.isValidExecutable() + ) { return ValidationInfo("Invalid path to $blockchain executable") } } diff --git a/src/main/kotlin/org/move/ide/notifications/InvalidBlockchainCliConfiguration.kt b/src/main/kotlin/org/move/ide/notifications/InvalidBlockchainCliConfiguration.kt index d497a0a76..62da5283b 100644 --- a/src/main/kotlin/org/move/ide/notifications/InvalidBlockchainCliConfiguration.kt +++ b/src/main/kotlin/org/move/ide/notifications/InvalidBlockchainCliConfiguration.kt @@ -26,10 +26,10 @@ class InvalidBlockchainCliConfiguration(project: Project): MvEditorNotificationP val blockchain = project.moveSettings.blockchain when (blockchain) { Blockchain.APTOS -> { - if (project.aptosExec.isValid()) return null + if (project.aptosExecPath.isValidExecutable()) return null } Blockchain.SUI -> { - if (project.suiPath.isValidExecutable()) return null + if (project.suiExecPath.isValidExecutable()) return null } } diff --git a/src/main/kotlin/org/move/stdext/Paths.kt b/src/main/kotlin/org/move/stdext/Paths.kt index f03ba85d6..fc18cbec8 100644 --- a/src/main/kotlin/org/move/stdext/Paths.kt +++ b/src/main/kotlin/org/move/stdext/Paths.kt @@ -33,3 +33,5 @@ fun Path.exists(): Boolean = Files.exists(this) fun executableName(toolName: String): String = if (SystemInfo.isWindows) "$toolName.exe" else toolName + +fun String.blankToNull(): String? = ifBlank { null } \ No newline at end of file From 26d82c03e38a4cb81e674baef47ebf5f5a4061aa Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Fri, 29 Mar 2024 21:43:11 +0300 Subject: [PATCH 03/14] add ui tests for project creation --- build.gradle.kts | 37 ++++ settings.gradle.kts | 1 + .../cli/settings/aptos/ChooseAptosCliPanel.kt | 2 + .../newProject/MoveProjectGeneratorPeer.kt | 44 +++-- .../org/move/ui/fixtures/ActionMenuFixture.kt | 32 +++ .../org/move/ui/fixtures/DialogFixture.kt | 36 ++++ .../kotlin/org/move/ui/fixtures/IdeaFrame.kt | 94 +++++++++ .../TextFieldWithBrowseButtonFixture.kt | 56 ++++++ .../org/move/ui/fixtures/WelcomeFrame.kt | 97 +++++++++ .../org/move/ui/utils/RemoteRobotExtension.kt | 144 ++++++++++++++ .../kotlin/org/move/ui/utils/StepsLogger.kt | 17 ++ .../test/kotlin/org/move/ui/IdeaFrameTest.kt | 23 +++ .../kotlin/org/move/ui/WelcomeFrameTest.kt | 186 ++++++++++++++++++ 13 files changed, 751 insertions(+), 18 deletions(-) create mode 100644 ui-tests/src/main/kotlin/org/move/ui/fixtures/ActionMenuFixture.kt create mode 100644 ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt create mode 100644 ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt create mode 100644 ui-tests/src/main/kotlin/org/move/ui/fixtures/TextFieldWithBrowseButtonFixture.kt create mode 100644 ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt create mode 100644 ui-tests/src/main/kotlin/org/move/ui/utils/RemoteRobotExtension.kt create mode 100644 ui-tests/src/main/kotlin/org/move/ui/utils/StepsLogger.kt create mode 100644 ui-tests/src/test/kotlin/org/move/ui/IdeaFrameTest.kt create mode 100644 ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 5249b6b94..30e9d19aa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,6 +21,8 @@ val pluginJarName = "intellij-move-$pluginVersion" val kotlinReflectVersion = "1.8.10" val aptosVersion = "3.1.0" +val remoteRobotVersion = "0.11.22" + group = pluginGroup version = pluginVersion @@ -254,6 +256,41 @@ project(":plugin") { } } } + + downloadRobotServerPlugin { + version.set(remoteRobotVersion) + } + + runIdeForUiTests { + systemProperty("robot-server.port", "8082") +// systemProperty "ide.mac.message.dialogs.as.sheets", "false" +// systemProperty "jb.privacy.policy.text", "" +// systemProperty "jb.consents.confirmation.enabled", "false" +// systemProperty "ide.mac.file.chooser.native", "false" +// systemProperty "jbScreenMenuBar.enabled", "false" +// systemProperty "apple.laf.useScreenMenuBar", "false" + systemProperty("idea.trust.all.projects", "true") + systemProperty("ide.show.tips.on.startup.default.value", "false") + } + } +} + +project(":ui-tests") { + dependencies { + implementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion") + implementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion") + implementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0") + + implementation("com.automation-remarks:video-recorder-junit5:2.0") + } + + tasks.named("test") { + useJUnitPlatform() } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 9b38bf18e..886c8a92e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,3 +8,4 @@ pluginManagement { } include("plugin") +include("ui-tests") diff --git a/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt b/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt index e42522b96..c3a344230 100644 --- a/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt +++ b/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt @@ -21,6 +21,7 @@ enum class AptosExecType { companion object { val isBundledSupportedForThePlatform: Boolean get() = !SystemInfo.isMac +// val isBundledSupportedForThePlatform: Boolean get() = false fun bundledPath(): String? = PluginPathManager.bundledAptosCli fun aptosPath(execType: AptosExecType, localAptosPath: String?): String { @@ -89,6 +90,7 @@ class ChooseAptosCliPanel(versionUpdateListener: (() -> Unit)?): Disposable { comment( "Bundled version is not available for this platform (refer to the official Aptos docs for more)" ) + .visible(!AptosExecType.isBundledSupportedForThePlatform) } row { cell(localRadioButton) diff --git a/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt b/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt index 129694ba0..6f82db0b4 100644 --- a/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt +++ b/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt @@ -12,10 +12,7 @@ import com.intellij.openapi.ui.ValidationInfo import com.intellij.openapi.util.Disposer import com.intellij.platform.GeneratorPeerImpl import com.intellij.ui.components.JBRadioButton -import com.intellij.ui.dsl.builder.Cell -import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.dsl.builder.selected +import com.intellij.ui.dsl.builder.* import org.move.cli.runConfigurations.InitProjectCli import org.move.cli.settings.Blockchain import org.move.cli.settings.MvProjectSettingsService @@ -78,23 +75,34 @@ class MoveProjectGeneratorPeer(val parentDisposable: Disposable): GeneratorPeerI buttonsGroup("Blockchain") { row { aptosRadioButton = radioButton("Aptos") - .bindSelected( - { generatorPeer.blockchain == Blockchain.APTOS }, - { - generatorPeer.blockchain = Blockchain.APTOS - checkValid?.run() - } - ) + .selected(generatorPeer.blockchain == Blockchain.APTOS) + .actionListener { _, _ -> + generatorPeer.blockchain = Blockchain.APTOS + checkValid?.run() + } +// .bindSelected( +// { generatorPeer.blockchain == Blockchain.APTOS }, +// { +// generatorPeer.blockchain = Blockchain.APTOS +// checkValid?.run() +// } +// ) suiRadioButton = radioButton("Sui") - .bindSelected( - { generatorPeer.blockchain == Blockchain.SUI }, - { - generatorPeer.blockchain = Blockchain.SUI - checkValid?.run() - } - ) + .selected(generatorPeer.blockchain == Blockchain.SUI) + .actionListener { _, _ -> + generatorPeer.blockchain = Blockchain.SUI + checkValid?.run() + } +// .bindSelected( +// { generatorPeer.blockchain == Blockchain.SUI }, +// { +// generatorPeer.blockchain = Blockchain.SUI +// checkValid?.run() +// } +// ) } } + chooseAptosCliPanel .attachToLayout(this) .visibleIf(aptosRadioButton!!.selected) diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/ActionMenuFixture.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/ActionMenuFixture.kt new file mode 100644 index 000000000..b4de042bc --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/ActionMenuFixture.kt @@ -0,0 +1,32 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +package org.move.ui.fixtures + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.ComponentFixture +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.waitFor + +fun RemoteRobot.actionMenu(text: String): ActionMenuFixture { + val xpath = byXpath("text '$text'", "//div[@class='ActionMenu' and @text='$text']") + waitFor { + findAll(xpath).isNotEmpty() + } + return findAll(xpath).first() +} + +fun RemoteRobot.actionMenuItem(text: String): ActionMenuItemFixture { + val xpath = byXpath("text '$text'", "//div[@class='ActionMenuItem' and @text='$text']") + waitFor { + findAll(xpath).isNotEmpty() + } + return findAll(xpath).first() +} + +@FixtureName("ActionMenu") +class ActionMenuFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : ComponentFixture(remoteRobot, remoteComponent) + +@FixtureName("ActionMenuItem") +class ActionMenuItemFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : ComponentFixture(remoteRobot, remoteComponent) \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt new file mode 100644 index 000000000..a286ff75a --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt @@ -0,0 +1,36 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +package org.move.ui.fixtures + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import org.move.ui.fixtures.DialogFixture.Companion +import java.time.Duration + +fun ContainerFixture.dialog( + title: String, + timeout: Duration = Duration.ofSeconds(20), + function: DialogFixture.() -> Unit = {} +): DialogFixture = step("Search for dialog with title $title") { + find(Companion.byTitle(title), timeout).apply(function) +} + +@FixtureName("Dialog") +class DialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +): CommonContainerFixture(remoteRobot, remoteComponent) { + + companion object { + @JvmStatic + fun byTitle(title: String) = byXpath("title $title", "//div[@title='$title' and @class='MyDialog']") + } + + val title: String + get() = callJs("component.getTitle();") +} \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt new file mode 100644 index 000000000..f2d91896a --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt @@ -0,0 +1,94 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +package org.move.ui.fixtures + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.waitFor +import java.time.Duration + +fun RemoteRobot.ideaFrame(function: IdeaFrame.() -> Unit) { + find(timeout = Duration.ofSeconds(10)).apply(function) +} + +@FixtureName("Idea frame") +@DefaultXpath("IdeFrameImpl type", "//div[@class='IdeFrameImpl']") +class IdeaFrame( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +) : + CommonContainerFixture(remoteRobot, remoteComponent) { + + val projectViewTree + get() = find(byXpath("ProjectViewTree", "//div[@class='ProjectViewTree']")) + + val projectName + get() = step("Get project name") { return@step callJs("component.getProject().getName()") } + + val menuBar: JMenuBarFixture + get() = step("Menu...") { + return@step remoteRobot.find(JMenuBarFixture::class.java, JMenuBarFixture.byType()) + } + + @JvmOverloads + fun dumbAware(timeout: Duration = Duration.ofMinutes(5), function: () -> Unit) { + step("Wait for smart mode") { + waitFor(duration = timeout, interval = Duration.ofSeconds(5)) { + runCatching { isDumbMode().not() }.getOrDefault(false) + } + function() + step("..wait for smart mode again") { + waitFor(duration = timeout, interval = Duration.ofSeconds(5)) { + isDumbMode().not() + } + } + } + } + + fun isDumbMode(): Boolean { + return callJs( + """ + const frameHelper = com.intellij.openapi.wm.impl.ProjectFrameHelper.getFrameHelper(component) + if (frameHelper) { + const project = frameHelper.getProject() + project ? com.intellij.openapi.project.DumbService.isDumb(project) : true + } else { + true + } + """, true + ) + } + + fun openFile(path: String) { + runJs( + """ + importPackage(com.intellij.openapi.fileEditor) + importPackage(com.intellij.openapi.vfs) + importPackage(com.intellij.openapi.wm.impl) + importClass(com.intellij.openapi.application.ApplicationManager) + + const path = '$path' + const frameHelper = ProjectFrameHelper.getFrameHelper(component) + if (frameHelper) { + const project = frameHelper.getProject() + const projectPath = project.getBasePath() + const file = LocalFileSystem.getInstance().findFileByPath(projectPath + '/' + path) + const openFileFunction = new Runnable({ + run: function() { + FileEditorManager.getInstance(project).openTextEditor( + new OpenFileDescriptor( + project, + file + ), true + ) + } + }) + ApplicationManager.getApplication().invokeLater(openFileFunction) + } + """, true + ) + } +} \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/TextFieldWithBrowseButtonFixture.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/TextFieldWithBrowseButtonFixture.kt new file mode 100644 index 000000000..0933e9337 --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/TextFieldWithBrowseButtonFixture.kt @@ -0,0 +1,56 @@ +package org.move.ui.fixtures + +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ComponentFixture +import com.intellij.remoterobot.fixtures.JLabelFixture +import com.intellij.remoterobot.search.locators.Locator +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.RelativeLocators +import java.time.Duration + +fun CommonContainerFixture.textFieldWithBrowseButton( + locator: Locator, + timeout: Duration = Duration.ofSeconds(5) +) = find(locator, timeout) + +fun CommonContainerFixture.textFieldWithBrowseButton(labelText: String): TextFieldWithBrowseButtonFixture { + val locator = TextFieldWithBrowseButtonFixture.byLabel(jLabel(labelText)) + return textFieldWithBrowseButton(locator) +} + + +class TextFieldWithBrowseButtonFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +): + ComponentFixture(remoteRobot, remoteComponent) { + + companion object { +// @JvmStatic +// fun byType() = Locators.byType(TextFieldWithBrowseButton::class.java) + + @JvmStatic + fun byLabel(fixture: JLabelFixture): Locator { + return RelativeLocators.byLabel(fixture) + } + } + + var text: String + set(value) = step("Set text '$value'") { + runJs( + "JTextComponentFixture(robot, component.getTextField())" + + ".setText('${value.replace("\\", "\\\\")}')" + ) + } + get() = step("Get text") { + callJs("component.getTextField().getText() || ''", true) + } + + val isEnabled: Boolean + get() = step("..is enabled?") { + callJs("component.isEnabled()", true) + } +} \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt new file mode 100644 index 000000000..0a6201075 --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt @@ -0,0 +1,97 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +package org.move.ui.fixtures + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.SearchContext +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.Locator +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.Locators +import com.intellij.remoterobot.utils.waitFor +import com.intellij.ui.dsl.builder.components.DslLabel +import java.time.Duration + +fun RemoteRobot.welcomeFrame(function: WelcomeFrame.() -> Unit) { + find(WelcomeFrame::class.java, Duration.ofSeconds(10)).apply(function) +} + +fun tryWithDelay(delay: Long = 1, condition: () -> Boolean) { + waitFor( + duration = Duration.ofSeconds(delay), + interval = Duration.ofSeconds(delay), + condition = condition + ) +} + +fun SearchContext.findOrNull(type: Class, locator: Locator) = + findAll(type, locator).firstOrNull() + +@FixtureName("Welcome Frame") +@DefaultXpath("type", "//div[@class='FlatWelcomeFrame']") +class WelcomeFrame( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +): + CommonContainerFixture(remoteRobot, remoteComponent) { + + fun newProjectButton(onStartup: Boolean = true) = + if (onStartup) { + // clean IDE + button(byXpath("//div[@defaulticon='createNewProjectTab.svg']"), Duration.ofSeconds(2)) + } else { + // projects opened before + button(byXpath("//div[@class='JBOptionButton' and @text='New Project']")) + } + + val versionLabel get() = jLabel(byXpath("//div[@class='VersionLabel']"), timeout = Duration.ofSeconds(1)) + val validationLabel: JLabelFixture? + get() = + findOrNull(JLabelFixture::class.java, byXpath("//div[@defaulticon='lightning.svg']")) + + val projectTypesList + get() = find(ComponentFixture::class.java, byXpath("//div[@class='JBList']")) + + val aptosRadioButton get() = radioButton("Aptos") + val suiRadioButton get() = radioButton("Sui") + + val bundledRadioButton get() = radioButton("Bundled") + val localRadioButton get() = radioButton("Local") + + val localPathTextField get() = + textFieldWithBrowseButton(byXpath("//div[@class='DialogPanel']//div[@class='TextFieldWithBrowseButton']")) + + val projectLocationTextField get() = textFieldWithBrowseButton("Location:") +// textField(byXpath("//div[@accessiblename='Location:' and @class='TextFieldWithBrowseButton']")) + + val createButton get() = button("Create") + val cancelButton get() = button("Cancel") + + @Suppress("UnstableApiUsage") + val bundledAptosUnsupportedComment: ComponentFixture? + get() { + val labelText = "is not available for this platform" + return findOrNull( + ComponentFixture::class.java, + Locators.byTypeAndPropertiesContains( + DslLabel::class.java, + Locators.XpathProperty.TEXT to labelText + ) + ) + } + + val createNewProjectLink + get() = actionLink( + byXpath( + "New Project", + "//div[(@class='MainButton' and @text='New Project') or (@accessiblename='New Project' and @class='JButton')]" + ) + ) + + val moreActions + get() = button(byXpath("More Action", "//div[@accessiblename='More Actions']")) + + val heavyWeightPopup + get() = remoteRobot.find(ComponentFixture::class.java, byXpath("//div[@class='HeavyWeightWindow']")) +} \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/utils/RemoteRobotExtension.kt b/ui-tests/src/main/kotlin/org/move/ui/utils/RemoteRobotExtension.kt new file mode 100644 index 000000000..d6f85d2ad --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/utils/RemoteRobotExtension.kt @@ -0,0 +1,144 @@ +package org.move.ui.utils + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.search.locators.byXpath +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.logging.HttpLoggingInterceptor +import org.junit.jupiter.api.extension.AfterTestExecutionCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.ParameterContext +import org.junit.jupiter.api.extension.ParameterResolver +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.io.File +import java.lang.reflect.Method +import javax.imageio.ImageIO + +class RemoteRobotExtension: AfterTestExecutionCallback, ParameterResolver { + private val url: String = System.getProperty("remote-robot-url") ?: "http://127.0.0.1:8082" + private val remoteRobot: RemoteRobot = + if (System.getProperty("debug-retrofit")?.equals("enable") == true) { + val interceptor: HttpLoggingInterceptor = HttpLoggingInterceptor().apply { + this.level = HttpLoggingInterceptor.Level.BODY + } + val client = OkHttpClient.Builder().apply { + this.addInterceptor(interceptor) + }.build() + RemoteRobot(url, client) + } else { + RemoteRobot(url) + } + private val client = OkHttpClient() + + override fun supportsParameter( + parameterContext: ParameterContext?, + extensionContext: ExtensionContext? + ): Boolean { + return parameterContext?.parameter?.type?.equals(RemoteRobot::class.java) ?: false + } + + override fun resolveParameter( + parameterContext: ParameterContext?, + extensionContext: ExtensionContext? + ): Any { + return remoteRobot + } + + override fun afterTestExecution(context: ExtensionContext?) { + val testMethod: Method = + context?.requiredTestMethod ?: throw IllegalStateException("test method is null") + val testMethodName = testMethod.name + val testFailed: Boolean = context.executionException?.isPresent ?: false + if (testFailed) { +// saveScreenshot(testMethodName) + saveIdeaFrames(testMethodName) + saveHierarchy(testMethodName) + } + } + + private fun saveScreenshot(testName: String) { + fetchScreenShot().save(testName) + } + + private fun saveHierarchy(testName: String) { + val hierarchySnapshot = + saveFile(url, "build/reports", "hierarchy-$testName.html") + if (File("build/reports/styles.css").exists().not()) { + saveFile("$url/styles.css", "build/reports", "styles.css") + } + println("Hierarchy snapshot: ${hierarchySnapshot.absolutePath}") + } + + private fun saveFile(url: String, folder: String, name: String): File { + val response = client.newCall(Request.Builder().url(url).build()).execute() + return File(folder).apply { + mkdirs() + }.resolve(name).apply { + writeText(response.body?.string() ?: "") + } + } + + private fun BufferedImage.save(name: String) { + val bytes = ByteArrayOutputStream().use { b -> + ImageIO.write(this, "png", b) + b.toByteArray() + } + File("build/reports").apply { mkdirs() }.resolve("$name.png").writeBytes(bytes) + } + + private fun saveIdeaFrames(testName: String) { + remoteRobot.findAll(byXpath("//div[@class='IdeFrameImpl']")) + .forEachIndexed { n, frame -> + val pic = try { + frame.callJs( + """ + importPackage(java.io) + importPackage(javax.imageio) + importPackage(java.awt.image) + const screenShot = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_ARGB); + component.paint(screenShot.getGraphics()) + let pictureBytes; + const baos = new ByteArrayOutputStream(); + try { + ImageIO.write(screenShot, "png", baos); + pictureBytes = baos.toByteArray(); + } finally { + baos.close(); + } + pictureBytes; + """, true + ) + } catch (e: Throwable) { + e.printStackTrace() + throw e + } + pic.inputStream().use { + ImageIO.read(it) + }.save(testName + "_" + n) + } + } + + private fun fetchScreenShot(): BufferedImage { + return remoteRobot.callJs( + """ + importPackage(java.io) + importPackage(javax.imageio) + const screenShot = new java.awt.Robot().createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())); + let pictureBytes; + const baos = new ByteArrayOutputStream(); + try { + ImageIO.write(screenShot, "png", baos); + pictureBytes = baos.toByteArray(); + } finally { + baos.close(); + } + pictureBytes; + """ + ).inputStream().use { + ImageIO.read(it) + } + } +} + diff --git a/ui-tests/src/main/kotlin/org/move/ui/utils/StepsLogger.kt b/ui-tests/src/main/kotlin/org/move/ui/utils/StepsLogger.kt new file mode 100644 index 000000000..84d36f90e --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/utils/StepsLogger.kt @@ -0,0 +1,17 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +package org.move.ui.utils + +import com.intellij.remoterobot.stepsProcessing.StepLogger +import com.intellij.remoterobot.stepsProcessing.StepWorker + +object StepsLogger { + private var initializaed = false + @JvmStatic + fun init() { + if (initializaed.not()) { + StepWorker.registerProcessor(StepLogger()) + initializaed = true + } + } +} \ No newline at end of file diff --git a/ui-tests/src/test/kotlin/org/move/ui/IdeaFrameTest.kt b/ui-tests/src/test/kotlin/org/move/ui/IdeaFrameTest.kt new file mode 100644 index 000000000..d68e8e1a3 --- /dev/null +++ b/ui-tests/src/test/kotlin/org/move/ui/IdeaFrameTest.kt @@ -0,0 +1,23 @@ +package org.move.ui + +import com.intellij.remoterobot.RemoteRobot +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.move.ui.fixtures.DialogFixture +import org.move.ui.fixtures.ideaFrame +import org.move.ui.utils.RemoteRobotExtension + +@ExtendWith(RemoteRobotExtension::class) +class IdeaFrameTest { + + @Test + fun `run view function test`(remoteRobot: RemoteRobot) = with(remoteRobot) { + ideaFrame { + val editorGutter = textEditor().gutter + editorGutter.getIcons().first().click() + + } + + val paramsDialog = DialogFixture.byTitle("Edit Function Parameters") + } +} \ No newline at end of file diff --git a/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt b/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt new file mode 100644 index 000000000..8ded9b116 --- /dev/null +++ b/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt @@ -0,0 +1,186 @@ +package org.move.ui + +import com.intellij.openapi.util.io.toCanonicalPath +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.utils.waitFor +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.io.TempDir +import org.move.ui.fixtures.* +import org.move.ui.utils.RemoteRobotExtension +import org.move.ui.utils.StepsLogger +import java.io.File +import java.nio.file.Paths +import java.time.Duration + + +@ExtendWith(RemoteRobotExtension::class) +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class WelcomeFrameTest { + init { + StepsLogger.init() + } + + @TempDir + lateinit var tempFolder: File + + @Test + @Order(1) + fun `check new project validation`(remoteRobot: RemoteRobot) = with(remoteRobot) { + welcomeFrame { + newProjectButton(onStartup = false).click() + projectTypesList.findText("Move").click() + } + + // check radio buttons behaviour + welcomeFrame { + aptosRadioButton.select() + + assert(bundledAptosUnsupportedComment == null) + + bundledRadioButton.select() + assert(bundledRadioButton.isSelected()) + assert(!localPathTextField.isEnabled) { "Local path should be disabled if Bundled is selected" } + + waitFor(interval = Duration.ofSeconds(1)) { versionLabel.value.contains("aptos") } + assert(validationLabel == null) + + localRadioButton.select() + assert(localRadioButton.isSelected()) + assert(localPathTextField.isEnabled) { "Local path should be enabled if Local is selected" } + + waitFor(interval = Duration.ofSeconds(1)) { versionLabel.value.contains("N/A") } + assert(validationLabel?.value == "Invalid path to Aptos executable") + } + + welcomeFrame { + suiRadioButton.select() + + tryWithDelay { versionLabel.value.contains("N/A") } + tryWithDelay { validationLabel?.value == "Invalid path to Sui executable" } + } + + welcomeFrame { + cancelButton.click() + } + } + + @Test + @Order(2) + fun `create new aptos project with bundled cli`(remoteRobot: RemoteRobot) = with(remoteRobot) { + welcomeFrame { + newProjectButton(onStartup = false).click() + projectTypesList.findText("Move").click() + } + + val projectName = "aptos_project" + val projectPath = Paths.get(tempFolder.canonicalPath, projectName).toCanonicalPath() + + welcomeFrame { + aptosRadioButton.select() + bundledRadioButton.select() + + assert(projectLocationTextField.isEnabled) + + projectLocationTextField.text = projectPath + + createButton.click() + } + + ideaFrame { + assert(textEditor().editor.text.contains("[dependencies.AptosFramework]")) + + // TODO: check settings + + menuBar.select("File", "Close Project") + } + + welcomeFrame { + findText(projectName).rightClick() + jPopupMenu().menuItem("Remove from Recent Projects…").click() + + dialog("Remove Recent Project") { + button("Remove").click() + } + } + } + + @Test + @Order(2) + fun `create new aptos project with local cli`(remoteRobot: RemoteRobot) = with(remoteRobot) { + welcomeFrame { + newProjectButton(onStartup = false).click() + projectTypesList.findText("Move").click() + } + + val projectName = "aptos_project" + val projectPath = Paths.get(tempFolder.canonicalPath, projectName).toCanonicalPath() + + welcomeFrame { + aptosRadioButton.select() + + localRadioButton.select() + localPathTextField.text = "/home/mkurnikov/bin/aptos" + + projectLocationTextField.text = projectPath + createButton.click() + } + + ideaFrame { + assert(textEditor().editor.text.contains("[dependencies.AptosFramework]")) + + // TODO: check settings + + menuBar.select("File", "Close Project") + } + + welcomeFrame { + findText(projectName).rightClick() + jPopupMenu().menuItem("Remove from Recent Projects…").click() + + dialog("Remove Recent Project") { + button("Remove").click() + } + } + } + + @Test + @Order(3) + fun `create new sui project`(remoteRobot: RemoteRobot) = with(remoteRobot) { + welcomeFrame { + newProjectButton(onStartup = false).click() + projectTypesList.findText("Move").click() + } + + val projectName = "sui_project" + val projectPath = Paths.get(tempFolder.canonicalPath, projectName).toCanonicalPath() + + welcomeFrame { + suiRadioButton.select() + localPathTextField.text = "/home/mkurnikov/bin/sui" + projectLocationTextField.text = projectPath + createButton.click() + } + + ideaFrame { + assert(textEditor().editor.text.contains("https://github.com/MystenLabs/sui.git")) + + // TODO: check settings + + menuBar.select("File", "Close Project") + } + + welcomeFrame { + findText(projectName).rightClick() + jPopupMenu().menuItem("Remove from Recent Projects…").click() + + dialog("Remove Recent Project") { + button("Remove").click() + } + } + } +} \ No newline at end of file From b0a8b362da009196b48b828c726737a579694d13 Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Sat, 30 Mar 2024 13:58:00 +0300 Subject: [PATCH 04/14] refactor ui tests, test correct settings for new projects --- .../org/move/ui/fixtures/DialogFixture.kt | 6 + .../kotlin/org/move/ui/fixtures/IdeaFrame.kt | 19 +++ .../ui/fixtures/MoveSettingsPanelFixture.kt | 40 +++++ .../TextFieldWithBrowseButtonFixture.kt | 1 - .../org/move/ui/fixtures/WelcomeFrame.kt | 23 +-- .../kotlin/org/move/ui/WelcomeFrameTest.kt | 161 +++++++++++------- 6 files changed, 176 insertions(+), 74 deletions(-) create mode 100644 ui-tests/src/main/kotlin/org/move/ui/fixtures/MoveSettingsPanelFixture.kt diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt index a286ff75a..81e7e53f8 100644 --- a/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt @@ -31,6 +31,12 @@ class DialogFixture( fun byTitle(title: String) = byXpath("title $title", "//div[@title='$title' and @class='MyDialog']") } + fun configurableEditor( + timeout: Duration = Duration.ofSeconds(20), + function: CommonContainerFixture.() -> Unit = {} + ) = + find(byXpath("//div[@class='ConfigurableEditor']"), timeout).apply(function) + val title: String get() = callJs("component.getTitle();") } \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt index f2d91896a..8591c1404 100644 --- a/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt @@ -33,6 +33,25 @@ class IdeaFrame( return@step remoteRobot.find(JMenuBarFixture::class.java, JMenuBarFixture.byType()) } + fun openSettingsDialog() { + menuBar.select("File", "Settings...") + Thread.sleep(1000) + } + fun closeProject() { + menuBar.select("File", "Close Project") + } + + fun settingsDialog(function: DialogFixture.() -> Unit): DialogFixture = dialog("Settings", function = function) + + fun DialogFixture.openMoveSettings() { + val settingsTreeView = find(byXpath("//div[@class='SettingsTreeView']")) + settingsTreeView.findText("Languages & Frameworks").click() + + configurableEditor { + find(byXpath("//div[@text='Move Language']")).click() + } + } + @JvmOverloads fun dumbAware(timeout: Duration = Duration.ofMinutes(5), function: () -> Unit) { step("Wait for smart mode") { diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/MoveSettingsPanelFixture.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/MoveSettingsPanelFixture.kt new file mode 100644 index 000000000..1159fcbd7 --- /dev/null +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/MoveSettingsPanelFixture.kt @@ -0,0 +1,40 @@ +package org.move.ui.fixtures + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import java.time.Duration + +fun ContainerFixture.moveSettingsPanel( + timeout: Duration = Duration.ofSeconds(20), + function: MoveSettingsPanelFixture.() -> Unit = {} +): MoveSettingsPanelFixture = + step("Search for Move settings panel") { + find(MoveSettingsPanelFixture::class.java, timeout).apply(function) + } + +@FixtureName("MoveSettingsPanel") +@DefaultXpath("DialogPanel type", "//div[@class='DialogPanel']") +class MoveSettingsPanelFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +): + CommonContainerFixture(remoteRobot, remoteComponent) { + + val aptosRadioButton get() = radioButton("Aptos") + val suiRadioButton get() = radioButton("Sui") + + val bundledRadioButton get() = radioButton("Bundled") + val localRadioButton get() = radioButton("Local") + + val versionLabel get() = jLabel(byXpath("//div[@class='VersionLabel']"), timeout = Duration.ofSeconds(1)) + + val localPathTextField + get() = + textFieldWithBrowseButton(byXpath("//div[@class='TextFieldWithBrowseButton']")) +} \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/TextFieldWithBrowseButtonFixture.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/TextFieldWithBrowseButtonFixture.kt index 0933e9337..b86a2a0af 100644 --- a/ui-tests/src/main/kotlin/org/move/ui/fixtures/TextFieldWithBrowseButtonFixture.kt +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/TextFieldWithBrowseButtonFixture.kt @@ -21,7 +21,6 @@ fun CommonContainerFixture.textFieldWithBrowseButton(labelText: String): TextFie return textFieldWithBrowseButton(locator) } - class TextFieldWithBrowseButtonFixture( remoteRobot: RemoteRobot, remoteComponent: RemoteComponent diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt index 0a6201075..8fd77bf9b 100644 --- a/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt @@ -45,7 +45,11 @@ class WelcomeFrame( button(byXpath("//div[@class='JBOptionButton' and @text='New Project']")) } - val versionLabel get() = jLabel(byXpath("//div[@class='VersionLabel']"), timeout = Duration.ofSeconds(1)) + fun selectNewProjectType(type: String, onStartup: Boolean = false) { + newProjectButton(onStartup).click() + projectTypesList.findText(type).click() + } + val validationLabel: JLabelFixture? get() = findOrNull(JLabelFixture::class.java, byXpath("//div[@defaulticon='lightning.svg']")) @@ -53,15 +57,6 @@ class WelcomeFrame( val projectTypesList get() = find(ComponentFixture::class.java, byXpath("//div[@class='JBList']")) - val aptosRadioButton get() = radioButton("Aptos") - val suiRadioButton get() = radioButton("Sui") - - val bundledRadioButton get() = radioButton("Bundled") - val localRadioButton get() = radioButton("Local") - - val localPathTextField get() = - textFieldWithBrowseButton(byXpath("//div[@class='DialogPanel']//div[@class='TextFieldWithBrowseButton']")) - val projectLocationTextField get() = textFieldWithBrowseButton("Location:") // textField(byXpath("//div[@accessiblename='Location:' and @class='TextFieldWithBrowseButton']")) @@ -81,6 +76,14 @@ class WelcomeFrame( ) } + fun removeProjectFromRecents(projectName: String) { + findText(projectName).rightClick() + jPopupMenu().menuItem("Remove from Recent Projects…").click() + dialog("Remove Recent Project") { + button("Remove").click() + } + } + val createNewProjectLink get() = actionLink( byXpath( diff --git a/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt b/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt index 8ded9b116..cfea30d4b 100644 --- a/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt +++ b/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt @@ -3,7 +3,6 @@ package org.move.ui import com.intellij.openapi.util.io.toCanonicalPath import com.intellij.remoterobot.RemoteRobot import com.intellij.remoterobot.utils.waitFor -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.MethodOrderer import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test @@ -17,6 +16,8 @@ import java.io.File import java.nio.file.Paths import java.time.Duration +const val APTOS_LOCAL_PATH = "/home/mkurnikov/bin/aptos" +const val SUI_LOCAL_PATH = "/home/mkurnikov/bin/sui" @ExtendWith(RemoteRobotExtension::class) @TestMethodOrder(MethodOrderer.OrderAnnotation::class) @@ -32,36 +33,39 @@ class WelcomeFrameTest { @Order(1) fun `check new project validation`(remoteRobot: RemoteRobot) = with(remoteRobot) { welcomeFrame { - newProjectButton(onStartup = false).click() - projectTypesList.findText("Move").click() + selectNewProjectType("Move") } // check radio buttons behaviour welcomeFrame { - aptosRadioButton.select() + moveSettingsPanel { + aptosRadioButton.select() - assert(bundledAptosUnsupportedComment == null) + assert(bundledAptosUnsupportedComment == null) - bundledRadioButton.select() - assert(bundledRadioButton.isSelected()) - assert(!localPathTextField.isEnabled) { "Local path should be disabled if Bundled is selected" } + bundledRadioButton.select() + assert(bundledRadioButton.isSelected()) + assert(!localPathTextField.isEnabled) { "Local path should be disabled if Bundled is selected" } - waitFor(interval = Duration.ofSeconds(1)) { versionLabel.value.contains("aptos") } - assert(validationLabel == null) + waitFor(interval = Duration.ofSeconds(1)) { versionLabel.value.contains("aptos") } + assert(validationLabel == null) - localRadioButton.select() - assert(localRadioButton.isSelected()) - assert(localPathTextField.isEnabled) { "Local path should be enabled if Local is selected" } + localRadioButton.select() + assert(localRadioButton.isSelected()) + assert(localPathTextField.isEnabled) { "Local path should be enabled if Local is selected" } - waitFor(interval = Duration.ofSeconds(1)) { versionLabel.value.contains("N/A") } - assert(validationLabel?.value == "Invalid path to Aptos executable") + waitFor(interval = Duration.ofSeconds(1)) { versionLabel.value.contains("N/A") } + assert(validationLabel?.value == "Invalid path to Aptos executable") + } } welcomeFrame { - suiRadioButton.select() + moveSettingsPanel { + suiRadioButton.select() - tryWithDelay { versionLabel.value.contains("N/A") } - tryWithDelay { validationLabel?.value == "Invalid path to Sui executable" } + tryWithDelay { versionLabel.value.contains("N/A") } + tryWithDelay { validationLabel?.value == "Invalid path to Sui executable" } + } } welcomeFrame { @@ -73,39 +77,48 @@ class WelcomeFrameTest { @Order(2) fun `create new aptos project with bundled cli`(remoteRobot: RemoteRobot) = with(remoteRobot) { welcomeFrame { - newProjectButton(onStartup = false).click() - projectTypesList.findText("Move").click() + selectNewProjectType("Move") } val projectName = "aptos_project" val projectPath = Paths.get(tempFolder.canonicalPath, projectName).toCanonicalPath() welcomeFrame { - aptosRadioButton.select() - bundledRadioButton.select() - - assert(projectLocationTextField.isEnabled) - - projectLocationTextField.text = projectPath + moveSettingsPanel { + aptosRadioButton.select() + bundledRadioButton.select() + assert(projectLocationTextField.isEnabled) + projectLocationTextField.text = projectPath + } createButton.click() } ideaFrame { assert(textEditor().editor.text.contains("[dependencies.AptosFramework]")) - // TODO: check settings - - menuBar.select("File", "Close Project") + openSettingsDialog() + settingsDialog { + openMoveSettings() + configurableEditor { + moveSettingsPanel { + assert(aptosRadioButton.isSelected()) + assert(!suiRadioButton.isSelected()) + + assert(bundledRadioButton.isSelected()) + assert(!localRadioButton.isSelected()) + assert(!localPathTextField.isEnabled) + } + } + button("Cancel").click() + } } + ideaFrame { + closeProject() + } welcomeFrame { - findText(projectName).rightClick() - jPopupMenu().menuItem("Remove from Recent Projects…").click() - - dialog("Remove Recent Project") { - button("Remove").click() - } + removeProjectFromRecents(projectName) } } @@ -113,38 +126,51 @@ class WelcomeFrameTest { @Order(2) fun `create new aptos project with local cli`(remoteRobot: RemoteRobot) = with(remoteRobot) { welcomeFrame { - newProjectButton(onStartup = false).click() - projectTypesList.findText("Move").click() + selectNewProjectType("Move") } val projectName = "aptos_project" val projectPath = Paths.get(tempFolder.canonicalPath, projectName).toCanonicalPath() welcomeFrame { - aptosRadioButton.select() + moveSettingsPanel { + aptosRadioButton.select() - localRadioButton.select() - localPathTextField.text = "/home/mkurnikov/bin/aptos" + localRadioButton.select() + localPathTextField.text = APTOS_LOCAL_PATH - projectLocationTextField.text = projectPath + projectLocationTextField.text = projectPath + } createButton.click() } ideaFrame { assert(textEditor().editor.text.contains("[dependencies.AptosFramework]")) - // TODO: check settings + openSettingsDialog() + settingsDialog { + openMoveSettings() + configurableEditor { + moveSettingsPanel { + assert(aptosRadioButton.isSelected()) + assert(!suiRadioButton.isSelected()) + + assert(!bundledRadioButton.isSelected()) + assert(localRadioButton.isSelected()) + + assert(localPathTextField.text == APTOS_LOCAL_PATH) + } + } + button("Cancel").click() + } - menuBar.select("File", "Close Project") } + ideaFrame { + closeProject() + } welcomeFrame { - findText(projectName).rightClick() - jPopupMenu().menuItem("Remove from Recent Projects…").click() - - dialog("Remove Recent Project") { - button("Remove").click() - } + removeProjectFromRecents(projectName) } } @@ -152,35 +178,44 @@ class WelcomeFrameTest { @Order(3) fun `create new sui project`(remoteRobot: RemoteRobot) = with(remoteRobot) { welcomeFrame { - newProjectButton(onStartup = false).click() - projectTypesList.findText("Move").click() + selectNewProjectType("Move") } val projectName = "sui_project" val projectPath = Paths.get(tempFolder.canonicalPath, projectName).toCanonicalPath() welcomeFrame { - suiRadioButton.select() - localPathTextField.text = "/home/mkurnikov/bin/sui" - projectLocationTextField.text = projectPath + moveSettingsPanel { + suiRadioButton.select() + localPathTextField.text = SUI_LOCAL_PATH + projectLocationTextField.text = projectPath + } createButton.click() } ideaFrame { assert(textEditor().editor.text.contains("https://github.com/MystenLabs/sui.git")) - // TODO: check settings - - menuBar.select("File", "Close Project") + openSettingsDialog() + settingsDialog { + openMoveSettings() + configurableEditor { + moveSettingsPanel { + assert(!aptosRadioButton.isSelected()) + assert(suiRadioButton.isSelected()) + + assert(localPathTextField.text == SUI_LOCAL_PATH) + } + } + button("Cancel").click() + } } + ideaFrame { + closeProject() + } welcomeFrame { - findText(projectName).rightClick() - jPopupMenu().menuItem("Remove from Recent Projects…").click() - - dialog("Remove Recent Project") { - button("Remove").click() - } + removeProjectFromRecents(projectName) } } } \ No newline at end of file From 7c26e5636175f78e1cc20ebba4ab4e44f140bf5e Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Mon, 1 Apr 2024 13:16:31 +0300 Subject: [PATCH 05/14] fetch aptos/sui binary from PATH for project generator peer --- .../cli/settings/MvProjectSettingsService.kt | 15 +++++++++++- .../cli/settings/aptos/ChooseAptosCliPanel.kt | 3 ++- .../newProject/MoveProjectGeneratorPeer.kt | 7 ++++-- src/main/kotlin/org/move/utils/EnvUtils.kt | 23 +++++++++++++++++++ .../kotlin/org/move/ui/WelcomeFrameTest.kt | 7 ++++-- 5 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/org/move/utils/EnvUtils.kt diff --git a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt index 76895be3e..35ce20d31 100644 --- a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt +++ b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt @@ -11,12 +11,25 @@ import org.move.cli.settings.aptos.AptosExecType import org.move.stdext.exists import org.move.stdext.isExecutableFile import org.move.stdext.toPathOrNull +import org.move.utils.EnvUtils import java.nio.file.Path enum class Blockchain { APTOS, SUI; override fun toString(): String = if (this == APTOS) "Aptos" else "Sui" + + companion object { + fun aptosFromPATH(): String? { + // TODO: run --version and check whether it's a real Aptos CLI executable + return EnvUtils.findInPATH("aptos")?.toAbsolutePath()?.toString() + } + + fun suiFromPATH(): String? { + // TODO: same as in Aptos + return EnvUtils.findInPATH("sui")?.toAbsolutePath()?.toString() + } + } } val Project.moveSettings: MvProjectSettingsService get() = service() @@ -50,8 +63,8 @@ class MvProjectSettingsService( var blockchain: Blockchain by enum(Blockchain.APTOS) var aptosExecType: AptosExecType by enum(defaultAptosExecType) - var localAptosPath: String? by string() + var localAptosPath: String? by string() var localSuiPath: String? by string() var foldSpecs: Boolean by property(false) diff --git a/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt b/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt index c3a344230..6dc088234 100644 --- a/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt +++ b/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt @@ -24,6 +24,7 @@ enum class AptosExecType { // val isBundledSupportedForThePlatform: Boolean get() = false fun bundledPath(): String? = PluginPathManager.bundledAptosCli + fun aptosPath(execType: AptosExecType, localAptosPath: String?): String { return when (execType) { BUNDLED -> bundledPath() ?: "" @@ -97,7 +98,7 @@ class ChooseAptosCliPanel(versionUpdateListener: (() -> Unit)?): Disposable { .actionListener { _, _ -> updateVersion() } - cell(localPathField ) + cell(localPathField) .enabledIf(localRadioButton.selected) .align(AlignX.FILL) .resizableColumn() diff --git a/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt b/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt index 6f82db0b4..265c5cebc 100644 --- a/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt +++ b/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt @@ -38,9 +38,12 @@ class MoveProjectGeneratorPeer(val parentDisposable: Disposable): GeneratorPeerI val defaultProjectSettings = ProjectManager.getInstance().defaultProject.getService(MvProjectSettingsService::class.java) blockchain = defaultProjectSettings.blockchain + + val localAptosPath = defaultProjectSettings.localAptosPath ?: Blockchain.aptosFromPATH() + val localSuiPath = defaultProjectSettings.localSuiPath ?: Blockchain.suiFromPATH() chooseAptosCliPanel.data = - ChooseAptosCliPanel.Data(defaultProjectSettings.aptosExecType, defaultProjectSettings.localAptosPath) - chooseSuiCliPanel.data = ChooseSuiCliPanel.Data(defaultProjectSettings.localSuiPath) + ChooseAptosCliPanel.Data(defaultProjectSettings.aptosExecType, localAptosPath) + chooseSuiCliPanel.data = ChooseSuiCliPanel.Data(localSuiPath) } private var checkValid: Runnable? = null diff --git a/src/main/kotlin/org/move/utils/EnvUtils.kt b/src/main/kotlin/org/move/utils/EnvUtils.kt new file mode 100644 index 000000000..02264a9e5 --- /dev/null +++ b/src/main/kotlin/org/move/utils/EnvUtils.kt @@ -0,0 +1,23 @@ +package org.move.utils + +import org.move.stdext.exists +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +object EnvUtils { + fun findInPATH(binaryName: String): Path? { + val dirPaths = getEnv("PATH")?.split(":").orEmpty() + for (dirPath in dirPaths) { + val path = Paths.get(dirPath, binaryName) + if (path.exists()) { + return path + } + } + return null + } + + fun getEnv(name: String): String? = System.getenv(name) +} + diff --git a/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt b/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt index cfea30d4b..66168abbf 100644 --- a/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt +++ b/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt @@ -9,7 +9,10 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.io.TempDir -import org.move.ui.fixtures.* +import org.move.ui.fixtures.ideaFrame +import org.move.ui.fixtures.moveSettingsPanel +import org.move.ui.fixtures.tryWithDelay +import org.move.ui.fixtures.welcomeFrame import org.move.ui.utils.RemoteRobotExtension import org.move.ui.utils.StepsLogger import java.io.File @@ -31,7 +34,7 @@ class WelcomeFrameTest { @Test @Order(1) - fun `check new project validation`(remoteRobot: RemoteRobot) = with(remoteRobot) { + fun `new project validation`(remoteRobot: RemoteRobot) = with(remoteRobot) { welcomeFrame { selectNewProjectType("Move") } From c1b982151ceab72bd036c2d972ec3cd937c809bc Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Tue, 2 Apr 2024 14:15:36 +0300 Subject: [PATCH 06/14] detect blockchain type automatically on project creation --- .../MoveLangProjectOpenProcessor.kt | 31 +++++++++++++++++++ .../resources/META-INF/intellij-move-core.xml | 2 ++ 2 files changed, 33 insertions(+) diff --git a/src/main/kotlin/org/move/ide/newProject/MoveLangProjectOpenProcessor.kt b/src/main/kotlin/org/move/ide/newProject/MoveLangProjectOpenProcessor.kt index 866f00f85..9611c6702 100644 --- a/src/main/kotlin/org/move/ide/newProject/MoveLangProjectOpenProcessor.kt +++ b/src/main/kotlin/org/move/ide/newProject/MoveLangProjectOpenProcessor.kt @@ -1,15 +1,46 @@ package org.move.ide.newProject +import com.intellij.ide.util.RunOnceUtil import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.startup.StartupManager import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.readText import com.intellij.platform.PlatformProjectOpenProcessor import com.intellij.projectImport.ProjectOpenProcessor import org.move.cli.Consts +import org.move.cli.moveProjectsService +import org.move.cli.settings.Blockchain +import org.move.cli.settings.moveSettings import org.move.ide.MoveIcons +import org.move.openapiext.rootDir import javax.swing.Icon +class InferBlockchainTypeOnStartupActivity: ProjectActivity { + override suspend fun execute(project: Project) { + RunOnceUtil.runOnceForProject(project, ID) { + val moveToml = project.rootDir?.findChild("Move.toml") ?: return@runOnceForProject + val moveTomlText = moveToml.readText() + val blockchain = + when { + moveTomlText.contains("https://github.com/MystenLabs/sui.git") -> Blockchain.SUI + else -> Blockchain.APTOS + } + + if (project.moveSettings.blockchain != blockchain) { + project.moveSettings.modify { + it.blockchain = blockchain + } + } + } + } + + companion object { + const val ID = "org.move.InferBlockchainTypeOnStartupActivity" + } +} + /// called only when IDE opens a project from existing sources class MoveLangProjectOpenProcessor: ProjectOpenProcessor() { override val name: String get() = "Move" diff --git a/src/main/resources/META-INF/intellij-move-core.xml b/src/main/resources/META-INF/intellij-move-core.xml index fdb8b66ac..18bd2521c 100644 --- a/src/main/resources/META-INF/intellij-move-core.xml +++ b/src/main/resources/META-INF/intellij-move-core.xml @@ -284,6 +284,8 @@ + From ee0fec3bf5e926da11fb5a876a2b17ba5eb035ce Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Tue, 2 Apr 2024 14:16:09 +0300 Subject: [PATCH 07/14] add ui tests for project opening --- .../org/move/ui/fixtures/DialogFixture.kt | 12 +- .../kotlin/org/move/ui/fixtures/IdeaFrame.kt | 51 ++++- .../org/move/ui/fixtures/WelcomeFrame.kt | 94 +++++++-- ...{WelcomeFrameTest.kt => NewProjectTest.kt} | 195 ++++++++++++------ .../example-packages/aptos_package/Move.toml | 15 ++ .../aptos_package/sources/.gitkeep | 0 .../example-packages/sui_package/Move.toml | 38 ++++ .../sui_package/sources/.gitkeep | 0 8 files changed, 326 insertions(+), 79 deletions(-) rename ui-tests/src/test/kotlin/org/move/ui/{WelcomeFrameTest.kt => NewProjectTest.kt} (51%) create mode 100644 ui-tests/src/test/resources/example-packages/aptos_package/Move.toml create mode 100644 ui-tests/src/test/resources/example-packages/aptos_package/sources/.gitkeep create mode 100644 ui-tests/src/test/resources/example-packages/sui_package/Move.toml create mode 100644 ui-tests/src/test/resources/example-packages/sui_package/sources/.gitkeep diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt index 81e7e53f8..91cf5721c 100644 --- a/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/DialogFixture.kt @@ -21,7 +21,7 @@ fun ContainerFixture.dialog( } @FixtureName("Dialog") -class DialogFixture( +open class DialogFixture( remoteRobot: RemoteRobot, remoteComponent: RemoteComponent ): CommonContainerFixture(remoteRobot, remoteComponent) { @@ -39,4 +39,14 @@ class DialogFixture( val title: String get() = callJs("component.getTitle();") +} + +class SettingsDialogFixture( + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +): + DialogFixture(remoteRobot, remoteComponent) { + + + } \ No newline at end of file diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt index 8591c1404..964ed274d 100644 --- a/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt @@ -7,8 +7,10 @@ import com.intellij.remoterobot.data.RemoteComponent import com.intellij.remoterobot.fixtures.* import com.intellij.remoterobot.search.locators.byXpath import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.Locators import com.intellij.remoterobot.utils.waitFor import java.time.Duration +import javax.swing.JMenu fun RemoteRobot.ideaFrame(function: IdeaFrame.() -> Unit) { find(timeout = Duration.ofSeconds(10)).apply(function) @@ -30,28 +32,67 @@ class IdeaFrame( val menuBar: JMenuBarFixture get() = step("Menu...") { - return@step remoteRobot.find(JMenuBarFixture::class.java, JMenuBarFixture.byType()) + return@step remoteRobot.find( + JMenuBarFixture::class.java, JMenuBarFixture.byType(), Duration.ofSeconds(5)) } - fun openSettingsDialog() { + private fun openSettingsDialog() { + if (!remoteRobot.isMac()) { + waitFor { + findAll( + Locators.byTypeAndProperties(JMenu::class.java, Locators.XpathProperty.ACCESSIBLE_NAME to "File") + ) + .isNotEmpty() + } + } menuBar.select("File", "Settings...") - Thread.sleep(1000) + waitFor { + findAll(DialogFixture.byTitle("Settings")).isNotEmpty() + } } + fun closeProject() { menuBar.select("File", "Close Project") } fun settingsDialog(function: DialogFixture.() -> Unit): DialogFixture = dialog("Settings", function = function) - fun DialogFixture.openMoveSettings() { + fun DialogFixture.selectMoveSettings() { val settingsTreeView = find(byXpath("//div[@class='SettingsTreeView']")) settingsTreeView.findText("Languages & Frameworks").click() - configurableEditor { find(byXpath("//div[@text='Move Language']")).click() } } +// fun openSettings(function: SettingsDialogFixture.() -> Unit) { +// if (!remoteRobot.isMac()) { +// waitFor { +// findAll( +// Locators.byTypeAndProperties(JMenu::class.java, Locators.XpathProperty.ACCESSIBLE_NAME to "File") +// ) +// .isNotEmpty() +// } +// } +// menuBar.select("File", "Settings...") +// step("Search for dialog with title 'Settings'") { +// find( +// DialogFixture.byTitle("Settings"), Duration.ofSeconds(2)).apply(function) +// } +// +// } + + fun openMoveSettings(function: MoveSettingsPanelFixture.() -> Unit) { + openSettingsDialog() + settingsDialog { + selectMoveSettings() + configurableEditor { + moveSettingsPanel(function = function) + } + button("OK").click() + } + } + @JvmOverloads fun dumbAware(timeout: Duration = Duration.ofMinutes(5), function: () -> Unit) { step("Wait for smart mode") { diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt index 8fd77bf9b..6448b6ba9 100644 --- a/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt @@ -8,21 +8,35 @@ import com.intellij.remoterobot.data.RemoteComponent import com.intellij.remoterobot.fixtures.* import com.intellij.remoterobot.search.locators.Locator import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step import com.intellij.remoterobot.utils.Locators +import com.intellij.remoterobot.utils.keyboard import com.intellij.remoterobot.utils.waitFor import com.intellij.ui.dsl.builder.components.DslLabel +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths import java.time.Duration +import javax.swing.JMenu +import kotlin.math.abs fun RemoteRobot.welcomeFrame(function: WelcomeFrame.() -> Unit) { find(WelcomeFrame::class.java, Duration.ofSeconds(10)).apply(function) } -fun tryWithDelay(delay: Long = 1, condition: () -> Boolean) { - waitFor( - duration = Duration.ofSeconds(delay), - interval = Duration.ofSeconds(delay), - condition = condition - ) +fun RemoteRobot.openProject(projectPath: Path) { + welcomeFrame { + openProjectAt(projectPath) + } +// ideaFrame { +// find( +// Locators.byTypeAndProperties( +// JMenu::class.java, +// Locators.XpathProperty.ACCESSIBLE_NAME to "File" +// ), +// timeout = Duration.ofSeconds(5) +// ) +// } } fun SearchContext.findOrNull(type: Class, locator: Locator) = @@ -36,17 +50,37 @@ class WelcomeFrame( ): CommonContainerFixture(remoteRobot, remoteComponent) { - fun newProjectButton(onStartup: Boolean = true) = - if (onStartup) { - // clean IDE - button(byXpath("//div[@defaulticon='createNewProjectTab.svg']"), Duration.ofSeconds(2)) - } else { - // projects opened before - button(byXpath("//div[@class='JBOptionButton' and @text='New Project']")) + val newProjectButton: JButtonFixture + get() { + val cleanStartupButton = + findOrNull(JButtonFixture::class.java, byXpath("//div[@defaulticon='createNewProjectTab.svg']")) + if (cleanStartupButton != null) { + return cleanStartupButton + } + + return button( + byXpath("//div[@class='JBOptionButton' and @text='New Project']"), + timeout = Duration.ofSeconds(2) + ) + } + + val openProjectButton: JButtonFixture + get() { + val cleanStartupButton = + findOrNull(JButtonFixture::class.java, byXpath("//div[@defaulticon='open.svg']")) + if (cleanStartupButton != null) { + return cleanStartupButton + } + + return button( + byXpath("//div[@class='JBOptionButton' and @text='Open']"), + timeout = Duration.ofSeconds(2) + ) } - fun selectNewProjectType(type: String, onStartup: Boolean = false) { - newProjectButton(onStartup).click() + + fun selectNewProjectType(type: String) { + newProjectButton.click() projectTypesList.findText(type).click() } @@ -76,6 +110,36 @@ class WelcomeFrame( ) } + fun openProjectAt(path: Path) { + openProjectButton.click() + dialog("Open File or Project") { + val fileTree = jTree() + fileTree.collapsePath("/") + fileTree.clickPath("/") + + val pathTextField = textField(byXpath("//div[@class='BorderlessTextField']")) + pathTextField.click() + + val absPath = path.toAbsolutePath().toString().drop(1) + keyboard { + enterText(absPath, 10) + } + + val treePath = arrayOf("/") + absPath.split(File.separator).toTypedArray() + waitFor { + fileTree.isPathExists(*treePath) + } + fileTree.clickPath(*treePath) + + Thread.sleep(300) + button("OK").click() + } + } + + fun openRecentProject(projectName: String) { + findText(projectName).click() + } + fun removeProjectFromRecents(projectName: String) { findText(projectName).rightClick() jPopupMenu().menuItem("Remove from Recent Projects…").click() diff --git a/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt b/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt similarity index 51% rename from ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt rename to ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt index 66168abbf..dee205107 100644 --- a/ui-tests/src/test/kotlin/org/move/ui/WelcomeFrameTest.kt +++ b/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt @@ -2,6 +2,9 @@ package org.move.ui import com.intellij.openapi.util.io.toCanonicalPath import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.fixtures.ComponentFixture +import com.intellij.remoterobot.steps.CommonSteps +import com.intellij.remoterobot.utils.Locators import com.intellij.remoterobot.utils.waitFor import org.junit.jupiter.api.MethodOrderer import org.junit.jupiter.api.Order @@ -9,22 +12,21 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.io.TempDir -import org.move.ui.fixtures.ideaFrame -import org.move.ui.fixtures.moveSettingsPanel -import org.move.ui.fixtures.tryWithDelay -import org.move.ui.fixtures.welcomeFrame +import org.move.ui.fixtures.* import org.move.ui.utils.RemoteRobotExtension import org.move.ui.utils.StepsLogger import java.io.File +import java.nio.file.Path import java.nio.file.Paths import java.time.Duration +import javax.swing.JMenu const val APTOS_LOCAL_PATH = "/home/mkurnikov/bin/aptos" const val SUI_LOCAL_PATH = "/home/mkurnikov/bin/sui" @ExtendWith(RemoteRobotExtension::class) @TestMethodOrder(MethodOrderer.OrderAnnotation::class) -class WelcomeFrameTest { +class NewProjectTest { init { StepsLogger.init() } @@ -32,6 +34,18 @@ class WelcomeFrameTest { @TempDir lateinit var tempFolder: File + private fun getResourcesDir(): Path { + return Paths.get("").toAbsolutePath() + .resolve("src").resolve("test").resolve("resources") + } + + private fun getExamplePackagesDir() = getResourcesDir().resolve("example-packages") + private fun copyExamplePackageToTempFolder(packageName: String) { + val tempPackagePath = tempFolder.toPath().resolve(packageName) + getExamplePackagesDir().resolve(packageName).toFile().copyRecursively(tempPackagePath.toFile()) + Thread.sleep(500) + } + @Test @Order(1) fun `new project validation`(remoteRobot: RemoteRobot) = with(remoteRobot) { @@ -55,10 +69,13 @@ class WelcomeFrameTest { localRadioButton.select() assert(localRadioButton.isSelected()) + assert(localPathTextField.isEnabled) { "Local path should be enabled if Local is selected" } + localPathTextField.text = "" + Thread.sleep(1000) - waitFor(interval = Duration.ofSeconds(1)) { versionLabel.value.contains("N/A") } - assert(validationLabel?.value == "Invalid path to Aptos executable") + waitFor { versionLabel.value.contains("N/A") } + waitFor { validationLabel?.value == "Invalid path to Aptos executable" } } } @@ -66,8 +83,11 @@ class WelcomeFrameTest { moveSettingsPanel { suiRadioButton.select() - tryWithDelay { versionLabel.value.contains("N/A") } - tryWithDelay { validationLabel?.value == "Invalid path to Sui executable" } + localPathTextField.text = "" + Thread.sleep(1000) + + waitFor { versionLabel.value.contains("N/A") } + waitFor { validationLabel?.value == "Invalid path to Sui executable" } } } @@ -100,26 +120,17 @@ class WelcomeFrameTest { ideaFrame { assert(textEditor().editor.text.contains("[dependencies.AptosFramework]")) - openSettingsDialog() - settingsDialog { - openMoveSettings() - configurableEditor { - moveSettingsPanel { - assert(aptosRadioButton.isSelected()) - assert(!suiRadioButton.isSelected()) - - assert(bundledRadioButton.isSelected()) - assert(!localRadioButton.isSelected()) - assert(!localPathTextField.isEnabled) - } - } - button("Cancel").click() + openMoveSettings { + assert(aptosRadioButton.isSelected()) + assert(!suiRadioButton.isSelected()) + + assert(bundledRadioButton.isSelected()) + assert(!localRadioButton.isSelected()) + assert(!localPathTextField.isEnabled) } } - ideaFrame { - closeProject() - } + CommonSteps(remoteRobot).closeProject() welcomeFrame { removeProjectFromRecents(projectName) } @@ -150,28 +161,18 @@ class WelcomeFrameTest { ideaFrame { assert(textEditor().editor.text.contains("[dependencies.AptosFramework]")) - openSettingsDialog() - settingsDialog { - openMoveSettings() - configurableEditor { - moveSettingsPanel { - assert(aptosRadioButton.isSelected()) - assert(!suiRadioButton.isSelected()) - - assert(!bundledRadioButton.isSelected()) - assert(localRadioButton.isSelected()) - - assert(localPathTextField.text == APTOS_LOCAL_PATH) - } - } - button("Cancel").click() - } + openMoveSettings { + assert(aptosRadioButton.isSelected()) + assert(!suiRadioButton.isSelected()) - } + assert(!bundledRadioButton.isSelected()) + assert(localRadioButton.isSelected()) - ideaFrame { - closeProject() + assert(localPathTextField.text == APTOS_LOCAL_PATH) + } } + + CommonSteps(remoteRobot).closeProject() welcomeFrame { removeProjectFromRecents(projectName) } @@ -199,18 +200,30 @@ class WelcomeFrameTest { ideaFrame { assert(textEditor().editor.text.contains("https://github.com/MystenLabs/sui.git")) - openSettingsDialog() - settingsDialog { - openMoveSettings() - configurableEditor { - moveSettingsPanel { - assert(!aptosRadioButton.isSelected()) - assert(suiRadioButton.isSelected()) - - assert(localPathTextField.text == SUI_LOCAL_PATH) - } - } - button("Cancel").click() + openMoveSettings { + assert(!aptosRadioButton.isSelected()) + assert(suiRadioButton.isSelected()) + assert(localPathTextField.text == SUI_LOCAL_PATH) + } + } + + CommonSteps(remoteRobot).closeProject() + welcomeFrame { + removeProjectFromRecents(projectName) + } + } + + @Test + fun `import existing aptos package`(remoteRobot: RemoteRobot) = with(remoteRobot) { + copyExamplePackageToTempFolder("aptos_package") + + val tempPackagePath = tempFolder.toPath().resolve("aptos_package") + openProject(tempPackagePath) + + ideaFrame { + openMoveSettings { + assert(aptosRadioButton.isSelected()) + assert(!suiRadioButton.isSelected()) } } @@ -218,7 +231,73 @@ class WelcomeFrameTest { closeProject() } welcomeFrame { - removeProjectFromRecents(projectName) + removeProjectFromRecents("aptos_package") + } + } + + @Test + fun `import existing sui package`(remoteRobot: RemoteRobot) = with(remoteRobot) { + copyExamplePackageToTempFolder("sui_package") + + val projectPath = tempFolder.toPath().resolve("sui_package") + openProject(projectPath) + + ideaFrame { + openMoveSettings { + assert(!aptosRadioButton.isSelected()) + assert(suiRadioButton.isSelected()) + } + } + + CommonSteps(remoteRobot).closeProject() + welcomeFrame { + removeProjectFromRecents("sui_package") + } + } + + @Test + @Order(2) + fun `explicit sui blockchain setting should retain even if wrong`(remoteRobot: RemoteRobot) = with(remoteRobot) { + copyExamplePackageToTempFolder("aptos_package") + + // opens as Aptos package + val projectPath = tempFolder.toPath().resolve("aptos_package") + welcomeFrame { + openProjectAt(projectPath) + } + + // mark project as Sui + ideaFrame { + openMoveSettings { + suiRadioButton.select() + } + } + + // reopen project to see that no ProjectActivity or OpenProcessor changed the setting + ideaFrame { + closeProject() + } + welcomeFrame { + openRecentProject("aptos_package") + } + + ideaFrame { + openMoveSettings { + assert(!aptosRadioButton.isSelected()) + assert(suiRadioButton.isSelected()) + } + } + + CommonSteps(remoteRobot).closeProject() + welcomeFrame { + removeProjectFromRecents("aptos_package") } } + + // TODO +// @Test +// fun `no default compile configuration should be created in pycharm`(remoteRobot: RemoteRobot) = with(remoteRobot) { +// +// +// } } \ No newline at end of file diff --git a/ui-tests/src/test/resources/example-packages/aptos_package/Move.toml b/ui-tests/src/test/resources/example-packages/aptos_package/Move.toml new file mode 100644 index 000000000..822e35b0f --- /dev/null +++ b/ui-tests/src/test/resources/example-packages/aptos_package/Move.toml @@ -0,0 +1,15 @@ +[package] +name = "MyAptosPackage" +version = "1.0.0" +authors = [] + +[addresses] + +[dev-addresses] + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-core.git" +rev = "mainnet" +subdir = "aptos-move/framework/aptos-framework" + +[dev-dependencies] diff --git a/ui-tests/src/test/resources/example-packages/aptos_package/sources/.gitkeep b/ui-tests/src/test/resources/example-packages/aptos_package/sources/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ui-tests/src/test/resources/example-packages/sui_package/Move.toml b/ui-tests/src/test/resources/example-packages/sui_package/Move.toml new file mode 100644 index 000000000..ca3b2e85e --- /dev/null +++ b/ui-tests/src/test/resources/example-packages/sui_package/Move.toml @@ -0,0 +1,38 @@ +[package] +name = "MySuiPackage" + +# edition = "2024.alpha" # To use the Move 2024 edition, currently in alpha +# license = "" # e.g., "MIT", "GPL", "Apache 2.0" +# authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } + +# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. +# Revision can be a branch, a tag, and a commit hash. +# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } + +# For local dependencies use `local = path`. Path is relative to the package root +# Local = { local = "../path/to" } + +# To resolve a version conflict and force a specific version for dependency +# override use `override = true` +# Override = { local = "../conflicting/version", override = true } + +[addresses] +mysuipackage = "0x0" + +# Named addresses will be accessible in Move as `@name`. They're also exported: +# for example, `std = "0x1"` is exported by the Standard Library. +# alice = "0xA11CE" + +[dev-dependencies] +# The dev-dependencies section allows overriding dependencies for `--test` and +# `--dev` modes. You can introduce test-only dependencies here. +# Local = { local = "../path/to/dev-build" } + +[dev-addresses] +# The dev-addresses section allows overwriting named addresses for the `--test` +# and `--dev` modes. +# alice = "0xB0B" + diff --git a/ui-tests/src/test/resources/example-packages/sui_package/sources/.gitkeep b/ui-tests/src/test/resources/example-packages/sui_package/sources/.gitkeep new file mode 100644 index 000000000..e69de29bb From 77505a4795efa13afd43ca4e8bbc3aca01d51934 Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Tue, 2 Apr 2024 19:29:06 +0300 Subject: [PATCH 08/14] require restart on plugin update --- plugin/src/main/resources/META-INF/plugin.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/resources/META-INF/plugin.xml b/plugin/src/main/resources/META-INF/plugin.xml index e31c816c6..3f36a544b 100644 --- a/plugin/src/main/resources/META-INF/plugin.xml +++ b/plugin/src/main/resources/META-INF/plugin.xml @@ -1,4 +1,7 @@ - + + org.move.lang Move Language From f43f3ee8ffb4458620aee44dcfeb24598332fa01 Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Tue, 2 Apr 2024 19:39:21 +0300 Subject: [PATCH 09/14] bump to 1.36.0 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 30e9d19aa..ea1927b99 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ fun prop(name: String): String = ?: error("Property `$name` is not defined in gradle.properties for environment `$shortPlatformVersion`") val shortPlatformVersion = prop("shortPlatformVersion") -val codeVersion = "1.35.0" +val codeVersion = "1.36.0" val pluginVersion = "$codeVersion.$shortPlatformVersion" val pluginGroup = "org.move" val javaVersion = JavaVersion.VERSION_17 From b2856729cfbbaee2a904012ac0351933a1a621d5 Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Wed, 3 Apr 2024 13:51:47 +0300 Subject: [PATCH 10/14] rename projectAware -> externalSystem --- .../MoveExternalSystemProjectAware.kt | 0 .../{projectAware => externalSystem}/MoveSettingsFilesService.kt | 0 .../cli/{projectAware => externalSystem}/ImportProjectTest.kt | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/main/kotlin/org/move/cli/{projectAware => externalSystem}/MoveExternalSystemProjectAware.kt (100%) rename src/main/kotlin/org/move/cli/{projectAware => externalSystem}/MoveSettingsFilesService.kt (100%) rename src/test/kotlin/org/move/cli/{projectAware => externalSystem}/ImportProjectTest.kt (100%) diff --git a/src/main/kotlin/org/move/cli/projectAware/MoveExternalSystemProjectAware.kt b/src/main/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAware.kt similarity index 100% rename from src/main/kotlin/org/move/cli/projectAware/MoveExternalSystemProjectAware.kt rename to src/main/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAware.kt diff --git a/src/main/kotlin/org/move/cli/projectAware/MoveSettingsFilesService.kt b/src/main/kotlin/org/move/cli/externalSystem/MoveSettingsFilesService.kt similarity index 100% rename from src/main/kotlin/org/move/cli/projectAware/MoveSettingsFilesService.kt rename to src/main/kotlin/org/move/cli/externalSystem/MoveSettingsFilesService.kt diff --git a/src/test/kotlin/org/move/cli/projectAware/ImportProjectTest.kt b/src/test/kotlin/org/move/cli/externalSystem/ImportProjectTest.kt similarity index 100% rename from src/test/kotlin/org/move/cli/projectAware/ImportProjectTest.kt rename to src/test/kotlin/org/move/cli/externalSystem/ImportProjectTest.kt From 83531409f4fbab55f3b4043798efe41b3ee29c03 Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Wed, 3 Apr 2024 17:36:46 +0300 Subject: [PATCH 11/14] add auto-import with external system integration api --- .../org/move/cli/MoveProjectsService.kt | 111 +++++++-- .../MoveExternalSystemProjectAware.kt | 114 +++++---- .../MoveSettingsFilesService.kt | 26 +- .../cli/settings/MvProjectSettingsService.kt | 4 + .../settings/MvProjectSettingsServiceBase.kt | 10 +- .../org/move/utils/tests/MvProjectTestBase.kt | 21 ++ .../move/utils/tests/RunConfigurationUtils.kt | 111 +++++++++ .../resources/META-INF/intellij-move-core.xml | 2 +- .../cli/externalSystem/ImportProjectTest.kt | 2 +- .../MoveExternalSystemProjectAwareTest.kt | 228 ++++++++++++++++++ .../test/kotlin/org/move/ui/IdeaFrameTest.kt | 23 -- 11 files changed, 532 insertions(+), 120 deletions(-) create mode 100644 src/main/kotlin/org/move/utils/tests/RunConfigurationUtils.kt create mode 100644 src/test/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAwareTest.kt delete mode 100644 ui-tests/src/test/kotlin/org/move/ui/IdeaFrameTest.kt diff --git a/src/main/kotlin/org/move/cli/MoveProjectsService.kt b/src/main/kotlin/org/move/cli/MoveProjectsService.kt index d8286fbe3..9b6a524ea 100644 --- a/src/main/kotlin/org/move/cli/MoveProjectsService.kt +++ b/src/main/kotlin/org/move/cli/MoveProjectsService.kt @@ -7,7 +7,10 @@ import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.externalSystem.autoimport.AutoImportProjectTracker +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTracker import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.project.Project import com.intellij.openapi.project.ex.ProjectEx import com.intellij.openapi.roots.ModuleRootModificationUtil @@ -24,6 +27,7 @@ import com.intellij.psi.PsiFile import com.intellij.psi.PsiInvalidElementAccessException import com.intellij.psi.util.parents import com.intellij.util.messages.Topic +import org.move.cli.externalSystem.MoveExternalSystemProjectAware import org.move.cli.settings.MvProjectSettingsServiceBase.* import org.move.cli.settings.MvProjectSettingsServiceBase.Companion.MOVE_SETTINGS_TOPIC import org.move.cli.settings.debugErrorOrFallback @@ -39,6 +43,7 @@ import org.move.stdext.FileToMoveProjectCache import java.io.File import java.nio.file.Path import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionException val Project.moveProjectsService get() = service() @@ -46,34 +51,35 @@ val Project.hasMoveProject get() = this.moveProjectsService.allProjects.isNotEmp class MoveProjectsService(val project: Project): Disposable { - private val refreshOnBuildDirChangeWatcher = - BuildDirectoryWatcher(emptyList()) { scheduleProjectsRefresh("build/ directory changed") } +// private val refreshOnBuildDirChangeWatcher = +// BuildDirectoryWatcher(emptyList()) { scheduleProjectsRefresh("build/ directory changed") } var initialized = false init { - with(project.messageBus.connect()) { - if (!isUnitTestMode) { - subscribe(VirtualFileManager.VFS_CHANGES, refreshOnBuildDirChangeWatcher) - subscribe(VirtualFileManager.VFS_CHANGES, MoveTomlWatcher { - // on every Move.toml change - // TODO: move to External system integration - scheduleProjectsRefresh("Move.toml changed") - }) - } - subscribe( - MOVE_SETTINGS_TOPIC, - object: MoveSettingsListener { - override fun > settingsChanged(e: SettingsChangedEventBase) { - // on every Move Language plugin settings change - scheduleProjectsRefresh("plugin settings changed") - } - }) - } + registerProjectAware(project, this) + +// with(project.messageBus.connect()) { +// if (!isUnitTestMode) { +// subscribe(VirtualFileManager.VFS_CHANGES, refreshOnBuildDirChangeWatcher) +//// subscribe(VirtualFileManager.VFS_CHANGES, MoveTomlWatcher { +//// // on every Move.toml change +//// // TODO: move to External system integration +//// scheduleProjectsRefresh("Move.toml changed") +//// }) +// } +// subscribe( +// MOVE_SETTINGS_TOPIC, +// object: MoveSettingsListener { +// override fun > settingsChanged(e: SettingsChangedEventBase) { +// // on every Move Language plugin settings change +// scheduleProjectsRefresh("plugin settings changed") +// } +// }) +// } } - val allProjects: List - get() = this.projects.state + val allProjects: List get() = this.projects.state fun scheduleProjectsRefresh(reason: String? = null): CompletableFuture> { LOG.logProjectsRefresh("scheduled", reason) @@ -84,6 +90,32 @@ class MoveProjectsService(val project: Project): Disposable { return moveProjectsFut } + private fun registerProjectAware(project: Project, disposable: Disposable) { + // There is no sense to register `CargoExternalSystemProjectAware` for default project. + // Moreover, it may break searchable options building. + // Also, we don't need to register `CargoExternalSystemProjectAware` in light tests because: + // - we check it only in heavy tests + // - it heavily depends on service disposing which doesn't work in light tests + if (project.isDefault || isUnitTestMode && (project as? ProjectEx)?.isLight == true) return + + val moveProjectAware = MoveExternalSystemProjectAware(project) + val projectTracker = ExternalSystemProjectTracker.getInstance(project) + projectTracker.register(moveProjectAware, disposable) + projectTracker.activate(moveProjectAware.projectId) + + @Suppress("UnstableApiUsage") + project.messageBus.connect(disposable) + .subscribe(MOVE_SETTINGS_TOPIC, object : MoveSettingsListener { + override fun > settingsChanged(e: SettingsChangedEventBase) { + if (e.affectsMoveProjectsMetadata) { + val tracker = AutoImportProjectTracker.getInstance(project) + tracker.markDirty(moveProjectAware.projectId) + tracker.scheduleProjectRefresh() + } + } + }) + } + // requires ReadAccess fun findMoveProjectForPsiElement(psiElement: PsiElement): MoveProject? { // read access required for the psiElement.containingFile @@ -182,12 +214,15 @@ class MoveProjectsService(val project: Project): Disposable { private fun modifyProjectModel( updater: (List) -> CompletableFuture> ): CompletableFuture> { + val refreshStatusPublisher = project.messageBus.syncPublisher(MoveProjectsService.MOVE_PROJECTS_REFRESH_TOPIC) + val wrappedUpdater = { projects: List -> + refreshStatusPublisher.onRefreshStarted() updater(projects) } return projects.updateAsync(wrappedUpdater) .thenApply { projects -> - refreshOnBuildDirChangeWatcher.setWatchedProjects(projects) +// refreshOnBuildDirChangeWatcher.setWatchedProjects(projects) invokeAndWaitIfNeeded { runWriteAction { // remove file -> moveproject associations from cache @@ -210,6 +245,19 @@ class MoveProjectsService(val project: Project): Disposable { } projects } + .handle { projects, err -> + val status = err?.toRefreshStatus() ?: MoveRefreshStatus.SUCCESS + refreshStatusPublisher.onRefreshFinished(status) + projects + } + } + + private fun Throwable.toRefreshStatus(): MoveRefreshStatus { + return when { + this is ProcessCanceledException -> MoveRefreshStatus.CANCEL + this is CompletionException && cause is ProcessCanceledException -> MoveRefreshStatus.CANCEL + else -> MoveRefreshStatus.FAILURE + } } override fun dispose() {} @@ -221,11 +269,28 @@ class MoveProjectsService(val project: Project): Disposable { "move projects changes", MoveProjectsListener::class.java ) + + val MOVE_PROJECTS_REFRESH_TOPIC: Topic = Topic( + "Move Projects refresh", + MoveProjectsRefreshListener::class.java + ) + } fun interface MoveProjectsListener { fun moveProjectsUpdated(service: MoveProjectsService, projects: Collection) } + + interface MoveProjectsRefreshListener { + fun onRefreshStarted() + fun onRefreshFinished(status: MoveRefreshStatus) + } + + enum class MoveRefreshStatus { + SUCCESS, + FAILURE, + CANCEL + } } fun setupProjectRoots(project: Project, moveProjects: List) { diff --git a/src/main/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAware.kt b/src/main/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAware.kt index 490da3e4b..b0be7d9ed 100644 --- a/src/main/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAware.kt +++ b/src/main/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAware.kt @@ -1,60 +1,54 @@ -package org.move.cli.projectAware - -//class MoveExternalSystemProjectAware( -// private val project: Project -//) : ExternalSystemProjectAware { -// -// override val projectId: ExternalSystemProjectId = ExternalSystemProjectId(MOVE_SYSTEM_ID, project.name) -// -// override val settingsFiles: Set -// get() { -// val settingsFilesService = MoveSettingsFilesService.getInstance(project) -// // Always collect fresh settings files -// return settingsFilesService.collectSettingsFiles().toSet() -// } -// -// override fun reloadProject(context: ExternalSystemProjectReloadContext) { -// FileDocumentManager.getInstance().saveAllDocuments() -// project.moveProjects.refreshAllProjects() -// } -// -// override fun subscribe(listener: ExternalSystemProjectListener, parentDisposable: Disposable) { -// project.messageBus.connect(parentDisposable).subscribe( -// MoveProjectsService.MOVE_PROJECTS_REFRESH_TOPIC, -// object : MoveProjectsService.MoveProjectsRefreshListener { -// override fun onRefreshStarted() { -// listener.onProjectReloadStart() -// } -// -// override fun onRefreshFinished(status: MoveProjectsService.MoveRefreshStatus) { -// val externalStatus = when (status) { -// MoveProjectsService.MoveRefreshStatus.SUCCESS -> ExternalSystemRefreshStatus.SUCCESS -// MoveProjectsService.MoveRefreshStatus.FAILURE -> ExternalSystemRefreshStatus.FAILURE -// MoveProjectsService.MoveRefreshStatus.CANCEL -> ExternalSystemRefreshStatus.CANCEL -// } -// listener.onProjectReloadFinish(externalStatus) -// } -// } -// ) -// } -// -// companion object { -// val MOVE_SYSTEM_ID: ProjectSystemId = ProjectSystemId("Move") -// -// fun register(project: Project, disposable: Disposable) { -// val moveProjectAware = MoveExternalSystemProjectAware(project) -// val projectTracker = ExternalSystemProjectTracker.getInstance(project) -// projectTracker.register(moveProjectAware, disposable) -// projectTracker.activate(moveProjectAware.projectId) -// -// project.messageBus.connect(disposable) -// .subscribe(MoveProjectSettingsService.MOVE_SETTINGS_TOPIC, -// object : MoveSettingsListener { -// override fun moveSettingsChanged(e: MoveSettingsChangedEvent) { -// AutoImportProjectTracker.getInstance(project) -// .markDirty(moveProjectAware.projectId) -// } -// }) -// } -// } -//} +package org.move.cli.externalSystem + +import com.intellij.openapi.Disposable +import com.intellij.openapi.externalSystem.autoimport.* +import com.intellij.openapi.externalSystem.model.ProjectSystemId +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.project.Project +import org.move.cli.MoveProjectsService +import org.move.cli.MoveProjectsService.MoveRefreshStatus +import org.move.cli.moveProjectsService + +class MoveExternalSystemProjectAware( + private val project: Project +): + ExternalSystemProjectAware { + + override val projectId: ExternalSystemProjectId = ExternalSystemProjectId(MOVE_SYSTEM_ID, project.name) + + override val settingsFiles: Set + get() { + val settingsFilesService = MoveSettingsFilesService.getInstance(project) + // Always collect fresh settings files + return settingsFilesService.collectSettingsFiles() + } + + override fun reloadProject(context: ExternalSystemProjectReloadContext) { + FileDocumentManager.getInstance().saveAllDocuments() + project.moveProjectsService.scheduleProjectsRefresh() + } + + override fun subscribe(listener: ExternalSystemProjectListener, parentDisposable: Disposable) { + project.messageBus.connect(parentDisposable).subscribe( + MoveProjectsService.MOVE_PROJECTS_REFRESH_TOPIC, + object : MoveProjectsService.MoveProjectsRefreshListener { + override fun onRefreshStarted() { + listener.onProjectReloadStart() + } + + override fun onRefreshFinished(status: MoveRefreshStatus) { + val externalStatus = when (status) { + MoveRefreshStatus.SUCCESS -> ExternalSystemRefreshStatus.SUCCESS + MoveRefreshStatus.FAILURE -> ExternalSystemRefreshStatus.FAILURE + MoveRefreshStatus.CANCEL -> ExternalSystemRefreshStatus.CANCEL + } + listener.onProjectReloadFinish(externalStatus) + } + } + ) + } + + companion object { + val MOVE_SYSTEM_ID: ProjectSystemId = ProjectSystemId("Move") + } +} diff --git a/src/main/kotlin/org/move/cli/externalSystem/MoveSettingsFilesService.kt b/src/main/kotlin/org/move/cli/externalSystem/MoveSettingsFilesService.kt index 82bdb506c..0fd77b235 100644 --- a/src/main/kotlin/org/move/cli/externalSystem/MoveSettingsFilesService.kt +++ b/src/main/kotlin/org/move/cli/externalSystem/MoveSettingsFilesService.kt @@ -1,24 +1,36 @@ -package org.move.cli.projectAware +package org.move.cli.externalSystem import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import org.move.cli.Consts +import org.move.cli.MoveProject import org.move.cli.moveProjectsService +import org.move.stdext.blankToNull @Service(Service.Level.PROJECT) class MoveSettingsFilesService(private val project: Project) { - fun collectSettingsFiles(): List { - val out = mutableListOf() + + fun collectSettingsFiles(): Set { + val out = mutableSetOf() for (moveProject in project.moveProjectsService.allProjects) { - for (movePackage in moveProject.movePackages()) { - val root = movePackage.contentRoot.path - out.add("$root/${Consts.MANIFEST_FILE}") - } + moveProject.collectSettingsFiles(out) } return out } + private fun MoveProject.collectSettingsFiles(out: MutableSet) { + for (movePackage in this.movePackages()) { + val root = movePackage.contentRoot.path + out.add("$root/${Consts.MANIFEST_FILE}") + + val packageName = movePackage.packageName.blankToNull() + if (packageName != null) { + out.add("$root/build/$packageName/BuildInfo.yaml") + } + } + } + companion object { fun getInstance(project: Project): MoveSettingsFilesService = project.service() } diff --git a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt index 35ce20d31..cb5c43df8 100644 --- a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt +++ b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt @@ -60,11 +60,15 @@ class MvProjectSettingsService( // default values for settings class MoveProjectSettings: MvProjectSettingsBase() { + @AffectsMoveProjectsMetadata var blockchain: Blockchain by enum(Blockchain.APTOS) + @AffectsMoveProjectsMetadata var aptosExecType: AptosExecType by enum(defaultAptosExecType) + @AffectsMoveProjectsMetadata var localAptosPath: String? by string() + @AffectsMoveProjectsMetadata var localSuiPath: String? by string() var foldSpecs: Boolean by property(false) diff --git a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsServiceBase.kt b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsServiceBase.kt index bb8aba2af..3995cf2b8 100644 --- a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsServiceBase.kt +++ b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsServiceBase.kt @@ -13,12 +13,12 @@ import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.memberProperties import org.move.cli.settings.MvProjectSettingsServiceBase.MvProjectSettingsBase -abstract class MvProjectSettingsServiceBase>( +abstract class MvProjectSettingsServiceBase>( val project: Project, state: T -) : SimplePersistentStateComponent(state) { +): SimplePersistentStateComponent(state) { - abstract class MvProjectSettingsBase> : BaseState() { + abstract class MvProjectSettingsBase>: BaseState() { abstract fun copy(): T } @@ -57,7 +57,7 @@ abstract class MvProjectSettingsServiceBase>( } interface MoveSettingsListener { - fun > settingsChanged(e: SettingsChangedEventBase) + fun > settingsChanged(e: SettingsChangedEventBase) } protected abstract fun createSettingsChangedEvent(oldEvent: T, newEvent: T): SettingsChangedEventBase @@ -70,7 +70,7 @@ abstract class MvProjectSettingsServiceBase>( } } - abstract class SettingsChangedEventBase>(val oldState: T, val newState: T) { + abstract class SettingsChangedEventBase>(val oldState: T, val newState: T) { private val moveProjectsMetadataAffectingProps: List> = oldState.javaClass.kotlin.memberProperties.filter { it.findAnnotation() != null } diff --git a/src/main/kotlin/org/move/utils/tests/MvProjectTestBase.kt b/src/main/kotlin/org/move/utils/tests/MvProjectTestBase.kt index a4bb16018..361240e10 100644 --- a/src/main/kotlin/org/move/utils/tests/MvProjectTestBase.kt +++ b/src/main/kotlin/org/move/utils/tests/MvProjectTestBase.kt @@ -10,6 +10,7 @@ import com.intellij.psi.impl.PsiManagerEx import com.intellij.testFramework.builders.ModuleFixtureBuilder import com.intellij.testFramework.fixtures.CodeInsightFixtureTestCase import com.intellij.testFramework.fixtures.CodeInsightTestFixture +import com.intellij.util.ui.UIUtil import org.intellij.lang.annotations.Language import org.move.cli.moveProjectsService import org.move.cli.settings.Blockchain @@ -100,4 +101,24 @@ abstract class MvProjectTestBase : CodeInsightFixtureTestCase Boolean + ) { + repeat(retries) { + UIUtil.dispatchAllInvocationEvents() + if (action()) { + return + } + Thread.sleep(10) + } + error(errorMessage) + } } diff --git a/src/main/kotlin/org/move/utils/tests/RunConfigurationUtils.kt b/src/main/kotlin/org/move/utils/tests/RunConfigurationUtils.kt new file mode 100644 index 000000000..11bd3e18d --- /dev/null +++ b/src/main/kotlin/org/move/utils/tests/RunConfigurationUtils.kt @@ -0,0 +1,111 @@ +package org.move.utils.tests + +import com.intellij.execution.ExecutionListener +import com.intellij.execution.configurations.RunConfiguration +import com.intellij.execution.impl.ExecutionManagerImpl +import com.intellij.execution.process.* +import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.execution.ui.RunContentDescriptor +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.Key +import com.intellij.testFramework.PlatformTestUtil +import com.intellij.util.ConcurrencyUtil +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@Throws(InterruptedException::class) +fun CountDownLatch.waitFinished(timeoutMs: Long): Boolean { + for (i in 1..timeoutMs / ConcurrencyUtil.DEFAULT_TIMEOUT_MS) { + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() + if (await(ConcurrencyUtil.DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) return true + } + return false +} + +/** + * Capturing adapter that removes ANSI escape codes from the output + */ +class AnsiAwareCapturingProcessAdapter : ProcessAdapter(), AnsiEscapeDecoder.ColoredTextAcceptor { + val output = ProcessOutput() + + private val decoder = object : AnsiEscapeDecoder() { + override fun getCurrentOutputAttributes(outputType: Key<*>) = outputType + } + + override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) = + decoder.escapeText(event.text, outputType, this) + + private fun addToOutput(text: String, outputType: Key<*>) { + if (outputType === ProcessOutputTypes.STDERR) { + output.appendStderr(text) + } else { + output.appendStdout(text) + } + } + + override fun processTerminated(event: ProcessEvent) { + output.exitCode = event.exitCode + } + + override fun coloredTextAvailable(text: String, attributes: Key<*>) = + addToOutput(text, attributes) +} + +class TestExecutionListener( + private val parentDisposable: Disposable, + private val configuration: RunConfiguration +) : ExecutionListener { + + private val latch = CountDownLatch(1) + private val processListener = AnsiAwareCapturingProcessAdapter() + + private var isProcessStarted = false + private var notStartedCause: Throwable? = null + private var descriptor: RunContentDescriptor? = null + + override fun processNotStarted(executorId: String, env: ExecutionEnvironment, cause: Throwable?) { + checkAndExecute(env) { + notStartedCause = cause + latch.countDown() + } + } + + override fun processStarting(executorId: String, env: ExecutionEnvironment, handler: ProcessHandler) { + checkAndExecute(env) { + isProcessStarted = true + handler.addProcessListener(processListener) + Disposer.register(parentDisposable) { + ExecutionManagerImpl.stopProcess(handler) + } + } + } + + override fun processTerminated(executorId: String, env: ExecutionEnvironment, handler: ProcessHandler, exitCode: Int) { + checkAndExecute(env) { + descriptor = env.contentToReuse?.also { + Disposer.register(parentDisposable, it) + } + latch.countDown() + } + } + + private fun checkAndExecute(env: ExecutionEnvironment, action: () -> Unit) { + if (env.runProfile == configuration) { + action() + } + } + + @Throws(InterruptedException::class) + fun waitFinished(timeoutMs: Long = 5000): TestExecutionResult? { + if (!latch.waitFinished(timeoutMs)) return null + val output = if (isProcessStarted) processListener.output else null + return TestExecutionResult(output, descriptor, notStartedCause) + } +} + +data class TestExecutionResult( + val output: ProcessOutput?, + val descriptor: RunContentDescriptor?, + val notStartedCause: Throwable? +) diff --git a/src/main/resources/META-INF/intellij-move-core.xml b/src/main/resources/META-INF/intellij-move-core.xml index 18bd2521c..e27b6d3fc 100644 --- a/src/main/resources/META-INF/intellij-move-core.xml +++ b/src/main/resources/META-INF/intellij-move-core.xml @@ -305,7 +305,7 @@ - + diff --git a/src/test/kotlin/org/move/cli/externalSystem/ImportProjectTest.kt b/src/test/kotlin/org/move/cli/externalSystem/ImportProjectTest.kt index f225040c2..c92e79cb1 100644 --- a/src/test/kotlin/org/move/cli/externalSystem/ImportProjectTest.kt +++ b/src/test/kotlin/org/move/cli/externalSystem/ImportProjectTest.kt @@ -1,4 +1,4 @@ -package org.move.cli.projectAware +package org.move.cli.externalSystem import org.move.utils.tests.MvProjectTestBase diff --git a/src/test/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAwareTest.kt b/src/test/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAwareTest.kt new file mode 100644 index 000000000..7b6e55f41 --- /dev/null +++ b/src/test/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAwareTest.kt @@ -0,0 +1,228 @@ +package org.move.cli.externalSystem + +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.externalSystem.autoimport.AutoImportProjectNotificationAware +import com.intellij.openapi.externalSystem.autoimport.AutoImportProjectTracker +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectId +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.PathUtil +import org.move.cli.MoveProjectsService +import org.move.lang.core.psi.MvPath +import org.move.utils.tests.MvProjectTestBase +import org.move.utils.tests.TestProject +import org.move.utils.tests.waitFinished +import java.io.IOException +import java.util.concurrent.CountDownLatch + +@Suppress("UnstableApiUsage") +class MoveExternalSystemProjectAwareTest: MvProjectTestBase() { + + private val notificationAware get() = AutoImportProjectNotificationAware.getInstance(project) + private val projectTracker get() = AutoImportProjectTracker.getInstance(project) + + private val moveSystemId: ExternalSystemProjectId + get() = projectTracker + .getActivatedProjects() + .first { it.systemId == MoveExternalSystemProjectAware.MOVE_SYSTEM_ID } + + override fun setUp() { + super.setUp() + AutoImportProjectTracker.enableAutoReloadInTests(testRootDisposable) + } + + fun `test modifications`() { + val testProject = testProject { + namedMoveToml("RootPackage") + build { + dir("RootPackage") { + buildInfoYaml(""" +--- +compiled_package_info: + package_name: RootPackage +""") + } + } + sources { + main( + """ + module 0x1::main { /*caret*/ } + """ + ) + } + dir("child1") { + namedMoveToml("ChildPackage1") + sources { + main( + """ + module 0x1::main {} + """ + ) + } + } + dir("child2") { + namedMoveToml("ChildPackage2") + sources { + main( + """ + module 0x1::main {} + """ + ) + } + } + } + + assertNotificationAware(event = "after project creation") + + testProject.checkFileModification("Move.toml", triggered = true) + testProject.checkFileModification("build/RootPackage/BuildInfo.yaml", triggered = true) + testProject.checkFileModification("child1/Move.toml", triggered = true) + testProject.checkFileModification("child2/Move.toml", triggered = true) + + testProject.checkFileModification("sources/main.move", triggered = false) + testProject.checkFileModification("child1/sources/main.move", triggered = false) + testProject.checkFileModification("child2/sources/main.move", triggered = false) + } + + fun `test reload`() { + val testProject = testProject { + moveToml(""" + [package] + name = "MainPackage" + + [dependencies] + #Dep = { local = "./dep" } + """) + sources { + main(""" + module 0x1::main { + fun main() { + 0x1::dep::call(); + //^ + } + } + """) + } + dir("dep") { + namedMoveToml("Dep") + sources { + move("dep.move", """ + module 0x1::dep { + public fun call() {} + } + """) + } + } + } + assertNotificationAware(event = "initial project creation") + + testProject.checkReferenceIsResolved("sources/main.move", shouldNotResolve = true) + + val moveToml = testProject.file("Move.toml") + runWriteAction { + VfsUtil.saveText(moveToml, VfsUtil.loadText(moveToml).replace("#", "")) + } + assertNotificationAware(moveSystemId, event = "modification in Cargo.toml") + + scheduleProjectReload() + assertNotificationAware(event = "project reloading") + + runWithInvocationEventsDispatching("Failed to resolve the reference") { + testProject.findElementInFile("sources/main.move").reference?.resolve() != null + } + } + + private fun TestProject.checkFileModification(path: String, triggered: Boolean) { + val file = file(path) + val initialText = VfsUtil.loadText(file) + checkModification("modifying of", path, triggered, + apply = { VfsUtil.saveText(file, "$initialText\nsome text") }, + revert = { VfsUtil.saveText(file, initialText) } + ) + } + + private fun TestProject.checkFileDeletion(path: String, triggered: Boolean) { + val file = file(path) + val initialText = VfsUtil.loadText(file) + checkModification("removing of ", path, triggered, + apply = { file.delete(file.fileSystem) }, + revert = { createFile(rootDirectory, path, initialText) } + ) + } + + private fun TestProject.checkFileCreation(path: String, triggered: Boolean) { + checkModification("creation of", path, triggered, + apply = { createFile(rootDirectory, path) }, + revert = { + val file = file(path) + file.delete(file.fileSystem) + } + ) + } + + private fun checkModification( + eventName: String, + path: String, + triggered: Boolean, + apply: () -> Unit, + revert: () -> Unit + ) { + runWriteAction { + apply() + } + val externalSystems = if (triggered) arrayOf(moveSystemId) else arrayOf() + assertNotificationAware(*externalSystems, event = "$eventName $path") + + runWriteAction { + revert() + } + assertNotificationAware(event = "revert $eventName $path") + } + + private fun scheduleProjectReload() { + val newDisposable = Disposer.newDisposable() + val startLatch = CountDownLatch(1) + val endLatch = CountDownLatch(1) + project.messageBus.connect(newDisposable).subscribe( + MoveProjectsService.MOVE_PROJECTS_REFRESH_TOPIC, + object: MoveProjectsService.MoveProjectsRefreshListener { + override fun onRefreshStarted() { + startLatch.countDown() + } + + override fun onRefreshFinished(status: MoveProjectsService.MoveRefreshStatus) { + endLatch.countDown() + } + } + ) + + try { + projectTracker.scheduleProjectRefresh() + + if (!startLatch.waitFinished(1000)) error("Move project reloading hasn't started") + if (!endLatch.waitFinished(5000)) error("Move project reloading hasn't finished") + } finally { + Disposer.dispose(newDisposable) + } + } + + private fun assertNotificationAware(vararg projects: ExternalSystemProjectId, event: String) { + val message = + if (projects.isEmpty()) "Notification must be expired" else "Notification must be notified" + assertEquals("$message on $event", projects.toSet(), notificationAware.getProjectsWithNotification()) + } + + @Throws(IOException::class) + private fun createFile(root: VirtualFile, path: String, text: String = ""): VirtualFile { + val name = PathUtil.getFileName(path) + val parentPath = PathUtil.getParentPath(path) + var parent = root + if (parentPath.isNotEmpty()) { + parent = VfsUtil.createDirectoryIfMissing(root, parentPath) ?: error("Failed to create $parentPath directory") + } + val file = parent.createChildData(parent.fileSystem, name) + VfsUtil.saveText(file, text) + return file + } +} \ No newline at end of file diff --git a/ui-tests/src/test/kotlin/org/move/ui/IdeaFrameTest.kt b/ui-tests/src/test/kotlin/org/move/ui/IdeaFrameTest.kt deleted file mode 100644 index d68e8e1a3..000000000 --- a/ui-tests/src/test/kotlin/org/move/ui/IdeaFrameTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.move.ui - -import com.intellij.remoterobot.RemoteRobot -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.move.ui.fixtures.DialogFixture -import org.move.ui.fixtures.ideaFrame -import org.move.ui.utils.RemoteRobotExtension - -@ExtendWith(RemoteRobotExtension::class) -class IdeaFrameTest { - - @Test - fun `run view function test`(remoteRobot: RemoteRobot) = with(remoteRobot) { - ideaFrame { - val editorGutter = textEditor().gutter - editorGutter.getIcons().first().click() - - } - - val paramsDialog = DialogFixture.byTitle("Edit Function Parameters") - } -} \ No newline at end of file From 961d3ee9f8002a4a830ac46b940a96d276420bab Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Fri, 5 Apr 2024 13:06:53 +0300 Subject: [PATCH 12/14] initial sync task view support, fetch packages for sui --- .../org/move/cli/LibraryRootsProvider.kt | 6 +- src/main/kotlin/org/move/cli/MovePackage.kt | 40 +-- src/main/kotlin/org/move/cli/MoveProject.kt | 19 +- .../org/move/cli/MoveProjectsService.kt | 3 + .../org/move/cli/MoveProjectsSyncTask.kt | 317 ++++++++++++++++-- .../org/move/cli/ProcessProgressListener.kt | 15 + .../kotlin/org/move/cli/manifest/MoveToml.kt | 6 +- .../{InitProjectCli.kt => BlockchainCli.kt} | 46 ++- .../MvCapturingProcessHandler.kt | 3 +- .../move/cli/sentryReporter/SentryContexts.kt | 4 +- .../cli/settings/MvProjectSettingsService.kt | 32 +- .../cli/settings/aptos/ChooseAptosCliPanel.kt | 19 +- .../ide/newProject/MoveProjectGenerator.kt | 38 ++- .../newProject/MoveProjectGeneratorPeer.kt | 32 +- .../MoveExternalSystemProjectAwareTest.kt | 3 + 15 files changed, 463 insertions(+), 120 deletions(-) create mode 100644 src/main/kotlin/org/move/cli/ProcessProgressListener.kt rename src/main/kotlin/org/move/cli/runConfigurations/{InitProjectCli.kt => BlockchainCli.kt} (65%) diff --git a/src/main/kotlin/org/move/cli/LibraryRootsProvider.kt b/src/main/kotlin/org/move/cli/LibraryRootsProvider.kt index 8b8db6e36..9a356b818 100644 --- a/src/main/kotlin/org/move/cli/LibraryRootsProvider.kt +++ b/src/main/kotlin/org/move/cli/LibraryRootsProvider.kt @@ -62,8 +62,10 @@ private val MoveProject.ideaLibraries: Collection } .map { val sourceRoots = it.layoutPaths().mapNotNull { p -> p.toVirtualFile() }.toMutableSet() - it.moveToml.tomlFile - ?.virtualFile?.let { f -> sourceRoots.add(f) } + val tomlFile = it.moveToml.tomlFile.virtualFile + sourceRoots.add(tomlFile) +// it.moveToml.tomlFile +// .virtualFile.let { f -> sourceRoots.add(f) } MoveLangLibrary(it.packageName, sourceRoots, emptySet(), MoveIcons.MOVE_LOGO, null) } diff --git a/src/main/kotlin/org/move/cli/MovePackage.kt b/src/main/kotlin/org/move/cli/MovePackage.kt index 266deeeee..e575ec8b5 100644 --- a/src/main/kotlin/org/move/cli/MovePackage.kt +++ b/src/main/kotlin/org/move/cli/MovePackage.kt @@ -5,7 +5,6 @@ import com.intellij.openapi.vfs.VirtualFile import org.move.cli.manifest.AptosConfigYaml import org.move.cli.manifest.MoveToml import org.move.lang.toNioPathOrNull -import org.move.openapiext.checkReadAccessAllowed import org.move.openapiext.pathAsPath import org.move.openapiext.resolveExisting import java.nio.file.Path @@ -14,13 +13,29 @@ data class MovePackage( val project: Project, val contentRoot: VirtualFile, val moveToml: MoveToml, - val aptosConfigYaml: AptosConfigYaml?, ) { val packageName = this.moveToml.packageName ?: "" val sourcesFolder: VirtualFile? get() = contentRoot.takeIf { it.isValid }?.findChild("sources") val testsFolder: VirtualFile? get() = contentRoot.takeIf { it.isValid }?.findChild("tests") + val aptosConfigYaml: AptosConfigYaml? + get() { + var root: VirtualFile? = contentRoot + while (true) { + if (root == null) break + val candidatePath = root + .findChild(".aptos") + ?.takeIf { it.isDirectory } + ?.findChild("config.yaml") + if (candidatePath != null) { + return AptosConfigYaml.fromPath(candidatePath.pathAsPath) + } + root = root.parent + } + return null + } + fun moveFolders(): List = listOfNotNull(sourcesFolder, testsFolder) fun layoutPaths(): List { @@ -48,24 +63,9 @@ data class MovePackage( } companion object { - fun fromMoveToml(moveToml: MoveToml): MovePackage? { - checkReadAccessAllowed() - val contentRoot = moveToml.tomlFile?.parent?.virtualFile ?: return null - - var aptosConfigYaml: AptosConfigYaml? = null - var root: VirtualFile? = contentRoot - while (true) { - if (root == null) break - val candidatePath = root - .findChild(".aptos") - ?.takeIf { it.isDirectory } - ?.findChild("config.yaml") - if (candidatePath != null) { - aptosConfigYaml = AptosConfigYaml.fromPath(candidatePath.pathAsPath) ?: break - } - root = root.parent - } - return MovePackage(moveToml.project, contentRoot, moveToml, aptosConfigYaml) + fun fromMoveToml(moveToml: MoveToml): MovePackage { + val contentRoot = moveToml.tomlFile.virtualFile.parent + return MovePackage(moveToml.project, contentRoot, moveToml) } } } diff --git a/src/main/kotlin/org/move/cli/MoveProject.kt b/src/main/kotlin/org/move/cli/MoveProject.kt index 666cd86c2..5d6332352 100644 --- a/src/main/kotlin/org/move/cli/MoveProject.kt +++ b/src/main/kotlin/org/move/cli/MoveProject.kt @@ -3,6 +3,7 @@ package org.move.cli import com.intellij.openapi.project.Project import com.intellij.openapi.util.UserDataHolderBase import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiFileFactory import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.GlobalSearchScopes import com.intellij.psi.util.CachedValueProvider @@ -20,13 +21,15 @@ import org.move.openapiext.contentRoots import org.move.stdext.chain import org.move.stdext.iterateMoveVirtualFiles import org.move.stdext.wrapWithList +import org.toml.lang.TomlLanguage +import org.toml.lang.psi.TomlFile import java.nio.file.Path data class MoveProject( val project: Project, val currentPackage: MovePackage, val dependencies: List>, -) : UserDataHolderBase() { +): UserDataHolderBase() { val contentRoot: VirtualFile get() = this.currentPackage.contentRoot val contentRootPath: Path? get() = this.currentPackage.contentRoot.toNioPathOrNull() @@ -129,8 +132,18 @@ data class MoveProject( fun forTests(project: Project): MoveProject { checkUnitTestMode() val contentRoot = project.contentRoots.first() - val moveToml = MoveToml(project) - val movePackage = MovePackage(project, contentRoot, moveToml, null) + val tomlFile = + PsiFileFactory.getInstance(project) + .createFileFromText( + TomlLanguage, + """ + [package] + name = "MyPackage" + """ + ) as TomlFile + + val moveToml = MoveToml(project, tomlFile) + val movePackage = MovePackage(project, contentRoot, moveToml) return MoveProject( project, movePackage, diff --git a/src/main/kotlin/org/move/cli/MoveProjectsService.kt b/src/main/kotlin/org/move/cli/MoveProjectsService.kt index 9b6a524ea..834c22d5b 100644 --- a/src/main/kotlin/org/move/cli/MoveProjectsService.kt +++ b/src/main/kotlin/org/move/cli/MoveProjectsService.kt @@ -1,6 +1,7 @@ package org.move.cli import com.intellij.execution.RunManager +import com.intellij.ide.impl.isTrusted import com.intellij.openapi.Disposable import com.intellij.openapi.application.invokeAndWaitIfNeeded import com.intellij.openapi.application.runWriteAction @@ -155,8 +156,10 @@ class MoveProjectsService(val project: Project): Disposable { private fun doRefreshProjects(project: Project, reason: String?): CompletableFuture> { val moveProjectsFut = CompletableFuture>() + val syncTask = MoveProjectsSyncTask(project, moveProjectsFut, reason) project.taskQueue.run(syncTask) + return moveProjectsFut.thenApply { updatedProjects -> runOnlyInNonLightProject(project) { setupProjectRoots(project, updatedProjects) diff --git a/src/main/kotlin/org/move/cli/MoveProjectsSyncTask.kt b/src/main/kotlin/org/move/cli/MoveProjectsSyncTask.kt index 43a1373ee..6a0711471 100644 --- a/src/main/kotlin/org/move/cli/MoveProjectsSyncTask.kt +++ b/src/main/kotlin/org/move/cli/MoveProjectsSyncTask.kt @@ -1,85 +1,267 @@ +@file:Suppress("UnstableApiUsage") + package org.move.cli +import com.intellij.build.BuildContentDescriptor +import com.intellij.build.BuildDescriptor +import com.intellij.build.DefaultBuildDescriptor +import com.intellij.build.SyncViewManager +import com.intellij.build.events.BuildEventsNls +import com.intellij.build.events.MessageEvent +import com.intellij.build.progress.BuildProgress +import com.intellij.build.progress.BuildProgressDescriptor +import com.intellij.execution.process.ProcessAdapter +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.runReadAction import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Computable -import com.intellij.psi.PsiDocumentManager +import com.intellij.openapi.util.NlsContexts +import com.intellij.openapi.vfs.VirtualFile import org.move.cli.manifest.MoveToml -import org.move.cli.manifest.TomlDependency +import org.move.cli.settings.Blockchain.APTOS +import org.move.cli.settings.Blockchain.SUI +import org.move.cli.settings.blockchain +import org.move.cli.settings.suiCli import org.move.lang.toNioPathOrNull import org.move.lang.toTomlFile +import org.move.openapiext.TaskResult import org.move.openapiext.contentRoots import org.move.openapiext.resolveExisting import org.move.openapiext.toVirtualFile import org.move.stdext.iterateFiles +import org.move.stdext.unwrapOrElse import org.move.stdext.withExtended +import java.nio.file.Path import java.util.concurrent.CompletableFuture +import javax.swing.JComponent class MoveProjectsSyncTask( project: Project, private val future: CompletableFuture>, private val reason: String? -) : Task.Backgroundable(project, "Reloading Move packages", true) { +): Task.Backgroundable(project, "Reloading Move packages", true) { override fun run(indicator: ProgressIndicator) { indicator.isIndeterminate = true val before = System.currentTimeMillis() LOG.logProjectsRefresh("started", reason) - // aptos move fetch - fetchDependencies() - - val moveProjects = PsiDocumentManager - .getInstance(project) - .commitAndRunReadAction(Computable { loadProjects(project) }) - LOG.logProjectsRefresh("finished in ${System.currentTimeMillis() - before}ms", reason) - future.complete(moveProjects) - } - private fun fetchDependencies() { - // run `aptos move fetch` here + val syncProgress = SyncViewManager.createBuildProgress(project) + + val refreshedProjects = try { + syncProgress.start(createSyncProgressDescriptor(indicator)) + + val refreshedProjects = doRun(indicator, syncProgress) + + // TODO: +// val isUpdateFailed = refreshedProjects.any { it.mergedStatus is CargoProject.UpdateStatus.UpdateFailed } +// if (isUpdateFailed) { +// syncProgress.fail() +// } else { +// syncProgress.finish() +// } + syncProgress.finish() + + refreshedProjects + } catch (e: Throwable) { + if (e is ProcessCanceledException) { + syncProgress.cancel() + } else { + syncProgress.fail() + } + future.completeExceptionally(e) + throw e + } + future.complete(refreshedProjects) + + val elapsed = System.currentTimeMillis() - before + LOG.logProjectsRefresh("finished Move projects Sync Task in $elapsed ms", reason) } - companion object { - private val LOG = logger() + private fun doRun( + indicator: ProgressIndicator, + syncProgress: BuildProgress + ): List { + val moveProjects = mutableListOf() - private data class DepId(val rootPath: String) + for (contentRoot in project.contentRoots) { + contentRoot.iterateFiles({ it.name == Consts.MANIFEST_FILE }) { moveTomlFile -> + indicator.checkCanceled() + + val projectDirName = moveTomlFile.parent.name + syncProgress.runWithChildProgress( + "Sync $projectDirName project", + createContext = { it }, + action = { childProgress -> + val context = SyncContext(project, indicator, syncProgress.id, childProgress) + loadProject( + moveTomlFile, projects = moveProjects, context = context + ) + } + ) + + true + } + } - fun loadProjects(project: Project): List { - val projects = mutableListOf() - for (contentRoot in project.contentRoots) { - contentRoot.iterateFiles({ it.name == Consts.MANIFEST_FILE }) { - val rawDepQueue = ArrayDeque>() - val root = it.parent?.toNioPathOrNull() ?: return@iterateFiles true - val tomlFile = it.toTomlFile(project) ?: return@iterateFiles true + return moveProjects + } + + private fun loadProject( + moveTomlFile: VirtualFile, + projects: MutableList, + context: SyncContext + ) { + val projectRoot = moveTomlFile.parent?.toNioPathOrNull() ?: error("cannot be invalid path") - val moveToml = MoveToml.fromTomlFile(tomlFile, root) - rawDepQueue.addAll(moveToml.deps) + context.runWithChildProgress("Fetching dependency packages") { childContext -> + fetchProjectDependencies( + projectRoot, listener = SyncProcessAdapter(childContext) + ) + TaskResult.Ok(Unit) + } - val rootPackage = MovePackage.fromMoveToml(moveToml) ?: return@iterateFiles true + val (rootPackage, deps) = + (context.runWithChildProgress("Loading dependencies") { childContext -> + // Blocks till completed or cancelled by the toml / file change + runReadAction { + val tomlFile = moveTomlFile.toTomlFile(project)!! + val moveToml = MoveToml.fromTomlFile(tomlFile, projectRoot) + val rootPackage = MovePackage.fromMoveToml(moveToml) val deps = mutableListOf>() - val visitedDepIds = mutableSetOf( - DepId(rootPackage.contentRoot.path) - ) - loadDependencies(project, moveToml, deps, visitedDepIds, true) + val visitedDepIds = mutableSetOf(DepId(rootPackage.contentRoot.path)) + loadDependencies(project, moveToml, deps, visitedDepIds, true, childContext.progress) + + TaskResult.Ok(Pair(rootPackage, deps)) + } + } as TaskResult.Ok).value + + val moveProject = MoveProject(project, rootPackage, deps) + projects.add(moveProject) + } + + private fun fetchProjectDependencies(projectDir: Path, listener: ProcessProgressListener) { + when (project.blockchain) { + SUI -> { + val suiCli = project.suiCli + if (suiCli == null) { + listener.error("Invalid Sui CLI configuration", "") + return + } + suiCli.fetchPackageDependencies( + projectDir, + owner = project, + processListener = listener + ).unwrapOrElse { + listener.error("Failed to fetch dependencies", it.message.orEmpty()) + } + } + APTOS -> { + // TODO: not supported by CLI yet + } + } + } + + private fun createSyncProgressDescriptor(progress: ProgressIndicator): BuildProgressDescriptor { + val buildContentDescriptor = BuildContentDescriptor( + null, + null, + object: JComponent() {}, + "Move" + ) + buildContentDescriptor.isActivateToolWindowWhenFailed = true + buildContentDescriptor.isActivateToolWindowWhenAdded = false +// buildContentDescriptor.isNavigateToError = project.rustSettings.autoShowErrorsInEditor + val refreshAction = ActionManager.getInstance().getAction("Move.RefreshAllProjects") + val descriptor = DefaultBuildDescriptor(Any(), "Move", project.basePath!!, System.currentTimeMillis()) + .withContentDescriptor { buildContentDescriptor } + .withRestartAction(refreshAction) + .withRestartAction(StopAction(progress)) + return object: BuildProgressDescriptor { + override fun getTitle(): String = descriptor.title + override fun getBuildDescriptor(): BuildDescriptor = descriptor + } + } + + private class StopAction(private val progress: ProgressIndicator): + DumbAwareAction({ "Stop" }, AllIcons.Actions.Suspend) { + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = progress.isRunning + } + + override fun actionPerformed(e: AnActionEvent) { + progress.cancel() + } + } + + data class SyncContext( + val project: Project, +// val toolchain: RsToolchainBase, + val progress: ProgressIndicator, + val buildId: Any, + val syncProgress: BuildProgress + ) { - projects.add(MoveProject(project, rootPackage, deps)) - true + val id: Any get() = syncProgress.id + + fun runWithChildProgress( + @NlsContexts.ProgressText title: String, + action: (SyncContext) -> TaskResult + ): TaskResult { + progress.checkCanceled() + progress.text = title + + return syncProgress.runWithChildProgress( + title, + { copy(syncProgress = it) }, + action + ) { childProgress, result -> + when (result) { + is TaskResult.Ok -> childProgress.finish() + is TaskResult.Err -> { + childProgress.message( + result.reason, + result.message.orEmpty(), + MessageEvent.Kind.ERROR, + null + ) + childProgress.fail() + } } } - return projects } + fun withProgressText(@NlsContexts.ProgressText @NlsContexts.ProgressTitle text: String) { + progress.text = text + syncProgress.progress(text) + } + } + + companion object { + private val LOG = logger() + + private data class DepId(val rootPath: String) + private fun loadDependencies( project: Project, rootMoveToml: MoveToml, deps: MutableList>, visitedIds: MutableSet, isRoot: Boolean, + progress: ProgressIndicator ) { + // checks for the cancel() of the whole SyncTask + progress.checkCanceled() + var parsedDeps = rootMoveToml.deps if (isRoot) { parsedDeps = parsedDeps.withExtended(rootMoveToml.dev_deps) @@ -97,14 +279,75 @@ class MoveProjectsSyncTask( val depMoveToml = MoveToml.fromTomlFile(depTomlFile, depRoot) // first try to parse MovePackage from dependency, no need for nested if parent is invalid - val depPackage = MovePackage.fromMoveToml(depMoveToml) ?: continue + val depPackage = MovePackage.fromMoveToml(depMoveToml) // parse all nested dependencies with their address maps visitedIds.add(depId) - loadDependencies(project, depMoveToml, deps, visitedIds, false) + loadDependencies(project, depMoveToml, deps, visitedIds, false, progress) deps.add(Pair(depPackage, addressMap)) } } } } + +private class SyncProcessAdapter( + private val context: MoveProjectsSyncTask.SyncContext +): ProcessAdapter(), + ProcessProgressListener { +// override fun onTextAvailable(event: ProcessEvent, outputType: Key) { +// val text = event.text.trim { it <= ' ' } +// if (text.startsWith("Updating") || text.startsWith("Downloading")) { +// context.withProgressText(text) +// } +// if (text.startsWith("Vendoring")) { +// // This code expect that vendoring message has the following format: +// // "Vendoring %package_name% v%package_version% (%src_dir%) to %dst_dir%". +// // So let's extract "Vendoring %package_name% v%package_version%" part and show it for users +// val index = text.indexOf(" (") +// val progressText = if (index != -1) text.substring(0, index) else text +// context.withProgressText(progressText) +// } +// } + + override fun error(title: String, message: String) = context.error(title, message) + override fun warning(title: String, message: String) = context.warning(title, message) +} + +private fun BuildProgress.runWithChildProgress( + @BuildEventsNls.Title title: String, + createContext: (BuildProgress) -> T, + action: (T) -> R, + onResult: (BuildProgress, R) -> Unit = { progress, _ -> progress.finish() } +): R { + val childProgress = startChildProgress(title) + try { + val context = createContext(childProgress) + val result = action(context) + onResult(childProgress, result) + return result + } catch (e: Throwable) { + if (e is ProcessCanceledException) { + cancel() + } else { + fail() + } + throw e + } +} + +private fun MoveProjectsSyncTask.SyncContext.error( + @BuildEventsNls.Title title: String, + @BuildEventsNls.Message message: String +) { + syncProgress.message(title, message, com.intellij.build.events.MessageEvent.Kind.ERROR, null) +} + +private fun MoveProjectsSyncTask.SyncContext.warning( + @BuildEventsNls.Title title: String, + @BuildEventsNls.Message message: String +) { + syncProgress.message(title, message, com.intellij.build.events.MessageEvent.Kind.WARNING, null) +} + + diff --git a/src/main/kotlin/org/move/cli/ProcessProgressListener.kt b/src/main/kotlin/org/move/cli/ProcessProgressListener.kt new file mode 100644 index 000000000..6fd5bd4b3 --- /dev/null +++ b/src/main/kotlin/org/move/cli/ProcessProgressListener.kt @@ -0,0 +1,15 @@ +/* + * Use of this source code is governed by the MIT license that can be + * found in the LICENSE file. + */ + +package org.move.cli + +import com.intellij.build.events.BuildEventsNls +import com.intellij.execution.process.ProcessListener + +@Suppress("UnstableApiUsage") +interface ProcessProgressListener : ProcessListener { + fun error(@BuildEventsNls.Title title: String, @BuildEventsNls.Message message: String) + fun warning(@BuildEventsNls.Title title: String, @BuildEventsNls.Message message: String) +} diff --git a/src/main/kotlin/org/move/cli/manifest/MoveToml.kt b/src/main/kotlin/org/move/cli/manifest/MoveToml.kt index 2542ea557..45b002d3d 100644 --- a/src/main/kotlin/org/move/cli/manifest/MoveToml.kt +++ b/src/main/kotlin/org/move/cli/manifest/MoveToml.kt @@ -3,13 +3,14 @@ package org.move.cli.manifest import com.intellij.openapi.project.Project import org.move.cli.* import org.move.openapiext.* +import org.move.openapiext.common.isUnitTestMode import org.move.stdext.chain import org.toml.lang.psi.TomlFile import java.nio.file.Path class MoveToml( val project: Project, - val tomlFile: TomlFile? = null, + val tomlFile: TomlFile, val packageTable: MoveTomlPackageTable? = null, val addresses: RawAddressMap = mutableRawAddressMap(), @@ -46,6 +47,9 @@ class MoveToml( companion object { fun fromTomlFile(tomlFile: TomlFile, projectRoot: Path): MoveToml { + // needs read access for Toml + checkReadAccessAllowed() + val packageTomlTable = tomlFile.getTable("package") var packageTable: MoveTomlPackageTable? = null if (packageTomlTable != null) { diff --git a/src/main/kotlin/org/move/cli/runConfigurations/InitProjectCli.kt b/src/main/kotlin/org/move/cli/runConfigurations/BlockchainCli.kt similarity index 65% rename from src/main/kotlin/org/move/cli/runConfigurations/InitProjectCli.kt rename to src/main/kotlin/org/move/cli/runConfigurations/BlockchainCli.kt index 84d9ada87..5cbb67930 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/InitProjectCli.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/BlockchainCli.kt @@ -1,18 +1,21 @@ package org.move.cli.runConfigurations +import com.intellij.execution.process.ProcessListener import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import org.move.cli.Consts -import org.move.cli.settings.aptos.AptosExecType import org.move.openapiext.* import org.move.openapiext.common.isUnitTestMode -import org.move.stdext.RsResult -import org.move.stdext.toPathOrNull +import org.move.stdext.RsResult.Err +import org.move.stdext.RsResult.Ok import org.move.stdext.unwrapOrElse import java.nio.file.Path -sealed class InitProjectCli { +sealed class BlockchainCli { + + abstract val cliLocation: Path + abstract fun init( project: Project, parentDisposable: Disposable, @@ -20,7 +23,7 @@ sealed class InitProjectCli { packageName: String, ): MvProcessResult - data class Aptos(val aptosExecType: AptosExecType, val localAptosPath: String?): InitProjectCli() { + data class Aptos(override val cliLocation: Path): BlockchainCli() { override fun init( project: Project, parentDisposable: Disposable, @@ -39,23 +42,19 @@ sealed class InitProjectCli { ), workingDirectory = project.rootPath ) - - val aptosExecPath = - AptosExecType.aptosPath(this.aptosExecType, this.localAptosPath).toPathOrNull() - ?: error("Provided aptosPath should be validated before calling init()") commandLine - .toGeneralCommandLine(aptosExecPath) + .toGeneralCommandLine(cliLocation) .execute(parentDisposable) - .unwrapOrElse { return RsResult.Err(it) } + .unwrapOrElse { return Err(it) } fullyRefreshDirectory(rootDirectory) val manifest = checkNotNull(rootDirectory.findChild(Consts.MANIFEST_FILE)) { "Can't find the manifest file" } - return RsResult.Ok(manifest) + return Ok(manifest) } } - data class Sui(val cliLocation: Path): InitProjectCli() { + data class Sui(override val cliLocation: Path): BlockchainCli() { override fun init( project: Project, parentDisposable: Disposable, @@ -75,12 +74,29 @@ sealed class InitProjectCli { ) commandLine.toGeneralCommandLine(this.cliLocation) .execute(parentDisposable) - .unwrapOrElse { return RsResult.Err(it) } + .unwrapOrElse { return Err(it) } fullyRefreshDirectory(rootDirectory) val manifest = checkNotNull(rootDirectory.findChild(Consts.MANIFEST_FILE)) { "Can't find the manifest file" } - return RsResult.Ok(manifest) + return Ok(manifest) + } + + fun fetchPackageDependencies( + projectDir: Path, + owner: Disposable, + processListener: ProcessListener + ): MvProcessResult { + val cli = + CliCommandLineArgs( + subCommand = "move", + arguments = listOf("build", "--fetch-deps-only", "--skip-fetch-latest-git-deps"), + workingDirectory = projectDir + ) + cli.toGeneralCommandLine(cliLocation) + .execute(owner, listener = processListener) + .unwrapOrElse { return Err(it) } + return Ok(Unit) } } } diff --git a/src/main/kotlin/org/move/cli/runConfigurations/MvCapturingProcessHandler.kt b/src/main/kotlin/org/move/cli/runConfigurations/MvCapturingProcessHandler.kt index 9c5e0ee47..c53d26f3c 100644 --- a/src/main/kotlin/org/move/cli/runConfigurations/MvCapturingProcessHandler.kt +++ b/src/main/kotlin/org/move/cli/runConfigurations/MvCapturingProcessHandler.kt @@ -7,12 +7,13 @@ package org.move.cli.runConfigurations import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.CapturingAnsiEscapesAwareProcessHandler import com.intellij.execution.process.CapturingProcessHandler import com.intellij.util.io.BaseOutputReader import org.move.stdext.RsResult class MvCapturingProcessHandler private constructor(commandLine: GeneralCommandLine) : - CapturingProcessHandler(commandLine) { + CapturingAnsiEscapesAwareProcessHandler(commandLine) { override fun readerOptions(): BaseOutputReader.Options = BaseOutputReader.Options.BLOCKING diff --git a/src/main/kotlin/org/move/cli/sentryReporter/SentryContexts.kt b/src/main/kotlin/org/move/cli/sentryReporter/SentryContexts.kt index 1164cc302..bbeefd7c1 100644 --- a/src/main/kotlin/org/move/cli/sentryReporter/SentryContexts.kt +++ b/src/main/kotlin/org/move/cli/sentryReporter/SentryContexts.kt @@ -20,8 +20,8 @@ data class MoveProjectContext( val syntheticLibraries: List ) { companion object { - fun from(moveProject: MoveProject): MoveProjectContext? { - val tomlFile = moveProject.currentPackage.moveToml.tomlFile ?: return null + fun from(moveProject: MoveProject): MoveProjectContext { + val tomlFile = moveProject.currentPackage.moveToml.tomlFile val rawDeps = mutableListOf() val depsTable = tomlFile.getTable("dependencies") diff --git a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt index cb5c43df8..ccf8df557 100644 --- a/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt +++ b/src/main/kotlin/org/move/cli/settings/MvProjectSettingsService.kt @@ -6,6 +6,11 @@ import com.intellij.openapi.components.StoragePathMacros import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.psi.PsiManager +import org.move.cli.runConfigurations.BlockchainCli +import org.move.cli.runConfigurations.BlockchainCli.Aptos +import org.move.cli.runConfigurations.BlockchainCli.Sui +import org.move.cli.settings.Blockchain.APTOS +import org.move.cli.settings.Blockchain.SUI import org.move.cli.settings.MvProjectSettingsService.MoveProjectSettings import org.move.cli.settings.aptos.AptosExecType import org.move.stdext.exists @@ -56,8 +61,6 @@ class MvProjectSettingsService( val skipFetchLatestGitDeps: Boolean get() = state.skipFetchLatestGitDeps val dumpStateOnTestFailure: Boolean get() = state.dumpStateOnTestFailure - val aptosExecPath: String get() = AptosExecType.aptosPath(aptosExecType, localAptosPath) - // default values for settings class MoveProjectSettings: MvProjectSettingsBase() { @AffectsMoveProjectsMetadata @@ -68,6 +71,7 @@ class MvProjectSettingsService( @AffectsMoveProjectsMetadata var localAptosPath: String? by string() + @AffectsMoveProjectsMetadata var localSuiPath: String? by string() @@ -109,9 +113,29 @@ class MvProjectSettingsService( } } -val Project.aptosExecPath: Path? get() = this.moveSettings.aptosExecPath.toPathOrNull() +val Project.blockchain: Blockchain get() = this.moveSettings.blockchain + +fun Project.getBlockchainCli(blockchain: Blockchain): BlockchainCli? { + return when (blockchain) { + APTOS -> { + val aptosExecPath = + AptosExecType.aptosExecPath( + this.moveSettings.aptosExecType, + this.moveSettings.localAptosPath + ) + aptosExecPath?.let { Aptos(it) } + } + SUI -> this.moveSettings.localSuiPath?.toPathOrNull()?.let { Sui(it) } + } +} + +val Project.aptosCli: Aptos? get() = getBlockchainCli(APTOS) as? Aptos + +val Project.suiCli: Sui? get() = getBlockchainCli(SUI) as? Sui + +val Project.aptosExecPath: Path? get() = this.aptosCli?.cliLocation -val Project.suiExecPath: Path? get() = this.moveSettings.localSuiPath?.toPathOrNull() +val Project.suiExecPath: Path? get() = this.suiCli?.cliLocation fun Path?.isValidExecutable(): Boolean { return this != null diff --git a/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt b/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt index 6dc088234..1a0831edd 100644 --- a/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt +++ b/src/main/kotlin/org/move/cli/settings/aptos/ChooseAptosCliPanel.kt @@ -5,15 +5,20 @@ import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.SystemInfo import com.intellij.ui.components.JBRadioButton -import com.intellij.ui.dsl.builder.* +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.Row +import com.intellij.ui.dsl.builder.actionListener import com.intellij.ui.layout.selected import org.move.cli.settings.VersionLabel import org.move.cli.settings.aptos.AptosExecType.BUNDLED import org.move.cli.settings.aptos.AptosExecType.LOCAL +import org.move.cli.settings.isValidExecutable import org.move.openapiext.PluginPathManager import org.move.openapiext.pathField import org.move.stdext.blankToNull import org.move.stdext.toPathOrNull +import java.nio.file.Path enum class AptosExecType { BUNDLED, @@ -25,11 +30,13 @@ enum class AptosExecType { fun bundledPath(): String? = PluginPathManager.bundledAptosCli - fun aptosPath(execType: AptosExecType, localAptosPath: String?): String { - return when (execType) { - BUNDLED -> bundledPath() ?: "" - LOCAL -> localAptosPath ?: "" - } + fun aptosExecPath(execType: AptosExecType, localAptosPath: String?): Path? { + val pathCandidate = + when (execType) { + BUNDLED -> bundledPath()?.toPathOrNull() + LOCAL -> localAptosPath?.blankToNull()?.toPathOrNull() + } + return pathCandidate?.takeIf { it.isValidExecutable() } } } } diff --git a/src/main/kotlin/org/move/ide/newProject/MoveProjectGenerator.kt b/src/main/kotlin/org/move/ide/newProject/MoveProjectGenerator.kt index 086a1342e..4e2649ad0 100644 --- a/src/main/kotlin/org/move/ide/newProject/MoveProjectGenerator.kt +++ b/src/main/kotlin/org/move/ide/newProject/MoveProjectGenerator.kt @@ -12,15 +12,23 @@ import com.intellij.platform.DirectoryProjectGenerator import com.intellij.platform.DirectoryProjectGeneratorBase import com.intellij.platform.ProjectGeneratorPeer import org.move.cli.PluginApplicationDisposable -import org.move.cli.runConfigurations.InitProjectCli +import org.move.cli.runConfigurations.BlockchainCli import org.move.cli.settings.Blockchain +import org.move.cli.settings.Blockchain.APTOS +import org.move.cli.settings.Blockchain.SUI +import org.move.cli.settings.aptos.AptosExecType import org.move.cli.settings.moveSettings import org.move.ide.MoveIcons import org.move.openapiext.computeWithCancelableProgress -import org.move.stdext.blankToNull +import org.move.stdext.toPathOrNull import org.move.stdext.unwrapOrThrow -data class MoveProjectConfig(val blockchain: Blockchain, val initCli: InitProjectCli) +data class MoveProjectConfig( + val blockchain: Blockchain, + val aptosExecType: AptosExecType, + val localAptosPath: String?, + val localSuiPath: String? +) class MoveProjectGenerator: DirectoryProjectGeneratorBase(), CustomStepProjectGenerator { @@ -39,7 +47,19 @@ class MoveProjectGenerator: DirectoryProjectGeneratorBase(), ) { val packageName = project.name val blockchain = projectConfig.blockchain - val projectCli = projectConfig.initCli + val projectCli = + when (blockchain) { + APTOS -> { + val aptosPath = + AptosExecType.aptosExecPath(projectConfig.aptosExecType, projectConfig.localAptosPath) + ?: error("validated before") + BlockchainCli.Aptos(aptosPath) + } + SUI -> { + val suiPath = projectConfig.localSuiPath?.toPathOrNull() ?: error("validated before") + BlockchainCli.Sui(suiPath) + } + } val manifestFile = project.computeWithCancelableProgress("Generating $blockchain project...") { val manifestFile = @@ -56,12 +76,12 @@ class MoveProjectGenerator: DirectoryProjectGeneratorBase(), project.moveSettings.modify { it.blockchain = blockchain when (projectCli) { - is InitProjectCli.Aptos -> { - it.aptosExecType = projectCli.aptosExecType - it.localAptosPath = projectCli.localAptosPath?.blankToNull() + is BlockchainCli.Aptos -> { + it.aptosExecType = projectConfig.aptosExecType + it.localAptosPath = projectConfig.localAptosPath } - is InitProjectCli.Sui -> { - it.localSuiPath = projectCli.cliLocation.toString() + is BlockchainCli.Sui -> { + it.localSuiPath = projectConfig.localSuiPath } } } diff --git a/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt b/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt index 265c5cebc..b650c6cea 100644 --- a/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt +++ b/src/main/kotlin/org/move/ide/newProject/MoveProjectGeneratorPeer.kt @@ -12,8 +12,10 @@ import com.intellij.openapi.ui.ValidationInfo import com.intellij.openapi.util.Disposer import com.intellij.platform.GeneratorPeerImpl import com.intellij.ui.components.JBRadioButton -import com.intellij.ui.dsl.builder.* -import org.move.cli.runConfigurations.InitProjectCli +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.actionListener +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.selected import org.move.cli.settings.Blockchain import org.move.cli.settings.MvProjectSettingsService import org.move.cli.settings.aptos.AptosExecType @@ -49,20 +51,12 @@ class MoveProjectGeneratorPeer(val parentDisposable: Disposable): GeneratorPeerI private var checkValid: Runnable? = null override fun getSettings(): MoveProjectConfig { - val initProjectCli = - when (blockchain) { - Blockchain.APTOS -> { - val aptosExecType = this.chooseAptosCliPanel.data.aptosExecType - val localAptosPath = this.chooseAptosCliPanel.data.localAptosPath - InitProjectCli.Aptos(aptosExecType, localAptosPath) - } - Blockchain.SUI -> { - val suiPath = this.chooseSuiCliPanel.data.localSuiPath?.toPathOrNull() - ?: error("Should be validated separately") - InitProjectCli.Sui(suiPath) - } - } - return MoveProjectConfig(blockchain, initProjectCli) + return MoveProjectConfig( + blockchain = blockchain, + aptosExecType = this.chooseAptosCliPanel.data.aptosExecType, + localAptosPath = this.chooseAptosCliPanel.data.localAptosPath, + localSuiPath = this.chooseSuiCliPanel.data.localSuiPath + ) } override fun getComponent(myLocationField: TextFieldWithBrowseButton, checkValid: Runnable): JComponent { @@ -120,10 +114,8 @@ class MoveProjectGeneratorPeer(val parentDisposable: Disposable): GeneratorPeerI Blockchain.APTOS -> { val panelData = this.chooseAptosCliPanel.data val aptosExecPath = - AptosExecType.aptosPath(panelData.aptosExecType, panelData.localAptosPath).toPathOrNull() - if (aptosExecPath == null - || !aptosExecPath.isValidExecutable() - ) { + AptosExecType.aptosExecPath(panelData.aptosExecType, panelData.localAptosPath) + if (aptosExecPath == null) { return ValidationInfo("Invalid path to $blockchain executable") } } diff --git a/src/test/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAwareTest.kt b/src/test/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAwareTest.kt index 7b6e55f41..e2c5d2fbf 100644 --- a/src/test/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAwareTest.kt +++ b/src/test/kotlin/org/move/cli/externalSystem/MoveExternalSystemProjectAwareTest.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectId import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiDocumentManager import com.intellij.util.PathUtil import org.move.cli.MoveProjectsService import org.move.lang.core.psi.MvPath @@ -125,6 +126,8 @@ compiled_package_info: } assertNotificationAware(moveSystemId, event = "modification in Cargo.toml") + PsiDocumentManager.getInstance(project).commitAllDocuments() + scheduleProjectReload() assertNotificationAware(event = "project reloading") From e78a9737e54d1747e7df5bd457b480159ac83bae Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Fri, 5 Apr 2024 14:41:27 +0300 Subject: [PATCH 13/14] simplify ui tests --- .../kotlin/org/move/ui/fixtures/IdeaFrame.kt | 21 ----- .../org/move/ui/fixtures/WelcomeFrame.kt | 89 ++++++++++++++++--- .../test/kotlin/org/move/ui/NewProjectTest.kt | 81 +++++------------ 3 files changed, 100 insertions(+), 91 deletions(-) diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt index 964ed274d..59ee1f9b4 100644 --- a/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt @@ -51,10 +51,6 @@ class IdeaFrame( } } - fun closeProject() { - menuBar.select("File", "Close Project") - } - fun settingsDialog(function: DialogFixture.() -> Unit): DialogFixture = dialog("Settings", function = function) fun DialogFixture.selectMoveSettings() { @@ -65,23 +61,6 @@ class IdeaFrame( } } -// fun openSettings(function: SettingsDialogFixture.() -> Unit) { -// if (!remoteRobot.isMac()) { -// waitFor { -// findAll( -// Locators.byTypeAndProperties(JMenu::class.java, Locators.XpathProperty.ACCESSIBLE_NAME to "File") -// ) -// .isNotEmpty() -// } -// } -// menuBar.select("File", "Settings...") -// step("Search for dialog with title 'Settings'") { -// find( -// DialogFixture.byTitle("Settings"), Duration.ofSeconds(2)).apply(function) -// } -// -// } - fun openMoveSettings(function: MoveSettingsPanelFixture.() -> Unit) { openSettingsDialog() settingsDialog { diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt index 6448b6ba9..bd6b2a307 100644 --- a/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt @@ -8,6 +8,9 @@ import com.intellij.remoterobot.data.RemoteComponent import com.intellij.remoterobot.fixtures.* import com.intellij.remoterobot.search.locators.Locator import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.steps.CommonSteps +import com.intellij.remoterobot.steps.Step +import com.intellij.remoterobot.steps.StepParameter import com.intellij.remoterobot.stepsProcessing.step import com.intellij.remoterobot.utils.Locators import com.intellij.remoterobot.utils.keyboard @@ -20,23 +23,83 @@ import java.time.Duration import javax.swing.JMenu import kotlin.math.abs +val RemoteRobot.commonSteps get() = CommonSteps(this) + fun RemoteRobot.welcomeFrame(function: WelcomeFrame.() -> Unit) { find(WelcomeFrame::class.java, Duration.ofSeconds(10)).apply(function) } -fun RemoteRobot.openProject(projectPath: Path) { - welcomeFrame { - openProjectAt(projectPath) - } -// ideaFrame { -// find( -// Locators.byTypeAndProperties( -// JMenu::class.java, -// Locators.XpathProperty.ACCESSIBLE_NAME to "File" -// ), -// timeout = Duration.ofSeconds(5) -// ) -// } +fun RemoteRobot.openOrImportProject(absolutePath: Path) = openOrImportProject(absolutePath.toString()) + +@Step("Open or import project", "Open or import project '{1}'") +fun RemoteRobot.openOrImportProject(@StepParameter("Project absolute path", "") absolutePath: String) { + this.runJs( + """ + importClass(com.intellij.openapi.application.ApplicationManager) + importClass(com.intellij.ide.impl.ProjectUtil) + importClass(com.intellij.ide.impl.OpenProjectTask) + + let task + try { + task = OpenProjectTask.build() + } catch(e) { + task = OpenProjectTask.newProject() + } + + const path = new java.io.File("$absolutePath").toPath() + const openProjectFunction = new Runnable({ + run: function() { + ProjectUtil.openOrImport(path, task) + } + }) + + ApplicationManager.getApplication().invokeLater(openProjectFunction) + """, runInEdt = true) + + // TODO: wait for status bar to stop processing things +} + +fun RemoteRobot.closeProject() = CommonSteps(this).closeProject() + +@Step("Remove project from recents", "Remove project from recents") +fun RemoteRobot.removeLastRecentProject() { + this.runJs( + """ + importClass(com.intellij.openapi.application.ApplicationManager) + importClass(com.intellij.ide.RecentProjectsManagerBase) + + const removeRecentProjectFunction = new Runnable({ + run: function() { + const recentsProjectsManager = RecentProjectsManagerBase.getInstanceEx(); + const lastProjectPath = recentsProjectsManager.getLastOpenedProject(); + if (lastProjectPath != null) { + recentsProjectsManager.removePath(lastProjectPath); + } + } + }) + + ApplicationManager.getApplication().invokeLater(removeRecentProjectFunction) + """, runInEdt = true) +} + +fun RemoteRobot.removeProjectFromRecents(absolutePath: Path) = removeProjectFromRecents(absolutePath.toString()) + +@Step("Remove from recents", "Remove '{1}' from recents") +fun RemoteRobot.removeProjectFromRecents(@StepParameter("Project absolute path", "") absolutePath: String) { + this.runJs( + """ + importClass(com.intellij.openapi.application.ApplicationManager) + importClass(com.intellij.ide.RecentProjectsManager) + + const projectPath = new java.io.File("$absolutePath").toPath() + const removeRecentProjectFunction = new Runnable({ + run: function() { + RecentProjectsManager.getInstance().removePath(projectPath) + } + }) + + ApplicationManager.getApplication().invokeLater(removeRecentProjectFunction) + """, runInEdt = true) } fun SearchContext.findOrNull(type: Class, locator: Locator) = diff --git a/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt b/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt index dee205107..f9949f0f5 100644 --- a/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt +++ b/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt @@ -2,14 +2,8 @@ package org.move.ui import com.intellij.openapi.util.io.toCanonicalPath import com.intellij.remoterobot.RemoteRobot -import com.intellij.remoterobot.fixtures.ComponentFixture -import com.intellij.remoterobot.steps.CommonSteps -import com.intellij.remoterobot.utils.Locators import com.intellij.remoterobot.utils.waitFor -import org.junit.jupiter.api.MethodOrderer -import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.io.TempDir import org.move.ui.fixtures.* @@ -19,13 +13,11 @@ import java.io.File import java.nio.file.Path import java.nio.file.Paths import java.time.Duration -import javax.swing.JMenu const val APTOS_LOCAL_PATH = "/home/mkurnikov/bin/aptos" const val SUI_LOCAL_PATH = "/home/mkurnikov/bin/sui" @ExtendWith(RemoteRobotExtension::class) -@TestMethodOrder(MethodOrderer.OrderAnnotation::class) class NewProjectTest { init { StepsLogger.init() @@ -47,8 +39,7 @@ class NewProjectTest { } @Test - @Order(1) - fun `new project validation`(remoteRobot: RemoteRobot) = with(remoteRobot) { + fun `new project validation`(robot: RemoteRobot) = with(robot) { welcomeFrame { selectNewProjectType("Move") } @@ -72,7 +63,6 @@ class NewProjectTest { assert(localPathTextField.isEnabled) { "Local path should be enabled if Local is selected" } localPathTextField.text = "" - Thread.sleep(1000) waitFor { versionLabel.value.contains("N/A") } waitFor { validationLabel?.value == "Invalid path to Aptos executable" } @@ -84,7 +74,6 @@ class NewProjectTest { suiRadioButton.select() localPathTextField.text = "" - Thread.sleep(1000) waitFor { versionLabel.value.contains("N/A") } waitFor { validationLabel?.value == "Invalid path to Sui executable" } @@ -97,8 +86,7 @@ class NewProjectTest { } @Test - @Order(2) - fun `create new aptos project with bundled cli`(remoteRobot: RemoteRobot) = with(remoteRobot) { + fun `create new aptos project with bundled cli`(robot: RemoteRobot) = with(robot) { welcomeFrame { selectNewProjectType("Move") } @@ -130,15 +118,12 @@ class NewProjectTest { } } - CommonSteps(remoteRobot).closeProject() - welcomeFrame { - removeProjectFromRecents(projectName) - } + closeProject() + removeLastRecentProject() } @Test - @Order(2) - fun `create new aptos project with local cli`(remoteRobot: RemoteRobot) = with(remoteRobot) { + fun `create new aptos project with local cli`(robot: RemoteRobot) = with(robot) { welcomeFrame { selectNewProjectType("Move") } @@ -172,15 +157,12 @@ class NewProjectTest { } } - CommonSteps(remoteRobot).closeProject() - welcomeFrame { - removeProjectFromRecents(projectName) - } + closeProject() + removeLastRecentProject() } @Test - @Order(3) - fun `create new sui project`(remoteRobot: RemoteRobot) = with(remoteRobot) { + fun `create new sui project`(robot: RemoteRobot) = with(robot) { welcomeFrame { selectNewProjectType("Move") } @@ -207,18 +189,16 @@ class NewProjectTest { } } - CommonSteps(remoteRobot).closeProject() - welcomeFrame { - removeProjectFromRecents(projectName) - } + closeProject() + removeLastRecentProject() } @Test - fun `import existing aptos package`(remoteRobot: RemoteRobot) = with(remoteRobot) { + fun `import existing aptos package`(robot: RemoteRobot) = with(robot) { copyExamplePackageToTempFolder("aptos_package") val tempPackagePath = tempFolder.toPath().resolve("aptos_package") - openProject(tempPackagePath) + openOrImportProject(tempPackagePath) ideaFrame { openMoveSettings { @@ -227,20 +207,16 @@ class NewProjectTest { } } - ideaFrame { - closeProject() - } - welcomeFrame { - removeProjectFromRecents("aptos_package") - } + closeProject() + removeLastRecentProject() } @Test - fun `import existing sui package`(remoteRobot: RemoteRobot) = with(remoteRobot) { + fun `import existing sui package`(robot: RemoteRobot) = with(robot) { copyExamplePackageToTempFolder("sui_package") val projectPath = tempFolder.toPath().resolve("sui_package") - openProject(projectPath) + openOrImportProject(projectPath) ideaFrame { openMoveSettings { @@ -249,22 +225,17 @@ class NewProjectTest { } } - CommonSteps(remoteRobot).closeProject() - welcomeFrame { - removeProjectFromRecents("sui_package") - } + closeProject() + removeLastRecentProject() } @Test - @Order(2) - fun `explicit sui blockchain setting should retain even if wrong`(remoteRobot: RemoteRobot) = with(remoteRobot) { + fun `explicit sui blockchain setting should retain even if wrong`(robot: RemoteRobot) = with(robot) { copyExamplePackageToTempFolder("aptos_package") // opens as Aptos package val projectPath = tempFolder.toPath().resolve("aptos_package") - welcomeFrame { - openProjectAt(projectPath) - } + openOrImportProject(projectPath) // mark project as Sui ideaFrame { @@ -273,10 +244,8 @@ class NewProjectTest { } } + closeProject() // reopen project to see that no ProjectActivity or OpenProcessor changed the setting - ideaFrame { - closeProject() - } welcomeFrame { openRecentProject("aptos_package") } @@ -288,15 +257,13 @@ class NewProjectTest { } } - CommonSteps(remoteRobot).closeProject() - welcomeFrame { - removeProjectFromRecents("aptos_package") - } + closeProject() + removeLastRecentProject() } // TODO // @Test -// fun `no default compile configuration should be created in pycharm`(remoteRobot: RemoteRobot) = with(remoteRobot) { +// fun `no default compile configuration should be created in pycharm`(robot: RemoteRobot) = with(robot) { // // // } From 9cbb807e8df4108f18cf77b2a51bbef38e393d65 Mon Sep 17 00:00:00 2001 From: Maksim Kurnikov Date: Fri, 5 Apr 2024 16:03:12 +0300 Subject: [PATCH 14/14] ui-tests: extract closeProject() into tearDown, fix intermittent failure --- .../kotlin/org/move/ui/fixtures/IdeaFrame.kt | 2 + .../org/move/ui/fixtures/WelcomeFrame.kt | 15 +++++++- .../test/kotlin/org/move/ui/NewProjectTest.kt | 37 +++++++++++-------- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt index 59ee1f9b4..d0151bf1e 100644 --- a/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/IdeaFrame.kt @@ -36,6 +36,8 @@ class IdeaFrame( JMenuBarFixture::class.java, JMenuBarFixture.byType(), Duration.ofSeconds(5)) } + val inlineProgressPanel get() = find(byXpath("//div[@class='InlineProgressPanel']")) + private fun openSettingsDialog() { if (!remoteRobot.isMac()) { waitFor { diff --git a/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt b/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt index bd6b2a307..497e2b1f0 100644 --- a/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt +++ b/ui-tests/src/main/kotlin/org/move/ui/fixtures/WelcomeFrame.kt @@ -15,6 +15,7 @@ import com.intellij.remoterobot.stepsProcessing.step import com.intellij.remoterobot.utils.Locators import com.intellij.remoterobot.utils.keyboard import com.intellij.remoterobot.utils.waitFor +import com.intellij.remoterobot.utils.waitForIgnoringError import com.intellij.ui.dsl.builder.components.DslLabel import java.io.File import java.nio.file.Path @@ -56,10 +57,20 @@ fun RemoteRobot.openOrImportProject(@StepParameter("Project absolute path", "") ApplicationManager.getApplication().invokeLater(openProjectFunction) """, runInEdt = true) - // TODO: wait for status bar to stop processing things + // check that idea frame is opened, and no progress bar present +// val ideaFrame = find(timeout = Duration.ofSeconds(10)) +// waitFor { +// val progressPanel = ideaFrame.inlineProgressPanel +// progressPanel.findAll(byXpath("//div[@class='MyComponent']")).isEmpty() +// } } -fun RemoteRobot.closeProject() = CommonSteps(this).closeProject() +fun RemoteRobot.closeProject() { + CommonSteps(this).closeProject() + waitFor(description = "Wait for the Welcome screen to appear") { + findAll().isNotEmpty() + } +} @Step("Remove project from recents", "Remove project from recents") fun RemoteRobot.removeLastRecentProject() { diff --git a/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt b/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt index f9949f0f5..ca5a3da45 100644 --- a/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt +++ b/ui-tests/src/test/kotlin/org/move/ui/NewProjectTest.kt @@ -3,6 +3,7 @@ package org.move.ui import com.intellij.openapi.util.io.toCanonicalPath import com.intellij.remoterobot.RemoteRobot import com.intellij.remoterobot.utils.waitFor +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.io.TempDir @@ -38,6 +39,12 @@ class NewProjectTest { Thread.sleep(500) } + @AfterEach + fun tearDown(robot: RemoteRobot) = with(robot) { + closeProject() + removeLastRecentProject() + } + @Test fun `new project validation`(robot: RemoteRobot) = with(robot) { welcomeFrame { @@ -118,8 +125,8 @@ class NewProjectTest { } } - closeProject() - removeLastRecentProject() +// closeProject() +// removeLastRecentProject() } @Test @@ -157,8 +164,8 @@ class NewProjectTest { } } - closeProject() - removeLastRecentProject() +// closeProject() +// removeLastRecentProject() } @Test @@ -189,8 +196,8 @@ class NewProjectTest { } } - closeProject() - removeLastRecentProject() +// closeProject() +// removeLastRecentProject() } @Test @@ -207,8 +214,8 @@ class NewProjectTest { } } - closeProject() - removeLastRecentProject() +// closeProject() +// removeLastRecentProject() } @Test @@ -225,8 +232,8 @@ class NewProjectTest { } } - closeProject() - removeLastRecentProject() +// closeProject() +// removeLastRecentProject() } @Test @@ -244,11 +251,9 @@ class NewProjectTest { } } - closeProject() // reopen project to see that no ProjectActivity or OpenProcessor changed the setting - welcomeFrame { - openRecentProject("aptos_package") - } + closeProject() + openOrImportProject(projectPath) ideaFrame { openMoveSettings { @@ -257,8 +262,8 @@ class NewProjectTest { } } - closeProject() - removeLastRecentProject() +// closeProject() +// removeLastRecentProject() } // TODO