blob: 214508bfcdc5c1595ace0bb27be11593c985c55e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.junit;
import java.awt.EventQueue;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.management.LockInfo;
import java.lang.management.ManagementFactory;
import java.lang.management.MonitorInfo;
import java.lang.management.PlatformLoggingMXBean;
import java.lang.management.RuntimeMXBean;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.lang.ref.Reference;
import java.lang.reflect.Field;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import junit.framework.TestResult;
import org.netbeans.insane.live.LiveReferences;
import org.netbeans.insane.live.Path;
import org.netbeans.insane.scanner.CountingVisitor;
import org.netbeans.insane.scanner.ScannerUtils;
import org.netbeans.junit.diff.Diff;
import org.netbeans.junit.internal.MemoryPreferencesFactory;
import org.netbeans.junit.internal.NbModuleLogHandler;
/**
* NetBeans extension to JUnit's {@link TestCase}.
* Adds various abilities such as comparing golden files, getting a working
* directory for test files, testing memory usage, etc.
*/
public abstract class NbTestCase extends TestCase implements NbTest {
static {
MethodOrder.initialize();
}
/**
* active filter
*/
private Filter filter;
/** the amount of time the test was executing for
*/
private long time;
/** our working directory */
private String workDirPath;
/**
* Constructs a test case with the given name.
* Normally you will just use:
* <pre>
* public class MyTest extends NbTestCase {
* public MyTest(String name) {super(name);}
* public void testWhatever() {...}
* }
* </pre>
* @param name name of the test case
*/
public NbTestCase(String name) {
super(name);
}
/**
* Sets active filter.
* @param filter Filter to be set as active for current test, null will reset filtering.
*/
public @Override void setFilter(Filter filter) {
this.filter = filter;
}
/**
* Returns expected fail message.
* @return expected fail message if it's expected this test fail, null otherwise.
*/
public @Override String getExpectedFail() {
if (filter == null) {
return null;
}
return filter.getExpectedFail(this.getName());
}
/**
* Checks if a test isn't filtered out by the active filter.
* @return true if the test can run
*/
public @Override boolean canRun() {
if (NbTestSuite.ignoreRandomFailures()) {
if (getClass().isAnnotationPresent(RandomlyFails.class)) {
System.err.println("Skipping " + getClass().getName());
return false;
}
try {
if (getClass().getMethod(getName()).isAnnotationPresent(RandomlyFails.class)) {
System.err.println("Skipping " + getClass().getName() + "." + getName());
return false;
}
} catch (NoSuchMethodException x) {
// Specially named methods; let it pass.
}
}
if (null == filter) {
//System.out.println("NBTestCase.canRun(): filter == null name=" + name ());
return true; // no filter was aplied
}
boolean isIncluded = filter.isIncluded(this.getName());
//System.out.println("NbTestCase.canRun(): filter.isIncluded(this.getName())="+isIncluded+" ; this="+this);
return isIncluded;
}
/**
* Provide ability for tests, setUp and tearDown to request that they run only in the AWT event queue.
* By default, false.
* @return true to run all test methods, setUp and tearDown in the EQ, false to run in whatever thread
*/
protected boolean runInEQ() {
return false;
}
private static final long vmDeadline;
static {
boolean debugMode = false;
// check if we are debugged
RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
List<String> args = runtime.getInputArguments();
if (args.contains("-Xdebug") || args.contains("-agentlib:jdwp")) { //NOI18N
debugMode = true;
} else {
for (String arg : args) {
if (arg.startsWith("-agentlib:jdwp=")) {
debugMode = true;
break;
}
}
}
Integer vmTimeRemaining = Integer.getInteger("nbjunit.hard.timeout");
if (vmTimeRemaining != null && !debugMode) {
vmDeadline = System.currentTimeMillis() + vmTimeRemaining;
} else {
vmDeadline = -1L;
}
}
private static ThreadLocal<Boolean> DEFAULT_TIME_OUT_CALLED = new ThreadLocal<Boolean>();
/** Provides support for tests that can have problems with terminating.
* Runs the test in a "watchdog" that measures the time the test shall
* take and if it does not terminate it reports a failure including a thread dump.
* <p>If the system property {@code nbjunit.hard.timeout} is set to a number
* (of milliseconds) by which the whole VM must exit (as in the {@code timeout}
* property to Ant's {@code <junit>} task),
* this "soft timeout" will default to some portion of the remaining time, so
* that we can capture a meaningful thread dump rather than simply report that the
* test took too long. (If the VM crashes, the hard timeout kicks in.) Otherwise the
* default is 0 (no soft timeout). For an Ant-based NBM project, {@code common.xml}
* specifies 600000 (ten minutes) as a default hard timeout, and sets the system
* property, so soft timeouts should be the default.
* @return amount ms to give one test to finish or 0 to disable time outs
* @since 1.20
*/
protected int timeOut() {
DEFAULT_TIME_OUT_CALLED.set(true);
return 0;
}
private int computeTimeOut() {
if (vmDeadline == -1L) {
return 0;
}
Boolean prev = DEFAULT_TIME_OUT_CALLED.get();
try {
DEFAULT_TIME_OUT_CALLED.set(null);
int tm = timeOut();
if (!Boolean.TRUE.equals(DEFAULT_TIME_OUT_CALLED.get())) {
return tm;
}
} finally {
DEFAULT_TIME_OUT_CALLED.set(prev);
}
int remaining = (int) (vmDeadline - System.currentTimeMillis());
if (remaining > 1500) {
return (remaining - 1000) / 2;
}
return 1500;
}
/**
* Allows easy collecting of log messages send thru java.util.logging API.
* Overwrite and return the log level to collect logs to logging file.
* If the method returns non-null level, then the level is assigned to
* the <code>Logger.getLogger({@linkplain #logRoot logRoot()})</code> and the messages reported to it
* are then send into regular log file (which is accessible thru {@link NbTestCase#getLog})
* and in case of failure the last few messages is also included
* in <code>failure.getMessage()</code>.
*
* @return default implementation returns <code>null</code> which disables any logging
* support in test
* @since 1.27
* @see Log#enable
*/
protected Level logLevel() {
return null;
}
/**
* If overriding {@link #logLevel}, may override this as well to collect messages from only some code.
* @return {@code ""} (default) to collect messages from all loggers; or {@code "my.pkg"} or {@code "my.pkg.Class"} etc.
* @since 1.68
*/
protected String logRoot() {
return "";
}
/**
* Runs the test case, while conditionally skip some according to result of
* {@link #canRun} method.
*/
@Override
public void run(final TestResult result) {
if (canRun()) {
System.setProperty("netbeans.full.hack", "true"); // NOI18N
System.setProperty("java.util.prefs.PreferencesFactory",
MemoryPreferencesFactory.class.getName());//NOI18N
try {
Preferences.userRoot().sync();
} catch(BackingStoreException bex) {}
Level lev = logLevel();
if (lev != null) {
Log.configure(lev, logRoot(), NbTestCase.this);
}
super.run(result);
}
}
private static StringBuilder printThreadInfo(ThreadInfo ti, StringBuilder sb) {
// print thread information
printThread(ti, sb);
// print stack trace with locks
StackTraceElement[] stacktrace = ti.getStackTrace();
MonitorInfo[] monitors = ti.getLockedMonitors();
for (int i = 0; i < stacktrace.length; i++) {
StackTraceElement ste = stacktrace[i];
sb.append("\t at " + ste.toString()).append("\n");
for (MonitorInfo mi : monitors) {
if (mi.getLockedStackDepth() == i) {
sb.append("\t - locked " + mi).append("\n");
}
}
}
sb.append("\n");
return sb;
}
private static StringBuilder printThread(ThreadInfo ti, StringBuilder sb) {
sb.append("\"" + ti.getThreadName() + "\"" + " Id="
+ ti.getThreadId() + " in " + ti.getThreadState());
if (ti.getLockName() != null) {
sb.append(" waiting on lock=" + ti.getLockName());
}
if (ti.isSuspended()) {
sb.append(" (suspended)");
}
if (ti.isInNative()) {
sb.append(" (running in native)");
}
sb.append("\n");
if (ti.getLockOwnerName() != null) {
sb.append("\t owned by " + ti.getLockOwnerName() + " Id="
+ ti.getLockOwnerId()).append("\n");
}
return sb;
}
private static void printMonitorInfo(ThreadInfo ti, MonitorInfo[] monitors, StringBuilder sb) {
sb.append("\tLocked monitors: count = " + monitors.length).append("\n");
for (MonitorInfo mi : monitors) {
sb.append("\t - " + mi + " locked at ").append("\n");
sb.append("\t " + mi.getLockedStackDepth() + " "
+ mi.getLockedStackFrame()).append("\n");
}
}
private static void printLockInfo(LockInfo[] locks, StringBuilder sb) {
sb.append("\tLocked synchronizers: count = " + locks.length).append("\n");
for (LockInfo li : locks) {
sb.append("\t - " + li).append("\n");
}
sb.append("\n");
}
private static String threadDump() {
ThreadMXBean tmx = ManagementFactory.getPlatformMXBean(ThreadMXBean.class);
ThreadInfo[] threads = tmx.dumpAllThreads(tmx.isSynchronizerUsageSupported(), tmx.isObjectMonitorUsageSupported());
StringBuilder sb = new StringBuilder();
for (ThreadInfo ti : threads) {
printThreadInfo(ti, sb);
}
long[] lockedThreads = tmx.isSynchronizerUsageSupported() ? tmx.findDeadlockedThreads() : null;
long[] monitorLockedThreads = tmx.findMonitorDeadlockedThreads();
if (lockedThreads != null) {
sb.append("\n================\nDead-locked threads:\n");
ThreadInfo[] infos = tmx.getThreadInfo(lockedThreads, true, tmx.isObjectMonitorUsageSupported());
for (ThreadInfo ti : infos) {
printThreadInfo(ti, sb);
printLockInfo(ti.getLockedSynchronizers(), sb);
sb.append("\n");
}
} else if (monitorLockedThreads != null) {
ThreadInfo[] infos = tmx.getThreadInfo(monitorLockedThreads, Integer.MAX_VALUE);
for (ThreadInfo ti : infos) {
// print thread information
printThread(ti, sb);
printMonitorInfo(ti, ti.getLockedMonitors(), sb);
}
}
return sb.toString();
}
/**
* Runs the bare test sequence. It checks {@link #runInEQ} and possibly
* schedules the call of <code>setUp</code>, <code>runTest</code> and <code>tearDown</code>
* to AWT event thread. It also consults {@link #timeOut} and if so, it starts a
* count down and aborts the <code>runTest</code> if the time out expires.
* @exception Throwable if any exception is thrown
*/
@Override
public void runBare() throws Throwable {
abstract class Guard implements Runnable {
private boolean finished;
private Throwable t;
public abstract void doSomething() throws Throwable;
public @Override void run() {
try {
doSomething();
} catch (Throwable thrwn) {
if (MethodOrder.isShuffled()) {
thrwn = Log.wrapWithAddendum(thrwn, "(executed in shuffle mode, run with -DNbTestCase.order=" + MethodOrder.getSeed() + " to reproduce the order)", true);
}
this.t = Log.wrapWithMessages(thrwn, getWorkDirPath());
} finally {
synchronized (this) {
finished = true;
notifyAll();
}
}
}
public synchronized void waitFinished() throws Throwable {
waitFinished(0);
}
public synchronized void waitFinished(final int timeout) throws Throwable {
long time = timeout;
long startTime = System.currentTimeMillis();
while (!finished){
try {
wait(time);
if (timeout > 0) {
time = timeout - (System.currentTimeMillis() - startTime);
if (time < 1) {
break;
}
}
} catch (InterruptedException ex) {
if (t == null) {
t = ex;
}
}
}
if (t != null) {
throw t;
}
if (!finished) {
throw Log.wrapWithMessages(new AssertionFailedError ("The test " + getName() + " did not finish in " + timeout + "ms\n" +
threadDump())
, getWorkDirPath());
}
}
}
/* original sequence from TestCase.runBare():
setUp();
try {
runTest();
} finally {
tearDown();
}
*/
// setUp
if(runInEQ()) {
Guard setUp = new Guard() {
public @Override void doSomething() throws Throwable {
setUp();
}
};
EventQueue.invokeLater(setUp);
// need to have timeout because previous test case can block AWT thread
setUp.waitFinished(computeTimeOut());
} else {
setUp();
}
try {
// runTest
Guard runTest = new Guard() {
public @Override void doSomething() throws Throwable {
long now = System.nanoTime();
try {
runTest();
} catch (Throwable t) {
noteWorkDir(workdirNoCreate());
throw noteRandomness(t);
} finally {
long last = System.nanoTime() - now;
if (last < 1) {
last = 1;
}
NbTestCase.this.time = last;
}
}
};
if (runInEQ()) {
EventQueue.invokeLater(runTest);
runTest.waitFinished(computeTimeOut());
} else {
if (computeTimeOut() == 0) {
// Regular test.
runTest.run();
runTest.waitFinished();
} else {
// Regular test with time out
Thread watchDog = new Thread(runTest, "Test Watch Dog: " + getName());
watchDog.start();
runTest.waitFinished(computeTimeOut());
}
}
} finally {
// tearDown
if(runInEQ()) {
Guard tearDown = new Guard() {
public @Override void doSomething() throws Throwable {
tearDown();
}
};
EventQueue.invokeLater(tearDown);
// need to have timeout because test can block AWT thread
tearDown.waitFinished(computeTimeOut());
} else {
tearDown();
}
}
}
/**
* Make a note of the working directory for a failed test.
* If running inside Hudson, archive it and show the presumed artifact location.
*/
private void noteWorkDir(File wd) {
if (!wd.isDirectory()) {
return;
}
try {
String buildURL = System.getenv("BUILD_URL");
if (buildURL != null) {
String workspace = new File(System.getenv("WORKSPACE")).getCanonicalPath();
if (!workspace.endsWith(File.separator)) {
workspace += File.separator;
}
String path = wd.getCanonicalPath();
if (path.startsWith(workspace)) {
copytree(wd, new File(wd.getParentFile(), wd.getName() + "-FAILED"));
System.err.println("Working directory: " + buildURL + "artifact/" +
path.substring(workspace.length()).replace(File.separatorChar, '/') + "-FAILED/");
return;
}
}
System.err.println("Working directory: " + wd);
} catch (Exception x) {
x.printStackTrace(); // do not mask real error
}
}
static void copytree(File from, File to) throws IOException {
if (from.isDirectory()) {
if (!to.mkdirs()) {
throw new IOException("mkdir: " + to);
}
for (File f : from.listFiles()) {
copytree(f, new File(to, f.getName()));
}
} else {
InputStream is = new FileInputStream(from);
try {
OutputStream os = new FileOutputStream(to);
try {
// XXX using FileChannel would be more efficient, but more complicated
BufferedInputStream bis = new BufferedInputStream(is);
BufferedOutputStream bos = new BufferedOutputStream(os);
int c;
while ((c = bis.read()) != -1) {
bos.write(c);
}
bos.flush();
bos.close();
} finally {
os.close();
}
} finally {
is.close();
}
}
}
private Throwable noteRandomness(Throwable t) {
Class<?> c = getClass();
if (c.isAnnotationPresent(RandomlyFails.class)) {
return Log.wrapWithAddendum(t, "(" + c.getSimpleName() + " marked @RandomlyFails so try just running test again)", false);
}
try {
if (c.getMethod(getName()).isAnnotationPresent(RandomlyFails.class)) {
return Log.wrapWithAddendum(t, "(" + c.getSimpleName() + "." + getName() + " marked @RandomlyFails so try just running test again)", false);
}
} catch (NoSuchMethodException x) {}
return t;
// XXX would be nice to actually try to rerun the test (but would make runBare more complicated)
}
/** Parses the test name to find out whether it encodes a number. The
* testSomeName1343 represents number 1343.
* @return the number
* @exception may throw AssertionFailedError if the number is not found in the test name
*/
protected final int getTestNumber() {
try {
Matcher m = Pattern.compile("test[a-zA-Z]*([0-9]+)").matcher(getName());
assertTrue("Name does not contain numbers: " + getName(), m.find());
return Integer.valueOf(m.group(1)).intValue();
} catch (Exception ex) {
ex.printStackTrace();
fail("Name: " + getName() + " does not represent number");
return 0;
}
}
/** in nanoseconds */
final long getExecutionTime() {
return time;
}
// additional asserts !!!!
/**
* Asserts that two files are the same (their content is identical), when files
* differ {@link org.netbeans.junit.AssertionFileFailedError AssertionFileFailedError} exception is thrown.
* Depending on the Diff implementation additional output can be generated to the file/dir specified by the
* <b>diff</b> param.
* @param message the detail message for this assertion
* @param test first file to be compared, by the convention this should be the test-generated file
* @param pass second file to be compared, it should be so called 'golden' file, which defines
* the correct content for the test-generated file.
* @param diff file, where differences will be stored, when null differences will not be stored. In case
* it points to directory the result file name is constructed from the <b>pass</b> argument and placed to that
* directory. Constructed file name consists from the name of pass file (without extension and path) appended
* by the '.diff'.
* @param externalDiff instance of class implementing the {@link org.netbeans.junit.diff.Diff} interface, it has to be
* already initialized, when passed in this assertFile function.
*/
static public void assertFile(String message, String test, String pass, String diff, Diff externalDiff) {
Diff diffImpl = null == externalDiff ? Manager.getSystemDiff() : externalDiff;
File diffFile = getDiffName(pass, null == diff ? null : new File(diff));
if (null == diffImpl) {
fail("diff is not available");
} else {
try {
if (null == diffFile) {
if (diffImpl.diff(test, pass, null)) {
throw new AssertionFileFailedError(message, "");
}
} else {
if (diffImpl.diff(test, pass, diffFile.getAbsolutePath())) {
throw new AssertionFileFailedError(message, diffFile.getAbsolutePath());
}
}
} catch (IOException e) {
fail("exception in assertFile : " + e.getMessage());
}
}
}
/**
* Asserts that two files are the same, it uses specific {@link org.netbeans.junit.diff.Diff Diff} implementation to
* compare two files and stores possible differences in the output file.
* @param test first file to be compared, by the convention this should be the test-generated file
* @param pass second file to be compared, it should be so called 'golden' file, which defines the
* correct content for the test-generated file.
* @param diff file, where differences will be stored, when null differences will not be stored. In case
* it points to directory the result file name is constructed from the <b>pass</b> argument and placed to that
* directory. Constructed file name consists from the name of pass file (without extension and path) appended
* by the '.diff'.
* @param externalDiff instance of class implementing the {@link org.netbeans.junit.diff.Diff} interface, it has to be
* already initialized, when passed in this assertFile function.
*/
static public void assertFile(String test, String pass, String diff, Diff externalDiff) {
assertFile(null, test, pass, diff, externalDiff);
}
/**
* Asserts that two files are the same, it compares two files and stores possible differences
* in the output file, the message is displayed when assertion fails.
* @param message the detail message for this assertion
* @param test first file to be compared, by the convention this should be the test-generated file
* @param pass second file to be compared, it should be so called 'golden' file, which defines the
* correct content for the test-generated file.
* @param diff file, where differences will be stored, when null differences will not be stored. In case
* it points to directory the result file name is constructed from the <b>pass</b> argument and placed to that
* directory. Constructed file name consists from the name of pass file (without extension and path) appended
* by the '.diff'.
*/
static public void assertFile(String message, String test, String pass, String diff) {
assertFile(message, test, pass, diff, null);
}
/**
* Asserts that two files are the same, it compares two files and stores possible differences
* in the output file.
* @param test first file to be compared, by the convention this should be the test-generated file
* @param pass second file to be compared, it should be so called 'golden' file, which defines the
* correct content for the test-generated file.
* @param diff file, where differences will be stored, when null differences will not be stored. In case
* it points to directory the result file name is constructed from the <b>pass</b> argument and placed to that
* directory. Constructed file name consists from the name of pass file (without extension and path) appended
* by the '.diff'.
*/
static public void assertFile(String test, String pass, String diff) {
assertFile(null, test, pass, diff, null);
}
/**
* Asserts that two files are the same, it just compares two files and doesn't produce any additional output.
* @param test first file to be compared, by the convention this should be the test-generated file
* @param pass second file to be compared, it should be so called 'golden' file, which defines the
* correct content for the test-generated file.
*/
static public void assertFile(String test, String pass) {
assertFile(null, test, pass, null, null);
}
/**
* Asserts that two files are the same (their content is identical), when files
* differ {@link org.netbeans.junit.AssertionFileFailedError AssertionFileFailedError} exception is thrown.
* Depending on the Diff implementation additional output can be generated to the file/dir specified by the
* <b>diff</b> param.
* @param message the detail message for this assertion
* @param test first file to be compared, by the convention this should be the test-generated file
* @param pass second file to be compared, it should be so called 'golden' file, which defines
* the correct content for the test-generated file.
* @param diff file, where differences will be stored, when null differences will not be stored. In case
* it points to directory the result file name is constructed from the <b>pass</b> argument and placed to that
* directory. Constructed file name consists from the name of pass file (without extension and path) appended
* by the '.diff'.
* @param externalDiff instance of class implementing the {@link org.netbeans.junit.diff.Diff} interface, it has to be
* already initialized, when passed in this assertFile function.
*/
static public void assertFile(String message, File test, File pass, File diff, Diff externalDiff) {
Diff diffImpl = null == externalDiff ? Manager.getSystemDiff() : externalDiff;
File diffFile = getDiffName(pass.getAbsolutePath(), diff);
/*
System.out.println("NbTestCase.assertFile(): diffFile="+diffFile);
System.out.println("NbTestCase.assertFile(): diffImpl="+diffImpl);
System.out.println("NbTestCase.assertFile(): externalDiff="+externalDiff);
*/
if (null == diffImpl) {
fail("diff is not available");
} else {
try {
if (diffImpl.diff(test, pass, diffFile)) {
throw new AssertionFileFailedError(message, null == diffFile ? "" : diffFile.getAbsolutePath());
}
} catch (IOException e) {
fail("exception in assertFile : " + e.getMessage());
}
}
}
/**
* Asserts that two files are the same, it uses specific {@link org.netbeans.junit.diff.Diff Diff} implementation to
* compare two files and stores possible differences in the output file.
* @param test first file to be compared, by the convention this should be the test-generated file
* @param pass second file to be compared, it should be so called 'golden' file, which defines the
* correct content for the test-generated file.
* @param diff file, where differences will be stored, when null differences will not be stored. In case
* it points to directory the result file name is constructed from the <b>pass</b> argument and placed to that
* directory. Constructed file name consists from the name of pass file (without extension and path) appended
* by the '.diff'.
* @param externalDiff instance of class implementing the {@link org.netbeans.junit.diff.Diff} interface, it has to be
* already initialized, when passed in this assertFile function.
*/
static public void assertFile(File test, File pass, File diff, Diff externalDiff) {
assertFile(null, test, pass, diff, externalDiff);
}
/**
* Asserts that two files are the same, it compares two files and stores possible differences
* in the output file, the message is displayed when assertion fails.
* @param message the detail message for this assertion
* @param test first file to be compared, by the convention this should be the test-generated file
* @param pass second file to be compared, it should be so called 'golden' file, which defines the
* correct content for the test-generated file.
* @param diff file, where differences will be stored, when null differences will not be stored. In case
* it points to directory the result file name is constructed from the <b>pass</b> argument and placed to that
* directory. Constructed file name consists from the name of pass file (without extension and path) appended
* by the '.diff'.
*/
static public void assertFile(String message, File test, File pass, File diff) {
assertFile(message, test, pass, diff, null);
}
/**
* Asserts that two files are the same, it compares two files and stores possible differences
* in the output file.
* @param test first file to be compared, by the convention this should be the test-generated file
* @param pass second file to be compared, it should be so called 'golden' file, which defines the
* correct content for the test-generated file.
* @param diff file, where differences will be stored, when null differences will not be stored. In case
* it points to directory the result file name is constructed from the <b>pass</b> argument and placed to that
* directory. Constructed file name consists from the name of pass file (without extension and path) appended
* by the '.diff'.
*/
static public void assertFile(File test, File pass, File diff) {
assertFile(null, test, pass, diff, null);
}
/**
* Asserts that two files are the same, it just compares two files and doesn't produce any additional output.
* @param test first file to be compared, by the convention this should be the test-generated file
* @param pass second file to be compared, it should be so called 'golden' file, which defines the
* correct content for the test-generated file.
*/
static public void assertFile(File test, File pass) {
assertFile("Difference between " + test + " and " + pass, test, pass, null, null);
}
/**
*/
static private File getDiffName(String pass, File diff) {
if (null == diff) {
return null;
}
if (!diff.exists() || diff.isFile()) {
return diff;
}
StringBuilder d = new StringBuilder();
int i1, i2;
d.append(diff.getAbsolutePath());
i1 = pass.lastIndexOf('\\');
i2 = pass.lastIndexOf('/');
i1 = i1 > i2 ? i1 : i2;
i1 = -1 == i1 ? 0 : i1 + 1;
i2 = pass.lastIndexOf('.');
i2 = -1 == i2 ? pass.length() : i2;
if (0 < d.length()) {
d.append("/");
}
d.append(pass.substring(i1, i2));
d.append(".diff");
return new File(d.toString());
}
// methods for work with tests' workdirs
/** Returns path to test method working directory as a String. Path is constructed
* as ${nbjunit.workdir}/${package}.${classname}/${testmethodname}. (The nbjunit.workdir
* property should be set in junit.properties; otherwise the default is ${java.io.tmpdir}/tests.)
* Please note that this method does not guarantee that the working directory really exists.
* @return a path to a test method working directory
*/
public String getWorkDirPath() {
if (workDirPath != null) {
return workDirPath;
}
String name = getName();
// start - PerformanceTestCase overrides getName() method and then
// name can contain illegal characters
String osName = System.getProperty("os.name");
if (osName != null && osName.startsWith("Windows")) {
char ntfsIllegal[] ={'"','/','\\','?','<','>','|',':'};
for (int i=0; i<ntfsIllegal.length; i++) {
name = name.replace(ntfsIllegal[i], '~');
}
}
// end
// #94319 - shorten workdir path if the following is too long
// "Manager.getWorkDirPath()+File.separator+getClass().getName()+File.separator+name"
int len1 = Manager.getWorkDirPath().length();
String clazz = getClass().getName();
int len2 = clazz.length();
int len3 = name.length();
int tooLong = Integer.getInteger("nbjunit.too.long", 100);
if (len1 + len2 + len3 > tooLong) {
clazz = abbrevDots(clazz);
len2 = clazz.length();
}
if (len1 + len2 + len3 > tooLong) {
name = abbrevCapitals(name);
}
String p = Manager.getWorkDirPath() + File.separator + clazz + File.separator + name;
String realP;
for (int i = 0; ; i++) {
realP = i == 0 ? p : p + "-" + i;
if (usedPaths.add(realP)) {
break;
}
}
workDirPath = realP;
return realP;
}
private static Set<String> usedPaths = new HashSet<String>();
private static String abbrevDots(String dotted) {
StringBuilder sb = new StringBuilder();
String sep = "";
for (String item : dotted.split("\\.")) {
sb.append(sep);
sb.append(item.charAt(0));
sep = ".";
}
return sb.toString();
}
private static String abbrevCapitals(String name) {
if (name.startsWith("test")) {
name = name.substring(4);
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < name.length(); i++) {
if (Character.isUpperCase(name.charAt(i))) {
sb.append(Character.toLowerCase(name.charAt(i)));
}
}
if (sb.length() == 0) {
// for names without uppercase (e.g. test12345, test_a)
return name;
} else {
return sb.toString();
}
}
private File workdirNoCreate() {
return Manager.normalizeFile(new File(getWorkDirPath()));
}
/** Returns unique working directory for a test (each test method has a unique dir).
* If not available, method tries to create it. This method uses {@link #getWorkDirPath}
* method to determine the unique path.
* <p><strong>Warning:</strong> the working directory is <em>not</em> guaranteed
* to be empty when you get it, so if this is being called in {@link #setUp} you
* are strongly advised to first call {@link #clearWorkDir} to ensure that each
* test run starts with a clean slate.</p>
* @throws IOException if the directory cannot be created
* @return file to the working directory directory
*/
public File getWorkDir() throws IOException {
// construct path from workdir classpath + classname + methodname
/*
String path = this.getClass().getResource("").getFile().toString();
String srcElement="src";
String workdirElement="workdir";
int srcStart = path.lastIndexOf(srcElement);
// base path
path = path.substring(0,srcStart)+workdirElement;
// package+class
path += "/"+this.getClass().getName().replace('.','/');
// method name
path += "/"+getName();
*/
// new way how to get path - from defined property + classname +methodname
// now we have path, so if not available, create workdir
File workdir = workdirNoCreate();
if (workdir.exists()) {
if (!workdir.isDirectory()) {
// work dir exists, but is not directory - this should not happen
// trow exception
throw new IOException("workdir exists, but is not a directory, workdir = " + workdir);
} else {
// everything looks correctly, return the path
return workdir;
}
} else {
// we need to create it
boolean result = workdir.mkdirs();
if (result == false) {
// mkdirs() failed - throw an exception
throw new IOException("workdir creation failed: " + workdir);
} else {
// everything looks ok - return path
return workdir;
}
}
}
// private method for deleting a file/directory (and all its subdirectories/files)
private static void deleteFile(File file) throws IOException {
if (file.isDirectory() && file.equals(file.getCanonicalFile())) {
// file is a directory - delete sub files first
File files[] = file.listFiles();
for (int i = 0; i < files.length; i++) {
deleteFile(files[i]);
}
}
// file is a File :-)
boolean result = file.delete();
if (result == false ) {
// a problem has appeared
throw new IOException("Cannot delete file, file = "+file.getPath());
}
}
// private method for deleting every subfiles/subdirectories of a file object
static void deleteSubFiles(File file) throws IOException {
File files[] = file.getCanonicalFile().listFiles();
if (files != null) {
for (File f : files) {
deleteFile(f);
}
} else {
// probably do nothing - file is not a directory
}
}
/** Deletes all files including subdirectories in test's working directory.
* @throws IOException if any problem has occured during deleting files/directories
*/
public void clearWorkDir() throws IOException {
synchronized (logStreamTable) {
File workdir = getWorkDir();
closeAllStreams();
deleteSubFiles(workdir);
}
}
private String lastTestMethod=null;
private boolean hasTestMethodChanged() {
if (!this.getName().equals(lastTestMethod)) {
lastTestMethod=this.getName();
return true;
} else {
return false;
}
}
// hashtable holding all already used logs and correspondig printstreams
private final Map<String,PrintStream> logStreamTable = new HashMap<String,PrintStream>();
private PrintStream getFileLog(String logName) throws IOException {
synchronized (logStreamTable) {
if (hasTestMethodChanged()) {
// we haven't used logging capability - create hashtables
closeAllStreams();
} else {
if (logStreamTable.containsKey(logName)) {
//System.out.println("Getting stream from cache:"+logName);
return logStreamTable.get(logName);
}
}
// we didn't used this log, so let's create it
OutputStream fileLog = new WFOS(new File(getWorkDir(),logName));
PrintStream printStreamLog = new PrintStream(fileLog,true);
logStreamTable.put(logName,printStreamLog);
//System.out.println("Created new stream:"+logName);
return printStreamLog;
}
}
private void closeAllStreams() {
for (PrintStream ps : logStreamTable.values()) {
ps.close();
}
logStreamTable.clear();
}
private static class WFOS extends FilterOutputStream {
private File f;
private int bytes;
public WFOS(File f) throws FileNotFoundException {
super(new FileOutputStream(f));
this.f = f;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
add(len);
out.write(b, off, len);
}
@Override
public void write(byte[] b) throws IOException {
add(b.length);
out.write(b);
}
@Override
public void write(int b) throws IOException {
add(1);
out.write(b);
}
private synchronized void add(int i) throws IOException {
bytes += i;
if (bytes >= 1048576L) { // 1mb
out.close();
File trim = new File(f.getParent(), "TRIMMED_" + f.getName());
trim.delete();
f.renameTo(trim);
f.delete();
out = new FileOutputStream(f);
bytes = 0;
}
}
} // end of WFOS
// private PrintStream wrapper for System.out
PrintStream systemOutPSWrapper = new PrintStream(System.out);
/** Returns named log stream. If log cannot be created as a file in the
* testmethod working directory, PrintStream created from System.out is used. Please
* note, that tests shoudn't call log.close() method, unless they really don't want
* to use this log anymore.
* @param logName name of the log - file in the working directory
* @return Log PrintStream
*/
public PrintStream getLog(String logName) {
try {
return getFileLog(logName);
} catch (IOException ioe) {
/// hey, file is not available - log will be made to System.out
// we should probably write a little note about it
//System.err.println("Test method "+this.getName()+" - cannot open file log to file:"+logName
// +" - defaulting to System.out");
return systemOutPSWrapper;
}
}
/** Return default log named as ${testmethod}.log. If the log cannot be created
* as a file in testmethod working directory, PrinterStream to System.out is returned
* @return log
*/
public PrintStream getLog() {
return getLog(this.getName()+".log");
}
/** Simple and easy to use method for printing a message to a default log
* @param message meesage to log
*/
public void log(String message) {
getLog().println(message);
}
/** Easy to use method for logging a message to a named log
* @param log which log to use
* @param message message to log
*/
public void log(String log, String message) {
getLog(log).println(message);
}
// reference file stuff ...
/** Get PrintStream to log inteded for reference files comparision. Reference
* log is stored as a file named ${testmethod}.ref in test method working directory.
* If the file cannot be created, the testcase will automatically fail.
* @return PrintStream to referencing log
*/
public PrintStream getRef() {
String refFilename = this.getName()+".ref";
try {
return getFileLog(refFilename);
} catch (IOException ioe) {
// canot get ref file - return system.out
//System.err.println("Test method "+this.getName()+" - cannot open ref file:"+refFilename
// +" - defaulting to System.out and failing test");
fail("Could not open reference file: "+refFilename);
return systemOutPSWrapper;
}
}
/** Easy to use logging method for printing a message to a reference log.
* @param message message to log
*/
public void ref(String message) {
getRef().println(message);
}
/** Get the test method specific golden file from ${xtest.data}/goldenfiles/${classname}
* directory. If not found, try also deprecated src/data/goldenfiles/${classname}
* resource directory.
* @param filename filename to get from golden files directory
* @return golden file
*/
public File getGoldenFile(String filename) {
String fullClassName = this.getClass().getName();
String goldenFileName = fullClassName.replace('.', '/')+"/"+filename;
// golden files are in ${xtest.data}/goldenfiles/${classname}/...
File goldenFile = new File(getDataDir()+"/goldenfiles/"+goldenFileName);
if(goldenFile.exists()) {
// Return if found, otherwise try to find golden file in deprecated
// location. When deprecated part is removed, add assertTrue(goldenFile.exists())
// instead of if clause.
return goldenFile;
}
/** Deprecated - this part is deprecated */
// golden files are in data/goldenfiles/${classname}/* ...
String className = fullClassName;
int lastDot = fullClassName.lastIndexOf('.');
if (lastDot != -1) {
className = fullClassName.substring(lastDot+1);
}
goldenFileName = className+"/"+filename;
URL url = this.getClass().getResource("data/goldenfiles/"+goldenFileName);
assertNotNull("Golden file not found in any of the following locations:\n "+
goldenFile+"\n "+
"src/"+fullClassName.replace('.', '/').substring(0, fullClassName.indexOf(className))+"data/goldenfiles/"+goldenFileName,
url);
String resString = convertNBFSURL(url);
goldenFile = new File(resString);
return goldenFile;
/** Deprecated end. */
}
/** Returns pointer to directory with test data (golden files, sample files, ...).
* It is the same from xtest.data property.
* @return data directory
*/
public File getDataDir() {
// XXX should this be deprecated?
String xtestData = System.getProperty("xtest.data");
if(xtestData != null) {
return Manager.normalizeFile(new File(xtestData));
} else {
// property not set => try to find it
URL codebase = getClass().getProtectionDomain().getCodeSource().getLocation();
if (!codebase.getProtocol().equals("file")) {
throw new Error("Cannot find data directory from " + codebase);
}
File dataDir;
try {
dataDir = new File(new File(codebase.toURI()).getParentFile(), "data");
} catch (URISyntaxException x) {
throw new Error(x);
}
return Manager.normalizeFile(dataDir);
}
}
/** Get the default testmethod specific golden file from
* data/goldenfiles/${classname}/${testmethodname}.pass
* @return filename to get from golden files resource directory
*/
public File getGoldenFile() {
return getGoldenFile(this.getName()+".pass");
}
/** Compares golden file and reference log. If both files are the
* same, test passes. If files differ, test fails and diff file is
* created (diff is created only when using native diff, for details
* see JUnit module documentation)
* @param testFilename reference log file name
* @param goldenFilename golden file name
* @param diffFilename diff file name (optional, if null, then no diff is created)
*/
public void compareReferenceFiles(String testFilename, String goldenFilename, String diffFilename) {
try {
if (!getRef().equals(systemOutPSWrapper)) {
// better flush the reference file
getRef().flush();
getRef().close();
}
File goldenFile = getGoldenFile(goldenFilename);
File testFile = new File(getWorkDir(),testFilename);
File diffFile = new File(getWorkDir(),diffFilename);
String message = "Files differ";
if(System.getProperty("xtest.home") == null) {
// show location of diff file only when run without XTest (run file in IDE)
message += "; check "+diffFile;
}
assertFile(message, testFile, goldenFile, diffFile);
} catch (IOException ioe) {
fail("Could not obtain working direcory");
}
}
/** Compares default golden file and default reference log. If both files are the
* same, test passes. If files differ, test fails and default diff (${methodname}.diff)
* file is created (diff is created only when using native diff, for details
* see JUnit module documentation)
*/
public void compareReferenceFiles() {
compareReferenceFiles(this.getName()+".ref",this.getName()+".pass",this.getName()+".diff");
}
// utility stuff for getting resources from NetBeans' filesystems
/** Converts NetBeans filesystem URL to absolute path.
* @param url URL to convert
* @return absolute path
* @deprecated No longer applicable as of NB 4.0 at the latest.
* <code>FileObject.getURL()</code> should be returning a <code>file</code>-protocol
* URL, which can be converted to a disk path using <code>new File(URI)</code>; or
* use <code>FileUtil.toFile</code>.
*/
@Deprecated
public static String convertNBFSURL(URL url) {
if(url == null) {
throw new IllegalArgumentException("Given URL should not be null.");
}
String externalForm = url.toExternalForm();
if (externalForm.startsWith("nbfs://")) {
// new nbfsurl format (post 06/2003)
return convertNewNBFSURL(url);
} else {
// old nbfsurl (and non nbfs urls)
return convertOldNBFSURL(url);
}
}
// radix for new nbfsurl
private final static int radix = 16;
// new nbfsurl decoder - assumes the external form
// begins with nbfs://
private static String convertNewNBFSURL(URL url) {
String externalForm = url.toExternalForm();
String path;
if (externalForm.startsWith("nbfs://nbhost/")) {
// even newer nbfsurl (hope it does not change soon)
// return path and omit first slash sign
path = url.getPath().substring(1);
} else {
path = externalForm.substring("nbfs://".length());
}
// convert separators (%2f = /, etc.)
StringBuilder sb = new StringBuilder();
int i = 0;
int len = path.length();
while (i < len) {
char ch = path.charAt(i++);
if (ch == '%' && (i+1) < len) {
char h1 = path.charAt(i++);
char h2 = path.charAt(i++);
// convert d1+d2 hex number to char
ch = (char)Integer.parseInt("" + h1 + h2, radix);
}
sb.append(ch);
}
return sb.toString();
}
// old nbfsurl decoder
private static String convertOldNBFSURL(URL url) {
String path = url.getFile();
if(url.getProtocol().equals("nbfs")) {
// delete prefix of special Filesystem (e.g. org.netbeans.modules.javacvs.JavaCvsFileSystem)
String prefixFS = "FileSystem ";
if(path.indexOf(prefixFS)>-1) {
path = path.substring(path.indexOf(prefixFS)+prefixFS.length());
}
// convert separators ("QB="/" etc.)
StringBuilder sb = new StringBuilder();
int i = 0;
int len = path.length();
while (i < len) {
char ch = path.charAt(i++);
if (ch == 'Q' && i < len) {
ch = path.charAt(i++);
switch (ch) {
case 'B':
sb.append('/');
break;
case 'C':
sb.append(':');
break;
case 'D':
sb.append('\\');
break;
case 'E':
sb.append('#');
break;
default:
// not a control sequence
sb.append('Q');
sb.append(ch);
break;
}
} else {
// not Q
sb.append(ch);
}
}
path = sb.toString();
}
return path;
}
/** Asserts that the object can be garbage collected. Tries to GC ref's referent.
* @param text the text to show when test fails.
* @param ref the referent to object that
* should be GCed
*/
public static void assertGC(String text, Reference<?> ref) {
assertGC(text, ref, Collections.emptySet());
}
/** Asserts that the object can be garbage collected. Tries to GC ref's referent.
* @param text the text to show when test fails.
* @param ref the referent to object that should be GCed
* @param rootsHint a set of objects that should be considered part of the
* rootset for this scan. This is useful if you want to verify that one structure
* (usually long living in real application) is not holding another structure
* in memory, without setting a static reference to the former structure.
* <h3>Example:</h3>
* <pre>
* // test body
* WeakHashMap map = new WeakHashMap();
* Object target = new Object();
* map.put(target, "Val");
*
* // verification step
* Reference ref = new WeakReference(target);
* target = null;
* assertGC("WeakMap does not hold the key", ref, Collections.singleton(map));
* </pre>
*/
public static void assertGC(final String text, final Reference<?> ref, final Set<?> rootsHint) {
NbModuleLogHandler.whileIgnoringOOME(new Runnable() {
@SuppressWarnings({"SleepWhileHoldingLock", "SleepWhileInLoop"})
public @Override void run() {
List<byte[]> alloc = new ArrayList<byte[]>();
int size = 100000;
for (int i = 0; i < 50; i++) {
if (ref.get() == null) {
return;
}
try {
System.gc();
} catch (OutOfMemoryError error) {
// OK
}
try {
System.runFinalization();
} catch (OutOfMemoryError error) {
// OK
}
try {
alloc.add(new byte[size]);
size = (int)(((double)size) * 1.3);
} catch (OutOfMemoryError error) {
size = size / 2;
}
try {
if (i % 3 == 0) {
Thread.sleep(321);
}
} catch (InterruptedException t) {
// ignore
}
}
alloc = null;
String str = null;
try {
str = findRefsFromRoot(ref.get(), rootsHint);
} catch (Exception e) {
throw new AssertionFailedErrorException(e);
} catch (OutOfMemoryError err) {
// OK
}
fail(text + ":\n" + str);
}
});
}
/** Assert size of some structure. Traverses the whole reference
* graph of objects accessible from given root object and check its size
* against the limit.
* @param message the text to show when test fails.
* @param limit maximal allowed heap size of the structure
* @param root the root object from which to traverse
*/
public static void assertSize(String message, int limit, Object root ) {
assertSize(message, Arrays.asList( new Object[] {root} ), limit);
}
/** Assert size of some structure. Traverses the whole reference
* graph of objects accessible from given roots and check its size
* against the limit.
* @param message the text to show when test fails.
* @param roots the collection of root objects from which to traverse
* @param limit maximal allowed heap size of the structure
*/
public static void assertSize(String message, Collection<?> roots, int limit) {
assertSize(message, roots, limit, new Object[0]);
}
/** Assert size of some structure. Traverses the whole reference
* graph of objects accessible from given roots and check its size
* against the limit.
* @param message the text to show when test fails.
* @param roots the collection of root objects from which to traverse
* @param limit maximal allowed heap size of the structure
* @param skip Array of objects used as a boundary during heap scanning,
* neither these objects nor references from these objects
* are counted.
*/
public static void assertSize(String message, Collection<?> roots, int limit, Object[] skip) {
org.netbeans.insane.scanner.Filter f = ScannerUtils.skipObjectsFilter(Arrays.asList(skip), false);
assertSize(message, roots, limit, f);
}
/** Assert size of some structure. Traverses the whole reference
* graph of objects accessible from given roots and check its size
* against the limit.
* @param message the text to show when test fails.
* @param roots the collection of root objects from which to traverse
* @param limit maximal allowed heap size of the structure
* @param skip custom filter for counted objects
* @return actual size or <code>-1</code> on internal error.
*/
public static int assertSize(String message, Collection<?> roots, int limit, final MemoryFilter skip) {
org.netbeans.insane.scanner.Filter f = new org.netbeans.insane.scanner.Filter() {
public @Override boolean accept(Object o, Object refFrom, Field ref) {
return !skip.reject(o);
}
};
return assertSize(message, roots, limit, f);
}
private static int assertSize(String message, Collection<?> roots, int limit,
org.netbeans.insane.scanner.Filter f) {
try {
final CountingVisitor counter = new CountingVisitor();
ScannerUtils.scan(f, counter, roots, false);
int sum = counter.getTotalSize();
if (sum > limit) {
StringBuilder sb = new StringBuilder(4096);
sb.append(message);
sb.append(": leak ").append(sum - limit).append(" bytes ");
sb.append(" over limit of ");
sb.append(limit).append(" bytes");
Set<Class<?>> classes = new TreeSet<Class<?>>(new Comparator<Class<?>>() {
public @Override int compare(Class<?> c1, Class<?> c2) {
int r = counter.getSizeForClass(c2) - counter.getSizeForClass(c1);
return r != 0 ? r : c1.hashCode() - c2.hashCode();
}
});
classes.addAll(counter.getClasses());
for (Class<?> cls : classes) {
if (counter.getCountForClass(cls) == 0) {
continue;
}
sb.append("\n ").append(cls.getName()).append(": ").
append(counter.getCountForClass(cls)).append(", ").
append(counter.getSizeForClass(cls)).append("B");
}
fail(sb.toString());
}
return sum;
} catch (Exception e) {
throw new AssertionFailedErrorException("Could not traverse reference graph", e);
}
}
/**
* Fails a test with known bug ID.
* @param bugID the bug number according bug report system.
*/
public static void failByBug(int bugID) {
throw new AssertionKnownBugError(bugID);
}
/**
* Fails a test with known bug ID and with the given message.
* @param bugID the bug number according bug report system.
* @param message the text to show when test fails.
*/
public static void failByBug(int bugID, String message) {
throw new AssertionKnownBugError(bugID, message);
}
private static String findRefsFromRoot(final Object target, final Set<?> rootsHint) throws Exception {
int count = Integer.getInteger("assertgc.paths", 1);
StringBuilder sb = new StringBuilder();
final Map<Object,Void> skip = new IdentityHashMap<Object,Void>();
org.netbeans.insane.scanner.Filter knownPath = new org.netbeans.insane.scanner.Filter() {
public @Override boolean accept(Object obj, Object referredFrom, Field reference) {
return !skip.containsKey(obj);
}
};
while (count-- > 0) {
@SuppressWarnings("unchecked")
Map<Object,Path> m = LiveReferences.fromRoots(Collections.singleton(target), (Set<Object>)rootsHint, null, knownPath);
Path p = m.get(target);
if (p == null) {
break;
}
if (sb.length() > 0) {
sb.append("\n\n");
}
sb.append(p);
for (; p != null; p=p.nextNode()) {
Object o = p.getObject();
if (o != target) {
skip.put(o, null);
}
}
}
return sb.length() > 0 ? sb.toString() : "Not found!!!";
}
}