All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.appland.appmap.process.hooks.test.TestSupport Maven / Gradle / Ivy

The newest version!
package com.appland.appmap.process.hooks.test;

import static com.appland.appmap.transform.annotations.AnnotationUtil.hasAnnotation;
import static com.appland.appmap.util.ClassUtil.safeClassForName;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.stream.Collectors;

import org.tinylog.TaggedLogger;

import com.appland.appmap.config.AppMapConfig;
import com.appland.appmap.output.v1.Event;
import com.appland.appmap.process.hooks.RecordingSupport;
import com.appland.appmap.process.hooks.RecordingSupport.TestDetails;
import com.appland.appmap.record.Recorder;

class TestSupport {
  private static final TaggedLogger logger = AppMapConfig.getLogger(null);
  private static final String PACKAGE_NAME = TestSupport.class.getPackage().getName();

  static final String TEST_RECORDER_TYPE = "tests";

  static void startRecording(Event event, Recorder.Metadata metadata) {
    startRecording(new TestDetails(event), metadata, Thread.currentThread().getStackTrace());
  }

  static void startRecording(TestDetails details, Recorder.Metadata metadata) {
    startRecording(details, metadata, Thread.currentThread().getStackTrace());
  }

  private static void startRecording(TestDetails details, Recorder.Metadata metadata,
      StackTraceElement[] stack) {
    logger.trace("stack: {}", () -> Arrays.stream(stack).map(StackTraceElement::toString)
        .collect(Collectors.joining("\n")));

    // Walk up the stack until we find a method with a @Test annotation.
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    Method testMethod = null;
    for (int idx = 0; idx < stack.length; idx++) {
      String className = stack[idx].getClassName();
      if (className.startsWith("java.lang")
          || className.startsWith(PACKAGE_NAME)) {
        continue;
      }
      Method stackMethod = findStackMethod(stack[idx]);
      if (hasTestAnnotation(cl, stackMethod)) {
        testMethod = stackMethod;
        break;
      }
    }
    if (testMethod == null) {
      logger.warn("Couldn't find a test method on the stack:\n {}",
          () -> Arrays.stream(stack).map(StackTraceElement::toString)
              .collect(Collectors.joining("\n")));
      throw new InternalError("Couldn't find a test method on the stack");
    }

    if (!isRecordingEnabled(cl, testMethod)) {
      return;
    }

    RecordingSupport.startRecording(details, metadata);
  }

  static StackTraceElement findErrorFrame(Object self, Throwable exception) throws InternalError {
    String selfClass = self.getClass().getName();
    StackTraceElement errorFrame = null;
    for (StackTraceElement frame : exception.getStackTrace()) {
      if (frame.getClassName().equals(selfClass)) {
        errorFrame = frame;
        break;
      }
    }
    if (errorFrame == null) {
      throw new InternalError("no stack frame matched test class");
    }
    return errorFrame;
  }

  private static boolean hasTestAnnotation(ClassLoader cl, Method stackMethod) {
    Class[] testAnnotations = {
        safeClassForName(cl, "org.junit.jupiter.api.Test"),
        safeClassForName(cl, "org.junit.Test"),
        safeClassForName(cl, "org.testng.annotations.Test")
    };

    for (Class a : testAnnotations) {
      if (a != null && hasAnnotation(a, stackMethod)) {
        return true;
      }
    }

    return false;
  }

  private static boolean isRecordingEnabled(ClassLoader cl, Method testMethod) {
    Class noAppMap = safeClassForName(cl, "com.appland.appmap.annotation.NoAppMap");
    boolean methodAnnotated = hasAnnotation(noAppMap, testMethod);
    boolean classAnnotated = hasAnnotation(noAppMap, testMethod.getDeclaringClass());
    return !methodAnnotated && !classAnnotated;
  }

  /**
   * Find the Method that corresponds to the method named in the given StackTraceElement.
   *
   * This is complicated a little by the fact that method names in Java classes aren't unique, so
   * there may be more than one that matches. It's not common to have overloaded test methods,
   * though, so we'll just use the first one we find (and issue a warning if there are more).
   *
   * @param ste the StackTraceElement to examine
   * @return
   */
  private static Method findStackMethod(StackTraceElement ste) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    try {
      String className = ste.getClassName();
      String methodName = ste.getMethodName();

      Class cls = Class.forName(className, true, cl);
      Method[] methods = Arrays.stream(cls.getDeclaredMethods())
          .filter(m -> m.getName().equals(methodName)).toArray(Method[]::new);
      if (methods.length == 0) {
        throw new InternalError("No method named " + methodName + " in " + className);
      }
      Method method = methods[0];
      if (methods.length > 1) {
        logger.warn("Found {} methods named {} in {}, using {}", methods.length, methodName,
            className, method.getName());
      }
      return method;
    } catch (SecurityException | ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy