Skip to content

Commit

Permalink
Merge branch 'jruby-1_7'
Browse files Browse the repository at this point in the history
* jruby-1_7:
  FrameType's INTERPRETER_ constants are a leaky abstraction - deprecate
  for now we do not care about duplicate .rb frames in re-thrown Java exception
  occupy less space with FrameType's static collections
  deprecate no longer used Helpers.invokeMethodMissing method
  toArray one-liner
  add a test reproducing backtrace issue #3177
  improve backtrace rewriting when native exception is re-thrown (fixes #3177)
  more test_backtraces TODO left-overs ... these are passing just fine as well
  removed TODOs in test_backtraces - they seem to be all passing
  cleanup test_backtraces
  save an interim builder+string with mangleStringForCleanJavaIdentifier + guess string builder length for possibly less internal array resizing
  avoid 'too much' of try-catch IOException - throw runtime error directly
  minor name mangler cleanup + "micro opts" - slightly less code to execute
  cleanup JRubyFile

Conflicts:
	core/src/main/java/org/jruby/runtime/backtrace/BacktraceData.java
	core/src/main/java/org/jruby/runtime/backtrace/FrameType.java
	core/src/main/java/org/jruby/util/JRubyFile.java
	test/test_backtraces.rb
kares committed Aug 10, 2015
2 parents 1b29283 + 6a8d8cc commit c9e67b3
Showing 7 changed files with 329 additions and 284 deletions.
2 changes: 1 addition & 1 deletion core/src/main/java/org/jruby/runtime/Helpers.java
Original file line number Diff line number Diff line change
@@ -152,7 +152,7 @@ public static RubyClass getSingletonClass(Ruby runtime, IRubyObject receiver) {
}
}

