Skip to content
This repository

Issue 9 - Make the AST position aware, use that for TypeNotFoundException, and pretty print exceptions in general #11

Open
wants to merge 11 commits into from

3 participants

Aaron Stone Robey Pointer Erik van oosten
Aaron Stone

In these changesets, I implement TypeNotFoundException reporting with the bad type's position in the file.

This is accomplished by:

  • Switching the thrift file input from a slurp-to-String to a StreamReader. This reader annotates the input with position information
  • Adding a Positional mixing to the top level Node class of the AST.
  • Calling the Parser positioned() combinator in useful places in the parser.
  • Subclassing several exceptions from a new ExceptionAt which has a Position instance var.

Caveats:

  • Unit tests not yet updated for the change from String to StreamReader.
  • Exceptions are mostly suppressed, so weird ones might be more confusing now.
  • I've been testing these based off the sbt11 branch because I cannot build locally with sbt 0.7, so these changesets have been cherry-picked to a new branch off master (though no errors or editing needed).
Aaron Stone

Update: tests pass (added overloaded methods to adapt from String to StreamReader, rather than changing the tests).

Robey Pointer robey commented on the diff
src/main/scala/com/twitter/scrooge/Main.scala
@@ -90,22 +90,35 @@ object Main {
90 90 }
91 91
92 92 for (inputFile <- thriftFiles) {
93   - val inputFileDir = new File(inputFile).getParent
94   - val importer = Importer.fileImporter(inputFileDir :: importPaths.toList)
95   - val parser = new ScroogeParser(importer)
96   - val doc0 = parser.parseFile(inputFile).mapNamespaces(namespaceMappings.toMap)
  93 + try {
  94 + if (verbose) print("+ Compiling %s".format(inputFile))
4
Robey Pointer Collaborator
robey added a note

we want to avoid printing this unless the file is going to be compiled.

Aaron Stone
sodabrew added a note
Robey Pointer Collaborator
robey added a note

oh, i see, because an exception could happen in the parsing even if we don't plan to rewrite the scala files.

i guess that bug's always been there. :)

Aaron Stone
sodabrew added a note

Yes, and caught me unawares a few times already as I was trying to figure out what scrooge was looking at when it bombed out :)

Prepending the filename to exceptions also helps, particularly in non-verbose mode where it's the only output you get on an error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/main/scala/com/twitter/scrooge/TypeResolver.scala
@@ -179,7 +186,17 @@ case class TypeResolver(
179 186 }
180 187
181 188 def apply(t: FieldType): FieldType = t match {
182   - case ReferenceType(name) => apply(name)
4
Robey Pointer Collaborator
robey added a note

why not add a parameter to the 3 exceptions, and have it add position at the time the exception is thrown?

Aaron Stone
sodabrew added a note

The exception is thrown by appy(), which only has access to the name string at that point. We could pass the node along to apply?

Robey Pointer Collaborator
robey added a note

yeah, i think it would be better to thread a Node through where the exceptions are thrown, so that it's obvious that every place that throws one of these exceptions will attach position info at the time. it's less clear when it's being handled only at apply(FieldType).

Aaron Stone
sodabrew added a note

Approach is changed to a DynamicVariable, see next commits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Aaron Stone

Rather than trying to thread the Node through the call stack, I added an implicit DynamicVariable class val which is set to the node being operated on at the top level. If an exception is thrown anywhere in the class, it picks up the current value of the dynamic variable and the position information is grabbed from it.

Robey Pointer robey commented on the diff
src/main/scala/com/twitter/scrooge/ScroogeParser.scala
((15 lines not shown))
233 235
234   - def parseFile(filename: String) = parse(importer(filename), document)
3
Robey Pointer Collaborator
robey added a note

do we not use the importer anymore? (i guess not, or the tests would have failed)

Aaron Stone
sodabrew added a note

Maybe not! I didn't look carefully to see if I could remove it.

Robey Pointer Collaborator
robey added a note

oh, we just fixed a bug in this, so we DO use it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Robey Pointer robey commented on the diff
src/main/scala/com/twitter/scrooge/TypeResolver.scala
@@ -114,7 +124,7 @@ case class TypeResolver(
114 124 * typeMap, and then returns an updated TypeResolver with the new
115 125 * definition bound, plus the resolved definition.
116 126 */
117   - def resolve(definition: Definition): ResolvedDefinition = {
  127 + def resolve(definition: Definition): ResolvedDefinition = dynCurrentNode.withValue(definition) {
6
Robey Pointer Collaborator
robey added a note

does this mean the error messages will only report on lines with the beginning of a definition? makes it seem like there's no need to attach the position to any other node.

Aaron Stone
sodabrew added a note

Good point. I went back and forth a lot attaching position() combinators and looking at where to put a withValue { } enclosure. To get pinpoint type error reporting, there would need to be more withValue { } blocks around many of the apply methods. Open to direction on how detailed to get here.

Robey Pointer Collaborator
robey added a note

i'm ... really not sure. :)

it would be good to have the position information for the actual token causing problems when we throw an exception, but it doesn't seem like that's possible without threading the AST node through the TypeResolver, either explicitly, or by calling withNode in a bunch of places whenever the focused AST node changes.

i'm leaning toward the explicit passing around of node objects, because the withValue() thing is a bit magickal to me -- i had to go look up what that is. but if it can be done with a few well-placed withValue() calls vs changing the argument list to every method in TypeResolver, then that's probably better.

Aaron Stone
sodabrew added a note

http://www.scala-lang.org/api/current/scala/util/DynamicVariable.html

Think of it as a stack. Everyone with lexical scope (e.g. is in the same class) to see the variable sees just the topmost value. Calls to withValue{ } push onto the stack going into the block, and pop off the stack when exiting the block. This way, you can have code inside your class that just calls for the current value and doesn't need to worry about the particular code path taken on the way there.

I was thinking about adding a withValue { } block around most of the apply() methods, and would be happy to do that.

Robey Pointer Collaborator
robey added a note

well, let's try that then. :)

Aaron Stone
sodabrew added a note

Just pushed some more withValue blocks. Error output is slightly more specific now (was previously reporting line x column 1, now reporting line x column y).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Aaron Stone

Ping. Ready to pull this in?

Robey Pointer
Collaborator
robey commented

sorry about that! ok, i think this is good, if you add the importer() back, and merge master in so it can be auto-merged. :)

Aaron Stone

Ok, will do!

Erik van oosten

Hi, what is the status of this pull request?

Aaron Stone

Oh man, I totally lost track of this work. I don't have time to rebase my branch right now, but maybe in a week or two.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
1  README.md
Source Rendered
@@ -163,3 +163,4 @@ included file `LICENSE`.
163 163 - Robey Pointer
164 164 - Ian Ownbey
165 165 - Jeremy Cloud
  166 +- Aaron Stone
9 src/main/scala/com/twitter/handlebar/Handlebar.scala
@@ -16,6 +16,9 @@
16 16
17 17 package com.twitter.handlebar
18 18
  19 +import scala.util.parsing.input.StreamReader
  20 +import java.io.StringReader
  21 +
19 22 case class Unpacker[T](
20 23 document: AST.Document,
21 24 unpacker: T => Dictionary,
@@ -29,6 +32,9 @@ object Handlebar {
29 32 import Dictionary._
30 33
31 34 def generate(template: String, dictionary: Dictionary): String =
  35 + generate(Parser(StreamReader(new StringReader(template))), dictionary)
  36 +
  37 + def generate(template: StreamReader, dictionary: Dictionary): String =
32 38 generate(Parser(template), dictionary)
33 39
34 40 def generate(document: Document, dictionary: Dictionary): String = {
@@ -61,7 +67,8 @@ object Handlebar {
61 67 }
62 68
63 69 case class Handlebar(document: AST.Document) {
64   - def this(template: String) = this(Parser(template))
  70 + def this(template: StreamReader) = this(Parser(template))
  71 + def this(template: String) = this(StreamReader(new StringReader(template)))
65 72
66 73 /**
67 74 * Create a string out of a template, using a dictionary to fill in the blanks.
5 src/main/scala/com/twitter/handlebar/Parser.scala
@@ -19,6 +19,8 @@ package com.twitter.handlebar
19 19 import scala.collection.mutable
20 20 import scala.util.parsing.combinator._
21 21 import scala.util.parsing.combinator.lexical._
  22 +import scala.util.parsing.input.StreamReader
  23 +import java.io.StringReader
22 24
23 25 class ParseException(reason: String, cause: Throwable) extends Exception(reason, cause) {
24 26 def this(reason: String) = this(reason, null)
@@ -64,7 +66,7 @@ object Parser extends RegexParsers {
64 66 def id = """[A-Za-z0-9_\.]+""".r
65 67
66 68 // rawr.
67   - def apply(in: String): Document = {
  69 + def apply(in: StreamReader): Document = {
68 70 CleanupWhitespace {
69 71 parseAll(document, in) match {
70 72 case Success(result, _) => result
@@ -73,6 +75,7 @@ object Parser extends RegexParsers {
73 75 }
74 76 }
75 77 }
  78 + def apply(in: String): Document = apply(StreamReader(new StringReader(in)))
76 79 }
77 80
78 81 /**
4 src/main/scala/com/twitter/scrooge/AST.scala
@@ -16,8 +16,10 @@
16 16
17 17 package com.twitter.scrooge
18 18
  19 +import scala.util.parsing.input.Positional
  20 +
19 21 object AST {
20   - sealed abstract class Node
  22 + sealed abstract class Node extends Positional
21 23 sealed abstract class Requiredness extends Node {
22 24 def isOptional = this eq Requiredness.Optional
23 25 def isRequired = this eq Requiredness.Required
42 src/main/scala/com/twitter/scrooge/Main.scala
@@ -90,22 +90,36 @@ object Main {
90 90 }
91 91
92 92 for (inputFile <- thriftFiles) {
93   - val inputFileDir = new File(inputFile).getParent
94   - val importer = Importer.fileImporter(inputFileDir :: importPaths.toList)
95   - val parser = new ScroogeParser(importer)
96   - val doc0 = parser.parseFile(inputFile).mapNamespaces(namespaceMappings.toMap)
  93 + try {
  94 + if (verbose) print("+ Compiling %s".format(inputFile))
97 95
98   - val outputFile = outputFilename map { new File(_) } getOrElse generator.outputFile(destFolder, doc0, inputFile)
99   - val lastModified = importer.lastModified(inputFile).getOrElse(Long.MaxValue)
100   - if (!(skipUnchanged && isUnchanged(outputFile, lastModified))) {
101   - if (verbose) println("+ Compiling %s".format(inputFile))
102   - val doc1 = TypeResolver().resolve(doc0).document
103   - val content = generator(doc1, flags.toSet)
  96 + val inputFileDir = new File(inputFile).getParent
  97 + val importer = Importer.fileImporter(inputFileDir :: importPaths.toList)
  98 + val parser = new ScroogeParser(importer)
  99 + val doc0 = parser.parseFile(inputFile).mapNamespaces(namespaceMappings.toMap)
  100 + val outputFile = outputFilename map { new File(_) } getOrElse generator.outputFile(destFolder, doc0, inputFile)
  101 + val lastModified = importer.lastModified(inputFile).getOrElse(Long.MaxValue)
104 102
105   - Option(outputFile.getParentFile).foreach { _.mkdirs() }
106   - val out = new FileWriter(outputFile)
107   - out.write(content)
108   - out.close()
  103 + if (skipUnchanged && isUnchanged(outputFile, lastModified)) {
  104 + if (verbose) print(" (unchanged)")
  105 +
  106 + } else {
  107 + val doc1 = TypeResolver().resolve(doc0).document
  108 + val content = generator(doc1, flags.toSet)
  109 +
  110 + Option(outputFile.getParentFile).foreach { _.mkdirs() }
  111 + val out = new FileWriter(outputFile)
  112 + out.write(content)
  113 + out.close()
  114 + }
  115 + } catch {
  116 + case ex: Exception => {
  117 + if (verbose) println("")
  118 + System.err.println(inputFile + ": " + ex)
  119 + System.exit(1)
  120 + }
  121 + } finally {
  122 + if (verbose) println("")
109 123 }
110 124 }
111 125 }
4 src/main/scala/com/twitter/scrooge/MustacheLoader.scala
@@ -20,6 +20,8 @@ import com.twitter.conversions.string._
20 20 import com.twitter.handlebar.Handlebar
21 21 import scala.collection.mutable.HashMap
22 22 import scala.io.Source
  23 +import scala.util.parsing.input.StreamReader
  24 +import java.io.{FileInputStream, InputStreamReader}
23 25
24 26 class HandlebarLoader(prefix: String, suffix: String = ".scala") {
25 27 private val cache = new HashMap[String, Handlebar]
@@ -32,7 +34,7 @@ class HandlebarLoader(prefix: String, suffix: String = ".scala") {
32 34 throw new NoSuchElementException("template not found: " + fullName)
33 35 }
34 36 case inputStream => {
35   - new Handlebar(Source.fromInputStream(inputStream).getLines().mkString("\n"))
  37 + new Handlebar(StreamReader(new InputStreamReader(inputStream)))
36 38 }
37 39 }
38 40 )
18 src/main/scala/com/twitter/scrooge/ScroogeParser.scala
@@ -19,6 +19,8 @@ package com.twitter.scrooge
19 19 import scala.collection.mutable
20 20 import scala.util.parsing.combinator._
21 21 import scala.util.parsing.combinator.lexical._
  22 +import scala.util.parsing.input.{Positional, StreamReader}
  23 +import java.io.{FileInputStream, InputStreamReader, StringReader}
22 24
23 25 class ParseException(reason: String, cause: Throwable) extends Exception(reason, cause) {
24 26 def this(reason: String) = this(reason, null)
@@ -77,15 +79,15 @@ class ScroogeParser(importer: Importer) extends RegexParsers {
77 79 MapConstant(Map(list.map { case k ~ x ~ v => (k, v) }: _*))
78 80 }
79 81
80   - def identifier = "[A-Za-z_][A-Za-z0-9\\._]*".r ^^ { x => Identifier(x) }
  82 + def identifier = positioned("[A-Za-z_][A-Za-z0-9\\._]*".r ^^ { x => Identifier(x) })
81 83
82 84 // types
83 85
84   - def fieldType: Parser[FieldType] = baseType | containerType | referenceType
  86 + def fieldType: Parser[FieldType] = positioned(baseType) | positioned(containerType) | positioned(referenceType)
85 87
86 88 def referenceType = identifier ^^ { x => ReferenceType(x.name) }
87 89
88   - def definitionType = baseType | containerType
  90 + def definitionType = positioned(baseType) | positioned(containerType)
89 91
90 92 def baseType: Parser[BaseType] = (
91 93 "bool" ^^^ TBool |
@@ -155,7 +157,7 @@ class ScroogeParser(importer: Importer) extends RegexParsers {
155 157
156 158 // definitions
157 159
158   - def definition = const | typedef | enum | senum | struct | exception | service
  160 + def definition = positioned(const) | positioned(typedef) | positioned(enum) | positioned(senum) | positioned(struct) | positioned(exception) | positioned(service)
159 161
160 162 def const = "const" ~> fieldType ~ identifier ~ ("=" ~> constant) ~ opt(listSeparator) ^^ {
161 163 case ftype ~ id ~ const ~ _ => Const(id.name, ftype, const)
@@ -222,14 +224,14 @@ class ScroogeParser(importer: Importer) extends RegexParsers {
222 224 def namespaceScope = "*" | (identifier ^^ { id => id.name })
223 225
224 226 // rawr.
225   -
226   - def parse[T](in: String, parser: Parser[T]): T = {
  227 + def parse[T](in: StreamReader, parser: Parser[T]): T = {
227 228 parseAll(parser, in) match {
228 229 case Success(result, _) => result
229   - case x @ Failure(msg, z) => throw new ParseException(x.toString)
  230 + case x @ Failure(msg, _) => throw new ParseException(x.toString)
230 231 case x @ Error(msg, _) => throw new ParseException(x.toString)
231 232 }
232 233 }
  234 + def parse[T](in: String, parser: Parser[T]): T = parse(StreamReader(new StringReader(in)), parser)
233 235
234   - def parseFile(filename: String) = parse(importer(filename), document)
  236 + def parseFile(filename: String) = parse(StreamReader(new InputStreamReader(new FileInputStream((filename)))), document)
235 237 }
120 src/main/scala/com/twitter/scrooge/TypeResolver.scala
@@ -18,10 +18,17 @@ package com.twitter.scrooge
18 18
19 19 import AST._
20 20 import scala.collection.mutable.ArrayBuffer
  21 +import scala.util.DynamicVariable
  22 +import scala.util.parsing.input.{Position, NoPosition}
21 23
22   -class TypeNotFoundException(name: String) extends Exception(name)
23   -class UndefinedSymbolException(name: String) extends Exception(name)
24   -class TypeMismatchException(name: String) extends Exception(name)
  24 +class ExceptionAt(name: String)(implicit node: DynamicVariable[Node]) extends Exception(name) {
  25 + val pos: Position = if (node.value eq null) NoPosition else node.value.pos
  26 + override def toString: String = super.toString + " at " + pos
  27 +}
  28 +
  29 +class TypeNotFoundException(name: String)(implicit node: DynamicVariable[Node]) extends ExceptionAt(name)
  30 +class UndefinedSymbolException(name: String)(implicit node: DynamicVariable[Node]) extends ExceptionAt(name)
  31 +class TypeMismatchException(name: String)(implicit node: DynamicVariable[Node]) extends ExceptionAt(name)
25 32
26 33 case class ResolvedDocument(document: Document, resolver: TypeResolver)
27 34 case class ResolvedDefinition(definition: Definition, resolver: TypeResolver)
@@ -31,6 +38,7 @@ object TypeResolver {
31 38 typeMap: Map[String,T],
32 39 includeMap: Map[String, ResolvedDocument],
33 40 next: TypeResolver => EntityResolver[T])
  41 + (implicit node: DynamicVariable[Node])
34 42 {
35 43 def apply(name: String): T = {
36 44 name match {
@@ -75,6 +83,8 @@ case class TypeResolver(
75 83 {
76 84 import TypeResolver._
77 85
  86 + implicit val dynCurrentNode: DynamicVariable[Node] = new DynamicVariable[Node](null)
  87 +
78 88 lazy val fieldTypeResolver: EntityResolver[FieldType] =
79 89 new EntityResolver(typeMap, includeMap, _.fieldTypeResolver)
80 90 lazy val serviceResolver: EntityResolver[Service] =
@@ -114,7 +124,7 @@ case class TypeResolver(
114 124 * typeMap, and then returns an updated TypeResolver with the new
115 125 * definition bound, plus the resolved definition.
116 126 */
117   - def resolve(definition: Definition): ResolvedDefinition = {
  127 + def resolve(definition: Definition): ResolvedDefinition = dynCurrentNode.withValue(definition) {
118 128 apply(definition) match {
119 129 case d @ Typedef(name, t) => ResolvedDefinition(d, define(name, t))
120 130 case e @ Enum(name, _) => ResolvedDefinition(e, define(name, EnumType(e)))
@@ -148,7 +158,7 @@ case class TypeResolver(
148 158 copy(serviceMap = serviceMap + (service.name -> service))
149 159 }
150 160
151   - def apply(definition: Definition): Definition = {
  161 + def apply(definition: Definition): Definition = dynCurrentNode.withValue(definition) {
152 162 definition match {
153 163 case d @ Typedef(name, t) => d.copy(`type` = apply(t))
154 164 case s @ Struct(_, fs) => s.copy(fields = fs.map(apply))
@@ -173,57 +183,63 @@ case class TypeResolver(
173 183 default = f.default.map { const => apply(const, fieldType) })
174 184 }
175 185
176   - def apply(t: FunctionType): FunctionType = t match {
177   - case Void => Void
178   - case t: FieldType => apply(t)
  186 + def apply(t: FunctionType): FunctionType = dynCurrentNode.withValue(t) {
  187 + t match {
  188 + case Void => Void
  189 + case t: FieldType => apply(t)
  190 + }
179 191 }
180 192
181   - def apply(t: FieldType): FieldType = t match {
182   - case ReferenceType(name) => apply(name)
183   - case m @ MapType(k, v, _) => m.copy(keyType = apply(k), valueType = apply(v))
184   - case s @ SetType(e, _) => s.copy(eltType = apply(e))
185   - case l @ ListType(e, _) => l.copy(eltType = apply(e))
186   - case _ => t
  193 + def apply(t: FieldType): FieldType = dynCurrentNode.withValue(t) {
  194 + t match {
  195 + case ReferenceType(name) => apply(name)
  196 + case m @ MapType(k, v, _) => m.copy(keyType = apply(k), valueType = apply(v))
  197 + case s @ SetType(e, _) => s.copy(eltType = apply(e))
  198 + case l @ ListType(e, _) => l.copy(eltType = apply(e))
  199 + case _ => t
  200 + }
187 201 }
188 202
189   - def apply(c: Constant, fieldType: FieldType): Constant = c match {
190   - case l @ ListConstant(elems) =>
191   - fieldType match {
192   - case ListType(eltType, _) => l.copy(elems = elems map { e => apply(e, eltType) } )
193   - case SetType(eltType, _) => SetConstant(elems map { e => apply(e, eltType) } toSet)
194   - case _ => throw new TypeMismatchException("Expecting " + fieldType + ", found " + l)
195   - }
196   - case m @ MapConstant(elems) =>
197   - fieldType match {
198   - case MapType(keyType, valType, _) =>
199   - m.copy(elems = elems.map { case (k, v) => (apply(k, keyType), apply(v, valType)) })
200   - case _ => throw new TypeMismatchException("Expecting " + fieldType + ", found " + m)
201   - }
202   - case i @ Identifier(name) =>
203   - fieldType match {
204   - case EnumType(enum, _) =>
205   - val valueName = name match {
206   - case QualifiedName(scope, QualifiedName(enumName, valueName)) =>
207   - if (fieldTypeResolver(scope, enumName) != fieldType) {
208   - throw new UndefinedSymbolException(scope + "." + enumName)
209   - } else {
210   - valueName
211   - }
212   - case QualifiedName(enumName, valueName) =>
213   - if (enumName != enum.name) {
214   - throw new UndefinedSymbolException(enumName)
215   - } else {
216   - valueName
217   - }
218   - case _ => name
219   - }
220   - enum.values.find(_.name == valueName) match {
221   - case None => throw new UndefinedSymbolException(name)
222   - case Some(value) => EnumValueConstant(enum, value)
223   - }
224   - case _ => throw new UndefinedSymbolException(name)
225   - }
226   - case _ => c
  203 + def apply(c: Constant, fieldType: FieldType): Constant = dynCurrentNode.withValue(c) {
  204 + c match {
  205 + case l @ ListConstant(elems) =>
  206 + fieldType match {
  207 + case ListType(eltType, _) => l.copy(elems = elems map { e => apply(e, eltType) } )
  208 + case SetType(eltType, _) => SetConstant(elems map { e => apply(e, eltType) } toSet)
  209 + case _ => throw new TypeMismatchException("Expecting " + fieldType + ", found " + l)
  210 + }
  211 + case m @ MapConstant(elems) =>
  212 + fieldType match {
  213 + case MapType(keyType, valType, _) =>
  214 + m.copy(elems = elems.map { case (k, v) => (apply(k, keyType), apply(v, valType)) })
  215 + case _ => throw new TypeMismatchException("Expecting " + fieldType + ", found " + m)
  216 + }
  217 + case i @ Identifier(name) =>
  218 + fieldType match {
  219 + case EnumType(enum, _) =>
  220 + val valueName = name match {
  221 + case QualifiedName(scope, QualifiedName(enumName, valueName)) =>
  222 + if (fieldTypeResolver(scope, enumName) != fieldType) {
  223 + throw new UndefinedSymbolException(scope + "." + enumName)
  224 + } else {
  225 + valueName
  226 + }
  227 + case QualifiedName(enumName, valueName) =>
  228 + if (enumName != enum.name) {
  229 + throw new UndefinedSymbolException(enumName)
  230 + } else {
  231 + valueName
  232 + }
  233 + case _ => name
  234 + }
  235 + enum.values.find(_.name == valueName) match {
  236 + case None => throw new UndefinedSymbolException(name)
  237 + case Some(value) => EnumValueConstant(enum, value)
  238 + }
  239 + case _ => throw new UndefinedSymbolException(name)
  240 + }
  241 + case _ => c
  242 + }
227 243 }
228 244
229 245 def apply(parent: ServiceParent): ServiceParent = {
2  src/test/scala/com/twitter/scrooge/ScroogeParserSpec.scala
@@ -162,7 +162,7 @@ class ScroogeParserSpec extends Specification {
162 162
163 163 "standard test file" in {
164 164 val parser = new ScroogeParser(Importer.resourceImporter(getClass))
165   - val doc = parser.parseFile("/test.thrift")
  165 + val doc = parser.parseFile("test.thrift")
166 166 // i guess not blowing up is a good first-pass test.
167 167 // might be nice to verify parts of it tho.
168 168 doc.headers.size mustEqual 13

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.