diff --git a/build.sbt b/build.sbt index 8738c88..27aae62 100644 --- a/build.sbt +++ b/build.sbt @@ -1,8 +1,10 @@ import org.jetbrains.sbtidea.{AutoJbr, JbrPlatform} lazy val scala213 = "2.13.10" -lazy val scalaPluginVersion = "2023.3.17" -lazy val pluginVersion = "2023.3.30" + sys.env.get("ZIO_INTELLIJ_BUILD_NUMBER").fold(".1")(v => s".$v") +lazy val scalaPluginVersion = "2023.3.19" +lazy val minorVersion = "1" +lazy val buildVersion = sys.env.getOrElse("ZIO_INTELLIJ_BUILD_NUMBER", minorVersion) +lazy val pluginVersion = s"2023.3.30.$buildVersion" ThisBuild / intellijPluginName := "zio-intellij" ThisBuild / intellijBuild := "233" diff --git a/src/main/scala/zio/intellij/testsupport/ZTestFrameworkConsoleProperties.scala b/src/main/scala/zio/intellij/testsupport/ZTestFrameworkConsoleProperties.scala new file mode 100644 index 0000000..92ab102 --- /dev/null +++ b/src/main/scala/zio/intellij/testsupport/ZTestFrameworkConsoleProperties.scala @@ -0,0 +1,137 @@ +package zio.intellij.testsupport + +import com.intellij.execution.Executor +import com.intellij.execution.process.{AnsiEscapeDecoder, ProcessOutputTypes} +import com.intellij.execution.testframework.TestConsoleProperties +import com.intellij.execution.testframework.sm.SMCustomMessagesParsing +import com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter +import com.intellij.util.ReflectionUtil +import jetbrains.buildServer.messages.serviceMessages._ +import org.jetbrains.plugins.scala.testingSupport.test.{AbstractTestRunConfiguration, ScalaTestFrameworkConsoleProperties} + +import java.io.PrintStream +import scala.util.control.NoStackTrace + +private[zio] class ZTestFrameworkConsoleProperties(configuration: AbstractTestRunConfiguration, executor: Executor) + extends ScalaTestFrameworkConsoleProperties(configuration, "ZIO Test", executor) + with SMCustomMessagesParsing { + + override def createTestEventsConverter( + testFrameworkName: String, + consoleProperties: TestConsoleProperties + ): OutputToGeneralTestEventsConverter = + new ZTestEventsConverter(testFrameworkName, consoleProperties) + + private class ZTestEventsConverter(testFrameworkName: String, consoleProperties: TestConsoleProperties) + extends OutputToGeneralTestEventsConverter(testFrameworkName, consoleProperties) { self => + + // This entire thing makes me cry :( + // All this is needed to emit a custom TestFailedEvent that contains the expected and actual values extracted from + // the output of a ZIO Test. This allows displaying a clickable hyperlink to see the differences in IDEA's built-in + // diff viewer, because it assumes a JUnit-style failure reporting ("Expected:", "Actual:") which ZIO Test doesn't do. + // Unfortunately, the visitor is both private and not overridable, so we have to resort to reflection to get to it. + private lazy val underlyingTestVisitor = ReflectionUtil + .findFieldInHierarchy(classOf[OutputToGeneralTestEventsConverter], _.getName == "myServiceMessageVisitor") + .get(self) + .asInstanceOf[ServiceMessageVisitor] + + private lazy val testVisitor = new ZTestVisitor(underlyingTestVisitor) + + override def processServiceMessage(message: ServiceMessage, visitor: ServiceMessageVisitor): Unit = + message.visit(testVisitor) + + } + + private class ZTestVisitor(underlying: ServiceMessageVisitor) extends DefaultServiceMessageVisitor { + private val regexFromHell = + raw"\[1m.\[34m([\s\S]*).\[0m.\[0m.*\[31mwas not equal to.*\[1m.\[34m([\s\S]*?).\[0m.\[0m".r + + override def visitTestFailed(testFailed: TestFailed): Unit = { + val details = testFailed.getStacktrace + val tf = regexFromHell + .findFirstMatchIn(details) + .map { m => + val expected = unescapeAnsi(m.group(1)).trim + val actual = unescapeAnsi(m.group(2)).trim + val ex = new Throwable with NoStackTrace { + override def printStackTrace(s: PrintStream): Unit = + s.println(details) + + override def toString: String = testFailed.getFailureMessage + } + new TestFailed(testFailed.getTestName, ex, actual, expected) + } + .getOrElse { + testFailed + } + + underlying.visitTestFailed(tf) + } + + private def unescapeAnsi(s: String): String = { + val builder = new StringBuilder() + new AnsiEscapeDecoder().escapeText(s, ProcessOutputTypes.STDOUT, (text, _) => builder.append(text)) + builder.result() + } + + override def visitTestSuiteStarted(testSuiteStarted: TestSuiteStarted): Unit = + underlying.visitTestSuiteStarted(testSuiteStarted) + + override def visitTestSuiteFinished(testSuiteFinished: TestSuiteFinished): Unit = + underlying.visitTestSuiteFinished(testSuiteFinished) + + override def visitTestStarted(testStarted: TestStarted): Unit = + underlying.visitTestStarted(testStarted) + + override def visitTestFinished(testFinished: TestFinished): Unit = + underlying.visitTestFinished(testFinished) + + override def visitTestIgnored(testIgnored: TestIgnored): Unit = + underlying.visitTestIgnored(testIgnored) + + override def visitTestStdOut(testStdOut: TestStdOut): Unit = + underlying.visitTestStdOut(testStdOut) + + override def visitTestStdErr(testStdErr: TestStdErr): Unit = + underlying.visitTestStdErr(testStdErr) + + override def visitPublishArtifacts(publishArtifacts: PublishArtifacts): Unit = + underlying.visitPublishArtifacts(publishArtifacts) + + override def visitProgressMessage(progressMessage: ProgressMessage): Unit = + underlying.visitProgressMessage(progressMessage) + + override def visitProgressStart(progressStart: ProgressStart): Unit = + underlying.visitProgressStart(progressStart) + + override def visitProgressFinish(progressFinish: ProgressFinish): Unit = + underlying.visitProgressFinish(progressFinish) + + override def visitBuildStatus(buildStatus: BuildStatus): Unit = + underlying.visitBuildStatus(buildStatus) + + override def visitBuildNumber(buildNumber: BuildNumber): Unit = + underlying.visitBuildNumber(buildNumber) + + override def visitBuildStatisticValue(buildStatisticValue: BuildStatisticValue): Unit = + underlying.visitBuildStatisticValue(buildStatisticValue) + + override def visitMessageWithStatus(message: Message): Unit = + underlying.visitMessageWithStatus(message) + + override def visitBlockOpened(blockOpened: BlockOpened): Unit = + underlying.visitBlockOpened(blockOpened) + + override def visitBlockClosed(blockClosed: BlockClosed): Unit = + underlying.visitBlockClosed(blockClosed) + + override def visitCompilationStarted(compilationStarted: CompilationStarted): Unit = + underlying.visitCompilationStarted(compilationStarted) + + override def visitCompilationFinished(compilationFinished: CompilationFinished): Unit = + underlying.visitCompilationFinished(compilationFinished) + + override def visitServiceMessage(serviceMessage: ServiceMessage): Unit = + underlying.visitServiceMessage(serviceMessage) + } +} diff --git a/src/main/scala/zio/intellij/testsupport/ZTestRunConfiguration.scala b/src/main/scala/zio/intellij/testsupport/ZTestRunConfiguration.scala index 68294ac..ada9fe4 100644 --- a/src/main/scala/zio/intellij/testsupport/ZTestRunConfiguration.scala +++ b/src/main/scala/zio/intellij/testsupport/ZTestRunConfiguration.scala @@ -125,8 +125,12 @@ sealed abstract class ZTestRunConfiguration(project: Project, configurationFacto val consoleView: ConsoleView = if (useIntegratedRunner) { - val consoleProperties = new ScalaTestFrameworkConsoleProperties(self, "ZIO Test", executor) - SMTestRunnerConnectionUtil.createAndAttachConsole("ZIO Test", processHandler, consoleProperties) + val consoleProperties = new ZTestFrameworkConsoleProperties(self, executor) + SMTestRunnerConnectionUtil.createAndAttachConsole( + consoleProperties.getTestFrameworkName, + processHandler, + consoleProperties + ) } else { val console = new ConsoleViewImpl(project, true) console.attachToProcess(processHandler)