Blob Blame History Raw
From 28db4ce2840d74f06b55c1e2c7c48f228708fb1d Mon Sep 17 00:00:00 2001
From: Martin Milata <mmilata@redhat.com>
Date: Wed, 28 Jan 2015 16:11:15 +0100
Subject: [PATCH] Add support for Ruby report type

Related: #1334604

Signed-off-by: Martin Milata <mmilata@redhat.com>
---
 include/Makefile.am                          |   5 +
 include/report_type.h                        |   1 +
 include/ruby/frame.h                         |  99 ++++++
 include/ruby/stacktrace.h                    |  73 ++++
 lib/Makefile.am                              |   2 +
 lib/abrt.c                                   |  31 ++
 lib/generic_frame.c                          |   1 +
 lib/generic_frame.h                          |   3 +-
 lib/generic_stacktrace.c                     |   1 +
 lib/generic_stacktrace.h                     |   3 +-
 lib/generic_thread.c                         |   1 +
 lib/generic_thread.h                         |   3 +-
 lib/report.c                                 |   3 +
 lib/ruby_frame.c                             | 504 +++++++++++++++++++++++++++
 lib/ruby_stacktrace.c                        | 387 ++++++++++++++++++++
 tests/Makefile.am                            |   3 +
 tests/ruby_frame.at                          | 285 +++++++++++++++
 tests/ruby_stacktrace.at                     | 316 +++++++++++++++++
 tests/ruby_stacktraces/ruby-01               |  23 ++
 tests/ruby_stacktraces/ruby-01-expected-json |  97 ++++++
 tests/ruby_stacktraces/ruby-02               |  37 ++
 tests/ruby_stacktraces/ruby-02-expected-json | 158 +++++++++
 tests/ruby_stacktraces/ruby-03               |   4 +
 tests/ruby_stacktraces/ruby-03-expected-json |  19 +
 tests/ruby_stacktraces/ruby-04               |  39 +++
 tests/ruby_stacktraces/ruby-04-expected-json | 166 +++++++++
 tests/testsuite.at                           |   2 +
 27 files changed, 2263 insertions(+), 3 deletions(-)
 create mode 100644 include/ruby/frame.h
 create mode 100644 include/ruby/stacktrace.h
 create mode 100644 lib/ruby_frame.c
 create mode 100644 lib/ruby_stacktrace.c
 create mode 100644 tests/ruby_frame.at
 create mode 100644 tests/ruby_stacktrace.at
 create mode 100644 tests/ruby_stacktraces/ruby-01
 create mode 100644 tests/ruby_stacktraces/ruby-01-expected-json
 create mode 100644 tests/ruby_stacktraces/ruby-02
 create mode 100644 tests/ruby_stacktraces/ruby-02-expected-json
 create mode 100644 tests/ruby_stacktraces/ruby-03
 create mode 100644 tests/ruby_stacktraces/ruby-03-expected-json
 create mode 100644 tests/ruby_stacktraces/ruby-04
 create mode 100644 tests/ruby_stacktraces/ruby-04-expected-json

diff --git a/include/Makefile.am b/include/Makefile.am
index f72ec66..bcffe31 100644
--- a/include/Makefile.am
+++ b/include/Makefile.am
@@ -49,3 +49,8 @@ pythonheadersdir = $(includedir)/satyr/python
 pythonheaders_HEADERS = \
 	python/frame.h \
 	python/stacktrace.h
+
+rubyheadersdir = $(includedir)/satyr/ruby
+rubyheaders_HEADERS = \
+	ruby/frame.h \
+	ruby/stacktrace.h
diff --git a/include/report_type.h b/include/report_type.h
index a51620d..0d411ee 100644
--- a/include/report_type.h
+++ b/include/report_type.h
@@ -32,6 +32,7 @@ enum sr_report_type
     SR_REPORT_KERNELOOPS,
     SR_REPORT_JAVA,
     SR_REPORT_GDB,
+    SR_REPORT_RUBY,
 
     /* Keep this the last entry. */
     SR_REPORT_NUM
diff --git a/include/ruby/frame.h b/include/ruby/frame.h
new file mode 100644
index 0000000..ffc3c67
--- /dev/null
+++ b/include/ruby/frame.h
@@ -0,0 +1,99 @@
+/*
+    ruby_frame.h
+
+    Copyright (C) 2015  ABRT Team
+    Copyright (C) 2015  Red Hat, Inc.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+#ifndef SATYR_RUBY_FRAME_H
+#define SATYR_RUBY_FRAME_H
+
+/**
+ * @file
+ * @brief Ruby frame structure and related routines.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "../report_type.h"
+#include <stdbool.h>
+#include <stdint.h>
+
+struct sr_location;
+struct sr_strbuf;
+struct sr_json_value;
+
+struct sr_ruby_frame
+{
+    enum sr_report_type type;
+
+    char *file_name;
+
+    uint32_t file_line;
+
+    bool special_function;
+
+    char *function_name;
+
+    uint32_t block_level;
+
+    uint32_t rescue_level;
+
+    struct sr_ruby_frame *next;
+};
+
+struct sr_ruby_frame *
+sr_ruby_frame_new();
+
+void
+sr_ruby_frame_init(struct sr_ruby_frame *frame);
+
+void
+sr_ruby_frame_free(struct sr_ruby_frame *frame);
+
+struct sr_ruby_frame *
+sr_ruby_frame_dup(struct sr_ruby_frame *frame, bool siblings);
+
+int
+sr_ruby_frame_cmp(struct sr_ruby_frame *frame1, struct sr_ruby_frame *frame2);
+
+int
+sr_ruby_frame_cmp_distance(struct sr_ruby_frame *frame1,
+                           struct sr_ruby_frame *frame2);
+
+struct sr_ruby_frame *
+sr_ruby_frame_append(struct sr_ruby_frame *dest,
+                     struct sr_ruby_frame *item);
+
+struct sr_ruby_frame *
+sr_ruby_frame_parse(const char **input, struct sr_location *location);
+
+char *
+sr_ruby_frame_to_json(struct sr_ruby_frame *frame);
+
+struct sr_ruby_frame *
+sr_ruby_frame_from_json(struct sr_json_value *root, char **error_message);
+
+void
+sr_ruby_frame_append_to_str(struct sr_ruby_frame *frame, struct sr_strbuf *dest);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/ruby/stacktrace.h b/include/ruby/stacktrace.h
new file mode 100644
index 0000000..caa3e0b
--- /dev/null
+++ b/include/ruby/stacktrace.h
@@ -0,0 +1,73 @@
+/*
+    ruby_stacktrace.h
+
+    Copyright (C) 2015  ABRT Team
+    Copyright (C) 2015  Red Hat, Inc.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+#ifndef SATYR_RUBY_STACKTRACE_H
+#define SATYR_RUBY_STACKTRACE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "../report_type.h"
+#include <stdint.h>
+
+struct sr_ruby_frame;
+struct sr_location;
+struct sr_json_value;
+
+struct sr_ruby_stacktrace
+{
+    enum sr_report_type type;
+
+    char *exception_name;
+
+    struct sr_ruby_frame *frames;
+};
+
+struct sr_ruby_stacktrace *
+sr_ruby_stacktrace_new();
+
+void
+sr_ruby_stacktrace_init(struct sr_ruby_stacktrace *stacktrace);
+
+void
+sr_ruby_stacktrace_free(struct sr_ruby_stacktrace *stacktrace);
+
+struct sr_ruby_stacktrace *
+sr_ruby_stacktrace_dup(struct sr_ruby_stacktrace *stacktrace);
+
+struct sr_ruby_stacktrace *
+sr_ruby_stacktrace_parse(const char **input,
+                         struct sr_location *location);
+
+char *
+sr_ruby_stacktrace_get_reason(struct sr_ruby_stacktrace *stacktrace);
+
+char *
+sr_ruby_stacktrace_to_json(struct sr_ruby_stacktrace *stacktrace);
+
+struct sr_ruby_stacktrace *
+sr_ruby_stacktrace_from_json(struct sr_json_value *root, char **error_message);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/lib/Makefile.am b/lib/Makefile.am
index f798347..26016f2 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -57,6 +57,8 @@ libsatyr_conv_la_SOURCES = \
 	python_stacktrace.c \
 	report.c \
 	rpm.c \
+	ruby_frame.c \
+	ruby_stacktrace.c \
 	sha1.c \
 	strbuf.c \
 	unstrip.c \
diff --git a/lib/abrt.c b/lib/abrt.c
index 4599e2c..a07d44a 100644
--- a/lib/abrt.c
+++ b/lib/abrt.c
@@ -30,6 +30,7 @@
 #include "python/stacktrace.h"
 #include "koops/stacktrace.h"
 #include "java/stacktrace.h"
+#include "ruby/stacktrace.h"
 #include "json.h"
 #include "location.h"
 #include <stdio.h>
@@ -567,6 +568,34 @@ sr_abrt_report_from_dir(const char *directory,
         }
     }
 
+    /* Ruby stacktrace. */
+    if (report->report_type == SR_REPORT_RUBY)
+    {
+        char *backtrace_contents = file_contents(directory, "backtrace",
+                                                 error_message);
+        if (!backtrace_contents)
+        {
+            sr_report_free(report);
+            return NULL;
+        }
+
+        /* Parse the Ruby stacktrace. */
+        struct sr_location location;
+        sr_location_init(&location);
+        const char *contents_pointer = backtrace_contents;
+        report->stacktrace = (struct sr_stacktrace *)sr_ruby_stacktrace_parse(
+            &contents_pointer,
+            &location);
+
+        free(backtrace_contents);
+        if (!report->stacktrace)
+        {
+            *error_message = sr_location_to_string(&location);
+            sr_report_free(report);
+            return NULL;
+        }
+    }
+
     return report;
 }
 
@@ -581,6 +610,8 @@ sr_abrt_type_from_analyzer(const char *analyzer)
         return SR_REPORT_KERNELOOPS;
     else if (0 == strncmp(analyzer, "Java", 4))
         return SR_REPORT_JAVA;
+    else if (0 == strncmp(analyzer, "Ruby", 4))
+        return SR_REPORT_RUBY;
 
     return SR_REPORT_INVALID;
 }
diff --git a/lib/generic_frame.c b/lib/generic_frame.c
index eb644b5..d5512c9 100644
--- a/lib/generic_frame.c
+++ b/lib/generic_frame.c
@@ -34,6 +34,7 @@ static struct frame_methods* dtable[SR_REPORT_NUM] =
     [SR_REPORT_KERNELOOPS] = &koops_frame_methods,
     [SR_REPORT_JAVA] = &java_frame_methods,
     [SR_REPORT_GDB] = &gdb_frame_methods,
+    [SR_REPORT_RUBY] = &ruby_frame_methods,
 };
 
 void
