/*
* Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.test.synth;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.BatchUpdateException;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import org.h2.constant.ErrorCode;
import org.h2.jdbc.JdbcConnection;
import org.h2.store.FileLister;
import org.h2.test.TestAll;
import org.h2.test.TestBase;
import org.h2.test.db.TestScript;
import org.h2.test.synth.sql.RandomGen;
import org.h2.tools.Backup;
import org.h2.tools.DeleteDbFiles;
import org.h2.tools.Restore;
import org.h2.util.IOUtils;
import org.h2.util.MathUtils;
import org.h2.util.New;
/**
* A test that calls random methods with random parameters from JDBC objects.
* This is sometimes called 'Fuzz Testing'.
*/
public class TestCrashAPI extends TestBase implements Runnable {
private static final boolean RECOVER_ALL = false;
private static final Class<?>[] INTERFACES = { Connection.class, PreparedStatement.class, Statement.class,
ResultSet.class, ResultSetMetaData.class, Savepoint.class,
ParameterMetaData.class, Clob.class, Blob.class, Array.class, CallableStatement.class };
private static final String DIR = "synth";
private ArrayList<Object> objects = New.arrayList();
private HashMap<Class <?>, ArrayList<Method>> classMethods = New.hashMap();
private RandomGen random = new RandomGen();
private ArrayList<String> statements = New.arrayList();
private int openCount;
private long callCount;
private volatile long maxWait = 60;
private volatile boolean stopped;
private volatile boolean running;
private Thread mainThread;
/**
* Run just this test.
*
* @param a ignored
*/
public static void main(String... a) throws Exception {
System.setProperty("h2.delayWrongPasswordMin", "0");
System.setProperty("h2.delayWrongPasswordMax", "0");
TestBase.createCaller().init().test();
}
@SuppressWarnings("deprecation")
public void run() {
while (--maxWait > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
maxWait++;
// ignore
}
if (maxWait > 0 && maxWait <= 10) {
println("stopping...");
stopped = true;
}
}
if (maxWait == 0 && running) {
objects.clear();
if (running) {
println("stopping (force)...");
for (StackTraceElement e : mainThread.getStackTrace()) {
System.out.println(e.toString());
}
mainThread.stop(new SQLException("stop"));
}
}
}
private static void recoverAll() {
org.h2.Driver.load();
File[] files = new File("temp/backup").listFiles();
Arrays.sort(files, new Comparator<File>() {
public int compare(File o1, File o2) {
return o1.getName().compareTo(o2.getName());
}
});
for (File f : files) {
if (!f.getName().startsWith("db-")) {
continue;
}
DeleteDbFiles.execute("data", null, true);
try {
Restore.execute(f.getAbsolutePath(), "data", null, true);
} catch (Exception e) {
System.out.println(f.getName() + " restore error " + e);
// ignore
}
ArrayList<String> dbFiles = FileLister.getDatabaseFiles("data", null, false);
for (String name: dbFiles) {
if (!name.endsWith(".h2.db")) {
continue;
}
name = name.substring(0, name.length() - 6);
try {
DriverManager.getConnection("jdbc:h2:data/" + name, "sa", "").close();
System.out.println(f.getName() + " OK");
} catch (SQLException e) {
System.out.println(f.getName() + " " + e);
}
}
}
}
public void test() throws Exception {
if (RECOVER_ALL) {
recoverAll();
return;
}
if (config.mvcc || config.networked) {
return;
}
int len = getSize(2, 6);
Thread t = new Thread(this);
try {
mainThread = Thread.currentThread();
t.start();
running = true;
for (int i = 0; i < len && !stopped; i++) {
int seed = MathUtils.randomInt(Integer.MAX_VALUE);
testCase(seed);
deleteDb();
}
} finally {
running = false;
deleteDb();
maxWait = -1;
t.join();
}
}
private void deleteDb() {
try {
deleteDb(getBaseDir() + "/" + DIR, null);
} catch (Exception e) {
// ignore
}
}
private Connection getConnection(int seed, boolean delete) throws SQLException {
openCount++;
if (delete) {
deleteDb();
}
// can not use FILE_LOCK=NO, otherwise something could be written into
// the database in the finalize method
String add = ";MAX_QUERY_TIMEOUT=10000";
// int testing;
// if(openCount >= 32) {
// int test;
// Runtime.getRuntime().halt(0);
// System.exit(1);
// }
// System.out.println("now open " + openCount);
// add += ";TRACE_LEVEL_FILE=3";
// config.logMode = 2;
// }
String dbName = "crashApi" + seed;
String url = getURL(DIR + "/" + dbName, true) + add;
// int test;
// url += ";DB_CLOSE_ON_EXIT=FALSE";
// int test;
// url += ";TRACE_LEVEL_FILE=3";
Connection conn = null;
String fileName = "temp/backup/db-" + uniqueId++ + ".zip";
Backup.execute(fileName, getBaseDir() + "/" + DIR, dbName, true);
// close databases earlier
System.gc();
try {
conn = DriverManager.getConnection(url, "sa", getPassword(""));
// delete the backup if opening was successful
IOUtils.delete(fileName);
} catch (SQLException e) {
if (e.getErrorCode() == ErrorCode.WRONG_USER_OR_PASSWORD) {
// delete if the password changed
IOUtils.delete(fileName);
}
throw e;
}
int len = random.getInt(50);
int first = random.getInt(statements.size() - len);
int end = first + len;
Statement stat = conn.createStatement();
stat.execute("SET LOCK_TIMEOUT 10");
stat.execute("SET WRITE_DELAY 0");
if (random.nextBoolean()) {
if (random.nextBoolean()) {
double g = random.nextGaussian();
int size = (int) Math.abs(10000 * g * g);
stat.execute("SET CACHE_SIZE " + size);
} else {
stat.execute("SET CACHE_SIZE 0");
}
}
stat.execute("SCRIPT NOPASSWORDS NOSETTINGS");
for (int i = first; i < end && i < statements.size() && !stopped; i++) {
try {
stat.execute("SELECT * FROM TEST WHERE ID=1");
} catch (Throwable t) {
printIfBad(seed, -i, -1, t);
}
try {
stat.execute("SELECT * FROM TEST WHERE ID=1 OR ID=1");
} catch (Throwable t) {
printIfBad(seed, -i, -1, t);
}
String sql = statements.get(i);
try {
// if(openCount == 32) {
// int test;
// System.out.println("stop!");
// }
stat.execute(sql);
} catch (Throwable t) {
printIfBad(seed, -i, -1, t);
}
}
if (random.nextBoolean()) {
try {
conn.commit();
} catch (Throwable t) {
printIfBad(seed, 0, -1, t);
}
}
return conn;
}
public void testCase(int seed) throws SQLException {
printTime("seed: " + seed);
callCount = 0;
openCount = 0;
random = new RandomGen();
random.setSeed(seed);
Connection c1 = getConnection(seed, true);
Connection conn = null;
for (int i = 0; i < 2000 && !stopped; i++) {
// if(i % 10 == 0) {
// for(int j=0; j<objects.size(); j++) {
// System.out.print(objects.get(j));
// System.out.print(" ");
// }
// System.out.println();
// Thread.sleep(1);
// }
if (objects.size() == 0) {
try {
long start = System.currentTimeMillis();
conn = getConnection(seed, false);
long connectTime = System.currentTimeMillis() - start;
if (connectTime > 2000) {
System.out.println("??? connected in " + connectTime);
}
} catch (SQLException e) {
if ("08004".equals(e.getSQLState())) {
// Wrong user/password [08004]
try {
c1.createStatement().execute("SET PASSWORD ''");
} catch (Throwable t) {
// power off or so
break;
}
try {
long start = System.currentTimeMillis();
conn = getConnection(seed, false);
long connectTime = System.currentTimeMillis() - start;
if (connectTime > 2000) {
System.out.println("??? connected2 in " + connectTime);
}
} catch (Throwable t) {
printIfBad(seed, -i, -1, t);
}
} else if ("90098".equals(e.getSQLState())) {
// The database has been closed
break;
} else {
printIfBad(seed, -i, -1, e);
}
}
objects.add(conn);
}
int objectId = random.getInt(objects.size());
if (random.getBoolean(1)) {
objects.remove(objectId);
continue;
}
if (random.getInt(2000) == 0 && conn != null) {
((JdbcConnection) conn).setPowerOffCount(random.getInt(50));
}
Object o = objects.get(objectId);
if (o == null) {
objects.remove(objectId);
continue;
}
Class<?> in = getJdbcInterface(o);
ArrayList<Method> methods = classMethods.get(in);
Method m = methods.get(random.getInt(methods.size()));
Object o2 = callRandom(seed, i, objectId, o, m);
if (o2 != null) {
objects.add(o2);
}
}
try {
if (conn != null) {
conn.close();
}
c1.close();
} catch (Throwable t) {
printIfBad(seed, -101010, -1, t);
try {
deleteDb();
} catch (Throwable t2) {
printIfBad(seed, -101010, -1, t2);
}
}
objects.clear();
}
private void printError(int seed, int id, Throwable t) {
StringWriter writer = new StringWriter();
t.printStackTrace(new PrintWriter(writer));
String s = writer.toString();
TestBase.logError("new TestCrashAPI().init(test).testCase(" +
seed + "); // Bug " + s.hashCode() + " id=" + id +
" callCount=" + callCount + " openCount=" + openCount +
" " + t.getMessage(), t);
throw new RuntimeException(t);
}
private Object callRandom(int seed, int id, int objectId, Object o, Method m) {
Class<?>[] paramClasses = m.getParameterTypes();
Object[] params = new Object[paramClasses.length];
for (int i = 0; i < params.length; i++) {
params[i] = getRandomParam(paramClasses[i]);
}
Object result = null;
try {
callCount++;
result = m.invoke(o, params);
} catch (IllegalArgumentException e) {
TestBase.logError("error", e);
} catch (IllegalAccessException e) {
TestBase.logError("error", e);
} catch (InvocationTargetException e) {
Throwable t = e.getTargetException();
printIfBad(seed, id, objectId, t);
}
if (result == null) {
return null;
}
Class<?> in = getJdbcInterface(result);
if (in == null) {
return null;
}
return result;
}
private void printIfBad(int seed, int id, int objectId, Throwable t) {
if (t instanceof BatchUpdateException) {
// do nothing
} else if (t.getClass().getName().indexOf("SQLClientInfoException") >= 0) {
// do nothing
} else if (t instanceof SQLException) {
SQLException s = (SQLException) t;
int errorCode = s.getErrorCode();
if (errorCode == 0) {
printError(seed, id, s);
} else if (errorCode == ErrorCode.OBJECT_CLOSED) {
if (objectId >= 0 && objects.size() > 0) {
// TODO at least call a few more times after close - maybe
// there is still an error
objects.remove(objectId);
}
} else if (errorCode == ErrorCode.GENERAL_ERROR_1) {
// General error [HY000]
printError(seed, id, s);
}
} else {
printError(seed, id, t);
}
}
private Object getRandomParam(Class<?> type) {
if (type == int.class) {
return random.getRandomInt();
} else if (type == byte.class) {
return (byte) random.getRandomInt();
} else if (type == short.class) {
return (short) random.getRandomInt();
} else if (type == long.class) {
return random.getRandomLong();
} else if (type == float.class) {
return (float) random.getRandomDouble();
} else if (type == boolean.class) {
return random.nextBoolean();
} else if (type == double.class) {
return new Double(random.getRandomDouble());
} else if (type == String.class) {
if (random.getInt(10) == 0) {
return null;
}
int randomId = random.getInt(statements.size());
String sql = statements.get(randomId);
if (random.getInt(10) == 0) {
sql = random.modify(sql);
}
return sql;
} else if (type == int[].class) {
// TODO test with 'shared' arrays (make sure database creates a
// copy)
return random.getIntArray();
} else if (type == java.io.Reader.class) {
return null;
} else if (type == java.sql.Array.class) {
return null;
} else if (type == byte[].class) {
// TODO test with 'shared' arrays (make sure database creates a
// copy)
return random.getByteArray();
} else if (type == Map.class) {
return null;
} else if (type == Object.class) {
return null;
} else if (type == java.sql.Date.class) {
return random.randomDate();
} else if (type == java.sql.Time.class) {
return random.randomTime();
} else if (type == java.sql.Timestamp.class) {
return random.randomTimestamp();
} else if (type == java.io.InputStream.class) {
return null;
} else if (type == String[].class) {
return null;
} else if (type == java.sql.Clob.class) {
return null;
} else if (type == java.sql.Blob.class) {
return null;
} else if (type == Savepoint.class) {
// TODO should use generated savepoints
return null;
} else if (type == Calendar.class) {
return Calendar.getInstance();
} else if (type == java.net.URL.class) {
return null;
} else if (type == java.math.BigDecimal.class) {
return new java.math.BigDecimal("" + random.getRandomDouble());
} else if (type == java.sql.Ref.class) {
return null;
}
return null;
}
private Class<?> getJdbcInterface(Object o) {
for (Class <?> in : o.getClass().getInterfaces()) {
if (classMethods.get(in) != null) {
return in;
}
}
return null;
}
private void initMethods() {
for (Class<?> inter : INTERFACES) {
classMethods.put(inter, new ArrayList<Method>());
}
for (Class<?> inter : INTERFACES) {
ArrayList<Method> list = classMethods.get(inter);
for (Method m : inter.getMethods()) {
list.add(m);
}
}
}
public TestBase init(TestAll conf) throws Exception {
super.init(conf);
if (config.mvcc || config.networked) {
return this;
}
startServerIfRequired();
TestScript script = new TestScript();
ArrayList<String> add = script.getAllStatements(config);
initMethods();
org.h2.Driver.load();
statements.addAll(add);
return this;
}
}