Add a class that can generate a subset of buck.py and populate constructor args reflectively. Summary: As part of the work to add a plugin mechanism, we need a way of allowing new extensions to declare what arguments and dependencies they have without needing to modify buck.py. The BuildableArgInspector provides that. Test Plan: buck test --all
diff --git a/src/com/facebook/buck/rules/ArgObjectPopulatomatic.java b/src/com/facebook/buck/rules/ArgObjectPopulatomatic.java new file mode 100644 index 0000000..073b914 --- /dev/null +++ b/src/com/facebook/buck/rules/ArgObjectPopulatomatic.java
@@ -0,0 +1,130 @@ +/* + * Copyright 2013-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.facebook.buck.rules; + +import com.facebook.buck.model.BuildTarget; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; + +/** + * Used to derive information from the constructor args returned by {@link Description} instances. + * There are two major uses this information is put to: populating the DTO object from the + * {@link BuildRuleFactoryParams}, which is populated by the functions added to Buck's core build + * file parsing script. The second function of this class is to generate those functions. + * <p> + * The constructor arg is examined for public fields and writable properties, as defined by the + * JavaBean specification. Reflection is used to determine the type of the field, and it's possible + * to indicate that a field isn't mandatory by using {@link Optional} declarations. If the name of + * the property to be exposed to a build file does not match the field/bean property name, then a + * {@link Hint} can be used to set it. + * <p> + * As an example, the arguments for a build target defined as: + * <pre> + * example_library( + * name = 'example', + * srcs = [ '//foo:bar', 'Eggs.java', ], + * optional_thing = True, + * ) + * </pre> + * Could be defined as a constructor arg that looks like: + * <pre> + * public class ExampleLibraryArg { + * public String name; + * public ImmutableSortedSet>SourcePath> srcs; + * @Hint(name = "optional_thing") + * public Optional>Boolean> isNeeded; + * } + * </pre> + */ +// TODO(simons): Revisit class name when we add Descriptions +public class ArgObjectPopulatomatic { + + private final Path basePath; + + /** + * Constructor. {@code pathFromProjectRootToBuildFile} is the path relative to the project root to + * the build file that has called the build rule's function in buck.py. This is used for resolving + * additional paths to ones relative to the project root, and to allow {@link BuildTarget} + * instances to be fully qualified. + * + * @param pathFromProjectRootToBuildFile The path from the root of the project to the directory of + * the build file that this function is being created from. + */ + public ArgObjectPopulatomatic(Path pathFromProjectRootToBuildFile) { + Preconditions.checkNotNull(pathFromProjectRootToBuildFile); + + // Without this check an IndexOutOfBounds exception is thrown by normalize. + if (pathFromProjectRootToBuildFile.toString().isEmpty()) { + this.basePath = pathFromProjectRootToBuildFile; + } else { + this.basePath = pathFromProjectRootToBuildFile.normalize(); + } + } + + /** + * Use the information contained in the {@code params} to fill in the public fields and settable + * properties of {@code dto}. The following rules are used: + * <ul> + * <li>Boolean values are set to true or false.</li> + * <li>{@link BuildRule}s will be resovled.</li> + * <li>{@link BuildTarget}s are resolved and will be fully qualified.</li> + * <li>Numeric values are handled as if being cast from Long.</li> + * <li>{@link SourcePath} instances will be set to the appropriate implementation.</li> + * <li>{@link Path} declarations will be set to be relative to the project root.</li> + * <li>Strings will be set "as is".</li> + * <li>{@link List}s and {@link Set}s will be populated with the expected generic type, + * provided the wildcarding allows an upperbound to be determined to be one of the above.</li> + * </ul> + * + * Any property that is marked as being an {@link Optional} field will be set to a default value + * if none is set. This is typically {@link Optional#absent()}, but in the case of collections is + * an empty collection. + * + * @param ruleResolver The resolver to use when looking up {@link BuildRule}s. + * @param params The parameters to be used to populate the {@code dto} instance. + * @param dto The constructor dto to be populated. + */ + public void populate(BuildRuleResolver ruleResolver, BuildRuleFactoryParams params, Object dto) { + Set<ParamInfo> allInfo = getAllParamInfo(dto); + + for (ParamInfo info : allInfo) { + info.setFromParams(ruleResolver, dto, params); + } + } + + ImmutableSet<ParamInfo> getAllParamInfo(Object dto) { + Class<?> argClass = dto.getClass(); + + ImmutableSet.Builder<ParamInfo> allInfo = ImmutableSet.builder(); + + for (Field field : argClass.getFields()) { + if (Modifier.isFinal(field.getModifiers())) { + continue; + } + allInfo.add(new ParamInfo(basePath, field)); + } + + return allInfo.build(); + } +}
diff --git a/src/com/facebook/buck/rules/BUCK b/src/com/facebook/buck/rules/BUCK index a92ba22..2d2026d 100644 --- a/src/com/facebook/buck/rules/BUCK +++ b/src/com/facebook/buck/rules/BUCK
@@ -32,8 +32,10 @@ 'AbstractBuildRuleBuilderParams.java', 'AbstractSourcePath.java', 'AnnotationProcessingData.java', + 'ArgObjectPopulatomatic.java', 'ArtifactCache.java', 'BinaryBuildRule.java', + 'BuckPyFunction.java', 'Buildable.java', 'Buildables.java', 'BuildableContext.java', @@ -55,8 +57,10 @@ 'DefaultBuildRuleBuilderParams.java', 'DependencyGraph.java', 'FileSourcePath.java', + 'Hint.java', 'JavaPackageFinder.java', 'OnDiskBuildInfo.java', + 'ParamInfo.java', 'RuleKey.java', 'RuleKeyBuilderFactory.java', 'Sha1HashCode.java', @@ -140,12 +144,12 @@ '//src/com/facebook/buck/step:step', '//src/com/facebook/buck/step/fs:fs', '//src/com/facebook/buck/test:test', + '//src/com/facebook/buck/timing:timing', '//src/com/facebook/buck/util:constants', '//src/com/facebook/buck/util/collect:collect', '//src/com/facebook/buck/util:exceptions', '//src/com/facebook/buck/util:io', '//src/com/facebook/buck/util:network', - '//src/com/facebook/buck/timing:timing', '//src/com/facebook/buck/util:util', '//src/com/facebook/buck/util/concurrent:concurrent', '//src/com/facebook/buck/util/environment:environment',
diff --git a/src/com/facebook/buck/rules/BuckPyFunction.java b/src/com/facebook/buck/rules/BuckPyFunction.java new file mode 100644 index 0000000..c9f7a4e --- /dev/null +++ b/src/com/facebook/buck/rules/BuckPyFunction.java
@@ -0,0 +1,131 @@ +/* + * Copyright 2013-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.facebook.buck.rules; + +import com.facebook.buck.util.HumanReadableException; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; + +import java.util.SortedSet; + +/** + * Used to generate a function for use within buck.py for the rule described by a + * {@link Description}. + */ +class BuckPyFunction { + + private final ArgObjectPopulatomatic argsInspector; + + public BuckPyFunction(ArgObjectPopulatomatic argsInspector) { + this.argsInspector = Preconditions.checkNotNull(argsInspector); + } + + String toPythonFunction(BuildRuleType type, Object dto) { + Preconditions.checkNotNull(type); + Preconditions.checkNotNull(dto); + + StringBuilder builder = new StringBuilder(); + + SortedSet<ParamInfo> mandatory = Sets.newTreeSet(); + SortedSet<ParamInfo> optional = Sets.newTreeSet(); + + for (ParamInfo param : argsInspector.getAllParamInfo(dto)) { + if (isSkippable(param)) { + continue; + } + + if (param.isOptional()) { + optional.add(param); + } else { + mandatory.add(param); + } + } + + builder.append("@provide_for_build\n") + .append("def ").append(type.getName()).append("(name, "); + + // Construct the args. + for (ParamInfo param : Iterables.concat(mandatory, optional)) { + appendPythonParameter(builder, param); + } + builder.append("visibility=[], build_env=None):\n") + + // Define the rule. + .append(" add_rule({\n") + .append(" 'type' : '").append(type.getName()).append("',\n") + .append(" 'name' : name,\n"); + + // Iterate over args. + for (ParamInfo param : Iterables.concat(mandatory, optional)) { + builder.append(" '") + .append(param.getName()) + .append("' : ") + .append(param.getName()) + .append(",\n"); + } + + builder.append(" 'visibility' : visibility,\n"); + builder.append(" }, build_env)\n\n"); + + return builder.toString(); + } + + private void appendPythonParameter(StringBuilder builder, ParamInfo param) { + builder.append(param.getName()); + if (param.isOptional()) { + builder.append("=").append(getPythonDefault(param)); + } + builder.append(", "); + } + + private String getPythonDefault(ParamInfo param) { + if (param.getContainerType() != null) { + return "[]"; + } + + if (Boolean.class.equals(param.getType())) { + return "False"; + } + + if (Number.class.isAssignableFrom(param.getType())) { + return "0"; + } + + if (String.class.equals(param.getType())) { + return "''"; + } + + return "None"; + } + + private boolean isSkippable(ParamInfo param) { + if ("name".equals(param.getName())) { + if (!String.class.equals(param.getType())) { + throw new HumanReadableException("'name' parameter must be a java.lang.String"); + } + return true; + } + + if ("visibility".equals(param.getName())) { + throw new HumanReadableException( + "'visibility' parameter must be omitted. It will be passed to the rule at run time."); + } + + return false; + } +}
diff --git a/src/com/facebook/buck/rules/Hint.java b/src/com/facebook/buck/rules/Hint.java new file mode 100644 index 0000000..f5ae611 --- /dev/null +++ b/src/com/facebook/buck/rules/Hint.java
@@ -0,0 +1,38 @@ +/* + * Copyright 2013-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.facebook.buck.rules; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + + +/** + * Represents hints given when deal with the value returned by + * {@link Description#getConstructorArg()}. + */ +@Retention(RUNTIME) +@Target({FIELD, METHOD}) +public @interface Hint { + /** + * @return The name to use in preference to the field or property name (eg. "field_name") + */ + String name(); +}
diff --git a/src/com/facebook/buck/rules/ParamInfo.java b/src/com/facebook/buck/rules/ParamInfo.java new file mode 100644 index 0000000..0c75f73 --- /dev/null +++ b/src/com/facebook/buck/rules/ParamInfo.java
@@ -0,0 +1,392 @@ +/* + * Copyright 2013-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.facebook.buck.rules; + +import static com.facebook.buck.model.BuildTarget.BUILD_TARGET_PREFIX; + +import com.facebook.buck.model.BuildTarget; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Lists; +import com.google.common.primitives.Primitives; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; + +import javax.annotation.Nullable; + +/** + * Parameter information derived from the object returned by + * {@link Description#getConstructorArg()}. + */ +// DO NOT EXPOSE OUT OF PACKAGE. +class ParamInfo implements Comparable<ParamInfo> { + private final String name; + + /** + * Is the property optional (indicated by being Optional or @Hinted with a default value. + */ + private final boolean isOptional; + + /** + * If the value is a List or Set, which of the two is it. + */ + @Nullable + private final Class<?> containerType; + + /** + * The type of the field, or the contained type of an Optional or a Collection. + */ + private final Class<?> type; + private final Path pathRelativeToProjectRoot; + + private Field field; + + /** + * @param pathRelativeToProjectRoot The path to the directory containing the build file this param + * is for. + * @param field The field in the constructor arg that this represents. + */ + public ParamInfo(Path pathRelativeToProjectRoot, Field field) { + this.pathRelativeToProjectRoot = Preconditions.checkNotNull(pathRelativeToProjectRoot); + + this.field = Preconditions.checkNotNull(field); + Hint hint = field.getAnnotation(Hint.class); + this.name = determineName(field.getName(), hint); + + Class<?> rawType = field.getType(); + Type genericType = field.getGenericType(); + + this.isOptional = Optional.class.isAssignableFrom(rawType); + + // TODO(simons): Check for a wildcard here. + if (List.class.isAssignableFrom(rawType) || Set.class.isAssignableFrom(rawType)) { + this.containerType = rawType; + this.type = determineGenericType(genericType); + } else if (Optional.class.isAssignableFrom(rawType)) { + this.type = unwrapGenericType(genericType); + + Class<?> container = null; + + if (genericType instanceof ParameterizedType) { + Type containerType = ((ParameterizedType) genericType).getActualTypeArguments()[0]; + if (containerType instanceof ParameterizedType) { + Type rawContainerType = ((ParameterizedType) containerType).getRawType(); + if (!(rawContainerType instanceof Class)) { + throw new RuntimeException("Container type isn't a class: " + rawContainerType); + } + container = (Class<?>) rawContainerType; + if (!Collection.class.isAssignableFrom(container)) { + throw new RuntimeException("Cannot determine container type: " + container); + } + } + } + containerType = container; + } else { + this.containerType = null; + this.type = Primitives.wrap(rawType); + } + } + + String getName() { + return name; + } + + boolean isOptional() { + return isOptional; + } + + @Nullable + Class<?> getContainerType() { + return containerType; + } + + Class<?> getType() { + return type; + } + + private Class<?> unwrapGenericType(Type type) { + if (!(type instanceof ParameterizedType)) { + return (Class<?>) type; + } + + Type[] types = ((ParameterizedType) type).getActualTypeArguments(); + if (types.length != 1) { + throw new IllegalArgumentException("Unable to determine generic type"); + } + + if (types[0] instanceof WildcardType) { + throw new IllegalStateException("Generic types must be specific: " + type); + } + + if (types[0] instanceof ParameterizedType) { + return unwrapGenericType(types[0]); + } + return (Class<?>) types[0]; + } + + private String determineName(String name, @Nullable Hint hint) { + return hint == null ? name : hint.name(); + } + + private Class<?> determineGenericType(Type genericType) { + if (!(genericType instanceof ParameterizedType)) { + throw new IllegalArgumentException("Collection type was not generic"); + } + + Type[] types = ((ParameterizedType) genericType).getActualTypeArguments(); + if (types.length != 1) { + throw new IllegalArgumentException("Unable to determine generic type"); + } + + if (types[0] instanceof WildcardType) { + WildcardType wild = (WildcardType) types[0]; + + if (Object.class.equals(wild.getUpperBounds()[0])) { + throw new IllegalArgumentException("Generic types must be specific: " + genericType); + } + return Primitives.wrap((Class<?>) wild.getUpperBounds()[0]); + } + + return Primitives.wrap((Class<?>) types[0]); + } + + public void setFromParams( + BuildRuleResolver ruleResolver, + Object dto, + BuildRuleFactoryParams params) { + if (containerType != null) { + set(ruleResolver, dto, params.getOptionalListAttribute(name)); + } else if (Path.class.isAssignableFrom(type) || + SourcePath.class.isAssignableFrom(type) || + String.class.equals(type)) { + set(ruleResolver, dto, params.getOptionalStringAttribute(name).orNull()); + } else if (Number.class.isAssignableFrom(type)) { + set(ruleResolver, dto, params.getRequiredLongAttribute(name)); + } else if (BuildTarget.class.isAssignableFrom(type)) { + Optional<BuildTarget> optionalTarget = params.getOptionalBuildTarget(name); + set(ruleResolver, dto, optionalTarget.orNull()); + } else if (BuildRule.class.isAssignableFrom(type)) { + Optional<BuildTarget> optionalTarget = params.getOptionalBuildTarget(name); + set(ruleResolver, dto, optionalTarget.orNull()); + } else if (Boolean.class.equals(type)) { + set(ruleResolver, dto, params.getBooleanAttribute(name)); + } else { + throw new RuntimeException("Unknown type: " + type); + } + } + + /** + * Sets a single property of the {@code dto}, coercing types as necessary. + * + * @param resolver {@link com.facebook.buck.rules.BuildRuleResolver} used for {@link com.facebook.buck.rules.BuildRule} instances. + * @param dto The constructor DTO on which the value should be set. + * @param value The value, which may be coerced depending on the type on {@code dto}. + */ + public void set(BuildRuleResolver resolver, Object dto, @Nullable Object value) { + if (value == null) { + if (!isOptional) { + throw new IllegalArgumentException(String.format( + "%s cannot be null. Build file can be found in %s.", + dto, pathRelativeToProjectRoot)); + } + + value = Optional.absent(); + } else if (isOptional && matchesDefaultValue(value)) { + if (value instanceof Collection) { + value = asCollection(resolver, Lists.newArrayList()); + value = Optional.of(value); + } else { + value = Optional.absent(); + } + } else if (containerType == null) { + value = coerceToExpectedType(resolver, value); + if (isOptional) { + value = Optional.of(value); + } + } else { + value = asCollection(resolver, value); + if (isOptional) { + value = Optional.of(value); + } + } + + try { + field.set(dto, value); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } catch (ClassCastException | IllegalArgumentException | NullPointerException e) { + throw new IllegalArgumentException(String.format( + "Unable to convert '%s' to %s in build file in %s", + value, type, pathRelativeToProjectRoot)); + } + } + + private boolean matchesDefaultValue(Object value) { + if (value instanceof String && "".equals(value)) { + return true; + } + + if (value instanceof Number && ((Number)value).intValue() == 0) { + return true; + } + + if (Boolean.FALSE.equals(value)) { + return true; + } + + if (value instanceof List && ((List) value).isEmpty()) { + return true; + } + + return false; + } + + private Object coerceToExpectedType(BuildRuleResolver ruleResolver, Object value) { + if (BuildRule.class.isAssignableFrom(type)) { + BuildTarget target = asBuildTarget(value); + return ruleResolver.get(target); + } + + if (BuildTarget.class.isAssignableFrom(type)) { + return asBuildTarget(value); + } + + // All paths should be relative to the base path. + if (Path.class.isAssignableFrom(type)) { + return asNormalizedPath(value); + } + + if (SourcePath.class.isAssignableFrom(type)) { + BuildTarget target = asBuildTarget(value); + if (target != null) { + return new BuildTargetSourcePath(target); + } + Path path = asNormalizedPath(value); + return new FileSourcePath(pathRelativeToProjectRoot.relativize(path).toString()); + } + + if (value instanceof Number) { + Number num = (Number) value; + if (Double.class.equals(type)) { + return num.doubleValue(); + } else if (Integer.class.equals(type)) { + return num.intValue(); + } else if (Float.class.equals(type)) { + return num.floatValue(); + } else if (Long.class.equals(type)) { + return num.longValue(); // not strictly necessary, but included for completeness. + } else if (Short.class.equals(type)) { + return num.shortValue(); + } + } + + // We're going to cheat and let the JVM take the strain of converting between primitive and + // object wrapper types, but it has a habit of coercing to a String that should be avoided. + if (String.class.equals(type) && !String.class.equals(value.getClass())) { + throw new IllegalArgumentException( + String.format("Unable to convert '%s' to %s", value, type)); + } + + return value; + } + + @Nullable + private BuildTarget asBuildTarget(Object value) { + if (value instanceof BuildTarget) { + return (BuildTarget) value; + } + + Preconditions.checkArgument(value instanceof String, + "Expected argument '%s' to be a build target", value); + + String param = (String) value; + int colon = param.indexOf(':'); + if (colon == 0 && param.length() > 1) { + return new BuildTarget( + BUILD_TARGET_PREFIX + pathRelativeToProjectRoot.toString(), + param.substring(1)); + } else if (colon > 0 && param.length() > 2) { + return new BuildTarget(param.substring(0, colon), param.substring(colon + 1)); + } + return null; + } + + private Path asNormalizedPath(Object value) { + Preconditions.checkArgument(value instanceof String, + "Expected argument '%s' to be a string in build file in %s", + value, pathRelativeToProjectRoot); + + return pathRelativeToProjectRoot.resolve((String) value).normalize(); + } + + @SuppressWarnings("unchecked") + private Object asCollection(BuildRuleResolver ruleResolver, Object value) { + if (!(value instanceof Collection)) { + throw new IllegalArgumentException(String.format( + "May not set '%s' on a collection type in build file in %s", + value, pathRelativeToProjectRoot)); + } + + List<Object> collection = Lists.newArrayList(); + + for (Object obj : (Iterable<Object>) value) { + collection.add(coerceToExpectedType(ruleResolver, obj)); + } + + if (SortedSet.class.isAssignableFrom(containerType)) { + return ImmutableSortedSet.copyOf(collection); + } else if (Set.class.isAssignableFrom(containerType)) { + return ImmutableSet.copyOf(collection); + } else { + return ImmutableList.copyOf(collection); + } + } + + /** + * Only valid when comparing {@link ParamInfo} instances from the same description. + */ + @Override + public int compareTo(ParamInfo that) { + return this.name.compareTo(that.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ParamInfo)) { + return false; + } + + ParamInfo that = (ParamInfo) obj; + return name.equals(that.getName()); + } +}
diff --git a/test/com/facebook/buck/rules/ArgObjectPopulatomaticTest.java b/test/com/facebook/buck/rules/ArgObjectPopulatomaticTest.java new file mode 100644 index 0000000..a207fd6 --- /dev/null +++ b/test/com/facebook/buck/rules/ArgObjectPopulatomaticTest.java
@@ -0,0 +1,480 @@ +/* + * Copyright 2013-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.facebook.buck.rules; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.facebook.buck.model.BuildTarget; +import com.facebook.buck.model.BuildTargetFactory; +import com.facebook.buck.parser.BuildTargetParser; +import com.facebook.buck.util.ProjectFilesystem; +import com.google.common.base.Optional; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@SuppressWarnings("unused") // Many unused fields in sample DTO objects. +public class ArgObjectPopulatomaticTest { + + private Path basePath; + private ArgObjectPopulatomatic inspector; + private BuildRuleResolver ruleResolver; + + @Before + public void setUpInspector() { + basePath = Paths.get("example", "path"); + inspector = new ArgObjectPopulatomatic(basePath); + ruleResolver = new BuildRuleResolver(); + } + + @Test + public void shouldNotPopulateAnEmptyArg() { + class Dto { + } + + Dto dto = new Dto(); + try { + inspector.populate( + ruleResolver, buildRuleFactoryParams(ImmutableMap.<String, Object>of()), dto); + } catch (RuntimeException e) { + fail("Did not expect an exception to be thrown:\n" + Throwables.getStackTraceAsString(e)); + } + } + + @Test + public void shouldPopulateAStringValue() { + class Dto { + public String name; + } + + Dto dto = new Dto(); + inspector.populate( + ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of("name", "cheese")), + dto); + + assertEquals("cheese", dto.name); + } + + @Test + public void shouldPopulateABooleanValue() { + class Dto { + public boolean value; + } + + Dto dto = new Dto(); + inspector.populate(ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of("value", true)), + dto); + + assertTrue(dto.value); + } + + @Test + public void shouldPopulateBuildTargetValues() { + class Dto { + public BuildTarget target; + public BuildTarget local; + } + + Dto dto = new Dto(); + inspector.populate(ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of( + "target", "//cake:walk", + "local", ":fish" + )), + dto); + + assertEquals(BuildTargetFactory.newInstance("//cake:walk"), dto.target); + assertEquals(BuildTargetFactory.newInstance("//example/path:fish"), dto.local); + } + + @Test + public void shouldPopulateANumericValue() { + class Dto { + public long number; + } + + Dto dto = new Dto(); + inspector.populate(ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of("number", 42L)), + dto); + + assertEquals(42, dto.number); + } + + @Test + public void shouldPopulateAPathValue() { + class Dto { + @Hint(name = "some_path") + public Path somePath; + } + + Dto dto = new Dto(); + inspector.populate(ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of("some_path", "Fish.java")), + dto); + + assertEquals(Paths.get("example/path", "Fish.java"), dto.somePath); + } + + @Test + public void shouldPopulateSourcePaths() { + class Dto { + public SourcePath filePath; + public SourcePath targetPath; + } + + BuildTarget target = BuildTargetFactory.newInstance("//example/path:peas"); + Dto dto = new Dto(); + inspector.populate( + ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of( + "filePath", "cheese.txt", + "targetPath", ":peas" + )), + dto); + + assertEquals(new FileSourcePath("cheese.txt"), dto.filePath); + assertEquals(new BuildTargetSourcePath(target), dto.targetPath); + } + + @Test + public void shouldPopulateAnImmutableSortedSet() { + class Dto { + public ImmutableSortedSet<BuildTarget> deps; + } + + BuildTarget t1 = BuildTargetFactory.newInstance("//please/go:here"); + BuildTarget t2 = BuildTargetFactory.newInstance("//example/path:there"); + + Dto dto = new Dto(); + // Note: the ordering is reversed from the natural ordering + inspector.populate( + ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of( + "deps", ImmutableList.of("//please/go:here", ":there"))), + dto); + + assertEquals(ImmutableSortedSet.of(t2, t1), dto.deps); + } + + @Test + public void shouldPopulateSets() { + class Dto { + public Set<Path> paths; + } + + Dto dto = new Dto(); + inspector.populate( + ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of( + "paths", ImmutableList.of("one", "two"))), + dto); + + assertEquals( + ImmutableSet.of(Paths.get("example/path/one"), Paths.get("example/path/two")), + dto.paths); + } + + @Test + public void shouldPopulateLists() { + class Dto { + public List<String> list; + } + + Dto dto = new Dto(); + inspector.populate( + ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of( + "list", ImmutableList.of("alpha", "beta"))), + dto); + + assertEquals(ImmutableList.of("alpha", "beta"), dto.list); + } + + @Test + public void collectionsCanBeOptionalAndWillBeSetToAnOptionalEmptyCollectionIfMissing() { + class Dto { + public Optional<Set<BuildTarget>> targets; + } + + Dto dto = new Dto(); + Map<String, Object> args = Maps.newHashMap(); + args.put("targets", Lists.newArrayList()); + + inspector.populate(ruleResolver, buildRuleFactoryParams(args), dto); + + assertEquals(Optional.of(Sets.newHashSet()), dto.targets); + } + + @Test(expected = RuntimeException.class) + public void shouldBeAnErrorToAttemptToSetASingleValueToACollection() { + class Dto { + public String file; + } + + Dto dto = new Dto(); + inspector.populate( + ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of("file", ImmutableList.of("a", "b"))), + dto); + } + + @Test(expected = RuntimeException.class) + public void shouldBeAnErrorToAttemptToSetACollectionToASingleValue() { + class Dto { + public Set<String> strings; + } + + Dto dto = new Dto(); + inspector.populate( + ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of("strings", "isn't going to happen")), + dto); + } + + @Test(expected = RuntimeException.class) + public void shouldBeAnErrorToSetTheWrongTypeOfValueInACollection() { + class Dto { + public Set<String> strings; + } + + Dto dto = new Dto(); + inspector.populate( + ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of( + "strings", ImmutableSet.of(true, false))), + dto); + } + + @Test + public void shouldNormalizePaths() { + class Dto { + public Path path; + } + + Dto dto = new Dto(); + inspector.populate( + ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of("path", "./foo/../bar/./fish.txt")), + dto); + + assertEquals(basePath.resolve("bar/fish.txt").normalize(), dto.path); + } + + @Test(expected = IllegalArgumentException.class) + public void lowerBoundGenericTypesCauseAnException() { + class Dto { + public List<? super BuildTarget> nope; + } + + inspector.populate( + ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of( + "nope", ImmutableList.of("//will/not:happen"))), + new Dto()); + } + + public void shouldSetBuildTargetParameters() { + class Dto { + public BuildTarget single; + public BuildTarget sameBuildFileTarget; + public List<BuildTarget> targets; + } + Dto dto = new Dto(); + + inspector.populate( + ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of( + "single", "//com/example:cheese", + "sameBuildFileTarget", ":cake", + "targets", ImmutableList.of(":cake", "//com/example:cheese") + )), + dto); + + BuildTarget cheese = BuildTargetFactory.newInstance("//com/example:cheese"); + BuildTarget cake = BuildTargetFactory.newInstance("//example/path:cake"); + + assertEquals(cheese, dto.single); + assertEquals(cake, dto.sameBuildFileTarget); + assertEquals(ImmutableList.of(cake, cheese), dto.targets); + } + + @Test + public void shouldSetBuildRulesIfRequested() { + class Dto { + public BuildRule directDep; + } + + BuildTarget target = BuildTargetFactory.newInstance("//some/exmaple:target"); + BuildRule rule = new FakeBuildRule(new BuildRuleType("example"), target); + + BuildRuleResolver resolver = new BuildRuleResolver(ImmutableMap.of(target, rule)); + + Dto dto = new Dto(); + inspector.populate( + resolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of( + "directDep", target.getFullyQualifiedName())), + dto); + + assertEquals(dto.directDep, rule); + } + + @Test + public void upperBoundGenericTypesCauseValuesToBeSetToTheUpperBound() { + class Dto { + public List<? extends SourcePath> yup; + } + + BuildTarget target = BuildTargetFactory.newInstance("//will:happen"); + Dto dto = new Dto(); + inspector.populate( + ruleResolver, + buildRuleFactoryParams(ImmutableMap.<String, Object>of( + "yup", ImmutableList.of(target.getFullyQualifiedName()))), + dto); + + BuildTargetSourcePath path = new BuildTargetSourcePath(target); + assertEquals(ImmutableList.of(path), dto.yup); + } + + @Test + public void canPopulateSimpleConstructorArgFromBuildFactoryParams() { + class Dto { + public String required; + public Optional<String> notRequired; + + public int num; + // Turns out there no optional number params + //public Optional<Long> optionalLong; + + public boolean needed; + // Turns out there are no optional boolean params + //public Optional<Boolean> notNeeded; + + public SourcePath aSrcPath; + public Optional<SourcePath> notASrcPath; + + public Path aPath; + public Optional<Path> notAPath; + } + + ImmutableMap<String, Object> args = ImmutableMap.<String, Object>builder() + .put("required", "cheese") + .put("notRequired", "cake") + // Long because that's what comes from python. + .put("num", 42L) + // Skipping optional Long. + .put("needed", true) + // Skipping optional boolean. + .put("aSrcPath", ":path") + .put("aPath", "./File.java") + .put("notAPath", "./NotFile.java") + .build(); + Dto dto = new Dto(); + inspector.populate(ruleResolver, buildRuleFactoryParams(args), dto); + + assertEquals("cheese", dto.required); + assertEquals("cake", dto.notRequired.get()); + assertEquals(42, dto.num); + assertTrue(dto.needed); + BuildTargetSourcePath expected = new BuildTargetSourcePath( + BuildTargetFactory.newInstance("//example/path:path")); + assertEquals(expected, dto.aSrcPath); + assertEquals(Paths.get("example/path/NotFile.java"), dto.notAPath.get()); + } + + @Test + /** + * Since we populated the params from the python script, and the python script inserts default + * values instead of nulls it's never possible for someone to see "Optional.absent()", but that's + * what we want as authors of buildables. Handle that case. + */ + public void shouldPopulateDefaultValuesAsBeingAbsent() { + class Dto { + public Optional<String> noString; + public Optional<String> defaultString; + + public Optional<SourcePath> noSourcePath; + public Optional<SourcePath> defaultSourcePath; + } + + ImmutableMap<String, Object> args = ImmutableMap.<String, Object>builder() + .put("defaultString", "") + .put("defaultSourcePath", "") + .build(); + Dto dto = new Dto(); + inspector.populate(ruleResolver, buildRuleFactoryParams(args), dto); + + assertEquals(Optional.absent(), dto.noString); + assertEquals(Optional.absent(), dto.defaultString); + assertEquals(Optional.absent(), dto.noSourcePath); + assertEquals(Optional.absent(), dto.defaultSourcePath); + } + + @Test + public void shouldResolveBuildRulesFromTargetsAndAssignToFields() { + class Dto { + public BuildRule rule; + } + + BuildTarget target = BuildTargetFactory.newInstance("//i/love:lucy"); + BuildRule rule = new FakeBuildRule(new BuildRuleType("example"), target); + BuildRuleResolver resolver = new BuildRuleResolver(ImmutableMap.of(target, rule)); + + Dto dto = new Dto(); + inspector.populate( + resolver, + buildRuleFactoryParams( + ImmutableMap.<String, Object>of("rule", target.getFullyQualifiedName())), + dto); + + assertEquals(rule, dto.rule); + } + + public BuildRuleFactoryParams buildRuleFactoryParams(Map<String, Object> args) { + ProjectFilesystem filesystem = new ProjectFilesystem(new File(".")) { + @Override + public boolean exists(String pathRelativeToProjectRoot) { + return true; + } + }; + BuildTargetParser parser = new BuildTargetParser(filesystem); + BuildTarget target = BuildTargetFactory.newInstance("//example/path:three"); + return NonCheckingBuildRuleFactoryParams.createNonCheckingBuildRuleFactoryParams( + args, parser, target); + } +}
diff --git a/test/com/facebook/buck/rules/BuckPyFunctionTest.java b/test/com/facebook/buck/rules/BuckPyFunctionTest.java new file mode 100644 index 0000000..c094549 --- /dev/null +++ b/test/com/facebook/buck/rules/BuckPyFunctionTest.java
@@ -0,0 +1,142 @@ +/* + * Copyright 2013-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.facebook.buck.rules; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.facebook.buck.model.BuildTarget; +import com.facebook.buck.model.BuildTargetPattern; +import com.facebook.buck.util.HumanReadableException; +import com.google.common.base.Joiner; +import com.google.common.base.Optional; + +import org.junit.Before; +import org.junit.Test; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Set; + +@SuppressWarnings("unused") // Many unused fields in sample DTO objects. +public class BuckPyFunctionTest { + + private BuckPyFunction buckPyFunction; + + @Before + public void setUpInspector() { + Path basePath = Paths.get("example", "path"); + buckPyFunction = new BuckPyFunction(new ArgObjectPopulatomatic(basePath)); + } + + @Test + public void nameWillBeAddedIfMissing() { + class NoName { public String random; } + + String definition = buckPyFunction.toPythonFunction(new BuildRuleType("bad"), new NoName()); + + assertTrue(definition.contains("name")); + } + + @Test + public void visibilityWillBeAddedIfMissing() { + class NoVis { public String random; } + + String definition = buckPyFunction.toPythonFunction(new BuildRuleType("bad"), new NoVis()); + + assertTrue(definition.contains("visibility=[]")); + } + + @Test + public void shouldOnlyIncludeTheNameFieldOnce() { + class Named { public String name; } + + String definition = buckPyFunction.toPythonFunction(new BuildRuleType("named"), new Named()); + + assertEquals(Joiner.on("\n").join( + "@provide_for_build", + "def named(name, visibility=[], build_env=None):", + " add_rule({", + " 'type' : 'named',", + " 'name' : name,", + " 'visibility' : visibility,", + " }, build_env)", + "", + "" + ), definition); + } + + @Test(expected = HumanReadableException.class) + public void theNameFieldMustBeAString() { + class BadName { public int name; } + + buckPyFunction.toPythonFunction(new BuildRuleType("nope"), new BadName()); + } + + @Test + public void optionalFieldsAreGivenSensibleDefaultValues() { + class LotsOfOptions { + public Optional<String> thing; + public Optional<List<BuildTarget>> targets; + public Optional<Integer> version; + } + + String definition = buckPyFunction.toPythonFunction( + new BuildRuleType("optional"), new LotsOfOptions()); + + assertTrue(definition, definition.contains("targets=[], thing='', version=0")); + } + + @Test + public void optionalFieldsAreListedAfterMandatoryOnes() { + class Either { + // Alphabetical ordering is deliberate. + public Optional<String> cat; + public String dog; + public Optional<String> egg; + public String fake; + } + + String definition = buckPyFunction.toPythonFunction(new BuildRuleType("either"), new Either()); + + assertEquals(Joiner.on("\n").join( + "@provide_for_build", + "def either(name, dog, fake, cat='', egg='', visibility=[], build_env=None):", + " add_rule({", + " 'type' : 'either',", + " 'name' : name,", + " 'dog' : dog,", + " 'fake' : fake,", + " 'cat' : cat,", + " 'egg' : egg,", + " 'visibility' : visibility,", + " }, build_env)", + "", + "" + ), definition); + } + + @Test(expected = HumanReadableException.class) + public void visibilityOptionsMustNotBeSetAsTheyArePassedInBuildRuleParamsLater() { + class Visible { + public Set<BuildTargetPattern> visibility; + } + + buckPyFunction.toPythonFunction(new BuildRuleType("nope"), new Visible()); + } +}