@Test
 public void testImplicitDepsAreAddedCorrectly() throws NoSuchBuildTargetException {
   Description<GenruleDescription.Arg> genruleDescription = new GenruleDescription();
   Map<String, Object> instance =
       ImmutableMap.<String, Object>of(
           "srcs", ImmutableList.of(":baz", "//biz:baz"),
           "out", "AndroidManifest.xml",
           "cmd", "$(exe //bin:executable) $(location :arg)");
   ProjectFilesystem projectFilesystem = new AllExistingProjectFilesystem();
   BuildRuleFactoryParams params =
       new BuildRuleFactoryParams(projectFilesystem, BuildTargetFactory.newInstance("//foo:bar"));
   ConstructorArgMarshaller marshaller =
       new ConstructorArgMarshaller(
           new DefaultTypeCoercerFactory(ObjectMappers.newDefaultInstance()));
   ImmutableSet.Builder<BuildTarget> declaredDeps = ImmutableSet.builder();
   ImmutableSet.Builder<VisibilityPattern> visibilityPatterns = ImmutableSet.builder();
   GenruleDescription.Arg constructorArg = genruleDescription.createUnpopulatedConstructorArg();
   try {
     marshaller.populate(
         createCellRoots(projectFilesystem),
         projectFilesystem,
         params,
         constructorArg,
         declaredDeps,
         visibilityPatterns,
         instance);
   } catch (ConstructorArgMarshalException e) {
     fail("Expected constructorArg to be correctly populated.");
   }
   TargetNode<GenruleDescription.Arg> targetNode =
       new TargetNodeFactory(new DefaultTypeCoercerFactory(ObjectMappers.newDefaultInstance()))
           .create(
               Hashing.sha1().hashString(params.target.getFullyQualifiedName(), UTF_8),
               genruleDescription,
               constructorArg,
               params,
               declaredDeps.build(),
               visibilityPatterns.build(),
               createCellRoots(projectFilesystem));
   assertEquals(
       "SourcePaths and targets from cmd string should be extracted as extra deps.",
       ImmutableSet.of("//foo:baz", "//biz:baz", "//bin:executable", "//foo:arg"),
       FluentIterable.from(targetNode.getExtraDeps())
           .transform(Functions.toStringFunction())
           .toSet());
 }
public class HttpArtifactCacheEventTest {

  private static final ObjectMapper JSON_CONVERTER = ObjectMappers.newDefaultInstance();

  private static final ImmutableList<RuleKey> TEST_RULE_KEYS =
      ImmutableList.of(new RuleKey("1234567890"), new RuleKey("123456"), new RuleKey("1234"));
  private static final String TEST_RULE_KEYS_JSON = "[\"1234567890\",\"123456\",\"1234\"]";

  @Test
  public void jsonRepresentationContainsAllRuleKeysWithTransform() throws IOException {
    Iterable<RuleKey> ruleKeysInATransform =
        Iterables.transform(TEST_RULE_KEYS, Functions.<RuleKey>identity());

    HttpArtifactCacheEvent.Finished finishedEvent =
        createBuilder().setRuleKeys(ruleKeysInATransform).build();
    configureEvent(finishedEvent);
    String json = JSON_CONVERTER.writeValueAsString(finishedEvent);
    assertTrue(json.contains(TEST_RULE_KEYS_JSON));
  }

  @Test
  public void jsonRepresentationContainsAllRuleKeys() throws IOException {
    HttpArtifactCacheEvent.Finished finishedEvent =
        createBuilder().setRuleKeys(TEST_RULE_KEYS).build();
    configureEvent(finishedEvent);
    String json = JSON_CONVERTER.writeValueAsString(finishedEvent);
    assertTrue(json.contains(TEST_RULE_KEYS_JSON));
  }

  private static HttpArtifactCacheEvent.Finished.Builder createBuilder() {

    HttpArtifactCacheEvent.Scheduled scheduledEvent =
        HttpArtifactCacheEvent.newStoreScheduledEvent(
            Optional.of("target"), ImmutableSet.<RuleKey>of());
    HttpArtifactCacheEvent.Started startedEvent =
        HttpArtifactCacheEvent.newStoreStartedEvent(scheduledEvent);
    configureEvent(startedEvent);
    return HttpArtifactCacheEvent.newFinishedEventBuilder(startedEvent);
  }

