MetamorphicTestCaseFactory.java

package com.github.dakusui.thincrest.metamor;

import com.github.dakusui.thincrest.metamor.internals.InternalUtils;
import com.github.dakusui.thincrest_pcond.forms.Printables;

import java.text.MessageFormat;
import java.util.function.*;

import static com.github.dakusui.thincrest_pcond.forms.Predicates.transform;
import static java.util.Objects.requireNonNull;

/**
 * An interface of a factory for a metamorphic test case.
 *
 * @param <X> Type of "source value".
 * @param <I> Input type of the function under test.
 * @param <O> Output type of function under test.
 * @param <R> Input type of metamorphic relation.
 */
public interface MetamorphicTestCaseFactory<X, I, O, R> {
  /**
   * Returns a function under test.
   *
   * @return a function under test.
   */
  Function<I, O> fut();

  InputResolver.Sequence.Factory<X, I, O> inputResolverSequenceFactory();

  Function<Dataset<IoPair<I, O>>, R> metamorphicTransformer();

  Predicate<R> metamorphicChecker();

  default Predicate<Dataset<IoPair<I, O>>> metamorphicRelation() {
    return transform(metamorphicTransformer()).check(metamorphicChecker());
  }

  /**
   * Returns a function that executes the FUT for each element in `Dataset<InputResolver<I, O>>`.
   *
   * @return A function that executes the FUT for each element in `Dataset<InputResolver<I, O>>`.
   */
  default Function<Dataset<InputResolver<I, O>>, Dataset<IoPair<I, O>>> metamorphicExecutor() {
    return InternalUtils.createObservableProcessingPipeline("fut", this.metamorphicMapper(), this.inputResolverSequenceFactory().count(), inputVariableNameFormatter(), ioVariableName());
  }

  /**
   * A name of input variable.
   * This will be printed in the test report.
   *
   * @return A name of input variable.
   */
  String inputVariableName();

  /**
   * In metamorphic testing context, the function under test is executed multiple times with different input values.
   * The returned function renders input variable names so that they can be identified each other when an index is given.
   * By default, it returns a function that appends the given index.
   *
   * @return A function to render an input variable name corresponding to a given index.
   */
  default IntFunction<String> inputVariableNameFormatter() {
    return i -> this.inputVariableName() + "[" + i + "]";
  }

  default Function<IoContext<InputResolver<I, O>, IoPair<I, O>>, Function<InputResolver<I, O>, IoPair<I, O>>> metamorphicMapper() {
    return Printables.function(
        () -> "  " + fut(),
        ioContext -> Printables.function(
            () -> "input:" + ioContext.output(),
            inputResolver -> {
              I in = inputResolver.apply(ioContext.output());
              return IoPair.create(in, fut().apply(in));
            }));
  }

  /**
   * A builder method that returns a printable predicate that examines the function under test.
   *
   * @return A printable predicate that examines FUT with a given metamorphic relation.
   */
  default Predicate<X> toMetamorphicTestPredicate() {
    return transform(this.inputResolverSequenceFactory().andThen(this.metamorphicExecutor())).check(this.metamorphicRelation());
  }

  String ioVariableName();


  static <I, O> Builder<Object, I, O, Object> forFunctionUnderTest(String name, Function<I, O> fut) {
    return forFunctionUnderTest(Printables.function(name, fut));
  }

  static <I, O> Builder<Object, I, O, Object> forFunctionUnderTest(Function<I, O> fut) {
    return new Builder<Object, I, O, Object>().fut(fut);
  }

  class Impl<X, I, O, R> implements MetamorphicTestCaseFactory<X, I, O, R> {

    private final Function<I, O> fut;
    private final InputResolver.Sequence.Factory<X, I, O> inputResolverSequenceFactory;
    private final Function<Dataset<IoPair<I, O>>, R> metamorphicTransformer;
    private final Predicate<R> metamorphicChecker;
    private final String inputVariableName;
    private final String ioVariableName;

