mirror of
https://github.com/apple/pkl.git
synced 2026-01-11 22:30:54 +01:00
Add debug and run button for snippet tests (#1281)
This commit is contained in:
@@ -24,7 +24,14 @@ repositories {
|
|||||||
intellijPlatform { defaultRepositories() }
|
intellijPlatform { defaultRepositories() }
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies { intellijPlatform { create("IC", libs.versions.intellij.get()) } }
|
dependencies {
|
||||||
|
intellijPlatform {
|
||||||
|
create("IC", libs.versions.intellij.get())
|
||||||
|
bundledPlugin("com.intellij.java")
|
||||||
|
bundledPlugin("org.jetbrains.plugins.gradle")
|
||||||
|
bundledPlugin("JUnit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
kotlin {
|
kotlin {
|
||||||
|
|||||||
@@ -20,22 +20,17 @@ import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
|
|||||||
import com.intellij.openapi.project.DumbAware
|
import com.intellij.openapi.project.DumbAware
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.vfs.VirtualFile
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
import com.intellij.openapi.vfs.VirtualFileManager
|
|
||||||
|
|
||||||
class SnippetTestEditorProvider : FileEditorProvider, DumbAware {
|
class SnippetTestEditorProvider : FileEditorProvider, DumbAware {
|
||||||
|
override fun accept(project: Project, file: VirtualFile): Boolean = file.isSnippetTestInputFile()
|
||||||
private val hiddenExtensionRegex = Regex(".*[.]([^.]*)[.]pkl")
|
|
||||||
|
|
||||||
override fun accept(project: Project, file: VirtualFile): Boolean {
|
|
||||||
return isSnippetTestInputFile(file) && findOutputFile(file) != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createEditor(project: Project, file: VirtualFile): FileEditor {
|
override fun createEditor(project: Project, file: VirtualFile): FileEditor {
|
||||||
val textEditorProvider = TextEditorProvider.getInstance()
|
val textEditorProvider = TextEditorProvider.getInstance()
|
||||||
val outputFile = findOutputFile(file) ?: return textEditorProvider.createEditor(project, file)
|
val outputFile = findOutputFile(file)
|
||||||
|
|
||||||
val inputEditor = textEditorProvider.createEditor(project, file) as TextEditor
|
val inputEditor = textEditorProvider.createEditor(project, file) as TextEditor
|
||||||
val outputEditor = textEditorProvider.createEditor(project, outputFile) as TextEditor
|
val outputEditor =
|
||||||
|
outputFile?.let { textEditorProvider.createEditor(project, it) as TextEditor }
|
||||||
|
|
||||||
return SnippetTestSplitEditor(inputEditor, outputEditor)
|
return SnippetTestSplitEditor(inputEditor, outputEditor)
|
||||||
}
|
}
|
||||||
@@ -43,34 +38,4 @@ class SnippetTestEditorProvider : FileEditorProvider, DumbAware {
|
|||||||
override fun getEditorTypeId(): String = "snippet-test-split-editor"
|
override fun getEditorTypeId(): String = "snippet-test-split-editor"
|
||||||
|
|
||||||
override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.HIDE_DEFAULT_EDITOR
|
override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.HIDE_DEFAULT_EDITOR
|
||||||
|
|
||||||
private fun isSnippetTestInputFile(file: VirtualFile): Boolean {
|
|
||||||
val path = file.path
|
|
||||||
return path.contains("/src/test/files/") && path.contains("/input/") && file.extension == "pkl"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun possibleOutputPaths(testType: String, relativePath: String): String? {
|
|
||||||
return when (testType) {
|
|
||||||
"LanguageSnippetTests" ->
|
|
||||||
if (relativePath.matches(hiddenExtensionRegex)) relativePath.dropLast(4)
|
|
||||||
else relativePath.dropLast(3) + "pcf"
|
|
||||||
"FormatterSnippetTests" -> relativePath
|
|
||||||
"SnippetTests" -> relativePath.replaceAfterLast('.', "yaml")
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findOutputFile(inputFile: VirtualFile): VirtualFile? {
|
|
||||||
val path = inputFile.path
|
|
||||||
val inputPattern = Regex(".*/src/test/files/(\\w+)/input/(.+)$")
|
|
||||||
val match = inputPattern.find(path) ?: return null
|
|
||||||
|
|
||||||
val testType = match.groupValues[1]
|
|
||||||
val relativePath = match.groupValues[2]
|
|
||||||
val relativeOutputPath = possibleOutputPaths(testType, relativePath) ?: return null
|
|
||||||
val outputPath = path.replace("/input/$relativePath", "/output/$relativeOutputPath")
|
|
||||||
val fileManager = VirtualFileManager.getInstance()
|
|
||||||
return fileManager.findFileByUrl("file://$outputPath")
|
|
||||||
?: fileManager.findFileByUrl("file://${outputPath.replaceAfterLast('.', "err")}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,31 @@
|
|||||||
*/
|
*/
|
||||||
package org.pkl.internal.intellij
|
package org.pkl.internal.intellij
|
||||||
|
|
||||||
|
import com.intellij.execution.ExecutionListener
|
||||||
|
import com.intellij.execution.ExecutionManager
|
||||||
|
import com.intellij.execution.Executor
|
||||||
|
import com.intellij.execution.ProgramRunnerUtil
|
||||||
|
import com.intellij.execution.RunManager
|
||||||
|
import com.intellij.execution.executors.DefaultDebugExecutor
|
||||||
|
import com.intellij.execution.executors.DefaultRunExecutor
|
||||||
|
import com.intellij.execution.junit.JUnitConfiguration
|
||||||
|
import com.intellij.execution.junit.JUnitConfigurationType
|
||||||
|
import com.intellij.execution.process.ProcessHandler
|
||||||
|
import com.intellij.execution.runners.ExecutionEnvironment
|
||||||
import com.intellij.icons.AllIcons
|
import com.intellij.icons.AllIcons
|
||||||
import com.intellij.openapi.actionSystem.*
|
import com.intellij.openapi.actionSystem.*
|
||||||
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
import com.intellij.openapi.fileEditor.FileEditor
|
import com.intellij.openapi.fileEditor.FileEditor
|
||||||
import com.intellij.openapi.fileEditor.FileEditorLocation
|
import com.intellij.openapi.fileEditor.FileEditorLocation
|
||||||
import com.intellij.openapi.fileEditor.FileEditorState
|
import com.intellij.openapi.fileEditor.FileEditorState
|
||||||
import com.intellij.openapi.fileEditor.FileEditorStateLevel
|
import com.intellij.openapi.fileEditor.FileEditorStateLevel
|
||||||
import com.intellij.openapi.fileEditor.TextEditor
|
import com.intellij.openapi.fileEditor.TextEditor
|
||||||
|
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
|
||||||
|
import com.intellij.openapi.module.ModuleUtil
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.util.UserDataHolderBase
|
import com.intellij.openapi.util.UserDataHolderBase
|
||||||
import com.intellij.openapi.vfs.VirtualFile
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
|
import com.intellij.openapi.vfs.VirtualFileManager
|
||||||
import com.intellij.ui.components.JBPanel
|
import com.intellij.ui.components.JBPanel
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
@@ -32,25 +48,33 @@ import javax.swing.JSplitPane
|
|||||||
|
|
||||||
class SnippetTestSplitEditor(
|
class SnippetTestSplitEditor(
|
||||||
private val inputEditor: TextEditor,
|
private val inputEditor: TextEditor,
|
||||||
private val outputEditor: TextEditor,
|
private var outputEditor: TextEditor?,
|
||||||
) : UserDataHolderBase(), FileEditor {
|
) : UserDataHolderBase(), FileEditor {
|
||||||
|
|
||||||
private var currentViewMode = ViewMode.SPLIT
|
private var currentViewMode = if (outputEditor != null) ViewMode.SPLIT else ViewMode.INPUT_ONLY
|
||||||
|
|
||||||
private val splitPane: JSplitPane =
|
private val splitPane: JSplitPane =
|
||||||
JSplitPane(JSplitPane.HORIZONTAL_SPLIT, inputEditor.component, outputEditor.component).apply {
|
JSplitPane(JSplitPane.HORIZONTAL_SPLIT, inputEditor.component, outputEditor?.component).apply {
|
||||||
resizeWeight = 0.5
|
resizeWeight = 0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mainPanel =
|
private val mainPanel =
|
||||||
JBPanel<JBPanel<*>>(BorderLayout()).apply {
|
JBPanel<JBPanel<*>>(BorderLayout()).apply {
|
||||||
add(createToolbar(), BorderLayout.NORTH)
|
add(createToolbar(), BorderLayout.NORTH)
|
||||||
add(splitPane, BorderLayout.CENTER)
|
when (currentViewMode) {
|
||||||
|
ViewMode.INPUT_ONLY -> add(inputEditor.component, BorderLayout.CENTER)
|
||||||
|
ViewMode.SPLIT -> add(splitPane, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createToolbar(): JComponent {
|
private fun createToolbar(): JComponent {
|
||||||
val actionGroup =
|
val actionGroup =
|
||||||
DefaultActionGroup().apply {
|
DefaultActionGroup().apply {
|
||||||
|
add(RunTestAction())
|
||||||
|
add(DebugTestAction())
|
||||||
|
add(RunAllTestsAction())
|
||||||
|
add(OverwriteSnippetAction())
|
||||||
|
addSeparator()
|
||||||
add(ShowInputOnlyAction())
|
add(ShowInputOnlyAction())
|
||||||
add(ShowSplitAction())
|
add(ShowSplitAction())
|
||||||
}
|
}
|
||||||
@@ -62,7 +86,7 @@ class SnippetTestSplitEditor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setViewMode(mode: ViewMode) {
|
private fun setViewMode(mode: ViewMode) {
|
||||||
if (currentViewMode == mode) return
|
if (currentViewMode == mode || outputEditor == null) return
|
||||||
currentViewMode = mode
|
currentViewMode = mode
|
||||||
|
|
||||||
// Remove the current center component
|
// Remove the current center component
|
||||||
@@ -76,7 +100,7 @@ class SnippetTestSplitEditor(
|
|||||||
ViewMode.SPLIT -> {
|
ViewMode.SPLIT -> {
|
||||||
// Re-add components to splitPane in case they were removed
|
// Re-add components to splitPane in case they were removed
|
||||||
splitPane.leftComponent = inputEditor.component
|
splitPane.leftComponent = inputEditor.component
|
||||||
splitPane.rightComponent = outputEditor.component
|
splitPane.rightComponent = outputEditor!!.component
|
||||||
mainPanel.add(splitPane, BorderLayout.CENTER)
|
mainPanel.add(splitPane, BorderLayout.CENTER)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,33 +120,111 @@ class SnippetTestSplitEditor(
|
|||||||
override fun setState(state: FileEditorState) {
|
override fun setState(state: FileEditorState) {
|
||||||
if (state is SnippetTestSplitEditorState) {
|
if (state is SnippetTestSplitEditorState) {
|
||||||
inputEditor.setState(state.inputState)
|
inputEditor.setState(state.inputState)
|
||||||
outputEditor.setState(state.outputState)
|
if (outputEditor != null && state.outputState != null) {
|
||||||
|
outputEditor!!.setState(state.outputState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getState(level: FileEditorStateLevel): FileEditorState {
|
override fun getState(level: FileEditorStateLevel): FileEditorState {
|
||||||
return SnippetTestSplitEditorState(inputEditor.getState(level), outputEditor.getState(level))
|
return SnippetTestSplitEditorState(inputEditor.getState(level), outputEditor?.getState(level))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isModified(): Boolean = inputEditor.isModified || outputEditor.isModified
|
override fun isModified(): Boolean = inputEditor.isModified || outputEditor?.isModified ?: false
|
||||||
|
|
||||||
override fun isValid(): Boolean = inputEditor.isValid && outputEditor.isValid
|
override fun isValid(): Boolean = inputEditor.isValid && outputEditor?.isValid ?: true
|
||||||
|
|
||||||
override fun addPropertyChangeListener(listener: PropertyChangeListener) {
|
override fun addPropertyChangeListener(listener: PropertyChangeListener) {
|
||||||
inputEditor.addPropertyChangeListener(listener)
|
inputEditor.addPropertyChangeListener(listener)
|
||||||
outputEditor.addPropertyChangeListener(listener)
|
outputEditor?.addPropertyChangeListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removePropertyChangeListener(listener: PropertyChangeListener) {
|
override fun removePropertyChangeListener(listener: PropertyChangeListener) {
|
||||||
inputEditor.removePropertyChangeListener(listener)
|
inputEditor.removePropertyChangeListener(listener)
|
||||||
outputEditor.removePropertyChangeListener(listener)
|
outputEditor?.removePropertyChangeListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentLocation(): FileEditorLocation? = inputEditor.currentLocation
|
override fun getCurrentLocation(): FileEditorLocation? = inputEditor.currentLocation
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
inputEditor.dispose()
|
inputEditor.dispose()
|
||||||
outputEditor.dispose()
|
outputEditor?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the output editor by reloading the output file from disk. This is useful after
|
||||||
|
* running tests, as the output file may have been created or changed (e.g., from .pcf to .err).
|
||||||
|
*/
|
||||||
|
private fun refreshOutputEditor(project: Project) {
|
||||||
|
val inputFile = inputEditor.file ?: return
|
||||||
|
|
||||||
|
// Refresh the input file's parent to pick up any new output files
|
||||||
|
ApplicationManager.getApplication().invokeLater {
|
||||||
|
VirtualFileManager.getInstance().asyncRefresh {
|
||||||
|
val currentOutputFile = findOutputFile(inputFile)
|
||||||
|
val editorOutputFile = outputEditor?.file
|
||||||
|
when {
|
||||||
|
currentOutputFile != null && currentOutputFile != editorOutputFile -> {
|
||||||
|
// No output file exists; set split mode.
|
||||||
|
if (editorOutputFile == null) {
|
||||||
|
replaceOutputEditorAndSetSplitMode(project, currentOutputFile)
|
||||||
|
} else {
|
||||||
|
// The output file has changed (e.g., .pcf to .err or vice versa), or got created.
|
||||||
|
// We need to replace the output editor with a new one for the new file
|
||||||
|
replaceOutputEditor(project, currentOutputFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> currentOutputFile?.refresh(true, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the current output editor with a new one for the specified file. This is necessary
|
||||||
|
* when the output file type changes (e.g., from .pcf to .err).
|
||||||
|
*/
|
||||||
|
private fun replaceOutputEditorAndSetSplitMode(project: Project, newOutputFile: VirtualFile) {
|
||||||
|
ApplicationManager.getApplication().invokeLater {
|
||||||
|
val textEditorProvider = TextEditorProvider.getInstance()
|
||||||
|
val newEditor = textEditorProvider.createEditor(project, newOutputFile) as TextEditor
|
||||||
|
|
||||||
|
// Dispose the old editor
|
||||||
|
outputEditor?.dispose()
|
||||||
|
|
||||||
|
// Update the reference to the new editor
|
||||||
|
outputEditor = newEditor
|
||||||
|
setViewMode(ViewMode.SPLIT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the current output editor with a new one for the specified file. This is necessary
|
||||||
|
* when the output file type changes (e.g., from .pcf to .err).
|
||||||
|
*/
|
||||||
|
private fun replaceOutputEditor(project: Project, newOutputFile: VirtualFile) {
|
||||||
|
ApplicationManager.getApplication().invokeLater {
|
||||||
|
val textEditorProvider = TextEditorProvider.getInstance()
|
||||||
|
val newEditor = textEditorProvider.createEditor(project, newOutputFile) as TextEditor
|
||||||
|
|
||||||
|
// Dispose the old editor
|
||||||
|
outputEditor?.dispose()
|
||||||
|
|
||||||
|
// Update the reference to the new editor
|
||||||
|
outputEditor = newEditor
|
||||||
|
|
||||||
|
// Update the split pane with the new editor
|
||||||
|
when (currentViewMode) {
|
||||||
|
ViewMode.SPLIT -> {
|
||||||
|
splitPane.rightComponent = newEditor.component
|
||||||
|
splitPane.revalidate()
|
||||||
|
splitPane.repaint()
|
||||||
|
}
|
||||||
|
ViewMode.INPUT_ONLY -> {
|
||||||
|
// Nothing to do, output is not visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class ViewMode {
|
private enum class ViewMode {
|
||||||
@@ -151,15 +253,178 @@ class SnippetTestSplitEditor(
|
|||||||
if (state) setViewMode(ViewMode.SPLIT)
|
if (state) setViewMode(ViewMode.SPLIT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class RunTestAction :
|
||||||
|
AnAction("Run Test", "Run the snippet test", AllIcons.Actions.Execute) {
|
||||||
|
override fun actionPerformed(e: AnActionEvent) {
|
||||||
|
val project = e.project ?: return
|
||||||
|
executeTest(project, DefaultRunExecutor.getRunExecutorInstance())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class RunAllTestsAction :
|
||||||
|
AnAction("Run All Tests", "Run all snippet tests", AllIcons.Actions.RunAll) {
|
||||||
|
override fun actionPerformed(e: AnActionEvent) {
|
||||||
|
val project = e.project ?: return
|
||||||
|
executeAllTests(project, DefaultRunExecutor.getRunExecutorInstance())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class DebugTestAction :
|
||||||
|
AnAction("Debug Test", "Debug the snippet test", AllIcons.Actions.StartDebugger) {
|
||||||
|
override fun actionPerformed(e: AnActionEvent) {
|
||||||
|
val project = e.project ?: return
|
||||||
|
executeTest(project, DefaultDebugExecutor.getDebugExecutorInstance())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class OverwriteSnippetAction :
|
||||||
|
AnAction(
|
||||||
|
"Overwrite Snippet",
|
||||||
|
"Run test and regenerate expected output",
|
||||||
|
AllIcons.Actions.RerunAutomatically,
|
||||||
|
) {
|
||||||
|
override fun actionPerformed(e: AnActionEvent) {
|
||||||
|
val project = e.project ?: return
|
||||||
|
|
||||||
|
executeTest(
|
||||||
|
project,
|
||||||
|
DefaultRunExecutor.getRunExecutorInstance(),
|
||||||
|
mapOf("OVERWRITE_SNIPPETS" to "1"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a JUnit UniqueID selector for the snippet test. E.g.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* [engine:LanguageSnippetTestsEngine]/[inputDirNode:lambdas]/[inputFileNode:lambdaStackTrace2.pkl]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
private fun buildUniqueId(file: VirtualFile): String? {
|
||||||
|
val path = file.path
|
||||||
|
// Pattern: .../LanguageSnippetTests/input/lambdas/lambdaStackTrace2.pkl
|
||||||
|
val pattern = Regex(".*/([^/]+)/src/test/files/(\\w+)/input/(.+)$")
|
||||||
|
val match = pattern.find(path) ?: return null
|
||||||
|
|
||||||
|
val testType = match.groupValues[2] // e.g., "LanguageSnippetTests"
|
||||||
|
val relativePath = match.groupValues[3] // e.g., "lambdas/lambdaStackTrace2.pkl"
|
||||||
|
|
||||||
|
// Extract directory and filename
|
||||||
|
val parts = relativePath.split("/")
|
||||||
|
val fileName = parts.last()
|
||||||
|
val engineName = testType + "Engine"
|
||||||
|
val uniqueId = buildString {
|
||||||
|
append("[engine:$engineName]")
|
||||||
|
if (parts.size > 1) {
|
||||||
|
for (dir in parts.dropLast(1)) {
|
||||||
|
append("/[inputDirNode:$dir]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append("/[inputFileNode:$fileName]")
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueId
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeAllTests(
|
||||||
|
project: Project,
|
||||||
|
executor: Executor,
|
||||||
|
envVars: Map<String, String> = emptyMap(),
|
||||||
|
) {
|
||||||
|
val file = inputEditor.file ?: return
|
||||||
|
|
||||||
|
val path = file.path
|
||||||
|
// Pattern: .../LanguageSnippetTests/input/lambdas/lambdaStackTrace2.pkl
|
||||||
|
val pattern = Regex(".*/([^/]+)/src/test/files/(\\w+)/input/(.+)$")
|
||||||
|
val match = pattern.find(path) ?: return
|
||||||
|
|
||||||
|
val testType = match.groupValues[2] // e.g., "LanguageSnippetTests"
|
||||||
|
val uniqueId = "[engine:${testType}Engine]"
|
||||||
|
executeTest(project, executor, uniqueId, envVars)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeTest(
|
||||||
|
project: Project,
|
||||||
|
executor: Executor,
|
||||||
|
envVars: Map<String, String> = emptyMap(),
|
||||||
|
) {
|
||||||
|
val file = inputEditor.file ?: return
|
||||||
|
val uniqueId = buildUniqueId(file) ?: return
|
||||||
|
executeTest(project, executor, uniqueId, envVars)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeTest(
|
||||||
|
project: Project,
|
||||||
|
executor: Executor,
|
||||||
|
uniqueId: String,
|
||||||
|
envVars: Map<String, String> = emptyMap(),
|
||||||
|
) {
|
||||||
|
val file = inputEditor.file ?: return
|
||||||
|
val module = ModuleUtil.findModuleForFile(file, project) ?: return
|
||||||
|
|
||||||
|
val runManager = RunManager.getInstance(project)
|
||||||
|
val configurationType = JUnitConfigurationType.getInstance()
|
||||||
|
val configurationFactory = configurationType.configurationFactories.first()
|
||||||
|
|
||||||
|
val settings =
|
||||||
|
runManager.createConfiguration(
|
||||||
|
"Snippet Test: ${file.nameWithoutExtension}",
|
||||||
|
configurationFactory,
|
||||||
|
)
|
||||||
|
|
||||||
|
val configuration = settings.configuration as? JUnitConfiguration ?: return
|
||||||
|
val data = configuration.persistentData
|
||||||
|
|
||||||
|
data.TEST_OBJECT = JUnitConfiguration.TEST_UNIQUE_ID
|
||||||
|
data.setUniqueIds(uniqueId)
|
||||||
|
|
||||||
|
if (envVars.isNotEmpty()) {
|
||||||
|
data.envs = envVars.toMutableMap()
|
||||||
|
data.PASS_PARENT_ENVS = true
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration.setModule(module)
|
||||||
|
|
||||||
|
// Add listener to refresh output editor after test completes
|
||||||
|
val messageBus = project.messageBus.connect()
|
||||||
|
messageBus.subscribe(
|
||||||
|
ExecutionManager.EXECUTION_TOPIC,
|
||||||
|
object : ExecutionListener {
|
||||||
|
override fun processTerminated(
|
||||||
|
executorId: String,
|
||||||
|
env: ExecutionEnvironment,
|
||||||
|
handler: ProcessHandler,
|
||||||
|
exitCode: Int,
|
||||||
|
) {
|
||||||
|
// Check if this is our test run
|
||||||
|
if (env.runProfile == configuration) {
|
||||||
|
// Refresh the output editor after the test completes
|
||||||
|
refreshOutputEditor(project)
|
||||||
|
// Disconnect the listener after use
|
||||||
|
messageBus.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ProgramRunnerUtil.executeConfiguration(settings, executor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SnippetTestSplitEditorState(
|
data class SnippetTestSplitEditorState(
|
||||||
val inputState: FileEditorState,
|
val inputState: FileEditorState,
|
||||||
val outputState: FileEditorState,
|
val outputState: FileEditorState?,
|
||||||
) : FileEditorState {
|
) : FileEditorState {
|
||||||
override fun canBeMergedWith(otherState: FileEditorState, level: FileEditorStateLevel): Boolean {
|
override fun canBeMergedWith(otherState: FileEditorState, level: FileEditorStateLevel): Boolean {
|
||||||
return otherState is SnippetTestSplitEditorState &&
|
val other = otherState as? SnippetTestSplitEditorState ?: return false
|
||||||
inputState.canBeMergedWith(otherState.inputState, level) &&
|
if (!inputState.canBeMergedWith(other.inputState, level)) return false
|
||||||
outputState.canBeMergedWith(otherState.outputState, level)
|
val otherState = other.outputState
|
||||||
|
return when {
|
||||||
|
outputState == null && otherState == null -> true
|
||||||
|
outputState != null && otherState != null -> outputState.canBeMergedWith(otherState, level)
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.internal.intellij
|
||||||
|
|
||||||
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
|
import com.intellij.openapi.vfs.VirtualFileManager
|
||||||
|
|
||||||
|
internal fun VirtualFile.isSnippetTestInputFile(): Boolean {
|
||||||
|
return path.contains("/src/test/files/") && path.contains("/input/") && extension == "pkl"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val hiddenExtensionRegex = Regex(".*[.]([^.]*)[.]pkl")
|
||||||
|
|
||||||
|
private fun possibleOutputPaths(testType: String, relativePath: String): String? {
|
||||||
|
return when (testType) {
|
||||||
|
"LanguageSnippetTests" ->
|
||||||
|
if (relativePath.matches(hiddenExtensionRegex)) relativePath.dropLast(4)
|
||||||
|
else relativePath.dropLast(3) + "pcf"
|
||||||
|
"FormatterSnippetTests" -> relativePath
|
||||||
|
"SnippetTests" -> relativePath.replaceAfterLast('.', "yaml")
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun findOutputFile(inputFile: VirtualFile): VirtualFile? {
|
||||||
|
val path = inputFile.path
|
||||||
|
val inputPattern = Regex(".*/src/test/files/(\\w+)/input/(.+)$")
|
||||||
|
val match = inputPattern.find(path) ?: return null
|
||||||
|
|
||||||
|
val testType = match.groupValues[1]
|
||||||
|
val relativePath = match.groupValues[2]
|
||||||
|
val relativeOutputPath = possibleOutputPaths(testType, relativePath) ?: return null
|
||||||
|
val outputPath = path.replace("/input/$relativePath", "/output/$relativeOutputPath")
|
||||||
|
val fileManager = VirtualFileManager.getInstance()
|
||||||
|
return fileManager.findFileByUrl("file://$outputPath")
|
||||||
|
?: fileManager.findFileByUrl("file://${outputPath.replaceAfterLast('.', "err")}")
|
||||||
|
}
|
||||||
@@ -10,8 +10,10 @@
|
|||||||
]]></description>
|
]]></description>
|
||||||
|
|
||||||
<depends>com.intellij.modules.platform</depends>
|
<depends>com.intellij.modules.platform</depends>
|
||||||
|
<depends>com.intellij.java</depends>
|
||||||
|
<depends>org.jetbrains.plugins.gradle</depends>
|
||||||
|
|
||||||
<extensions defaultExtensionNs="com.intellij">
|
<extensions defaultExtensionNs="com.intellij">
|
||||||
<fileEditorProvider implementation="org.pkl.intellij.SnippetTestEditorProvider"/>
|
<fileEditorProvider implementation="org.pkl.internal.intellij.SnippetTestEditorProvider"/>
|
||||||
</extensions>
|
</extensions>
|
||||||
</idea-plugin>
|
</idea-plugin>
|
||||||
|
|||||||
Reference in New Issue
Block a user