Skip to content

Commit

Permalink
+
Browse files Browse the repository at this point in the history
  • Loading branch information
kyrylo committed Oct 10, 2017
1 parent 1eab0dd commit 1ee9384
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 64 deletions.
18 changes: 4 additions & 14 deletions lib/airbrake-ruby/backtrace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def stack_frame(config, match)
function: match[:function]
}

read_code_hunk(config.logger, frame) if config.code_hunks
populate_code(config, frame) if config.code_hunks
frame
end

Expand All @@ -194,19 +194,9 @@ def match_frame(regexp, stackframe)
Patterns::GENERIC.match(stackframe)
end

def read_code_hunk(logger, frame)
code_hunk = Airbrake::CodeHunk.new(frame[:file], frame[:line]).to_h
return unless code_hunk

unless code_hunk.key?(0)
frame[:code_hunk] = code_hunk
return
end

logger.error(
"#{LOG_LABEL} error while reading code hunk `#{file}:#{line}'. " \
"Reason: #{code_hunk[0]}"
)
def populate_code(config, frame)
code = Airbrake::CodeHunk.new(config).get(frame[:file], frame[:line])
frame[:code] = code if code
end
end
end
Expand Down
55 changes: 26 additions & 29 deletions lib/airbrake-ruby/code_hunk.rb
Original file line number Diff line number Diff line change
@@ -1,51 +1,48 @@
module Airbrake
##
# Represents a small hunk of code consisting of a base line and a couple lines
# around
# around it
# @api private
class CodeHunk
##
# @return [Integer] the maximum length of a line
MAX_LINE_LEN = 200

##
# @return [Integer] how many lines should be read around the base line
INTERVAL = 3
NLINES = 2

def initialize(file, line, interval = INTERVAL)
@file = file
@line = line

@start_line = [line - interval, 1].max
@end_line = line + interval

@code_hash = {}
def initialize(config)
@config = config
end

##
# @return [Hash{Integer=>String}, nil] code hunk around the base line. When
# an error occurrs, returns a zero key Hash
def to_h
return @code_hash unless @code_hash.empty?
return unless File.exist?(@file)
# @param [String] file The file to read
# @param [Integer] line The base line in the file
# @return [Hash{Integer=>String}, nil] lines of code around the base line
def get(file, line)
return unless File.exist?(file)

start_line = [line - NLINES, 1].max
end_line = line + NLINES
lines = {}

begin
fetch_code
File.foreach(file).with_index(1) do |l, i|
next if i < start_line
break if i > end_line

lines[i] = l[0...MAX_LINE_LEN].rstrip
end
rescue StandardError => ex
{ 0 => ex }
@config.logger.error(
"#{self.class.name}##{__method__}: can't read code hunk for " \
"#{file}:#{line}: #{ex}"
)
end

@code_hash
end

private

def fetch_code
File.foreach(@file).with_index(1) do |line, i|
next if i < @start_line
break if i > @end_line

@code_hash[i] = line[0...MAX_LINE_LEN].rstrip
end
return { 1 => '' } if lines.empty?
lines
end
end
end
4 changes: 1 addition & 3 deletions spec/backtrace_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -303,16 +303,14 @@
file: File.join(fixture_path('code.rb')),
line: 94,
function: 'to_json',
code_hunk: {
91 => ' def to_json',
code: {
92 => ' loop do',
93 => ' begin',
94 => ' json = @payload.to_json',
95 => ' rescue *JSON_EXCEPTIONS => ex',
# rubocop:disable Metrics/LineLength,Lint/InterpolationCheck
96 => ' @config.logger.debug("#{LOG_LABEL} `notice.to_json` failed: #{ex.class}: #{ex}")',
# rubocop:enable Metrics/LineLength,Lint/InterpolationCheck
97 => ' else'
}
}
]
Expand Down
49 changes: 31 additions & 18 deletions spec/code_hunk_spec.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
require 'spec_helper'

RSpec.describe Airbrake::CodeHunk do
let(:config) { Airbrake::Config.new }

describe "#to_h" do
context "when a file is empty" do
subject { described_class.new(fixture_path('empty_file.rb'), 1).to_h }
subject { described_class.new(config).get(fixture_path('empty_file.rb'), 1) }

it { is_expected.to eql({}) }
it { is_expected.to eq(1 => '') }
end

context "when a file doesn't exist" do
subject { described_class.new(fixture_path('banana.rb'), 1).to_h }
subject { described_class.new(config).get(fixture_path('banana.rb'), 1) }

it { is_expected.to be_nil }
end

context "when a file has less than INTERVAL lines before start line" do
subject { described_class.new(fixture_path('code.rb'), 1).to_h }
context "when a file has less than NLINES lines before start line" do
subject { described_class.new(config).get(fixture_path('code.rb'), 1) }

it do
is_expected.to(
Expand All @@ -25,29 +27,26 @@
# rubocop:disable Metrics/LineLength
3 => ' # Represents a chunk of information that is meant to be either sent to',
# rubocop:enable Metrics/LineLength
4 => ' # Airbrake or ignored completely.'
)
)
end
end

context "when a file has less than INTERVAL lines after end line" do
subject { described_class.new(fixture_path('code.rb'), 221).to_h }
context "when a file has less than NLINES lines after end line" do
subject { described_class.new(config).get(fixture_path('code.rb'), 222) }

it do
is_expected.to(
eq(
218 => ' end',
219 => ' end',
220 => ' end',
221 => 'end'
)
)
end
end

context "when a file has less lines than a code hunk requests" do
subject { described_class.new(fixture_path('short_file.rb'), 2).to_h }
context "when a file has less than NLINES lines before and after" do
subject { described_class.new(config).get(fixture_path('short_file.rb'), 2) }

it do
is_expected.to(
Expand All @@ -60,30 +59,44 @@
end
end

context "when a line location is in the middle of a file" do
subject { described_class.new(fixture_path('code.rb'), 100).to_h }
context "when a file has enough lines before and after" do
subject { described_class.new(config).get(fixture_path('code.rb'), 100) }

it do
is_expected.to(
eq(
97 => ' else',
98 => ' return json if json && json.bytesize <= MAX_NOTICE_SIZE',
99 => ' end',
100 => '',
101 => ' break if truncate == 0',
102 => ' end',
103 => ' end'
102 => ' end'
)
)
end
end

context "when a line exceeds the length limit" do
subject { described_class.new(fixture_path('long_line.txt'), 1).to_h }
subject { described_class.new(config).get(fixture_path('long_line.txt'), 1) }

it "strips the line" do
expect(subject[1]).to eq('l' + 'o' * 196 + 'ng')
end
end

context "when an error occurrs while fetching code" do
before do
expect(File).to receive(:foreach).and_raise(Errno::EACCES)
end

it "logs error and returns nil" do
out = StringIO.new
config = Airbrake::Config.new
config.logger = Logger.new(out)
expect(described_class.new(config).get(fixture_path('code.rb'), 1)).to(
eq(1 => '')
)
expect(out.string).to match(/can't read code hunk.+Permission denied/)
end
end
end
end

0 comments on commit 1ee9384

Please sign in to comment.