view tests/TestRunner.java @ 575:08c9918177b2 onclient

Do not check the updated row count, it varies between server versions
author Joeri van Ruth <joeri.van.ruth@monetdbsolutions.com>
date Wed, 06 Oct 2021 15:30:47 +0200 (2021-10-06)
parents 3370027aeb7f
children 13134a44dfc8
line wrap: on
line source
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.  If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright 1997 - July 2008 CWI, August 2008 - 2021 MonetDB B.V.
 */

import org.monetdb.jdbc.MonetConnection;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.sql.*;
import java.util.ArrayList;

public class TestRunner {
	public static final int VERBOSITY_NONE = 0;
	public static final int VERBOSITY_ON = 1;
	public static final int VERBOSITY_SHOW_ALL = 2;
	protected final String jdbcUrl;
	private final int verbosity;
	protected String currentTestName;
	protected final WatchDog watchDog;
	protected MonetConnection conn;
	protected Statement stmt;
	private StringWriter outBuffer;
	protected PrintWriter out;
	private Path tmpDir = null;

	public TestRunner(String jdbcUrl, int verbosity, boolean watchDogEnabled) {
		this.jdbcUrl = jdbcUrl;
		this.verbosity = verbosity;
		watchDog = new WatchDog();
		if (watchDogEnabled)
			watchDog.enable();
		else
			watchDog.disable();
	}

	protected int runTests(String testPrefix) throws SQLException {
		int testCount = 0;
		int skippedCount = 0;
		ArrayList<String> failures = new ArrayList<>();

		watchDog.stop();
		try {
			final String initialPrefix = "test_";
			if (testPrefix == null)
				testPrefix = "";
			final String methodPrefix = initialPrefix + testPrefix;

			for (Method method : this.getClass().getDeclaredMethods()) {
				if (method.getParameterCount() != 0) {
					continue;
				}
				if (!method.getName().startsWith(initialPrefix)) {
					continue;
				}
				testCount++;
				// so user can add $ to force full match
				String augmentedMethodName = method.getName() + "$";
				if (!augmentedMethodName.startsWith(methodPrefix)) {
					skippedCount++;
					continue;
				}
				String testName = method.getName().substring(initialPrefix.length());
				boolean succeeded = runTest(testName, method);
				if (!succeeded)
					failures.add(testName);
			}
		} finally {
			watchDog.stop();
		}

		if (testCount > 0 && skippedCount == testCount && !testPrefix.isEmpty()) {
			System.err.printf("None of the %d tests matched prefix '%s'%n", testCount, testPrefix);
			return 1;
		}

		int failureCount = failures.size();
		if (failureCount > 0) {
			System.out.println();
			System.out.printf("Ran %d out of %d %s tests, %d failed: %s%n",
					testCount - skippedCount, testCount,
					this.getClass().getSimpleName(),
					failureCount,
					String.join(", ", failures)
					);
		} else if (verbosity >= VERBOSITY_ON) {
			System.out.println();
			System.out.printf("Ran %d out of %d tests, none failed%n", testCount - skippedCount, testCount);
		}

		return failureCount;
	}

	private synchronized boolean runTest(String testName, Method method) throws SQLException {
		currentTestName = testName;
		watchDog.setContext("test " + testName);
		watchDog.setDuration(3_000);
		outBuffer = new StringWriter();
		out = new PrintWriter(outBuffer);

		Connection genericConnection = DriverManager.getConnection(jdbcUrl);
		conn = genericConnection.unwrap(MonetConnection.class);
		stmt = conn.createStatement();

		boolean failed = true;
		try {
			long duration;
			try {
				long t0 = System.currentTimeMillis();
				method.invoke(this);
				long t1 = System.currentTimeMillis();
				duration = t1 - t0;
			} catch (InvocationTargetException e) {
				Throwable cause = e.getCause();
				if (cause instanceof Failure)
					throw (Failure) cause;
				else if (cause instanceof Exception) {
					throw (Exception) cause;
				} else {
					throw e;
				}
			}

			failed = false;

			if (verbosity > VERBOSITY_ON)
				System.out.println();
			if (verbosity >= VERBOSITY_ON)
				System.out.println("Test " + testName + " succeeded in " + duration + "ms");
			if (verbosity >= VERBOSITY_SHOW_ALL)
				dumpOutput(testName);
		} catch (Failure e) {
			System.out.println();
			System.out.println("Test " + testName + " failed");
			dumpOutput(testName);
		} catch (Exception e) {
			System.out.println();
			System.out.println("Test " + testName + " failed:");
			e.printStackTrace(System.out);
			dumpOutput(testName);
			// Show the inner bits of the exception again, they may have scrolled off screen
			Throwable t = e;
			while (t.getCause() != null) {
				t = t.getCause();
			}
			System.out.println("Innermost cause was " + t);
			if (t.getStackTrace().length > 0) {
				System.out.println("                 at " + t.getStackTrace()[0]);
			}
		} finally {
			watchDog.setContext(null);
			if (failed && verbosity == VERBOSITY_ON) {
				// next test case will not print separator
				System.out.println();
			}
			stmt.close();
			conn.close();
		}

		return !failed;
	}

	private void dumpOutput(String testName) {
		String output = outBuffer.getBuffer().toString();
		if (output.isEmpty()) {
			System.out.println("(Test did not produce any output)");
		} else {
			System.out.println("------ Accumulated output for test " + testName + ":");
			boolean terminated = output.endsWith(System.lineSeparator());
			if (terminated) {
				System.out.print(output);
			} else {
				System.out.println(output);
			}
			System.out.println("------ End of accumulated output" + (terminated ? "" : " (no trailing newline)"));
		}
	}