    public Impl(Function<I, O> fut, InputResolver.Sequence.Factory<X, I, O> inputResolverSequenceFactory, Function<Dataset<IoPair<I, O>>, R> metamorphicTransformer, Predicate<R> metamorphicChecker, String inputVariableName, String ioVariableName) {
      this.fut = fut;
      this.inputResolverSequenceFactory = inputResolverSequenceFactory;
      this.metamorphicTransformer = metamorphicTransformer;
      this.metamorphicChecker = metamorphicChecker;
      this.inputVariableName = inputVariableName;
      this.ioVariableName = ioVariableName;
    }

    @Override
    public Function<I, O> fut() {
      return this.fut;
    }

    @Override
    public InputResolver.Sequence.Factory<X, I, O> inputResolverSequenceFactory() {
      return this.inputResolverSequenceFactory;
    }

    @Override
    public Function<Dataset<IoPair<I, O>>, R> metamorphicTransformer() {
      return this.metamorphicTransformer;
    }

    @Override
    public Predicate<R> metamorphicChecker() {
      return this.metamorphicChecker;
    }

    @Override
    public String inputVariableName() {
      return this.inputVariableName;
    }

    @Override
    public String ioVariableName() {
      return this.ioVariableName;
    }

  }

  abstract class BuilderBase<B extends BuilderBase<B, X, I, O, R>, X, I, O, R> {
    abstract static class InputResolverSequenceFactoryProvider<X, I, O> implements Supplier<InputResolver.Sequence.Factory<X, I, O>> {
      final BuilderBase<?, X, I, O, ?> parent;

      protected InputResolverSequenceFactoryProvider(BuilderBase<?, X, I, O, ?> parent) {
        this.parent = parent;
      }

      abstract void add(Function<Object, String> formatter, Function<X, I> function);

      abstract int count();
    }

    protected Function<I, O> fut;
    protected InputResolverSequenceFactoryProvider<X, I, O> inputResolverSequenceFactoryProvider;
    protected Predicate<R> checker;
    protected String sourceVariableName;
    protected String inputVariableName;
    protected String ioVariableName;
    protected String outputVariableName;

    protected BuilderBase() {
      this.sourceVariableName("x")
          .inputVariableName("input")
          .ioVariableName("io")
          .outputVariableName("out");
    }

    protected <BB extends BuilderBase<BB, XX, I, O, RR>, XX, RR> BB newBuilder(Supplier<BB> constructor) {
      return constructor.get()
          .fut(this.fut)
          .sourceVariableName(this.sourceVariableName)
          .inputVariableName(this.inputVariableName)
          .ioVariableName(this.ioVariableName)
          .outputVariableName(this.outputVariableName);
    }

    protected <BB extends BuilderBase<BB, X, I, O, RR>, RR> BB newBuilderWithSpecifiedRelationType(Supplier<BB> constructor) {
      return newBuilder(constructor)
          .inputResolverSequenceFactoryProvider(this.inputResolverSequenceFactoryProvider);
    }

    protected <BB extends BuilderBase<BB, XX, I, O, R>, XX> BB newBuilderWithSpecifiedSourceType(Supplier<BB> constructor) {
      return this.newBuilder(constructor);
    }

    @SuppressWarnings("unchecked")
    public B sourceVariableName(String sourceVariableName) {
      this.sourceVariableName = sourceVariableName;
      return (B) this;
    }

    @SuppressWarnings("unchecked")
    public B inputVariableName(String inputVariableName) {
      this.inputVariableName = inputVariableName;
      return (B) this;
    }

    @SuppressWarnings("unchecked")
    public B outputVariableName(String outputVariableName) {
      this.outputVariableName = requireNonNull(outputVariableName);
      return (B) this;
    }

    @SuppressWarnings("unchecked")
    public B ioVariableName(String ioVariableName) {
      this.ioVariableName = requireNonNull(ioVariableName);
      return (B) this;
    }

    @SuppressWarnings("unchecked")
    B inputResolverSequenceFactoryProvider(InputResolverSequenceFactoryProvider<X, I, O> inputResolverSequenceFactory) {
      this.inputResolverSequenceFactoryProvider = requireNonNull(inputResolverSequenceFactory);
      return (B) this;
    }

    public B inputResolverSequenceFactory(InputResolver.Sequence.Factory<X, I, O> inputResolverSequenceFactory) {
      Utils.requireState(this.inputResolverSequenceFactoryProvider == null, "Input Resolver Sequence Factory is already set.");
      return this.inputResolverSequenceFactoryProvider(new InputResolverSequenceFactoryProvider<X, I, O>(this) {

        @Override
        public InputResolver.Sequence.Factory<X, I, O> get() {
          return inputResolverSequenceFactory;
        }

        @Override
        void add(Function<Object, String> formatter, Function<X, I> function) {
          throw new IllegalStateException();
        }

        @Override
        int count() {
          return inputResolverSequenceFactory.count();
        }
      });
    }

    public B addInputResolvers(Function<InputResolver.Sequence.Factory.Builder<X, I, O>, InputResolver.Sequence.Factory<X, I, O>> b) {
      return this.addInputResolvers(this.sourceVariableName, b);
    }

    public B addInputResolvers(String variableName, Function<InputResolver.Sequence.Factory.Builder<X, I, O>, InputResolver.Sequence.Factory<X, I, O>> b) {
      InputResolver.Sequence.Factory.Builder<X, I, O> ib = new InputResolver.Sequence.Factory.Builder<>(this.inputVariableName, variableName);
      return this.inputResolverSequenceFactory(b.apply(ib));
    }

    @SuppressWarnings("unchecked")
    public B addInputResolver(Function<Object, String> formatter, Function<X, I> f) {
      requireNonNull(formatter);
      requireNonNull(f);
      if (this.inputResolverSequenceFactoryProvider == null) {
        this.inputResolverSequenceFactoryProvider = new InputResolverSequenceFactoryProvider<X, I, O>(this) {
          int count = 0;
          Consumer<InputResolver.Sequence.Factory.Builder<X, I, O>> inputResolverAdder = b -> {
          };

          @Override
          void add(Function<Object, String> formatter, Function<X, I> f) {
            inputResolverAdder = inputResolverAdder.andThen(b -> b.function(formatter, f));
            count++;
          }

          @Override
          int count() {
            return count;
          }

          @Override
          public InputResolver.Sequence.Factory<X, I, O> get() {
            InputResolver.Sequence.Factory.Builder<X, I, O> b = new InputResolver.Sequence.Factory.Builder<>(BuilderBase.this.inputVariableName, BuilderBase.this.sourceVariableName);
            this.inputResolverAdder.accept(b);
            return b.build();
          }
        };
      }
      this.inputResolverSequenceFactoryProvider.add(formatter, f);
      return (B) this;
    }

    public abstract <BB extends BuilderBase<BB, XX, I, O, R>, XX> BB sourceValueType(XX sourceType);

    /**
     * Let this object know the source type.
     *
     * @param sourceType The type of the source value.x
     * @param <BB>       The type of this object.
     * @param <XX>       The type of the input value.
     * @return This object
     */
    @SuppressWarnings("unused")
    public <BB extends BuilderBase<BB, XX, I, O, R>, XX> BB sourceValueType(Class<XX> sourceType) {
      return this.sourceValueType((XX) null);
    }

    /**
     * Let this factory know that the source value and the input values are the same type.
     *
     * @param <BB> The type of this builder.
     * @return This object
     */
    @SuppressWarnings("unchecked")
    public <BB extends BuilderBase<BB, I, I, O, R>> BB makeInputResolversEndomorphic() {
      return (BB) this.sourceValueType((I) null)
          .addInputResolver(x -> String.format("%s", x), Function.identity());
    }


