/*
 *
 * Derby - Class org.apache.derbyTesting.junit.BaseTestCase
 *
 * 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.apache.derbyTesting.junit;

import org.apache.derbyTesting.functionTests.harness.JavaVersionHolder;
import org.apache.derbyTesting.functionTests.util.PrivilegedFileOpsForTests;
import junit.framework.Assert;
import junit.framework.TestCase;
import junit.framework.AssertionFailedError;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.InterruptedIOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.net.URL;
import java.sql.SQLException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;

import java.security.PrivilegedActionException;
import java.util.ArrayList;


/**
 * Base class for JUnit tests.
 */
public abstract class BaseTestCase
    extends TestCase {

    private static final String JACOCO_AGENT_PROP = "derby.tests.jacoco.agent";

    protected final static String ERRORSTACKTRACEFILE = "error-stacktrace.out";
    protected final static String DEFAULT_DB_DIR      = "system";
    protected final static String DERBY_LOG           = "derby.log";
    
    private static int debugPort; // default 8800
    /**
     * No argument constructor made private to enforce naming of test cases.
     * According to JUnit documentation, this constructor is provided for
     * serialization, which we don't currently use.
     *
     * @see #BaseTestCase(String)
     */
    private BaseTestCase() {}

    /**
     * Create a test case with the given name.
     *
     * @param name name of the test case.
     */
    public BaseTestCase(String name) {
        super(name);
    }
    
    /**
     * Run the test and force installation of a security
     * manager with the default test policy file.
     * Individual tests can run without a security
     * manager or with a different policy file using
     * the decorators obtained from SecurityManagerSetup.
     * <BR>
     * Method is final to ensure security manager is
     * enabled by default. Tests should not need to
     * override runTest, instead use test methods
     * setUp, tearDown methods and decorators.
     */
    public void runBare() throws Throwable {
        TestConfiguration config = getTestConfiguration();
        boolean trace = config.doTrace();
        boolean stopAfterFirstFail = config.stopAfterFirstFail();
        long startTime = 0;
        if ( trace )
        {
            startTime = System.currentTimeMillis();
            out.println();
            String  junitClassName = this.getClass().getName();
            junitClassName=Utilities.formatTestClassNames(junitClassName);
            out.print(traceClientType());
            out.print(junitClassName+"."+getName() + " ");
        }

        // install a default security manager if one has not already been
        // installed
        if ( System.getSecurityManager() == null )
        {
            if (config.defaultSecurityManagerSetup())
            {
                assertSecurityManager();
            }
        }

        try {
            super.runBare();   
        }
        // To log the exception to file, copy the derby.log file and copy
        // the database of the failed test.
        catch (Throwable running) {
            PrintWriter stackOut = null;
            try{
                String failPath = PrivilegedFileOpsForTests.getAbsolutePath(getFailureFolder());
                // Write the stack trace of the error/failure to file.
                stackOut = new PrintWriter(
                        PrivilegedFileOpsForTests.getFileOutputStream(
                            new File(failPath, ERRORSTACKTRACEFILE), true));
                stackOut.println("[Error/failure logged at " +
                        new java.util.Date() + "]");
                running.printStackTrace(stackOut);
                stackOut.println(); // Add an extra blank line.
                // Copy the derby.log file.
                File origLog = new File(DEFAULT_DB_DIR, DERBY_LOG);
                File newLog = new File(failPath, DERBY_LOG);
                PrivilegedFileOpsForTests.copy(origLog, newLog);
                // Copy some other likely files, the rolling log files
                // These might occur if the tests are run with 
                // derby.stream.error.style=rollingFile
                for (int i=0; i < 10; i++) {
                    String logName = "derby-" + i + ".log";
                    File origRolLog = new File(DEFAULT_DB_DIR, logName);
                    File newRolLog = new File(failPath, logName);
                    PrivilegedFileOpsForTests.copy(origRolLog, newRolLog);
                }
                // Copy the database.
                String dbName = TestConfiguration.getCurrent().getDefaultDatabaseName();
                File dbDir = new File(DEFAULT_DB_DIR, dbName );
                File newDbDir = new File(failPath, dbName);
                PrivilegedFileOpsForTests.copy(dbDir,newDbDir);
           }
            catch (IOException ioe) {
                // We need to throw the original exception so if there
                // is an exception saving the db or derby.log we will print it
                // and additionally try to log it to file.
                BaseTestCase.printStackTrace(ioe);
                if (stackOut != null) {
                    stackOut.println("Copying derby.log or database failed:");
                    ioe.printStackTrace(stackOut);
                    stackOut.println();
                }
            }
            finally {
                if (stackOut != null) {
                    stackOut.close();
                }
                if (stopAfterFirstFail) {
                    // if run with -Dderby.tests.stopAfterFirstFail=true
                    // exit after reporting failure. Useful for debugging
                    // cascading failures or errors that lead to hang.
                    running.printStackTrace(out);
                    System.exit(1);
                }
                else
                    throw running;
            }
        }
        finally{
            if ( trace )
            {
                long timeUsed = System.currentTimeMillis() - startTime;
                out.print("used " + timeUsed + " ms ");
            }
        }
    }

    /**
     * Return the current configuration for the test.
     */
    public final TestConfiguration getTestConfiguration()
    {
    	return TestConfiguration.getCurrent();
    }
    
    /**
     * Get the folder where a test leaves any information
     * about its failure.
     * @return Folder to use.
     * @see TestConfiguration#getFailureFolder(TestCase)
     */
    public final File getFailureFolder() {
        return getTestConfiguration().getFailureFolder(this);
    }
    
    /**
     * Print alarm string
     * @param text String to print
     */
    public static void alarm(final String text) {
        out.println("ALARM: " + text);
    }

    /**
     * Print debug string.
     * @param text String to print
     */
    public static void println(final String text) {
        if (TestConfiguration.getCurrent().isVerbose()) {
            out.println("DEBUG: " + text);
            out.flush();
        }
    }

    /**
     * Print trace string.
     * @param text String to print
     */
    public static void traceit(final String text) {
        if (TestConfiguration.getCurrent().doTrace()) {
            out.println(text);
        }
    }

    /**
     * Print debug string.
     * @param t Throwable object to print stack trace from
     */
    public static void printStackTrace(Throwable t) 
    {
        while ( t!= null) {
            t.printStackTrace(out);
            out.flush();
            
            if (t instanceof SQLException)  {
                t = ((SQLException) t).getNextException();
            } else {
                break;
            }
        }
    }

    private final static PrintStream out = System.out;

    /**
     * Change the value of {@code System.out}.
     *
     * @param out the new stream
     */
    protected static void setSystemOut(final PrintStream out) {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                System.setOut(out);
                return null;
            }
        });
    }

    /**
     * Change the value of {@code System.err}.
     *
     * @param err the new stream
     */
    protected static void setSystemErr(final PrintStream err) {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                System.setErr(err);
                return null;
            }
        });
    }

    /**
     * Set system property
     *
     * @param name name of the property
     * @param value value of the property
     */
    protected static void setSystemProperty(final String name, 
					    final String value)
    {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                System.setProperty(name, value);
                return null;
            }
        });
    }

    /**
     * Remove system property
     *
     * @param name name of the property
     */
    public static void removeSystemProperty(final String name)
	{
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                System.getProperties().remove(name);
                return null;
            }
        });
    }

    /**
     * Get system property.
     *
     * @param name name of the property
     */
    protected static String getSystemProperty(final String name)
	{
        return AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty(name);
            }
        });
    }
    
    /**
     * Get files in a directory which contain certain prefix
     * 
     * @param dir
     *        The directory we are checking for files with certain prefix
     * @param prefix
     *        The prefix pattern we are interested.
     * @return The list indicates files with certain prefix.
     */
    protected static String[] getFilesWith(final File dir, String prefix) {
        return AccessController.doPrivileged(new PrivilegedAction<String[]>() {
                    public String[] run() {
                        //create a FilenameFilter and override its accept-method to file
                        //files start with "javacore"*
                        FilenameFilter filefilter = new FilenameFilter() {
                            public boolean accept(File dir, String name) {
                                //if the file has prefix javacore return true, else false
                                return name.startsWith("javacore");
                            }
                        };
                        return dir.list(filefilter);
                    }
                });
    }
    
    /**
     * Obtain the URL for a test resource, e.g. a policy
     * file or a SQL script.
     * @param name Resource name, typically - org.apache.derbyTesing.something
     * @return URL to the resource, null if it does not exist.
     */
    protected static URL getTestResource(final String name)
	{
        return AccessController.doPrivileged(new PrivilegedAction<URL>() {
            public URL run() {
                return BaseTestCase.class.getClassLoader().getResource(name);
            }
        });
    }
  
    /**
     * Open the URL for a a test resource, e.g. a policy
     * file or a SQL script.
     * @param url URL obtained from getTestResource
     * @return An open stream
    */
    protected static InputStream openTestResource(final URL url)
        throws PrivilegedActionException
    {
        return AccessController.doPrivileged(
                new PrivilegedExceptionAction<InputStream>() {
            public InputStream run() throws IOException {
                return url.openStream();
            }
        });
    }
    
    /**
     * Assert a security manager is installed.
     *
     */
    public static void assertSecurityManager()
    {
    	assertNotNull("No SecurityManager installed",
    			System.getSecurityManager());
    }

    /**
     * Compare the contents of two streams.
     * The streams are closed after they are exhausted.
     *
     * @param is1 the first stream
     * @param is2 the second stream
     * @throws IOException if reading from the streams fail
     * @throws AssertionFailedError if the stream contents are not equal
     */
    public static void assertEquals(InputStream is1, InputStream is2)
            throws IOException {
        if (is1 == null || is2 == null) {
            assertNull("InputStream is2 is null, is1 is not", is1);
            assertNull("InputStream is1 is null, is2 is not", is2);
            return;
        }
        long index = 0;
        int b1 = is1.read();
        int b2 = is2.read();
        do {
            // Avoid string concatenation for every byte in the stream.
            if (b1 != b2) {
                assertEquals("Streams differ at index " + index, b1, b2);
            }
            index++;
            b1 = is1.read();
            b2 = is2.read();
        } while (b1 != -1 || b2 != -1);
        is1.close();
        is2.close();
    }

    /**
     * Compare the contents of two readers.
     * The readers are closed after they are exhausted.
     *
     * @param r1 the first reader
     * @param r2 the second reader
     * @throws IOException if reading from the streams fail
     * @throws AssertionFailedError if the reader contents are not equal
     */
    public static void assertEquals(Reader r1, Reader r2)
            throws IOException {
        long index = 0;
        if (r1 == null || r2 == null) {
            assertNull("Reader r2 is null, r1 is not", r1);
            assertNull("Reader r1 is null, r2 is not", r2);
            return;
        }
        int c1 = r1.read();
        int c2 = r2.read();
        do {
            // Avoid string concatenation for every char in the stream.
            if (c1 != c2) {
                assertEquals("Streams differ at index " + index, c1, c2);
            }
            index++;
            c1 = r1.read();
            c2 = r2.read();
        } while (c1 != -1 || c2 != -1);
        r1.close();
        r2.close();
    }

    /**
     * Assert that the detailed messages of the 2 passed-in Throwable's are
     * equal (rather than '=='), as well as their class types.
     *
     * @param t1 first throwable to compare
     * @param t2 second throwable to compare
     */
    public static void assertThrowableEquals(Throwable t1,
                                             Throwable t2) {
        // Ensure non-null throwable's are being passed.
        assertNotNull(
            "Passed-in throwable t1 cannot be null to assert detailed message",
            t1);
        assertNotNull(
            "Passed-in throwable t2 cannot be null to assert detailed message",
            t2);

        // Now verify that the passed-in throwable are of the same type
        assertEquals("Throwable class types are different",
                     t1.getClass().getName(), t2.getClass().getName());

        // Here we finally check that the detailed message of both
        // throwable's is the same
        assertEquals("Detailed messages of the throwable's are different",
                     t1.getMessage(), t2.getMessage());
    }
    
    /**
     * <p>
     * Assert the equivalence of two byte arrays.
     * </p>
     */
    public  static  void    assertEquals( byte[] expected, byte[] actual )
    {
        if ( assertSameNullness( expected, actual ) ) { return; }
        
        assertEquals( expected.length, actual.length );
        for ( int i = 0; i < expected.length; i++ )
        {
            assertEquals( Integer.toString( i ), expected[ i ], actual[ i ] );
        }
    }

    /**
     * Assert that two objects are either both null or neither null.
     * Returns true if they are null.
     */
    public  static  boolean assertSameNullness( Object expected, Object actual )
    {
        if ( expected ==  null )
        {
            assertNull( actual );
            return true;
        }
        else
        {
            assertNotNull( actual );
            return false;
        }
    }

    /**
     * <p>
     * Assert the equivalence of two int arrays.
     * </p>
     */
    public  static  void    assertEquals( int[] expected, int[] actual )
    {
        if ( assertSameNullness( expected, actual ) ) { return; }
        
        assertEquals( expected.length, actual.length );
        for ( int i = 0; i < expected.length; i++ )
        {
            assertEquals( Integer.toString( i ), expected[ i ], actual[ i ] );
        }
    }

    /**
     * <p>
     * Assert the equivalence of two long arrays.
     * </p>
     */
    public  static  void    assertEquals( long[] expected, long[] actual )
    {
        if ( assertSameNullness( expected, actual ) ) { return; }
        
        assertEquals( expected.length, actual.length );
        for ( int i = 0; i < expected.length; i++ )
        {
            assertEquals( Integer.toString( i ), expected[ i ], actual[ i ] );
        }
    }

    /**
     * Assert that two files in the filesystem are identical.
     * 
     * @param file1 the first file to compare
     * @param file2 the second file to compare
     */
	public static void assertEquals(final File file1, final File file2) {
		AccessController.doPrivileged
        (new PrivilegedAction<Void>() {
        	public Void run() {
        		try {
					InputStream f1 = new BufferedInputStream(new FileInputStream(file1));
					InputStream f2 = new BufferedInputStream(new FileInputStream(file2));

					assertEquals(f1, f2);
				} catch (FileNotFoundException e) {
					fail("FileNotFoundException in assertEquals(File,File): " + e.getMessage());
					e.printStackTrace();
				} catch (IOException e) {
					fail("IOException in assertEquals(File, File): " + e.getMessage());
					e.printStackTrace();
				}
				return null;
        	}
        });
	}
    
	/**
	 * Execute command using 'java' executable and verify that it completes
	 * with expected results
	 * @param expectedString String to compare the resulting output with. May be
	 *     null if the output is not expected to be of interest.
	 * @param cmd array of java arguments for command
	 * @param expectedExitValue expected return value from the command
	 * @throws InterruptedException
	 * @throws IOException
	 */
	public static void assertExecJavaCmdAsExpected(String[] expectedString,
	        String[] cmd, int expectedExitValue) throws InterruptedException,
	        IOException {

	    Process pr = execJavaCmd(cmd);
	    String output = readProcessOutput(pr);
	    int exitValue = pr.exitValue();
	    String expectedStrings = "";
	    for (int i = 0; i < expectedString.length; i++) 
	        expectedStrings += "\t[" +i + "]" + expectedString[i] +  "\n";
	    Assert.assertEquals("expectedExitValue:" + expectedExitValue +
	            " does not match exitValue:" + exitValue +"\n" +
	            "expected output strings:\n" + expectedStrings + 
	            " actual output:" + output,
	            expectedExitValue, exitValue);
	    if (expectedString != null) {
	        for (int i = 0; i < expectedString.length; i++) {
	            assertTrue("Could not find expectedString:" +
	                    expectedString[i] + " in output:" + output,
	                    output.indexOf(expectedString[i]) >= 0);
	        }
	    }
	}

    /**
     * Same as {@link #execJavaCmd( String, String, String[], File, boolean )}
     * but with {@code addClassPath == true}.
     */
    public static Process execJavaCmd(
        String jvm, String cp, String[] cmd, final File dir)
            throws IOException {
        return execJavaCmd(jvm, cp, cmd, dir, true);
    }

	/**
	 * Execute a java command and return the process.
	 * The caller should decide what to do with the process, if anything,
	 * typical activities would be to do a pr.waitFor, or to
	 * get a getInputStream or getErrorStream
	 * Note, that for verifying the output of a Java process, there is
	 * assertExecJavaCmdAsExpected
	 * 
     * @param jvm the path to the java executable, or {@code null} to use
     *            the default executable returned by
     *            {@link #getJavaExecutableName()}
     * @param cp  the classpath for the spawned process, or {@code null} to
     *            inherit the classpath from the parent process
	 * @param cmd array of java arguments for command
     * @param dir working directory for the sub-process, or {@code null} to
     *            run in the same directory as the main test process
     * @param addClassPath if {@code true},add classpath
	 * @return the process that was started
	 * @throws IOException
	 */
    public static Process execJavaCmd(
        String jvm, String cp, String[] cmd, final File dir, boolean addClassPath)
            throws IOException {

        // Is this an invocation of a jar file with java -jar ...?
        final boolean isJarInvocation = cmd.length > 0 && cmd[0].equals("-jar");

	    ArrayList<String> cmdlist = new ArrayList<String>();
        cmdlist.add(jvm == null ? getJavaExecutableName() : jvm);
	    if (isJ9Platform())
	    {
	        cmdlist.add("-jcl:foun11");
            // also add the setting for emma.active so any tests
            // that fork will work correctly. See DERBY-5558.
            String emmaactive=getSystemProperty("emma.active");
            if (emmaactive != null) {
                cmdlist.add("-Demma.active=" + emmaactive);            
            }
            // Do the same for jacoco.active, see DERBY-6079.
            String jacocoactive = getSystemProperty("jacoco.active");
            if (jacocoactive != null) {
                cmdlist.add("-Djacoco.active=" + jacocoactive);
            }
	    }

        if (isCVM()) {
            // DERBY-5642: The default maximum heap size on CVM is very low.
            // Increase it to prevent OOME in the forked process.
            cmdlist.add("-Xmx32M");
        }

        if (runsWithEmma()) {
            // DERBY-5801: If many processes write to the same file, it may
            // end up corrupted. Let each process have its own file to which
            // it writes coverage data.
            cmdlist.add("-Demma.coverage.out.file=" + getEmmaOutFile());

            // DERBY-5810: Make sure that emma.jar is included on the
            // classpath of the sub-process. (Only needed if a specific
            // classpath has been specified. Otherwise, the sub-process
            // inherits the classpath from the parent process, which
            // already includes emma.jar.)
            if (cp != null) {
                cp += File.pathSeparator + getEmmaJar().getPath();
            }

            // DERBY-5821: When starting a sub-process with java -jar, the
            // classpath argument will be ignored, so we cannot add emma.jar
            // that way. Add it to the boot classpath instead.
            if (isJarInvocation) {
                cmdlist.add("-Xbootclasspath/a:" + getEmmaJar().getPath());
            }
        }

        if (runsWithJaCoCo()) {
            // Property (http://www.eclemma.org/jacoco/trunk/doc/agent.html):
            // -javaagent:[yourpath/]jacocoagent.jar=[opt1]=[val1],[opt2]=[val2]
            String agent = getSystemProperty(JACOCO_AGENT_PROP);
            cmdlist.add(agent + (agent.endsWith("=") ? "": ",") +
                    "destfile=" + getJaCoCoOutFile());
            cmdlist.add("-Djacoco.active=");
        }

        if (isSunJVM() && Boolean.valueOf(
                    getSystemProperty("derby.test.debugSubprocesses")).
                booleanValue()) {
            setupForDebuggerAttach(cmdlist);
        }
        
        if (isJarInvocation) {
            // If -jar is specified, the Java command will ignore the user's
            // classpath, so don't set it. Fail if an explicit classpath has
            // been set in addition to -jar, as that's probably a mistake in
            // the calling code.
            assertNull("Both -jar and classpath specified", cp);
        } else if (addClassPath) {
            cmdlist.add("-classpath");
            cmdlist.add(cp == null ? getSystemProperty("java.class.path") : cp);
        }

	    for (int i =0; i < cmd.length;i++) {
	        cmdlist.add(cmd[i]);
	    }
	    final String[] command = (String[]) cmdlist.toArray(cmd);
	    println("execute java command:");
	    for (int i = 0; i < command.length; i++) {
	        println("command[" + i + "]" + command[i]);
	    }
	    try {
            return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<Process>() {
                public Process run() throws IOException {
                    return Runtime.getRuntime().exec(
                            command, (String[]) null, dir);
	            }
	        });
	    } catch (PrivilegedActionException pe) {
            throw (IOException) pe.getException();
	    }
	}

    /**
     * Execute a java command and return the process. The process will run
     * in the same directory as the main test process. This method is a
     * shorthand for {@code execJavaCmd(null, null, cmd, null)}.
     */
    public static Process execJavaCmd(String[] cmd) throws IOException {
        return execJavaCmd(null, null, cmd, null);
    }

    /**
     * Return the executable name for spawning java commands.
     * This will be <path to j9>/j9  for j9 jvms.
     * @return full path to java executable.
     */
    public static final String getJavaExecutableName() {
        String vmname = getSystemProperty("com.ibm.oti.vm.exe");

        if (vmname == null) {
            vmname = getSystemProperty("java.vm.name");

            // Sun phoneME
            if ("CVM".equals(vmname)) {
                vmname = getSystemProperty("java.home") +
                    File.separator + "bin" +
                    File.separator + "cvm";
            } else {
                vmname = getSystemProperty("java.home") +
                    File.separator + "bin" +
                    File.separator + "java";
            }
        }

        return vmname;
    }

    /**
     * <p>
     * Return the current directory.
     * </p>
     */
    public  static  File    currentDirectory()
    {
        return new File( getSystemProperty( "user.dir" ) );
    }

    /**
     * @return true if this is a j9 VM
     */
    public static final boolean isJ9Platform() {
        return getSystemProperty("com.ibm.oti.vm.exe") != null;
    }

    public static final boolean isSunJVM() {
        String vendor = getSystemProperty("java.vendor");
        return "Sun Microsystems Inc.".equals(vendor) ||
                "Oracle Corporation".equals(vendor);
    }

    /**
     * Check if this is a CVM-based VM (like phoneME or Oracle Java ME
     * Embedded Client).
     */
    public static boolean isCVM() {
        return "CVM".equals(getSystemProperty("java.vm.name"));
    }

    /**
     * Check if the VM is phoneME.
     *
     * @return true if it is phoneME
     */
    public static boolean isPhoneME() {
        return isCVM() &&
                getSystemProperty("java.vm.version").startsWith("phoneme");
    }

    /**
     * Determine if there is a platform match with os.name.
     * This method uses an exact equals. Other methods might be useful
     * later for starts with.
     * 
     * @param osName value we want to check against the system property
     *      os.name
     * @return return true if osName is an exact match for osName
     */
    
    public static final boolean isPlatform(String osName)  {

        return getSystemProperty("os.name").equals(osName);
    }

    /**
     * Determine if platform is a Windows variant.
     * <p>
     * Return true if platform is a windows platform.  Just looks for
     * os.name starting with "Windows".  The os.name property
     * can have at least the following values (there are probably more):
     *
     * AIX
     * Digital Unix
     * FreeBSD
     * HP UX
     * Irix
     * Linux
     * Mac OS
     * Mac OS X
     * MPE/iX
     * Netware 4.11
     * OS/2
     * SunOS
     * Windows 2000
     * Windows 95
     * Windows 98
     * Windows NT
     * Windows Vista
     * Windows XP
     * <p>
     *
     * @return true if running on a Windows platform.
     **/
    public static final boolean isWindowsPlatform() {
        return getSystemProperty("os.name").startsWith("Windows");
    }
    
    /**
     * Check if this is java 5
     * @return true if java.version system property starts with 1.5
     */
    public static final boolean isJava5() {
        return getSystemProperty("java.version").startsWith("1.5");
    }

    public static final boolean isJava7() {
        return getSystemProperty("java.version").startsWith("1.7");
    }

    public static final boolean isJava8() {
        return getSystemProperty("java.version").startsWith("1.8");
    }

    public static final boolean runsWithEmma() {
        return getSystemProperty("java.class.path").indexOf("emma.jar") != -1;
    }

    public static boolean runsWithJaCoCo() {
        return SecurityManagerSetup.jacocoEnabled;
    }

    /**
     * Counter used to produce unique file names based on process count.
     *
     * @see #getEmmaOutFile()
     * @see #getJaCoCoOutFile()
     */
    private static int spawnedCount = 0;

    /**
     * Get a unique file object that can be used by sub-processes to store
     * JaCoCo code coverage data. Each separate sub-process should have its
     * own file in order to prevent corruption of the coverage data.
     *
     * @return a file to which a sub-process can write code coverage data
     */
    private static synchronized File getJaCoCoOutFile() {
        return new File(currentDirectory(),
                "jacoco.exec." + (++spawnedCount));
    }

    /**
     * Get a unique file object that can be used by sub-processes to store
     * EMMA code coverage data. Each separate sub-process should have its
     * own file in order to prevent corruption of the coverage data.
     *
     * @return a file to which a sub-process can write code coverage data
     */
    private static synchronized File getEmmaOutFile() {
        return new File(currentDirectory(),
                "coverage-" + (++spawnedCount) + ".ec");
    }

    /**
     * Get a URL pointing to {@code emma.jar}, if the tests are running
     * with EMMA code coverage. The method returns {@code null} if the
     * tests are not running with EMMA.
     */
    public static URL getEmmaJar() {
        return SecurityManagerSetup.getURL("com.vladium.emma.EMMAException");
    }

    /**
     * Returns the major version of the class specification version supported
     * by the running JVM.
     * <ul>
     *  <li>48 = Java 1.4</li>
     *  <li>49 = Java 1.5</li>
     *  <li>50 = Java 1.6</li>
     *  <li>51 = Java 1.7</li>
     * </ul>
     *
     * @return Major version of class version specification, i.e. 49 for 49.0,
     *      or -1 if the version can't be obtained for some reason.
     */
    public static int getClassVersionMajor() {
        String tmp = getSystemProperty("java.class.version");
        if (tmp == null) {
            println("VM doesn't have property java.class.version");
            return -1;
        }
        // Is String.split safe to use by now?
        int dot = tmp.indexOf('.');
        int major = -1;
        try {
            major = Integer.parseInt(tmp.substring(0, dot));
        } catch (NumberFormatException nfe) {
            // Ignore, return -1.
        }
        return major;
    }

    /**
     * Check if we have old style (before Sun Java 1.7) Solaris interruptible
     * IO. On Sun Java 1.5 &gt;= update 22 and Sun Java 1.6 this can be disabled
     * with Java option {@code -XX:-UseVMInterruptibleIO}. On Sun Java 1.7 it
     * is by default disabled.
     *
     * @return true if we have old style interruptible IO
     */
    public static final boolean hasInterruptibleIO() {

        boolean interruptibleIO = false;

        try {
            AccessController.doPrivileged(
                new PrivilegedExceptionAction<Void>() {
                    public Void run() throws
                        IOException, InterruptedIOException {

                        TestConfiguration curr = TestConfiguration.getCurrent();

                        String sysHome = getSystemProperty("derby.system.home");

                        StringBuffer arbitraryRAFFileNameB = new StringBuffer();

                        arbitraryRAFFileNameB.append(sysHome);
                        arbitraryRAFFileNameB.append(File.separatorChar);
                        arbitraryRAFFileNameB.append("derby.log");

                        String arbitraryRAFFileName =
                            arbitraryRAFFileNameB.toString();
                        // Create if it does not exist:
                        new File(sysHome).mkdirs(); // e.g. "system"
                        new File(arbitraryRAFFileName).createNewFile();

                        RandomAccessFile f = new RandomAccessFile(
                            arbitraryRAFFileName, "r");

                        try {
                            Thread.currentThread().interrupt();
                            f.read();
                        } finally {
                            Thread.interrupted(); // clear flag
                            f.close();
                        }

                        return null;
                    }});
        } catch (PrivilegedActionException e) {
            if (e.getCause() instanceof InterruptedIOException) {
                interruptibleIO = true;
            } else {
                // Better to assume nothing when the test fails. Then, tests
                // will not be skipped and we would not miss that something is
                // amiss.
                println("Could not test for interruptible IO," +
                        " so assuming we don't have it: " + e);
                e.getCause().printStackTrace();
                return false;
            }
        }

        return interruptibleIO;
    }


    public static final boolean isIBMJVM() {
        return ("IBM Corporation".equals(
                getSystemProperty("java.vendor")));
    }

    /**
     * Reads output from a process and returns it as a string.
     * <p>
     * This will block until the process terminates.
     * 
     * @param pr a running process
     * @return Output of the process, both STDOUT and STDERR.
     * @throws InterruptedException if interrupted while waiting for the
     *      subprocess or one of the output collector threads to terminate
     */
    public static String readProcessOutput(Process pr)
            throws InterruptedException {
        SpawnedProcess wrapper = new SpawnedProcess(pr, "readProcessOutput");
        wrapper.suppressOutputOnComplete();
        try {
            wrapper.complete();
        } catch (IOException ioe) {
            fail("process completion method failed", ioe);
        }
        String output = "<STDOUT>" + wrapper.getFullServerOutput() +
                "<END STDOUT>\n";
        output += "<STDERR>" + wrapper.getFullServerError() +
                "<END STDERR>\n";
        return output;
    }

    /**
     * Deletes the specified directory and all its files and subdirectories.
     * <p>
     * This method will attempt to delete all the files inside the root
     * directory, even if one of the delete operations fails.
     * <p>
     * After having tried to delete all files once, any remaining files will be
     * attempted deleted again after a pause. This is repeated, resulting
     * in multiple failed delete attempts for any single file before the method
     * gives up and raises a failure.
     * <p>
     * The approach above will mask any slowness involved in releasing file
     * handles, but should fail if a file handle actually isn't released on a
     * system that doesn't allow deletes on files with open handles (i.e.
     * Windows). It will also mask slowness caused by the JVM, the file system,
     * or the operation system.
     *
     * @param dir the root to start deleting from (root will also be deleted)
     */
    public static void assertDirectoryDeleted(File dir) {
        File[] fl = null;
        int attempts = 0;
        while (attempts < 4) {
            try {
                Thread.sleep(attempts * 2000);
            } catch (InterruptedException ie) {
                // Ignore
            }
            try {
                fl = PrivilegedFileOpsForTests.persistentRecursiveDelete(dir);
                attempts++;
            } catch (FileNotFoundException fnfe) {
                if (attempts == 0) {
                    fail("directory doesn't exist: " +
                            PrivilegedFileOpsForTests.getAbsolutePath(dir));
                } else  {
                    // In the previous iteration we saw remaining files, but
                    // now the root directory is gone. Not what we expected...
                    System.out.println("<assertDirectoryDeleted> root " +
                            "directory unexpectedly gone - delayed, " +
                            "external or concurrent delete?");
                    return;
                }
            }
            if (fl.length == 0) {
                return;
            } else {
                // Print the list of remaining files to stdout for debugging.
                StringBuffer sb = new StringBuffer();
                sb.append("<assertDirectoryDeleted> attempt ").append(attempts).
                    append(" left ").append(fl.length).
                    append(" files/dirs behind:");
                for (int i = 0; i < fl.length; i++) {
                    sb.append(' ').append(i).append('=').append(fl[i]);
                }
                System.out.println(sb);
            }
        }
        // If we failed to delete some of the files, list them and obtain some
        // information about each file.
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < fl.length; i++) {
            File f = fl[i];
            sb.append(PrivilegedFileOpsForTests.getAbsolutePath(f)).append(' ').
                append(PrivilegedFileOpsForTests.getFileInfo(f)).append(", ");
        }
        sb.deleteCharAt(sb.length() - 1).deleteCharAt(sb.length() - 1);
        fail("Failed to delete " + fl.length + " files (root=" +
                PrivilegedFileOpsForTests.getAbsolutePath(dir) + "): " +
                sb.toString());
    }

    /**
     * Remove the directory and its contents.
     * @param path Path of the directory
     */
    public static void removeDirectory(String path)
    {
        DropDatabaseSetup.removeDirectory(path);
    }
    /**
     * Remove the directory and its contents.
     * @param dir File of the directory
     */
    public static void removeDirectory(File dir)
    {
        DropDatabaseSetup.removeDirectory(dir);
    }
 
    /**
     * Remove all the files in the list
     * @param list the list contains all the files
     */
    public static void removeFiles(String[] list)
    {
        DropDatabaseSetup.removeFiles(list);
    }
    /**
     * Fail; attaching an exception for more detail on cause.
     *
     * @param msg message explaining the failure
     * @param t the cause of the failure
     *
     * @exception AssertionFailedError
     */
    public static void fail(String msg, Throwable t)
            throws AssertionFailedError {
        throw newAssertionFailedError(msg, t);
    }

    /**
     * Create a new AssertionFailedError linked to another Throwable.
     *
     * @param message message explaining the failure
     * @param cause the cause of the failure
     * @return an AssertionFailedError
     */
    public static AssertionFailedError newAssertionFailedError(
            String message, Throwable cause) {
        AssertionFailedError e = new AssertionFailedError(message);
        e.initCause(cause);
        return e;
    }

    /**
     * assert a method from an executing test
     * 
     * @param testLaunchMethod
     *            complete pathname of the method to be executed
     * @throws Exception
     */
    public static void assertLaunchedJUnitTestMethod(String testLaunchMethod)
            throws Exception 
    {
        String[] cmd = new String[] { "junit.textui.TestRunner", "-m",
        testLaunchMethod };
        assertExecJavaCmdAsExpected(new String[] { "OK (1 test)" }, cmd, 0);
    }
    
    /**
     * assert a method from an executing test
     *
     * @param testLaunchMethod
     *            complete pathname of the method to be executed
     * @param databaseName
     *            name of the database to be used
     * @throws Exception
     */
    public static void assertLaunchedJUnitTestMethod(String testLaunchMethod,
            String databaseName)
            throws Exception 
    {
        String[] cmd = new String[] { 
                "-Dderby.tests.defaultDatabaseName=" + databaseName, 
                "junit.textui.TestRunner", "-m", testLaunchMethod };
        assertExecJavaCmdAsExpected(new String[] { "OK (1 test)" }, cmd, 0);
    }

    /** Returns once the system timer has advanced at least one tick. */
    public static void sleepAtLeastOneTick() {
        long currentTime = System.currentTimeMillis(); 
        while (System.currentTimeMillis() == currentTime) {
            sleep(1);
        }
    }

    /** Return true if the JVM is at least at the indicated rev level */
    public static boolean vmAtLeast( int major, int minor )
    {
        String version = AccessController.doPrivileged
            (new PrivilegedAction<String>(){
                public String run(){
                    return System.getProperty( "java.version" );
                }
            }
                );
                   
        JavaVersionHolder jvh =  new JavaVersionHolder( version );

        return jvh.atLeast( major, minor );
    }

    /** Makes the current thread sleep up to {@code ms} milliseconds. */
    public static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException ie) {
            // For now we just print a warning if we are interrupted.
            alarm("sleep interrupted");
        }
    }

    private static String traceClientType() {
       if (TestConfiguration.getCurrent().getJDBCClient().isEmbedded()) {
            return "(emb)";
        } else {
            return "(net)";
        }
    }
    
    private static void setupForDebuggerAttach(ArrayList<String> cmdlist) {
        if (debugPort == 0) {
            // lazy initialization
            String dbp = getSystemProperty("derby.test.debugPortBase");
            debugPort = 8800; // default
            if (dbp != null) {
                try {
                    debugPort = Integer.parseInt(dbp);
                } catch (NumberFormatException e) {
                    // never mind
                }
            }
        }
        
        char suspend = 'y'; // default
        String susp = getSystemProperty("derby.test.debugSuspend");
        if (susp != null && "n".equals(susp.toLowerCase())) {
            suspend = 'n';
        }
        
        cmdlist.add("-Xdebug");
        cmdlist.add("-Xrunjdwp:transport=dt_socket,address=" + (debugPort++) +
                ",server=y,suspend=" + suspend);
    }
} // End class BaseTestCase