diff --git a/lib/generic_frame.h b/lib/generic_frame.h
index a61626d..e9f392d 100644
--- a/lib/generic_frame.h
+++ b/lib/generic_frame.h
@@ -48,7 +48,8 @@ struct frame_methods
 };
 
 extern struct frame_methods core_frame_methods, python_frame_methods,
-       koops_frame_methods, gdb_frame_methods, java_frame_methods;
+       koops_frame_methods, gdb_frame_methods, java_frame_methods,
+       ruby_frame_methods;
 
 void
 frame_append_bthash_text(struct sr_frame *frame, enum sr_bthash_flags flags,
diff --git a/lib/generic_stacktrace.c b/lib/generic_stacktrace.c
index feb1dbc..f98a482 100644
--- a/lib/generic_stacktrace.c
+++ b/lib/generic_stacktrace.c
@@ -40,6 +40,7 @@ static struct stacktrace_methods* dtable[SR_REPORT_NUM] =
     [SR_REPORT_KERNELOOPS] = &koops_stacktrace_methods,
     [SR_REPORT_JAVA] = &java_stacktrace_methods,
     [SR_REPORT_GDB] = &gdb_stacktrace_methods,
+    [SR_REPORT_RUBY] = &ruby_stacktrace_methods,
 };
 
 /* In case stacktrace type supports only one thread, return the stacktrace itself. */
diff --git a/lib/generic_stacktrace.h b/lib/generic_stacktrace.h
index 8e3d629..6e1a444 100644
--- a/lib/generic_stacktrace.h
+++ b/lib/generic_stacktrace.h
@@ -55,7 +55,8 @@ struct stacktrace_methods
 };
 
 extern struct stacktrace_methods core_stacktrace_methods, python_stacktrace_methods,
-       koops_stacktrace_methods, gdb_stacktrace_methods, java_stacktrace_methods;
+       koops_stacktrace_methods, gdb_stacktrace_methods, java_stacktrace_methods,
+       ruby_stacktrace_methods;
 
 /* Macros to generate accessors for the "threads" member. */
 #define DEFINE_THREADS_FUNC(name, concrete_t) \
diff --git a/lib/generic_thread.c b/lib/generic_thread.c
index 59fff1b..16748b5 100644
--- a/lib/generic_thread.c
+++ b/lib/generic_thread.c
@@ -131,6 +131,7 @@ static struct thread_methods* dtable[SR_REPORT_NUM] =
     [SR_REPORT_KERNELOOPS] = &koops_thread_methods,
     [SR_REPORT_JAVA] = &java_thread_methods,
     [SR_REPORT_GDB] = &gdb_thread_methods,
+    [SR_REPORT_RUBY] = &ruby_thread_methods,
 };
 
 struct sr_frame *
diff --git a/lib/generic_thread.h b/lib/generic_thread.h
index 005e5ac..8635ad2 100644
--- a/lib/generic_thread.h
+++ b/lib/generic_thread.h
@@ -56,7 +56,8 @@ struct thread_methods
 };
 
 extern struct thread_methods core_thread_methods, python_thread_methods,
-       koops_thread_methods, gdb_thread_methods, java_thread_methods;
+       koops_thread_methods, gdb_thread_methods, java_thread_methods,
+       ruby_thread_methods;
 
 /* Macros to generate accessors for the "frames" member. */
 #define DEFINE_FRAMES_FUNC(name, concrete_t) \
diff --git a/lib/report.c b/lib/report.c
index 411df64..614b213 100644
--- a/lib/report.c
+++ b/lib/report.c
@@ -42,6 +42,7 @@ static char *report_types[] =
     [SR_REPORT_KERNELOOPS] = "kerneloops",
     [SR_REPORT_JAVA] = "java",
     [SR_REPORT_GDB] = "gdb",
+    [SR_REPORT_RUBY] = "ruby",
     NULL
 };
 
@@ -189,6 +190,7 @@ sr_report_to_json(struct sr_report *report)
     case SR_REPORT_PYTHON:
     case SR_REPORT_KERNELOOPS:
     case SR_REPORT_JAVA:
+    case SR_REPORT_RUBY:
         report_type = sr_report_type_to_string(report->report_type);
         reason = sr_stacktrace_get_reason(report->stacktrace);
         break;
@@ -383,6 +385,7 @@ sr_report_from_json(struct sr_json_value *root, char **error_message)
         case SR_REPORT_PYTHON:
         case SR_REPORT_KERNELOOPS:
         case SR_REPORT_JAVA:
+        case SR_REPORT_RUBY:
             report->stacktrace = sr_stacktrace_from_json(report->report_type, problem, error_message);
             break;
         default:
diff --git a/lib/ruby_frame.c b/lib/ruby_frame.c
new file mode 100644
index 0000000..9a9a317
--- /dev/null
+++ b/lib/ruby_frame.c
@@ -0,0 +1,504 @@
+/*
+    ruby_frame.c
+
+    Copyright (C) 2015  Red Hat, Inc.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+#include "ruby/frame.h"
+#include "utils.h"
+#include "location.h"
+#include "strbuf.h"
+#include "json.h"
+#include "generic_frame.h"
+#include "thread.h"
+#include "stacktrace.h"
+#include "internal_utils.h"
+#include <string.h>
+#include <inttypes.h>
+#include <ctype.h>
+
+/* Method table */
+
+static void
+ruby_append_bthash_text(struct sr_ruby_frame *frame, enum sr_bthash_flags flags,
+                          struct sr_strbuf *strbuf);
+static void
+ruby_append_duphash_text(struct sr_ruby_frame *frame, enum sr_duphash_flags flags,
+                           struct sr_strbuf *strbuf);
+
+DEFINE_NEXT_FUNC(ruby_next, struct sr_frame, struct sr_ruby_frame)
+DEFINE_SET_NEXT_FUNC(ruby_set_next, struct sr_frame, struct sr_ruby_frame)
+
+struct frame_methods ruby_frame_methods =
+{
+    .append_to_str = (append_to_str_fn_t) sr_ruby_frame_append_to_str,
+    .next = (next_frame_fn_t) ruby_next,
+    .set_next = (set_next_frame_fn_t) ruby_set_next,
+    .cmp = (frame_cmp_fn_t) sr_ruby_frame_cmp,
+    .cmp_distance = (frame_cmp_fn_t) sr_ruby_frame_cmp_distance,
+    .frame_append_bthash_text =
+        (frame_append_bthash_text_fn_t) ruby_append_bthash_text,
+    .frame_append_duphash_text =
+        (frame_append_duphash_text_fn_t) ruby_append_duphash_text,
+    .frame_free = (frame_free_fn_t) sr_ruby_frame_free,
+};
+
+/* Public functions */
+
+struct sr_ruby_frame *
+sr_ruby_frame_new()
+{
+    struct sr_ruby_frame *frame =
+        sr_malloc(sizeof(struct sr_ruby_frame));
+
+    sr_ruby_frame_init(frame);
+    return frame;
+}
+
+void
+sr_ruby_frame_init(struct sr_ruby_frame *frame)
+{
+    memset(frame, 0, sizeof(struct sr_ruby_frame));
+    frame->type = SR_REPORT_RUBY;
+}
+
+void
+sr_ruby_frame_free(struct sr_ruby_frame *frame)
+{
+    if (!frame)
+        return;
+
+    free(frame->file_name);
+    free(frame->function_name);
+    free(frame);
+}
+
+struct sr_ruby_frame *
+sr_ruby_frame_dup(struct sr_ruby_frame *frame, bool siblings)
+{
+    struct sr_ruby_frame *result = sr_ruby_frame_new();
+    memcpy(result, frame, sizeof(struct sr_ruby_frame));
+
+    /* Handle siblings. */
+    if (siblings)
+    {
+        if (result->next)
+            result->next = sr_ruby_frame_dup(result->next, true);
+    }
+    else
+        result->next = NULL; /* Do not copy that. */
+
+    /* Duplicate all strings. */
+    if (result->file_name)
+        result->file_name = sr_strdup(result->file_name);
+
+    if (result->function_name)
+        result->function_name = sr_strdup(result->function_name);
+
+    return result;
+}
+
+int
+sr_ruby_frame_cmp(struct sr_ruby_frame *frame1,
+                  struct sr_ruby_frame *frame2)
+{
+    /* function_name */
+    int function_name = sr_strcmp0(frame1->function_name,
+                                   frame2->function_name);
+    if (function_name != 0)
+        return function_name;
+
+    /* file_name */
+    int file_name = sr_strcmp0(frame1->file_name,
+                               frame2->file_name);
+    if (file_name != 0)
+        return file_name;
+
+    /* file_line */
+    int file_line = frame1->file_line - frame2->file_line;
+    if (file_line != 0)
+        return file_line;
+
+    /* special_function */
+    int special_function = frame1->special_function - frame2->special_function;
+    if (special_function != 0)
+        return special_function;
+
+    /* block_level */
+    int block_level = frame1->block_level - frame2->block_level;
+    if (block_level != 0)
+            return block_level;
+
+    /* rescue_level */
+    int rescue_level = frame1->rescue_level - frame2->rescue_level;
+    if (rescue_level != 0)
+            return rescue_level;
+
+    return 0;
+}
+
+int
+sr_ruby_frame_cmp_distance(struct sr_ruby_frame *frame1,
+                           struct sr_ruby_frame *frame2)
+{
+    /* function_name */
+    int function_name = sr_strcmp0(frame1->function_name,
+                                   frame2->function_name);
+    if (function_name != 0)
+        return function_name;
+
+    /* file_name */
+    int file_name = sr_strcmp0(frame1->file_name,
+                               frame2->file_name);
+    if (file_name != 0)
+        return file_name;
+
+    /* special_function */
+    int special_function = frame1->special_function - frame2->special_function;
+    if (special_function != 0)
+        return special_function;
+
+    return 0;
+}
+
+struct sr_ruby_frame *
+sr_ruby_frame_append(struct sr_ruby_frame *dest,
+                     struct sr_ruby_frame *item)
+{
+    if (!dest)
+        return item;
+
+    struct sr_ruby_frame *dest_loop = dest;
+    while (dest_loop->next)
+        dest_loop = dest_loop->next;
+
+    dest_loop->next = item;
+    return dest;
+}
+
+struct sr_ruby_frame *
+sr_ruby_frame_parse(const char **input,
+                    struct sr_location *location)
+{
+    const char *local_input = *input;
+    struct sr_ruby_frame *frame = sr_ruby_frame_new();
+
+    /* take everything before the backtick
+     * /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+     * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+     */
+    char *filename_lineno_in = NULL;
+    if (!sr_parse_char_cspan(&local_input, "`", &filename_lineno_in))
+    {
+        location->message = sr_strdup("Unable to find the '`' character "
+                                      "identifying the beginning of function name.");
+        goto fail;
+    }
+
+    size_t l = strlen(filename_lineno_in);
+    location->column += l;
+
+    char *p = filename_lineno_in + l;
+
+    /* /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+     *                                                           ^^^^
+     */
+    p -= strlen(":in ");
+    if (p < filename_lineno_in || 0 != strcmp(":in ", p))
+    {
+        location->column -= strlen(":in ");
+        location->message = sr_strdup("Unable to find ':in ' preceding the "
+                                      "backtick character.");
+        goto fail;
+    }
+
+    /* Find the beginning of the line number. */
+    *p = '\0';
+    do {
+        p--;
+    } while (p >= filename_lineno_in && isdigit(*p));
+    p++;
+
+    /* /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+     *                                                         ^^
+     */
+    char *p_copy = p;
+    int lineno_len = sr_parse_uint32((const char **)&p_copy, &frame->file_line);
+    if (lineno_len <= 0)
+    {
+        location->message = sr_strdup("Unable to find line number before ':in '");
+        goto fail;
+    }
+
+    /* /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+     *                                                        ^
+     */
+    p--;
+    if (p < filename_lineno_in || *p != ':')
+    {
+        location->column -= lineno_len;
+        location->message = sr_strdup("Unable to fin the ':' character "
+                                      "preceding the line number");
+        goto fail;
+    }
+
+    /* Everything before the colon is the file name. */
+    *p = '\0';
+    frame->file_name = filename_lineno_in;
+    filename_lineno_in = NULL;
+
+    if(!sr_skip_char(&local_input, '`'))
+    {
+        location->message = sr_strdup("Unable to find the '`' character "
+                                      "identifying the beginning of function name.");
+        goto fail;
+    }
+
+    location->column++;
+
+    /* The part in quotes can look like:
+     * `rescue in rescue in block (3 levels) in func'
+     * parse the number of rescues and blocks
+     */
+    while (sr_skip_string(&local_input, "rescue in "))
+    {
+        frame->rescue_level++;
+        location->column += strlen("rescue in ");
+    }
+
+    if (sr_skip_string(&local_input, "block in "))
+    {
+        frame->block_level = 1;
+        location->column += strlen("block in");
+    }
+    else if(sr_skip_string(&local_input, "block ("))
+    {
+        location->column += strlen("block (");
+
+        int len = sr_parse_uint32(&local_input, &frame->block_level);
+        if (len == 0 || !sr_skip_string(&local_input, " levels) in "))
+        {
+            location->message = sr_strdup("Unable to parse block depth.");
+            goto fail;
+        }
+        location->column += len + strlen(" levels) in ");
+    }
+
+    if (sr_skip_char(&local_input, '<'))
+    {
+        location->column++;
+        frame->special_function = true;
+    }
+
+    if (!sr_parse_char_cspan(&local_input, "'>", &frame->function_name))
+    {
+        location->message = sr_strdup("Unable to find the \"'\" character "
+                                      "delimiting the function name.");
+        goto fail;
+    }
+    location->column += strlen(frame->function_name);
+
+    if (frame->special_function)
+    {
+        if (!sr_skip_char(&local_input, '>'))
+        {
+            location->message = sr_strdup("Unable to find the \">\" character "
+                                          "delimiting the function name.");
+            goto fail;
+        }
+        location->column++;
+    }
+
+    if (!sr_skip_char(&local_input, '\''))
+    {
+        location->message = sr_strdup("Unable to find the \"'\" character "
+                                      "delimiting the function name.");
+        goto fail;
+    }
+    location->column++;
+
+    *input = local_input;
+    return frame;
+
+fail:
+    sr_ruby_frame_free(frame);
+    free(filename_lineno_in);
+    return NULL;
+}
+
+char *
+sr_ruby_frame_to_json(struct sr_ruby_frame *frame)
+{
+    struct sr_strbuf *strbuf = sr_strbuf_new();
+
+    /* Source file name. */
+    if (frame->file_name)
+    {
+        sr_strbuf_append_str(strbuf, ",   \"file_name\": ");
+        sr_json_append_escaped(strbuf, frame->file_name);
+        sr_strbuf_append_str(strbuf, "\n");
+    }
+
+    /* Source file line. */
+    if (frame->file_line)
+    {
+        sr_strbuf_append_strf(strbuf,
+                              ",   \"file_line\": %"PRIu32"\n",
+                              frame->file_line);
+    }
+
+    /* Function name / special function. */
+    if (frame->function_name)
+    {
+        if (frame->special_function)
+            sr_strbuf_append_str(strbuf, ",   \"special_function\": ");
+        else
+            sr_strbuf_append_str(strbuf, ",   \"function_name\": ");
+
+        sr_json_append_escaped(strbuf, frame->function_name);
+        sr_strbuf_append_str(strbuf, "\n");
+    }
+
+    /* Block level. */
+    if (frame->block_level > 0)
+    {
+        sr_strbuf_append_strf(strbuf,
+                              ",   \"block_level\": %"PRIu32"\n",
+                              frame->block_level);
+    }
+
+    /* Rescue level. */
+    if (frame->rescue_level > 0)
+    {
+        sr_strbuf_append_strf(strbuf,
+                              ",   \"rescue_level\": %"PRIu32"\n",
+                              frame->rescue_level);
+    }
+
+
+    strbuf->buf[0] = '{';
+    sr_strbuf_append_char(strbuf, '}');
+    return sr_strbuf_free_nobuf(strbuf);
+}
+
+struct sr_ruby_frame *
+sr_ruby_frame_from_json(struct sr_json_value *root, char **error_message)
+{
+    if (!JSON_CHECK_TYPE(root, SR_JSON_OBJECT, "frame"))
+        return NULL;
+
+    struct sr_ruby_frame *result = sr_ruby_frame_new();
+    struct sr_json_value *val;
+
+    /* Source file name */
+    if ((val = json_element(root, "file_name")))
+    {
+        if (!JSON_CHECK_TYPE(val, SR_JSON_STRING, "file_name"))
+            goto fail;
+
+        result->file_name = sr_strdup(val->u.string.ptr);
+    }
+
+    /* Function name / special function. */
+    if ((val = json_element(root, "function_name")))
+    {
+        if (!JSON_CHECK_TYPE(val, SR_JSON_STRING, "function_name"))
+            goto fail;
+
+        result->special_function = false;
+        result->function_name = sr_strdup(val->u.string.ptr);
+    }
+    else if ((val = json_element(root, "special_function")))
+    {
+        if (!JSON_CHECK_TYPE(val, SR_JSON_STRING, "special_function"))
+            goto fail;
+
+        result->special_function = true;
+        result->function_name = sr_strdup(val->u.string.ptr);
+    }
+
+    bool success =
+        JSON_READ_UINT32(root, "file_line", &result->file_line) &&
+        JSON_READ_UINT32(root, "block_level", &result->block_level) &&
+        JSON_READ_UINT32(root, "rescue_level", &result->rescue_level);
+
+    if (!success)
+        goto fail;
+
+    return result;
+
+fail:
+    sr_ruby_frame_free(result);
+    return NULL;
+}
+
+void
+sr_ruby_frame_append_to_str(struct sr_ruby_frame *frame,
+                            struct sr_strbuf *dest)
+{
+    for (int i = 0; i < frame->rescue_level; i++)
+    {
+        sr_strbuf_append_str(dest, "rescue in ");
+    }
+
+    if (frame->block_level == 1)
+    {
+        sr_strbuf_append_str(dest, "block in ");
+    }
+    else if (frame->block_level > 1)
+    {
+        sr_strbuf_append_strf(dest, "block (%u levels) in ", (unsigned)frame->block_level);
+    }
+
+    sr_strbuf_append_strf(dest, "%s%s%s",
+                          (frame->special_function ? "<" : ""),
+                          (frame->function_name ? frame->function_name : "??"),
+                          (frame->special_function ? ">" : ""));
+
+    if (frame->file_name)
+    {
+        sr_strbuf_append_strf(dest, " in %s", frame->file_name);
+
+        if (frame->file_line)
+        {
+            sr_strbuf_append_strf(dest, ":%d", frame->file_line);
+        }
+    }
+}
+
+static void
+ruby_append_bthash_text(struct sr_ruby_frame *frame, enum sr_bthash_flags flags,
+                        struct sr_strbuf *strbuf)
+{
+    sr_strbuf_append_strf(strbuf,
+                          "%s, %"PRIu32", %s, %d, %"PRIu32", %"PRIu32"\n",
+                          OR_UNKNOWN(frame->file_name),
+                          frame->file_line,
+                          OR_UNKNOWN(frame->function_name),
+                          frame->special_function,
+                          frame->block_level,
+                          frame->rescue_level);
+}
+
+static void
+ruby_append_duphash_text(struct sr_ruby_frame *frame, enum sr_duphash_flags flags,
+                         struct sr_strbuf *strbuf)
+{
+    /* filename:line */
+    sr_strbuf_append_strf(strbuf, "%s:%"PRIu32"\n",
+                          OR_UNKNOWN(frame->file_name),
+                          frame->file_line);
+}
diff --git a/lib/ruby_stacktrace.c b/lib/ruby_stacktrace.c
new file mode 100644
index 0000000..4adde08
--- /dev/null
+++ b/lib/ruby_stacktrace.c
@@ -0,0 +1,387 @@
+/*
+    ruby_stacktrace.c
+
+    Copyright (C) 2015  Red Hat, Inc.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+#include "ruby/stacktrace.h"
+#include "ruby/frame.h"
+#include "location.h"
+#include "utils.h"
+#include "json.h"
+#include "sha1.h"
+#include "report_type.h"
+#include "strbuf.h"
+#include "generic_stacktrace.h"
+#include "generic_thread.h"
+#include "internal_utils.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <inttypes.h>
+
+/* Method tables */
+
+static void
+ruby_append_bthash_text(struct sr_ruby_stacktrace *stacktrace, enum sr_bthash_flags flags,
+                          struct sr_strbuf *strbuf);
+
+DEFINE_FRAMES_FUNC(ruby_frames, struct sr_ruby_stacktrace)
+DEFINE_SET_FRAMES_FUNC(ruby_set_frames, struct sr_ruby_stacktrace)
+DEFINE_PARSE_WRAPPER_FUNC(ruby_parse, SR_REPORT_RUBY)
+
+struct thread_methods ruby_thread_methods =
+{
+    .frames = (frames_fn_t) ruby_frames,
+    .set_frames = (set_frames_fn_t) ruby_set_frames,
+    .cmp = (thread_cmp_fn_t) NULL,
+    .frame_count = (frame_count_fn_t) thread_frame_count,
+    .next = (next_thread_fn_t) thread_no_next_thread,
+    .set_next = (set_next_thread_fn_t) NULL,
+    .thread_append_bthash_text =
+        (thread_append_bthash_text_fn_t) thread_no_bthash_text,
+    .thread_free = (thread_free_fn_t) sr_ruby_stacktrace_free,
+    .remove_frame = (remove_frame_fn_t) thread_remove_frame,
+    .remove_frames_above =
+        (remove_frames_above_fn_t) thread_remove_frames_above,
+    .thread_dup = (thread_dup_fn_t) sr_ruby_stacktrace_dup,
+    .normalize = (normalize_fn_t) thread_no_normalization,
+};
+
+struct stacktrace_methods ruby_stacktrace_methods =
+{
+    .parse = (parse_fn_t) ruby_parse,
+    .parse_location = (parse_location_fn_t) sr_ruby_stacktrace_parse,
+    .to_short_text = (to_short_text_fn_t) stacktrace_to_short_text,
+    .to_json = (to_json_fn_t) sr_ruby_stacktrace_to_json,
+    .from_json = (from_json_fn_t) sr_ruby_stacktrace_from_json,
+    .get_reason = (get_reason_fn_t) sr_ruby_stacktrace_get_reason,
+    .find_crash_thread = (find_crash_thread_fn_t) stacktrace_one_thread_only,
+    .threads = (threads_fn_t) stacktrace_one_thread_only,
+    .set_threads = (set_threads_fn_t) NULL,
+    .stacktrace_free = (stacktrace_free_fn_t) sr_ruby_stacktrace_free,
+    .stacktrace_append_bthash_text =
+        (stacktrace_append_bthash_text_fn_t) ruby_append_bthash_text,
+};
+
+/* Public functions */
+
+struct sr_ruby_stacktrace *
+sr_ruby_stacktrace_new()
+{
+    struct sr_ruby_stacktrace *stacktrace =
+        sr_malloc(sizeof(struct sr_ruby_stacktrace));
+
+    sr_ruby_stacktrace_init(stacktrace);
+    return stacktrace;
+}
+
+void
+sr_ruby_stacktrace_init(struct sr_ruby_stacktrace *stacktrace)
+{
+    memset(stacktrace, 0, sizeof(struct sr_ruby_stacktrace));
+    stacktrace->type = SR_REPORT_RUBY;
+}
+
+void
+sr_ruby_stacktrace_free(struct sr_ruby_stacktrace *stacktrace)
+{
+    if (!stacktrace)
+        return;
+
+    while (stacktrace->frames)
+    {
+        struct sr_ruby_frame *frame = stacktrace->frames;
+        stacktrace->frames = frame->next;
+        sr_ruby_frame_free(frame);
+    }
+
+    free(stacktrace->exception_name);
+    free(stacktrace);
+}
+
+struct sr_ruby_stacktrace *
+sr_ruby_stacktrace_dup(struct sr_ruby_stacktrace *stacktrace)
+{
+    struct sr_ruby_stacktrace *result = sr_ruby_stacktrace_new();
+    memcpy(result, stacktrace, sizeof(struct sr_ruby_stacktrace));
+
+    if (result->exception_name)
+        result->exception_name = sr_strdup(result->exception_name);
+
+    if (result->frames)
+        result->frames = sr_ruby_frame_dup(result->frames, true);
+
+    return result;
+}
+
+struct sr_ruby_stacktrace *
+sr_ruby_stacktrace_parse(const char **input,
+                         struct sr_location *location)
+{
+    const char *local_input = *input;
+    struct sr_ruby_stacktrace *stacktrace = sr_ruby_stacktrace_new();
+    char *message_and_class = NULL;
+
+    /* /some/thing.rb:13:in `method': exception message (Exception::Class)\n\tfrom ...
+     * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+     */
+    stacktrace->frames = sr_ruby_frame_parse(&local_input, location);
+    if (!stacktrace->frames)
+    {
+        location->message = sr_asprintf("Topmost stacktrace frame not found: %s",
+                            location->message ? location->message : "(unknown reason)");
+        goto fail;
+    }
+
+    /* /some/thing.rb:13:in `method': exception message (Exception::Class)\n\tfrom ...
+     *                              ^^
+     */
+    if (!sr_skip_string(&local_input, ": "))
+    {
+        location->message = sr_strdup("Unable to find the colon after first function name.");
+        goto fail;
+    }
+    location->column += strlen(": ");
+
+    /* /some/thing.rb:13:in `method': exception message (Exception::Class)\n\tfrom ...
+     *                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+     */
+    if (!sr_parse_char_cspan(&local_input, "\t", &message_and_class))
+    {
+        location->message = sr_strdup("Unable to find the exception type and message.");
+        goto fail;
+    }
+
+    /* /some/thing.rb:13:in `method': exception message (Exception::Class)\n\tfrom ...
+     *                                                                    ^^
+     */
+    size_t l = strlen(message_and_class);
+    location->column += l;
+
+    char *p = message_and_class + l - 1;
+    if (p < message_and_class || *p != '\n')
+    {
+        location->column--;
+        location->message = sr_strdup("Unable to find the new line character after "
+                                      "the end of exception class");
+        goto fail;
+    }
+
+    /* /some/thing.rb:13:in `method': exception message (Exception::Class)\n\tfrom ...
+     *                                                                   ^
+     */
+    p--;
+    if (p < message_and_class || *p != ')')
+    {
+        location->column -= 2;
+        location->message = sr_strdup("Unable to find the ')' character identifying "
+                                      "the end of exception class");
+        goto fail;
+    }
+
+    /* /some/thing.rb:13:in `method': exception message (Exception::Class)\n\tfrom ...
+     *                                                   ^^^^^^^^^^^^^^^^
+     */
+    *p = '\0';
+    do {
+        p--;
+    } while (p >= message_and_class && *p != '(');
+    p++;
+
+    if (strlen(p) <= 0)
+    {
+        location->message = sr_strdup("Unable to find the '(' character identifying "
+                                      "the beginning of the exception class");
+        goto fail;
+    }
+    stacktrace->exception_name = sr_strdup(p);
+
+    /* /some/thing.rb:13:in `method': exception message (Exception::Class)\n\tfrom ...
+     *                                                  ^
+     */
+    p--;
+    if (p < message_and_class || *p != '(')
+    {
+        location->message = sr_strdup("Unable to find the '(' character identifying "
+                                      "the beginning of the exception class");
+        goto fail;
+    }
+
+    /* Throw away the message, it may contain sensitive data. */
+    free(message_and_class);
+    message_and_class = p = NULL;
+
+    /* /some/thing.rb:13:in `method': exception message (Exception::Class)\n\tfrom ...
+     *                                   we're here now, increment the line ^^
+     */
+    location->column = 0;
+    location->line++;
+
+    struct sr_ruby_frame *last_frame = stacktrace->frames;
+    while (*local_input)
+    {
+        /* The exception message can continue on the lines after the topmost frame
+         * - skip those.
+         */
+        int skipped = sr_skip_string(&local_input, "\tfrom ");
+        if (!skipped)
+        {
+            location->message = sr_strdup("Frame header not found.");
+            goto fail;
+        }
+        location->column += skipped;
+
+        last_frame->next = sr_ruby_frame_parse(&local_input, location);
+        if (!last_frame->next)
+        {
+            /* location->message is already set */
+            goto fail;
+        }
+
+        /* Eat newline (except at the end of file). */
+        if (!sr_skip_char(&local_input, '\n') && *local_input != '\0')
+        {
+            location->message = sr_strdup("Expected newline after stacktrace frame.");
+            goto fail;
+        }
+        location->column = 0;
+        location->line++;
+        last_frame = last_frame->next;
+    }
+
+    *input = local_input;
+    return stacktrace;
+
+fail:
+    sr_ruby_stacktrace_free(stacktrace);
+    free(message_and_class);
+    return NULL;
+}
+
+char *
+sr_ruby_stacktrace_to_json(struct sr_ruby_stacktrace *stacktrace)
+{
+    struct sr_strbuf *strbuf = sr_strbuf_new();
+
+    /* Exception class name. */
+    if (stacktrace->exception_name)
+    {
+        sr_strbuf_append_str(strbuf, ",   \"exception_name\": ");
+        sr_json_append_escaped(strbuf, stacktrace->exception_name);
+        sr_strbuf_append_str(strbuf, "\n");
+    }
+
+    /* Frames. */
+    if (stacktrace->frames)
+    {
+        struct sr_ruby_frame *frame = stacktrace->frames;
+        sr_strbuf_append_str(strbuf, ",   \"stacktrace\":\n");
+        while (frame)
+        {
+            if (frame == stacktrace->frames)
+                sr_strbuf_append_str(strbuf, "      [ ");
+            else
+                sr_strbuf_append_str(strbuf, "      , ");
+
+            char *frame_json = sr_ruby_frame_to_json(frame);
+            char *indented_frame_json = sr_indent_except_first_line(frame_json, 8);
+            sr_strbuf_append_str(strbuf, indented_frame_json);
+            free(indented_frame_json);
+            free(frame_json);
+            frame = frame->next;
+            if (frame)
+                sr_strbuf_append_str(strbuf, "\n");
+        }
+
+        sr_strbuf_append_str(strbuf, " ]\n");
+    }
+
+    if (strbuf->len > 0)
+        strbuf->buf[0] = '{';
+    else
+        sr_strbuf_append_char(strbuf, '{');
+
+    sr_strbuf_append_char(strbuf, '}');
+    return sr_strbuf_free_nobuf(strbuf);
+}
+
+struct sr_ruby_stacktrace *
+sr_ruby_stacktrace_from_json(struct sr_json_value *root, char **error_message)
+{
+    if (!JSON_CHECK_TYPE(root, SR_JSON_OBJECT, "stacktrace"))
+        return NULL;
+
+    struct sr_ruby_stacktrace *result = sr_ruby_stacktrace_new();
+
+    /* Exception name. */
+    if (!JSON_READ_STRING(root, "exception_name", &result->exception_name))
+        goto fail;
+
+    /* Frames. */
+    struct sr_json_value *stacktrace = json_element(root, "stacktrace");
+    if (stacktrace)
+    {
+        if (!JSON_CHECK_TYPE(stacktrace, SR_JSON_ARRAY, "stacktrace"))
+            goto fail;
+
+        struct sr_json_value *frame_json;
+        FOR_JSON_ARRAY(stacktrace, frame_json)
+        {
+            struct sr_ruby_frame *frame = sr_ruby_frame_from_json(frame_json,
+                error_message);
+
+            if (!frame)
+                goto fail;
+
+            result->frames = sr_ruby_frame_append(result->frames, frame);
+        }
+    }
+
+    return result;
+
+fail:
+    sr_ruby_stacktrace_free(result);
+    return NULL;
+}
+
+char *
+sr_ruby_stacktrace_get_reason(struct sr_ruby_stacktrace *stacktrace)
+{
+    char *exc = "Unknown error";
+    char *file = "<unknown>";
+    uint32_t line = 0;
+
+    struct sr_ruby_frame *frame = stacktrace->frames;
+    if (frame)
+    {
+        file = frame->file_name;
+        line = frame->file_line;
+    }
+
+    if (stacktrace->exception_name)
+        exc = stacktrace->exception_name;
+
+    return sr_asprintf("%s in %s:%"PRIu32, exc, file, line);
+}
+
+static void
+ruby_append_bthash_text(struct sr_ruby_stacktrace *stacktrace, enum sr_bthash_flags flags,
+                        struct sr_strbuf *strbuf)
+{
+    sr_strbuf_append_strf(strbuf, "Exception: %s\n", OR_UNKNOWN(stacktrace->exception_name));
+    sr_strbuf_append_char(strbuf, '\n');
+}
diff --git a/tests/Makefile.am b/tests/Makefile.am
index d69f493..0eb33a5 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1,5 +1,6 @@
 EXTRA_DIST = gdb_stacktraces \
              java_stacktraces \