  private static void configureEvent(AbstractBuckEvent event) {
    event.configure(-1, -1, -1, new BuildId());
  }
}
/**
 * {@link DexProducedFromJavaLibrary} is a {@link BuildRule} that serves a very specific purpose: it
 * takes a {@link JavaLibrary} and dexes the output of the {@link JavaLibrary} if its list of
 * classes is non-empty. Because it is expected to be used with pre-dexing, we always pass the
 * {@code --force-jumbo} flag to {@code dx} in this buildable.
 *
 * <p>Most {@link BuildRule}s can determine the (possibly null) path to their output file from their
 * definition. This is an anomaly because we do not know whether this will write a {@code .dex} file
 * until runtime. Unfortunately, because there is no such thing as an empty {@code .dex} file, we
 * cannot write a meaningful "dummy .dex" if there are no class files to pass to {@code dx}.
 */
public class DexProducedFromJavaLibrary extends AbstractBuildRule
    implements AbiRule, HasBuildTarget, InitializableFromDisk<BuildOutput> {

  private static final ObjectMapper MAPPER = ObjectMappers.newDefaultInstance();
  private static final Function<String, HashCode> TO_HASHCODE =
      new Function<String, HashCode>() {
        @Override
        public HashCode apply(String input) {
          return HashCode.fromString(input);
        }
      };

  @VisibleForTesting static final String LINEAR_ALLOC_KEY_ON_DISK_METADATA = "linearalloc";
  static final String CLASSNAMES_TO_HASHES = "classnames_to_hashes";

  private final JavaLibrary javaLibrary;
  private final BuildOutputInitializer<BuildOutput> buildOutputInitializer;

  @VisibleForTesting
  DexProducedFromJavaLibrary(
      BuildRuleParams params, SourcePathResolver resolver, JavaLibrary javaLibrary) {
    super(params, resolver);
    this.javaLibrary = javaLibrary;
    this.buildOutputInitializer = new BuildOutputInitializer<>(params.getBuildTarget(), this);
  }

  @Override
  public ImmutableList<Step> getBuildSteps(
      BuildContext context, final BuildableContext buildableContext) {
    ImmutableList.Builder<Step> steps = ImmutableList.builder();

    steps.add(new RmStep(getProjectFilesystem(), getPathToDex(), /* shouldForceDeletion */ true));

    // Make sure that the buck-out/gen/ directory exists for this.buildTarget.
    steps.add(new MkdirStep(getProjectFilesystem(), getPathToDex().getParent()));

    // If there are classes, run dx.
    final ImmutableSortedMap<String, HashCode> classNamesToHashes =
        javaLibrary.getClassNamesToHashes();
    final boolean hasClassesToDx = !classNamesToHashes.isEmpty();
    final Supplier<Integer> linearAllocEstimate;
    if (hasClassesToDx) {
      Path pathToOutputFile = javaLibrary.getPathToOutput();
      EstimateLinearAllocStep estimate =
          new EstimateLinearAllocStep(getProjectFilesystem(), pathToOutputFile);
      steps.add(estimate);
      linearAllocEstimate = estimate;

      // To be conservative, use --force-jumbo for these intermediate .dex files so that they can be
      // merged into a final classes.dex that uses jumbo instructions.
      DxStep dx =
          new DxStep(
              getProjectFilesystem(),
              getPathToDex(),
              Collections.singleton(pathToOutputFile),
              EnumSet.of(
                  DxStep.Option.USE_CUSTOM_DX_IF_AVAILABLE,
                  DxStep.Option.RUN_IN_PROCESS,
                  DxStep.Option.NO_OPTIMIZE,
                  DxStep.Option.FORCE_JUMBO));
      steps.add(dx);

      // The `DxStep` delegates to android tools to build a ZIP with timestamps in it, making
      // the output non-deterministic.  So use an additional scrubbing step to zero these out.
      steps.add(new ZipScrubberStep(getProjectFilesystem(), getPathToDex()));

    } else {
      linearAllocEstimate = Suppliers.ofInstance(0);
    }

    // Run a step to record artifacts and metadata. The values recorded depend upon whether dx was
    // run.
    String stepName = hasClassesToDx ? "record_dx_success" : "record_empty_dx";
    AbstractExecutionStep recordArtifactAndMetadataStep =
        new AbstractExecutionStep(stepName) {
          @Override
          public StepExecutionResult execute(ExecutionContext context) throws IOException {
            if (hasClassesToDx) {
              buildableContext.recordArtifact(getPathToDex());
            }

            buildableContext.addMetadata(
                LINEAR_ALLOC_KEY_ON_DISK_METADATA, String.valueOf(linearAllocEstimate.get()));

            // Record the classnames to hashes map.
            buildableContext.addMetadata(
                CLASSNAMES_TO_HASHES,
                context
                    .getObjectMapper()
                    .writeValueAsString(
                        Maps.transformValues(classNamesToHashes, Functions.toStringFunction())));

            return StepExecutionResult.SUCCESS;
          }
        };
    steps.add(recordArtifactAndMetadataStep);

    return steps.build();
  }

  @Override
  public BuildOutput initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) throws IOException {
    int linearAllocEstimate =
        Integer.parseInt(onDiskBuildInfo.getValue(LINEAR_ALLOC_KEY_ON_DISK_METADATA).get());
    Map<String, String> map =
        MAPPER.readValue(
            onDiskBuildInfo.getValue(CLASSNAMES_TO_HASHES).get(),
            new TypeReference<Map<String, String>>() {});
    Map<String, HashCode> classnamesToHashes = Maps.transformValues(map, TO_HASHCODE);
    return new BuildOutput(linearAllocEstimate, ImmutableSortedMap.copyOf(classnamesToHashes));
  }

  @Override
  public BuildOutputInitializer<BuildOutput> getBuildOutputInitializer() {
    return buildOutputInitializer;
  }

  static class BuildOutput {
    private final int linearAllocEstimate;
    private final ImmutableSortedMap<String, HashCode> classnamesToHashes;

    BuildOutput(int linearAllocEstimate, ImmutableSortedMap<String, HashCode> classnamesToHashes) {
      this.linearAllocEstimate = linearAllocEstimate;
      this.classnamesToHashes = classnamesToHashes;
    }
  }

  @Override
  @Nullable
  public Path getPathToOutput() {
    // A .dex file is not guaranteed to be generated, so we return null to be conservative.
    return null;
  }

  public Path getPathToDex() {
    return BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "%s.dex.jar");
  }

  public boolean hasOutput() {
    return !getClassNames().isEmpty();
  }

  ImmutableSortedMap<String, HashCode> getClassNames() {
    // TODO(bolinfest): Assert that this Buildable has been built. Currently, there is no way to do
    // that from a Buildable (but there is from an AbstractCachingBuildRule).
    return buildOutputInitializer.getBuildOutput().classnamesToHashes;
  }

  int getLinearAllocEstimate() {
    return buildOutputInitializer.getBuildOutput().linearAllocEstimate;
  }

  /**
   * The only dep for this rule should be {@link #javaLibrary}. Therefore, the ABI key for the deps
   * of this buildable is the hash of the {@code .class} files for {@link #javaLibrary}.
   */
  @Override
  public Sha1HashCode getAbiKeyForDeps(DefaultRuleKeyBuilderFactory defaultRuleKeyBuilderFactory) {
    return computeAbiKey(javaLibrary.getClassNamesToHashes());
  }

  @VisibleForTesting
  static Sha1HashCode computeAbiKey(ImmutableSortedMap<String, HashCode> classNames) {
    Hasher hasher = Hashing.sha1().newHasher();
    for (Map.Entry<String, HashCode> entry : classNames.entrySet()) {
      hasher.putUnencodedChars(entry.getKey());
      hasher.putByte((byte) 0);
      hasher.putUnencodedChars(entry.getValue().toString());
      hasher.putByte((byte) 0);
    }
    return Sha1HashCode.fromHashCode(hasher.hash());
  }
}