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

jp.co.moneyforward.autotest.framework.internal.InternalUtils Maven / Gradle / Ivy

The newest version!
package jp.co.moneyforward.autotest.framework.internal;

import com.github.dakusui.actionunit.actions.Composite;
import com.github.dakusui.actionunit.actions.Leaf;
import com.github.dakusui.actionunit.core.Action;
import com.github.dakusui.actionunit.core.Context;
import com.github.dakusui.osynth.core.utils.MethodUtils;
import com.github.valid8j.pcond.forms.Printables;
import jp.co.moneyforward.autotest.framework.annotations.DependsOn;
import jp.co.moneyforward.autotest.framework.annotations.Given;
import jp.co.moneyforward.autotest.framework.annotations.Named;
import jp.co.moneyforward.autotest.framework.annotations.When;
import jp.co.moneyforward.autotest.framework.core.AutotestException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.opentest4j.TestAbortedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static com.github.dakusui.actionunit.core.ActionSupport.leaf;
import static com.github.dakusui.actionunit.utils.InternalUtils.toStringIfOverriddenOrNoname;
import static com.github.valid8j.classic.Requires.requireNonNull;
import static com.github.valid8j.pcond.internals.InternalUtils.getMethod;
import static java.io.File.createTempFile;
import static java.lang.Thread.currentThread;
import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;
import static jp.co.moneyforward.autotest.actions.web.SendKey.MASK_PREFIX;
import static jp.co.moneyforward.autotest.framework.action.Scene.DUMMY_OUTPUT_VARIABLE_NAME;

/// An internal utility class of the **insdog** framework.
public enum InternalUtils {
  ;
  
  public static final Logger LOGGER = LoggerFactory.getLogger(InternalUtils.class);
  
  /// Returns an `Optional` of a `String` that contains a branch name.
  /// This method internally calls `InternalUtils#currentBranchNameFor(new File("."))`.
  ///
  /// @return An `Optional` of branch name `String`.
  /// @see InternalUtils#currentBranchNameFor(File)
  public static Optional currentBranchName() {
    return currentBranchNameFor(projectDir());
  }
  
  /// Returns an `Optional` of a `String` that contains a branch name, if the given `projectDir` has `.git` directory and a current branch name of it can be retrieved.
  /// An exception will be thrown on a failure during this step.
  ///
  /// Otherwise, an empty `Optional` will be returned.
  ///
  /// @return An `Optional` of branch name `String`.
  public static Optional currentBranchNameFor(File projectDir) {
    if (!projectDir.exists())
      return Optional.empty();
    
    var builder = new FileRepositoryBuilder()
        .setMustExist(true)
        .findGitDir(projectDir.getAbsoluteFile())
        .readEnvironment();
    if (builder.getGitDir() == null)
      return Optional.empty();
    
    try {
      //NOSONAR
      try (Repository repository = builder.build()) {
        return Optional.of(repository.getBranch());
      }
    } catch (IOException e) {
      throw wrap(e);
    }
  }
  
  public static boolean isPresumablyRunningFromIDE() {
    return !isRunByTool();
  }
  
  private static boolean isRunByTool() {
    return isRunByGithubActions()
        || isRunUnderPitest()
        || isRunUnderSurefire();
  }
  
  private static boolean isRunByGithubActions() {
    return !Objects.equals(System.getenv("GITHUB_ACTIONS"), null);
  }
  
  public static boolean isRunUnderSurefire() {
    return System.getProperty("surefire.real.class.path") != null;
  }
  
  public static boolean isRunUnderPitest() {
    return Objects.equals(System.getProperty("underpitest"), "yes");
  }
  
  public static String composeResultMessageLine(Class testClass, String stageName, String line) {
    return String.format("%-20s: %-11s %s", testClass.getSimpleName(), stageName + ":", line);
  }
  
  public static File projectDir() {
    return new File(".");
  }
  
  public static String simpleClassNameOf(Class clazz) {
    return MethodUtils.simpleClassNameOf(clazz);
  }
  
  public static Stream flattenIfSequential(Action a) {
    return a instanceof Composite composite && !composite.isParallel() ? composite.children().stream()
                                                                       : Stream.of(a);
  }
  
  /// A shorthand method of `shorten(string, 120)`.
  ///
  /// @param string A string to be shortened.
  /// @return A shortened string.
  public static String shorten(String string) {
    return shorten(string, 120);
  }
  