// TODO: Only used by interface implementation; eliminate it
@Deprecated // no longer used - only been used by interface implementation
public static IRubyObject invokeMethodMissing(IRubyObject receiver, String name, IRubyObject[] args) {
ThreadContext context = receiver.getRuntime().getCurrentContext();

48 changes: 18 additions & 30 deletions core/src/main/java/org/jruby/runtime/backtrace/BacktraceData.java
Original file line number Diff line number Diff line change
@@ -83,38 +83,28 @@ private RubyStackTraceElement[] constructBacktrace(Map<String, Map<String, Strin
}

// Java-based Ruby core methods
String rubyName = null;
if (
fullTrace || // full traces show all elements
(rubyName = getBoundMethodName(boundMethods, className, methodName)) != null // if a bound Java impl, always show
) {

if (rubyName == null) rubyName = methodName;
String rubyName = methodName; // when fullTrace == true
if ( fullTrace || // full traces show all elements
( rubyName = getBoundMethodName(boundMethods, className, methodName) ) != null ) { // if a bound Java impl, always show

// add package to filename
filename = packagedFilenameFromElement(filename, className);

// mask .java frames out for e.g. Kernel#caller
if (maskNative) {
// for Kernel#caller, don't show .java frames in the trace
dupFrame = true;
dupFrameName = rubyName;
continue;
dupFrame = true; dupFrameName = rubyName; continue;
}

// construct Ruby trace element
trace.add(new RubyStackTraceElement(className, rubyName, filename, line, false));

// if not full trace, we're done; don't check interpreted marker
if (!fullTrace) {
continue;
}
if ( ! fullTrace ) continue;
}

// Interpreted frames
if (rubyFrameIndex >= 0 &&
FrameType.INTERPRETED_CLASSES.contains(className) &&
FrameType.INTERPRETED_FRAMES.containsKey(methodName)) {
if ( rubyFrameIndex >= 0 && FrameType.isInterpreterFrame(className, methodName) ) {

// pop interpreter frame
BacktraceElement rubyFrame = rubyTrace[rubyFrameIndex--];
@@ -146,18 +136,12 @@ private RubyStackTraceElement[] constructBacktrace(Map<String, Map<String, Strin

// if all else fails and this is a non-JRuby element we want to include, add it
if (includeNonFiltered && !isFilteredClass(className)) {
trace.add(new RubyStackTraceElement(
className,
methodName,
packagedFilenameFromElement(filename, className),
line,
false
));
filename = packagedFilenameFromElement(filename, className);
trace.add(new RubyStackTraceElement(className, methodName, filename, line, false));
}
}

RubyStackTraceElement[] rubyStackTrace = new RubyStackTraceElement[trace.size()];
return trace.toArray(rubyStackTrace);
return trace.toArray(new RubyStackTraceElement[trace.size()]);
}

public static String getBoundMethodName(Map<String,Map<String,String>> boundMethods, String className, String methodName) {
@@ -168,15 +152,19 @@ public static String getBoundMethodName(Map<String,Map<String,String>> boundMeth
return javaToRuby.get(methodName);
}

private static String packagedFilenameFromElement(String filename, String className) {
private static String packagedFilenameFromElement(final String filename, final String className) {
// stick package on the beginning
if (filename == null) {
return className.replace('.', '/');
}
if (filename == null) return className.replace('.', '/');

int lastDot = className.lastIndexOf('.');
if (lastDot == -1) return filename;
return className.substring(0, lastDot + 1).replace('.', '/') + filename;

final String pkgPath = className.substring(0, lastDot + 1).replace('.', '/');
// in case a native exception is re-thrown we might end-up rewriting twice e.g. :
// 1st time className = org.jruby.RubyArray filename = RubyArray.java
// 2nd time className = org.jruby.RubyArray filename = org/jruby/RubyArray.java
if (filename.indexOf('/') > -1 && filename.startsWith(pkgPath)) return filename;
return pkgPath + filename;
}

// ^(org\\.jruby)|(sun\\.reflect)
16 changes: 14 additions & 2 deletions core/src/main/java/org/jruby/runtime/backtrace/FrameType.java
Original file line number Diff line number Diff line change
@@ -14,8 +14,10 @@

public enum FrameType {
METHOD, BLOCK, EVAL, CLASS, MODULE, METACLASS, ROOT;
public static final Set<String> INTERPRETED_CLASSES = new HashSet<String>();
public static final Map<String, FrameType> INTERPRETED_FRAMES = new HashMap<String, FrameType>();
@Deprecated // no longer accessed directly
public static final Set<String> INTERPRETED_CLASSES = new HashSet<String>(4, 1);
@Deprecated // no longer accessed directly
public static final Map<String, FrameType> INTERPRETED_FRAMES = new HashMap<String, FrameType>(8, 1);

static {
INTERPRETED_CLASSES.add(Interpreter.class.getName());
@@ -31,4 +33,14 @@ public enum FrameType {
INTERPRETED_FRAMES.put("INTERPRET_BLOCK", FrameType.BLOCK);
INTERPRETED_FRAMES.put("INTERPRET_ROOT", FrameType.ROOT);
}

public static boolean isInterpreterFrame(final String className, final String methodName) {
return INTERPRETED_CLASSES.contains(className) &&
INTERPRETED_FRAMES.containsKey(methodName);
}

public static FrameType getInterpreterFrame(final String methodName) {
return INTERPRETED_FRAMES.get(methodName);
}

}
79 changes: 36 additions & 43 deletions core/src/main/java/org/jruby/util/JRubyFile.java
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
* rights and limitations under the License.
*
* Copyright (C) 2006 Ola Bini <ola@ologix.com>
*
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
@@ -35,20 +35,15 @@
import java.io.FilenameFilter;
import java.io.IOException;

import jnr.posix.JavaSecuredFile;
import jnr.posix.POSIX;
import org.jruby.Ruby;
import org.jruby.RubyFile;
import org.jruby.runtime.ThreadContext;

import jnr.posix.JavaSecuredFile;
import org.jruby.Ruby;
import org.jruby.platform.Platform;
import java.util.jar.JarFile;
import java.util.jar.JarEntry;
import java.util.zip.ZipEntry;
import java.io.IOException;
import org.jruby.runtime.ThreadContext;

/**
* <p>This file acts as an alternative to NormalizedFile, due to the problems with current working
* <p>This file acts as an alternative to NormalizedFile, due to the problems with current working
* directory.</p>
*
*/
@@ -93,7 +88,7 @@ private static FileResource createResource(Ruby runtime, String cwd, String path
return new NullDeviceResource(runtime.getPosix());
}

if (pathname.contains(":")) { // scheme-oriented resources
if (pathname.indexOf(':') > 0) { // scheme-oriented resources
if (pathname.startsWith("classpath:")) {
pathname = pathname.replace("classpath:", "uri:classloader:/");
}
@@ -104,13 +99,13 @@ private static FileResource createResource(Ruby runtime, String cwd, String path
if (pathname.startsWith("file:")) {
pathname = pathname.substring(5);

if ("".equals(pathname)) return EmptyFileResource.create(pathname);
if (pathname.length() == 0) return EmptyFileResource.create(pathname);
}
}

File internal = new JavaSecuredFile(pathname);
if (cwd != null && !internal.isAbsolute() && (cwd.startsWith("uri:") || cwd.startsWith("file:"))) {
return createResource(runtime, null, cwd + "/" + pathname);
return createResource(runtime, null, cwd + '/' + pathname);
}

// If any other special resource types fail, count it as a filesystem backed resource.
@@ -121,13 +116,12 @@ private static FileResource createResource(Ruby runtime, String cwd, String path
public static String normalizeSeps(String path) {
if (Platform.IS_WINDOWS) {
return path.replace(File.separatorChar, '/');
} else {
return path;
}
return path;
}

private static JRubyFile createNoUnicodeConversion(String cwd, String pathname) {
if (pathname == null || pathname.equals("") || Ruby.isSecurityRestricted()) {
if (pathname == null || pathname.length() == 0 || Ruby.isSecurityRestricted()) {
return JRubyNonExistentFile.NOT_EXIST;
}
if(pathname.startsWith("file:")) {
@@ -138,7 +132,7 @@ private static JRubyFile createNoUnicodeConversion(String cwd, String pathname)
return new JRubyFile(internal);
}
if(cwd != null && cwd.startsWith("uri:") && !pathname.startsWith("uri:") && !pathname.contains("!/")) {
return new JRubyFile(cwd + "/" + pathname);
return new JRubyFile(cwd + '/' + pathname);
}
internal = new JavaSecuredFile(cwd, pathname);
if(!internal.isAbsolute()) {
@@ -161,29 +155,31 @@ protected JRubyFile(String filename) {

@Override
public String getAbsolutePath() {
if(super.getPath().startsWith("uri:")) {
final String path = super.getPath();
if (path.startsWith("uri:")) {
// TODO better do not collapse // to / for uri: files
return super.getPath().replaceFirst(":/([^/])", "://$1" );
return path.replaceFirst(":/([^/])", "://$1" );
}
return normalizeSeps(new File(super.getPath()).getAbsolutePath());
return normalizeSeps(new File(path).getAbsolutePath());
}

@Override
public String getCanonicalPath() throws IOException {
try {
return normalizeSeps(super.getCanonicalPath());
} catch (IOException e) {
// usually IOExceptions don't tell us anything about the path,
// so add an extra wrapper to give more debugging help.
throw (IOException) new IOException("Unable to canonicalize path: " + getAbsolutePath()).initCause(e);
}
try {
return normalizeSeps(super.getCanonicalPath());
}
catch (IOException e) {
// usually IOExceptions don't tell us anything about the path,
// so add an extra wrapper to give more debugging help.
throw new IOException("Unable to canonicalize path: " + getAbsolutePath(), e);
}
}

@Override
public String getPath() {
return normalizeSeps(super.getPath());
}

@Override
public String toString() {
return normalizeSeps(super.toString());
@@ -201,21 +197,18 @@ public File getCanonicalFile() throws IOException {

@Override
public String getParent() {
String par = super.getParent();
if (par != null) {
par = normalizeSeps(par);
String parent = super.getParent();
if (parent != null) {
parent = normalizeSeps(parent);
}
return par;
return parent;
}

@Override
public File getParentFile() {
String par = getParent();
if (par == null) {
return this;
} else {
return new JRubyFile(par);
}
String parent = getParent();
if (parent == null) return this;
return new JRubyFile(parent);
}

public static File[] listRoots() {
@@ -255,7 +248,7 @@ public File[] listFiles() {
if (files == null) {
return null;
}

JRubyFile[] smartFiles = new JRubyFile[files.length];
for (int i = 0, j = files.length; i < j; i++) {
smartFiles[i] = createNoUnicodeConversion(super.getAbsolutePath(), files[i].getPath());
@@ -269,7 +262,7 @@ public File[] listFiles(final FileFilter filter) {
if (files == null) {
return null;
}

JRubyFile[] smartFiles = new JRubyFile[files.length];
for (int i = 0,j = files.length; i < j; i++) {
smartFiles[i] = createNoUnicodeConversion(super.getAbsolutePath(), files[i].getPath());
@@ -283,7 +276,7 @@ public File[] listFiles(final FilenameFilter filter) {
if (files == null) {
return null;
}

JRubyFile[] smartFiles = new JRubyFile[files.length];
for (int i = 0,j = files.length; i < j; i++) {
smartFiles[i] = createNoUnicodeConversion(super.getAbsolutePath(), files[i].getPath());
274 changes: 141 additions & 133 deletions core/src/main/java/org/jruby/util/JavaNameMangler.java
Original file line number Diff line number Diff line change
@@ -14,7 +14,6 @@
import org.jruby.ir.IRScriptBody;
import org.jruby.platform.Platform;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.regex.Pattern;

@@ -26,8 +25,8 @@ public class JavaNameMangler {
public static final Pattern PATH_SPLIT = Pattern.compile("[/\\\\]");

public static String mangledFilenameForStartupClasspath(String filename) {
if (filename.equals("-e")) {
return "ruby/__dash_e__";
if (filename.length() == 2 && filename.charAt(0) == '-' && filename.charAt(1) == 'e') {
return "ruby/__dash_e__"; // "-e"
}

return mangleFilenameForClasspath(filename, null, "", false, false);
@@ -43,160 +42,167 @@ public static String mangleFilenameForClasspath(String filename, String parent,

public static String mangleFilenameForClasspath(String filename, String parent, String prefix, boolean canonicalize,
boolean preserveIdentifiers) {
try {
String classPath = "";
if(filename.indexOf("!") != -1) {
String before = filename.substring(6, filename.indexOf("!"));
String classPath; final int idx = filename.indexOf('!');
if (idx != -1) {
String before = filename.substring(6, idx);
try {
if (canonicalize) {
classPath = new JRubyFile(before + filename.substring(filename.indexOf("!")+1)).getCanonicalPath().toString();
classPath = new JRubyFile(before + filename.substring(idx + 1)).getCanonicalPath();
} else {
classPath = new JRubyFile(before + filename.substring(filename.indexOf("!")+1)).toString();
classPath = new JRubyFile(before + filename.substring(idx + 1)).toString();
}
} else {
try {
if (canonicalize) {
classPath = new JRubyFile(filename).getCanonicalPath().toString();
} else {
classPath = new JRubyFile(filename).toString();
}
} catch (IOException ioe) {
// could not get canonical path, just use given path
classPath = filename;
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
} else {
try {
if (canonicalize) {
classPath = new JRubyFile(filename).getCanonicalPath();
} else {
classPath = new JRubyFile(filename).toString();
}
} catch (IOException ioe) {
// could not get canonical path, just use given path
classPath = filename;
}
}

if (parent != null && parent.length() > 0) {
String parentPath;
try {
if (canonicalize) {
parentPath = new JRubyFile(parent).getCanonicalPath().toString();
} else {
parentPath = new JRubyFile(parent).toString();
}
} catch (IOException ioe) {
// could not get canonical path, just use given path
parentPath = parent;
}
if (!classPath.startsWith(parentPath)) {
throw new FileNotFoundException("File path " + classPath +
" does not start with parent path " + parentPath);
if (parent != null && parent.length() > 0) {
String parentPath;
try {
if (canonicalize) {
parentPath = new JRubyFile(parent).getCanonicalPath();
} else {
parentPath = new JRubyFile(parent).toString();
}
int parentLength = parentPath.length();
classPath = classPath.substring(parentLength);
} catch (IOException ioe) {
// could not get canonical path, just use given path
parentPath = parent;
}
if (!classPath.startsWith(parentPath)) {
throw new RuntimeException("File path " + classPath +
" does not start with parent path " + parentPath);
}
int parentLength = parentPath.length();
classPath = classPath.substring(parentLength);
}

String[] pathElements = PATH_SPLIT.split(classPath);
StringBuilder newPath = new StringBuilder(prefix);

for (String element : pathElements) {
if (element.length() <= 0) {
continue;
}
String[] pathElements = PATH_SPLIT.split(classPath);
StringBuilder newPath = new StringBuilder(classPath.length() + 16).append(prefix);

if (newPath.length() > 0) {
newPath.append("/");
}
for (String element : pathElements) {
if (element.length() <= 0) {
continue;
}

if (!Character.isJavaIdentifierStart(element.charAt(0))) {
newPath.append("$");
}
if (newPath.length() > 0) {
newPath.append('/');
}

String pathId = element;
if (!preserveIdentifiers) {
pathId = mangleStringForCleanJavaIdentifier(element);
}
newPath.append(pathId);
if (!Character.isJavaIdentifierStart(element.charAt(0))) {
newPath.append('$');
}

// strip off "_dot_rb" for .rb files
int dotRbIndex = newPath.indexOf("_dot_rb");
if (dotRbIndex != -1 && dotRbIndex == newPath.length() - 7) {
newPath.delete(dotRbIndex, dotRbIndex + 7);
if (!preserveIdentifiers) {
mangleStringForCleanJavaIdentifier(newPath, element);
}
else {
newPath.append(element);
}
}

return newPath.toString();
} catch (IOException ioe) {
ioe.printStackTrace();
throw new RuntimeException(ioe);
// strip off "_dot_rb" for .rb files
int dotRbIndex = newPath.indexOf("_dot_rb");
if (dotRbIndex != -1 && dotRbIndex == newPath.length() - 7) {
newPath.delete(dotRbIndex, dotRbIndex + 7);
}

return newPath.toString();
}

public static String mangleStringForCleanJavaIdentifier(final String name) {
StringBuilder cleanBuffer = new StringBuilder(name.length() * 3);
mangleStringForCleanJavaIdentifier(cleanBuffer, name);
return cleanBuffer.toString();
}

public static String mangleStringForCleanJavaIdentifier(String name) {
char[] characters = name.toCharArray();
StringBuilder cleanBuffer = new StringBuilder();
private static void mangleStringForCleanJavaIdentifier(final StringBuilder buffer,
final String name) {
final char[] chars = name.toCharArray();
final int len = chars.length;
buffer.ensureCapacity(buffer.length() + len * 2);
boolean prevWasReplaced = false;
for (int i = 0; i < characters.length; i++) {
if ((i == 0 && Character.isJavaIdentifierStart(characters[i]))
|| Character.isJavaIdentifierPart(characters[i])) {
cleanBuffer.append(characters[i]);
for (int i = 0; i < len; i++) {
if ((i == 0 && Character.isJavaIdentifierStart(chars[i]))
|| Character.isJavaIdentifierPart(chars[i])) {
buffer.append(chars[i]);
prevWasReplaced = false;
} else {
if (!prevWasReplaced) {
cleanBuffer.append("_");
}
prevWasReplaced = true;
switch (characters[i]) {
case '?':
cleanBuffer.append("p_");
continue;
case '!':
cleanBuffer.append("b_");
continue;
case '<':
cleanBuffer.append("lt_");
continue;
case '>':
cleanBuffer.append("gt_");
continue;
case '=':
cleanBuffer.append("equal_");
continue;
case '[':
if ((i + 1) < characters.length && characters[i + 1] == ']') {
cleanBuffer.append("aref_");
i++;
} else {
cleanBuffer.append("lbracket_");
}
continue;
case ']':
cleanBuffer.append("rbracket_");
continue;
case '+':
cleanBuffer.append("plus_");
continue;
case '-':
cleanBuffer.append("minus_");
continue;
case '*':
cleanBuffer.append("times_");
continue;
case '/':
cleanBuffer.append("div_");
continue;
case '&':
cleanBuffer.append("and_");
continue;
case '.':
cleanBuffer.append("dot_");
continue;
case '@':
cleanBuffer.append("at_");
default:
cleanBuffer.append(Integer.toHexString(characters[i])).append("_");
continue;
}

if (!prevWasReplaced) buffer.append('_');
prevWasReplaced = true;

switch (chars[i]) {
case '?':
buffer.append("p_");
continue;
case '!':
buffer.append("b_");
continue;
case '<':
buffer.append("lt_");
continue;
case '>':
buffer.append("gt_");
continue;
case '=':
buffer.append("equal_");
continue;
case '[':
if ((i + 1) < len && chars[i + 1] == ']') {
buffer.append("aref_");
i++;
} else {
buffer.append("lbracket_");
}
continue;
case ']':
buffer.append("rbracket_");
continue;
case '+':
buffer.append("plus_");
continue;
case '-':
buffer.append("minus_");
continue;
case '*':
buffer.append("times_");
continue;
case '/':
buffer.append("div_");
continue;
case '&':
buffer.append("and_");
continue;
case '.':
buffer.append("dot_");
continue;
case '@':
buffer.append("at_");
default:
buffer.append(Integer.toHexString(chars[i])).append('_');
}
}
return cleanBuffer.toString();
}

private static final String DANGEROUS_CHARS = "\\/.;:$[]<>";
private static final String REPLACEMENT_CHARS = "-|,?!%{}^_";
private static final char ESCAPE_C = '\\';
private static final char NULL_ESCAPE_C = '=';
private static final String NULL_ESCAPE = ESCAPE_C+""+NULL_ESCAPE_C;
private static final String NULL_ESCAPE = ESCAPE_C +""+ NULL_ESCAPE_C;

public static String mangleMethodName(String name) {
public static String mangleMethodName(final String name) {
// scan for characters that need escaping
StringBuilder builder = null; // lazy
for (int i = 0; i < name.length(); i++) {
@@ -209,8 +215,9 @@ public static String mangleMethodName(String name) {
builder.append(NULL_ESCAPE);
builder.append(name.substring(0, i));
}
builder.append(ESCAPE_C).append((char)escape);
} else if (builder != null) builder.append(candidate);
builder.append(ESCAPE_C).append((char) escape);
}
else if (builder != null) builder.append(candidate);
}

if (builder != null) return builder.toString();
@@ -220,21 +227,22 @@ public static String mangleMethodName(String name) {

public static String demangleMethodName(String name) {
if (!name.startsWith(NULL_ESCAPE)) return name;

StringBuilder builder = new StringBuilder();
for (int i = 2; i < name.length(); i++) {
final int len = name.length();
StringBuilder builder = new StringBuilder(len);
for (int i = 2; i < len; i++) {
char candidate = name.charAt(i);
if (candidate == ESCAPE_C) {
i++;
char escaped = name.charAt(i);
char unescape = unescapeChar(escaped);
builder.append(unescape);
} else builder.append(candidate);
}
else builder.append(candidate);
}

return builder.toString();
}

public static boolean willMethodMangleOk(String name) {
if (Platform.IS_IBM) {
// IBM's JVM is much less forgiving, so we disallow anythign with non-alphanumeric, _, and $
173 changes: 98 additions & 75 deletions test/jruby/test_backtraces.rb
Original file line number Diff line number Diff line change
@@ -6,53 +6,13 @@
# Behavior of MRI 1.9 is different:
# http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/15589
class TestBacktraces < Test::Unit::TestCase
def setup
@offset = nil
end

# Convenience method to obtain the exception,
# and to print the stack trace, if needed.
def get_exception(verbose=false)
begin
@get_exception_yield_line = __LINE__ + 1
yield
rescue Exception => ex
puts ex.backtrace.join("\n") if verbose
ex
end
end

# Main verification method that performs actual checks
# on the stacktraces.
def check(expectations, exception)
backtrace = []
expectations.strip.split("\n").each { |line|
line.strip!

# if line starts with +nnn, we prepend
# the current file and offset
md = line.match(/^\+(\d+)(:.*)/)
if (md)
flunk("@offset is not defined in the test case") unless @offset
# For JRuby, we soften this requirement, since native calls will
# show their actual .java file and line, rather than the caller.
#line = "#{__FILE__}:#{$1.to_i + @offset}#{$2}"
line = /.*:#{$1.to_i + @offset}#{$2}/
end
}
backtrace.each_with_index { |expected, idx|
# Soften, per above comment
#assert_equal(expected, exception.backtrace[idx])
assert expected =~ exception.backtrace[idx]
}
end

def test_simple_exception
@offset = __LINE__
raise RuntimeError.new("Test")
rescue Exception => ex
expectation = "+1:in `test_simple_exception'"
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

import org.jruby.test.TestHelper
@@ -66,7 +26,7 @@ def test_java_backtrace
flunk("test_java_backtrace not in backtrace")
end
end

def test_simple_exception_recursive
@offset = __LINE__
def meth(n)
@@ -81,7 +41,7 @@ def meth(n)
+4:in `meth'
+6:in `test_simple_exception_recursive'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

def test_native_exception_recursive
@@ -99,7 +59,7 @@ def meth(n)
+4:in `meth'
+6:in `test_native_exception_recursive'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

def test_exception_from_block
@@ -119,22 +79,22 @@ def bar
+2:in `foo'
+7:in `test_exception_from_block'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

def test_exception_from_for
array = [1,2,3,4,5]
@offset = __LINE__
for element in array
raise RuntimeError
for e in array
raise RuntimeError if e
end
rescue Exception => ex
expectation = %q{
+2:in `test_exception_from_for'
+1:in `each'
+1:in `test_exception_from_for'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

def test_exception_from_proc
@@ -149,7 +109,7 @@ def test_exception_from_proc
+3:in `call'
+3:in `test_exception_from_proc'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

def test_exception_from_lambda
@@ -164,32 +124,30 @@ def test_exception_from_lambda
+3:in `call'
+3:in `test_exception_from_lambda'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

# TODO: currently fails
def XXXtest_exception_from_array_plus
def test_exception_from_array_plus
@offset = __LINE__
[1,2,3] + 5
rescue Exception => ex
expectation = %q{
+1:in `+'
+1:in `test_exception_from_array_plus'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

# JRUBY-2138
# # TODO: currently fails
def XXXtest_exception_from_string_plus
def test_exception_from_string_plus
@offset = __LINE__
"hello" + nil
rescue Exception => ex
expectation = %q{
+1:in `+'
+1:in `test_exception_from_string_plus'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

def test_exception_from_string_sub
@@ -200,23 +158,21 @@ def test_exception_from_string_sub
+1:in `sub'
+1:in `test_exception_from_string_sub'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

# TODO: currently fails
def XXXtest_zero_devision_exception
def test_zero_devision_exception
@offset = __LINE__
1/0
rescue Exception => ex
expectation = %q{
+1:in `/'
+1:in `test_zero_devision_exception'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

# TODO: currently fails
def XXXtest_exeption_from_object_send
def test_exeption_from_object_send
@offset = __LINE__
"hello".__send__(:sub, /l/, 5)
rescue Exception => ex
@@ -225,20 +181,18 @@ def XXXtest_exeption_from_object_send
+1:in `__send__'
+1:in `test_exeption_from_object_send'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

# TODO: currently fails
def XXXtest_arity_exception
def test_arity_exception
@offset = __LINE__
"hello".sub
rescue Exception => ex
expectation = "+1:in `sub'"
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

# TODO: currently fails
def XXXtest_exception_from_eval
def test_exception_from_eval
ex = get_exception {
@offset = __LINE__
eval("raise RuntimeError.new")
@@ -249,11 +203,10 @@ def XXXtest_exception_from_eval
+1:in `test_exception_from_eval'
#{__FILE__}:#{@get_exception_yield_line}:in `get_exception'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

# TODO: currently fails
def XXXtest_exception_from_block_inside_eval
def test_exception_from_block_inside_eval
ex = get_exception {
@offset = __LINE__
eval("def foo; yield; end; foo { raise RuntimeError.new }")
@@ -266,7 +219,7 @@ def XXXtest_exception_from_block_inside_eval
+1:in `test_exception_from_block_inside_eval'
#{__FILE__}:#{@get_exception_yield_line}:in `get_exception'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
end

# JRUBY-2695
@@ -291,9 +244,79 @@ def test_exception_from_thread_with_abort_on_exception_true
expectation = %Q{
+2:in `test_exception_from_thread_with_abort_on_exception_true'
}
check(expectation, ex)
assert_exception_backtrace(expectation, ex)
ensure
Thread.abort_on_exception = false
$stderr = STDERR
end

def test_throwing_runnable_backtrace # GH-3177
fixnum_times_ = 'org.jruby.RubyFixnum.times(org/jruby/RubyFixnum.java:'
backtrace = nil

i = 0
throwing = org.jruby.javasupport.test.ThrowingRunnable.new do
1.times {
begin
throwing.doRun( (i += 1) > 0 )
rescue java.lang.Exception
assert e = $!.backtrace.find { |e| e.index('org.jruby.RubyFixnum.times') }
assert_equal fixnum_times_, e[ 0...fixnum_times_.size ]
backtrace = $!.backtrace.dup
raise
end
}
end

begin
throwing.doRun(false)
rescue java.lang.Exception
# puts $!.backtrace
# second rewriting of the same exception :
assert e = $!.backtrace.find { |e| e.index('org.jruby.RubyFixnum.times') }
assert_equal fixnum_times_, e[ 0...fixnum_times_.size ]
# NOTE back-trace gets duplicate .rb calls - seems not necessary to fix?!
# assert_equal backtrace, $!.backtrace # expect the same back-trace
else
fail 'expected to throw a java.lang.Exception'
end
end

private

# Convenience method to obtain the exception,
# and to print the stack trace, if needed.
def get_exception(verbose = false)
begin
@get_exception_yield_line = __LINE__ + 1
yield
rescue Exception => ex
puts ex.backtrace.join("\n") if verbose
ex
end
end

# Main verification method that performs actual checks
# on the stacktraces.
def assert_exception_backtrace(expectations, exception)
backtrace = []
expectations.strip.split("\n").each { |line|
line.strip!

# if line starts with +nnn, we prepend the current file and offset
if line.match /^\+(\d+)(:.*)/
flunk("@offset is not defined in the test case") unless @offset ||= nil
# For JRuby, we soften this requirement, since native calls will
# show their actual .java file and line, rather than the caller.
#line = "#{__FILE__}:#{$1.to_i + @offset}#{$2}"
line = /.*:#{$1.to_i + @offset}#{$2}/
end
}
backtrace.each_with_index { |expected, idx|
# Soften, per above comment
#assert_equal(expected, exception.backtrace[idx])
assert expected =~ exception.backtrace[idx]
}
end

end
21 changes: 21 additions & 0 deletions test/org/jruby/javasupport/test/ThrowingRunnable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.jruby.javasupport.test;

/**
* https://github.com/jruby/jruby/issues/3177
*
* @see test_backtraces.rb
*/
public class ThrowingRunnable {

final Runnable runnable;

public ThrowingRunnable(Runnable run) {
runnable = run;
}

public void doRun(boolean fail) throws Exception {
if (fail) throw new Exception();
runnable.run();
}

}

0 comments on commit c9e67b3

Please sign in to comment.