Compare commits

..

No commits in common. "master" and "v1.0.0" have entirely different histories.

18 changed files with 276 additions and 568 deletions

View file

@ -1,3 +0,0 @@
Lint/NotNil:
Excluded:
- spec/backtracer/backtrace/frame/parser_spec.cr

View file

@ -1,46 +0,0 @@
name: CI
on:
push:
pull_request:
schedule:
- cron: "0 3 * * 1" # Every monday at 3 AM
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
crystal: [latest, nightly]
runs-on: ${{ matrix.os }}
steps:
- name: Install Crystal
uses: oprypin/install-crystal@v1
with:
crystal: ${{ matrix.crystal }}
- name: Download source
uses: actions/checkout@v2
- name: Install dependencies
run: shards install
env:
SHARDS_OPTS: --ignore-crystal-version
- name: Run specs
run: |
crystal spec
crystal spec --no-debug
- name: Run specs (release)
run: |
crystal spec --release
crystal spec --release --no-debug
- name: Check formatting
run: crystal tool format --check
- name: Run ameba linter
run: bin/ameba

20
.travis.yml Normal file
View file

@ -0,0 +1,20 @@
language: crystal
crystal:
- latest
- nightly
jobs:
allow_failures:
- crystal: nightly
install:
- shards install
script:
- crystal spec
- crystal spec --no-debug
- crystal spec --release
- crystal spec --release --no-debug
- crystal tool format --check
- bin/ameba

View file

@ -1,4 +1,4 @@
# backtracer.cr [![CI](https://github.com/Sija/backtracer.cr/actions/workflows/ci.yml/badge.svg)](https://github.com/Sija/backtracer.cr/actions/workflows/ci.yml) [![Releases](https://img.shields.io/github/release/Sija/backtracer.cr.svg)](https://github.com/Sija/backtracer.cr/releases) [![License](https://img.shields.io/github/license/Sija/backtracer.cr.svg)](https://github.com/Sija/backtracer.cr/blob/master/LICENSE) # backtracer.cr [![Build Status](https://travis-ci.com/Sija/backtracer.cr.svg?branch=master)](https://travis-ci.com/Sija/backtracer.cr) [![Releases](https://img.shields.io/github/release/Sija/backtracer.cr.svg)](https://github.com/Sija/backtracer.cr/releases) [![License](https://img.shields.io/github/license/Sija/backtracer.cr.svg)](https://github.com/Sija/backtracer.cr/blob/master/LICENSE)
Crystal shard aiming to assist with parsing backtraces into a structured form. Crystal shard aiming to assist with parsing backtraces into a structured form.

View file

@ -1,5 +1,5 @@
name: backtracer name: backtracer
version: 1.2.2 version: 1.0.0
authors: authors:
- Sijawusz Pur Rahnama <sija@sija.pl> - Sijawusz Pur Rahnama <sija@sija.pl>
@ -7,7 +7,7 @@ authors:
development_dependencies: development_dependencies:
ameba: ameba:
github: crystal-ameba/ameba github: crystal-ameba/ameba
version: ~> 1.5.0 version: ~> 0.13.0
crystal: ">= 0.35.0" crystal: ">= 0.35.0"

View file

@ -1,60 +0,0 @@
require "../../../spec_helper"
def with_foo_context(&)
yield Backtracer::Backtrace::Frame::Context.new(
lineno: 10,
pre: %w[foo bar baz],
line: "violent offender!",
post: %w[boo far faz],
)
end
describe Backtracer::Backtrace::Frame::Context do
describe ".to_a" do
it "works with empty #pre and #post" do
context = Backtracer::Backtrace::Frame::Context.new(
lineno: 1,
pre: %w[],
line: "violent offender!",
post: %w[],
)
context.to_a.should eq(["violent offender!"])
end
it "returns array with #pre, #line and #post strings" do
with_foo_context do |context|
context.to_a.should eq([
"foo", "bar", "baz",
"violent offender!",
"boo", "far", "faz",
])
end
end
end
describe ".to_h" do
it "works with empty #pre and #post" do
context = Backtracer::Backtrace::Frame::Context.new(
lineno: 1,
pre: %w[],
line: "violent offender!",
post: %w[],
)
context.to_h.should eq({1 => "violent offender!"})
end
it "returns hash with #pre, #line and #post strings" do
with_foo_context do |context|
context.to_h.should eq({
7 => "foo",
8 => "bar",
9 => "baz",
10 => "violent offender!",
11 => "boo",
12 => "far",
13 => "faz",
})
end
end
end
end

