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

com.indeed.proctor.common.ProctorUtils Maven / Gradle / Ivy

The newest version!
package com.indeed.proctor.common;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.indeed.proctor.common.el.MulticontextReadOnlyVariableMapper;
import com.indeed.proctor.common.model.Allocation;
import com.indeed.proctor.common.model.Audit;
import com.indeed.proctor.common.model.ConsumableTestDefinition;
import com.indeed.proctor.common.model.Payload;
import com.indeed.proctor.common.model.Range;
import com.indeed.proctor.common.model.TestBucket;
import com.indeed.proctor.common.model.TestDefinition;
import com.indeed.proctor.common.model.TestMatrixArtifact;
import com.indeed.proctor.common.model.TestMatrixDefinition;
import com.indeed.proctor.common.model.TestMatrixVersion;
import com.indeed.proctor.common.model.TestType;
import org.apache.commons.lang3.StringUtils;
import org.apache.el.ExpressionFactoryImpl;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.Strings;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.el.ELContext;
import javax.el.ExpressionFactory;
import javax.el.FunctionMapper;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.joining;

/** Helper functions mostly to verify TestMatrix instances. */
public abstract class ProctorUtils {
    private static final ObjectMapper OBJECT_MAPPER_NON_AUTOCLOSE =
            Serializers.lenient().configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
    private static final ObjectWriter OBJECT_WRITER =
            OBJECT_MAPPER_NON_AUTOCLOSE.writerWithDefaultPrettyPrinter();
    private static final ObjectMapper OBJECT_MAPPER = Serializers.lenient();
    private static final Logger LOGGER = LogManager.getLogger(ProctorUtils.class);
    private static final SpecificationGenerator SPECIFICATION_GENERATOR =
            new SpecificationGenerator();
    public static final String UNITLESS_ALLOCATION_IDENTIFIER = "missingExperimentalUnit";

    public static MessageDigest createMessageDigest() {
        try {
            return MessageDigest.getInstance("MD5");
        } catch (final NoSuchAlgorithmException e) {
            throw new RuntimeException("Impossible no MD5", e);
        }
    }

    @Nonnull
    public static Map convertToValueExpressionMap(
            @Nonnull final ExpressionFactory expressionFactory,
            @Nonnull final Map values) {
        final Map context = new HashMap<>(values.size());
        for (final Entry entry : values.entrySet()) {
            final ValueExpression ve =
                    expressionFactory.createValueExpression(entry.getValue(), Object.class);
            context.put(entry.getKey(), ve);
        }
        return context;
    }

    @Nonnull
    public static Map convertConstantsToValueExpressionMap(
            @Nonnull final ExpressionFactory expressionFactory,
            @Nonnull final Map values) {
        final Map context =
                convertToValueExpressionMap(expressionFactory, values);
        context.put(
                UNITLESS_ALLOCATION_IDENTIFIER,
                expressionFactory.createValueExpression(true, Object.class));
        return context;
    }

    @SuppressWarnings("UnusedDeclaration") // TODO Remove?
    public static String convertToArtifact(@Nonnull final TestMatrixVersion testMatrix)
            throws IOException {
        try (final StringWriter sw = new StringWriter()) {
            final TestMatrixArtifact artifact = convertToConsumableArtifact(testMatrix);
            serializeArtifact(sw, artifact);
            return sw.toString();
        }
    }

    /** @deprecated Use serialization library like jackson */
    @Deprecated
    public static void serializeArtifact(final Writer writer, final TestMatrixArtifact artifact)
            throws IOException {
        serializeObject(writer, artifact);
    }

    /** @deprecated Use serialization library like jackson */
    @Deprecated
    public static void serializeArtifact(final JsonGenerator jsonGenerator, final Proctor proctor)
            throws IOException {
        jsonGenerator.writeObject(proctor.getArtifact());
    }

    /** @deprecated Use serialization library like jackson */
    @Deprecated
    @SuppressWarnings("UnusedDeclaration") // TODO Remove?
    public static void serializeTestDefinition(final Writer writer, final TestDefinition definition)
            throws IOException {
        serializeObject(writer, definition);
    }

    /** @deprecated Use serialization library like jackson */
    @Deprecated
    @SuppressWarnings("UnusedDeclaration")
    public static JsonNode readJsonFromFile(final File input) throws IOException {
        return OBJECT_MAPPER.readValue(input, JsonNode.class);
    }

    /** @deprecated Use serialization library like jackson */
    @Deprecated
    public static void serializeTestSpecification(
            final Writer writer, final TestSpecification specification) throws IOException {
        serializeObject(writer, specification);
    }

    private static  void serializeObject(final Writer writer, final T artifact)
            throws IOException {
        OBJECT_WRITER.writeValue(writer, artifact);
    }

    @Nonnull
    public static TestMatrixArtifact convertToConsumableArtifact(
            @Nonnull final TestMatrixVersion testMatrix) {
        final Audit audit = new Audit();
        final Date published =
                Preconditions.checkNotNull(testMatrix.getPublished(), "Missing publication date");
        audit.setUpdated(published.getTime());
        audit.setVersion(testMatrix.getVersion());
        audit.setUpdatedBy(testMatrix.getAuthor());

        final TestMatrixArtifact artifact = new TestMatrixArtifact();
        artifact.setAudit(audit);

        final TestMatrixDefinition testMatrixDefinition =
                Preconditions.checkNotNull(
                        testMatrix.getTestMatrixDefinition(), "Missing test matrix definition");

        final Map testDefinitions = testMatrixDefinition.getTests();

        final Map consumableTestDefinitions =
                Maps.newLinkedHashMap();
        for (final Entry entry : testDefinitions.entrySet()) {
            final TestDefinition td = entry.getValue();
            final ConsumableTestDefinition ctd = ConsumableTestDefinition.fromTestDefinition(td);
            consumableTestDefinitions.put(entry.getKey(), ctd);
        }

        artifact.setTests(consumableTestDefinitions);
        return artifact;
    }

    /** @deprecated Use {@link ConsumableTestDefinition#fromTestDefinition} */
    @Nonnull
    @Deprecated
    public static ConsumableTestDefinition convertToConsumableTestDefinition(
            @Nonnull final TestDefinition td) {
        return ConsumableTestDefinition.fromTestDefinition(td);
    }

