Compare commits

..

25 commits

Author SHA1 Message Date
Sijawusz Pur Rahnama
d461ca301b
Merge pull request #2 from Sija/bump-ameba-version-to-1.5.0
Bump ameba version to ~> 1.5.0
2023-10-12 17:39:02 +02:00
Sijawusz Pur Rahnama
abdac766c5 Bump ameba version to ~> 1.5.0 2023-10-12 17:33:12 +02:00
Sijawusz Pur Rahnama
b4d0390bcd
Merge pull request #1 from Sija/bump-ameba
Bump ameba to ~> 1.5.0
2023-10-11 04:14:50 +02:00
Sijawusz Pur Rahnama
7e92356a50 Bump ameba to ~> 1.5.0 2023-10-11 04:10:23 +02:00
Sijawusz Pur Rahnama
a7eed4b230 Bump ameba dependency and fix found issues 2023-03-06 05:58:15 +01:00
Sijawusz Pur Rahnama
8410b11478
Bump ameba dependency 2022-10-26 02:19:07 +02:00
Sijawusz Pur Rahnama
07d6dc4381 Bump to v1.2.2 2022-08-13 23:35:36 +02:00
Sijawusz Pur Rahnama
30287e3025 Bump ameba to ~> 1.0.0 2022-08-13 23:35:36 +02:00
Sijawusz Pur Rahnama
14f5d77cdd Migrate from Travis CI to GitHub Actions 2021-03-10 15:45:11 +01:00
Sijawusz Pur Rahnama
5649a04adf Bump ameba dependency 2021-03-01 14:47:39 +01:00
Sijawusz Pur Rahnama
f18b0546f8 Bump to v1.2.1 2021-01-10 15:09:49 +01:00
Sijawusz Pur Rahnama
5e1ea0bd78 Refactor Frame#context to avoid reading whole file at once 2021-01-10 15:08:41 +01:00
Sijawusz Pur Rahnama
1ff793f00b Fix edge case where lineno <= context_lines 2021-01-10 14:29:29 +01:00
Sijawusz Pur Rahnama
63ca71ba56 Bump to v1.2.0 2021-01-04 03:26:40 +01:00
Sijawusz Pur Rahnama
521bf7ff28 Add Context#to_a 2021-01-04 03:24:44 +01:00
Sijawusz Pur Rahnama
0663fbfa01 Add specs for Frame#context 2021-01-04 03:07:14 +01:00
Sijawusz Pur Rahnama
d88a17f3e6 Cache Frame#context results 2021-01-04 03:07:14 +01:00
Sijawusz Pur Rahnama
0300476813 Refactor context tuple into a dedicated Context struct 2021-01-04 03:07:14 +01:00
Sijawusz Pur Rahnama
e3ee3a494d Bump to v1.1.0 2021-01-02 19:22:18 +01:00
Sijawusz Pur Rahnama
7d7192ec09 Fix doc comments 2021-01-02 19:18:50 +01:00
Sijawusz Pur Rahnama
45b9cefe73 Remove unreliable specs 2021-01-01 23:53:55 +01:00
Sijawusz Pur Rahnama
57f5746d6e Extend specs coverage 2021-01-01 16:36:58 +01:00
Sijawusz Pur Rahnama
c41fcb5d28 Remove redundant Configuration#in_app_pattern 2020-12-30 22:13:51 +01:00
Sijawusz Pur Rahnama
8ad814c7c1 Add documentation 2020-12-29 18:45:29 +01:00
Sijawusz Pur Rahnama
742b723237 Add Backtracer.configure 2020-12-28 22:17:23 +01:00
18 changed files with 568 additions and 276 deletions

3
.ameba.yml Normal file
View file

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

46
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,46 @@
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

View file