  /// Shorten a `string` to the specified `length`.
  /// In case `string` contains  a carriage return (`\r`), a substring from the beginning of the `string` to the position
  /// of the character will be returned.
  ///
  /// @param string A string to be shortened.
  /// @param length A length to which `string` to be shortened.
  /// @return A shortened string.
  public static String shorten(String string, int length) {
    int crPos = string.indexOf('\r');
    return string.substring(0, Math.min(length,
                                        crPos < 0 ? string.length()
                                                  : crPos));
  }
  
  public static String mask(Object o) {
    return Objects.toString(o).replaceAll("((" + MASK_PREFIX + ").*)", MASK_PREFIX);
  }
  
  public static Stream flattenSequentialAction(Action action) {
    // NOSONAR: In Java 21, there is a feature called pattern matching, where this use of switch should be encouraged.
    return switch (action) {
      case Composite a -> (!a.isParallel()) ? a.children().stream()
                                            : Stream.of(a);
      default -> Stream.of(action);
    };
  }
  
  /// Returns a "name" a given `method`.
  /// If the method has `@Named` annotation and its value is set, the value will be returned.
  /// If the value is equal to `Named.DEFAULT_VALUE`, the name of the method itself will be returned.
  ///
  /// This method should be called for a method with `@Named` annotation.
  ///
  /// @param m A method whose name should be returned.
  /// @return The name of the method the framework recognizes.
  public static String nameOf(Method m) {
    Named annotation = m.getAnnotation(Named.class);
    //NOSONAR
    assert annotation != null : Objects.toString(m);
    if (!Objects.equals(annotation.value(), Named.DEFAULT_VALUE)) return annotation.value();
    return m.getName();
  }
  
  /// Note that resolution is done based on the value of `Named` annotation first.
  ///
  /// @param methodName A name of a method to be found.
  /// @param klass      A class from which a method is searched.
  /// @return An optional containing a found method, otherwise, empty.
  public static Optional findMethodByName(String methodName, Class klass) {
    return Arrays.stream(klass.getMethods())
                 .filter(m -> m.isAnnotationPresent(Named.class))
                 .filter(m -> Objects.equals(nameOf(m), methodName))
                 .findFirst();
  }
  
  public static boolean isDependencyAnnotationPresent(Method m) {
    //noinspection removal
    return m.isAnnotationPresent(Given.class)
        || m.isAnnotationPresent(When.class)
        || m.isAnnotationPresent(DependsOn.class);
  }
  
  public static String[] getDependencyAnnotationValues(Method m) {
    //noinspection removal
    return InternalUtils.concat(Arrays.stream(Optional.ofNullable(m.getAnnotation(Given.class))
                                                      .map(Given::value)
                                                      .orElse(new String[0])),
                                Arrays.stream(Optional.ofNullable(m.getAnnotation(When.class))
                                                      .map(When::value)
                                                      .orElse(new String[0])),
                                Arrays.stream(Optional.ofNullable(m.getAnnotation(DependsOn.class))
                                                      .map(DependsOn::value)
                                                      .orElse(new String[0])))
                        .toArray(String[]::new);
  }
  
  public static String variableNameToString(String variableName) {
    if (Objects.equals(variableName, DUMMY_OUTPUT_VARIABLE_NAME))
      return "";
    if (Objects.equals(variableName, "*ALL*"))
      return "*";
    return variableName;
  }
  
  public static String spaces(int i) {
    return " ".repeat(Math.max(0, i));
  }
  
  // NOSONAR: Intrusive warning. Number of hierarchical depth should not be checked against very well known library such as opentest4j
  public static class AssumptionViolation extends TestAbortedException {
    public AssumptionViolation(String message) {
      super(message);
    }
  }
  
  /// Creates a `Date` object from a string formatted with `MMM/dd/yyyy`.
  /// `Locale.US` is used to create a `SimpleDateFormat` object.
  ///
  /// @param dateString A string from which a `Date` object is created.
  /// @return A date object created from `dateString`.
  public static Date date(String dateString) {
    try {
      return new SimpleDateFormat("MMM/dd/yyyy", Locale.US).parse(dateString);
    } catch (ParseException e) {
      throw wrap(e);
    }
  }
  
  /// Returns a `Date` object from the current date.
  ///
  /// @return A date object created from the current date.
  public static Date now() {
    return new Date();
  }
  
  public static String dateToSafeString(Date date) {
    return new SimpleDateFormat("HHmmss", Locale.US).format(date).replaceAll("[,. :\\-/]", "");
  }
  
