diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64e2ac6c..0eceb407 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -127,7 +127,7 @@ jobs: node-version: lts/* - name: Install elm tools - run: npm install --global elm elm-test elm-format elm-review lamdera + run: npm install --global elm elm-test elm-test-rs elm-format elm-review lamdera # Run tests - name: Run Tests diff --git a/src/main/kotlin/org/elm/ide/icons/ElmIcons.kt b/src/main/kotlin/org/elm/ide/icons/ElmIcons.kt index f37cc4ee..7bac03f2 100644 --- a/src/main/kotlin/org/elm/ide/icons/ElmIcons.kt +++ b/src/main/kotlin/org/elm/ide/icons/ElmIcons.kt @@ -27,6 +27,11 @@ object ElmIcons { val UNION_TYPE = getIcon("type.png") val TYPE_ALIAS = getIcon("type.png") + // TEST ICONS + + val RUN = getIcon("run.svg") + val RUN_ALL = getIcon("runAll.svg") + private fun getIcon(path: String): Icon { return IconLoader.getIcon("/icons/$path", ElmIcons::class.java) } diff --git a/src/main/kotlin/org/elm/ide/lineMarkers/ElmLineMarkerProvider.kt b/src/main/kotlin/org/elm/ide/lineMarkers/ElmLineMarkerProvider.kt index c3a6ff81..2dc1e665 100644 --- a/src/main/kotlin/org/elm/ide/lineMarkers/ElmLineMarkerProvider.kt +++ b/src/main/kotlin/org/elm/ide/lineMarkers/ElmLineMarkerProvider.kt @@ -42,7 +42,10 @@ class ElmLineMarkerProvider : LineMarkerProviderDescriptor() { private val optionProviders = mapOf( ElmExposureLineMarkerProvider.OPTION to { ElmExposureLineMarkerProvider() }, - ElmRecursiveCallLineMarkerProvider.OPTION to { ElmRecursiveCallLineMarkerProvider() } + ElmRecursiveCallLineMarkerProvider.OPTION to { ElmRecursiveCallLineMarkerProvider() }, + ElmTestModuleLineMarkerProvider.OPTION to { ElmTestModuleLineMarkerProvider() }, + ElmTestDescribeLineMarkerProvider.OPTION to { ElmTestDescribeLineMarkerProvider() }, + ElmTestSingleLineMarkerProvider.OPTION to { ElmTestSingleLineMarkerProvider() } ) private val OPTIONS = optionProviders.keys.toTypedArray() \ No newline at end of file diff --git a/src/main/kotlin/org/elm/ide/lineMarkers/ElmRunActions.kt b/src/main/kotlin/org/elm/ide/lineMarkers/ElmRunActions.kt new file mode 100644 index 00000000..605ed07d --- /dev/null +++ b/src/main/kotlin/org/elm/ide/lineMarkers/ElmRunActions.kt @@ -0,0 +1,44 @@ +package org.elm.ide.lineMarkers + +import com.intellij.execution.ProgramRunnerUtil +import com.intellij.execution.executors.DefaultRunExecutor +import com.intellij.execution.impl.SingleConfigurationConfigurable +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.psi.PsiElement +import org.elm.ide.test.run.ElmTestRunConfiguration +import org.elm.ide.test.run.ElmTestRunConfigurationSettingsBuilder + +/** * Action to run all tests for a given file or directory */ +class RunAllTestsAction(private val element: PsiElement) : AnAction("Run All Tests") { + override fun actionPerformed(event: AnActionEvent) { + ProgramRunnerUtil.executeConfiguration( + ElmTestRunConfigurationSettingsBuilder.createAndRegisterFromElement(element), + DefaultRunExecutor.getRunExecutorInstance() + ) + } +} + +/** * Action to run a test filtered by test or describe description */ +class RunFilteredTestAction(private val element: PsiElement, private val filter: String) : AnAction("Run Filtered Test") { + override fun actionPerformed(event: AnActionEvent) { + ProgramRunnerUtil.executeConfiguration( + ElmTestRunConfigurationSettingsBuilder.createAndRegisterFromElement(element, filter), + DefaultRunExecutor.getRunExecutorInstance() + ) + } +} + +/** * Creates a run configuration and opens editor */ +class ModifyRunConfiguration(private val element: PsiElement, private val filter: String? = null) : AnAction("Modify Run Configuration") { + override fun actionPerformed(event: AnActionEvent) { + ShowSettingsUtil.getInstance().editConfigurable( + element.project, + SingleConfigurationConfigurable.editSettings( + ElmTestRunConfigurationSettingsBuilder.createAndRegisterFromElement(element, filter), + null + ) + ) + } +} diff --git a/src/main/kotlin/org/elm/ide/lineMarkers/ElmTestDescribeLineMarkerProvider.kt b/src/main/kotlin/org/elm/ide/lineMarkers/ElmTestDescribeLineMarkerProvider.kt new file mode 100644 index 00000000..f347b255 --- /dev/null +++ b/src/main/kotlin/org/elm/ide/lineMarkers/ElmTestDescribeLineMarkerProvider.kt @@ -0,0 +1,32 @@ +package org.elm.ide.lineMarkers + +import com.intellij.codeInsight.daemon.GutterIconDescriptor +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.psi.PsiElement +import org.elm.ide.icons.ElmIcons +import org.elm.ide.test.core.ElmTestElementNavigator +import org.elm.workspace.elmToolchain + +/** Handles adding a gutter icon for running tests under a describe */ +class ElmTestDescribeLineMarkerProvider : ElmTestLineMarkerProvider() { + companion object { + val OPTION = GutterIconDescriptor.Option("elm.testDescribe", "Test describe", ElmIcons.RUN) + } + + /** Add gutter icons for the describe line */ + override fun shouldAddGutterIcon(element: PsiElement): Boolean { + return element.project.elmToolchain.isElmTestRsEnabled && element.text == "describe" + } + + override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? { + if (!shouldAddGutterIcon(element)) return null + val filter = ElmTestElementNavigator.findTestDescription(element) ?: return null + + return createLineMarkerInfo( + element, + ElmIcons.RUN, + "Run describe", + listOf(RunFilteredTestAction(element, filter), ModifyRunConfiguration(element, filter)), + ) + } +} diff --git a/src/main/kotlin/org/elm/ide/lineMarkers/ElmTestLineMarkerProvider.kt b/src/main/kotlin/org/elm/ide/lineMarkers/ElmTestLineMarkerProvider.kt new file mode 100644 index 00000000..55bad147 --- /dev/null +++ b/src/main/kotlin/org/elm/ide/lineMarkers/ElmTestLineMarkerProvider.kt @@ -0,0 +1,62 @@ +package org.elm.ide.lineMarkers + +import com.intellij.codeInsight.daemon.GutterIconNavigationHandler +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.LineMarkerProvider +import com.intellij.ide.DataManager +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.psi.PsiElement +import com.intellij.ui.awt.RelativePoint +import com.intellij.util.FunctionUtil +import org.elm.workspace.elmToolchain +import java.awt.event.MouseEvent +import javax.swing.Icon + +/** * Abstract class to handle common operations for all test marker classes */ +abstract class ElmTestLineMarkerProvider : LineMarkerProvider { + /** * Create a popup with a list of given actions */ + private class PopupHandler(val actions: List) : GutterIconNavigationHandler { + override fun navigate(e: MouseEvent, elt: PsiElement) { + val group = DefaultActionGroup().apply { + addAll(actions) + } + + val popup = JBPopupFactory.getInstance() + .createActionGroupPopup( + null, + group, + DataManager.getInstance().getDataContext(e.component), + JBPopupFactory.ActionSelectionAid.MNEMONICS, + true + ) + + popup.show(RelativePoint(e)) + } + } + + /** * Returns true if the line should contain a gutter icon for the type of marker */ + abstract fun shouldAddGutterIcon(element: PsiElement): Boolean + + /** * Returns true if the file contains tests */ + protected fun isTestFile(element: PsiElement): Boolean { + return element.containingFile.text.contains("import Test exposing") + } + + /** Create a gutter icon on a given line with an icon, tooltip, and list of popup actions */ + protected fun createLineMarkerInfo(element: PsiElement, icon: Icon, tooltip: String, actions: List, ): LineMarkerInfo<*>? { + if (!isTestFile(element) || !shouldAddGutterIcon(element)) return null + + return LineMarkerInfo( + element, + element.textRange, + icon, + FunctionUtil.constant(tooltip), + PopupHandler(actions), + GutterIconRenderer.Alignment.LEFT + ) { tooltip } + } +} diff --git a/src/main/kotlin/org/elm/ide/lineMarkers/ElmTestModuleLineMarkerProvider.kt b/src/main/kotlin/org/elm/ide/lineMarkers/ElmTestModuleLineMarkerProvider.kt new file mode 100644 index 00000000..21f64344 --- /dev/null +++ b/src/main/kotlin/org/elm/ide/lineMarkers/ElmTestModuleLineMarkerProvider.kt @@ -0,0 +1,30 @@ +package org.elm.ide.lineMarkers + +import com.intellij.codeInsight.daemon.GutterIconDescriptor +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.psi.PsiElement +import org.elm.ide.icons.ElmIcons +import org.elm.lang.core.psi.ElmTypes + +/** * Handles adding a gutter icon for running all tests in a module */ +class ElmTestModuleLineMarkerProvider : ElmTestLineMarkerProvider() { + companion object { + val OPTION = GutterIconDescriptor.Option("elm.testModule", "Test module", ElmIcons.RUN_ALL) + } + + /** * Add gutter icons for the module line */ + override fun shouldAddGutterIcon(element: PsiElement): Boolean { + return element.node.elementType == ElmTypes.MODULE + } + + override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? { + if (!shouldAddGutterIcon(element)) return null + + return createLineMarkerInfo( + element, + ElmIcons.RUN_ALL, + "Run all tests in this module", + listOf(RunAllTestsAction(element), ModifyRunConfiguration(element)), + ) + } +} diff --git a/src/main/kotlin/org/elm/ide/lineMarkers/ElmTestSingleLineMarkerProvider.kt b/src/main/kotlin/org/elm/ide/lineMarkers/ElmTestSingleLineMarkerProvider.kt new file mode 100644 index 00000000..13674f35 --- /dev/null +++ b/src/main/kotlin/org/elm/ide/lineMarkers/ElmTestSingleLineMarkerProvider.kt @@ -0,0 +1,32 @@ +package org.elm.ide.lineMarkers + +import com.intellij.codeInsight.daemon.GutterIconDescriptor +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.psi.PsiElement +import org.elm.ide.icons.ElmIcons +import org.elm.ide.test.core.ElmTestElementNavigator +import org.elm.workspace.elmToolchain + +/** * Handles adding a gutter icon for running a specific test */ +class ElmTestSingleLineMarkerProvider : ElmTestLineMarkerProvider() { + companion object { + val OPTION = GutterIconDescriptor.Option("elm.testSingle", "Test single", ElmIcons.RUN) + } + + /** * Add gutter icons for the test line */ + override fun shouldAddGutterIcon(element: PsiElement): Boolean { + return element.project.elmToolchain.isElmTestRsEnabled && element.text == "test" + } + + override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? { + if (!shouldAddGutterIcon(element)) return null + val filter = ElmTestElementNavigator.findTestDescription(element) ?: return null + + return createLineMarkerInfo( + element, + ElmIcons.RUN, + "Run test", + listOf(RunFilteredTestAction(element, filter), ModifyRunConfiguration(element, filter)), + ) + } +} diff --git a/src/main/kotlin/org/elm/ide/test/core/ElmTestElementNavigator.kt b/src/main/kotlin/org/elm/ide/test/core/ElmTestElementNavigator.kt new file mode 100644 index 00000000..4d4cc235 --- /dev/null +++ b/src/main/kotlin/org/elm/ide/test/core/ElmTestElementNavigator.kt @@ -0,0 +1,48 @@ +package org.elm.ide.test.core + +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiWhiteSpace +import org.elm.lang.core.psi.elements.ElmBinOpExpr +import org.elm.lang.core.psi.elements.ElmFunctionCallExpr +import org.elm.lang.core.psi.elements.ElmStringConstantExpr + +object ElmTestElementNavigator { + /** + * Find the test or describe description text without surrounding quotes + * + * This will only match + * - test "this works" + * - describe "this test" + * + * Exactly and not any programatic calls + */ + fun findTestDescription(element: PsiElement?): String? { + if (element == null) return null + val callExpr = getFunctionCallExpr(element) ?: return null + + // look for a string directly after the test or describe function call + return (callExpr + .children + .filterNot { it is PsiWhiteSpace || it is PsiComment } + .getOrNull(1) as? ElmStringConstantExpr) + ?.text + ?.removeSurrounding("\"") + } + + /** Find the next parent function call given a list of target names */ + private fun getFunctionCallExpr(element: PsiElement?, targets: List = listOf("test", "describe")): ElmFunctionCallExpr? { + var current = element + while (current != null) { + val unwrapped = if (current is ElmBinOpExpr) { + // handles creating a run config from in a test body + current.children.firstOrNull { it is ElmFunctionCallExpr } ?: current + } else { + current + } + if (unwrapped is ElmFunctionCallExpr && unwrapped.target.text.substringAfterLast(".") in targets) return unwrapped + current = current.parent + } + return null + } +} diff --git a/src/main/kotlin/org/elm/ide/test/core/ElmTestJsonProcessor.kt b/src/main/kotlin/org/elm/ide/test/core/ElmTestJsonProcessor.kt index 1362f189..0c3670b4 100644 --- a/src/main/kotlin/org/elm/ide/test/core/ElmTestJsonProcessor.kt +++ b/src/main/kotlin/org/elm/ide/test/core/ElmTestJsonProcessor.kt @@ -12,6 +12,7 @@ import org.elm.ide.test.core.LabelUtils.toLocationUrl import org.elm.ide.test.core.json.CompileErrors import org.elm.ide.test.core.json.Error import java.nio.file.Path +import kotlin.math.roundToLong /** * Processes events from a test run by elm-test. @@ -75,7 +76,7 @@ class ElmTestJsonProcessor(private val testsRelativeDirPath: String) { fun testEvents(path: Path, obj: JsonObject): Sequence { return when (getStatus(obj)) { "pass" -> { - val duration = java.lang.Long.parseLong(obj.get("duration").asString) + val duration = obj.get("duration").asDouble.roundToLong() sequenceOf(newTestStartedEvent(path)) .plus(newTestFinishedEvent(path, duration)) } diff --git a/src/main/kotlin/org/elm/ide/test/run/ElmTestRunConfiguration.kt b/src/main/kotlin/org/elm/ide/test/run/ElmTestRunConfiguration.kt index e68e3157..65060a79 100644 --- a/src/main/kotlin/org/elm/ide/test/run/ElmTestRunConfiguration.kt +++ b/src/main/kotlin/org/elm/ide/test/run/ElmTestRunConfiguration.kt @@ -9,7 +9,14 @@ import com.intellij.openapi.options.SettingsEditor import com.intellij.openapi.project.Project import com.intellij.openapi.util.InvalidDataException import com.intellij.openapi.util.WriteExternalException +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager import org.elm.ide.test.run.ElmTestConfigurationFactory.Companion.RUN_ICON +import org.elm.lang.core.psi.ElmFile import org.jdom.Element import java.nio.file.Paths @@ -18,6 +25,12 @@ class ElmTestRunConfiguration internal constructor(project: Project, factory: Co var options = Options() + override fun clone(): RunConfiguration { + val copy = super.clone() as ElmTestRunConfiguration + copy.options = options.copy() + return copy + } + override fun getConfigurationEditor(): SettingsEditor = ElmTestSettingsEditor(project) @@ -26,8 +39,59 @@ class ElmTestRunConfiguration internal constructor(project: Project, factory: Co override fun getState(executor: Executor, executionEnvironment: ExecutionEnvironment) = ElmTestRunProfileState(executionEnvironment, this) - class Options { - var elmFolder: String? = null + data class Options(var elmFolder: String? = null, var filteredTestConfig: FilteredTest? = null) + + data class FilteredTest(val filePath: String, val moduleName: String, val testIsDirectory: Boolean, val filter: String?) { + companion object { + fun from(path: String?, project: Project, filter: String? = null): FilteredTest? { + if (path.isNullOrBlank()) return null + + val virtualFile = VirtualFileManager.getInstance().findFileByUrl("file://$path") ?: return null + + return from(virtualFile, project, filter) + } + + fun from(path: String?, moduleName: String?, filter: String? = null): FilteredTest? { + if (path.isNullOrBlank() || moduleName.isNullOrBlank()) return null + val isDirectory = !path.substringAfterLast('/').contains('.') + + return FilteredTest(path, moduleName, isDirectory, filter) + } + + fun from(virtualFile: VirtualFile, project: Project, filter: String? = null): FilteredTest? { + val psiManager = PsiManager.getInstance(project) + + if (virtualFile.isDirectory) { + val psiDirectory = psiManager.findDirectory(virtualFile) ?: return null + return from(psiDirectory, filter) + } else { + val psiFile = psiManager.findFile(virtualFile) ?: return null + return from(psiFile, filter) + } + } + + fun from(element: PsiElement, filter: String? = null): FilteredTest? { + return when (element) { + is PsiDirectory -> from(element, filter) + else -> element.containingFile?.let { from(it, filter) } + } + } + + fun from(psiFile: PsiFile, filter: String? = null): FilteredTest? { + val elmFile = psiFile as? ElmFile ?: return null + val moduleName = elmFile.getModuleDecl()?.name ?: return null + + return FilteredTest(psiFile.virtualFile.path, moduleName, false, filter) + } + + fun from(psiDirectory: PsiDirectory, filter: String? = null): FilteredTest? { + return FilteredTest(psiDirectory.virtualFile.path, psiDirectory.name, true, filter) + } + } + + fun runnableFilePath(): String { + return if (testIsDirectory) "$filePath/**/*.elm" else filePath + } } override fun getIcon() = RUN_ICON @@ -43,8 +107,21 @@ class ElmTestRunConfiguration internal constructor(project: Project, factory: Co } override fun suggestedName(): String? { - val elmFolder = options.elmFolder ?: return null - return "Tests in ${Paths.get(elmFolder).fileName}" + val elmFolder = options.elmFolder + + if (!options.filteredTestConfig?.moduleName.isNullOrBlank()) { + return if (options.filteredTestConfig?.filter.isNullOrBlank()) { + "Tests in ${options.filteredTestConfig?.moduleName}" + } else { + "Tests in ${options.filteredTestConfig?.moduleName}: ${options.filteredTestConfig?.filter}" + } + } + + if (elmFolder != null) { + return "Tests in ${Paths.get(elmFolder).fileName}" + } + + return null } companion object { @@ -52,17 +129,30 @@ class ElmTestRunConfiguration internal constructor(project: Project, factory: Co // fun writeOptions(options: Options, element: Element) { - val e = Element(ElmTestRunConfiguration::class.java.simpleName) - if (options.elmFolder != null) { - e.setAttribute("elm-folder", options.elmFolder) + val name = ElmTestRunConfiguration::class.java.simpleName + val e = element.getChild(name) ?: Element(name).also { element.addContent(it) } + + e.setAttribute("elm-folder", options.elmFolder ?: "") + if (options.filteredTestConfig != null) { + e.setAttribute("test-file-path", options.filteredTestConfig?.filePath) + e.setAttribute("test-file-module", options.filteredTestConfig?.moduleName) + if (!options.filteredTestConfig?.filter.isNullOrBlank()) { + e.setAttribute("test-filter", options.filteredTestConfig?.filter) + } } - element.addContent(e) } fun readOptions(element: Element): Options { return Options().apply { val name = ElmTestRunConfiguration::class.java.simpleName - elmFolder = element.getChild(name)?.getAttribute("elm-folder")?.value + val child = element.getChild(name) + elmFolder = child?.getAttribute("elm-folder")?.value + filteredTestConfig = FilteredTest.from( + child?.getAttribute("test-file-path")?.value, + child?.getAttribute("test-file-module")?.value, + child?.getAttribute("test-filter")?.value + ) + } } } diff --git a/src/main/kotlin/org/elm/ide/test/run/ElmTestRunConfigurationProducer.kt b/src/main/kotlin/org/elm/ide/test/run/ElmTestRunConfigurationProducer.kt index 41092073..07ecdc31 100644 --- a/src/main/kotlin/org/elm/ide/test/run/ElmTestRunConfigurationProducer.kt +++ b/src/main/kotlin/org/elm/ide/test/run/ElmTestRunConfigurationProducer.kt @@ -4,23 +4,47 @@ import com.intellij.execution.actions.ConfigurationContext import com.intellij.execution.actions.LazyRunConfigurationProducer import com.intellij.openapi.util.Ref import com.intellij.psi.PsiElement +import org.elm.ide.test.core.ElmTestElementNavigator +import org.elm.workspace.elmToolchain import org.elm.workspace.elmWorkspace class ElmTestRunConfigurationProducer : LazyRunConfigurationProducer() { override fun getConfigurationFactory() = - ElmTestConfigurationFactory(ElmTestRunConfigurationType()) + ElmTestRunConfigurationType.instance.configurationFactories.single() override fun setupConfigurationFromContext(configuration: ElmTestRunConfiguration, context: ConfigurationContext, sourceElement: Ref): Boolean { val elmFolder = getCandidateElmFolder(context) ?: return false + val vfile = context.location?.virtualFile + configuration.options.elmFolder = elmFolder + if (vfile != null) { + configuration.options.filteredTestConfig = ElmTestRunConfiguration.FilteredTest.from(sourceElement.get(), getFilter(context)) + } + configuration.setGeneratedName() + return true } override fun isConfigurationFromContext(configuration: ElmTestRunConfiguration, context: ConfigurationContext): Boolean { val elmFolder = getCandidateElmFolder(context) ?: return false - return elmFolder == configuration.options.elmFolder + val vfile = context.location?.virtualFile + + var result = configuration.options.elmFolder == elmFolder + val config = configuration.options.filteredTestConfig + + if (config == null) return result + + if (config.moduleName.isNotBlank()) { + result = result && config.filePath == vfile?.path + } + + if (!config.filter.isNullOrBlank()) { + result = result && config.filter == getFilter(context) + } + + return result } private fun getCandidateElmFolder(context: ConfigurationContext): String? { @@ -28,4 +52,9 @@ class ElmTestRunConfigurationProducer : LazyRunConfigurationProducer
- + @@ -18,7 +18,7 @@ - + @@ -27,6 +27,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/kotlin/org/elm/ide/test/run/ElmTestSettingsEditor.kt b/src/main/kotlin/org/elm/ide/test/run/ElmTestSettingsEditor.kt index 72a1294e..ed7a6afa 100644 --- a/src/main/kotlin/org/elm/ide/test/run/ElmTestSettingsEditor.kt +++ b/src/main/kotlin/org/elm/ide/test/run/ElmTestSettingsEditor.kt @@ -17,16 +17,23 @@ import com.intellij.openapi.options.SettingsEditor import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox import org.elm.ide.test.core.ElmProjectTestsHelper +import org.elm.workspace.elmToolchain import javax.swing.JComponent import javax.swing.JPanel +import javax.swing.JTextField class ElmTestSettingsEditor internal constructor(project: Project) : SettingsEditor() { private val helper: ElmProjectTestsHelper = ElmProjectTestsHelper(project) private var myPanel: JPanel? = null private var projectChooser: ComboBox? = null + private var testFilePathField: JTextField? = null + private var testFilter: JTextField? = null + private var isElmTestRsEnabled: Boolean = project.elmToolchain.isElmTestRsEnabled override fun createEditor(): JComponent { helper.allNames().forEach { projectChooser!!.addItem(it) } + testFilter?.setEnabled(isElmTestRsEnabled) + return this.myPanel!! } @@ -35,12 +42,18 @@ class ElmTestSettingsEditor internal constructor(project: Project) : SettingsEdi configuration.options.elmFolder?.let { helper.nameByProjectDirPath(it) } + this.testFilePathField!!.text = configuration.options.filteredTestConfig?.filePath + this.testFilter!!.text = configuration.options.filteredTestConfig?.filter } override fun applyEditorTo(configuration: ElmTestRunConfiguration) { - val selectedItem = projectChooser!!.selectedItem - if (selectedItem != null && selectedItem is String) { - configuration.options.elmFolder = helper.projectDirPathByName(selectedItem) + val selectedProject = projectChooser!!.selectedItem + if (selectedProject != null && selectedProject is String) { + configuration.options.elmFolder = helper.projectDirPathByName(selectedProject) } + + val field = testFilePathField!!.text.trim().ifEmpty { null } + val filter = testFilter!!.text.trim().ifEmpty { null } + configuration.options.filteredTestConfig = ElmTestRunConfiguration.FilteredTest.from(field, configuration.project, filter) } } diff --git a/src/main/kotlin/org/elm/workspace/ElmToolchain.kt b/src/main/kotlin/org/elm/workspace/ElmToolchain.kt index 0c58d8b8..97fef513 100644 --- a/src/main/kotlin/org/elm/workspace/ElmToolchain.kt +++ b/src/main/kotlin/org/elm/workspace/ElmToolchain.kt @@ -14,8 +14,9 @@ const val elmCompilerTool = "elm" const val lamderaCompilerTool = "lamdera" const val elmFormatTool = "elm-format" const val elmTestTool = "elm-test" +const val elmTestRsTool = "elm-test-rs" const val elmReviewTool = "elm-review" -val elmTools = listOf(elmCompilerTool, lamderaCompilerTool, elmFormatTool, elmTestTool, elmReviewTool) +val elmTools = listOf(elmCompilerTool, lamderaCompilerTool, elmFormatTool, elmTestTool, elmTestRsTool, elmReviewTool) data class ElmToolchain( val elmCompilerPath: Path?, @@ -23,15 +24,17 @@ data class ElmToolchain( val elmFormatPath: Path?, val elmTestPath: Path?, val elmReviewPath: Path?, + val isElmTestRsEnabled: Boolean, val isElmFormatOnSaveEnabled: Boolean ) { - constructor(elmCompilerPath: String, lamderaCompilerPath: String, elmFormatPath: String, elmTestPath: String, elmReviewPath: String, isElmFormatOnSaveEnabled: Boolean) : + constructor(elmCompilerPath: String, lamderaCompilerPath: String, elmFormatPath: String, elmTestPath: String, elmReviewPath: String, isElmTestRsEnabled: Boolean, isElmFormatOnSaveEnabled: Boolean) : this( if (elmCompilerPath.isNotBlank() && Files.exists(Paths.get(elmCompilerPath))) Paths.get(elmCompilerPath) else null, if (lamderaCompilerPath.isNotBlank() && Files.exists(Paths.get(lamderaCompilerPath))) Paths.get(lamderaCompilerPath) else null, if (elmFormatPath.isNotBlank() && Files.exists(Paths.get(elmFormatPath))) Paths.get(elmFormatPath) else null, if (elmTestPath.isNotBlank() && Files.exists(Paths.get(elmTestPath))) Paths.get(elmTestPath) else null, if (elmReviewPath.isNotBlank() && Files.exists(Paths.get(elmReviewPath))) Paths.get(elmReviewPath) else null, + isElmTestRsEnabled, isElmFormatOnSaveEnabled ) @@ -71,7 +74,7 @@ data class ElmToolchain( elmCompilerPath = elmCompilerPath ?: suggestions[elmCompilerTool], lamderaCompilerPath = lamderaCompilerPath ?: suggestions[lamderaCompilerTool], elmFormatPath = elmFormatPath ?: suggestions[elmFormatTool], - elmTestPath = elmTestPath ?: suggestions[elmTestTool], + elmTestPath = if (isElmTestRsEnabled) elmTestPath ?: suggestions[elmTestRsTool] else elmTestPath ?: suggestions[elmTestTool], elmReviewPath = elmReviewPath ?: suggestions[elmReviewTool] ) } @@ -89,6 +92,7 @@ data class ElmToolchain( */ const val SIDECAR_FILENAME = "elm.intellij.json" + const val DEFAULT_ELM_TEST_RS = false const val DEFAULT_FORMAT_ON_SAVE = true /** @@ -100,6 +104,7 @@ data class ElmToolchain( elmFormatPath = null, elmTestPath = null, elmReviewPath = null, + isElmTestRsEnabled = DEFAULT_ELM_TEST_RS, isElmFormatOnSaveEnabled = DEFAULT_FORMAT_ON_SAVE ) diff --git a/src/main/kotlin/org/elm/workspace/ElmWorkspaceService.kt b/src/main/kotlin/org/elm/workspace/ElmWorkspaceService.kt index 5eb897b9..73db3410 100644 --- a/src/main/kotlin/org/elm/workspace/ElmWorkspaceService.kt +++ b/src/main/kotlin/org/elm/workspace/ElmWorkspaceService.kt @@ -34,6 +34,7 @@ import org.elm.openapiext.* import org.elm.utils.MyDirectoryIndex import org.elm.utils.joinAll import org.elm.utils.runAsyncTask +import org.elm.workspace.ElmToolchain.Companion.DEFAULT_ELM_TEST_RS import org.elm.workspace.ElmToolchain.Companion.DEFAULT_FORMAT_ON_SAVE import org.elm.workspace.ElmToolchain.Companion.ELM_JSON import org.elm.workspace.commandLineTools.ElmCLI @@ -92,6 +93,7 @@ class ElmWorkspaceService(val intellijProject: Project) : PersistentStateCompone val lamderaCompilerPath: String = "", val elmFormatPath: String = "", val elmTestPath: String = "", + val isElmTestRsEnabled: Boolean = DEFAULT_ELM_TEST_RS, val elmReviewPath: String = "", val isElmFormatOnSaveEnabled: Boolean = DEFAULT_FORMAT_ON_SAVE ) @@ -106,6 +108,7 @@ class ElmWorkspaceService(val intellijProject: Project) : PersistentStateCompone elmFormatPath = raw.elmFormatPath, elmTestPath = raw.elmTestPath, elmReviewPath = raw.elmReviewPath, + isElmTestRsEnabled = raw.isElmTestRsEnabled, isElmFormatOnSaveEnabled = raw.isElmFormatOnSaveEnabled ) return Settings(toolchain = toolchain) @@ -136,6 +139,7 @@ class ElmWorkspaceService(val intellijProject: Project) : PersistentStateCompone elmFormatPath = toolchain.elmFormatPath.toString(), elmTestPath = toolchain.elmTestPath.toString(), elmReviewPath = toolchain.elmReviewPath.toString(), + isElmTestRsEnabled = toolchain.isElmTestRsEnabled, isElmFormatOnSaveEnabled = toolchain.isElmFormatOnSaveEnabled ) } @@ -439,6 +443,7 @@ class ElmWorkspaceService(val intellijProject: Project) : PersistentStateCompone settingsElement.setAttribute("lamderaCompilerPath", raw.lamderaCompilerPath) settingsElement.setAttribute("elmFormatPath", raw.elmFormatPath) settingsElement.setAttribute("elmTestPath", raw.elmTestPath) + settingsElement.setAttribute("isElmTestRsEnabled", raw.isElmTestRsEnabled.toString()) settingsElement.setAttribute("elmReviewPath", raw.elmReviewPath) settingsElement.setAttribute("isElmFormatOnSaveEnabled", raw.isElmFormatOnSaveEnabled.toString()) @@ -458,6 +463,10 @@ class ElmWorkspaceService(val intellijProject: Project) : PersistentStateCompone val elmFormatPath = settingsElement.getAttributeValue("elmFormatPath") ?: "" val elmTestPath = settingsElement.getAttributeValue("elmTestPath") ?: "" val elmReviewPath = settingsElement.getAttributeValue("elmReviewPath") ?: "" + val isElmTestRsEnabled = settingsElement + .getAttributeValue("isElmTestRsEnabled") + .takeIf { it != null && it.isNotBlank() }?.toBoolean() + ?: DEFAULT_ELM_TEST_RS val isElmFormatOnSaveEnabled = settingsElement .getAttributeValue("isElmFormatOnSaveEnabled") .takeIf { it != null && it.isNotBlank() }?.toBoolean() @@ -469,6 +478,7 @@ class ElmWorkspaceService(val intellijProject: Project) : PersistentStateCompone lamderaCompilerPath = lamderaCompilerPath, elmFormatPath = elmFormatPath, elmTestPath = elmTestPath, + isElmTestRsEnabled = isElmTestRsEnabled, elmReviewPath = elmReviewPath, isElmFormatOnSaveEnabled = isElmFormatOnSaveEnabled ) diff --git a/src/main/kotlin/org/elm/workspace/commandLineTools/ElmTestCLI.kt b/src/main/kotlin/org/elm/workspace/commandLineTools/ElmTestCLI.kt index f1da4362..45d29036 100644 --- a/src/main/kotlin/org/elm/workspace/commandLineTools/ElmTestCLI.kt +++ b/src/main/kotlin/org/elm/workspace/commandLineTools/ElmTestCLI.kt @@ -7,13 +7,16 @@ import com.intellij.execution.process.ProcessHandler import com.intellij.openapi.diagnostic.debug import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project +import org.elm.ide.test.run.ElmTestRunConfiguration.FilteredTest import org.elm.openapiext.GeneralCommandLine import org.elm.openapiext.Result import org.elm.openapiext.execute import org.elm.workspace.ElmProject import org.elm.workspace.ParseException import org.elm.workspace.Version +import org.elm.workspace.elmTestRsTool import org.elm.workspace.elmTestTool +import org.elm.workspace.elmToolchain import java.nio.file.Path private val log = logger() @@ -30,18 +33,28 @@ class ElmTestCLI(private val executablePath: Path) { * * @param elmCompilerPath The path to the Elm compiler. * @param elmProject The [ElmProject] containing the tests to be run. + * @param filteredTest An instance of the filter parameters for the test + * @param isElmTestRsEnabled True if using elm-test-rs */ - fun runTestsProcessHandler(elmCompilerPath: Path, elmProject: ElmProject): ProcessHandler { + fun runTestsProcessHandler(elmCompilerPath: Path, elmProject: ElmProject, filteredTest: FilteredTest?, isElmTestRsEnabled: Boolean): ProcessHandler { val commandLine = GeneralCommandLine(executablePath.toString(), "--report=json") .withWorkDirectory(elmProject.projectDirPath.toString()) .withParameters("--compiler", elmCompilerPath.toString()) .withRedirectErrorStream(true) .withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE) + if (filteredTest != null) { + log.debug { """Test file or directory was provided: "${elmProject.testsRelativeDirPath}". Will specify this path as argument to elm-test.""" } + + if (isElmTestRsEnabled && !filteredTest.filter.isNullOrBlank()) { + commandLine.withParameters("--filter", filteredTest.filter) + } + + commandLine.withParameters(filteredTest.runnableFilePath()) // By default elm-test will process tests in a folder called "tests", under the current working directory // (in this case elmProject.projectDirPath). If the project has a custom location for tests we need to supply a // path to that folder. - if (elmProject.isCustomTestsDir) { + } else if (elmProject.isCustomTestsDir) { log.debug { """Tests are in custom location: "${elmProject.testsRelativeDirPath}". Will specify this path as argument to elm-test.""" } commandLine.withParameters(elmProject.testsRelativeDirPath) } else { @@ -53,11 +66,13 @@ class ElmTestCLI(private val executablePath: Path) { fun queryVersion(project: Project): Result { + val tool = if (project.elmToolchain.isElmTestRsEnabled) elmTestRsTool else elmTestTool + // Output of `elm-test --version` is a single line containing the version number, // e.g. `0.19.0-beta9\n`, trimming off the "-betaN" suffix, if present. val firstLine = try { GeneralCommandLine(executablePath).withParameters("--version") - .execute(elmTestTool, project) + .execute(tool, project) .stdoutLines .firstOrNull() } catch (e: ExecutionException) { @@ -68,7 +83,13 @@ class ElmTestCLI(private val executablePath: Path) { return Result.Err("no output from elm-test") } - val trimmedFirstLine = firstLine.takeWhile { it != '-' } + val trimmedFirstLine = if (project.elmToolchain.isElmTestRsEnabled) { + if (!firstLine.contains("elm-test-rs")) return Result.Err("Could not parse elm-test-rs version: $firstLine") + + firstLine.removePrefix("elm-test-rs").trim() + } else { + firstLine.takeWhile { it != '-' } + } return try { Result.Ok(Version.parse(trimmedFirstLine)) diff --git a/src/main/kotlin/org/elm/workspace/ui/ElmWorkspaceConfigurable.kt b/src/main/kotlin/org/elm/workspace/ui/ElmWorkspaceConfigurable.kt index 92dd349e..87ca8f45 100644 --- a/src/main/kotlin/org/elm/workspace/ui/ElmWorkspaceConfigurable.kt +++ b/src/main/kotlin/org/elm/workspace/ui/ElmWorkspaceConfigurable.kt @@ -59,9 +59,14 @@ class ElmWorkspaceConfigurable( private val elmFormatOnSaveCheckbox = JCheckBox() private val elmFormatShortcutLabel = HyperlinkLabel() private val elmTestVersionLabel = JLabel() + private val elmTestRsCheckbox = JCheckBox() private val elmReviewVersionLabel = JLabel() override fun createComponent(): JComponent { + elmTestRsCheckbox.addChangeListener { + elmTestPathField.text = autoDiscoverPathTo(elmTestTool()) + update() + } elmFormatOnSaveCheckbox.addChangeListener { update() } elmFormatShortcutLabel.addHyperlinkListener { showActionShortcut(ElmExternalFormatAction.ID) @@ -69,25 +74,26 @@ class ElmWorkspaceConfigurable( val panel = layout { block("Elm Compiler") { - row("Location:", pathFieldPlusAutoDiscoverButton(elmPathField, elmCompilerTool)) + row("Location:", pathFieldPlusAutoDiscoverButton(elmPathField) { elmCompilerTool }) row("Version:", elmVersionLabel) } block(elmFormatTool) { - row("Location:", pathFieldPlusAutoDiscoverButton(elmFormatPathField, elmFormatTool)) + row("Location:", pathFieldPlusAutoDiscoverButton(elmFormatPathField) { elmFormatTool }) row("Version:", elmFormatVersionLabel) row("Keyboard shortcut:", elmFormatShortcutLabel) row("Run when file saved?", elmFormatOnSaveCheckbox) } block(elmTestTool) { - row("Location:", pathFieldPlusAutoDiscoverButton(elmTestPathField, elmTestTool)) + row("Location:", pathFieldPlusAutoDiscoverButton(elmTestPathField) { elmTestTool() }) row("Version:", elmTestVersionLabel) + row("Use elm-test-rs?:", elmTestRsCheckbox) } block(elmReviewTool) { - row("Location:", pathFieldPlusAutoDiscoverButton(elmReviewPathField, elmReviewTool)) + row("Location:", pathFieldPlusAutoDiscoverButton(elmReviewPathField) { elmReviewTool }) row("Version:", elmReviewVersionLabel) } block("Lamdera Compiler") { - row("Location:", pathFieldPlusAutoDiscoverButton(lamderaPathField, lamderaCompilerTool)) + row("Location:", pathFieldPlusAutoDiscoverButton(lamderaPathField) { lamderaCompilerTool }) row("Version:", lamderaVersionLabel) } block("") { @@ -107,11 +113,11 @@ class ElmWorkspaceConfigurable( return panel } - private fun pathFieldPlusAutoDiscoverButton(field: TextFieldWithBrowseButton, executableName: String): JPanel { + private fun pathFieldPlusAutoDiscoverButton(field: TextFieldWithBrowseButton, getExecutableName: () -> String): JPanel { val panel = JPanel().apply { layout = BoxLayout(this, BoxLayout.X_AXIS) } with(panel) { add(field) - add(JButton("Auto Discover").apply { addActionListener { field.text = autoDiscoverPathTo(executableName) } }) + add(JButton("Auto Discover").apply { addActionListener { field.text = autoDiscoverPathTo(getExecutableName()) } }) } return panel } @@ -127,6 +133,11 @@ class ElmWorkspaceConfigurable( keymapPanel.selectAction(actionId) } } + + fun elmTestTool(): String { + return if (isElmTestRsEnabledAndSelected()) elmTestRsTool else elmTestTool + } + data class Results( val compilerResult: Result, val lamderaResult: Result, @@ -242,7 +253,7 @@ class ElmWorkspaceConfigurable( } is Result.Err -> { when { - !elmTestPath.isValidFor(elmTestTool) -> { + !elmTestPath.isValidFor(elmTestTool()) -> { text = "" foreground = JBColor.foreground() } @@ -302,6 +313,7 @@ class ElmWorkspaceConfigurable( val elmFormatPath = settings?.elmFormatPath val isElmFormatOnSaveEnabled = settings?.isElmFormatOnSaveEnabled val elmTestPath = settings?.elmTestPath + val isElmTestRsEnabled = settings?.isElmTestRsEnabled val elmReviewPath = settings?.elmReviewPath if (elmCompilerPath != null) { @@ -317,6 +329,7 @@ class ElmWorkspaceConfigurable( if (elmTestPath != null) { elmTestPathField.text = elmTestPath } + elmTestRsCheckbox.isSelected = isElmTestRsEnabled == true if (elmReviewPath != null) { elmReviewPathField.text = elmReviewPath } @@ -331,11 +344,15 @@ class ElmWorkspaceConfigurable( elmFormatPath = elmFormatPathField.text, elmTestPath = elmTestPathField.text, elmReviewPath = elmReviewPathField.text, + isElmTestRsEnabled = isElmTestRsEnabledAndSelected(), isElmFormatOnSaveEnabled = isOnSaveHookEnabledAndSelected() ) } } + private fun isElmTestRsEnabledAndSelected() = + elmTestRsCheckbox.isEnabled && elmTestRsCheckbox.isSelected + private fun isOnSaveHookEnabledAndSelected() = elmFormatOnSaveCheckbox.isEnabled && elmFormatOnSaveCheckbox.isSelected @@ -346,6 +363,7 @@ class ElmWorkspaceConfigurable( || elmFormatPathField.text != settings.elmFormatPath || elmTestPathField.text != settings.elmTestPath || elmReviewPathField.text != settings.elmReviewPath + || isElmTestRsEnabledAndSelected() != settings.isElmTestRsEnabled || isOnSaveHookEnabledAndSelected() != settings.isElmFormatOnSaveEnabled } diff --git a/src/main/resources/icons/run.svg b/src/main/resources/icons/run.svg new file mode 100644 index 00000000..dd11f50d --- /dev/null +++ b/src/main/resources/icons/run.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/icons/runAll.svg b/src/main/resources/icons/runAll.svg new file mode 100644 index 00000000..9ba2ad2c --- /dev/null +++ b/src/main/resources/icons/runAll.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/icons/runAll_dark.svg b/src/main/resources/icons/runAll_dark.svg new file mode 100644 index 00000000..03616d3a --- /dev/null +++ b/src/main/resources/icons/runAll_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/icons/run_dark.svg b/src/main/resources/icons/run_dark.svg new file mode 100644 index 00000000..0c199c7d --- /dev/null +++ b/src/main/resources/icons/run_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/test/kotlin/org/elm/ide/lineMarkers/ElmTestTestLineMarkerProviderTest.kt b/src/test/kotlin/org/elm/ide/lineMarkers/ElmTestTestLineMarkerProviderTest.kt new file mode 100644 index 00000000..303288f5 --- /dev/null +++ b/src/test/kotlin/org/elm/ide/lineMarkers/ElmTestTestLineMarkerProviderTest.kt @@ -0,0 +1,71 @@ +package org.elm.ide.lineMarkers + +import org.elm.workspace.ElmToolchain +import org.elm.workspace.elmWorkspace +import org.junit.Test + + +class ElmTestTestLineMarkerProviderTest : ElmLineMarkerProviderTestBase() { + fun withElmTestRsEnabled(enabled: Boolean, test: () -> Unit) { + val toolchain = ElmToolchain( + elmCompilerPath = "", + lamderaCompilerPath = "", + elmFormatPath = "", + elmTestPath = "", + elmReviewPath = "", + isElmTestRsEnabled = enabled, + isElmFormatOnSaveEnabled = false + ) + + project.elmWorkspace.useToolchain(toolchain) + + test() + } + + @Test + fun `test with elm-test properly adds markers`() { + withElmTestRsEnabled(false) { + doTestByText( + """ +module Main --> Run all tests in this module + +import Test exposing (..) + +myTest : Test +myTest = + describe "my test" + [ test "1 == 1" <| \_ -> Expect.equal 1 1 + ] + """ + ) + } + } + + @Test + fun `test with elm-test-rs properly adds markers`() { + withElmTestRsEnabled(true) { + doTestByText( + """ +module Main --> Run all tests in this module + +import Test exposing (..) + +myTest : Test +myTest = + describe "my test" --> Run describe + [ test "1 == 1" <| \_ -> Expect.equal 1 1 --> Run test + ] + """ + ) + } + } + + + @Test + fun `test non-test module can't run tests`() = doTestByText( + """ +module Main exposing (..) + +""") + +} diff --git a/src/test/kotlin/org/elm/ide/test/core/ElmTestElementNavigatorTest.kt b/src/test/kotlin/org/elm/ide/test/core/ElmTestElementNavigatorTest.kt new file mode 100644 index 00000000..4734a140 --- /dev/null +++ b/src/test/kotlin/org/elm/ide/test/core/ElmTestElementNavigatorTest.kt @@ -0,0 +1,49 @@ +package org.elm.ide.test.core + +import org.elm.lang.ElmTestBase +import org.junit.Test + +class ElmTestElementNavigatorTest : ElmTestBase() { + @Test + fun `find description from test`() { + val psiFile = myFixture.configureByText("ElmTest.elm", getTestFile()) + val testElement = psiFile.findElementAt(psiFile.text.indexOf("test \"1 == 1\" <|")) + val result = ElmTestElementNavigator.findTestDescription(testElement) + + assertEquals("1 == 1", result) + } + + @Test + fun `find description from describe`() { + val psiFile = myFixture.configureByText("ElmTest.elm", getTestFile()) + val testElement = psiFile.findElementAt(psiFile.text.indexOf("describe \"my suite\"")) + val result = ElmTestElementNavigator.findTestDescription(testElement) + + assertEquals("my suite", result) + } + + @Test + fun `walk up to find first test`() { + val psiFile = myFixture.configureByText("ElmTest.elm", getTestFile()) + val testElement = psiFile.findElementAt(psiFile.text.indexOf("\\_ -> Expect.equal 1 1")) + val result = ElmTestElementNavigator.findTestDescription(testElement) + + assertEquals("1 == 1", result) + } + + private fun getTestFile(): String { + return """ + module Example exposing (..) + + import Test exposing (test) + import Expect + + suite : Test + suite = + describe "my suite" + [ test "1 == 1" <| + \_ -> Expect.equal 1 1 + ] + """.trimIndent() + } +} diff --git a/src/test/kotlin/org/elm/ide/test/run/ElmTestRunConfigurationTest.kt b/src/test/kotlin/org/elm/ide/test/run/ElmTestRunConfigurationTest.kt index e9e29017..d0142141 100644 --- a/src/test/kotlin/org/elm/ide/test/run/ElmTestRunConfigurationTest.kt +++ b/src/test/kotlin/org/elm/ide/test/run/ElmTestRunConfigurationTest.kt @@ -13,6 +13,71 @@ class ElmTestRunConfigurationTest { fun writeOptions() { val root = Element("ROOT") + val options = Options() + options.elmFolder = "folder" + options.filteredTestConfig = ElmTestRunConfiguration.FilteredTest.from("Foo.elm", "Foo") + + writeOptions(options, root) + + assertEquals(1, root.children.size.toLong()) + assertEquals(ElmTestRunConfiguration::class.java.simpleName, root.children[0].name) + assertEquals(3, root.children[0].attributes.size.toLong()) + assertEquals("elm-folder", root.children[0].attributes[0].name) + assertEquals("folder", root.children[0].attributes[0].value) + assertEquals("test-file-path", root.children[0].attributes[1].name) + assertEquals("Foo.elm", root.children[0].attributes[1].value) + assertEquals(false, options.filteredTestConfig?.testIsDirectory) + assertEquals("Foo.elm", options.filteredTestConfig?.runnableFilePath()) + } + + @Test + fun writeOptionsWithDirectory() { + val root = Element("ROOT") + + val options = Options() + options.elmFolder = "folder" + options.filteredTestConfig = ElmTestRunConfiguration.FilteredTest.from("foo", "Foo") + + writeOptions(options, root) + + assertEquals(1, root.children.size.toLong()) + assertEquals(ElmTestRunConfiguration::class.java.simpleName, root.children[0].name) + assertEquals(3, root.children[0].attributes.size.toLong()) + assertEquals("elm-folder", root.children[0].attributes[0].name) + assertEquals("folder", root.children[0].attributes[0].value) + assertEquals("test-file-path", root.children[0].attributes[1].name) + assertEquals("foo", root.children[0].attributes[1].value) + assertEquals(true, options.filteredTestConfig?.testIsDirectory) + assertEquals("foo/**/*.elm", options.filteredTestConfig?.runnableFilePath()) + assertEquals(null, options.filteredTestConfig?.filter) + } + + @Test + fun writeOptionsWithFilter() { + val root = Element("ROOT") + + val options = Options() + options.elmFolder = "folder" + options.filteredTestConfig = ElmTestRunConfiguration.FilteredTest.from("Foo.elm", "Foo", filter = "bar") + + writeOptions(options, root) + + assertEquals(1, root.children.size.toLong()) + assertEquals(ElmTestRunConfiguration::class.java.simpleName, root.children[0].name) + assertEquals(4, root.children[0].attributes.size.toLong()) + assertEquals("elm-folder", root.children[0].attributes[0].name) + assertEquals("folder", root.children[0].attributes[0].value) + assertEquals("test-file-path", root.children[0].attributes[1].name) + assertEquals("Foo.elm", root.children[0].attributes[1].value) + assertEquals(false, options.filteredTestConfig?.testIsDirectory) + assertEquals("Foo.elm", options.filteredTestConfig?.runnableFilePath()) + assertEquals("bar", options.filteredTestConfig?.filter) + } + + @Test + fun writeOptionsWithNullTestFile() { + val root = Element("ROOT") + val options = Options() options.elmFolder = "folder" @@ -29,6 +94,55 @@ class ElmTestRunConfigurationTest { fun roundTrip() { val root = Element("ROOT") + val options = Options() + options.elmFolder = "folder" + options.filteredTestConfig = ElmTestRunConfiguration.FilteredTest.from("Foo.elm", "foo") + + writeOptions(options, root) + val options2 = readOptions(root) + + assertEquals(options.elmFolder, options2.elmFolder) + assertEquals(options.filteredTestConfig, options2.filteredTestConfig) + assertEquals(options.filteredTestConfig?.runnableFilePath(), options2.filteredTestConfig?.runnableFilePath()) + } + + @Test + fun roundTripWithDirectory() { + val root = Element("ROOT") + + val options = Options() + options.elmFolder = "folder" + options.filteredTestConfig = ElmTestRunConfiguration.FilteredTest.from("foo", "foo") + + writeOptions(options, root) + val options2 = readOptions(root) + + assertEquals(options.elmFolder, options2.elmFolder) + assertEquals(options.filteredTestConfig, options2.filteredTestConfig) + assertEquals(options.filteredTestConfig?.runnableFilePath(), options2.filteredTestConfig?.runnableFilePath()) + } + + @Test + fun roundTripWithFilter() { + val root = Element("ROOT") + + val options = Options() + options.elmFolder = "folder" + options.filteredTestConfig = ElmTestRunConfiguration.FilteredTest.from("Foo.elm", "foo", filter = "bar") + + writeOptions(options, root) + val options2 = readOptions(root) + + assertEquals(options.elmFolder, options2.elmFolder) + assertEquals(options.filteredTestConfig, options2.filteredTestConfig) + assertEquals(options.filteredTestConfig?.runnableFilePath(), options2.filteredTestConfig?.runnableFilePath()) + assertEquals(options.filteredTestConfig?.filter, options2.filteredTestConfig?.filter) + } + + @Test + fun roundTripWithNullTestFile() { + val root = Element("ROOT") + val options = Options() options.elmFolder = "folder" @@ -36,6 +150,8 @@ class ElmTestRunConfigurationTest { val options2 = readOptions(root) assertEquals(options.elmFolder, options2.elmFolder) + assertEquals(options.filteredTestConfig, options2.filteredTestConfig) + assertEquals(options.filteredTestConfig?.runnableFilePath(), options2.filteredTestConfig?.runnableFilePath()) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/org/elm/workspace/ElmTestRunConfigurationSettingsBuilderTest.kt b/src/test/kotlin/org/elm/workspace/ElmTestRunConfigurationSettingsBuilderTest.kt new file mode 100644 index 00000000..51660b01 --- /dev/null +++ b/src/test/kotlin/org/elm/workspace/ElmTestRunConfigurationSettingsBuilderTest.kt @@ -0,0 +1,58 @@ +package org.elm.workspace + +import org.elm.FileTree +import org.elm.ide.test.run.ElmTestRunConfigurationSettingsBuilder +import org.elm.fileTree +import org.elm.ide.test.run.ElmTestRunConfiguration + +class ElmTestRunConfigurationSettingsBuilderTest : ElmWorkspaceTestBase() { + fun `test createAndRegisterFromElement with test file`() { + val projectStructure = createProjectStructure() + + val testProject = projectStructure.create(project, elmWorkspaceDirectory) + val psiElement = testProject.psiFile("tests/ExampleTest.elm") + + val configSettings = ElmTestRunConfigurationSettingsBuilder.createAndRegisterFromElement(psiElement) + + assertNotNull(configSettings) + val config = configSettings.configuration as ElmTestRunConfiguration + + assertEquals(true, config.options.filteredTestConfig?.filePath?.contains("tests/ExampleTest.elm")) + assertEquals(false, config.options.filteredTestConfig?.testIsDirectory) + assertEquals(null, config.options.filteredTestConfig?.filter) + assertEquals(true, config.options.filteredTestConfig?.runnableFilePath()?.contains("tests/ExampleTest.elm")) + } + + fun `test createAndRegisterFromElement with filter`() { + val projectStructure = createProjectStructure() + + val testProject = projectStructure.create(project, elmWorkspaceDirectory) + val psiElement = testProject.psiFile("tests/ExampleTest.elm") + + val configSettings = ElmTestRunConfigurationSettingsBuilder.createAndRegisterFromElement(psiElement, "my test") + + assertNotNull(configSettings) + val config = configSettings.configuration as ElmTestRunConfiguration + + assertEquals(true, config.options.filteredTestConfig?.filePath?.contains("tests/ExampleTest.elm")) + assertEquals(false, config.options.filteredTestConfig?.testIsDirectory) + assertEquals("my test", config.options.filteredTestConfig?.filter) + assertEquals(true, config.options.filteredTestConfig?.runnableFilePath()?.contains("tests/ExampleTest.elm")) + } + + private fun createProjectStructure(): FileTree { + return fileTree { + dir("tests") { + elm( + "ExampleTest.elm", """ + module ExampleTest exposing (..) + import Test exposing (test) + foo = test "my test" <| \_ -> Expect.equal 1 1 + bar = test "my second test" <| \_ -> Expect.equal 1 1 + --^ + """.trimIndent() + ) + } + } + } +} diff --git a/src/test/kotlin/org/elm/workspace/ElmToolchainTest.kt b/src/test/kotlin/org/elm/workspace/ElmToolchainTest.kt index 742eec22..1add2dcd 100644 --- a/src/test/kotlin/org/elm/workspace/ElmToolchainTest.kt +++ b/src/test/kotlin/org/elm/workspace/ElmToolchainTest.kt @@ -10,34 +10,34 @@ class ElmToolchainTest { @Test fun `looksLikeValidToolchain allows an executable file`() { val rawElmPath = Paths.get(ClassLoader.getSystemResource("org/elm/workspace/fixtures/elm-executable").toURI()) - val toolchain = ElmToolchain(rawElmPath, null, null, null, null, false) + val toolchain = ElmToolchain(rawElmPath, null, null, null, null, false, false) assertTrue(toolchain.looksLikeValidToolchain()) } @Test fun `looksLikeValidToolchain does not allow a non-executable file`() { val rawElmPath = Paths.get(ClassLoader.getSystemResource("org/elm/workspace/fixtures/non-executable").toURI()) - val toolchain = ElmToolchain(rawElmPath, null, null, null,null, false) + val toolchain = ElmToolchain(rawElmPath, null, null, null, null, false, false) assertFalse(toolchain.looksLikeValidToolchain()) } @Test fun `looksLikeValidToolchain allows bare elm if it is on the path`() { val pathSearchLocation = Paths.get(ClassLoader.getSystemResource("org/elm/workspace/fixtures/PATH").toURI()) - val toolchain = ElmToolchain(Paths.get("elm"), null, null, null, null, false) + val toolchain = ElmToolchain(Paths.get("elm"), null, null, null, null, false, false) assertTrue(toolchain.looksLikeValidToolchain(sequenceOf(pathSearchLocation))) } @Test fun `looksLikeValidToolchain does not allow bare elm if it is not on the path`() { val pathSearchLocation = Paths.get(ClassLoader.getSystemResource("org/elm/workspace/fixtures/NOT_PATH").toURI()) - val toolchain = ElmToolchain(Paths.get("elm"), null, null, null, null, false) + val toolchain = ElmToolchain(Paths.get("elm"), null, null, null, null, false, false) assertFalse(toolchain.looksLikeValidToolchain(sequenceOf(pathSearchLocation))) } @Test fun `looksLikeValidToolchain does not allow an empty path`() { - val toolchain = ElmToolchain(null, null, null, null, null, false) + val toolchain = ElmToolchain(null, null, null, null, null, false, false) assertFalse(toolchain.looksLikeValidToolchain()) } } diff --git a/src/test/kotlin/org/elm/workspace/ElmWorkspaceServiceTest.kt b/src/test/kotlin/org/elm/workspace/ElmWorkspaceServiceTest.kt index 7dee94ce..7c11aeef 100644 --- a/src/test/kotlin/org/elm/workspace/ElmWorkspaceServiceTest.kt +++ b/src/test/kotlin/org/elm/workspace/ElmWorkspaceServiceTest.kt @@ -265,7 +265,7 @@ class ElmWorkspaceServiceTest : ElmWorkspaceTestBase() { - + """.trimIndent()