    public static ProctorSpecification readSpecification(final File inputFile) {
        final ProctorSpecification spec;
        InputStream stream = null;
        try {
            stream = new BufferedInputStream(new FileInputStream(inputFile));
            spec = readSpecification(stream);
        } catch (final IOException e) {
            throw new RuntimeException("Unable to read test set from " + inputFile, e);
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (final IOException e) {
                    LOGGER.error("Suppressing throwable thrown when closing " + inputFile, e);
                }
            }
        }
        return spec;
    }

    public static ProctorSpecification readSpecification(final InputStream inputFile) {
        final ProctorSpecification spec;
        try {
            spec = OBJECT_MAPPER_NON_AUTOCLOSE.readValue(inputFile, ProctorSpecification.class);
        } catch (final IOException e) {
            throw new RuntimeException("Unable to read test set from " + inputFile, e);
        }
        return spec;
    }

    /**
     * Verifies that the TestMatrix is compatible with all the required tests. Removes non-required
     * tests from the TestMatrix Replaces invalid or missing tests (buckets are not compatible) with
     * default implementation returning the fallback value see defaultFor
     *
     * @param testMatrix the {@link TestMatrixArtifact} to be verified.
     * @param matrixSource a {@link String} of the source of proctor artifact. For example, a path
     *     of proctor artifact file.
     * @param requiredTests a {@link Map} of required test. The {@link TestSpecification} would be
     *     verified
     * @param functionMapper a given el {@link FunctionMapper}
     * @return a {@link ProctorLoadResult} to describe the result of verification. It contains
     *     errors of verification and a list of missing test.
     */
    public static ProctorLoadResult verifyAndConsolidate(
            @Nonnull final TestMatrixArtifact testMatrix,
            final String matrixSource,
            @Nonnull final Map requiredTests,
            @Nonnull final FunctionMapper functionMapper) {
        return verifyAndConsolidate(
                testMatrix,
                matrixSource,
                requiredTests,
                functionMapper,
                ProvidedContext.nonEvaluableContext(),
                Collections.emptySet());
    }

    public static ProctorLoadResult verifyAndConsolidate(
            @Nonnull final TestMatrixArtifact testMatrix,
            final String matrixSource,
            @Nonnull final Map requiredTests,
            @Nonnull final FunctionMapper functionMapper,
            final ProvidedContext providedContext) {
        return verifyAndConsolidate(
                testMatrix,
                matrixSource,
                requiredTests,
                functionMapper,
                providedContext,
                Collections.emptySet());
    }

    /** @param testMatrix will be modified by removing unused tests and adding missing tests */
    public static ProctorLoadResult verifyAndConsolidate(
            @Nonnull final TestMatrixArtifact testMatrix,
            final String matrixSource,
            @Nonnull final Map requiredTests,
            @Nonnull final FunctionMapper functionMapper,
            final ProvidedContext providedContext,
            @Nonnull final Set dynamicTests) {
        final ProctorLoadResult result =
                verify(
                        testMatrix,
                        matrixSource,
                        requiredTests,
                        functionMapper,
                        providedContext,
                        dynamicTests);

        final Map definedTests = testMatrix.getTests();
        // Remove any invalid tests so that any required ones will be replaced with default values
        // during the
        // consolidation below (just like missing tests). Any non-required tests can safely be
        // ignored.
        for (final String invalidTest : result.getTestsWithErrors()) {
            // TODO - mjs - gross that this depends on the mutability of the returned map, but then
            // so does the
            //  consolidate method below.
            definedTests.remove(invalidTest);
        }
        // Remove any invalid dynamic tests. This ones won't be replaced with default values
        // because they are not required tests.
        for (final String invalidDynamicTest : result.getDynamicTestWithErrors()) {
            definedTests.remove(invalidDynamicTest);
        }

        consolidate(testMatrix, requiredTests, dynamicTests);

        return result;
    }

    /**
     * Verifies that the TestMatrix is correct and sane without using a specification. The Proctor
     * API doesn't use a test specification so that it can serve all tests in the matrix without
     * restriction. Does a limited set of sanity checks that are applicable when there is no
     * specification, and thus no required tests or provided context.
     *
     * @param testMatrix the {@link TestMatrixArtifact} to be verified.
     * @param matrixSource a {@link String} of the source of proctor artifact. For example, a path
     *     of proctor artifact file.
     * @return a {@link ProctorLoadResult} to describe the result of verification. It contains
     *     errors of verification and a list of missing test.
     */
    public static ProctorLoadResult verifyWithoutSpecification(
            @Nonnull final TestMatrixArtifact testMatrix, final String matrixSource) {
        final ProctorLoadResult.Builder resultBuilder = ProctorLoadResult.newBuilder();

        for (final Entry entry :
                testMatrix.getTests().entrySet()) {
            final String testName = entry.getKey();
            final ConsumableTestDefinition testDefinition = entry.getValue();

            try {
                verifyInternallyConsistentDefinition(testName, matrixSource, testDefinition);
            } catch (final IncompatibleTestMatrixException e) {
                LOGGER.info(String.format("Unable to load test matrix for %s", testName), e);
                resultBuilder.recordError(testName, e);
            }
        }
        return resultBuilder.build();
    }

    /** verify with default function mapper and empty context and no dynamic tests */
    public static ProctorLoadResult verify(
            @Nonnull final TestMatrixArtifact testMatrix,
            final String matrixSource,
            @Nonnull final Map requiredTests) {
        return verify(
                testMatrix,
                matrixSource,
                requiredTests,
                RuleEvaluator.FUNCTION_MAPPER,
                ProvidedContext.nonEvaluableContext(), // use default function mapper
                Collections.emptySet());
    }

    /** verify with default function mapper and empty context */
    public static ProctorLoadResult verify(
            @Nonnull final TestMatrixArtifact testMatrix,
            final String matrixSource,
            @Nonnull final Map requiredTests,
            @Nonnull final Set dynamicTests) {
        return verify(
                testMatrix,
                matrixSource,
                requiredTests,
                RuleEvaluator.FUNCTION_MAPPER,
                ProvidedContext.nonEvaluableContext(), // use default function mapper
                dynamicTests);
    }

    /** verify with default function mapper and no dynamic tests */
    public static ProctorLoadResult verify(
            @Nonnull final TestMatrixArtifact testMatrix,
            final String matrixSource,
            @Nonnull final Map requiredTests,
            @Nonnull final FunctionMapper functionMapper,
            final ProvidedContext providedContext) {
        return verify(
                testMatrix,
                matrixSource,
                requiredTests,
                functionMapper,
                providedContext,
                Collections.emptySet());
    }

    /**
     * Does not mutate the TestMatrix. Verifies that the test matrix contains all the required tests
     * and that each required test is valid.
     *
     * @param testMatrix the {@link TestMatrixArtifact} to be verified.
     * @param matrixSource a {@link String} of the source of proctor artifact. For example, a path
     *     of proctor artifact file.
     * @param requiredTests a {@link Map} of required test. The {@link TestSpecification} would be
     *     verified
     * @param functionMapper a given el {@link FunctionMapper}
     * @param providedContext a {@link Map} containing variables describing the context in which the
     *     request is executing. These will be supplied to verifying all rules.
     * @param dynamicTests a {@link Set} of dynamic tests determined by filters.
     * @return a {@link ProctorLoadResult} to describe the result of verification. It contains
     *     errors of verification and a list of missing test.
     */
    public static ProctorLoadResult verify(
            @Nonnull final TestMatrixArtifact testMatrix,
            final String matrixSource,
            @Nonnull final Map requiredTests,
            @Nonnull final FunctionMapper functionMapper,
            final ProvidedContext providedContext,
            @Nonnull final Set dynamicTests) {
        final ProctorLoadResult.Builder resultBuilder = ProctorLoadResult.newBuilder();

        final Set testsToLoad = Sets.union(requiredTests.keySet(), dynamicTests);
        final Map definedTests = testMatrix.getTests();

        final Set missingTests = new HashSet<>();
        final Set incompatibleTests = new HashSet<>();

        for (final String testName : testsToLoad) {
            if (!definedTests.containsKey(testName)) {
                // required by specification but missing in test matrix
                resultBuilder.recordMissing(testName);
                missingTests.add(testName);
            } else if (requiredTests.containsKey(testName)) {
                // required by specification
                try {
                    verifyRequiredTest(
                            testName,
                            definedTests.get(testName),
                            requiredTests.get(testName),
                            matrixSource,
                            functionMapper,
                            providedContext);
                } catch (final IncompatibleTestMatrixException e) {
                    resultBuilder.recordError(testName, e);
                    incompatibleTests.add(testName);
                }
            } else if (dynamicTests.contains(testName)) {
                // resolved by dynamic filter
                try {
                    verifyDynamicTest(
                            testName,
                            definedTests.get(testName),
                            matrixSource,
                            functionMapper,
                            providedContext);
                } catch (final IncompatibleTestMatrixException e) {
                    resultBuilder.recordIncompatibleDynamicTest(testName, e);
                    incompatibleTests.add(testName);
                }
            }
        }

        final Map errorReasonsOfTestsByDependency =
                TestDependencies.validateDependenciesAndReturnReasons(
                        testsToLoad.stream()
                                .filter(
                                        testName ->
                                                !missingTests.contains(testName)
                                                        && !incompatibleTests.contains(testName))
                                .collect(
                                        Collectors.toMap(testName -> testName, definedTests::get)));

        errorReasonsOfTestsByDependency.forEach(
                (testName, errorReason) -> {
                    final String message = "Invalid dependency field is detected: " + errorReason;
                    if (requiredTests.containsKey(testName)) {
                        resultBuilder.recordError(
                                testName, new IncompatibleTestMatrixException(message));
                    } else if (dynamicTests.contains(testName)) {
                        resultBuilder.recordIncompatibleDynamicTest(
                                testName, new IncompatibleTestMatrixException(message));
                    }
                });

        resultBuilder.recordVerifiedRules(providedContext.shouldEvaluate());

        return resultBuilder.build();
    }

    /**
     * Verifies that a single required test is valid against {@link TestSpecification} and {@link
     * FunctionMapper} and {@link ProvidedContext}.
     *
     * @param testName the name of the test
     * @param testDefinition {@link ConsumableTestDefinition} of the test
     * @param testSpecification {@link TestSpecification} defined in an application for the test
     * @param matrixSource a {@link String} of the source of proctor artifact. For example, a path
     *     of proctor artifact file.
     * @param functionMapper a given el {@link FunctionMapper}
     * @param providedContext a {@link Map} containing variables describing the context in which the
     *     request is executing. These will be supplied to verifying all rules.
     * @throws IncompatibleTestMatrixException if validation is failed.
     */
    public static void verifyRequiredTest(
            @Nonnull final String testName,
            @Nonnull final ConsumableTestDefinition testDefinition,
            @Nonnull final TestSpecification testSpecification,
            @Nonnull final String matrixSource,
            @Nonnull final FunctionMapper functionMapper,
            final ProvidedContext providedContext)
            throws IncompatibleTestMatrixException {
        final Set knownBucketValues = new HashSet<>();
        for (final Integer bucketValue : testSpecification.getBuckets().values()) {
            if (bucketValue == null) {
                throw new IncompatibleTestMatrixException(
                        "Test specification of " + testName + " has null in buckets");
            }
            if (!knownBucketValues.add(bucketValue)) {
                throw new IncompatibleTestMatrixException(
                        "Test specification of "
                                + testName
                                + " has duplicated buckets for value "
                                + bucketValue);
            }
        }

        verifyTest(
                testName,
                testDefinition,
                testSpecification,
                knownBucketValues,
                matrixSource,
                functionMapper,
                providedContext);
    }

    /**
     * Verifies that a single dynamic test is valid against {@link FunctionMapper} and {@link
     * ProvidedContext}.
     *
     * @param testName the name of the test
     * @param testDefinition {@link ConsumableTestDefinition} of the test
     * @param matrixSource a {@link String} of the source of proctor artifact. For example, a path
     *     of proctor artifact file.
     * @param functionMapper a given el {@link FunctionMapper}
     * @param providedContext a {@link Map} containing variables describing the context in which the
     *     request is executing. These will be supplied to verifying all rules.
     * @throws IncompatibleTestMatrixException if validation is failed.
     */
    public static void verifyDynamicTest(
            @Nonnull final String testName,
            @Nonnull final ConsumableTestDefinition testDefinition,
            @Nonnull final String matrixSource,
            @Nonnull final FunctionMapper functionMapper,
            final ProvidedContext providedContext)
            throws IncompatibleTestMatrixException {
        verifyTest(
                testName,
                testDefinition,
                // hack: use empty test spec to not verify buckets and payloads
                new TestSpecification(),
                // this parameter is ignored
                Collections.emptySet(),
                matrixSource,
                functionMapper,
                providedContext);
    }

    private static void verifyTest(
            @Nonnull final String testName,
            @Nonnull final ConsumableTestDefinition testDefinition,
            @Nonnull final TestSpecification testSpecification,
            @Nonnull final Set knownBuckets,
            @Nonnull final String matrixSource,
            @Nonnull final FunctionMapper functionMapper,
            final ProvidedContext providedContext)
            throws IncompatibleTestMatrixException {
        final List allocations = testDefinition.getAllocations();

        final TestType declaredType = testDefinition.getTestType();
        if (!TestType.all().contains(declaredType)) {
            throw new IncompatibleTestMatrixException(
                    String.format(
                            "Test '%s' is included in the application specification but refers to unknown id type '%s'.",
                            testName, declaredType));
        }
        verifyInternallyConsistentDefinition(
                testName, matrixSource, testDefinition, functionMapper, providedContext);

        if (!testSpecification.getBuckets().isEmpty()) {
            /*
             * test the matrix for adherence to this application's requirements, if buckets were specified
             */
            final Set unknownBuckets = Sets.newHashSet();

            for (final Allocation allocation : allocations) {
                final List ranges = allocation.getRanges();
                //  ensure that each range refers to a known bucket
                for (final Range range : ranges) {
                    // Externally consistent (application's requirements)
                    if (!knownBuckets.contains(range.getBucketValue())) {
                        // If the bucket has a positive allocation, add it to the list of
                        // unknownBuckets
                        if (range.getLength() > 0) {
                            unknownBuckets.add(range.getBucketValue());
                        }
                    }
                }
            }

            if (!unknownBuckets.isEmpty()) {
                final String bucketString =
                        (unknownBuckets.size() > 1 ? "bucket values: " : "bucket value: ")
                                + Strings.join(unknownBuckets, ',');
                throw new IncompatibleTestMatrixException(
                        "Proctor specification in your application does not contain "
                                + bucketString
                                + ". Please update the proctor specification first");
            }
        }

        final PayloadSpecification payloadSpec = testSpecification.getPayload();
        if (payloadSpec != null) {
            final String specifiedPayloadTypeName =
                    Preconditions.checkNotNull(payloadSpec.getType(), "Missing payload spec type");
            final PayloadType specifiedPayloadType =
                    PayloadType.payloadTypeForName(specifiedPayloadTypeName);
            final Map specificationPayloadTypes = payloadSpec.getSchema();
            if (specifiedPayloadType == PayloadType.MAP) {
                if (specificationPayloadTypes == null || specificationPayloadTypes.isEmpty()) {
                    throw new IncompatibleTestMatrixException(
                            String.format(
                                    "The bucket definition of test %s has no payload, but the application is "
                                            + "expecting one. Add a payload to your test definition, or if there should not be one, "
                                            + "remove it from the application's Proctor specification. You can copy the Proctor "
                                            + "specification from the specification tab for the test on Proctor Webapp and add it "
                                            + "to the application's json file that contains the test specification.",
                                    testName));
                }
            }

            if (specifiedPayloadType == null) {
                // This is probably redundant vs. TestGroupsGenerator.
                throw new IncompatibleTestMatrixException(
                        "For test "
                                + testName
                                + " from "
                                + matrixSource
                                + " test specification payload type unknown: "
                                + specifiedPayloadTypeName);
            }
            final String payloadValidatorRule = payloadSpec.getValidator();

            // TODO(pwp): add some test constants?
            final RuleEvaluator ruleEvaluator =
                    makeRuleEvaluator(RuleEvaluator.EXPRESSION_FACTORY, functionMapper);

            for (final TestBucket bucket : testDefinition.getBuckets()) {
                final Payload payload = bucket.getPayload();
                if (payload != null) {
                    if (!Payload.hasType(payload, specifiedPayloadType)) {
                        throw new IncompatibleTestMatrixException(
                                "For test "
                                        + testName
                                        + " from "
                                        + matrixSource
                                        + " expected payload of type "
                                        + specifiedPayloadType.payloadTypeName
                                        + " but matrix has a test bucket payload with wrong type: "
                                        + bucket);
                    }
                    if (specifiedPayloadType == PayloadType.MAP) {
                        checkMapPayloadTypes(
                                payload,
                                specificationPayloadTypes,
                                matrixSource,
                                testName,
                                specifiedPayloadType,
                                payloadValidatorRule,
                                bucket,
                                functionMapper);
                    } else if (payloadValidatorRule != null) {
                        final boolean payloadIsValid =
                                evaluatePayloadValidator(
                                        ruleEvaluator, payloadValidatorRule, payload);
                        if (!payloadIsValid) {
                            throw new IncompatibleTestMatrixException(
                                    "For test "
                                            + testName
                                            + " from "
                                            + matrixSource
                                            + " payload validation rule "
                                            + payloadValidatorRule
                                            + " failed for test bucket: "
                                            + bucket);
                        }
                    }
                }
            }
        }
    }

    private static void checkMapPayloadTypes(
            final Payload payload,
            final Map specificationPayloadTypes,
            final String matrixSource,
            final String testName,
            final PayloadType specifiedPayloadType,
            final String payloadValidatorRule,
            final TestBucket bucket,
            final FunctionMapper functionMapper)
            throws IncompatibleTestMatrixException {
        final RuleEvaluator ruleEvaluator =
                makeRuleEvaluator(RuleEvaluator.EXPRESSION_FACTORY, functionMapper);
        if (payload.getMap() == null) {
            throw new IncompatibleTestMatrixException(
                    "For test "
                            + testName
                            + " from "
                            + matrixSource
                            + " expected payload of type "
                            + specifiedPayloadType.payloadTypeName
                            + " but matrix has a test bucket payload with wrong type: "
                            + bucket);
        }
        if (specificationPayloadTypes.size() > payload.getMap().size()) {
            throw new IncompatibleTestMatrixException(
                    "For test "
                            + testName
                            + " from "
                            + matrixSource
                            + " expected payload of equal size to specification "
                            + specifiedPayloadType
                            + "  but matrix has a test bucket payload with wrong type size: "
                            + bucket);
        }
        final Map bucketPayloadMap = payload.getMap();
        for (final Entry specificationPayloadEntry :
                specificationPayloadTypes.entrySet()) {
            if (!bucketPayloadMap.containsKey(specificationPayloadEntry.getKey())) {
                throw new IncompatibleTestMatrixException(
                        "For test "
                                + testName
                                + " from "
                                + matrixSource
                                + " expected payload of same order and variable names as specificied in "
                                + specifiedPayloadType
                                + " but matrix has a test bucket payload with wrong type: "
                                + bucket);
            }
            final PayloadType expectedPayloadType;
            try {
                expectedPayloadType =
                        PayloadType.payloadTypeForName(specificationPayloadEntry.getValue());
            } catch (final IllegalArgumentException e) {
                throw new IncompatibleTestMatrixException(
                        "For test "
                                + testName
                                + " from "
                                + matrixSource
                                + " specification payload type unknown in: "
                                + specifiedPayloadType.payloadTypeName);
            }
            if (expectedPayloadType == PayloadType.MAP) {
                throw new IncompatibleTestMatrixException(
                        "For test "
                                + testName
                                + " from "
                                + matrixSource
                                + " specification payload type has unallowed nested map types: "
                                + specifiedPayloadType.payloadTypeName);
            }
            final Object actualPayload = bucketPayloadMap.get(specificationPayloadEntry.getKey());
            if (actualPayload instanceof ArrayList) {
                for (final Object actualPayloadEntry : (ArrayList) actualPayload) {
                    final Class actualClazz = actualPayloadEntry.getClass();
                    if (PayloadType.STRING_ARRAY == expectedPayloadType) {
                        if (!String.class.isAssignableFrom(actualClazz)) {
                            throw new IncompatibleTestMatrixException(
                                    "For test "
                                            + testName
                                            + " from "
                                            + matrixSource
                                            + " expected payload of type "
                                            + specifiedPayloadType.payloadTypeName
                                            + " but matrix has a test bucket payload with wrong nested type: "
                                            + bucket);
                        }
                    } else if (PayloadType.LONG_ARRAY == expectedPayloadType
                            || PayloadType.DOUBLE_ARRAY == expectedPayloadType) {
                        if (!Number.class.isAssignableFrom(actualClazz)) {
                            throw new IncompatibleTestMatrixException(
                                    "For test "
                                            + testName
                                            + " from "
                                            + matrixSource
                                            + " expected payload of type "
                                            + specifiedPayloadType.payloadTypeName
                                            + " but matrix has a test bucket payload with wrong nested type: "
                                            + bucket);
                        }
                    } else {
                        throw new IncompatibleTestMatrixException(
                                "For test "
                                        + testName
                                        + " from "
                                        + matrixSource
                                        + " expected payload of type "
                                        + specifiedPayloadType.payloadTypeName
                                        + " but matrix has a test bucket payload with wrong nested type: "
                                        + bucket);
                    }
                }
            } else if (PayloadType.DOUBLE_ARRAY == expectedPayloadType
                    || PayloadType.STRING_ARRAY == expectedPayloadType
                    || PayloadType.LONG_ARRAY == expectedPayloadType) {
                throw new IncompatibleTestMatrixException(
                        "For test "
                                + testName
                                + " from "
                                + matrixSource
                                + " expected payload of type "
                                + specifiedPayloadType.payloadTypeName
                                + " but matrix has a test bucket payload with wrong nested type: "
                                + bucket);
            } else if (PayloadType.DOUBLE_VALUE == expectedPayloadType
                    || PayloadType.LONG_VALUE == expectedPayloadType) {
                final Class actualClazz = actualPayload.getClass();
                if (!Number.class.isAssignableFrom(actualClazz)) {
                    throw new IncompatibleTestMatrixException(
                            "For test "
                                    + testName
                                    + " from "
                                    + matrixSource
                                    + " expected payload of type "
                                    + specifiedPayloadType.payloadTypeName
                                    + " but matrix has a test bucket payload with wrong nested type: "
                                    + bucket);
                }
            } else {
                try {
                    if (!Class.forName("java.lang." + expectedPayloadType.javaClassName)
                            .isInstance(actualPayload)) {
                        throw new IncompatibleTestMatrixException(
                                "For test "
                                        + testName
                                        + " from "
                                        + matrixSource
                                        + " expected payload of type "
                                        + specifiedPayloadType.payloadTypeName
                                        + " but matrix has a test bucket payload with wrong nested type: "
                                        + bucket);
                    }
                } catch (final ClassNotFoundException e) {
                    throw new IncompatibleTestMatrixException(
                            "For test "
                                    + testName
                                    + " from "
                                    + matrixSource
                                    + " incompatible payload type?");
                }
            }
        }
        if (payloadValidatorRule != null) {
            final boolean payloadIsValid =
                    evaluatePayloadMapValidator(ruleEvaluator, payloadValidatorRule, payload);
            if (!payloadIsValid) {
                throw new IncompatibleTestMatrixException(
                        "For test "
                                + testName
                                + " from "
                                + matrixSource
                                + " payload validation rule "
                                + payloadValidatorRule
                                + " failed for test bucket: "
                                + bucket);
            }
        }
    }

    /**
     * minimizes TestMatrix by removing non-required test definitions, also add definitions from
     * missing tests
     */
    private static void consolidate(
            @Nonnull final TestMatrixArtifact testMatrix,
            @Nonnull final Map requiredTests,
            @Nonnull final Set dynamicTests) {
        final Map definedTests = testMatrix.getTests();

        // Sets.difference returns a "view" on the original set, which would require concurrent
        // modification while
        // iterating (copying the set will prevent this)
        final Set toRemove =
                ImmutableSet.copyOf(
                        Sets.difference(
                                definedTests.keySet(),
                                Sets.union(requiredTests.keySet(), dynamicTests)));

        for (final String testInMatrixNotRequired : toRemove) {
            //  we don't care about this test
            definedTests.remove(testInMatrixNotRequired);
        }

        // Next, for any required tests that are missing, ensure that
        //  there is a nonnull test definition in the matrix
        final Set missing =
                ImmutableSet.copyOf(Sets.difference(requiredTests.keySet(), definedTests.keySet()));
        for (final String testNotInMatrix : missing) {
            definedTests.put(
                    testNotInMatrix,
                    defaultFor(testNotInMatrix, requiredTests.get(testNotInMatrix)));
        }

        // Now go through definedTests: for each test, if the test spec
        // didn't ask for a payload, then remove any payload that is in
        // the test matrix.  If buckets exist in the specification that
        // do not in the matrix, add buckets with null payloads to allow
        // forcing buckets that aren't in the matrix but are in the spec.
        for (final Entry next : definedTests.entrySet()) {
            final String testName = next.getKey();
            final ConsumableTestDefinition testDefinition = next.getValue();
            if (!requiredTests.containsKey(testName)) {
                // We don't care here about dynamically resolved tests
                continue;
            }
            final TestSpecification testSpec = requiredTests.get(testName);

            final boolean noPayloads = (testSpec.getPayload() == null);
            final Set bucketValues = Sets.newHashSet();
            List buckets = testDefinition.getBuckets();

            for (final TestBucket bucket : buckets) {
                // Note bucket values that exist in matrix.
                bucketValues.add(bucket.getValue());
            }

            if (noPayloads) {
                testDefinition.setBuckets(
                        buckets.stream()
                                .map(
                                        bucket ->
                                                TestBucket.builder()
                                                        .from(bucket)
                                                        .payload(null) // stomp the unexpected
                                                        // payload if exists.
                                                        .build())
                                .collect(Collectors.toList()));
            }

            boolean replaceBuckets = false;
            final Map specBuckets = testSpec.getBuckets();
            for (final Entry bucketSpec : specBuckets.entrySet()) {
                if (!bucketValues.contains(bucketSpec.getValue())) {
                    if (!replaceBuckets) {
                        buckets = Lists.newArrayList(buckets);
                        replaceBuckets = true;
                    }
                    buckets.add(
                            new TestBucket(bucketSpec.getKey(), bucketSpec.getValue(), null, null));
                }
            }

            if (replaceBuckets) {
                testDefinition.setBuckets(buckets);
            }
        }
    }

    @Nonnull
    private static ConsumableTestDefinition defaultFor(
            final String testName, @Nonnull final TestSpecification testSpecification) {
        final String missingTestSoleBucketName = "inactive";
        final String missingTestSoleBucketDescription = "fallback because missing in matrix";
        final int fallbackValue = testSpecification.getFallbackValue();
        final Allocation allocation =
                new Allocation(null, Collections.singletonList(new Range(fallbackValue, 1.0)));

        return ConsumableTestDefinition.fromTestDefinition(
                TestDefinition.builder()
                        .setVersion("default")
                        .setTestType(TestType.RANDOM)
                        .setSalt(testName)
                        .setBuckets(
                                ImmutableList.of(
                                        new TestBucket(
                                                missingTestSoleBucketName,
                                                fallbackValue,
                                                missingTestSoleBucketDescription)))
                        // Force a nonnull allocation just in case something somewhere assumes 1.0
                        // total allocation
                        .setAllocations(Collections.singletonList(allocation))
                        .setSilent(false) // non-silent, though typically fallbackValue -1 has same
                        // effect
                        .setDescription(testName)
                        .build());
    }

    public static ProvidedContext convertContextToTestableMap(
            final Map providedContext) {
        return convertContextToTestableMap(providedContext, Collections.emptyMap());
    }

    public static ProvidedContext convertContextToTestableMap(
            final Map providedContext,
            final Map ruleVerificationContext) {
        final ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
        final Map primitiveVals = new HashMap<>();
        primitiveVals.put("int", 0);
        primitiveVals.put("integer", 0);
        primitiveVals.put("long", (long) 0);
        primitiveVals.put("bool", true);
        primitiveVals.put("boolean", true);
        primitiveVals.put("short", (short) 0);
        primitiveVals.put("string", "");
        primitiveVals.put("double", (double) 0);
        primitiveVals.put("char", "");
        primitiveVals.put("character", "");
        primitiveVals.put("byte", (byte) 0);

        if (providedContext != null) {
            final Map newProvidedContext = new HashMap<>();
            final Set uninstantiatedIdentifiers = Sets.newHashSet();
            for (final Entry entry : providedContext.entrySet()) {
                final String identifier = entry.getKey();
                Object toAdd = null;
                if (ruleVerificationContext.containsKey(identifier)) {
                    toAdd = ruleVerificationContext.get(identifier);
                    LOGGER.debug(
                            String.format(
                                    "Use instance for identifier {%s} provided by user %s",
                                    identifier, toAdd));
                } else {
                    LOGGER.debug(
                            String.format(
                                    "Identifier {%s} is not provided, instantiate it via default constructor",
                                    identifier));
                    final String iobjName = entry.getValue();
                    final String objName = iobjName;

                    if (primitiveVals.get(objName.toLowerCase()) != null) {
                        toAdd = primitiveVals.get(objName.toLowerCase());
                    } else {
                        try {
                            final Class clazz = Class.forName(objName);
                            if (clazz.isEnum()) { // If it is a user defined enum
                                toAdd = clazz.getEnumConstants()[0];
                            } else { // If it is a user defined non enum class
                                toAdd = clazz.newInstance();
                            }
                        } catch (final IllegalAccessException e) {
                            uninstantiatedIdentifiers.add(identifier);
                            LOGGER.debug(
                                    "Couldn't access default constructor of "
                                            + iobjName
                                            + " in providedContext. Rule verification will skip this identifier - "
                                            + identifier);
                        } catch (final InstantiationException e) {
                            uninstantiatedIdentifiers.add(identifier);
                            // if a default constructor is not defined, use this flag to not set
                            // context and not evaluate rules
                            LOGGER.debug(
                                    "Couldn't find default constructor for "
                                            + iobjName
                                            + " in providedContext. Rule verification will skip this identifier - "
                                            + identifier);
                        } catch (final ClassNotFoundException e) {
                            uninstantiatedIdentifiers.add(identifier);
                            LOGGER.error("Class not found for " + iobjName + " in providedContext");
                        }
                    }
                }
                newProvidedContext.put(identifier, toAdd);
            }
            /* evaluate the rule even if defaultConstructor method does not exist, */
            return ProvidedContext.forValueExpressionMap(
                    ProctorUtils.convertToValueExpressionMap(expressionFactory, newProvidedContext),
                    uninstantiatedIdentifiers);
        }
        return ProvidedContext.nonEvaluableContext();
    }

    /**
     * verifyInternallyConsistentDefinition with default functionMapper, but do not evaluate rule
     * against any context
     */
    public static void verifyInternallyConsistentDefinition(
            final String testName,
            final String matrixSource,
            @Nonnull final ConsumableTestDefinition testDefinition)
            throws IncompatibleTestMatrixException {
        verifyInternallyConsistentDefinition(
                testName, matrixSource, testDefinition, ProvidedContext.nonEvaluableContext());
    }

    /**
     * verifyInternallyConsistentDefinition with default functionMapper and evaluate against context
     */
    public static void verifyInternallyConsistentDefinition(
            final String testName,
            final String matrixSource,
            @Nonnull final ConsumableTestDefinition testDefinition,
            final ProvidedContext providedContext)
            throws IncompatibleTestMatrixException {
        verifyInternallyConsistentDefinition(
                testName,
                matrixSource,
                testDefinition,
                RuleEvaluator.FUNCTION_MAPPER,
                providedContext);
    }

    /**
     * verify: - test/allocation rules has valid syntax - if providedContext.shouldEvaluate, also
     * verifies that rule contains only identifiers from context - if
     * providedContext.shouldEvaluate, also verifies test/allocation rule evaluates to boolean -
     * buckets have same payload type
     *
     * @throws IncompatibleTestMatrixException on violations
     */
    private static void verifyInternallyConsistentDefinition(
            final String testName,
            final String matrixSource,
            @Nonnull final ConsumableTestDefinition testDefinition,
            final FunctionMapper functionMapper,
            final ProvidedContext providedContext)
            throws IncompatibleTestMatrixException {
        final List allocations = testDefinition.getAllocations();
        final ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
        // verify test rule is valid EL
        final String testRule = testDefinition.getRule();

        final Map testConstants =
                convertConstantsToValueExpressionMap(
                        expressionFactory, testDefinition.getConstants());
        final VariableMapper variableMapper =
                new MulticontextReadOnlyVariableMapper(testConstants, providedContext.getContext());
        final RuleEvaluator ruleEvaluator =
                new RuleEvaluator(expressionFactory, functionMapper, testDefinition.getConstants());
        final ELContext elContext = ruleEvaluator.createELContext(variableMapper);

        try {
            RuleVerifyUtils.verifyRule(
                    testRule,
                    providedContext.shouldEvaluate(),
                    expressionFactory,
                    elContext,
                    providedContext.getUninstantiatedIdentifiers());
        } catch (final InvalidRuleException e) {
            throw new IncompatibleTestMatrixException(
                    String.format("Invalid rule in %s: %s", testName, e.getMessage()), e);
        }

        if (allocations.isEmpty()) {
            throw new IncompatibleTestMatrixException(
                    "No allocations specified in test " + testName);
        }
        final List buckets = testDefinition.getBuckets();

        /*
         * test the definition for consistency with itself
         */
        final Set definedBuckets = Sets.newHashSet();
        for (final TestBucket bucket : buckets) {
            definedBuckets.add(bucket.getValue());
        }

        for (int i = 0; i < allocations.size(); i++) {
            final Allocation allocation = allocations.get(i);
            final List ranges = allocation.getRanges();
            if ((ranges == null) || ranges.isEmpty()) {
                throw new IncompatibleTestMatrixException(
                        "Allocation range has no buckets, needs to add up to 1.");
            }
            if (testDefinition.getEnableUnitlessAllocations()) {

                final boolean isUnitlessAllocation = isUnitlessAllocation(allocation);
                if (isUnitlessAllocation
                        && ranges.stream().noneMatch(range -> range.getLength() > 0.9999)) {
                    throw new IncompatibleTestMatrixException(
                            "Allocation with \"missingExperimentalUnit\" in rule must have one bucket set to 100%");
                }
            }

            //  ensure that each range refers to a known bucket
            double bucketTotal = 0;
            for (final Range range : ranges) {
                bucketTotal += range.getLength();
                // Internally consistent
                if (!definedBuckets.contains(range.getBucketValue())) {
                    throw new IncompatibleTestMatrixException(
                            "Allocation range in "
                                    + testName
                                    + " from "
                                    + matrixSource
                                    + " refers to unknown bucket value "
                                    + range.getBucketValue());
                }
            }
            //  I hate floating points.  TODO: extract a required precision constant/parameter?
            //  compensate for FP imprecision.  TODO: determine what these bounds really should be
            // by testing stuff
            if (bucketTotal < 0.9999 || bucketTotal > 1.0001) {
                throw new IncompatibleTestMatrixException(
                        testName
                                + " range with rule "
                                + allocation.getRule()
                                + " does not add up to 1 : "
                                + ranges.stream()
                                        .map(r -> Double.toString(r.getLength()))
                                        .collect(joining(" + "))
                                + " = "
                                + bucketTotal);
            }
            final String rule = allocation.getRule();
            final boolean lastAllocation = i == (allocations.size() - 1);
            if (!lastAllocation && isEmptyElExpression(rule)) {
                throw new IncompatibleTestMatrixException(
                        "Allocation["
                                + i
                                + "] for test "
                                + testName
                                + " from "
                                + matrixSource
                                + " has empty rule: "
                                + allocation.getRule());
            }

            try {
                RuleVerifyUtils.verifyRule(
                        rule,
                        providedContext.shouldEvaluate(),
                        expressionFactory,
                        elContext,
                        providedContext.getUninstantiatedIdentifiers());
            } catch (final InvalidRuleException e) {
                throw new IncompatibleTestMatrixException(
                        String.format(
                                "Invalid allocation rule in %s: %s", testName, e.getMessage()),
                        e);
            }
        }

        /*
         * When defined, within a single test, all test bucket payloads
         * should be supplied; they should all have just one type each,
         * and they should all be the same type.
         */
        Payload nonEmptyPayload = null;
        final List bucketsWithoutPayloads = Lists.newArrayList();
        for (final TestBucket bucket : buckets) {
            final Payload p = bucket.getPayload();
            if (p != null) {
                if (p.numFieldsDefined() > 1) {
                    throw new IncompatibleTestMatrixException(
                            "Test "
                                    + testName
                                    + " from "
                                    + matrixSource
                                    + " has a test bucket payload with multiple types: "
                                    + bucket);
                }
                if (nonEmptyPayload == null) {
                    nonEmptyPayload = p;
                } else if (!nonEmptyPayload.sameType(p)) {
                    throw new IncompatibleTestMatrixException(
                            "Test "
                                    + testName
                                    + " from "
                                    + matrixSource
                                    + " has test bucket: "
                                    + bucket
                                    + " incompatible with type of payload: "
                                    + nonEmptyPayload);
                }
            } else {
                bucketsWithoutPayloads.add(bucket);
            }
        }
        if ((nonEmptyPayload != null) && (!bucketsWithoutPayloads.isEmpty())) {
            throw new IncompatibleTestMatrixException(
                    "Test "
                            + testName
                            + " from "
                            + matrixSource
                            + " has some test buckets without payloads: "
                            + bucketsWithoutPayloads);
        }
    }

    /**
     * Returns flag whose value indicates if the string is null, empty or only contains whitespace
     * characters
     *
     * @param s a string
     * @return true if the string is null, empty or only contains whitespace characters
     * @deprecated Use StringUtils.isBlank
     */
    @Deprecated
    static boolean isEmptyWhitespace(@Nullable final String s) {
        return StringUtils.isBlank(s);
    }

    /**
     * Generates a usable test specification for a given test definition Uses the bucket with
     * smallest value as the fallback value
     *
     * @param testDefinition a {@link TestDefinition}
     * @return a {@link TestSpecification} which corresponding to given test definition.
     * @deprecated use SpecificationGenerator
     */
    @Deprecated
    public static TestSpecification generateSpecification(
            @Nonnull final TestDefinition testDefinition) {
        return SPECIFICATION_GENERATOR.generateSpecification(testDefinition);
    }

    /**
     * Removes the expression braces "${ ... }" surrounding the rule.
     *
     * @param rule a given rule String.
     * @return the given rule with the most outside braces stripped
     */
    @CheckForNull
    public static String removeElExpressionBraces(@Nullable final String rule) {
        if (StringUtils.isBlank(rule)) {
            return null;
        }
        int startchar = 0; // inclusive
        int endchar = rule.length() - 1; // inclusive

        // garbage free trim()
        while (startchar < rule.length() && Character.isWhitespace(rule.charAt(startchar))) {
            ++startchar;
        }
        while (endchar > startchar && Character.isWhitespace(rule.charAt(endchar))) {
            --endchar;
        }

        if (rule.regionMatches(startchar, "${", 0, 2) && rule.charAt(endchar) == '}') {
            startchar += 2; // skip "${"
            --endchar; // skip '}'
        }
        // garbage free trim()
        while (startchar < rule.length() && Character.isWhitespace(rule.charAt(startchar))) {
            ++startchar;
        }
        while (endchar > startchar && Character.isWhitespace(rule.charAt(endchar))) {
            --endchar;
        }
        if (endchar < startchar) {
            // null instead of empty string for consistency with 'isEmptyWhitespace' check at the
            // beginning
            return null;
        }
        return rule.substring(startchar, endchar + 1);
    }

    /** An allocaction-free version of {@code StringUtils.isBlank(removeElExpressionBraces(s))} */
    static boolean isEmptyElExpression(@Nullable final String rule) {
        return ElExpressionClassification.EMPTY == clasifyElExpression(rule, false);
    }

    static ElExpressionClassification clasifyElExpression(
            @Nullable final String rule, final boolean checkForBooleanConstants) {
        if (StringUtils.isBlank(rule)) {
            return ElExpressionClassification.EMPTY;
        }
        int startchar = 0; // inclusive
        int endchar = rule.length() - 1; // inclusive

        // garbage free trim()
        while (startchar < rule.length() && Character.isWhitespace(rule.charAt(startchar))) {
            ++startchar;
        }
        while (endchar > startchar && Character.isWhitespace(rule.charAt(endchar))) {
            --endchar;
        }

        if (rule.regionMatches(startchar, "${", 0, 2) && rule.charAt(endchar) == '}') {
            startchar += 2; // skip "${"
            --endchar; // skip '}'
        }
        // garbage free trim()
        while (startchar < rule.length() && Character.isWhitespace(rule.charAt(startchar))) {
            ++startchar;
        }
        while (endchar > startchar && Character.isWhitespace(rule.charAt(endchar))) {
            --endchar;
        }
        if (endchar < startchar) {
            return ElExpressionClassification.EMPTY;
        }

        if (checkForBooleanConstants) {
            final String trueText = Boolean.TRUE.toString();
            if (trueText.regionMatches(true, 0, rule, startchar, trueText.length())) {
                return ElExpressionClassification.CONSTANT_TRUE;
            }

            final String falseText = Boolean.FALSE.toString();
            if (falseText.regionMatches(true, 0, rule, startchar, falseText.length())) {
                return ElExpressionClassification.CONSTANT_FALSE;
            }
        }

        return ElExpressionClassification.OTHER;
    }

    enum ElExpressionClassification {
        CONSTANT_TRUE,
        CONSTANT_FALSE,
        EMPTY,
        OTHER;
    }

    // Make a new RuleEvaluator that captures the test constants.
    // TODO(pwp): add some test constants?
    @Nonnull
    private static RuleEvaluator makeRuleEvaluator(
            final ExpressionFactory expressionFactory, final FunctionMapper functionMapper) {
        // Make the expression evaluation context.
        final Map testConstants = Collections.emptyMap();

        return new RuleEvaluator(expressionFactory, functionMapper, testConstants);
    }

    private static boolean evaluatePayloadMapValidator(
            @Nonnull final RuleEvaluator ruleEvaluator,
            final String rule,
            @Nonnull final Payload payload)
            throws IncompatibleTestMatrixException {
        try {
            return ruleEvaluator.evaluateBooleanRule(rule, payload.getMap());
        } catch (final IllegalArgumentException e) {
            LOGGER.error("Unable to evaluate rule ${" + rule + "} with payload " + payload, e);
        }
        return true;
    }

    private static boolean evaluatePayloadValidator(
            @Nonnull final RuleEvaluator ruleEvaluator,
            final String rule,
            @Nonnull final Payload payload)
            throws IncompatibleTestMatrixException {
        final Map values = Collections.singletonMap("value", payload.fetchAValue());

        try {
            return ruleEvaluator.evaluateBooleanRule(rule, values);
        } catch (final IllegalArgumentException e) {
            LOGGER.error("Unable to evaluate rule ${" + rule + "} with payload " + payload, e);
        }

        return false;
    }

    private static boolean isUnitlessAllocation(final Allocation allocation) {
        if (allocation == null || allocation.getRule() == null) {
            return false;
        }
        final int unitlessIdentifierIndex =
                allocation.getRule().indexOf(UNITLESS_ALLOCATION_IDENTIFIER);
        final boolean unitlessButFalse =
                allocation.getRule().contains(UNITLESS_ALLOCATION_IDENTIFIER + " == false");
        return !unitlessButFalse
                && unitlessIdentifierIndex != -1
                && (unitlessIdentifierIndex == 0
                        || allocation.getRule().charAt(unitlessIdentifierIndex - 1) != '!');
    }

    public static boolean containsUnitlessAllocation(@Nonnull final TestDefinition testDefinition) {
        return testDefinition.getEnableUnitlessAllocations()
                && testDefinition.getAllocations().stream()
                        .anyMatch(
                                (allocation) ->
                                        isUnitlessAllocation(allocation)
                                                && allocation.getRanges().stream()
                                                        .anyMatch(
                                                                range ->
                                                                        range.getLength()
                                                                                > 0.9999));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy