/*
 * The Apache Software License, Version 1.1
 *
 * Copyright (C) 2000-2002 The Apache Software Foundation.  All rights
 * reserved.
 * Copyright (C) 2009 John Lewis
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "Ant" and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package net.sourceforge.cobertura.thread.test;

import groovy.util.AntBuilder;
import groovy.util.Node;
import net.sourceforge.cobertura.ant.ReportTask;
import net.sourceforge.cobertura.test.util.TestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.tools.ant.taskdefs.Echo;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.types.DirSet;
import org.apache.tools.ant.types.Path;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

public class MultipleClassloaderFunctionalTest {

	/*
	 * This tests the case where there are multiple classloaders that each
	 * load the Cobertura classes.   Cobertura uses static fields to
	 * hold the data.   When there are multiple classloaders, each classloader
	 * will keep track of the line counts for the classes that it loads.  
	 * 
	 * The static initializers for the Cobertura classes are also called for
	 * each classloader.   So, there is one shutdown hook for each classloader.
	 * So, when the JVM exits, each shutdown hook will try to write the
	 * data it has kept to the datafile.   They will do this at the same
	 * time.   Before Java 6, this seemed to work fine, but with Java 6, there
	 * seems to have been a change with how file locks are implemented.   So,
	 * care has to be taken to make sure only one thread locks a file at a time.
	 * 
	 */

	AntBuilder ant = TestUtils.getCoberturaAntBuilder(TestUtils
			.getCoberturaClassDir());
	TestUtils testUtil = new TestUtils();

	private static String CALLED_CODE = "\n package mypackage;" + "\n "
			+ "\n public class Called {" + "\n	 public static void callThis()"
			+ "\n  {" + "\n 	}" + "\n }";

	@Before
	public void setUp() throws IOException {
		FileUtils.deleteDirectory(new File("/tmp/src"));
	}

	@After
	public void tearDown() throws IOException {
		FileUtils.deleteDirectory(new File("/tmp/src"));
	}

	@Test
	public void multipleClassloadersTest() throws Exception {
		Echo echo = new Echo();
		echo.setMessage("Running multiple classloader test.");
		echo.execute();
		runTest();
	}

	/*
	 * This code creates a Java class that has a main method that will create two
	 * classloaders.   Each classloader will then load the mypackage.Called class
	 * defined above.  Then, the mypackage.Called.callThis() method is called once.
	 */
	private String getMainCode(File instrumentDir) {
		return "\n package mypackage;"
				+ "\n "
				+ "\n import java.net.URLClassLoader;"
				+ "\n import java.net.URL;"
				+ "\n import java.io.File;"
				+ "\n import java.lang.reflect.Method;"
				+ "\n "
				+ "\n public class Main {"
				+ "\n 	public static void main(String[] args) throws Throwable"
				+ "\n 	{"
				+ "\n 		createClassloaderAndCallMethod();"
				+ "\n  		createClassloaderAndCallMethod();"
				+ "\n 	}"
				+ "\n "
				+ "\n 	/*"
				+ "\n 	 * Create a classloader that loads the instrumented code and the cobertura classes."
				+ "\n 	 * Then, call the mypackage.Called.callThis() static method."
				+ "\n 	 */"
				+ "\n 	public static void createClassloaderAndCallMethod() throws Throwable"
				+ "\n 	{"
				+ "\n 		File instrumentDir = new File(\""
				+ instrumentDir.getAbsolutePath().replace('\\', '/')
				+ "\");"
				+ "\n 		File coberturaClassDir = new File(\""
				+ TestUtils.getCoberturaClassDir().getAbsolutePath()
						.replace('\\', '/')
				+ "\");"
				+ "\n 		File dependencySlf4j = new File(\""
				+ TestUtils.createDependencyPath("org.slf4j", "slf4j-api", "1.7.5").toString()
						.replace('\\', '/')
				+ "\");"
				+ "\n "
				+ "\n 		/*"
				+ "\n 		 * Create a classloader with a null parent classloader to ensure that this"
				+ "\n 		 * classloader loads the Cobertura classes itself."
				+ "\n 		 */"
				+ "\n          URLClassLoader loader = new URLClassLoader("
				+ "\n         		new URL[] {instrumentDir.toURL(), coberturaClassDir.toURL(), dependencySlf4j.toURL()}, null);"
				+ "\n "
				+ "\n 		// use reflection to call mypackage.Called.callThis() once."
				+ "\n         Class calledClass = loader.loadClass(\"mypackage.Called\");"
				+ "\n         Method method = calledClass.getMethod(\"callThis\", null);"
				+ "\n " + "\n         method.invoke(null, null);" + "\n 	}"
				+ "\n }";
	}

	private void runTest() throws Exception {
		/*
		 * Use a temporary directory and create a Main.java source file
		 * that creates multiple classloaders.   Also, create a Called.java
		 * file that defines the mypackage.Called.callThis() method that
		 * will be called by mypackage.Main.main().
		 */
		File tempDir = TestUtils.getTempDir();
		File srcDir = new File(tempDir, "src");
		File instrumentDir = new File(tempDir, "instrument");

		File mainSourceFile = new File(srcDir, "mypackage/Main.java");
		File datafile = new File(srcDir, "cobertura.ser");
		mainSourceFile.getParentFile().mkdirs();

		BufferedWriter bw = null;
		try {
			bw = new BufferedWriter(new FileWriter(mainSourceFile));
			bw.write(getMainCode(instrumentDir));
		} catch (IOException e) {
			e.printStackTrace();
			fail();
		} finally {
			IOUtils.closeQuietly(bw);
		}

		File calledSourceFile = new File(srcDir, "mypackage/Called.java");

		try {
			bw = new BufferedWriter(new FileWriter(calledSourceFile));
			bw.write(CALLED_CODE);
		} catch (IOException e) {
			e.printStackTrace();
			fail();
		} finally {
			IOUtils.closeQuietly(bw);
		}

		TestUtils.compileSource(ant, srcDir);

		TestUtils.instrumentClasses(ant, srcDir, datafile, instrumentDir);

		/*
		 * Kick off the Main class.   I'll use the non-instrumented classes, but
		 * I think you could use the instrumented ones as well.
		 */
		DirSet dirSet = new DirSet();
		dirSet.setDir(srcDir);
		dirSet.setProject(TestUtils.project);
		Path classpath = new Path(TestUtils.project);
		classpath.addDirset(dirSet);
		classpath.addDirset(TestUtils.getCoberturaClassDirSet());
		// This is needed to fix issue #213
		classpath.add(TestUtils.createDependencyPath("org.slf4j", "slf4j-api", "1.7.5"));

		Java java = new Java();
		java.setProject(TestUtils.project);
		java.setClassname("mypackage.Main");
		java.setDir(srcDir);
		java.setFork(true);
		java.setFailonerror(true);
		java.setClasspath(classpath);
		java.execute();

		/*
		 * Now create a cobertura xml file and make sure the correct counts are in it.
		 */
		ReportTask reportTask = new ReportTask();
		reportTask.setProject(TestUtils.project);
		reportTask.setDataFile(datafile.getAbsolutePath());
		reportTask.setFormat("xml");
		reportTask.setDestDir(srcDir);
		reportTask.execute();

		Node dom = TestUtils.getXMLReportDOM(srcDir.getAbsolutePath()
				+ "/coverage.xml");

		List<Node> lines = TestUtils.getLineCounts(dom, "mypackage.Called",
				"callThis", null);

		// the callThis() method is empty, so the only line is the ending brace.
		assertEquals(1, lines.size());

		for (int i = 0; i < lines.size(); i++) {
			assertEquals("hit count incorrect", "2", lines.get(i).attribute(
					"hits"));
		}
	}
}
