Skip to content

Commit 26ae5a8

Browse files
mergify[bot]kivikakkjackkoenig
authored
PeekPokeAPI: include source location on failed expect() calls. (backport #4144) (#4149)
* PeekPokeAPI: include source location on failed expect() calls. (#4144) * simulator: add SourceInfo to expect calls and report. * simulator: add test for failed expects. * simulator: attempt to extract source line. * simulator: make testableData.expect's sourceInfo parameter explicit. * simulator: add factory method for giving failed expect sourceInfo/extraContext. (cherry picked from commit 45dd82a) # Conflicts: # src/test/scala/chiselTests/simulator/SimulatorSpec.scala * Resolve backport conflicts and binary compatibility issues * Run scalafmt --------- Co-authored-by: Asherah Connor <ashe@kivikakk.ee> Co-authored-by: Jack Koenig <koenig@sifive.com>
1 parent fd3ac01 commit 26ae5a8

File tree

3 files changed

+119
-32
lines changed

3 files changed

+119
-32
lines changed

core/src/main/scala/chisel3/internal/Error.scala

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,28 @@ object ExceptionHelpers {
2929
def ellipsis(message: Option[String] = None): StackTraceElement =
3030
new StackTraceElement("..", " ", message.getOrElse(""), -1)
3131

32+
private[chisel3] def getErrorLineInFile(sourceRoots: Seq[File], sl: SourceLine): List[String] = {
33+
def tryFileInSourceRoot(sourceRoot: File): Option[List[String]] = {
34+
try {
35+
val file = new File(sourceRoot, sl.filename)
36+
val lines = Source.fromFile(file).getLines()
37+
var i = 0
38+
while (i < (sl.line - 1) && lines.hasNext) {
39+
lines.next()
40+
i += 1
41+
}
42+
val line = lines.next()
43+
val caretLine = (" " * (sl.col - 1)) + "^"
44+
Some(line :: caretLine :: Nil)
45+
} catch {
46+
case scala.util.control.NonFatal(_) => None
47+
}
48+
}
49+
val sourceRootsWithDefault = if (sourceRoots.nonEmpty) sourceRoots else Seq(new File("."))
50+
// View allows us to search the directories one at a time and early out
51+
sourceRootsWithDefault.view.map(tryFileInSourceRoot(_)).collectFirst { case Some(value) => value }.getOrElse(Nil)
52+
}
53+
3254
/** Utility methods that can be added to exceptions.
3355
*/
3456
implicit class ThrowableHelpers(throwable: Throwable) {
@@ -254,28 +276,6 @@ private[chisel3] class ErrorLog(
254276
throwOnFirstError: Boolean) {
255277
import ErrorLog.withColor
256278

257-
private def getErrorLineInFile(sl: SourceLine): List[String] = {
258-
def tryFileInSourceRoot(sourceRoot: File): Option[List[String]] = {
259-
try {
260-
val file = new File(sourceRoot, sl.filename)
261-
val lines = Source.fromFile(file).getLines()
262-
var i = 0
263-
while (i < (sl.line - 1) && lines.hasNext) {
264-
lines.next()
265-
i += 1
266-
}
267-
val line = lines.next()
268-
val caretLine = (" " * (sl.col - 1)) + "^"
269-
Some(line :: caretLine :: Nil)
270-
} catch {
271-
case scala.util.control.NonFatal(_) => None
272-
}
273-
}
274-
val sourceRootsWithDefault = if (sourceRoots.nonEmpty) sourceRoots else Seq(new File("."))
275-
// View allows us to search the directories one at a time and early out
276-
sourceRootsWithDefault.view.map(tryFileInSourceRoot(_)).collectFirst { case Some(value) => value }.getOrElse(Nil)
277-
}
278-
279279
/** Returns an appropriate location string for the provided source info.
280280
* If the source info is of `NoSourceInfo` type, the source location is looked up via stack trace.
281281
* If the source info is `None`, an empty string is returned.
@@ -292,7 +292,8 @@ private[chisel3] class ErrorLog(
292292
// id is optional because it has only been applied to warnings, TODO apply to errors
293293
private def logWarningOrError(msg: String, si: Option[SourceInfo], isFatal: Boolean): Unit = {
294294
val location = errorLocationString(si)
295-
val sourceLineAndCaret = si.collect { case sl: SourceLine => getErrorLineInFile(sl) }.getOrElse(Nil)
295+
val sourceLineAndCaret =
296+
si.collect { case sl: SourceLine => ExceptionHelpers.getErrorLineInFile(sourceRoots, sl) }.getOrElse(Nil)
296297
val fullMessage = if (location.isEmpty) msg else s"$location: $msg"
297298
val errorLines = fullMessage :: sourceLineAndCaret
298299
val entry = ErrorEntry(errorLines, isFatal)

src/main/scala/chisel3/simulator/PeekPokeAPI.scala

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,27 @@ package chisel3.simulator
33
import svsim._
44
import chisel3._
55

6+
import chisel3.experimental.{SourceInfo, SourceLine, UnlocatableSourceInfo}
7+
import chisel3.internal.ExceptionHelpers
8+
69
object PeekPokeAPI extends PeekPokeAPI
710

811
trait PeekPokeAPI {
912
case class FailedExpectationException[T](observed: T, expected: T, message: String)
1013
extends Exception(s"Failed Expectation: Observed value '$observed' != $expected. $message")
14+
object FailedExpectationException {
15+
def apply[T](
16+
observed: T,
17+
expected: T,
18+
message: String,
19+
sourceInfo: SourceInfo,
20+
extraContext: Seq[String]
21+
): FailedExpectationException[T] = {
22+
val fullMessage = s"$message ${sourceInfo.makeMessage(x => x)}" +
23+
(if (extraContext.nonEmpty) s"\n${extraContext.mkString("\n")}" else "")
24+
new FailedExpectationException(observed, expected, fullMessage)
25+
}
26+
}
1127

1228
implicit class testableClock(clock: Clock) {
1329
def step(cycles: Int = 1): Unit = {
@@ -56,25 +72,47 @@ trait PeekPokeAPI {
5672
}
5773

5874
final def peek(): T = encode(data.peekValue())
59-
final def expect(expected: T): Unit = {
75+
// Added for binary compatibility, not callable directly but can be called if compiled against old Chisel
76+
private[simulator] final def expect(expected: T): Unit = _expect(expected, UnlocatableSourceInfo)
77+
final def expect(expected: T)(implicit sourceInfo: SourceInfo): Unit = _expect(expected, sourceInfo)
78+
// Added to avoid ambiguity errors when using binary compatibility shim
79+
private def _expect(expected: T, sourceInfo: SourceInfo): Unit = {
6080
data.expect(
6181
expected.litValue,
6282
encode(_).litValue,
63-
(observed: BigInt, expected: BigInt) => s"Expectation failed: observed value $observed != $expected"
83+
(observed: BigInt, expected: BigInt) => s"Expectation failed: observed value $observed != $expected",
84+
sourceInfo
6485
)
6586
}
66-
final def expect(expected: T, message: String): Unit = {
67-
data.expect(expected.litValue, encode(_).litValue, (_: BigInt, _: BigInt) => message)
87+
// Added for binary compatibility, not callable directly but can be called if compiled against old Chisel
88+
private[simulator] def expect(expected: T, message: String): Unit =
89+
_expect(expected, message, UnlocatableSourceInfo)
90+
final def expect(expected: T, message: String)(implicit sourceInfo: SourceInfo): Unit =
91+
_expect(expected, message, sourceInfo)
92+
// Added to avoid ambiguity errors when using binary compatibility shim
93+
private def _expect(expected: T, message: String, sourceInfo: SourceInfo): Unit = {
94+
data.expect(expected.litValue, encode(_).litValue, (_: BigInt, _: BigInt) => message, sourceInfo)
6895
}
69-
final def expect(expected: BigInt): Unit = {
96+
// Added for binary compatibility, not callable directly but can be called if compiled against old Chisel
97+
private[simulator] def expect(expected: BigInt): Unit = _expect(expected, UnlocatableSourceInfo)
98+
final def expect(expected: BigInt)(implicit sourceInfo: SourceInfo): Unit = _expect(expected, sourceInfo)
99+
// Added to avoid ambiguity errors when using binary compatibility shim
100+
private def _expect(expected: BigInt, sourceInfo: SourceInfo): Unit = {
70101
data.expect(
71102
expected,
72103
_.asBigInt,
73-
(observed: BigInt, expected: BigInt) => s"Expectation failed: observed value $observed != $expected"
104+
(observed: BigInt, expected: BigInt) => s"Expectation failed: observed value $observed != $expected",
105+
sourceInfo
74106
)
75107
}
76-
final def expect(expected: BigInt, message: String): Unit = {
77-
data.expect(expected, _.asBigInt, (_: BigInt, _: BigInt) => message)
108+
// Added for binary compatibility, not callable directly but can be called if compiled against old Chisel
109+
private[simulator] def expect(expected: BigInt, message: String): Unit =
110+
_expect(expected, message, UnlocatableSourceInfo)
111+
final def expect(expected: BigInt, message: String)(implicit sourceInfo: SourceInfo): Unit =
112+
_expect(expected, message, sourceInfo)
113+
// Added to avoid ambiguity errors when using binary compatibility shim
114+
private def _expect(expected: BigInt, message: String, sourceInfo: SourceInfo): Unit = {
115+
data.expect(expected, _.asBigInt, (_: BigInt, _: BigInt) => message, sourceInfo)
78116
}
79117

80118
}
@@ -122,17 +160,40 @@ trait PeekPokeAPI {
122160
val simulationPort = module.port(data)
123161
simulationPort.get(isSigned = isSigned)
124162
}
163+
@deprecated("Use version that takes a SourceInfo", "Chisel 6.5.0")
125164
def expect[T](
126165
expected: T,
127166
encode: (Simulation.Value) => T,
128167
buildMessage: (T, T) => String
168+
): Unit = expect(expected, encode, buildMessage)
169+
def expect[T](
170+
expected: T,
171+
encode: (Simulation.Value) => T,
172+
buildMessage: (T, T) => String,
173+
sourceInfo: SourceInfo
129174
): Unit = {
130175
val module = AnySimulatedModule.current
131176
module.willPeek()
132177
val simulationPort = module.port(data)
178+
133179
simulationPort.check(isSigned = isSigned) { observedValue =>
134180
val observed = encode(observedValue)
135-
if (observed != expected) throw FailedExpectationException(observed, expected, buildMessage(observed, expected))
181+
if (observed != expected) {
182+
val extraContext =
183+
sourceInfo match {
184+
case sl: SourceLine =>
185+
ExceptionHelpers.getErrorLineInFile(Seq(), sl)
186+
case _ =>
187+
Seq()
188+
}
189+
throw FailedExpectationException(
190+
observed,
191+
expected,
192+
buildMessage(observed, expected),
193+
sourceInfo,
194+
extraContext
195+
)
196+
}
136197
}
137198
}
138199
}

src/test/scala/chiselTests/simulator/SimulatorSpec.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,31 @@ class SimulatorSpec extends AnyFunSpec with Matchers {
7070
assert(result === 12)
7171
}
7272

73+
it("reports failed expects correctly") {
74+
val simulator = new VerilatorSimulator("test_run_dir/simulator/GCDSimulator")
75+
val thrown = the[PeekPokeAPI.FailedExpectationException[_]] thrownBy {
76+
simulator
77+
.simulate(new GCD()) { module =>
78+
import PeekPokeAPI._
79+
val gcd = module.wrapped
80+
gcd.io.a.poke(24.U)
81+
gcd.io.b.poke(36.U)
82+
gcd.io.loadValues.poke(1.B)
83+
gcd.clock.step()
84+
gcd.io.loadValues.poke(0.B)
85+
gcd.clock.step(10)
86+
gcd.io.result.expect(5)
87+
}
88+
.result
89+
}
90+
thrown.getMessage must include("Observed value '12' != 5.")
91+
(thrown.getMessage must include).regex(
92+
""" @\[src/test/scala/chiselTests/simulator/SimulatorSpec\.scala \d+:\d+\]"""
93+
)
94+
thrown.getMessage must include("gcd.io.result.expect(5)")
95+
thrown.getMessage must include(" ^")
96+
}
97+
7398
it("simulate a circuit with zero-width ports") {
7499
val width = 0
75100
// Run a simulation with zero width foo

0 commit comments

Comments
 (0)