    /**
     * Specifies a function under test.
     *
     * @param fut A function under test
     * @return This builder object
     */
    @SuppressWarnings("unchecked")
    public B fut(Function<I, O> fut) {
      this.fut = requireNonNull(fut);
      return (B) this;
    }

    public <P> MetamorphicTestCaseFactoryWithPreformer.Builder<X, I, O, P, R> withPreformer() {
      return this.newBuilderWithSpecifiedRelationType(MetamorphicTestCaseFactoryWithPreformer.Builder::new);
    }

    public Builder<X, I, O, R> skipPreformer() {
      return this.newBuilderWithSpecifiedRelationType(Builder::new);
    }

    public abstract <P> MetamorphicTestCaseFactoryWithPreformer.Builder<X, I, O, P, R> preformer(Function<IoPair<I, O>, P> preformer);

    public <P> MetamorphicTestCaseFactoryWithPreformer.Builder<X, I, O, P, R> preformer(String preformerName, Function<IoPair<I, O>, P> preformer) {
      return this.preformer(Printables.function(preformerName, preformer));
    }

    public MetamorphicTestCaseFactoryWithPreformer.Builder<X, I, O, O, R> outputOnly() {
      return this.preformer("outputOnly", IoPair::output);
    }

    @SuppressWarnings("unchecked")
    public B checker(Predicate<R> checker) {
      this.checker = requireNonNull(checker);
      return (B) this;
    }

    public MetamorphicTestCaseFactory<X, I, O, R> check(String name, Predicate<R> checker) {
      return this.check(Printables.predicate(name, checker));
    }

    public MetamorphicTestCaseFactory<X, I, O, R> check(Predicate<R> checker) {
      return checker(checker).build();
    }

    public abstract MetamorphicTestCaseFactory<X, I, O, R> build();
  }

  class Builder<X, I, O, R> extends BuilderBase<Builder<X, I, O, R>, X, I, O, R> {
    private Function<Dataset<IoPair<I, O>>, R> transformer;

    public Builder() {
    }

    @SuppressWarnings("unchecked")
    @Override
    public <BB extends BuilderBase<BB, XX, I, O, R>, XX> BB sourceValueType(XX sourceType) {
      return (BB) this.<Builder<XX, I, O, R>, XX>newBuilderWithSpecifiedSourceType(Builder::new);
    }

    public Builder<X, I, O, Proposition> propositionFactory(Function<Dataset<IoPair<I, O>>, Proposition> pf) {
      return this.<Builder<X, I, O, Proposition>, Proposition>newBuilderWithSpecifiedRelationType(Builder::new)
          .transformer(pf)
          .checker(PropositionPredicate.INSTANCE);
    }

    public MetamorphicTestCaseFactory<X, I, O, Proposition> proposition(Function<Object[], String> formatter, Predicate<Dataset<IoPair<I, O>>> p) {
      return this.propositionFactory(
              Proposition.Factory.create(
                  p,
                  formatter,
                  i -> ioVariableName + "[" + i + "]",
                  inputResolverSequenceFactoryProvider.count()))
          .build();
    }

    public MetamorphicTestCaseFactory<X, I, O, Proposition> proposition(String propositionName, Predicate<Dataset<IoPair<I, O>>> p) {
      return this.proposition(args -> MessageFormat.format(propositionName, args), p);
    }

    public <P> MetamorphicTestCaseFactoryWithPreformer.Builder<X, I, O, P, R> preformer(Function<IoPair<I, O>, P> preformer) {
      return this.<P>withPreformer().preformer(preformer);
    }

    public Builder<X, I, O, R> transformer(Function<Dataset<IoPair<I, O>>, R> transformer) {
      this.transformer = requireNonNull(transformer);
      return this;
    }

    @Override
    public MetamorphicTestCaseFactory<X, I, O, R> build() {
      return new Impl<>(this.fut, this.inputResolverSequenceFactoryProvider.get(), this.transformer, this.checker, this.inputVariableName, this.ioVariableName);
    }
  }
}