/** * Candybean is a next generation automation and testing framework suite. * It is a collection of components that foster test automation, execution * configuration, data abstraction, results illustration, tag-based execution, * top-down and bottom-up batches, mobile variants, test translation across * languages, plain-language testing, and web service testing. * Copyright (C) 2013 SugarCRM, Inc. <candybean@sugarcrm.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.sugarcrm.candybean.runner; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.logging.Logger; import javax.xml.bind.JAXBException; import org.junit.Test; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.RunnerScheduler; import com.sugarcrm.candybean.automation.Candybean; import com.sugarcrm.candybean.exceptions.CandybeanException; import com.sugarcrm.candybean.utilities.CandybeanLogger; /** * Custom JUnit test runner class. When a test is annotated to use this runner, * the runner will look at the defined and specified logic to determine if a method * will execute. * * If a test is listed by SimpleClassName.TestName in a given 'block list' file, that * test is not run. This feature provides extra control for not executing tests that * perhaps are known to fail or currently under development. The block list file path * is specified using the system variable "blocklist" (either camel or lower case). * */ public class VRunner extends BlockJUnit4ClassRunner { public static final String BLOCKLIST_PATH_KEY = "blocklist"; public static final String BLOCKLIST_COMMENT = "#"; private static int threadCounter = 0; private static Logger logger; private static Candybean candybean; public VRunner(Class<?> klass) throws InitializationError, SecurityException, IOException { super(klass); try { candybean = Candybean.getInstance(); boolean parallelTests = Boolean.parseBoolean(candybean.config.getValue("parallel.enabled","false")); if (parallelTests) { setScheduler(new NonBlockingAsynchronousRunner(Integer.parseInt(Candybean.getInstance().config.getValue("parallel.threads", "4")))); } } catch (CandybeanException e1) { logger = Logger.getLogger(VRunner.class.getSimpleName()); logger.severe("Unable to instantiate candybean."); } } // TODO add validation/error checking, flexibility in provided class/method name, etc. @Override protected List<FrameworkMethod> computeTestMethods() { /* * We must always use the candybean configured logger, so we attempt * to instantiate candybean to retrieve its named logger first */ try { logger = Logger.getLogger(Candybean.getInstance().getClass().getSimpleName()); ((CandybeanLogger)logger).removeAllHandlers(); } catch (CandybeanException e) { logger = Logger.getLogger(VRunner.class.getSimpleName()); } List<FrameworkMethod> testMethods = getTestClass().getAnnotatedMethods(Test.class); if (testMethods == null || testMethods.size() == 0) { return testMethods; } final List<FrameworkMethod> finalTestMethods = new ArrayList<FrameworkMethod>(testMethods.size()); try { String blockListPathValue = System.getProperty(VRunner.BLOCKLIST_PATH_KEY); if (blockListPathValue != null) { logger.info("Blocklist enabled via system variable: " + VRunner.BLOCKLIST_PATH_KEY + ":" + blockListPathValue); testMethods = this.removeBlockedTests(testMethods); // scrub tests for blocked tests } for (final FrameworkMethod method : testMethods) { logger.finer("method: " + method.getName()); final VTag vTag = method.getAnnotation(VTag.class); if (vTag != null) { if (vTag.tags().length != 0) { if (!vTag.tagLogicClass().isEmpty() && !vTag.tagLogicMethod().isEmpty()) { for (String tag : vTag.tags()) { logger.finer("method:" + method.getName() + ", tag:" + tag + ", logic class:" + vTag.tagLogicClass() + ", logic method:" + vTag.tagLogicMethod()); Class<?> c = Class.forName(vTag.tagLogicClass()); Method m = c.getDeclaredMethod(vTag.tagLogicMethod(), tag.getClass()); if ((boolean) m.invoke(null, (Object) tag)) { logger.info("Adding test to execution list -- tag logic succeeds: " + method.getName()); finalTestMethods.add(method); } } } else { for (String tag : vTag.tags()) { if (Boolean.parseBoolean(System.getProperty(tag))) { logger.info("Adding test to execution list -- sysvar tag true: " + method.getName()); finalTestMethods.add(method); } } } } else { logger.info("Adding test to execution list -- empty tags: " + method.getName()); finalTestMethods.add(method); } } else { logger.info("Adding test to execution list -- no tags: " + method.getName()); finalTestMethods.add(method); } } } catch (Exception e) { logger.severe(e.getMessage()); } return finalTestMethods; } private List<FrameworkMethod> removeBlockedTests(List<FrameworkMethod> tests) throws FileNotFoundException, IOException { Set<String> blockListTests = this.getBlockedTestNames(); logger.info("Blocked tests: "); Iterator<String> iter = blockListTests.iterator(); while (iter.hasNext()) { logger.info(" " + iter.next()); } List<FrameworkMethod> removeTests = new ArrayList<FrameworkMethod>(); for (FrameworkMethod test : tests) { String testName = test.getMethod().getDeclaringClass().getSimpleName() + "." + test.getMethod().getName(); logger.info("Testing for: " + testName); if (blockListTests.contains(testName)) { logger.info("Blocklist test positive. Found."); removeTests.add(test); // add to separate list to avoid concurrent modification } else { logger.info("Blocklist test negative. Not found."); } } for (FrameworkMethod removeTest : removeTests) { logger.info("Removing blocked test from execution: " + removeTest.getName()); tests.remove(removeTest); } return tests; } private Set<String> getBlockedTestNames() throws FileNotFoundException, IOException { try{ Set<String> blockedTestNames = new HashSet<String>(); String blockListPath = System.getProperty(VRunner.BLOCKLIST_PATH_KEY, Candybean.ROOT_DIR + File.separator + "blocklist.txt"); BufferedReader fileReader = new BufferedReader(new FileReader(blockListPath)); String blockLine = fileReader.readLine().trim(); while (blockLine != null) { int commentIndex = blockLine.indexOf(VRunner.BLOCKLIST_COMMENT); while (commentIndex >= 0) { String newBlockLine = blockLine.substring(0, commentIndex); logger.info("Removing comments from blockLine '" + blockLine + "' to '" + newBlockLine + "'"); blockLine = newBlockLine.trim(); commentIndex = blockLine.indexOf(VRunner.BLOCKLIST_COMMENT); } if (!blockLine.equals("")) blockedTestNames.add(blockLine); blockLine = fileReader.readLine(); } fileReader.close(); return blockedTestNames; } catch (FileNotFoundException fnfe) { throw new FileNotFoundException("The given blocklist file was not found; ensure path is correct for the system variable: " + BLOCKLIST_PATH_KEY); } } /** * Adds a {@link TestRecorder} listener to the JUnit {@link RunNotifier} which * listens to a failing state of a test annotated with {@link Record}. */ @Override public void run(final RunNotifier notifier) { try { notifier.addFirstListener(TestRecorder.getInstance()); } catch (SecurityException e) { logger.info("Unable to instantiate test recorder"); } catch (IOException e) { logger.info("Unable to instantiate test recorder"); } catch (JAXBException e) { logger.info("Unable to instantiate test recorder"); } catch (CandybeanException e) { logger.info("Unable to instantiate test recorder"); } super.run(notifier); } private static class NonBlockingAsynchronousRunner implements RunnerScheduler { private final List<Future<Object>> futures = Collections.synchronizedList(new ArrayList<Future<Object>>()); private final ExecutorService fService; public NonBlockingAsynchronousRunner(int threads) { fService = Executors.newFixedThreadPool(threads, new CandybeanThreadFactory(threads)); } public void schedule(final Runnable childStatement) { final Callable<Object> objectCallable = new Callable<Object>() { public Object call() throws Exception { childStatement.run(); return null; } }; futures.add(fService.submit(objectCallable)); } public void finished() { waitForCompletion(); } public void waitForCompletion() { for (Future<Object> each : futures) try { each.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } private static class CandybeanThreadFactory implements ThreadFactory { private int numOfThreads = 4; public CandybeanThreadFactory(int numOfThreads) { super(); this.numOfThreads = numOfThreads; } public Thread newThread(Runnable r) { if(threadCounter == numOfThreads){ threadCounter = 0; } return new Thread(r, candybean.config.getValue("parallel.threadNamePattern","")+threadCounter++); } } }