  /// Concatenates given streams.
  ///
  /// @param streams Streams to be concatenated.
  /// @param      The type of the values streamed by the given `streams`.
  /// @return Concatenated stream.
  @SafeVarargs
  public static  Stream concat(Stream... streams) {
    if (streams.length == 0)
      return Stream.empty();
    if (streams.length == 1)
      return streams[0];
    if (streams.length == 2)
      return Stream.concat(streams[0], streams[1]);
    else
      return Stream.concat(streams[0], concat(Arrays.copyOfRange(streams, 1, streams.length)));
  }
  
  /// Returns an action context for InsDog.
  /// The returned context is designed to print a proper message when each value in the action context is a variable store.
  ///
  /// @return A created context.
  public static Context createContext() {
    class InsDogContext extends Context.Impl {
      @Override
      public  V valueOf(String variableStoreName) {
        try {
          return super.valueOf(variableStoreName);
        } catch (NoSuchElementException e) {
          String message = "Variable Store: <" + variableStoreName + "> not found. ";
          LOGGER.error(message);
          LOGGER.debug("Caused by: <" + e.getMessage() + ">", e);
          throw new AutotestException(message + ": Caused by: <" + e.getMessage() + ">", null);
        }
      }
    }
    return new InsDogContext();
  }
  
  /// Creates a consumer, which gives a `consumerName`, when `toString` method is called.
  ///
  /// @param consumerName A name of the created consumer. Returned from `toString`.
  /// @param consumer     A consumer from which the returned object is created.
  /// @return A consumer which executes the `accept` method of the consumer and returns `consumerName` for `toString`.
  public static Consumer printableConsumer(final String consumerName, Consumer consumer) {
    return new Consumer<>() {
      @Override
      public void accept(Context context) {
        consumer.accept(context);
      }
      
      @Override
      public String toString() {
        return consumerName.replace("\n", " ");
      }
    };
  }
  
  /// Creates a leaf action, which executes the `accept` method of `contextConsumer`.
  /// Inside this method, the given `contextConsumer` method is made printable using the `printableConsumer` method.
  /// Then it will be passed to `ActionSupport#leaf` method to turn it into an action.
  ///
  /// @param name            A name of the action.
  /// @param contextConsumer A consumer to define the behavior of the returned action.
  /// @return A leaf action created from the `contextConsumer`.
  public static Action action(String name, Consumer contextConsumer) {
    return leaf(printableConsumer(name, contextConsumer));
  }
  
  /// Creates a trivial leaf action, which is the same as an action created by `InternalUtils.action(String, Consumer)`.
  ///
  /// @param name            A name of the action.
  /// @param contextConsumer A consumer that defines the behavior of the action.
  public static Action trivialAction(String name, Consumer contextConsumer) {
    return new TrivialAction(printableConsumer(name, contextConsumer));
  }
  
  /// Returns a predicate that tests if the date given to it is after the `date`.
  ///
  /// @param date The returned predicate returns `true` if a given date is after this.
  /// @return A predicate to check if a given date is after `date`.
  public static Predicate dateAfter(Date date) {
    return Printables.predicate("after[" + date + "]", d -> d.after(date));
  }
  
  /// Checks if the given `object` has a `toString` method which overrides `Object#toString`.
  ///
  /// @param object An object to be checked.
  /// @return `true` - `toString` method is overridden / `false` - otherwise.
  public static boolean isToStringOverridden(Object object) {
    return getMethod(object.getClass(), "toString").getDeclaringClass() != Object.class;
  }
  
  ///
  /// // @formatter:off
  /// Wraps a given exception `e` with a framework specific exception, `AutotestException`.
  ///
  /// This method has `RuntimeException` as return value type, however, this method will never return a value but throws an exception.
  /// The return type is defined to be able to write a caller code in the following style, which increases readability.
  ///
  /// ```java
  /// try {
  ///   doSomthing()
  /// } catch (SomeCheckedException e) {
  ///   throw wrap(e);
  /// }
  /// ```
  ///
  /// If a given exception `e` is a `RuntimeException`, or an `Error`, it will not be wrapped, but `e` will be directly thrown.
  ///
  /// // @formatter:on
  ///
  /// @param e An exception to be wrapped.
  /// @return This method will never return any value.
  ///
  public static RuntimeException wrap(Throwable e) {
    if (e instanceof InvocationTargetException exception) {
      e = exception.getTargetException();
    }
    if (e instanceof RuntimeException exception) {
      throw exception;
    }
    if (e instanceof Error error) {
      throw error;
    }
    throw new AutotestException("Exception was detected: [" + e.getClass().getSimpleName() + "]: " + e.getMessage(), e);
  }
  
