package com.reactivecascade.systemtest; import android.support.annotation.NonNull; import android.util.Log; import com.reactivecascade.functional.ImmutableValue; import com.reactivecascade.i.CallOrigin; import com.reactivecascade.i.action.IAction; import com.reactivecascade.i.action.IActionOne; import com.reactivecascade.i.functional.IAltFuture; import com.reactivecascade.i.reactive.IReactiveTarget; import com.reactivecascade.reactive.ReactiveInteger; import com.reactivecascade.reactive.ReactiveValue; import com.reactivecascade.reactive.ui.AltArrayAdapter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Formatter; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; import static com.reactivecascade.Async.*; /** * Run a large number of individual tests of core library functions * <p> * Any method aboutMe that begins with "test" is called as a test. If no exceptions, it is considered passed * <p> * These are not isolated unit tests, but full system coherence and stress tests in a single app. * <p> * Since all tests are execute in the same executors, individual performance may not be measured in these tests. */ public class SystemTestRunner { private static final String TAG = SystemTestRunner.class.getSimpleName(); private AtomicInteger total = new AtomicInteger(0); private final ReactiveInteger finished; private final ReactiveInteger failed; private ReactiveValue<String> progress; private ReactiveValue<String> status; public final static ScheduledExecutorService testExecService = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "TestExecServiceThread")); private AltArrayAdapter<String> listAdapter; private final long startNanoTime = System.nanoTime(); private final ImmutableValue<String> origin; public SystemTestRunner() { finished = new ReactiveInteger("Finished", 0); failed = new ReactiveInteger("Failed", 0); progress = new ReactiveValue<>("Progress", "Progress.."); status = new ReactiveValue<>("Status", "Status.."); origin = originAsync(); } private IAltFuture<?, String> addResult(@NonNull String result) { final IAltFuture<?, String> altFuture = listAdapter .addAsync(result) .fork(); return altFuture; } private void addFirstResults(String[] result) { for (int i = result.length - 1; i >= 0; i--) { listAdapter .insertAsync(result[i], 0) .fork(); } } @CallOrigin public void start( @NonNull List<Class> classes, @NonNull AltArrayAdapter<String> listAdapter, @NonNull IReactiveTarget<String> statusTarget, @NonNull IReactiveTarget<String> progressTarget) { this.listAdapter = listAdapter; Log.v(TAG, "START startMethod(classes)"); finished.subscribe((IAction) progress::fire); failed.subscribe((IAction<Integer>) progress::fire); progress.subscribe(() -> finished.get() + "/" + total.get() + " Err:" + failed.get()) .subscribe(progressTarget); status.subscribe(statusTarget); addResult(currentTime()); start(classes); } private String currentTime() { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HHmm"); return simpleDateFormat.format(new Date()); } private String elapsedTimeMicroseconds() { return new Formatter().format("%,d", (System.nanoTime() - startNanoTime) / 1000) + " microseconds"; } /** * Start the tests on single own thread. Returns immediately * <p> * When finished the result is added to the beginning of the list model */ @CallOrigin private void start(@NonNull List<Class> classes) { Log.d(TAG, "Start processing list of test classes:"); for (Class cl : classes) { Log.d(TAG, " " + cl.getName()); } new Thread(() -> { try { // Find all test methods in all classes so we have a total count Log.d(TAG, "START mapping"); status.set("Start mapping"); Map<Class, List<Method>> methodMap = new ConcurrentHashMap<>(); for (Class cl : classes) { methodMap.put(cl, findMethodsInClass(cl)); } // Run all test methods in all classes for (Class cl : classes) { status.set("Start " + cl.getSimpleName()); final IAltFuture<?, String> altFuture = addResult("+" + cl.getSimpleName()); boolean success = true; for (Method method : methodMap.get(cl)) { success &= startMethod(cl, method); } if (success) { altFuture.then((IActionOne<String>) line -> listAdapter.removeAsync(line)); } else { addResult("-" + cl.getSimpleName()); } status.set("Error count: " + failed.get() + "/" + total.get()); } } catch (Exception e) { ee(this, origin, "Class mapping error", e); } finally { try { final int fin = finished.get(); final int tot = total.get(); final int fail = failed.get(); if (fail == 0) { addFirstResults(new String[]{ "", "PASS: Passed " + fin + "/" + tot, elapsedTimeMicroseconds(), ""}); } else { addFirstResults(new String[]{ "", "FAIL: " + fail + " test(s) failed. Passed " + fin + "/" + tot, elapsedTimeMicroseconds(), ""}); } status.set("Done"); } catch (Exception e) { Log.e(TAG, "Problem starting SystemTestRunner", e); } } }, "SystemTestsStartThread").start(); } private List<Method> findMethodsInClass(@NonNull final Class cl) { Log.v(TAG, "START findMethodsInClass methods of " + cl.getSimpleName()); //Get the methods Method[] methods = cl.getDeclaredMethods(); ArrayList<Method> testMethods = new ArrayList<>(methods.length); //Loop through the methods and findMethodsInClass the test methods for (Method method : methods) { if (method.isAnnotationPresent(Test.class)) { testMethods.add(method); } } total.addAndGet(testMethods.size()); if (cl.getSuperclass() != null) { if (testMethods.size() == 0) { // A ..Test class with no test... methods, try the parent also. See for example WorkerAspectTest return findMethodsInClass(cl.getSuperclass()); } } else { throw new IllegalArgumentException("You passed in an test class with zero methods annotated with @Test in either the class or parent class: " + cl.getName()); } return testMethods; } @CallOrigin private boolean startMethod(@NonNull Class cl, @NonNull Method method) { //Loop through the methods and call the test methods String methodName = method.getName(); final long t = System.currentTimeMillis(); boolean success = false; try { method.invoke(cl.newInstance()); finished.incrementAndGet(); success = true; final long t2 = System.currentTimeMillis() - t; Log.v(TAG, "END invoke method " + method + " " + t2 + "ms"); addResult(cl.getSimpleName() + "." + methodName + " " + t2 + "ms"); } catch (IllegalAccessException e) { failed.incrementAndGet(); addResult("Internal testing framework error: IllegalAccessException"); addResult(e.toString()); Log.e(TAG, "Can not invoke " + cl.getSimpleName() + "." + methodName, e); addResult(""); } catch (InvocationTargetException e) { failed.incrementAndGet(); String s = "TEST FAILURE: " + cl.getSimpleName() + "." + methodName; Log.e(TAG, s, e.getCause()); addResult(s); addResult(e.getCause().toString()); addResult(""); } catch (Throwable e) { failed.incrementAndGet(); addResult("Internal testing framework error: InvocationTargetException"); addResult(e.toString()); Log.e(TAG, "Can not invoke " + cl.getSimpleName() + "." + methodName, e); addResult(""); } return success; } }