Skip to content

Commit

Permalink
Merge pull request #8 from telebugs/send-code
Browse files Browse the repository at this point in the history
Send code with errors
  • Loading branch information
kyrylo committed Jun 8, 2024
2 parents 38d0f0d + 525e1c8 commit 68a10aa
Show file tree
Hide file tree
Showing 13 changed files with 327 additions and 2 deletions.
2 changes: 2 additions & 0 deletions lib/telebugs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
require_relative "telebugs/notice"
require_relative "telebugs/error_message"
require_relative "telebugs/backtrace"
require_relative "telebugs/file_cache"
require_relative "telebugs/code_hunk"

module Telebugs
# The general error that this library uses when it wants to raise.
Expand Down
44 changes: 44 additions & 0 deletions lib/telebugs/code_hunk.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module Telebugs
# Represents a small hunk of code consisting of a base line and a couple lines
# around it
class CodeHunk
MAX_LINE_LEN = 200

# How many lines should be read around the base line.
AROUND_LINES = 2

def self.get(file, line)
start_line = [line - AROUND_LINES, 1].max

lines = get_lines(file, start_line, line + AROUND_LINES)
return {start_line: 0, lines: []} if lines.empty?

{
start_line: start_line,
lines: lines
}
end

private_class_method def self.get_lines(file, start_line, end_line)
lines = []
return lines unless (cached_file = get_from_cache(file))

cached_file.with_index(1) do |l, i|
next if i < start_line
break if i > end_line

lines << l[0...MAX_LINE_LEN].rstrip
end

lines
end

private_class_method def self.get_from_cache(file)
Telebugs::FileCache[file] ||= File.foreach(file)
rescue
nil
end
end
end
8 changes: 7 additions & 1 deletion lib/telebugs/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ module Telebugs
class Config
ERROR_API_URL = "https://api.telebugs.com/2024-03-28/errors"

attr_accessor :api_key
attr_accessor :api_key,
:root_directory

attr_reader :api_url

class << self
Expand All @@ -28,6 +30,10 @@ def api_url=(url)
def reset
self.api_key = nil
self.api_url = ERROR_API_URL
self.root_directory = File.realpath(
(defined?(Bundler) && Bundler.root) ||
Dir.pwd
)
end
end
end
41 changes: 41 additions & 0 deletions lib/telebugs/file_cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module Telebugs
module FileCache
MAX_SIZE = 50
MUTEX = Mutex.new

# Associates the value given by +value+ with the key given by +key+. Deletes
# entries that exceed +MAX_SIZE+.
def self.[]=(key, value)
MUTEX.synchronize do
data[key] = value
data.delete(data.keys.first) if data.size > MAX_SIZE
end
end

# Retrieve an object from the cache.
def self.[](key)
MUTEX.synchronize do
data[key]
end
end

# Checks whether the cache is empty. Needed only for the test suite.
def self.empty?
MUTEX.synchronize do
data.empty?
end
end

def self.reset
MUTEX.synchronize do
@data = {}
end
end

private_class_method def self.data
@data ||= {}
end
end
end
18 changes: 17 additions & 1 deletion lib/telebugs/notice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,27 @@ def errors_as_json(error)
{
type: e.class.name,
message: ErrorMessage.parse(e),
backtrace: Backtrace.parse(e)
backtrace: attach_code(Backtrace.parse(e))
}
end
end

def attach_code(b)
b.each do |frame|
next unless frame[:file]
next unless File.exist?(frame[:file])
next unless frame[:line]
next unless frame_belogns_to_root_directory?(frame)
next if %r{vendor/bundle}.match?(frame[:file])

frame[:code] = CodeHunk.get(frame[:file], frame[:line])
end
end

def frame_belogns_to_root_directory?(frame)
frame[:file].start_with?(Telebugs::Config.instance.root_directory)
end

def truncate
0
end
Expand Down
39 changes: 39 additions & 0 deletions test/fixtures/project_root/code.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

class Botley
RESPONSES = {
/hello/i => "Oh, hello there! Are you ready to have some fun?",
/how are you/i => "I'm just a bunch of code, but thanks for asking! How about you?",
/what is your name/i => "I'm Botley, your friendly (and sometimes cheeky) virtual assistant!",
/joke/i => "Why don't scientists trust atoms? Because they make up everything!",
/bye|goodbye|exit/i => "Goodbye! Don't miss me too much!",
/thank you|thanks/i => "You're welcome! I'm here all week.",
/default/ => "I'm not sure what you mean, but it sounds intriguing!"
}

def initialize
puts "Botley: Hello! I'm Botley, your virtual assistant. Type 'goodbye' to exit."
end

def start
loop do
print "You: "
user_input = gets.chomp
response = respond_to(user_input)
puts "Botley: #{response}"
break if user_input.match?(/bye|goodbye|exit/i)
end
end

private

def respond_to(input)
RESPONSES.each do |pattern, response|
return response if input.match?(pattern)
end
RESPONSES[:default]
end
end