View file

@ -1,202 +0,0 @@
require "../../../spec_helper"
describe Backtracer::Backtrace::Frame::Parser do
describe ".parse" do
it "fails to parse an empty string" do
expect_raises(ArgumentError) { with_frame("", &.itself) }
end
context "when --no-debug flag is set" do
it "parses frame with any value as method" do
backtrace_line = "__crystal_main"
with_frame(backtrace_line) do |frame|
frame.lineno.should be_nil
frame.column.should be_nil
frame.method.should eq(backtrace_line)
frame.path.should be_nil
frame.relative_path.should be_nil
frame.under_src_path?.should be_false
frame.shard_name.should be_nil
frame.in_app?.should be_false
end
end
end
context "with ~proc signature" do
it "parses absolute path outside of src/ dir" do
path = "/usr/local/Cellar/crystal/0.27.2/src/fiber.cr"
backtrace_line = "~proc2Proc(Fiber, (IO::FileDescriptor | Nil))@#{path}:72"
with_frame(backtrace_line) do |frame|
frame.lineno.should eq(72)
frame.column.should be_nil
frame.method.should eq("~proc2Proc(Fiber, (IO::FileDescriptor | Nil))")
frame.path.should eq(path)
frame.absolute_path.should eq(frame.path)
frame.relative_path.should be_nil
frame.under_src_path?.should be_false
frame.shard_name.should be_nil
frame.in_app?.should be_false
end
end
it "parses relative path inside of lib/ dir" do
with_configuration do |configuration|
path = "lib/kemal/src/kemal/route.cr"
backtrace_line = "~procProc(HTTP::Server::Context, String)@#{path}:11"
with_frame(backtrace_line) do |frame|
frame.lineno.should eq(11)
frame.column.should be_nil
frame.method.should eq("~procProc(HTTP::Server::Context, String)")
frame.path.should eq(path)
frame.absolute_path.should eq(
File.join(configuration.src_path.not_nil!, path)
)
frame.relative_path.should eq(frame.path)
frame.under_src_path?.should be_false
frame.shard_name.should eq("kemal")
frame.in_app?.should be_false
end
end
end
end
it "parses absolute path outside of configuration.src_path" do
path = "/some/absolute/path/to/foo.cr"
with_foo_frame(path: path) do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq(path)
frame.absolute_path.should eq(frame.path)
frame.relative_path.should be_nil
frame.under_src_path?.should be_false
frame.shard_name.should be_nil
frame.in_app?.should be_false
end
end
context "with in_app? = false" do
it "parses absolute path outside of src/ dir" do
with_foo_frame(path: "#{__DIR__}/foo.cr") do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq("#{__DIR__}/foo.cr")
frame.absolute_path.should eq(frame.path)
frame.relative_path.should eq("spec/backtracer/backtrace/frame/foo.cr")
frame.under_src_path?.should be_true
frame.shard_name.should be_nil
frame.in_app?.should be_false
end
end
it "parses relative path outside of src/ dir" do
with_configuration do |configuration|
path = "some/relative/path/to/foo.cr"
with_foo_frame(path: path) do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq(path)
frame.absolute_path.should eq(
File.join(configuration.src_path.not_nil!, path)
)
frame.relative_path.should eq(frame.path)
frame.under_src_path?.should be_false
frame.shard_name.should be_nil
frame.in_app?.should be_false
end
end
end
end
context "with in_app? = true" do
it "parses absolute path inside of src/ dir" do
src_path = File.expand_path("../../../../src", __DIR__)
path = "#{src_path}/foo.cr"
with_foo_frame(path: path) do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq(path)
frame.absolute_path.should eq(frame.path)
frame.relative_path.should eq("src/foo.cr")
frame.under_src_path?.should be_true
frame.shard_name.should be_nil
frame.in_app?.should be_true
end
end
it "parses relative path inside of src/ dir" do
with_configuration do |configuration|
path = "src/foo.cr"
with_foo_frame(path: path) do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq(path)
frame.absolute_path.should eq(
File.join(configuration.src_path.not_nil!, path)
)
frame.relative_path.should eq(path)
frame.under_src_path?.should be_false
frame.shard_name.should be_nil
frame.in_app?.should be_true
end
end
end
end
context "with shard path" do
it "parses absolute path inside of lib/ dir" do
lib_path = File.expand_path("../../../../lib/bar", __DIR__)
path = "#{lib_path}/src/bar.cr"
with_foo_frame(path: path) do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq(path)
frame.absolute_path.should eq(frame.path)
frame.relative_path.should eq("lib/bar/src/bar.cr")
frame.under_src_path?.should be_true
frame.shard_name.should eq "bar"
frame.in_app?.should be_false
end
end
it "parses relative path inside of lib/ dir" do
with_configuration do |configuration|
path = "lib/bar/src/bar.cr"
with_foo_frame(path: path) do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq(path)
frame.absolute_path.should eq(
File.join(configuration.src_path.not_nil!, path)
)
frame.relative_path.should eq(path)
frame.under_src_path?.should be_false
frame.shard_name.should eq "bar"
frame.in_app?.should be_false
end
end
end
it "uses only folders for shard names" do
with_foo_frame(path: "lib/bar.cr") do |frame|
frame.shard_name.should be_nil
end
end
end
end
end