  /// Write a given `text` to a `file`.
  /// When the `file` already exists, `text` will be appended to it.
  /// `text` will be encoded into `UTF-8` since this method calls `Files.writeString(Path,String,OpenOption...)` internally.
  ///
  /// In case the `file` doesn't exist or its parent directories don't exist, this function will try to create them.
  ///
  /// On a failure, a runtime exception will be thrown.
  ///
  /// @param file A file to which `text` is written to.
  /// @param text A data to be written.
  public static void writeTo(File file, String text) {
    try {
      Files.createDirectories(file.getParentFile().toPath());
      Files.writeString(file.getAbsoluteFile().toPath(),
                        text,
                        CREATE,
                        APPEND);
    } catch (IOException e) {
      throw new AutotestException("Exception occurred while writing to file: " + file, e);
    }
  }
  
  /// Removes a given `file`, if exists.
  /// If it doesn't exist, this method does nothing.
  /// If the `file` is a directory, it must be empty.
  /// Otherwise, an exception will be thrown.
  ///
  /// @param file A file to be deleted.
  ///                                                             Must not be `null`.
  public static void removeFile(File file) {
    try {
      Path pathToDelete = requireNonNull(file).toPath();
      if (pathToDelete.toFile().exists()) {
        Files.delete(pathToDelete);
      }
    } catch (IOException e) {
      throw wrap(e);
    }
  }
  
  public static  List reverse(List list) {
    ArrayList reversed = new ArrayList<>(list);
    Collections.reverse(reversed);
    return reversed;
  }
  
  public record Entry(K key, V value) {
    public static  Entry $(K key, V value) {
      return new Entry<>(key, value);
    }
  }
  
  /// Copies the contents of a resource file from the classpath to a temporary file
  ///
  /// @param resourcePath A path to a resource on a class path to be materialized
  /// @return a temporary file path containing the contents of the resource
  public static File materializeResource(String resourcePath) {
    requireNonNull(resourcePath);
    try {
      var output = createTempFile("tmp", ".png", temporaryDirectory());
      output.deleteOnExit();
      materializeResource(output, resourcePath);
      return output;
    } catch (IOException e) {
      throw wrap(e);
    }
  }
  
  /// Copies the contents of a resource file from the classpath to a specified output file.
  ///
  /// @param output       The output file to which the resource contents will be written
  /// @param resourcePath A path to a resource on a class path to be materialized
  public static void materializeResource(File output, final String resourcePath) {
    requireNonNull(output);
    requireNonNull(resourcePath);
    var fileInputStreamOptional = Optional.ofNullable(currentThread().getContextClassLoader().getResourceAsStream(resourcePath));
    try (var fileInputStream = fileInputStreamOptional.orElseThrow(() -> new FileNotFoundException("Not found resource:<" + resourcePath + "> on the classpath"));
         var in = new BufferedInputStream(fileInputStream);
         var out = new BufferedOutputStream(new FileOutputStream(output))) {
      copyTo(in, out);
    } catch (IOException e) {
      throw wrap(e);
    }
  }
  
  public static void copyTo(InputStream in1, OutputStream out1) {
    try (var in = in1;
         var out = out1) {
      while (true) {
        byte[] bt = in.readNBytes(1024);
        if (bt.length == 0) break;
        out.write(bt);
        out.flush();
      }
    } catch (IOException e) {
      throw wrap(e);
    }
  }
  
  static final File TEMPORARY_DIRECTORY;
  
  public static File temporaryDirectory() {
    return TEMPORARY_DIRECTORY;
  }
  
  public static class TrivialAction implements Leaf {
    final Consumer consumer;
    
    public TrivialAction(Consumer consumer) {
      this.consumer = requireNonNull(consumer);
    }
    
    @Override
    public Runnable runnable(Context context) {
      return () -> consumer.accept(context);
    }
    
    @Override
    public void formatTo(Formatter formatter, int flags, int width, int precision) {
      formatter.format("%s", toStringIfOverriddenOrNoname(consumer));
    }
  }
  
  static {
    File dir = new File(new File(new File(System.getProperty("user.dir")), ".dependencies"), "tmp");
    if (dir.mkdirs()) {
      LOGGER.debug("Created temporary directory: {}", dir);
    }
    TEMPORARY_DIRECTORY = dir;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy