Skip to content

Commit 45dd82a

Browse files
authored
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.
1 parent 6de9640 commit 45dd82a

File tree

3 files changed

+94
-33
lines changed

3 files changed

+94
-33
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: 45 additions & 10 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}
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,27 @@ trait PeekPokeAPI {
5672
}
5773

5874
final def peek(): T = encode(data.peekValue())
59-
final def expect(expected: T): Unit = {
75+
final def expect(expected: T)(implicit sourceInfo: SourceInfo): Unit = {
6076
data.expect(
6177
expected.litValue,
6278
encode(_).litValue,
63-
(observed: BigInt, expected: BigInt) => s"Expectation failed: observed value $observed != $expected"
79+
(observed: BigInt, expected: BigInt) => s"Expectation failed: observed value $observed != $expected",
80+
sourceInfo
6481
)
6582
}
66-
final def expect(expected: T, message: String): Unit = {
67-
data.expect(expected.litValue, encode(_).litValue, (_: BigInt, _: BigInt) => message)
83+
final def expect(expected: T, message: String)(implicit sourceInfo: SourceInfo): Unit = {
84+
data.expect(expected.litValue, encode(_).litValue, (_: BigInt, _: BigInt) => message, sourceInfo)
6885
}
69-
final def expect(expected: BigInt): Unit = {
86+
final def expect(expected: BigInt)(implicit sourceInfo: SourceInfo): Unit = {
7087
data.expect(
7188
expected,
7289
_.asBigInt,
73-
(observed: BigInt, expected: BigInt) => s"Expectation failed: observed value $observed != $expected"
90+
(observed: BigInt, expected: BigInt) => s"Expectation failed: observed value $observed != $expected",
91+
sourceInfo
7492
)
7593
}
76-
final def expect(expected: BigInt, message: String): Unit = {
77-
data.expect(expected, _.asBigInt, (_: BigInt, _: BigInt) => message)
94+
final def expect(expected: BigInt, message: String)(implicit sourceInfo: SourceInfo): Unit = {
95+
data.expect(expected, _.asBigInt, (_: BigInt, _: BigInt) => message, sourceInfo)
7896
}
7997

8098
}
@@ -125,14 +143,31 @@ trait PeekPokeAPI {
125143
def expect[T](
126144
expected: T,
127145
encode: (Simulation.Value) => T,
128-
buildMessage: (T, T) => String
146+
buildMessage: (T, T) => String,
147+
sourceInfo: SourceInfo
129148
): Unit = {
130149
val module = AnySimulatedModule.current
131150
module.willPeek()
132151
val simulationPort = module.port(data)
152+
133153
simulationPort.check(isSigned = isSigned) { observedValue =>
134154
val observed = encode(observedValue)
135-
if (observed != expected) throw FailedExpectationException(observed, expected, buildMessage(observed, expected))
155+
if (observed != expected) {
156+
val extraContext =
157+
sourceInfo match {
158+
case sl: SourceLine =>
159+
ExceptionHelpers.getErrorLineInFile(Seq(), sl)
160+
case _ =>
161+
Seq()
162+
}
163+
throw FailedExpectationException(
164+
observed,
165+
expected,
166+
buildMessage(observed, expected),
167+
sourceInfo,
168+
extraContext
169+
)
170+
}
136171
}
137172
}
138173
}

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

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

74+
it("reports failed expects correctly") {
75+
val simulator = new VerilatorSimulator("test_run_dir/simulator/GCDSimulator")
76+
val thrown = the[PeekPokeAPI.FailedExpectationException[_]] thrownBy {
77+
simulator
78+
.simulate(new GCD()) { module =>
79+
import PeekPokeAPI._
80+
val gcd = module.wrapped
81+
gcd.io.a.poke(24.U)
82+
gcd.io.b.poke(36.U)
83+
gcd.io.loadValues.poke(1.B)
84+
gcd.clock.step()
85+
gcd.io.loadValues.poke(0.B)
86+
gcd.clock.step(10)
87+
gcd.io.result.expect(5)
88+
}
89+
.result
90+
}
91+
thrown.getMessage must include("Observed value '12' != 5.")
92+
(thrown.getMessage must include).regex(
93+
""" @\[src/test/scala/chiselTests/simulator/SimulatorSpec\.scala \d+:\d+\]"""
94+
)
95+
thrown.getMessage must include("gcd.io.result.expect(5)")
96+
thrown.getMessage must include(" ^")
97+
}
98+
7499
it("runs a design that includes an external module") {
75100
class Bar extends ExtModule with HasExtModuleInline {
76101
val a = IO(Output(Bool()))

0 commit comments

Comments
 (0)