Skip to content

Commit 6766a48

Browse files
authored
"Click to see difference" for non-trivial test assertion failures (#462)
* Implementing Click to see difference for non-trivial assertion failures * Regex hell
1 parent d769079 commit 6766a48

File tree

3 files changed

+147
-4
lines changed

3 files changed

+147
-4
lines changed

build.sbt

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import org.jetbrains.sbtidea.{AutoJbr, JbrPlatform}
22

33
lazy val scala213 = "2.13.10"
4-
lazy val scalaPluginVersion = "2023.3.17"
5-
lazy val pluginVersion = "2023.3.30" + sys.env.get("ZIO_INTELLIJ_BUILD_NUMBER").fold(".1")(v => s".$v")
4+
lazy val scalaPluginVersion = "2023.3.19"
5+
lazy val minorVersion = "1"
6+
lazy val buildVersion = sys.env.getOrElse("ZIO_INTELLIJ_BUILD_NUMBER", minorVersion)
7+
lazy val pluginVersion = s"2023.3.30.$buildVersion"
68

79
ThisBuild / intellijPluginName := "zio-intellij"
810
ThisBuild / intellijBuild := "233"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package zio.intellij.testsupport
2+
3+
import com.intellij.execution.Executor
4+
import com.intellij.execution.process.{AnsiEscapeDecoder, ProcessOutputTypes}
5+
import com.intellij.execution.testframework.TestConsoleProperties
6+
import com.intellij.execution.testframework.sm.SMCustomMessagesParsing
7+
import com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter
8+
import com.intellij.util.ReflectionUtil
9+
import jetbrains.buildServer.messages.serviceMessages._
10+
import org.jetbrains.plugins.scala.testingSupport.test.{AbstractTestRunConfiguration, ScalaTestFrameworkConsoleProperties}
11+
12+
import java.io.PrintStream
13+
import scala.util.control.NoStackTrace
14+
15+
private[zio] class ZTestFrameworkConsoleProperties(configuration: AbstractTestRunConfiguration, executor: Executor)
16+
extends ScalaTestFrameworkConsoleProperties(configuration, "ZIO Test", executor)
17+
with SMCustomMessagesParsing {
18+
19+
override def createTestEventsConverter(
20+
testFrameworkName: String,
21+
consoleProperties: TestConsoleProperties
22+
): OutputToGeneralTestEventsConverter =
23+
new ZTestEventsConverter(testFrameworkName, consoleProperties)
24+
25+
private class ZTestEventsConverter(testFrameworkName: String, consoleProperties: TestConsoleProperties)
26+
extends OutputToGeneralTestEventsConverter(testFrameworkName, consoleProperties) { self =>
27+
28+
// This entire thing makes me cry :(
29+
// All this is needed to emit a custom TestFailedEvent that contains the expected and actual values extracted from
30+
// the output of a ZIO Test. This allows displaying a clickable hyperlink to see the differences in IDEA's built-in
31+
// diff viewer, because it assumes a JUnit-style failure reporting ("Expected:", "Actual:") which ZIO Test doesn't do.
32+
// Unfortunately, the visitor is both private and not overridable, so we have to resort to reflection to get to it.
33+
private lazy val underlyingTestVisitor = ReflectionUtil
34+
.findFieldInHierarchy(classOf[OutputToGeneralTestEventsConverter], _.getName == "myServiceMessageVisitor")
35+
.get(self)
36+
.asInstanceOf[ServiceMessageVisitor]
37+
38+
private lazy val testVisitor = new ZTestVisitor(underlyingTestVisitor)
39+
40+
override def processServiceMessage(message: ServiceMessage, visitor: ServiceMessageVisitor): Unit =
41+
message.visit(testVisitor)
42+
43+
}
44+
45+
private class ZTestVisitor(underlying: ServiceMessageVisitor) extends DefaultServiceMessageVisitor {
46+
private val regexFromHell =
47+
raw"\[1m.\[34m([\s\S]*).\[0m.\[0m.*\[31mwas not equal to.*\[1m.\[34m([\s\S]*?).\[0m.\[0m".r
48+
49+
override def visitTestFailed(testFailed: TestFailed): Unit = {
50+
val details = testFailed.getStacktrace
51+
val tf = regexFromHell
52+
.findFirstMatchIn(details)
53+
.map { m =>
54+
val expected = unescapeAnsi(m.group(1)).trim
55+
val actual = unescapeAnsi(m.group(2)).trim
56+
val ex = new Throwable with NoStackTrace {
57+
override def printStackTrace(s: PrintStream): Unit =
58+
s.println(details)
59+
60+
override def toString: String = testFailed.getFailureMessage
61+
}
62+
new TestFailed(testFailed.getTestName, ex, actual, expected)
63+
}
64+
.getOrElse {
65+
testFailed
66+
}
67+
68+
underlying.visitTestFailed(tf)
69+
}
70+
71+
private def unescapeAnsi(s: String): String = {
72+
val builder = new StringBuilder()
73+
new AnsiEscapeDecoder().escapeText(s, ProcessOutputTypes.STDOUT, (text, _) => builder.append(text))
74+
builder.result()
75+
}
76+
77+
override def visitTestSuiteStarted(testSuiteStarted: TestSuiteStarted): Unit =
78+
underlying.visitTestSuiteStarted(testSuiteStarted)
79+
80+
override def visitTestSuiteFinished(testSuiteFinished: TestSuiteFinished): Unit =
81+
underlying.visitTestSuiteFinished(testSuiteFinished)
82+
83+
override def visitTestStarted(testStarted: TestStarted): Unit =
84+
underlying.visitTestStarted(testStarted)
85+
86+
override def visitTestFinished(testFinished: TestFinished): Unit =
87+
underlying.visitTestFinished(testFinished)
88+
89+
override def visitTestIgnored(testIgnored: TestIgnored): Unit =
90+
underlying.visitTestIgnored(testIgnored)
91+
92+
override def visitTestStdOut(testStdOut: TestStdOut): Unit =
93+
underlying.visitTestStdOut(testStdOut)
94+
95+
override def visitTestStdErr(testStdErr: TestStdErr): Unit =
96+
underlying.visitTestStdErr(testStdErr)
97+
98+
override def visitPublishArtifacts(publishArtifacts: PublishArtifacts): Unit =
99+
underlying.visitPublishArtifacts(publishArtifacts)
100+
101+
override def visitProgressMessage(progressMessage: ProgressMessage): Unit =
102+
underlying.visitProgressMessage(progressMessage)
103+
104+
override def visitProgressStart(progressStart: ProgressStart): Unit =
105+
underlying.visitProgressStart(progressStart)
106+
107+
override def visitProgressFinish(progressFinish: ProgressFinish): Unit =
108+
underlying.visitProgressFinish(progressFinish)
109+
110+
override def visitBuildStatus(buildStatus: BuildStatus): Unit =
111+
underlying.visitBuildStatus(buildStatus)
112+
113+
override def visitBuildNumber(buildNumber: BuildNumber): Unit =
114+
underlying.visitBuildNumber(buildNumber)
115+
116+
override def visitBuildStatisticValue(buildStatisticValue: BuildStatisticValue): Unit =
117+
underlying.visitBuildStatisticValue(buildStatisticValue)
118+
119+
override def visitMessageWithStatus(message: Message): Unit =
120+
underlying.visitMessageWithStatus(message)
121+
122+
override def visitBlockOpened(blockOpened: BlockOpened): Unit =
123+
underlying.visitBlockOpened(blockOpened)
124+
125+
override def visitBlockClosed(blockClosed: BlockClosed): Unit =
126+
underlying.visitBlockClosed(blockClosed)
127+
128+
override def visitCompilationStarted(compilationStarted: CompilationStarted): Unit =
129+
underlying.visitCompilationStarted(compilationStarted)
130+
131+
override def visitCompilationFinished(compilationFinished: CompilationFinished): Unit =
132+
underlying.visitCompilationFinished(compilationFinished)
133+
134+
override def visitServiceMessage(serviceMessage: ServiceMessage): Unit =
135+
underlying.visitServiceMessage(serviceMessage)
136+
}
137+
}

src/main/scala/zio/intellij/testsupport/ZTestRunConfiguration.scala

+6-2
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,12 @@ sealed abstract class ZTestRunConfiguration(project: Project, configurationFacto
125125

126126
val consoleView: ConsoleView =
127127
if (useIntegratedRunner) {
128-
val consoleProperties = new ScalaTestFrameworkConsoleProperties(self, "ZIO Test", executor)
129-
SMTestRunnerConnectionUtil.createAndAttachConsole("ZIO Test", processHandler, consoleProperties)
128+
val consoleProperties = new ZTestFrameworkConsoleProperties(self, executor)
129+
SMTestRunnerConnectionUtil.createAndAttachConsole(
130+
consoleProperties.getTestFrameworkName,
131+
processHandler,
132+
consoleProperties
133+
)
130134
} else {
131135
val console = new ConsoleViewImpl(project, true)
132136
console.attachToProcess(processHandler)

0 commit comments

Comments
 (0)