	private void fail(String message) throws Failure {
		out.println("FAILURE: " + message);
		throw new Failure(message);
	}

	private void checked(String quantity, Object actual) {
		out.println("  CHECKED: " + "<" + quantity + "> is " + actual + " as expected");
	}

	protected void assertEq(String quantity, Object expected, Object actual) throws Failure {
		if (expected.equals(actual)) {
			checked(quantity, actual);
		} else {
			fail("Expected <" + quantity + "> to be " + expected + " got " + actual);
		}
	}

	protected boolean execute(String query) throws SQLException {
		try {
			watchDog.start();
			out.println("EXECUTE: " + query);
			boolean result;
			result = stmt.execute(query);
			if (result) {
				out.println("  OK");
			} else {
				out.println("  OK, updated " + stmt.getUpdateCount() + " rows");
			}
			return result;
		} finally {
			watchDog.stop();
		}
	}

	protected void update(String query) throws SQLException, Failure {
		execute(query);
	}

	protected void expectError(String query, String expectedError) throws SQLException {
		try {
			execute(query);
		} catch (SQLException e) {
			if (e.getMessage().contains(expectedError)) {
				out.println("  GOT EXPECTED EXCEPTION: " + e.getMessage());
			} else {
				throw e;
			}
		}
	}

	protected void assertQueryInt(String query, int expected) throws SQLException, Failure {
		if (execute(query) == false) {
			fail("Query does not return a result set");
		}
		ResultSet rs = stmt.getResultSet();
		ResultSetMetaData metaData = rs.getMetaData();
		assertEq("column count", 1, metaData.getColumnCount());
		if (!rs.next()) {
			fail("Result set is empty");
		}
		int result = rs.getInt(1);
		if (rs.next()) {
			String message = "Result set has more than one row";
			fail(message);
		}
		rs.close();
		checked("row count", 1);
		assertEq("query result", expected, result);
	}

	protected String queryString(String query) throws SQLException, Failure {
		if (execute(query) == false) {
			fail("Query does not return a result set");
		}
		ResultSet rs = stmt.getResultSet();
		ResultSetMetaData metaData = rs.getMetaData();
		assertEq("column count", 1, metaData.getColumnCount());
		if (!rs.next()) {
			fail("Result set is empty");
		}
		String result = rs.getString(1);
		if (rs.next()) {
			String message = "Result set has more than one row";
			fail(message);
		}
		rs.close();
		checked("row count", 1);
		return result;
	}

	protected synchronized Path getTmpDir(String name) throws IOException {
		if (tmpDir == null) {
			tmpDir = Files.createTempDirectory("testMonetDB");
			Runtime.getRuntime().addShutdownHook(new Thread(() -> {
				try {
					Files.walkFileTree(tmpDir, new SimpleFileVisitor<Path>() {
						@Override
						public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
							Files.delete(file);
							return FileVisitResult.CONTINUE;
						}

						@Override
						public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
							Files.delete(dir);
							return FileVisitResult.CONTINUE;
						}
					});
				} catch (IOException e) {
					// we do this on a best effort basis
				}
			}));
		}
		Path p = tmpDir.resolve(name);
		Files.createDirectory(p);
		return p;
	}

	static class Failure extends Exception {

		public Failure(String message) {
			super(message);
		}

		public Failure(String message, Throwable cause) {
			super(message, cause);
		}

	}

	static class WatchDog {
		private boolean enabled;
		private long duration = 1000;
		private long started = 0;
		private String context = "no context";

		WatchDog() {
			Thread watchDog = new Thread(this::work);
			watchDog.setName("watchdog_timer");
			watchDog.setDaemon(true);
			watchDog.start();
		}

		synchronized void enable() {
			this.enabled = true;
			this.notifyAll();
		}

		synchronized void disable() {
			this.enabled = false;
			this.notifyAll();
		}

		synchronized void setContext(String context) {
			this.context = context;
		}

		synchronized void setDuration(long duration) {
			if (duration <= 0)
				throw new IllegalArgumentException("duration should be > 0");
			this.duration = duration;
			this.notifyAll();
		}

		synchronized void start() {
			started = System.currentTimeMillis();
			this.notifyAll();
		}

		synchronized void stop() {
			started = 0;
			this.notifyAll();
		}

		private synchronized void work() {
			long now;
			try {
				while (true) {
					now = System.currentTimeMillis();
					final long sleepTime;
					if (started < 0) {
						// client asked us to go away
						// System.err.println("++ EXIT");
						return;
					} else if (!enabled || started == 0) {
						// wait for client to enable/start us
						sleepTime = 600_000;
					} else {
						long deadline = started + duration;
						sleepTime = deadline - now;
					}
					// System.err.printf("++ now=%d, started=now%+d, duration=%d, sleep=%d%n",
					// 		now, started - now, duration, sleepTime
					// 		);
					if (sleepTime > 0) {
						this.wait(sleepTime);
					} else {
						trigger();
						return;
					}
				}
			} catch (InterruptedException e) {
				System.err.println("WATCHDOG TIMER INTERRUPTED, SHOULDN'T HAPPEN");
				System.exit(4);
			}
		}

		private void trigger() {
			String c = context != null ? context : "no context";
			System.err.println();
			System.err.println();
			System.err.println("WATCHDOG TIMER EXPIRED [" + c + "], KILLING TESTS");
			System.exit(3);
		}
	}
}