@ -1,20 +0,0 @@
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 [![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)
# 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)
Crystal shard aiming to assist with parsing backtraces into a structured form.

View file

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

View file

@ -0,0 +1,60 @@
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

@ -0,0 +1,202 @@
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,207 +1,6 @@
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 ".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
with_foo_frame do |frame|
frame.inspect.should match(/Backtrace::Frame(.*)$/)
@ -209,7 +8,7 @@ describe Backtracer::Backtrace::Frame do
end
it "#to_s" do
with_foo_frame do |frame|
with_foo_frame(path: "#{__DIR__}/foo.cr") do |frame|
frame.to_s.should eq "`foo_bar?` at #{__DIR__}/foo.cr:1:7"
end
end
@ -219,9 +18,44 @@ describe Backtracer::Backtrace::Frame do
with_foo_frame do |frame2|
frame.should eq(frame2)
end
with_foo_frame(method: "other_method") do |frame2|
frame.should_not eq(frame2)
with_foo_frame(method: "other_method") do |frame3|
frame.should_not eq(frame3)
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

View file

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

View file

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

View file

@ -1,2 +1,37 @@
require "spec"
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,6 +1,10 @@
module Backtracer
class_getter(configuration) { Configuration.new }
def self.configure(&) : Nil
yield configuration
end
def self.parse(backtrace : Array(String) | String, **options) : Backtrace
Backtrace::Parser.parse(backtrace, **options)
end

View file

@ -1,6 +1,8 @@
module Backtracer
# An object representation of a stack frame.
struct Backtrace::Frame
@context_cache = {} of Int32 => Context
# The method of this frame (such as `User.find`).
getter method : String
@ -21,7 +23,7 @@ module Backtracer
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
io << '`' << @method << '`'
if @path
@ -37,11 +39,24 @@ module Backtracer
io << ')'
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
return false unless src_path = configuration.src_path
!!path.try(&.starts_with?(src_path))
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?
return unless path = @path
return path unless path.starts_with?('/')
@ -51,6 +66,13 @@ module Backtracer
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?
return unless path = @path
return path if path.starts_with?('/')
@ -59,51 +81,67 @@ module Backtracer
end
end
# Returns name of the shard from which this frame originated.
#
# See `Configuration#modules_path_pattern`
def shard_name : String?
relative_path
.try(&.match(configuration.modules_path_pattern))
.try(&.["name"])
end
# Returns `true` if this frame originated from the app source code,
# `false` otherwise.
#
# See `Configuration#app_dirs_pattern`
def in_app? : Bool
!!(path.try(&.matches?(configuration.in_app_pattern)))
!!(relative_path.try(&.matches?(configuration.app_dirs_pattern)))
end
def context(context_lines : Int32? = nil) : {Array(String), String, Array(String)}?
# Returns `Context` record consisting of 3 elements - an array of context lines
# 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
return unless context_lines && (context_lines > 0)
cached = @context_cache[context_lines]?
return cached if cached
return unless (lineno = @lineno) && (lineno > 0)
return unless (path = @path) && File.readable?(path)
lines = File.read_lines(path)
lineidx = lineno - 1
context_line = nil
pre_context, post_context = %w[], %w[]
if context_line = lines[lineidx]?
pre_context = lines[Math.max(0, lineidx - context_lines), context_lines]
post_context = lines[Math.min(lines.size, lineidx + 1), context_lines]
{pre_context, context_line, post_context}
i = 0
File.each_line(path) do |line|
case i += 1
when lineno - context_lines...lineno
pre_context << line
when lineno
context_line = line
when lineno + 1..lineno + context_lines
post_context << line
end
end
def context_hash(context_lines : Int32? = nil) : Hash(Int32, String)?
return unless context = self.context(context_lines)
return unless lineno = @lineno
pre_context, context_line, post_context = 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
if context_line
@context_cache[context_lines] =
Context.new(
lineno: lineno,
pre: pre_context,
line: context_line,
post: post_context,
)
end
end
end

View file

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

View file

@ -2,6 +2,14 @@ module Backtracer
module Backtrace::Parser
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
configuration = options[:configuration]? || Backtracer.configuration

View file

@ -53,24 +53,32 @@ module Backtracer
/^(?<method>.+?)$/,
}
# Used in `#in_app_pattern`.
# Path considered as "root" of your project.
#
# See `Frame#under_src_path?`
property src_path : String? = {{ Process::INITIAL_PWD }}
# 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
# to set this to something like `/(src|engines)/`
property app_dirs_pattern = /src/
# `Regex` pattern matched against `Backtrace::Frame#file`.
property in_app_pattern : Regex { /^(#{src_path}\/)?(#{app_dirs_pattern})/ }
# to set this to something like `/^(src|engines)\//`
#
# See `Frame#in_app?`
property app_dirs_pattern = /^src\//
# Path pattern matching directories to be recognized as your app modules.
# Defaults to standard Shards setup (`lib/shard-name/...`).
#
# See `Frame#shard_name`
property modules_path_pattern = /^lib\/(?<name>[^\/]+)\/(?:.+)/
# Number of lines of code context to capture, or `nil` for none.
# Number of lines of code context to return by default, or `nil` for none.
#
# See `Frame#context`
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) {
[
->(line : String) { line unless line.matches?(IGNORED_LINES_PATTERN) },