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

com.indeed.proctor.consumer.gen.TestGroupsGenerator Maven / Gradle / Ivy

The newest version!
package com.indeed.proctor.consumer.gen;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.indeed.proctor.common.PayloadType;
import com.indeed.proctor.common.ProctorSpecification;
import com.indeed.proctor.common.Serializers;
import com.indeed.proctor.common.TestSpecification;
import com.indeed.proctor.common.dynamic.DynamicFilters;
import com.indeed.proctor.common.model.NameObfuscator;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringEscapeUtils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * Handles combining multiple proctor specs into one and building a root map for generating a
 * Proctor test groups file.
 *
 * 

Note: heavily based off of the original TestGroupsJavaGenerator. populateRootMap may still * have some code only needed for Java. * * @author andrewk */ public abstract class TestGroupsGenerator extends FreeMarkerCodeGenerator { private static final ObjectMapper OBJECT_MAPPER = Serializers.lenient(); private static final NameObfuscator TEST_NAME_OBFUSCATOR = new NameObfuscator(); public static final String PROVIDED_CONTEXT_FILENAME = "providedcontext.json"; public static final String DYNAMIC_FILTERS_FILENAME = "dynamicfilters.json"; /** * If a folder of split jsons defining a proctor specification is provided, this method iterates * over the folder contents, using the individual TestDefinition jsons and a * providedcontext.json and dynamicfilters.json to create one large temporary * ProctorSpecification json to be used for code generation */ public static ProctorSpecification makeTotalSpecification( final File inputDir, final String targetDir) throws CodeGenException { // If no name is provided use the name of the containing folder return makeTotalSpecification( inputDir, targetDir, inputDir.getPath().substring(inputDir.getPath().lastIndexOf(File.separator) + 1) + "Groups.json"); } public static ProctorSpecification makeTotalSpecification( final File inputDir, final String targetDir, final String outputFileName) throws CodeGenException { final File[] dirFiles = inputDir.listFiles(); return makeTotalSpecification(Arrays.asList(dirFiles), targetDir, outputFileName); } /** * Combines all input files into a total proctor specification and writes it to a file of the * path `targetDir`/`outputFileName`. * * @return combined total proctor specification */ public static ProctorSpecification makeTotalSpecification( final List inputFiles, final String targetDir, final String outputFileName) throws CodeGenException { final List providedContextFiles = new ArrayList<>(); final List dynamicFiltersFiles = new ArrayList<>(); for (final File file : inputFiles) { if (PROVIDED_CONTEXT_FILENAME.equals(file.getName())) { providedContextFiles.add(file); } else if (DYNAMIC_FILTERS_FILENAME.equals(file.getName())) { dynamicFiltersFiles.add(file); } } if (providedContextFiles.size() != 1) { throw new CodeGenException( "Incorrect amount of " + PROVIDED_CONTEXT_FILENAME + " in specified input folder." + " expected 1 but " + providedContextFiles.size() + ": " + providedContextFiles); } if (dynamicFiltersFiles.size() > 1) { throw new CodeGenException( "Incorrect amount of " + DYNAMIC_FILTERS_FILENAME + " in specified input folder." + " expected 0 or 1 but " + dynamicFiltersFiles.size() + ": " + dynamicFiltersFiles); } final Map testSpec = new LinkedHashMap<>(); Map providedContext = null; DynamicFilters dynamicFilters = null; for (final File file : inputFiles) { final String fileName = file.getName(); if (fileName.equals(PROVIDED_CONTEXT_FILENAME)) { try { providedContext = OBJECT_MAPPER.readValue(file, Map.class); } catch (final IOException e) { throw new CodeGenException( "Could not read json correctly " + file.getAbsolutePath() + " for provided context", e); } } else if (fileName.equals(DYNAMIC_FILTERS_FILENAME)) { try { dynamicFilters = OBJECT_MAPPER.readValue(file, DynamicFilters.class); } catch (final IOException e) { throw new CodeGenException( "Could not read json correctly " + file.getAbsolutePath() + " for dynamic filters", e); } } else if (fileName.endsWith(".json")) { final TestSpecification spec; try { spec = OBJECT_MAPPER.readValue(file, TestSpecification.class); } catch (final IOException e) { throw new CodeGenException( "Could not read json correctly " + file.getAbsolutePath() + " for a test specification", e); } final String specName = fileName.substring(0, fileName.indexOf(".json")); if (testSpec.containsKey(specName)) { throw new CodeGenException( "Multiple " + fileName + " found, each test should only have 1 spec file"); } testSpec.put(specName, spec); } } final ProctorSpecification proctorSpecification = new ProctorSpecification( ObjectUtils.defaultIfNull(providedContext, new LinkedHashMap<>()), testSpec, ObjectUtils.defaultIfNull(dynamicFilters, new DynamicFilters())); validateProctorSpecification(proctorSpecification); final File output = new File(targetDir, outputFileName); try { OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValue(output, proctorSpecification); } catch (final IOException e) { throw new CodeGenException( "Could not write to temp file " + output.getAbsolutePath(), e); } return proctorSpecification; } /** * Validate proctor specification at build time. Throws {@link CodeGenException} if an error is * found. */ private static void validateProctorSpecification(final ProctorSpecification spec) throws CodeGenException { for (final Map.Entry entry : spec.getTests().entrySet()) { final String testName = entry.getKey(); final TestSpecification testSpecification = entry.getValue(); validateTestSpecification(testName, testSpecification); } } @VisibleForTesting static void validateTestSpecification(final String testName, final TestSpecification testSpec) throws CodeGenException { final Set bucketValueSet = new HashSet<>(); for (final Integer bucketValue : testSpec.getBuckets().values()) { if (bucketValue == null) { throw new CodeGenException( "specification of " + testName + " has null bucket value"); } if (!bucketValueSet.add(bucketValue)) { throw new CodeGenException( "specification of " + testName + " has duplicated bucket value " + bucketValue); } } } @VisibleForTesting @Override Map populateRootMap( final ProctorSpecification spec, final Map baseContext, final String packageName, final String className) { final Map rootMap = new HashMap<>(baseContext); final Map tests = spec.getTests(); final List testDefs = Lists.newArrayListWithCapacity(tests.size()); final Map testDefsMap = new HashMap<>(tests.size()); final Set forcePayloadTests = new HashSet<>(); // Sort buckets and test names, to have consistent iterator final SortedSet sortedTestNames = new TreeSet<>(tests.keySet()); for (final String testName : sortedTestNames) { final Set> buckets = Sets.newLinkedHashSet(); final TestSpecification testSpecification = tests.get(testName); final Map.Entry[] sortedBuckets = testSpecification .getBuckets() .entrySet() .toArray(new Map.Entry[testSpecification.getBuckets().size()]); if (testSpecification.getPayload() != null && testSpecification.getPayload().getAllowForce()) { forcePayloadTests.add(testName); } Arrays.sort( sortedBuckets, new Comparator>() { public int compare( final Map.Entry e0, final Map.Entry e1) { if (e0.getValue().intValue() < e1.getValue().intValue()) { return -1; } return e0.getValue() == e1.getValue() ? 0 : 1; } }); boolean foundFallbackValue = false; for (final Map.Entry bucket : sortedBuckets) { final String bucketName = bucket.getKey(); final String enumName = toEnumName(bucketName); final Map bucketDef = new HashMap<>(); final String normalizedBucketName = toJavaIdentifier(bucketName); bucketDef.put("value", bucket.getValue()); bucketDef.put("name", bucketName); bucketDef.put("normalizedName", normalizedBucketName); bucketDef.put("enumName", enumName); bucketDef.put("javaClassName", uppercaseFirstChar(normalizedBucketName)); buckets.add(bucketDef); foundFallbackValue = foundFallbackValue || (bucket.getValue() == testSpecification.getFallbackValue()); } if (!buckets.isEmpty() && !foundFallbackValue) { throw new IllegalArgumentException( "Specified fallback value " + testSpecification.getFallbackValue() + " for test " + testName + " is not in the list of standard values: " + Arrays.toString(sortedBuckets)); } final String name = toJavaIdentifier(testName); final String enumName = toEnumName(name); final Map testDef = new HashMap<>(); testDef.put("name", testName); testDef.put("normalizedName", name); testDef.put("enumName", enumName); testDef.put("javaClassName", uppercaseFirstChar(name)); testDef.put("buckets", buckets); testDef.put("defaultValue", testSpecification.getFallbackValue()); final List> nestedPayloadsList = new ArrayList<>(); // Only define testDef.payloadJavaClass if the API user has // claimed she expects a payload. if (testSpecification.getPayload() != null) { final String specifiedPayloadTypeName = testSpecification.getPayload().getType(); final PayloadType specifiedPayloadType = PayloadType.payloadTypeForName(specifiedPayloadTypeName); if (specifiedPayloadType == PayloadType.MAP) { testDef.put("isMap", "true"); for (final Map.Entry entry : testSpecification.getPayload().getSchema().entrySet()) { final Map nestedPayloadsMap = new HashMap<>(); nestedPayloadsMap.put("key", entry.getKey()); final PayloadType payloadTypeForValue = PayloadType.payloadTypeForName(entry.getValue()); if (payloadTypeForValue != PayloadType.MAP) { nestedPayloadsMap.put("value", payloadTypeForValue.javaClassName); nestedPayloadsMap.put( "valueWithoutArray", payloadTypeForValue.javaClassName.substring( 0, payloadTypeForValue.javaClassName.length() - 2)); if (PayloadType.STRING_ARRAY == payloadTypeForValue) { nestedPayloadsMap.put("notANumber", "true"); } nestedPayloadsMap.put( "payloadTypeName", payloadTypeForValue.payloadTypeName); nestedPayloadsList.add(nestedPayloadsMap); } else { throw new IllegalArgumentException( "Nested Map Payloads are not allowed"); } } } if (specifiedPayloadType == PayloadType.JSON) { testDef.put("isJson", "true"); } addPayloadToTestDef(testDef, specifiedPayloadType); } if (testSpecification.getDescription() != null) { testDef.put( "description", StringEscapeUtils.escapeJava(testSpecification.getDescription())); } testDef.put("nestedPayloadsList", nestedPayloadsList); testDefs.add(testDef); final String hashedProctorName = TEST_NAME_OBFUSCATOR.obfuscateTestName(testName); testDefsMap.put(hashedProctorName, testDef); } rootMap.put("contextArguments", spec.getProvidedContext()); rootMap.put("mainClassName", className); rootMap.put("packageName", packageName); rootMap.put("testEnumName", "Test"); rootMap.put("testDefs", testDefs); rootMap.put("testDefsMap", testDefsMap); rootMap.put("forcePayloadTests", forcePayloadTests); return rootMap; } abstract void addPayloadToTestDef( final Map testDef, final PayloadType specifiedPayloadType); }