View file

@ -1,6 +1,207 @@
require "../../spec_helper" require "../../spec_helper"
private def parse_frame(line)
Backtracer::Backtrace::Frame::Parser.parse(line)
end
private def with_frame(method, path = nil, lineno = nil, column = nil)
line = String.build do |io|
if path
io << path
io << ':' << lineno if lineno
io << ':' << column if column
io << " in '" << method << '\''
else
io << method
end
end
yield parse_frame(line)
end
private def with_foo_frame(
method = "foo_bar?",
path = "#{__DIR__}/foo.cr",
lineno = 1,
column = 7
)
with_frame(method, path, lineno, column) do |frame|
yield frame
end
end
describe Backtracer::Backtrace::Frame do describe Backtracer::Backtrace::Frame do
describe ".parse" do
it "fails to parse an empty string" do
expect_raises(ArgumentError) { parse_frame("") }
end
context "when --no-debug flag is set" do
it "parses frame with any value as method" do
backtrace_line = "__crystal_main"
with_frame(backtrace_line) do |frame|
frame.lineno.should be_nil
frame.column.should be_nil
frame.method.should eq(backtrace_line)
frame.path.should be_nil
frame.relative_path.should be_nil
frame.under_src_path?.should be_false
frame.shard_name.should be_nil
frame.in_app?.should be_false
end
end
end
context "with ~proc signature" do
it "parses absolute path outside of src/ dir" do
backtrace_line = "~proc2Proc(Fiber, (IO::FileDescriptor | Nil))@/usr/local/Cellar/crystal/0.27.2/src/fiber.cr:72"
with_frame(backtrace_line) do |frame|
frame.lineno.should eq(72)
frame.column.should be_nil
frame.method.should eq("~proc2Proc(Fiber, (IO::FileDescriptor | Nil))")
frame.path.should eq("/usr/local/Cellar/crystal/0.27.2/src/fiber.cr")
frame.relative_path.should be_nil
frame.under_src_path?.should be_false
frame.shard_name.should be_nil
frame.in_app?.should be_false
end
end
it "parses relative path inside of lib/ dir" do
backtrace_line = "~procProc(HTTP::Server::Context, String)@lib/kemal/src/kemal/route.cr:11"
with_frame(backtrace_line) do |frame|
frame.lineno.should eq(11)
frame.column.should be_nil
frame.method.should eq("~procProc(HTTP::Server::Context, String)")
frame.path.should eq("lib/kemal/src/kemal/route.cr")
frame.relative_path.should eq("lib/kemal/src/kemal/route.cr")
frame.under_src_path?.should be_false
frame.shard_name.should eq("kemal")
frame.in_app?.should be_false
end
end
end
it "parses absolute path outside of configuration.src_path" do
path = "/some/absolute/path/to/foo.cr"
with_foo_frame(path: path) do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq(path)
frame.relative_path.should be_nil
frame.under_src_path?.should be_false
frame.shard_name.should be_nil
frame.in_app?.should be_false
end
end
context "with in_app? = false" do
it "parses absolute path outside of src/ dir" do
with_foo_frame do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq("#{__DIR__}/foo.cr")
frame.relative_path.should eq("spec/backtracer/backtrace/foo.cr")
frame.under_src_path?.should be_true
frame.shard_name.should be_nil
frame.in_app?.should be_false
end
end
it "parses relative path outside of src/ dir" do
path = "some/relative/path/to/foo.cr"
with_foo_frame(path: path) do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq(path)
frame.relative_path.should eq(path)
frame.under_src_path?.should be_false
frame.shard_name.should be_nil
frame.in_app?.should be_false
end
end
end
context "with in_app? = true" do
it "parses absolute path inside of src/ dir" do
src_path = File.expand_path("../../../src", __DIR__)
path = "#{src_path}/foo.cr"
with_foo_frame(path: path) do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq(path)
frame.relative_path.should eq("src/foo.cr")
frame.under_src_path?.should be_true
frame.shard_name.should be_nil
frame.in_app?.should be_true
end
end
it "parses relative path inside of src/ dir" do
path = "src/foo.cr"
with_foo_frame(path: path) do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq(path)
frame.relative_path.should eq(path)
frame.under_src_path?.should be_false
frame.shard_name.should be_nil
frame.in_app?.should be_true
end
end
end
context "with shard path" do
it "parses absolute path inside of lib/ dir" do
lib_path = File.expand_path("../../../lib/bar", __DIR__)
path = "#{lib_path}/src/bar.cr"
with_foo_frame(path: path) do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq(path)
frame.relative_path.should eq("lib/bar/src/bar.cr")
frame.under_src_path?.should be_true
frame.shard_name.should eq "bar"
frame.in_app?.should be_false
end
end
it "parses relative path inside of lib/ dir" do
path = "lib/bar/src/bar.cr"
with_foo_frame(path: path) do |frame|
frame.lineno.should eq(1)
frame.column.should eq(7)
frame.method.should eq("foo_bar?")
frame.path.should eq(path)
frame.relative_path.should eq(path)
frame.under_src_path?.should be_false
frame.shard_name.should eq "bar"
frame.in_app?.should be_false
end
end
it "uses only folders for shard names" do
with_foo_frame(path: "lib/bar.cr") do |frame|
frame.shard_name.should be_nil
end
end
end
end
it "#inspect" do it "#inspect" do
with_foo_frame do |frame| with_foo_frame do |frame|
frame.inspect.should match(/Backtrace::Frame(.*)$/) frame.inspect.should match(/Backtrace::Frame(.*)$/)
@ -8,7 +209,7 @@ describe Backtracer::Backtrace::Frame do
end end
it "#to_s" do it "#to_s" do
with_foo_frame(path: "#{__DIR__}/foo.cr") do |frame| with_foo_frame do |frame|
frame.to_s.should eq "`foo_bar?` at #{__DIR__}/foo.cr:1:7" frame.to_s.should eq "`foo_bar?` at #{__DIR__}/foo.cr:1:7"
end end
end end
@ -18,44 +219,9 @@ describe Backtracer::Backtrace::Frame do
with_foo_frame do |frame2| with_foo_frame do |frame2|
frame.should eq(frame2) frame.should eq(frame2)
end end
with_foo_frame(method: "other_method") do |frame3| with_foo_frame(method: "other_method") do |frame2|
frame.should_not eq(frame3) frame.should_not eq(frame2)
end end
end end
end end
{% unless flag?(:release) || !flag?(:debug) %}
describe "#context" do
it "returns proper lines" do
with_configuration do |configuration|
with_backtrace(caller) do |backtrace|
backtrace.frames.first.tap do |first_frame|
context_lines = configuration.context_lines.should_not be_nil
context = first_frame.context.should_not be_nil
lines = File.read_lines(__FILE__)
lineidx = context.lineno - 1
context.pre
.should eq(lines[Math.max(0, lineidx - context_lines), context_lines]?)
context.line
.should eq(lines[lineidx]?)
context.post
.should eq(lines[Math.min(lines.size, lineidx + 1), context_lines]?)
end
end
end
end
it "returns given amount of lines" do
with_backtrace(caller) do |backtrace|
backtrace.frames.first.tap do |first_frame|
context = first_frame.context(3).should_not be_nil
context.pre.size.should eq(3)
context.post.size.should eq(3)
end
end
end
end
{% end %}
end end

View file

@ -1,21 +0,0 @@
require "../../spec_helper"
describe Backtracer::Backtrace::Parser do
describe ".parse" do
it "handles `caller` as an input" do
with_backtrace(caller) do |backtrace|
backtrace.frames.should_not be_empty
end
end
it "handles `Exception#backtrace` as an input" do
begin
raise "Oh, no!"
rescue ex
with_backtrace(ex.backtrace) do |backtrace|
backtrace.frames.should_not be_empty
end
end
end
end
end

View file

@ -1,31 +1,24 @@
require "../spec_helper" require "../spec_helper"
describe Backtracer::Backtrace do describe Backtracer::Backtrace do
backtrace = Backtracer.parse(caller)
it "#frames" do it "#frames" do
with_backtrace(caller) do |backtrace| backtrace.frames.should be_a(Array(Backtracer::Backtrace::Frame))
backtrace.frames.should be_a(Array(Backtracer::Backtrace::Frame))
backtrace.frames.should_not be_empty
end
end end
it "#inspect" do it "#inspect" do
with_backtrace(caller) do |backtrace| backtrace.inspect.should match(/#<Backtrace: .*>$/)
backtrace.inspect.should match(/#<Backtrace: .+>$/)
end
end end
{% unless flag?(:release) || !flag?(:debug) %} {% unless flag?(:release) || !flag?(:debug) %}
it "#to_s" do it "#to_s" do
with_backtrace(caller) do |backtrace| backtrace.to_s.should match(/backtrace_spec.cr:4/)
backtrace.to_s.should match(/backtrace_spec.cr/)
end
end end
{% end %} {% end %}
it "#==" do it "#==" do
with_backtrace(caller) do |backtrace| backtrace2 = Backtracer::Backtrace.new(backtrace.frames)
backtrace2 = Backtracer::Backtrace.new(backtrace.frames) backtrace2.should eq(backtrace)
backtrace2.should eq(backtrace)
end
end end
end end

View file

@ -1,5 +1,9 @@
require "../spec_helper" require "../spec_helper"
private def with_configuration
yield Backtracer::Configuration.new
end
describe Backtracer::Configuration do describe Backtracer::Configuration do
it "should set #src_path to current dir from default" do it "should set #src_path to current dir from default" do
with_configuration do |configuration| with_configuration do |configuration|

View file

@ -1,37 +1,2 @@
require "spec" require "spec"
require "../src/backtracer" require "../src/backtracer"
def with_configuration(shared = true, &)
yield shared ? Backtracer.configuration : Backtracer::Configuration.new
end
def with_backtrace(backtrace, **options, &)
yield Backtracer::Backtrace::Parser.parse(backtrace, **options)
end
def with_frame(method, path = nil, lineno = nil, column = nil, **options, &)
line = String.build do |io|
if path
io << path
io << ':' << lineno if lineno
io << ':' << column if column
io << " in '" << method << '\''
else
io << method
end
end
yield Backtracer::Backtrace::Frame::Parser.parse(line, **options)
end
def with_foo_frame(
method = "foo_bar?",
path = "#{__DIR__}/foo.cr",
lineno = 1,
column = 7,
**options,
&
)
with_frame(method, path, lineno, column, **options) do |frame|
yield frame
end
end

View file

@ -1,10 +1,6 @@
module Backtracer module Backtracer
class_getter(configuration) { Configuration.new } class_getter(configuration) { Configuration.new }
def self.configure(&) : Nil
yield configuration
end
def self.parse(backtrace : Array(String) | String, **options) : Backtrace def self.parse(backtrace : Array(String) | String, **options) : Backtrace
Backtrace::Parser.parse(backtrace, **options) Backtrace::Parser.parse(backtrace, **options)
end end

View file

@ -1,8 +1,6 @@
module Backtracer module Backtracer
# An object representation of a stack frame. # An object representation of a stack frame.
struct Backtrace::Frame struct Backtrace::Frame
@context_cache = {} of Int32 => Context
# The method of this frame (such as `User.find`). # The method of this frame (such as `User.find`).
getter method : String getter method : String
@ -23,7 +21,7 @@ module Backtracer
def_equals_and_hash @method, @path, @lineno, @column def_equals_and_hash @method, @path, @lineno, @column
# Reconstructs the frame in a readable fashion. # Reconstructs the frame in a readable fashion
def to_s(io : IO) : Nil def to_s(io : IO) : Nil
io << '`' << @method << '`' io << '`' << @method << '`'
if @path if @path
@ -39,24 +37,11 @@ module Backtracer
io << ')' io << ')'
end end
# Returns `true` if `path` of this frame is within
# the `configuration.src_path`, `false` otherwise.
#
# See `Configuration#src_path`
def under_src_path? : Bool def under_src_path? : Bool
return false unless src_path = configuration.src_path return false unless src_path = configuration.src_path
!!path.try(&.starts_with?(src_path)) !!path.try(&.starts_with?(src_path))
end end
# Returns:
#
# - `path` as is, unless it's absolute - i.e. starts with `/`
# - `path` relative to `configuration.src_path` when `under_src_path?` is `true`
# - `nil` otherwise
#
# NOTE: returned path is not required to be `under_src_path?` - see point no. 1
#
# See `Configuration#src_path`
def relative_path : String? def relative_path : String?
return unless path = @path return unless path = @path
return path unless path.starts_with?('/') return path unless path.starts_with?('/')
@ -66,13 +51,6 @@ module Backtracer
end end
end end
# Returns:
#
# - `path` as is, if it's absolute - i.e. starts with `/`
# - `path` appended to `configuration.src_path`
# - `nil` otherwise
#
# See `Configuration#src_path`
def absolute_path : String? def absolute_path : String?
return unless path = @path return unless path = @path
return path if path.starts_with?('/') return path if path.starts_with?('/')
@ -81,67 +59,51 @@ module Backtracer
end end
end end
# Returns name of the shard from which this frame originated.
#
# See `Configuration#modules_path_pattern`
def shard_name : String? def shard_name : String?
relative_path relative_path
.try(&.match(configuration.modules_path_pattern)) .try(&.match(configuration.modules_path_pattern))
.try(&.["name"]) .try(&.["name"])
end end
# Returns `true` if this frame originated from the app source code,
# `false` otherwise.
#
# See `Configuration#app_dirs_pattern`
def in_app? : Bool def in_app? : Bool
!!(relative_path.try(&.matches?(configuration.app_dirs_pattern))) !!(path.try(&.matches?(configuration.in_app_pattern)))
end end
# Returns `Context` record consisting of 3 elements - an array of context lines def context(context_lines : Int32? = nil) : {Array(String), String, Array(String)}?
# before the `lineno`, line at `lineno`, and an array of context lines
# after the `lineno`. In case of failure it returns `nil`.
#
# Amount of returned context lines is taken from the *context_lines*
# argument if given, or `configuration.context_lines` otherwise.
#
# NOTE: amount of returned context lines might be lower than given
# in cases where `lineno` is near the start or the end of the file.
#
# See `Configuration#context_lines`
def context(context_lines : Int32? = nil) : Context?
context_lines ||= configuration.context_lines context_lines ||= configuration.context_lines
return unless context_lines && (context_lines > 0) return unless context_lines && (context_lines > 0)
cached = @context_cache[context_lines]?
return cached if cached
return unless (lineno = @lineno) && (lineno > 0) return unless (lineno = @lineno) && (lineno > 0)
return unless (path = @path) && File.readable?(path) return unless (path = @path) && File.readable?(path)
context_line = nil lines = File.read_lines(path)
pre_context, post_context = %w[], %w[] lineidx = lineno - 1
i = 0 if context_line = lines[lineidx]?
File.each_line(path) do |line| pre_context = lines[Math.max(0, lineidx - context_lines), context_lines]
case i += 1 post_context = lines[Math.min(lines.size, lineidx + 1), context_lines]
when lineno - context_lines...lineno {pre_context, context_line, post_context}
pre_context << line
when lineno
context_line = line
when lineno + 1..lineno + context_lines
post_context << line
end
end end
end
if context_line def context_hash(context_lines : Int32? = nil) : Hash(Int32, String)?
@context_cache[context_lines] = return unless context = self.context(context_lines)
Context.new( return unless lineno = @lineno
lineno: lineno,
pre: pre_context, pre_context, context_line, post_context = context
line: context_line,
post: post_context, ({} of Int32 => String).tap do |hash|
) pre_context.each_with_index do |code, index|
line = (lineno - pre_context.size) + index
hash[line] = code
end
hash[lineno] = context_line
post_context.each_with_index do |code, index|
line = lineno + (index + 1)
hash[line] = code
end
end end
end end
end end

View file

@ -1,46 +0,0 @@
module Backtracer
struct Backtrace::Frame::Context
# The line number this `Context` refers to.
getter lineno : Int32
# An array of lines before `lineno`.
getter pre : Array(String)
# The line at `lineno`.
getter line : String
# An array of lines after `lineno`.
getter post : Array(String)
def initialize(@lineno, @pre, @line, @post)
end
# Returns an array composed of context lines from `pre`,
# `line` and `post`.
def to_a : Array(String)
([] of String).tap do |ary|
ary.concat(pre)
ary << line
ary.concat(post)
end
end
# Returns hash with context lines, where line numbers are
# the keys and the lines itself are the values.
def to_h : Hash(Int32, String)
({} of Int32 => String).tap do |hash|
base_index = lineno - pre.size
pre.each_with_index do |code, index|
hash[base_index + index] = code
end
hash[lineno] = line
base_index = lineno + 1
post.each_with_index do |code, index|
hash[base_index + index] = code
end
end
end
end
end

View file

@ -2,13 +2,10 @@ module Backtracer
module Backtrace::Frame::Parser module Backtrace::Frame::Parser
extend self extend self
# Parses a single line of a given backtrace, where *line* is # Parses a single line of a given backtrace, where *unparsed_line* is
# the raw line from `caller` or some backtrace. # the raw line from `caller` or some backtrace.
# #
# Accepts options: # Returns the parsed backtrace frame on success or `nil` otherwise.
# - `configuration`: `Configuration` object - uses `Backtracer.configuration` if `nil`
#
# Returns parsed `Backtrace::Frame` on success or `nil` otherwise.
def parse?(line : String, **options) : Backtrace::Frame? def parse?(line : String, **options) : Backtrace::Frame?
return unless Configuration::LINE_PATTERNS.any? &.match(line) return unless Configuration::LINE_PATTERNS.any? &.match(line)
@ -23,7 +20,6 @@ module Backtracer
configuration: options[:configuration]? configuration: options[:configuration]?
end end
# Same as `parse?` but raises `ArgumentError` on error.
def parse(line : String, **options) : Backtrace::Frame def parse(line : String, **options) : Backtrace::Frame
parse?(line, **options) || parse?(line, **options) ||
raise ArgumentError.new("Error parsing line: #{line.inspect}") raise ArgumentError.new("Error parsing line: #{line.inspect}")

View file

@ -2,14 +2,6 @@ module Backtracer
module Backtrace::Parser module Backtrace::Parser
extend self extend self
# Parses *backtrace* (possibly obtained as a return value
# from `caller` or `Exception#backtrace` methods).
#
# Accepts options:
# - `configuration`: `Configuration` object - uses `Backtracer.configuration` if `nil`
# - `filters`: additional line filters - see `Configuration#line_filters`
#
# Returns parsed `Backtrace` object or raises `ArgumentError` otherwise.
def parse(backtrace : Array(String), **options) : Backtrace def parse(backtrace : Array(String), **options) : Backtrace
configuration = options[:configuration]? || Backtracer.configuration configuration = options[:configuration]? || Backtracer.configuration

View file

@ -53,32 +53,24 @@ module Backtracer
/^(?<method>.+?)$/, /^(?<method>.+?)$/,
} }
# Path considered as "root" of your project. # Used in `#in_app_pattern`.
#
# See `Frame#under_src_path?`
property src_path : String? = {{ Process::INITIAL_PWD }} property src_path : String? = {{ Process::INITIAL_PWD }}
# Directories to be recognized as part of your app. e.g. if you # Directories to be recognized as part of your app. e.g. if you
# have an `engines` dir at the root of your project, you may want # have an `engines` dir at the root of your project, you may want
# to set this to something like `/^(src|engines)\//` # to set this to something like `/(src|engines)/`
# property app_dirs_pattern = /src/
# See `Frame#in_app?`
property app_dirs_pattern = /^src\// # `Regex` pattern matched against `Backtrace::Frame#file`.
property in_app_pattern : Regex { /^(#{src_path}\/)?(#{app_dirs_pattern})/ }
# Path pattern matching directories to be recognized as your app modules. # Path pattern matching directories to be recognized as your app modules.
# Defaults to standard Shards setup (`lib/shard-name/...`). # Defaults to standard Shards setup (`lib/shard-name/...`).
#
# See `Frame#shard_name`
property modules_path_pattern = /^lib\/(?<name>[^\/]+)\/(?:.+)/ property modules_path_pattern = /^lib\/(?<name>[^\/]+)\/(?:.+)/
# Number of lines of code context to return by default, or `nil` for none. # Number of lines of code context to capture, or `nil` for none.
#
# See `Frame#context`
property context_lines : Int32? = 5 property context_lines : Int32? = 5
# Array of procs used for filtering backtrace lines before parsing.
# Each filter is expected to return a string, which is then passed
# onto the next filter, or ignored althoghether if `nil` is returned.
getter(line_filters) { getter(line_filters) {
[ [
->(line : String) { line unless line.matches?(IGNORED_LINES_PATTERN) }, ->(line : String) { line unless line.matches?(IGNORED_LINES_PATTERN) },