Add support for --force-jumbo in DxStep.

Summary: We need this for the upcoming predex diff.

Test Plan: Comprehensive unit test for DxStep: DxStepTest.
diff --git a/src/com/facebook/buck/android/DxStep.java b/src/com/facebook/buck/android/DxStep.java
index de51a3d..cde9cc9 100644
--- a/src/com/facebook/buck/android/DxStep.java
+++ b/src/com/facebook/buck/android/DxStep.java
@@ -24,26 +24,47 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 
 import java.nio.file.Path;
+import java.util.EnumSet;
 import java.util.Set;
 
 public class DxStep extends ShellStep {
 
+  /** Options to pass to {@code dx}. */
+  public static enum Option {
+    /** Specify the {@code --no-optimize} flag when running {@code dx}. */
+    NO_OPTIMIZE,
+
+    /** Specify the {@code --no-optimize} flag when running {@code dx}. */
+    FORCE_JUMBO,
+    ;
+  }
+
   private final String outputDexFile;
   private final Set<Path> filesToDex;
-  private final boolean optimize;
+  private final Set<Option> options;
 
   /**
-   * @param outputDexFile path to the file where the generated classes.dex should go
+   * @param outputDexFile path to the file where the generated classes.dex should go.
    * @param filesToDex each element in this set is a path to a .class file, a zip file of .class
-   *     files, or a directory of .class files
-   * @param optimize If false, specify {@code --no-optimize}.
+   *     files, or a directory of .class files.
    */
-  public DxStep(String outputDexFile, Iterable<Path> filesToDex, boolean optimize) {
+  public DxStep(String outputDexFile, Iterable<Path> filesToDex) {
+    this(outputDexFile, filesToDex, EnumSet.noneOf(DxStep.Option.class));
+  }
+
+  /**
+   * @param outputDexFile path to the file where the generated classes.dex should go.
+   * @param filesToDex each element in this set is a path to a .class file, a zip file of .class
+   *     files, or a directory of .class files.
+   * @param options to pass to {@code dx}.
+   */
+  public DxStep(String outputDexFile, Iterable<Path> filesToDex, EnumSet<Option> options) {
     this.outputDexFile = Preconditions.checkNotNull(outputDexFile);
     this.filesToDex = ImmutableSet.copyOf(filesToDex);
-    this.optimize = optimize;
+    this.options = Sets.immutableEnumSet(options);
   }
 
   @Override
@@ -59,10 +80,14 @@
       builder.add("--statistics");
     }
 
-    if (!optimize) {
+    if (options.contains(Option.NO_OPTIMIZE)) {
       builder.add("--no-optimize");
     }
 
+    if (options.contains(Option.FORCE_JUMBO)) {
+      builder.add("--force-jumbo");
+    }
+
     // verbose flag, if appropriate.
     if (context.getVerbosity().shouldUseVerbosityFlagIfAvailable()) {
       builder.add("--verbose");
diff --git a/src/com/facebook/buck/android/SmartDexingStep.java b/src/com/facebook/buck/android/SmartDexingStep.java
index 7f389d5..f329a2f 100644
--- a/src/com/facebook/buck/android/SmartDexingStep.java
+++ b/src/com/facebook/buck/android/SmartDexingStep.java
@@ -15,6 +15,7 @@
  */
 package com.facebook.buck.android;
 
+import com.facebook.buck.android.DxStep.Option;
 import com.facebook.buck.java.classes.ClasspathTraversal;
 import com.facebook.buck.java.classes.ClasspathTraverser;
 import com.facebook.buck.java.classes.DefaultClasspathTraverser;
@@ -52,6 +53,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Executors;
@@ -440,9 +442,13 @@
       Preconditions.checkState(newInputsHash != null, "Must call checkIsCached first!");
 
       List<Step> steps = Lists.newArrayList();
+
+      EnumSet<Option> dxOptions = optimizeDex
+          ? EnumSet.noneOf(DxStep.Option.class)
+          : EnumSet.of(DxStep.Option.NO_OPTIMIZE);
       if (useXzCompression()) {
         String tempDexJarOutput = outputPath.replaceAll("\\.jar\\.xz$", ".tmp.jar");
-        steps.add(new DxStep(tempDexJarOutput, srcs, optimizeDex));
+        steps.add(new DxStep(tempDexJarOutput, srcs, dxOptions));
         // We need to make sure classes.dex is STOREd in the .dex.jar file, otherwise .XZ
         // compression won't be effective.
         String repackedJar = outputPath.replaceAll("\\.xz$", "");
@@ -455,7 +461,7 @@
         steps.add(new RmStep(tempDexJarOutput, true));
         steps.add(new XzStep(repackedJar));
       } else {
-        steps.add(new DxStep(outputPath, srcs, optimizeDex));
+        steps.add(new DxStep(outputPath, srcs, dxOptions));
       }
       steps.add(new WriteFileStep(newInputsHash, outputHashPath));
 
diff --git a/src/com/facebook/buck/cli/BUCK b/src/com/facebook/buck/cli/BUCK
index acbbd5b..74081e7 100644
--- a/src/com/facebook/buck/cli/BUCK
+++ b/src/com/facebook/buck/cli/BUCK
@@ -78,6 +78,7 @@
   ],
   visibility = [
     '//test/com/facebook/buck/cli:',
+    '//test/com/facebook/buck/android:android',
     '//test/com/facebook/buck/testutil/integration:integration',
     '//src/com/facebook/buck/event:dependencies-for-external-projects-inner',
   ],
diff --git a/src/com/facebook/buck/cli/VerbosityParser.java b/src/com/facebook/buck/cli/VerbosityParser.java
index ddafd3f..442ca80 100644
--- a/src/com/facebook/buck/cli/VerbosityParser.java
+++ b/src/com/facebook/buck/cli/VerbosityParser.java
@@ -19,7 +19,7 @@
 import com.facebook.buck.util.Verbosity;
 import com.google.common.annotations.VisibleForTesting;
 
-class VerbosityParser {
+public class VerbosityParser {
 
   @VisibleForTesting static final String VERBOSE_LONG_ARG = "--verbose";
 
@@ -42,7 +42,7 @@
     return DEFAULT_VERBOSITY;
   }
 
-  private static Verbosity getVerbosityForLevel(int verbosityLevel) {
+  public static Verbosity getVerbosityForLevel(int verbosityLevel) {
     if (verbosityLevel >= 8) {
       return Verbosity.ALL;
     } else if (verbosityLevel >= 5) {
diff --git a/test/com/facebook/buck/android/BUCK b/test/com/facebook/buck/android/BUCK
index 83bdcd4..4d5fd00 100644
--- a/test/com/facebook/buck/android/BUCK
+++ b/test/com/facebook/buck/android/BUCK
@@ -12,6 +12,7 @@
     '//src/com/facebook/buck/android:rules',
     '//src/com/facebook/buck/android:split_dex',
     '//src/com/facebook/buck/android:steps',
+    '//src/com/facebook/buck/cli:cli',
     '//src/com/facebook/buck/dalvik:dalvik',
     '//src/com/facebook/buck/event:event',
     '//src/com/facebook/buck/graph:graph',
diff --git a/test/com/facebook/buck/android/DxStepTest.java b/test/com/facebook/buck/android/DxStepTest.java
new file mode 100644
index 0000000..939a04c
--- /dev/null
+++ b/test/com/facebook/buck/android/DxStepTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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.android;
+
+import static org.junit.Assert.assertTrue;
+
+import com.facebook.buck.android.DxStep.Option;
+import com.facebook.buck.cli.VerbosityParser;
+import com.facebook.buck.step.ExecutionContext;
+import com.facebook.buck.step.Step;
+import com.facebook.buck.step.TestExecutionContext;
+import com.facebook.buck.testutil.MoreAsserts;
+import com.facebook.buck.testutil.TestConsole;
+import com.facebook.buck.util.AndroidPlatformTarget;
+import com.facebook.buck.util.Verbosity;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+import org.easymock.EasyMock;
+import org.easymock.EasyMockSupport;
+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.EnumSet;
+
+public class DxStepTest extends EasyMockSupport {
+
+  private static Path SAMPLE_OUTPUT_PATH = Paths.get("buck-out/gen/classes.dex");
+
+  private static ImmutableSet<Path> SAMPLE_FILES_TO_DEX = ImmutableSet.of(
+      Paths.get("buck-out/gen/foo.dex.jar"),
+      Paths.get("buck-out/gen/bar.dex.jar"));
+
+  private Optional<AndroidPlatformTarget> androidPlatformTargetOptional;
+
+  @Before
+  public void setUp() {
+    AndroidPlatformTarget androidPlatformTarget = createMock(AndroidPlatformTarget.class);
+    EasyMock.expect(androidPlatformTarget.getDxExecutable()).andReturn(new File("/usr/bin/dx"));
+    androidPlatformTargetOptional = Optional.of(androidPlatformTarget);
+    replayAll();
+  }
+
+  @Test
+  public void testDxCommandNoOptimizeNoJumbo() {
+    // Context with --verbose 2.
+    ExecutionContext context = createExecutionContext(2);
+    Function<Path, Path> pathAbsolutifier = context.getProjectFilesystem().getAbsolutifier();
+
+    DxStep dx = new DxStep(SAMPLE_OUTPUT_PATH.toString(),
+        SAMPLE_FILES_TO_DEX,
+        EnumSet.of(Option.NO_OPTIMIZE));
+
+    String expected = String.format("/usr/bin/dx --dex --no-optimize --output %s %s",
+        SAMPLE_OUTPUT_PATH,
+        Joiner.on(' ').join(Iterables.transform(SAMPLE_FILES_TO_DEX, pathAbsolutifier)));
+    MoreAsserts.assertShellCommands(
+        "--no-optimize should be present, but --force-jumbo should not.",
+        ImmutableList.of(expected),
+        ImmutableList.<Step>of(dx),
+        context);
+    verifyAll();
+  }
+
+  @Test
+  public void testDxCommandOptimizeNoJumbo() {
+    // Context with --verbose 2.
+    ExecutionContext context = createExecutionContext(2);
+    Function<Path, Path> pathAbsolutifier = context.getProjectFilesystem().getAbsolutifier();
+
+    DxStep dx = new DxStep(SAMPLE_OUTPUT_PATH.toString(), SAMPLE_FILES_TO_DEX);
+
+    String expected = String.format("/usr/bin/dx --dex --output %s %s",
+        SAMPLE_OUTPUT_PATH,
+        Joiner.on(' ').join(Iterables.transform(SAMPLE_FILES_TO_DEX, pathAbsolutifier)));
+    MoreAsserts.assertShellCommands(
+        "Neither --no-optimize nor --force-jumbo should be present.",
+        ImmutableList.of(expected),
+        ImmutableList.<Step>of(dx),
+        context);
+    verifyAll();
+  }
+
+  @Test
+  public void testDxCommandNoOptimizeForceJumbo() {
+    // Context with --verbose 2.
+    ExecutionContext context = createExecutionContext(2);
+    Function<Path, Path> pathAbsolutifier = context.getProjectFilesystem().getAbsolutifier();
+
+    DxStep dx = new DxStep(SAMPLE_OUTPUT_PATH.toString(),
+        SAMPLE_FILES_TO_DEX,
+        EnumSet.of(DxStep.Option.NO_OPTIMIZE, DxStep.Option.FORCE_JUMBO));
+
+    String expected = String.format("/usr/bin/dx --dex --no-optimize --force-jumbo --output %s %s",
+        SAMPLE_OUTPUT_PATH,
+        Joiner.on(' ').join(Iterables.transform(SAMPLE_FILES_TO_DEX, pathAbsolutifier)));
+    MoreAsserts.assertShellCommands(
+        "Both --no-optimize and --force-jumbo should be present.",
+        ImmutableList.of(expected),
+        ImmutableList.<Step>of(dx),
+        context);
+    verifyAll();
+  }
+
+  @Test
+  public void testVerbose3AddsStatisticsFlag() {
+    // Context with --verbose 3.
+    ExecutionContext context = createExecutionContext(3);
+    Function<Path, Path> pathAbsolutifier = context.getProjectFilesystem().getAbsolutifier();
+
+    DxStep dx = new DxStep(SAMPLE_OUTPUT_PATH.toString(), SAMPLE_FILES_TO_DEX);
+
+    String expected = String.format("/usr/bin/dx --dex --statistics --output %s %s",
+        SAMPLE_OUTPUT_PATH,
+        Joiner.on(' ').join(Iterables.transform(SAMPLE_FILES_TO_DEX, pathAbsolutifier)));
+    MoreAsserts.assertShellCommands(
+        "Ensure that the --statistics flag is present.",
+        ImmutableList.of(expected),
+        ImmutableList.<Step>of(dx),
+        context);
+
+    assertTrue("Should print stdout to show statistics.",
+        dx.shouldPrintStdout(context.getVerbosity()));
+    assertTrue("Should print stderr to show statistics.",
+        dx.shouldPrintStderr(context.getVerbosity()));
+    verifyAll();
+  }
+
+  @Test
+  public void testVerbose10AddsVerboseFlagToDx() {
+    // Context with --verbose 10.
+    ExecutionContext context = createExecutionContext(10);
+    Function<Path, Path> pathAbsolutifier = context.getProjectFilesystem().getAbsolutifier();
+
+    DxStep dx = new DxStep(SAMPLE_OUTPUT_PATH.toString(), SAMPLE_FILES_TO_DEX);
+
+    String expected = String.format("/usr/bin/dx --dex --statistics --verbose --output %s %s",
+        SAMPLE_OUTPUT_PATH,
+        Joiner.on(' ').join(Iterables.transform(SAMPLE_FILES_TO_DEX, pathAbsolutifier)));
+    MoreAsserts.assertShellCommands(
+        "Ensure that the --statistics flag is present.",
+        ImmutableList.of(expected),
+        ImmutableList.<Step>of(dx),
+        context);
+
+    assertTrue("Should print stdout since `dx --verbose` is enabled.",
+        dx.shouldPrintStdout(context.getVerbosity()));
+    assertTrue("Should print stdout since `dx --verbose` is enabled.",
+        dx.shouldPrintStderr(context.getVerbosity()));
+    verifyAll();
+}
+
+  private ExecutionContext createExecutionContext(int verbosityLevel) {
+    TestConsole console = new TestConsole();
+    Verbosity verbosity = VerbosityParser.getVerbosityForLevel(verbosityLevel);
+    console.setVerbosity(verbosity);
+    return TestExecutionContext.newBuilder()
+        .setConsole(console)
+        .setAndroidPlatformTarget(androidPlatformTargetOptional)
+        .build();
+  }
+}