# Start the conversation with Botley
Botley.new.start
Empty file.
1 change: 1 addition & 0 deletions test/fixtures/project_root/long_line.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong line
1 change: 1 addition & 0 deletions test/fixtures/project_root/one_line.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Boom.new.call
87 changes: 87 additions & 0 deletions test/test_code_hunk.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# frozen_string_literal: true

require "test_helper"

class TestCodeHunk < Minitest::Test
def test_get_when_file_is_empty
hunk = Telebugs::CodeHunk.get("test/fixtures/project_root/empty_file.rb", 1)

assert_equal({start_line: 0, lines: []}, hunk)
end

def test_get_when_file_has_one_line
hunk = Telebugs::CodeHunk.get("test/fixtures/project_root/one_line.rb", 1)

assert_equal(
{
start_line: 1,
lines: ["Boom.new.call"]
},
hunk
)
end

def test_get
hunk = Telebugs::CodeHunk.get("test/fixtures/project_root/code.rb", 18)

assert_equal(
{
start_line: 16,
lines: [
" end",
"",
" def start",
" loop do",
" print \"You: \""
]
},
hunk
)
end

def test_get_with_edge_case_first_line
hunk = Telebugs::CodeHunk.get("test/fixtures/project_root/code.rb", 1)

assert_equal(
{
start_line: 1,
lines: [
"# frozen_string_literal: true",
"",
"class Botley"
]
},
hunk
)
end

def test_get_with_edge_case_last_line
hunk = Telebugs::CodeHunk.get("test/fixtures/project_root/code.rb", 39)

assert_equal(
{
start_line: 37,
lines: [
"",
"# Start the conversation with Botley",
"Botley.new.start"
]
},
hunk
)
end

def test_get_when_code_line_is_too_long
hunk = Telebugs::CodeHunk.get("test/fixtures/project_root/long_line.rb", 1)

assert_equal(
{
start_line: 1,
lines: [
"loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong"
]
},
hunk
)
end
end
6 changes: 6 additions & 0 deletions test/test_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@ def test_error_api_url

assert_equal URI("example.com"), Telebugs::Config.instance.api_url
end

def test_root_directory
Telebugs.configure { |c| c.root_directory = "/tmp" }

assert_equal "/tmp", Telebugs::Config.instance.root_directory
end
end
30 changes: 30 additions & 0 deletions test/test_file_cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

require "test_helper"

class TestFileCache < Minitest::Test
def teardown
Telebugs::FileCache.reset
end

def test_set_when_cache_limit_is_not_reached
max_size = Telebugs::FileCache::MAX_SIZE
max_size.times do |i|
Telebugs::FileCache["key#{i}"] = "value#{i}"
end

assert_equal "value0", Telebugs::FileCache["key0"]
assert_equal "value#{max_size - 1}", Telebugs::FileCache["key#{max_size - 1}"]
end

def test_set_when_cache_over_limit
max_size = 2 * Telebugs::FileCache::MAX_SIZE
max_size.times do |i|
Telebugs::FileCache["key#{i}"] = "value#{i}"
end

assert_nil Telebugs::FileCache["key49"]
assert_equal "value50", Telebugs::FileCache["key50"]
assert_equal "value#{max_size - 1}", Telebugs::FileCache["key#{max_size - 1}"]
end
end
52 changes: 52 additions & 0 deletions test/test_notice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@
require "test_helper"

class TestNotice < Minitest::Test
def fixture_path(filename)
File.expand_path(File.join("test", "fixtures", filename))
end

def project_root_path(filename)
fixture_path(File.join("project_root", filename))
end

def setup
Telebugs.configure do |c|
c.root_directory = project_root_path("")
end
end

def teardown
Telebugs::Config.instance.reset
end

def test_to_json_with_nested_errors
begin
raise StandardError.new("error 1")
Expand All @@ -22,10 +40,44 @@ def test_to_json_with_nested_errors
assert_equal "error 2", error1["message"]
assert error1.key?("backtrace")
assert error1["backtrace"].size > 0
assert_nil error1["backtrace"][0]["code"]

assert_equal "StandardError", error2["type"]
assert_equal "error 1", error2["message"]
assert error2.key?("backtrace")
assert error2["backtrace"].size > 0
assert_nil error2["backtrace"][0]["code"]
end

def test_to_json_code
error = RuntimeError.new
error.set_backtrace([
"#{project_root_path("code.rb")}:18:in `start'",
fixture_path("notroot.txt:3:in `pineapple'"),
"#{project_root_path("vendor/bundle/ignored_file.rb")}:2:in `ignore_me'"
])

n = Telebugs::Notice.new(error)
json = JSON.parse(n.to_json)
backtrace = json["errors"][0]["backtrace"]

assert_equal 3, backtrace.size

assert_equal(
{
"start_line" => 16,
"lines" => [
" end",
"",
" def start",
" loop do",
" print \"You: \""
]
},
backtrace[0]["code"]
)

assert_nil backtrace[1]["code"]
assert_nil backtrace[2]["code"]
end
end

0 comments on commit 68a10aa

Please sign in to comment.