From ad8521c22378fb167748588a7bb352d516cf6f7b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 26 Jul 2014 11:27:47 -0400 Subject: [PATCH 1/5] Include a block yield in the Ruby example. --- spec/stacktrace-spec.coffee | 29 ++++++++++++++++------------- spec/trace-fixtures.coffee | 12 +++++++----- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/spec/stacktrace-spec.coffee b/spec/stacktrace-spec.coffee index 2dbe3cb..4ee02b5 100644 --- a/spec/stacktrace-spec.coffee +++ b/spec/stacktrace-spec.coffee @@ -1,47 +1,50 @@ {Stacktrace} = require '../lib/stacktrace' -{RUBY_TRACE} = require './trace-fixtures' +{RUBY: {FUNCTION: TRACE}} = require './trace-fixtures' describe 'Stacktrace', -> describe 'with a Ruby trace', -> [trace, checksum] = [] beforeEach -> - trace = Stacktrace.parse(RUBY_TRACE) - checksum = '3e325af231517f1e4fbe80f70c2c95296250ba80dc4de90bd5ac9c581506d9a6' + trace = Stacktrace.parse(TRACE) + checksum = '9528763b5ab8ef052e2400e39d0f32dbe59ffcd06f039adc487f4f956511691f' describe 'preparation', -> it 'trims leading and trailing whitespace from each raw line', -> lines = (frame.rawLine for frame in trace.frames) expected = [ - "/home/smash/tmp/tracer/dir/file1.rb:3:in `innerfunction': Oh shit (RuntimeError)" - "from /home/smash/tmp/tracer/otherdir/file2.rb:5:in `outerfunction'" - "from entry.rb:7:in `toplevel'" - "from entry.rb:10:in `
'" + "/home/smash/samples/tracer/otherdir/file2.rb:6:in `block in outerfunction': whoops (RuntimeError)" + "from /home/smash/samples/tracer/dir/file1.rb:3:in `innerfunction'" + "from /home/smash/samples/tracer/otherdir/file2.rb:5:in `outerfunction'" + "from /home/smash/samples/tracer/entry.rb:7:in `toplevel'" + "from /home/smash/samples/tracer/entry.rb:10:in `
'" ] expect(lines).toEqual(expected) describe 'parsing a Ruby stack trace', -> it 'parses the error message', -> - expect(trace.message).toBe('Oh shit (RuntimeError)') + expect(trace.message).toBe('whoops (RuntimeError)') it 'parses file paths from each frame', -> filePaths = (frame.path for frame in trace.frames) expected = [ - '/home/smash/tmp/tracer/dir/file1.rb' - '/home/smash/tmp/tracer/otherdir/file2.rb' - 'entry.rb' - 'entry.rb' + '/home/smash/samples/tracer/otherdir/file2.rb' + '/home/smash/samples/tracer/dir/file1.rb' + '/home/smash/samples/tracer/otherdir/file2.rb' + '/home/smash/samples/tracer/entry.rb' + '/home/smash/samples/tracer/entry.rb' ] expect(filePaths).toEqual(expected) it 'parses line numbers from each frame', -> lineNumbers = (frame.lineNumber for frame in trace.frames) - expected = [3, 5, 7, 10] + expected = [6, 3, 5, 7, 10] expect(lineNumbers).toEqual(lineNumbers) it 'parses function names from each frame', -> functionNames = (frame.functionName for frame in trace.frames) expected = [ + 'block in outerfunction' 'innerfunction' 'outerfunction' 'toplevel' diff --git a/spec/trace-fixtures.coffee b/spec/trace-fixtures.coffee index 1c9970a..fbac95a 100644 --- a/spec/trace-fixtures.coffee +++ b/spec/trace-fixtures.coffee @@ -1,9 +1,11 @@ # Stack traces shared among several specs. module.exports = - RUBY_TRACE: """ - /home/smash/tmp/tracer/dir/file1.rb:3:in `innerfunction': Oh shit (RuntimeError) - from /home/smash/tmp/tracer/otherdir/file2.rb:5:in `outerfunction' - from entry.rb:7:in `toplevel' - from entry.rb:10:in `
' + RUBY: + FUNCTION: """ + /home/smash/samples/tracer/otherdir/file2.rb:6:in `block in outerfunction': whoops (RuntimeError) + from /home/smash/samples/tracer/dir/file1.rb:3:in `innerfunction' + from /home/smash/samples/tracer/otherdir/file2.rb:5:in `outerfunction' + from /home/smash/samples/tracer/entry.rb:7:in `toplevel' + from /home/smash/samples/tracer/entry.rb:10:in `
' """ From b0d8b3be73946b805292d86ce0db45ccce1b4b9d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 26 Jul 2014 15:29:11 -0400 Subject: [PATCH 2/5] Stub out the tracerParser framework. --- lib/parsers/ruby-trace-parser.coffee | 6 ++ lib/stacktrace.coffee | 9 +- lib/trace-parser.coffee | 102 +++++++++++++++++++++ spec/parsers/ruby-trace-parser-spec.coffee | 18 ++++ spec/parsers/trace-parser-driver.coffee | 7 ++ spec/trace-parser-spec.coffee | 6 ++ 6 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 lib/parsers/ruby-trace-parser.coffee create mode 100644 lib/trace-parser.coffee create mode 100644 spec/parsers/ruby-trace-parser-spec.coffee create mode 100644 spec/parsers/trace-parser-driver.coffee create mode 100644 spec/trace-parser-spec.coffee diff --git a/lib/parsers/ruby-trace-parser.coffee b/lib/parsers/ruby-trace-parser.coffee new file mode 100644 index 0000000..01ba661 --- /dev/null +++ b/lib/parsers/ruby-trace-parser.coffee @@ -0,0 +1,6 @@ + +module.exports = + + recognize: (f, {emitMessage, emitFrame, emitStack}) -> + + consume: (f, {emitMessage, emitFrame, emitStack}) -> diff --git a/lib/stacktrace.coffee b/lib/stacktrace.coffee index c43cc36..0606975 100644 --- a/lib/stacktrace.coffee +++ b/lib/stacktrace.coffee @@ -5,11 +5,13 @@ PREFIX = 'stacktrace://trace' REGISTRY = {} # Internal: A heuristically parsed and interpreted stacktrace. +# class Stacktrace constructor: (@frames = [], @message = '') -> # Internal: Compute the SHA256 checksum of the normalized stacktrace. + # getChecksum: -> body = (frame.rawLine for frame in @frames).join() sha = new jsSHA(body, 'TEXT') @@ -17,14 +19,17 @@ class Stacktrace # Internal: Generate a URL that can be used to launch or focus a # {StacktraceView}. + # getUrl: -> @url ?= "#{PREFIX}/#{@getChecksum()}" # Internal: Register this trace in a global map by its URL. + # register: -> REGISTRY[@getUrl()] = this # Internal: Remove this trace from the global map if it had previously been # registered. + # unregister: -> delete REGISTRY[@getUrl()] @@ -45,13 +50,15 @@ class Stacktrace REGISTRY = {} # Internal: A single stack frame within a {Stacktrace}. +# class Frame - constructor: (@rawLine, @path, @lineNumber, @functionName, @message = null) -> + constructor: (@rawLine, @path, @lineNumber, @functionName) -> # Internal: Parse a Ruby stack frame. This is a simple placeholder until I # put together a class hierarchy to handle frame recognition and parsing. +# parseRubyFrame = (rawLine) -> m = rawLine.trim().match /// ^ (?:from \s+)? # On all lines but the first diff --git a/lib/trace-parser.coffee b/lib/trace-parser.coffee new file mode 100644 index 0000000..b10c83d --- /dev/null +++ b/lib/trace-parser.coffee @@ -0,0 +1,102 @@ +{Stacktrace, Frame} = require './stacktrace' +fs = require 'fs' +path = require 'path' + +# Internal: Build a Frame instance with a simple DSL. +# +class FrameBuilder + + constructor: (@rawLine) -> + [@path, @lineNumber, @functionName, @message] = null + + path: (@path) -> + + lineNumber: (@lineNumber) -> + + functionName: (@functionName) -> + + message: (@message) -> + +# Internal: Use the collected information from a FrameBuilder to instantiate a Frame. +# +asFrame = (fb) -> + required = [ + { name: 'rawLine', ok: fb.rawLine? } + { name: 'path', ok: fb.path? } + { name: 'lineNumber', ok: fb.lineNumber? } + { name: 'functionName', ok: fb.functionName? } + ] + missing = (r.name for r in required when not r.ok) + + unless missing.length is 0 + e = new Error('Missing required frame attributes.') + e.missing = missing + e.rawLine = fb.rawLine + throw e + + new Frame(fb.rawLine, fb.path, fb.lineNumber, fb.functionName, fb.message) + +allTracers = null + +# Internal: Load stacktrace parsers from the parsers/ directory. +# +loadTracers = -> + allTracers = [] + parsersPath = path.resolve(__dirname, 'parsers') + for parserFile in fs.readdirSync(parsersPath) + allTracers.push require(path.join parsersPath, parserFile) + +# Internal: Parse zero or more stacktraces from a sample of text. +# +# text - String output sample that may contain one or more stacktraces from a +# supported language. +# tracers - If provided, use only the provided tracer objects. Otherwise, everything in parsers/ +# will be loaded and used. +# +# Returns: An Array of Stacktrace objects, in the order in which they occurred +# in the original sample. +# +traceParser = (text, tracers = null) -> + unless tracers? + loadTracers() unless allTracers? + tracers = allTracers + + stacks = [] + frames = [] + message = null + activeTracer = null + + finishStacktrace = -> + s = new Stacktrace(frames, message) + stacks.push s + + frames = [] + message = null + activeTracer = null + + for rawLine in text.split(/\r?\n/) + trimmed = rawLine.trim() + + # Mid-stack frame. + if activeTracer? + fb = new FrameBuilder(trimmed) + activeTracer.consume fb, + emitMessage: (m) -> message = m + emitFrame: -> frames.push asFrame fb + emitStack: finishStacktrace + + # Outside of a frame. Attempt to recognize the next trace by emitting at least one frame. + unless activeTracer? + for t in tracers + t.recognize fb, + emitMessage: (m) -> message = m + emitFrame: -> frames = [asFrame(fb)] + emitStack: finishStacktrace + if message? or frames.length > 0 + activeTracer = t + break + + stacks + +module.exports = + traceParser: traceParser diff --git a/spec/parsers/ruby-trace-parser-spec.coffee b/spec/parsers/ruby-trace-parser-spec.coffee new file mode 100644 index 0000000..9e11616 --- /dev/null +++ b/spec/parsers/ruby-trace-parser-spec.coffee @@ -0,0 +1,18 @@ +{traceParser} = require '../../lib/trace-parser' +rubyTracer = require '../../lib/parsers/ruby-trace-parser' +ts = require '../trace-fixtures' + +describe 'rubyTraceParser', -> + describe 'recognition', -> + + it 'parses a trace from each Ruby fixture', -> + for f in Object.keys(ts.RUBY) + result = traceParser(ts.RUBY[f], [rubyTracer]) + expect(result.length > 0).toBe(true) + + it "doesn't parse a trace from any non-Ruby fixture", -> + for k in Object.keys(ts) + if k isnt 'RUBY' + for f in Object.keys(ts[k]) + result = traceParser(ts[k][f], [rubyTracer]) + expect(result.length).toBe(0) diff --git a/spec/parsers/trace-parser-driver.coffee b/spec/parsers/trace-parser-driver.coffee new file mode 100644 index 0000000..cdc7abb --- /dev/null +++ b/spec/parsers/trace-parser-driver.coffee @@ -0,0 +1,7 @@ + +module.exports = + + # Public: Imitate sending a chunk of text to the chosen parser. + # + drive: (parser, text) -> + lines = (line.trim() for line in text.split(/\r?\n/)) diff --git a/spec/trace-parser-spec.coffee b/spec/trace-parser-spec.coffee new file mode 100644 index 0000000..a9d10d1 --- /dev/null +++ b/spec/trace-parser-spec.coffee @@ -0,0 +1,6 @@ +{traceParser} = require '../lib/trace-parser' + +describe 'traceParser', -> + describe 'with no traces', -> + it 'returns an empty array', -> + expect(traceParser('')).toEqual([]) From d5d7e221d8ac9878a4f7d29334266db93f674601 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 26 Jul 2014 15:55:58 -0400 Subject: [PATCH 3/5] Starting Ruby traces. --- lib/parsers/ruby-trace-parser.coffee | 32 +++++++++++++++++++++++++-- lib/trace-parser.coffee | 33 +++++++++++++++------------- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/lib/parsers/ruby-trace-parser.coffee b/lib/parsers/ruby-trace-parser.coffee index 01ba661..8d466fb 100644 --- a/lib/parsers/ruby-trace-parser.coffee +++ b/lib/parsers/ruby-trace-parser.coffee @@ -1,6 +1,34 @@ module.exports = - recognize: (f, {emitMessage, emitFrame, emitStack}) -> + recognize: (line, f, {emitMessage, emitFrame, emitStack}) -> + m = line.match /// ^ + ([^:]+) : # File path + (\d+) : # Line number + in \s* ` ([^']+) ' # Function name + : \s (.+) # Error message + $ + /// + return unless m? - consume: (f, {emitMessage, emitFrame, emitStack}) -> + f.path m[1] + f.lineNumber parseInt m[2] + f.functionName m[3] + + emitMessage m[4] + emitFrame() + + consume: (line, f, {emitMessage, emitFrame, emitStack}) -> + m = line.match /// ^ + from \s+ # from + ([^:]+) : # File path + (\d+) : # Line number + in \s* ` ([^']+) ' # Function name + $ + /// + return emitStack() unless m? + + f.path m[1] + f.lineNumber parseInt m[2] + f.functionName m[3] + emitFrame() diff --git a/lib/trace-parser.coffee b/lib/trace-parser.coffee index b10c83d..43dbfe8 100644 --- a/lib/trace-parser.coffee +++ b/lib/trace-parser.coffee @@ -1,40 +1,39 @@ {Stacktrace, Frame} = require './stacktrace' fs = require 'fs' path = require 'path' +util = require 'util' # Internal: Build a Frame instance with a simple DSL. # class FrameBuilder - constructor: (@rawLine) -> - [@path, @lineNumber, @functionName, @message] = null + constructor: (@_rawLine) -> + [@_path, @_lineNumber, @_functionName] = [] - path: (@path) -> + path: (@_path) -> - lineNumber: (@lineNumber) -> + lineNumber: (@_lineNumber) -> - functionName: (@functionName) -> - - message: (@message) -> + functionName: (@_functionName) -> # Internal: Use the collected information from a FrameBuilder to instantiate a Frame. # asFrame = (fb) -> required = [ - { name: 'rawLine', ok: fb.rawLine? } - { name: 'path', ok: fb.path? } - { name: 'lineNumber', ok: fb.lineNumber? } - { name: 'functionName', ok: fb.functionName? } + { name: 'rawLine', ok: fb._rawLine? } + { name: 'path', ok: fb._path? } + { name: 'lineNumber', ok: fb._lineNumber? } + { name: 'functionName', ok: fb._functionName? } ] missing = (r.name for r in required when not r.ok) unless missing.length is 0 - e = new Error('Missing required frame attributes.') + e = new Error("Missing required frame attributes: #{missing.join ', '}") e.missing = missing e.rawLine = fb.rawLine throw e - new Frame(fb.rawLine, fb.path, fb.lineNumber, fb.functionName, fb.message) + new Frame(fb._rawLine, fb._path, fb._lineNumber, fb._functionName) allTracers = null @@ -80,7 +79,7 @@ traceParser = (text, tracers = null) -> # Mid-stack frame. if activeTracer? fb = new FrameBuilder(trimmed) - activeTracer.consume fb, + activeTracer.consume trimmed, fb, emitMessage: (m) -> message = m emitFrame: -> frames.push asFrame fb emitStack: finishStacktrace @@ -88,7 +87,8 @@ traceParser = (text, tracers = null) -> # Outside of a frame. Attempt to recognize the next trace by emitting at least one frame. unless activeTracer? for t in tracers - t.recognize fb, + fb = new FrameBuilder(trimmed) + t.recognize trimmed, fb, emitMessage: (m) -> message = m emitFrame: -> frames = [asFrame(fb)] emitStack: finishStacktrace @@ -96,6 +96,9 @@ traceParser = (text, tracers = null) -> activeTracer = t break + # Finalize the last Stacktrace. + finishStacktrace() if frames.length > 0 + stacks module.exports = From a64ef8376cb9df45f560bed112904bfd10a21d4a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 26 Jul 2014 16:25:29 -0400 Subject: [PATCH 4/5] Use traceParser as a backend for Stacktrace.parse(). --- lib/stacktrace.coffee | 29 ++++++---------------- lib/trace-parser.coffee | 2 +- spec/parsers/ruby-trace-parser-spec.coffee | 2 +- spec/stacktrace-spec.coffee | 2 +- 4 files changed, 10 insertions(+), 25 deletions(-) diff --git a/lib/stacktrace.coffee b/lib/stacktrace.coffee index 0606975..cfa23fe 100644 --- a/lib/stacktrace.coffee +++ b/lib/stacktrace.coffee @@ -1,4 +1,5 @@ jsSHA = require 'jssha' +traceParser = null PREFIX = 'stacktrace://trace' @@ -33,12 +34,13 @@ class Stacktrace unregister: -> delete REGISTRY[@getUrl()] + # Public: Parse zero to many Stacktrace instances from a corpus of text. + # + # text - A raw blob of text. + # @parse: (text) -> - frames = [] - for rawLine in text.split(/\r?\n/) - f = parseRubyFrame(rawLine) - frames.push f if f? - new Stacktrace(frames, frames[0].message) + {traceParser} = require('./trace-parser') unless traceParser? + traceParser(text) # Internal: Return a registered trace, or null if none match the provided # URL. @@ -55,23 +57,6 @@ class Frame constructor: (@rawLine, @path, @lineNumber, @functionName) -> - -# Internal: Parse a Ruby stack frame. This is a simple placeholder until I -# put together a class hierarchy to handle frame recognition and parsing. -# -parseRubyFrame = (rawLine) -> - m = rawLine.trim().match /// ^ - (?:from \s+)? # On all lines but the first - ([^:]+) : # File path - (\d+) : # Line number - in \s* ` ([^']+) ' # Function name - (?: : \s (.*))? # Error message, only on the first - /// - - if m? - [raw, path, lineNumber, functionName, message] = m - new Frame(raw, path, lineNumber, functionName, message) - module.exports = PREFIX: PREFIX Stacktrace: Stacktrace diff --git a/lib/trace-parser.coffee b/lib/trace-parser.coffee index 43dbfe8..80f27a7 100644 --- a/lib/trace-parser.coffee +++ b/lib/trace-parser.coffee @@ -1,7 +1,7 @@ {Stacktrace, Frame} = require './stacktrace' +util = require 'util' fs = require 'fs' path = require 'path' -util = require 'util' # Internal: Build a Frame instance with a simple DSL. # diff --git a/spec/parsers/ruby-trace-parser-spec.coffee b/spec/parsers/ruby-trace-parser-spec.coffee index 9e11616..9bc0bdc 100644 --- a/spec/parsers/ruby-trace-parser-spec.coffee +++ b/spec/parsers/ruby-trace-parser-spec.coffee @@ -2,7 +2,7 @@ rubyTracer = require '../../lib/parsers/ruby-trace-parser' ts = require '../trace-fixtures' -describe 'rubyTraceParser', -> +describe 'rubyTracer', -> describe 'recognition', -> it 'parses a trace from each Ruby fixture', -> diff --git a/spec/stacktrace-spec.coffee b/spec/stacktrace-spec.coffee index 4ee02b5..889de8c 100644 --- a/spec/stacktrace-spec.coffee +++ b/spec/stacktrace-spec.coffee @@ -6,7 +6,7 @@ describe 'Stacktrace', -> [trace, checksum] = [] beforeEach -> - trace = Stacktrace.parse(TRACE) + [trace] = Stacktrace.parse(TRACE) checksum = '9528763b5ab8ef052e2400e39d0f32dbe59ffcd06f039adc487f4f956511691f' describe 'preparation', -> From e12a45fa7918b2099bb45e734f5cda5e74b65e20 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 26 Jul 2014 16:54:10 -0400 Subject: [PATCH 5/5] Initial support for CoffeeScript. --- lib/parsers/coffeescript-trace-parser.coffee | 29 +++++++++++++++++++ lib/trace-parser.coffee | 2 +- .../coffeescript-trace-parser-spec.coffee | 18 ++++++++++++ spec/parsers/trace-parser-driver.coffee | 7 ----- spec/trace-fixtures.coffee | 10 +++++++ 5 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 lib/parsers/coffeescript-trace-parser.coffee create mode 100644 spec/parsers/coffeescript-trace-parser-spec.coffee delete mode 100644 spec/parsers/trace-parser-driver.coffee diff --git a/lib/parsers/coffeescript-trace-parser.coffee b/lib/parsers/coffeescript-trace-parser.coffee new file mode 100644 index 0000000..4b70b11 --- /dev/null +++ b/lib/parsers/coffeescript-trace-parser.coffee @@ -0,0 +1,29 @@ + +module.exports = + + recognize: (line, f, {emitMessage, emitFrame, emitStack}) -> + m = line.match /// ^ + (.*Error) : # Error name + (.+) # Message + $ + /// + return unless m? + + emitMessage line + + consume: (line, f, {emitMessage, emitFrame, emitStack}) -> + m = line.match /// ^ + at \s+ + ([^(]+) # Function name + \( + ([^:]+) : # Path + (\d+) : # Line + (\d+) # Column + \) + /// + return emitStack() unless m? + + f.functionName m[1].trim() + f.path m[2] + f.lineNumber parseInt m[3] + emitFrame() diff --git a/lib/trace-parser.coffee b/lib/trace-parser.coffee index 80f27a7..43dbfe8 100644 --- a/lib/trace-parser.coffee +++ b/lib/trace-parser.coffee @@ -1,7 +1,7 @@ {Stacktrace, Frame} = require './stacktrace' -util = require 'util' fs = require 'fs' path = require 'path' +util = require 'util' # Internal: Build a Frame instance with a simple DSL. # diff --git a/spec/parsers/coffeescript-trace-parser-spec.coffee b/spec/parsers/coffeescript-trace-parser-spec.coffee new file mode 100644 index 0000000..707c57c --- /dev/null +++ b/spec/parsers/coffeescript-trace-parser-spec.coffee @@ -0,0 +1,18 @@ +{traceParser} = require '../../lib/trace-parser' +coffeeTracer = require '../../lib/parsers/coffeescript-trace-parser' +ts = require '../trace-fixtures' + +describe 'coffeeTracer', -> + describe 'recognition', -> + + it 'parses a trace from each CoffeeScript fixture', -> + for f in Object.keys(ts.COFFEESCRIPT) + result = traceParser(ts.COFFEESCRIPT[f], [coffeeTracer]) + expect(result.length > 0).toBe(true) + + it "doesn't parse a trace from any non-CoffeeScript fixture", -> + for k in Object.keys(ts) + if k isnt 'COFFEESCRIPT' + for f in Object.keys(ts[k]) + result = traceParser(ts[k][f], [coffeeTracer]) + expect(result.length).toBe(0) diff --git a/spec/parsers/trace-parser-driver.coffee b/spec/parsers/trace-parser-driver.coffee deleted file mode 100644 index cdc7abb..0000000 --- a/spec/parsers/trace-parser-driver.coffee +++ /dev/null @@ -1,7 +0,0 @@ - -module.exports = - - # Public: Imitate sending a chunk of text to the chosen parser. - # - drive: (parser, text) -> - lines = (line.trim() for line in text.split(/\r?\n/)) diff --git a/spec/trace-fixtures.coffee b/spec/trace-fixtures.coffee index fbac95a..e516aec 100644 --- a/spec/trace-fixtures.coffee +++ b/spec/trace-fixtures.coffee @@ -9,3 +9,13 @@ module.exports = from /home/smash/samples/tracer/entry.rb:7:in `toplevel' from /home/smash/samples/tracer/entry.rb:10:in `
' """ + COFFEESCRIPT: + ERROR: """ + Error: yep + at asFrame (/home/smash/code/stacktrace/lib/trace-parser.coffee:36:13) + at t.recognize.emitFrame (/home/smash/code/stacktrace/lib/trace-parser.coffee:95:35) + at Object.module.exports.recognize (/home/smash/code/stacktrace/lib/parsers/ruby-trace-parser.coffee:19:5) + at traceParser (/home/smash/code/stacktrace/lib/trace-parser.coffee:93:11) + at Function.Stacktrace.parse (/home/smash/code/stacktrace/lib/stacktrace.coffee:43:5) + at [object Object]. (/home/smash/code/stacktrace/spec/stacktrace-spec.coffee:9:28) + """