diff --git a/.ameba.yml b/.ameba.yml deleted file mode 100644 index 399b108..0000000 --- a/.ameba.yml +++ /dev/null @@ -1,3 +0,0 @@ -Lint/NotNil: - Excluded: - - spec/backtracer/backtrace/frame/parser_spec.cr diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index ac9ac2f..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -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 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7521934 --- /dev/null +++ b/.travis.yml @@ -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 diff --git a/README.md b/README.md index 8d17ade..03ca6ae 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/shard.yml b/shard.yml index 96ff072..20cf883 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: backtracer -version: 1.2.2 +version: 1.0.0 authors: - Sijawusz Pur Rahnama @@ -7,7 +7,7 @@ authors: development_dependencies: ameba: github: crystal-ameba/ameba - version: ~> 1.5.0 + version: ~> 0.13.0 crystal: ">= 0.35.0" diff --git a/spec/backtracer/backtrace/frame/context_spec.cr b/spec/backtracer/backtrace/frame/context_spec.cr deleted file mode 100644 index b6fc3aa..0000000 --- a/spec/backtracer/backtrace/frame/context_spec.cr +++ /dev/null @@ -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 diff --git a/spec/backtracer/backtrace/frame/parser_spec.cr b/spec/backtracer/backtrace/frame/parser_spec.cr deleted file mode 100644 index 7df2f9e..0000000 --- a/spec/backtracer/backtrace/frame/parser_spec.cr +++ /dev/null @@ -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 diff --git a/spec/backtracer/backtrace/frame_spec.cr b/spec/backtracer/backtrace/frame_spec.cr index 53c3766..5f6032c 100644 --- a/spec/backtracer/backtrace/frame_spec.cr +++ b/spec/backtracer/backtrace/frame_spec.cr @@ -1,6 +1,207 @@ 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(.*)$/) @@ -8,7 +209,7 @@ describe Backtracer::Backtrace::Frame do end 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" end end @@ -18,44 +219,9 @@ describe Backtracer::Backtrace::Frame do with_foo_frame do |frame2| frame.should eq(frame2) end - with_foo_frame(method: "other_method") do |frame3| - frame.should_not eq(frame3) + with_foo_frame(method: "other_method") do |frame2| + frame.should_not eq(frame2) 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 diff --git a/spec/backtracer/backtrace/parser_spec.cr b/spec/backtracer/backtrace/parser_spec.cr deleted file mode 100644 index 7713983..0000000 --- a/spec/backtracer/backtrace/parser_spec.cr +++ /dev/null @@ -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 diff --git a/spec/backtracer/backtrace_spec.cr b/spec/backtracer/backtrace_spec.cr index c8d58f2..36bc340 100644 --- a/spec/backtracer/backtrace_spec.cr +++ b/spec/backtracer/backtrace_spec.cr @@ -1,31 +1,24 @@ 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 + backtrace.frames.should be_a(Array(Backtracer::Backtrace::Frame)) end it "#inspect" do - with_backtrace(caller) do |backtrace| - backtrace.inspect.should match(/#$/) - end + backtrace.inspect.should match(/#$/) end {% unless flag?(:release) || !flag?(:debug) %} it "#to_s" do - with_backtrace(caller) do |backtrace| - backtrace.to_s.should match(/backtrace_spec.cr/) - end + backtrace.to_s.should match(/backtrace_spec.cr:4/) end {% end %} it "#==" do - with_backtrace(caller) do |backtrace| - backtrace2 = Backtracer::Backtrace.new(backtrace.frames) - backtrace2.should eq(backtrace) - end + backtrace2 = Backtracer::Backtrace.new(backtrace.frames) + backtrace2.should eq(backtrace) end end diff --git a/spec/backtracer/configuration_spec.cr b/spec/backtracer/configuration_spec.cr index c192860..76284c6 100644 --- a/spec/backtracer/configuration_spec.cr +++ b/spec/backtracer/configuration_spec.cr @@ -1,5 +1,9 @@ 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| diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index ed152bb..e0c7f38 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,37 +1,2 @@ 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 diff --git a/src/backtracer.cr b/src/backtracer.cr index c507a2d..e11a616 100644 --- a/src/backtracer.cr +++ b/src/backtracer.cr @@ -1,10 +1,6 @@ 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 diff --git a/src/backtracer/backtrace/frame.cr b/src/backtracer/backtrace/frame.cr index 5826422..c7fd0fd 100644 --- a/src/backtracer/backtrace/frame.cr +++ b/src/backtracer/backtrace/frame.cr @@ -1,8 +1,6 @@ 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 @@ -23,7 +21,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 @@ -39,24 +37,11 @@ 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?('/') @@ -66,13 +51,6 @@ 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?('/') @@ -81,67 +59,51 @@ 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 - !!(relative_path.try(&.matches?(configuration.app_dirs_pattern))) + !!(path.try(&.matches?(configuration.in_app_pattern))) end - # 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? + def context(context_lines : Int32? = nil) : {Array(String), String, Array(String)}? 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) - context_line = nil - pre_context, post_context = %w[], %w[] + lines = File.read_lines(path) + lineidx = lineno - 1 - 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 + 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} end + end - if context_line - @context_cache[context_lines] = - Context.new( - lineno: lineno, - pre: pre_context, - line: context_line, - post: post_context, - ) + 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 end end end diff --git a/src/backtracer/backtrace/frame/context.cr b/src/backtracer/backtrace/frame/context.cr deleted file mode 100644 index d5d22aa..0000000 --- a/src/backtracer/backtrace/frame/context.cr +++ /dev/null @@ -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 diff --git a/src/backtracer/backtrace/frame/parser.cr b/src/backtracer/backtrace/frame/parser.cr index 37f77ff..f771639 100644 --- a/src/backtracer/backtrace/frame/parser.cr +++ b/src/backtracer/backtrace/frame/parser.cr @@ -2,13 +2,10 @@ module Backtracer module Backtrace::Frame::Parser 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. # - # Accepts options: - # - `configuration`: `Configuration` object - uses `Backtracer.configuration` if `nil` - # - # Returns parsed `Backtrace::Frame` on success or `nil` otherwise. + # Returns the parsed backtrace frame on success or `nil` otherwise. def parse?(line : String, **options) : Backtrace::Frame? return unless Configuration::LINE_PATTERNS.any? &.match(line) @@ -23,7 +20,6 @@ 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}") diff --git a/src/backtracer/backtrace/parser.cr b/src/backtracer/backtrace/parser.cr index 30d6744..ade656e 100644 --- a/src/backtracer/backtrace/parser.cr +++ b/src/backtracer/backtrace/parser.cr @@ -2,14 +2,6 @@ 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 diff --git a/src/backtracer/configuration.cr b/src/backtracer/configuration.cr index 1b91465..c944188 100644 --- a/src/backtracer/configuration.cr +++ b/src/backtracer/configuration.cr @@ -53,32 +53,24 @@ module Backtracer /^(?.+?)$/, } - # Path considered as "root" of your project. - # - # See `Frame#under_src_path?` + # Used in `#in_app_pattern`. 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)\//` - # - # See `Frame#in_app?` - property app_dirs_pattern = /^src\// + # 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})/ } # 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\/(?[^\/]+)\/(?:.+)/ - # Number of lines of code context to return by default, or `nil` for none. - # - # See `Frame#context` + # Number of lines of code context to capture, or `nil` for none. 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) },