+             ruby_stacktraces \
              json_files \
              kerneloopses \
              programs \
@@ -50,6 +51,8 @@ TESTSUITE_AT =		\
   core_frame.at         \
   core_thread.at        \
   core_stacktrace.at    \
+  ruby_frame.at		\
+  ruby_stacktrace.at	\
   normalize.at  	\
   metrics.at 		\
   cluster.at  		\
diff --git a/tests/ruby_frame.at b/tests/ruby_frame.at
new file mode 100644
index 0000000..1b1b9d0
--- /dev/null
+++ b/tests/ruby_frame.at
@@ -0,0 +1,285 @@
+# Checking the satyr. -*- Autotest -*-
+
+AT_BANNER([Ruby frames])
+
+AT_TESTFUN([sr_ruby_frame_parse],
+[[
+#include "ruby/frame.h"
+#include "utils.h"
+#include "location.h"
+#include <assert.h>
+
+void
+check(char *input, char *file, unsigned line,
+      char *function, bool special, unsigned block_level, unsigned rescue_level)
+{
+  struct sr_location location;
+  sr_location_init(&location);
+
+  struct sr_ruby_frame *frame;
+  frame = sr_ruby_frame_parse(&input, &location);
+
+  assert(frame);
+  assert(frame->file_name && 0 == strcmp(frame->file_name, file));
+  assert(frame->file_line == line);
+  assert(frame->function_name && 0 == strcmp(frame->function_name, function));
+  assert(!!frame->special_function == !!special);
+  assert(frame->block_level == block_level);
+  assert(frame->rescue_level == rescue_level);
+  assert(*input == '\0');
+
+  sr_ruby_frame_free(frame);
+}
+
+int
+main(void)
+{
+  check("/usr/share/ruby/vendor_ruby/will_crash.rb:13:in `rescue in block (2 levels) in func'",
+        "/usr/share/ruby/vendor_ruby/will_crash.rb", 13, "func", false, 2, 1);
+
+  check("/home/u/work/will:crash/will_crash.rb:30:in `block in <class:WillClass>'",
+        "/home/u/work/will:crash/will_crash.rb", 30, "class:WillClass", true, 1, 0);
+
+  check("./will_ruby_raise:8:in `<main>'",
+        "./will_ruby_raise", 8, "main", true, 0, 0);
+
+  /* try failing */
+  struct sr_location location;
+  sr_location_init(&location);
+  char *input = "i;dikasdfxc";
+
+  struct sr_ruby_frame *frame  = sr_ruby_frame_parse(&input, &location);
+  assert(!frame);
+  assert(location.message);
+
+  return 0;
+}
+]])
+
+AT_TESTFUN([sr_ruby_frame_cmp],
+[[
+#include "ruby/frame.h"
+#include "utils.h"
+#include "location.h"
+#include <assert.h>
+
+int
+main(void)
+{
+  struct sr_location location;
+  char *line = "/usr/share/ruby/vendor_ruby/will_crash.rb:13:in `rescue in block (2 levels) in func'";
+  char *input;
+
+  sr_location_init(&location);
+  input = line;
+  struct sr_ruby_frame *frame1 = sr_ruby_frame_parse(&input, &location);
+  assert(frame1);
+
+  sr_location_init(&location);
+  input = line;
+  struct sr_ruby_frame *frame2 = sr_ruby_frame_parse(&input, &location);
+  assert(frame1);
+
+  assert(0 == sr_ruby_frame_cmp(frame1, frame2));
+  assert(frame1 != frame2);
+
+  frame2->file_line = 9000;
+  assert(0 != sr_ruby_frame_cmp(frame1, frame2));
+
+  sr_ruby_frame_free(frame1);
+  sr_ruby_frame_free(frame2);
+
+  return 0;
+}
+]])
+
+AT_TESTFUN([sr_ruby_frame_dup],
+[[
+#include "ruby/frame.h"
+#include "utils.h"
+#include "location.h"
+#include <assert.h>
+
+int
+main(void)
+{
+  struct sr_location location;
+  char *line = "/usr/share/ruby/vendor_ruby/will_crash.rb:13:in `rescue in block (2 levels) in func'";
+  char *input;
+
+  sr_location_init(&location);
+  input = line;
+  struct sr_ruby_frame *frame1 = sr_ruby_frame_parse(&input, &location);
+  assert(frame1);
+
+  struct sr_ruby_frame *frame2 = sr_ruby_frame_dup(frame1, false);
+
+  assert(0 == sr_ruby_frame_cmp(frame1, frame2));
+  assert(frame1 != frame2);
+  assert(frame1->function_name != frame2->function_name);
+
+  return 0;
+}
+]])
+
+AT_TESTFUN([sr_ruby_frame_append],
+[[
+#include "ruby/frame.h"
+#include "utils.h"
+#include "location.h"
+#include <assert.h>
+
+int
+main(void)
+{
+  struct sr_ruby_frame *frame1 = sr_ruby_frame_new();
+  struct sr_ruby_frame *frame2 = sr_ruby_frame_new();
+
+  assert(frame1->next == frame2->next);
+  assert(frame1->next == NULL);
+
+  sr_ruby_frame_append(frame1, frame2);
+  assert(frame1->next == frame2);
+
+  sr_ruby_frame_free(frame1);
+  sr_ruby_frame_free(frame2);
+
+  return 0;
+}
+]])
+
+AT_TESTFUN([sr_ruby_frame_to_json],
+[[
+#include "ruby/frame.h"
+#include "utils.h"
+#include "location.h"
+#include <assert.h>
+
+int
+main(void)
+{
+  struct sr_location location;
+  char *line = "/usr/share/ruby/vendor_ruby/will_crash.rb:13:in `rescue in block (2 levels) in func'";
+  char *input;
+
+  sr_location_init(&location);
+  input = line;
+  struct sr_ruby_frame *frame1 = sr_ruby_frame_parse(&input, &location);
+  assert(frame1);
+
+  char *expected = "{   \"file_name\": \"/usr/share/ruby/vendor_ruby/will_crash.rb\"\n"
+                   ",   \"file_line\": 13\n"
+                   ",   \"function_name\": \"func\"\n"
+                   ",   \"block_level\": 2\n"
+                   ",   \"rescue_level\": 1\n"
+                   "}";
+
+  char *json = sr_ruby_frame_to_json(frame1);
+  assert(0 == strcmp(json, expected));
+
+  return 0;
+}
+]])
+
+AT_TESTFUN([sr_ruby_frame_from_json],
+[[
+#include "ruby/frame.h"
+#include "utils.h"
+#include "json.h"
+#include "location.h"
+#include <assert.h>
+
+void
+check(char *input)
+{
+  struct sr_location location;
+  sr_location_init(&location);
+
+  struct sr_ruby_frame *frame1 = sr_ruby_frame_parse(&input, &location);
+  assert(frame1);
+
+  char *json = sr_ruby_frame_to_json(frame1);
+  char *error = NULL;
+
+  struct sr_json_value *root = sr_json_parse(json, &error);
+  assert(root);
+  struct sr_ruby_frame *frame2 = sr_ruby_frame_from_json(root, &error);
+  assert(frame2);
+  assert(0 == sr_ruby_frame_cmp(frame1, frame2));
+
+  return 0;
+}
+
+int
+main(void)
+{
+  check("/usr/share/ruby/vendor_ruby/will_crash.rb:13:in `rescue in block (2 levels) in func'");
+  check("/usr/share/ruby/vendor:ruby/will_crash.rb:13:in `block (22 levels) in <func>'");
+
+  return 0;
+}
+]])
+
+AT_TESTFUN([sr_ruby_frame_append_to_str],
+[[
+#include "ruby/frame.h"
+#include "utils.h"
+#include "location.h"
+#include "strbuf.h"
+#include <assert.h>
+
+int
+main(void)
+{
+  struct sr_location location;
+  char *line = "/usr/share/ruby/vendor_ruby/will_crash.rb:13:in `rescue in block (2 levels) in func'";
+  char *input;
+
+  sr_location_init(&location);
+  input = line;
+  struct sr_ruby_frame *frame1 = sr_ruby_frame_parse(&input, &location);
+  assert(frame1);
+
+  struct sr_strbuf *strbuf = sr_strbuf_new();
+  sr_ruby_frame_append_to_str(frame1, strbuf);
+  char *result = sr_strbuf_free_nobuf(strbuf);
+
+  assert(0 == strcmp(result, "rescue in block (2 levels) in func in /usr/share/ruby/vendor_ruby/will_crash.rb:13"));
+
+  return 0;
+}
+]])
+
+AT_TESTFUN([sr_ruby_frame_generic_functions],
+[[
+#include "ruby/frame.h"
+#include "frame.h"
+#include "utils.h"
+#include "location.h"
+#include "strbuf.h"
+#include <assert.h>
+
+int
+main(void)
+{
+  struct sr_location location;
+  char *line = "/usr/share/ruby/vendor_ruby/will_crash.rb:13:in `rescue in block (2 levels) in func'";
+  char *input;
+
+  sr_location_init(&location);
+  input = line;
+  struct sr_ruby_frame *frame1 = sr_ruby_frame_parse(&input, &location);
+  assert(frame1);
+
+  assert(sr_frame_next(frame1) == NULL);
+
+  struct sr_strbuf *strbuf = sr_strbuf_new();
+  sr_frame_append_to_str((struct sr_frame*)frame1, strbuf);
+  char *result = sr_strbuf_free_nobuf(strbuf);
+
+  assert(0 == strcmp(result, "rescue in block (2 levels) in func in /usr/share/ruby/vendor_ruby/will_crash.rb:13"));
+
+  sr_frame_free((struct sr_frame*)frame1);
+  return 0;
+}
+]])
diff --git a/tests/ruby_stacktrace.at b/tests/ruby_stacktrace.at
new file mode 100644
index 0000000..5a2d901
--- /dev/null
+++ b/tests/ruby_stacktrace.at
@@ -0,0 +1,316 @@
+# Checking the satyr. -*- Autotest -*-
+
+AT_BANNER([Ruby stacktrace])
+
+AT_TESTFUN([sr_ruby_stacktrace_parse],
+[[
+#include "ruby/stacktrace.h"
+#include "ruby/frame.h"
+#include "utils.h"
+#include "location.h"
+#include <assert.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+void
+check(char *filename, char *exc_name, unsigned frame_count, struct sr_ruby_frame *top_frame, struct sr_ruby_frame *second_frame, struct sr_ruby_frame *bottom_frame)
+{
+  char *error_message = NULL;
+  const char *file_contents = sr_file_to_string(filename, &error_message);
+  const char *input = file_contents;
+  struct sr_location location;
+  sr_location_init(&location);
+
+  struct sr_ruby_stacktrace *stacktrace = sr_ruby_stacktrace_parse(&input, &location);
+  assert(stacktrace);
+  assert(*input == '\0');
+  assert(0 == strcmp(stacktrace->exception_name, exc_name));
+
+  struct sr_ruby_frame *frame = stacktrace->frames;
+  int i = 0;
+  while (frame)
+  {
+    if (i==0 && top_frame)
+    {
+      assert(sr_ruby_frame_cmp(frame, top_frame) == 0);
+    }
+    else if (i == 1 && second_frame)
+    {
+      assert(sr_ruby_frame_cmp(frame, second_frame) == 0);
+    }
+
+    frame = frame->next;
+    i++;
+  }
+
+  assert(i == frame_count);
+  if (frame && bottom_frame)
+  {
+    assert(sr_ruby_frame_cmp(frame, bottom_frame) == 0);
+  }
+
+  sr_ruby_stacktrace_free(stacktrace);
+  free(file_contents);
+}
+
+int
+main(void)
+{
+  struct sr_ruby_frame top = {
+    .type = SR_REPORT_RUBY,
+    .file_name = "/usr/share/ruby/vendor_ruby/will_crash.rb",
+    .file_line = 13,
+    .special_function = false,
+    .function_name = "func",
+    .block_level = 2,
+    .rescue_level = 1
+  };
+  struct sr_ruby_frame second = {
+    .type = SR_REPORT_RUBY,
+    .file_name = "/usr/share/ruby/vendor_ruby/will_crash.rb",
+    .file_line = 10,
+    .special_function = false,
+    .function_name = "func",
+    .block_level = 2,
+    .rescue_level = 0
+  };
+  struct sr_ruby_frame bottom = {
+    .type = SR_REPORT_RUBY,
+    .file_name = "/usr/bin/will_ruby_raise",
+    .file_line = 8,
+    .special_function = true,
+    .function_name = "main",
+    .block_level = 0,
+    .rescue_level = 0
+  };
+  check("../../ruby_stacktraces/ruby-01", "Wrap::MyException", 21, &top, &second, &bottom);
+
+  struct sr_ruby_frame top2 = {
+    .type = SR_REPORT_RUBY,
+    .file_name = "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/class_module.rb",
+    .file_line = 173,
+    .special_function = false,
+    .function_name = "aref_prefix",
+    .block_level = 0,
+    .rescue_level = 0
+  };
+  struct sr_ruby_frame second2 = {
+    .type = SR_REPORT_RUBY,
+    .file_name = "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/class_module.rb",
+    .file_line = 181,
+    .special_function = false,
+    .function_name = "aref",
+    .block_level = 0,
+    .rescue_level = 0
+  };
+  check("../../ruby_stacktraces/ruby-02", "NotImplementedError", 37, &top2, &second2, NULL);
+
+  check("../../ruby_stacktraces/ruby-03", "RuntimeError", 4, NULL, NULL, NULL);
+
+  struct sr_ruby_frame top4 = {
+    .type = SR_REPORT_RUBY,
+    .file_name = "/usr/share/rubygems/rubygems/basic_specification.rb",
+    .file_line = 50,
+    .special_function = false,
+    .function_name = "contains_requirable_file?",
+    .block_level = 3,
+    .rescue_level = 0
+  };
+  struct sr_ruby_frame second4 = {
+    .type = SR_REPORT_RUBY,
+    .file_name = "/usr/share/rubygems/rubygems/basic_specification.rb",
+    .file_line = 50,
+    .special_function = false,
+    .function_name = "each",
+    .block_level = 0,
+    .rescue_level = 0
+  };
+  check("../../ruby_stacktraces/ruby-04", "SignalException", 39, &top4, &second4, NULL);
+
+  return 0;
+}
+]])
+
+AT_TESTFUN([sr_ruby_stacktrace_dup],
+[[
+#include "ruby/stacktrace.h"
+#include "ruby/frame.h"
+#include "utils.h"
+#include "location.h"
+#include <assert.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+void
+check(char *filename)
+{
+  char *error_message = NULL;
+  const char *file_contents = sr_file_to_string(filename, &error_message);
+  const char *input = file_contents;
+  struct sr_location location;
+  sr_location_init(&location);
+
+  struct sr_ruby_stacktrace *stacktrace1 = sr_ruby_stacktrace_parse(&input, &location);
+  struct sr_ruby_stacktrace *stacktrace2 = sr_ruby_stacktrace_dup(stacktrace1);
+
+  assert(stacktrace1 != stacktrace2);
+  assert(0 == strcmp(stacktrace1->exception_name, stacktrace2->exception_name));
+
+  struct sr_ruby_frame *f1 = stacktrace1->frames;
+  struct sr_ruby_frame *f2 = stacktrace2->frames;
+
+  while (f1 && f2)
+  {
+    assert(0 == sr_ruby_frame_cmp(f1, f2));
+    f1 = f1->next;
+    f2 = f2->next;
+  }
+  assert(f1 == NULL);
+  assert(f2 == NULL);
+
+  sr_ruby_stacktrace_free(stacktrace1);
+  sr_ruby_stacktrace_free(stacktrace2);
+  free(file_contents);
+}
+
+int
+main(void)
+{
+  check("../../ruby_stacktraces/ruby-01");
+  check("../../ruby_stacktraces/ruby-02");
+  check("../../ruby_stacktraces/ruby-03");
+  check("../../ruby_stacktraces/ruby-04");
+
+  return 0;
+}
+]])
+
+AT_TESTFUN([sr_ruby_stacktrace_get_reason],
+[[
+#include "ruby/stacktrace.h"
+#include "ruby/frame.h"
+#include "utils.h"
+#include "location.h"
+#include <assert.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+int
+main(void)
+{
+  char *error_message = NULL;
+  const char *file_contents = sr_file_to_string("../../ruby_stacktraces/ruby-03", &error_message);
+  const char *input = file_contents;
+  struct sr_location location;
+  sr_location_init(&location);
+  struct sr_ruby_stacktrace *stacktrace1 = sr_ruby_stacktrace_parse(&input, &location);
+
+  char *reason = sr_ruby_stacktrace_get_reason(stacktrace1);
+  char *expected = "RuntimeError in /usr/share/gems/gems/openshift-origin-node-1.18.0.1/lib/openshift-origin-node/utils/tc.rb:103";
+  assert(0 == strcmp(reason, expected));
+
+  sr_ruby_stacktrace_free(stacktrace1);
+  free(file_contents);
+
+  return 0;
+}
+]])
+
+AT_TESTFUN([sr_ruby_stacktrace_to_json],
+[[
+#include "ruby/stacktrace.h"
+#include "ruby/frame.h"
+#include "utils.h"
+#include "location.h"
+#include <assert.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+void
+check(char *filename, char *json_filename)
+{
+  char *error_message = NULL;
+  const char *file_contents = sr_file_to_string(filename, &error_message);
+  const char *input = file_contents;
+  struct sr_location location;
+  sr_location_init(&location);
+
+  struct sr_ruby_stacktrace *stacktrace1 = sr_ruby_stacktrace_parse(&input, &location);
+
+  char *expected = sr_file_to_string(json_filename, &error_message);
+  char *json = sr_ruby_stacktrace_to_json(stacktrace1);
+
+  assert(0 == strcmp(json, expected));
+
+  sr_ruby_stacktrace_free(stacktrace1);
+  free(json);
+  free(file_contents);
+}
+
+int
+main(void)
+{
+  check("../../ruby_stacktraces/ruby-01", "../../ruby_stacktraces/ruby-01-expected-json");
+  check("../../ruby_stacktraces/ruby-02", "../../ruby_stacktraces/ruby-02-expected-json");
+  check("../../ruby_stacktraces/ruby-03", "../../ruby_stacktraces/ruby-03-expected-json");
+  check("../../ruby_stacktraces/ruby-04", "../../ruby_stacktraces/ruby-04-expected-json");
+
+  return 0;
+}
+]])
+
+AT_TESTFUN([sr_ruby_stacktrace_from_json],
+[[
+#include "ruby/stacktrace.h"
+#include "ruby/frame.h"
+#include "utils.h"
+#include "location.h"
+#include <assert.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+void
+check(char *filename)
+{
+  char *error_message = NULL;
+  const char *file_contents = sr_file_to_string(filename, &error_message);
+  const char *input = file_contents;
+  struct sr_location location;
+  sr_location_init(&location);
+
+  struct sr_ruby_stacktrace *stacktrace1 = sr_ruby_stacktrace_parse(&input, &location);
+
+  char *json = sr_ruby_stacktrace_to_json(stacktrace1);
+  struct sr_ruby_stacktrace *stacktrace2 = sr_stacktrace_from_json_text(SR_REPORT_RUBY, json, &error_message);
+
+  assert(0 == strcmp(stacktrace1->exception_name, stacktrace2->exception_name));
+
+  struct sr_ruby_frame *f1 = stacktrace1->frames;
+  struct sr_ruby_frame *f2 = stacktrace2->frames;
+
+  while (f1 && f2)
+  {
+    assert(0 == sr_ruby_frame_cmp(f1, f2));
+    f1 = f1->next;
+    f2 = f2->next;
+  }
+  assert(f1 == NULL);
+  assert(f2 == NULL);
+
+  sr_ruby_stacktrace_free(stacktrace1);
+  sr_ruby_stacktrace_free(stacktrace2);
+  free(json);
+  free(file_contents);
+}
+
+int
+main(void)
+{
+  check("../../ruby_stacktraces/ruby-01");
+  check("../../ruby_stacktraces/ruby-02");
+  check("../../ruby_stacktraces/ruby-03");
+  check("../../ruby_stacktraces/ruby-04");
+
+  return 0;
+}
+]])
diff --git a/tests/ruby_stacktraces/ruby-01 b/tests/ruby_stacktraces/ruby-01
new file mode 100644
index 0000000..a635dd9
--- /dev/null
+++ b/tests/ruby_stacktraces/ruby-01
@@ -0,0 +1,23 @@
+/usr/share/ruby/vendor_ruby/will_crash.rb:13:in `rescue in block (2 levels) in func': Exception
+successfully
+raised. (Wrap::MyException)
+	from /usr/share/ruby/vendor_ruby/will_crash.rb:10:in `block (2 levels) in func'
+	from /usr/share/ruby/vendor_ruby/will_crash.rb:9:in `times'
+	from /usr/share/ruby/vendor_ruby/will_crash.rb:9:in `block in func'
+	from /usr/share/ruby/vendor_ruby/will_crash.rb:8:in `times'
+	from /usr/share/ruby/vendor_ruby/will_crash.rb:8:in `func'
+	from /usr/share/ruby/vendor_ruby/will_crash.rb:30:in `block in <class:WillClass>'
+	from /usr/share/ruby/vendor_ruby/will_crash.rb:28:in `times'
+	from /usr/share/ruby/vendor_ruby/will_crash.rb:28:in `rescue in rescue in rescue in <class:WillClass>'
+	from /usr/share/ruby/vendor_ruby/will_crash.rb:25:in `rescue in rescue in <class:WillClass>'
+	from /usr/share/ruby/vendor_ruby/will_crash.rb:22:in `rescue in <class:WillClass>'
+	from /usr/share/ruby/vendor_ruby/will_crash.rb:19:in `<class:WillClass>'
+	from /usr/share/ruby/vendor_ruby/will_crash.rb:6:in `<top (required)>'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+	from /usr/bin/will_ruby_raise:11:in `block (2 levels) in <module:WillModule>'
+	from /usr/bin/will_ruby_raise:10:in `times'
+	from /usr/bin/will_ruby_raise:10:in `block in <module:WillModule>'
+	from /usr/bin/will_ruby_raise:9:in `times'
+	from /usr/bin/will_ruby_raise:9:in `<module:WillModule>'
+	from /usr/bin/will_ruby_raise:8:in `<main>'
diff --git a/tests/ruby_stacktraces/ruby-01-expected-json b/tests/ruby_stacktraces/ruby-01-expected-json
new file mode 100644
index 0000000..493fb18
--- /dev/null
+++ b/tests/ruby_stacktraces/ruby-01-expected-json
@@ -0,0 +1,97 @@
+{   "exception_name": "Wrap::MyException"
+,   "stacktrace":
+      [ {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 13
+        ,   "function_name": "func"
+        ,   "block_level": 2
+        ,   "rescue_level": 1
+        }
+      , {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 10
+        ,   "function_name": "func"
+        ,   "block_level": 2
+        }
+      , {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 9
+        ,   "function_name": "times"
+        }
+      , {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 9
+        ,   "function_name": "func"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 8
+        ,   "function_name": "times"
+        }
+      , {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 8
+        ,   "function_name": "func"
+        }
+      , {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 30
+        ,   "special_function": "class:WillClass"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 28
+        ,   "function_name": "times"
+        }
+      , {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 28
+        ,   "special_function": "class:WillClass"
+        ,   "rescue_level": 3
+        }
+      , {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 25
+        ,   "special_function": "class:WillClass"
+        ,   "rescue_level": 2
+        }
+      , {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 22
+        ,   "special_function": "class:WillClass"
+        ,   "rescue_level": 1
+        }
+      , {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 19
+        ,   "special_function": "class:WillClass"
+        }
+      , {   "file_name": "/usr/share/ruby/vendor_ruby/will_crash.rb"
+        ,   "file_line": 6
+        ,   "special_function": "top (required)"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 55
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 55
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/bin/will_ruby_raise"
+        ,   "file_line": 11
+        ,   "special_function": "module:WillModule"
+        ,   "block_level": 2
+        }
+      , {   "file_name": "/usr/bin/will_ruby_raise"
+        ,   "file_line": 10
+        ,   "function_name": "times"
+        }
+      , {   "file_name": "/usr/bin/will_ruby_raise"
+        ,   "file_line": 10
+        ,   "special_function": "module:WillModule"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/bin/will_ruby_raise"
+        ,   "file_line": 9
+        ,   "function_name": "times"
+        }
+      , {   "file_name": "/usr/bin/will_ruby_raise"
+        ,   "file_line": 9
+        ,   "special_function": "module:WillModule"
+        }
+      , {   "file_name": "/usr/bin/will_ruby_raise"
+        ,   "file_line": 8
+        ,   "special_function": "main"
+        } ]
+}
\ No newline at end of file
diff --git a/tests/ruby_stacktraces/ruby-02 b/tests/ruby_stacktraces/ruby-02
new file mode 100644
index 0000000..dd24a02
--- /dev/null
+++ b/tests/ruby_stacktraces/ruby-02
@@ -0,0 +1,37 @@
+/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/class_module.rb:173:in `aref_prefix': missing aref_prefix for RDoc::SingleClass (NotImplementedError)
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/class_module.rb:181:in `aref'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/template/darkfish/class.rhtml:47:in `block in generate_class'
+	from /usr/share/ruby/erb.rb:850:in `eval'
+	from /usr/share/ruby/erb.rb:850:in `result'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb:724:in `template_result'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb:703:in `block in render_template'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb:698:in `open'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb:698:in `open'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb:698:in `render_template'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb:351:in `generate_class'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb:371:in `block in generate_class_files'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb:368:in `each'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb:368:in `generate_class_files'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb:245:in `generate'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb:137:in `block in document'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb:134:in `chdir'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb:134:in `document'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb:202:in `generate'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb:56:in `block in generation_hook'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb:55:in `each'
+	from /usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb:55:in `generation_hook'
+	from /usr/local/share/ruby/site_ruby/rubygems/request_set.rb:188:in `call'
+	from /usr/local/share/ruby/site_ruby/rubygems/request_set.rb:188:in `block in install'
+	from /usr/local/share/ruby/site_ruby/rubygems/request_set.rb:187:in `each'
+	from /usr/local/share/ruby/site_ruby/rubygems/request_set.rb:187:in `install'
+	from /usr/local/share/ruby/site_ruby/rubygems/dependency_installer.rb:394:in `install'
+	from /usr/local/share/ruby/site_ruby/rubygems/commands/update_command.rb:211:in `update_gem'
+	from /usr/local/share/ruby/site_ruby/rubygems/commands/update_command.rb:223:in `block in update_gems'
+	from /usr/local/share/ruby/site_ruby/rubygems/commands/update_command.rb:222:in `each'
+	from /usr/local/share/ruby/site_ruby/rubygems/commands/update_command.rb:222:in `update_gems'
+	from /usr/local/share/ruby/site_ruby/rubygems/commands/update_command.rb:98:in `execute'
+	from /usr/local/share/ruby/site_ruby/rubygems/command.rb:307:in `invoke_with_build_args'
+	from /usr/local/share/ruby/site_ruby/rubygems/command_manager.rb:168:in `process_args'
+	from /usr/local/share/ruby/site_ruby/rubygems/command_manager.rb:138:in `run'
+	from /usr/local/share/ruby/site_ruby/rubygems/gem_runner.rb:54:in `run'
+	from /bin/gem:21:in `<main>'
\ No newline at end of file
diff --git a/tests/ruby_stacktraces/ruby-02-expected-json b/tests/ruby_stacktraces/ruby-02-expected-json
new file mode 100644
index 0000000..1bef95c
--- /dev/null
+++ b/tests/ruby_stacktraces/ruby-02-expected-json
@@ -0,0 +1,158 @@
+{   "exception_name": "NotImplementedError"
+,   "stacktrace":
+      [ {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/class_module.rb"
+        ,   "file_line": 173
+        ,   "function_name": "aref_prefix"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/class_module.rb"
+        ,   "file_line": 181
+        ,   "function_name": "aref"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/template/darkfish/class.rhtml"
+        ,   "file_line": 47
+        ,   "function_name": "generate_class"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/share/ruby/erb.rb"
+        ,   "file_line": 850
+        ,   "function_name": "eval"
+        }
+      , {   "file_name": "/usr/share/ruby/erb.rb"
+        ,   "file_line": 850
+        ,   "function_name": "result"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb"
+        ,   "file_line": 724
+        ,   "function_name": "template_result"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb"
+        ,   "file_line": 703
+        ,   "function_name": "render_template"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb"
+        ,   "file_line": 698
+        ,   "function_name": "open"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb"
+        ,   "file_line": 698
+        ,   "function_name": "open"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb"
+        ,   "file_line": 698
+        ,   "function_name": "render_template"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb"
+        ,   "file_line": 351
+        ,   "function_name": "generate_class"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb"
+        ,   "file_line": 371
+        ,   "function_name": "generate_class_files"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb"
+        ,   "file_line": 368
+        ,   "function_name": "each"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb"
+        ,   "file_line": 368
+        ,   "function_name": "generate_class_files"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/generator/darkfish.rb"
+        ,   "file_line": 245
+        ,   "function_name": "generate"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb"
+        ,   "file_line": 137
+        ,   "function_name": "document"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb"
+        ,   "file_line": 134
+        ,   "function_name": "chdir"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb"
+        ,   "file_line": 134
+        ,   "function_name": "document"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb"
+        ,   "file_line": 202
+        ,   "function_name": "generate"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb"
+        ,   "file_line": 56
+        ,   "function_name": "generation_hook"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb"
+        ,   "file_line": 55
+        ,   "function_name": "each"
+        }
+      , {   "file_name": "/usr/share/gems/gems/rdoc-4.1.1/lib/rdoc/rubygems_hook.rb"
+        ,   "file_line": 55
+        ,   "function_name": "generation_hook"
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/request_set.rb"
+        ,   "file_line": 188
+        ,   "function_name": "call"
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/request_set.rb"
+        ,   "file_line": 188
+        ,   "function_name": "install"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/request_set.rb"
+        ,   "file_line": 187
+        ,   "function_name": "each"
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/request_set.rb"
+        ,   "file_line": 187
+        ,   "function_name": "install"
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/dependency_installer.rb"
+        ,   "file_line": 394
+        ,   "function_name": "install"
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/commands/update_command.rb"
+        ,   "file_line": 211
+        ,   "function_name": "update_gem"
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/commands/update_command.rb"
+        ,   "file_line": 223
+        ,   "function_name": "update_gems"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/commands/update_command.rb"
+        ,   "file_line": 222
+        ,   "function_name": "each"
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/commands/update_command.rb"
+        ,   "file_line": 222
+        ,   "function_name": "update_gems"
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/commands/update_command.rb"
+        ,   "file_line": 98
+        ,   "function_name": "execute"
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/command.rb"
+        ,   "file_line": 307
+        ,   "function_name": "invoke_with_build_args"
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/command_manager.rb"
+        ,   "file_line": 168
+        ,   "function_name": "process_args"
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/command_manager.rb"
+        ,   "file_line": 138
+        ,   "function_name": "run"
+        }
+      , {   "file_name": "/usr/local/share/ruby/site_ruby/rubygems/gem_runner.rb"
+        ,   "file_line": 54
+        ,   "function_name": "run"
+        }
+      , {   "file_name": "/bin/gem"
+        ,   "file_line": 21
+        ,   "special_function": "main"
+        } ]
+}
\ No newline at end of file
diff --git a/tests/ruby_stacktraces/ruby-03 b/tests/ruby_stacktraces/ruby-03
new file mode 100644
index 0000000..d931254
--- /dev/null
+++ b/tests/ruby_stacktraces/ruby-03
@@ -0,0 +1,4 @@
+/usr/share/gems/gems/openshift-origin-node-1.18.0.1/lib/openshift-origin-node/utils/tc.rb:103:in `get_interface_mtu': Unable to determine external network interface IP address. (RuntimeError)
+	from /usr/share/gems/gems/openshift-origin-node-1.18.0.1/lib/openshift-origin-node/utils/tc.rb:77:in `initialize'
+	from /sbin/oo-admin-ctl-tc:24:in `new'
+	from /sbin/oo-admin-ctl-tc:24:in `<main>'
\ No newline at end of file
diff --git a/tests/ruby_stacktraces/ruby-03-expected-json b/tests/ruby_stacktraces/ruby-03-expected-json
new file mode 100644
index 0000000..a1fa1f8
--- /dev/null
+++ b/tests/ruby_stacktraces/ruby-03-expected-json
@@ -0,0 +1,19 @@
+{   "exception_name": "RuntimeError"
+,   "stacktrace":
+      [ {   "file_name": "/usr/share/gems/gems/openshift-origin-node-1.18.0.1/lib/openshift-origin-node/utils/tc.rb"
+        ,   "file_line": 103
+        ,   "function_name": "get_interface_mtu"
+        }
+      , {   "file_name": "/usr/share/gems/gems/openshift-origin-node-1.18.0.1/lib/openshift-origin-node/utils/tc.rb"
+        ,   "file_line": 77
+        ,   "function_name": "initialize"
+        }
+      , {   "file_name": "/sbin/oo-admin-ctl-tc"
+        ,   "file_line": 24
+        ,   "function_name": "new"
+        }
+      , {   "file_name": "/sbin/oo-admin-ctl-tc"
+        ,   "file_line": 24
+        ,   "special_function": "main"
+        } ]
+}
\ No newline at end of file
diff --git a/tests/ruby_stacktraces/ruby-04 b/tests/ruby_stacktraces/ruby-04
new file mode 100644
index 0000000..12388a7
--- /dev/null
+++ b/tests/ruby_stacktraces/ruby-04
@@ -0,0 +1,39 @@
+/usr/share/rubygems/rubygems/basic_specification.rb:50:in `block (3 levels) in contains_requirable_file?': SIGTERM (SignalException)
+	from /usr/share/rubygems/rubygems/basic_specification.rb:50:in `each'
+	from /usr/share/rubygems/rubygems/basic_specification.rb:50:in `any?'
+	from /usr/share/rubygems/rubygems/basic_specification.rb:50:in `block (2 levels) in contains_requirable_file?'
+	from /usr/share/rubygems/rubygems/basic_specification.rb:49:in `each'
+	from /usr/share/rubygems/rubygems/basic_specification.rb:49:in `any?'
+	from /usr/share/rubygems/rubygems/basic_specification.rb:49:in `block in contains_requirable_file?'
+	from /usr/share/rubygems/rubygems/basic_specification.rb:45:in `each'
+	from /usr/share/rubygems/rubygems/basic_specification.rb:45:in `any?'
+	from /usr/share/rubygems/rubygems/basic_specification.rb:45:in `contains_requirable_file?'
+	from /usr/share/rubygems/rubygems/specification.rb:898:in `block in find_inactive_by_path'
+	from /usr/share/rubygems/rubygems/specification.rb:897:in `each'
+	from /usr/share/rubygems/rubygems/specification.rb:897:in `find'
+	from /usr/share/rubygems/rubygems/specification.rb:897:in `find_inactive_by_path'
+	from /usr/share/rubygems/rubygems.rb:183:in `try_activate'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:132:in `rescue in require'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:144:in `require'
+	from /usr/share/gems/gems/compass-0.12.2/lib/compass/sass_extensions/sprites/engines/chunky_png_engine.rb:2:in `<top (required)>'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+	from /usr/share/gems/gems/compass-0.12.2/lib/compass/sass_extensions/sprites/engines.rb:25:in `<top (required)>'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+	from /usr/share/gems/gems/compass-0.12.2/lib/compass/sass_extensions/sprites.rb:18:in `<top (required)>'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+	from /usr/share/gems/gems/compass-0.12.2/lib/compass/sass_extensions.rb:10:in `<top (required)>'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+	from /usr/share/gems/gems/compass-0.12.2/lib/compass.rb:5:in `block in <top (required)>'
+	from /usr/share/gems/gems/compass-0.12.2/lib/compass.rb:4:in `each'
+	from /usr/share/gems/gems/compass-0.12.2/lib/compass.rb:4:in `<top (required)>'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+	from /usr/share/rubygems/rubygems/core_ext/kernel_require.rb:55:in `require'
+	from /usr/share/gems/gems/compass-0.12.2/bin/compass:20:in `block in <top (required)>'
+	from /usr/share/gems/gems/compass-0.12.2/bin/compass:8:in `fallback_load_path'
+	from /usr/share/gems/gems/compass-0.12.2/bin/compass:19:in `<top (required)>'
+	from /usr/bin/compass:23:in `load'
+	from /usr/bin/compass:23:in `<main>'
\ No newline at end of file
diff --git a/tests/ruby_stacktraces/ruby-04-expected-json b/tests/ruby_stacktraces/ruby-04-expected-json
new file mode 100644
index 0000000..22bca55
--- /dev/null
+++ b/tests/ruby_stacktraces/ruby-04-expected-json
@@ -0,0 +1,166 @@
+{   "exception_name": "SignalException"
+,   "stacktrace":
+      [ {   "file_name": "/usr/share/rubygems/rubygems/basic_specification.rb"
+        ,   "file_line": 50
+        ,   "function_name": "contains_requirable_file?"
+        ,   "block_level": 3
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/basic_specification.rb"
+        ,   "file_line": 50
+        ,   "function_name": "each"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/basic_specification.rb"
+        ,   "file_line": 50
+        ,   "function_name": "any?"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/basic_specification.rb"
+        ,   "file_line": 50
+        ,   "function_name": "contains_requirable_file?"
+        ,   "block_level": 2
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/basic_specification.rb"
+        ,   "file_line": 49
+        ,   "function_name": "each"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/basic_specification.rb"
+        ,   "file_line": 49
+        ,   "function_name": "any?"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/basic_specification.rb"
+        ,   "file_line": 49
+        ,   "function_name": "contains_requirable_file?"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/basic_specification.rb"
+        ,   "file_line": 45
+        ,   "function_name": "each"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/basic_specification.rb"
+        ,   "file_line": 45
+        ,   "function_name": "any?"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/basic_specification.rb"
+        ,   "file_line": 45
+        ,   "function_name": "contains_requirable_file?"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/specification.rb"
+        ,   "file_line": 898
+        ,   "function_name": "find_inactive_by_path"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/specification.rb"
+        ,   "file_line": 897
+        ,   "function_name": "each"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/specification.rb"
+        ,   "file_line": 897
+        ,   "function_name": "find"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/specification.rb"
+        ,   "file_line": 897
+        ,   "function_name": "find_inactive_by_path"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems.rb"
+        ,   "file_line": 183
+        ,   "function_name": "try_activate"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 132
+        ,   "function_name": "require"
+        ,   "rescue_level": 1
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 144
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/share/gems/gems/compass-0.12.2/lib/compass/sass_extensions/sprites/engines/chunky_png_engine.rb"
+        ,   "file_line": 2
+        ,   "special_function": "top (required)"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 55
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 55
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/share/gems/gems/compass-0.12.2/lib/compass/sass_extensions/sprites/engines.rb"
+        ,   "file_line": 25
+        ,   "special_function": "top (required)"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 55
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 55
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/share/gems/gems/compass-0.12.2/lib/compass/sass_extensions/sprites.rb"
+        ,   "file_line": 18
+        ,   "special_function": "top (required)"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 55
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 55
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/share/gems/gems/compass-0.12.2/lib/compass/sass_extensions.rb"
+        ,   "file_line": 10
+        ,   "special_function": "top (required)"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 55
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 55
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/share/gems/gems/compass-0.12.2/lib/compass.rb"
+        ,   "file_line": 5
+        ,   "special_function": "top (required)"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/share/gems/gems/compass-0.12.2/lib/compass.rb"
+        ,   "file_line": 4
+        ,   "function_name": "each"
+        }
+      , {   "file_name": "/usr/share/gems/gems/compass-0.12.2/lib/compass.rb"
+        ,   "file_line": 4
+        ,   "special_function": "top (required)"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 55
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/share/rubygems/rubygems/core_ext/kernel_require.rb"
+        ,   "file_line": 55
+        ,   "function_name": "require"
+        }
+      , {   "file_name": "/usr/share/gems/gems/compass-0.12.2/bin/compass"
+        ,   "file_line": 20
+        ,   "special_function": "top (required)"
+        ,   "block_level": 1
+        }
+      , {   "file_name": "/usr/share/gems/gems/compass-0.12.2/bin/compass"
+        ,   "file_line": 8
+        ,   "function_name": "fallback_load_path"
+        }
+      , {   "file_name": "/usr/share/gems/gems/compass-0.12.2/bin/compass"
+        ,   "file_line": 19
+        ,   "special_function": "top (required)"
+        }
+      , {   "file_name": "/usr/bin/compass"
+        ,   "file_line": 23
+        ,   "function_name": "load"
+        }
+      , {   "file_name": "/usr/bin/compass"
+        ,   "file_line": 23
+        ,   "special_function": "main"
+        } ]
+}
\ No newline at end of file
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 904fe64..f27ed13 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -15,6 +15,8 @@ m4_include([koops_stacktrace.at])
 m4_include([core_frame.at])
 m4_include([core_thread.at])
 m4_include([core_stacktrace.at])
+m4_include([ruby_frame.at])
+m4_include([ruby_stacktrace.at])
 m4_include([operating_system.at])
 m4_include([normalize.at])
 m4_include([metrics.at])
-- 
1.8.3.1